diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 86307017..80ef3fb9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,8 +22,8 @@ jobs: with: node-version: '20' - - name: Install dependencies - run: npm install + - name: Setup Build Environment + run: npm run setup - name: Build Electron app env: diff --git a/.github/workflows/prod-test.yml b/.github/workflows/prod-test.yml index 28093deb..a94ec631 100644 --- a/.github/workflows/prod-test.yml +++ b/.github/workflows/prod-test.yml @@ -24,4 +24,4 @@ jobs: run: npm run setup - name: dry run production build - run: npm run prod-dryrun \ No newline at end of file + run: npm run prod-dryrun diff --git a/.gitignore b/.gitignore index 6c821a6f..c66e12fd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,15 @@ .DS_Store +.docusaurus .env app.json +build data dist +jszip-esm.js logs mod node_modules package-lock.json -src/ext/three.js +three.js tmp -.docusaurus -build +pack diff --git a/app-el.js b/app-el.js index 7419adb6..513f9afe 100644 --- a/app-el.js +++ b/app-el.js @@ -2,6 +2,7 @@ const { app, shell, session, BrowserWindow } = require('electron'); const path = require('path'); const server = require('@gridspace/app-server'); +const pkgd = app.isPackaged; const basDir = __dirname; const usrDir = app.getPath("userData"); const appDir = path.join(usrDir, 'gapp'); @@ -11,6 +12,8 @@ const datDir = path.join(appDir, 'data'); const debug = process.argv.slice(2).map(v => v.replaceAll('-', '')).contains('debugg'); const devel = process.argv.slice(2).map(v => v.replaceAll('-', '')).contains('devel'); +process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = true; + // console.log({ appDir, usrDir, logDir, datDir, basDir }); // console.log({ argv: process.argv, debug, devel }); // console.log({ @@ -26,9 +29,9 @@ server({ data: datDir, conf: cnfDir, logs: logDir, - cache: path.join(basDir, "data", "cache"), single: true, electron: true, + pkgd, debug }); diff --git a/app.js b/app.js index b42b158f..4c27cc73 100644 --- a/app.js +++ b/app.js @@ -12,35 +12,31 @@ const uglify = require('uglify-js'); const moment = require('moment'); const agent = require('express-useragent'); const license = require_fresh('./src/moto/license.js'); -const version = license.VERSION || "rogue"; +const version = license.version || "rogue"; const netdb = require('@gridspace/net-level-client'); const PATH = require('path'); -const fileCache = {}; -const code_src = {}; +const append = { mesh:'', kiri:'' }; const code = {}; const mods = {}; const load = []; -const synth = {}; const api = {}; -const wrap = {}; let forceUseCache = false; let serviceWorker = true; let crossOrigin = false; let setupFn; -let cacheDir; let startTime; let oversion; let dversion; let lastmod; let logger; let debug; -let over; let http; let util; let dir; let log; +let pre; const EventEmitter = require('events'); class AppEmitter extends EventEmitter {} @@ -51,38 +47,24 @@ netdb.create = async function(map = {}) { Object.assign(map, JSON.parse(fs.readFileSync(map.conf))); } const client = new netdb(); - if (map.host && map.port) await client.open(map.host, map.port); - if (map.user && map.pass) await client.auth(map.user, map.pass); - if (map.base) await client.use(map.base); + try { + if (map.host && map.port) await client.open(map.host, map.port); + if (map.user && map.pass) await client.auth(map.user, map.pass); + if (map.base) await client.use(map.base); + } catch (e) { + logger.log({ netdb_setup_error: e }); + } logger.log({ netdb: map.host, user: map.user, base: map.base }); 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; startTime = time(); lastmod = mod.util.lastmod; logger = mod.log; + pre = ENV.pre || mod.meta.pre; debug = ENV.debug || mod.meta.debug; oversion = ENV.over || mod.meta.over; crossOrigin = ENV.xorigin || mod.meta.xorigin || false; @@ -94,185 +76,26 @@ function init(mod) { if (ENV.single) console.log({ cwd: process.cwd(), env: ENV }); dversion = debug ? `_${version}` : version; - cacheDir = ENV.cache || mod.util.datadir("cache"); - if (ENV.single) logger.log({ cacheDir }); forceUseCache = ENV.cache ? true : false; - const approot = PATH.join("main","gapp"); - const refcache = {}; - 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(); + if (!ENV.electron) { + generateDevices(); } - // 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]}); } mod.on.test((req) => { - let cookie = cookieValue(req.headers.cookie, "version") || undefined; + if (req.app?.path?.startsWith(pre)) { + req.url = req.url.substring(pre.length); + req.app.path = req.app.path.substring(pre.length); + req.app.ispre = true; + return true; + } + let cookie = cookieValue(req.headers.cookie, "version") || ''; let vmatch = mod.meta.version || "*"; if (!Array.isArray(vmatch)) { vmatch = [ vmatch ]; @@ -283,22 +106,30 @@ function init(mod) { return vmatch.indexOf(cookie) >= 0; }); + mod.on.testv((version) => { + let vmatch = mod.meta.version || "*"; + if (!Array.isArray(vmatch)) { + vmatch = [ vmatch ]; + } + if (vmatch.indexOf("*") >= 0) { + return true; + } + return vmatch.indexOf(version) >= 0; + }); + mod.add(handleSetup); mod.add(handleOptions); - mod.add(handleWasm); + mod.add(serveWasm); + mod.add(serveCode); mod.add(fullpath({ - "/kiri" : redir("/kiri/", 301), - "/mesh" : redir("/mesh/", 301), - "/meta" : redir("/meta/", 301), - "/kiri/index.html" : redir("/kiri/", 301), - "/mesh/index.html" : redir("/mesh/", 301), - "/meta/index.html" : redir("/meta/", 301) + "/kiri" : redir((pre??"") + "/kiri/", 301), + "/mesh" : redir((pre??"") + "/mesh/", 301), + "/meta" : redir((pre??"") + "/meta/", 301), + "/kiri/index.html" : redir((pre??"") + "/kiri/", 301), + "/mesh/index.html" : redir((pre??"") + "/mesh/", 301), + "/meta/index.html" : redir((pre??"") + "/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 +140,16 @@ 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"); + // module loader function load_modules(root, force) { - // load modules lastmod(`${dir}/${root}`) && fs.readdirSync(`${dir}/${root}`).forEach(mdir => { const modpath = `${root}/${mdir}`; if (dir.charAt(0) === '.' && !ENV.single) return; @@ -356,7 +174,7 @@ function init(mod) { // load optional local modules load_modules('mods'); - // run loads injected by modules + // run load functions injected by modules while (load.length) { try { load.shift()(); @@ -365,8 +183,12 @@ function init(mod) { } } - // runs after module loads / injects - prepareScripts(); + // minify appends in production mode + if (!debug) { + for (let key of Object.keys(append)) { + append[key] = minify(append[key]); + } + } }; // either add module assets to path or require(init.js) @@ -377,6 +199,9 @@ function loadModule(mod, dir) { if (lastmod(`${mod.dir}/${dir}/.ignore`)) { return; } + // either load the module, or if it's not a module + // directory, serve the module as static content + // bound to the root lastmod(`${mod.dir}/${dir}/init.js`) ? initModule(mod, `./${dir}/init.js`, dir) : mod.static("/", `${mod.dir}/${dir}`); @@ -384,21 +209,19 @@ function loadModule(mod, dir) { // load module and call returned function with helper object function initModule(mod, file, dir) { - logger.log({module: file}); + logger.log({ module: file, dir }); require_fresh(file)({ // express functions added here show up at "/api/" url root api: api, adm: { - reload: prepareScripts, - setver: (ver) => { oversion = ver }, - crossOrigin: (bool) => { crossOrigin = bool } + setver(ver) { oversion = ver }, + crossOrigin(bool) { crossOrigin = bool } }, events, const: { args: {}, meta: mod.meta, - debug: debug, - script: script, + debug, moddir: dir, rootdir: mod.dir, version: oversion || version @@ -411,54 +234,43 @@ function initModule(mod, file, dir) { }, mod: mods, util: { + guid, + time, log: logger.log, - time: time, - guid: guid, + logger: log.new, mkdirs: util.mkdir, - isfile: util.isfile, confdir: util.confdir, datadir: util.datadir, lastmod: lastmod, - obj2string: obj2string, - string2obj: string2obj, + isfile: util.isfile, getCookieValue: cookieValue, - logger: log.new - }, - inject: (code, file, opt = {}) => { - let codelist = script[code]; - if (!codelist) { - return logger.log(`inject missing target "${code}"`); - } - const path = `${dir}/${file}`; - codelist.push(path); - if (opt.cachever) { - cachever[path] = opt.cachever; - } + obj2string(o) { return JSON.stringify(o) }, + string2obj(s) { return JSON.parse(s) }, }, path: { - any: arg => { mod.add(arg) }, - pre: arg => { mod.add(prepath(arg)) }, - map: arg => { mod.add(fixedmap(arg)) }, - full: arg => { mod.add(fullpath(arg)) }, - static: (root, pre) => { - mod.static(pre || "/", root); - }, - code: (endpoint, path) => { - let fpath = PATH.join(mod.dir, path); - if (debug) { - code[endpoint] = fs.readFileSync(fpath); + any(arg) { mod.add(arg) }, + code() { + const [ path, file ] = [ ...arguments ]; + if (lastmod(file)) { + code[path] = fs.readFileSync(file); + // console.log({ CODE: path, file }); + } else if (lastmod(mod.dir + '/' + file)) { + const alt = mod.dir + '/' + file; + code[path] = fs.readFileSync(alt); + // console.log({ CODE: path, alt }); } else { - code[endpoint] = minify(fpath); + console.log({ MISSING_CODE: path, file }); } - code_src[endpoint] = { - endpoint, - path: path, - mod: lastmod(fpath) - }; }, + full(arg) { mod.add(fullpath(arg)) }, + map(arg) { mod.add(fixedmap(arg)) }, + pre(arg) { mod.add(prepath(arg)) }, redir: redir, remap: remap, - setup: fn => { setupFn = fn } + setup(fn) { setupFn = fn }, + static(root, pre) { + mod.static(pre || "/", root); + }, }, handler: { addCORS: addCorsHeaders, @@ -470,66 +282,29 @@ function initModule(mod, file, dir) { ws: { register: mod.wss }, - onload: (fn) => { + inject(code, file, opt = {}) { + const path = mod.dir + '/' + dir + '/' + file; + try { + const body = fs.readFileSync(path); + // console.log({ inject: code, file, opt }); + if (opt.first) { + append[code] = body.toString() + '\n' + append[code]; + } else { + append[code] += body.toString() + '\n'; + } + } catch (e) { + console.log({ missing_file: path, dir, mod }); + } + }, + onload(fn) { load.push(fn); }, - onexit: (fn) => { + onexit(fn) { mod.on.exit(fn); } }); } -const script = { - kiri : [ - "@devices", - "kiri/ui", - "&main/kiri", - "&kiri/lang-en" - ], - kiri_work : [ - "kiri-run/worker", - "&main/kiri", - ], - kiri_pool : [ - "&kiri-run/minion", - "&main/kiri", - ], - engine : [ - "&kiri-run/engine", - "&main/kiri", - ], - frame : [ - "kiri-run/frame" - ], - meta : [ - "main/meta", - ], - mesh : [ - "&main/mesh" - ], - mesh_work : [ - "&mesh/work" - ], - mesh_pool : [ - "&mesh/pool" - ], - cache : [ - "moto/license", - "main/service", - ], - service : [ - "moto/license", - "moto/service" - ] -}; - -// prevent caching of specified modules -const cachever = {}; - -function promise(resolve, reject) { - return new Promise(resolve, reject); -} - function rval() { return (Math.round(Math.random()*0xffffffff)).toString(36); } @@ -542,14 +317,6 @@ function time() { return Date.now(); } -function obj2string(o) { - return JSON.stringify(o); -} - -function string2obj(s) { - return JSON.parse(s); -} - function handleSetup(req, res, next) { if (setupFn) { setupFn(req, res, next); @@ -558,14 +325,37 @@ function handleSetup(req, res, next) { } } +const productionMap = { + '/lib/mesh/work.js' : '/lib/pack/mesh-work.js', + '/lib/main/mesh.js' : '/lib/pack/mesh-main.js', + '/lib/main/kiri.js' : '/lib/pack/kiri-main.js', + '/lib/kiri/run/engine.js' : '/lib/pack/kiri-eng.js', + '/lib/kiri/run/minion.js' : '/lib/pack/kiri-pool.js', + '/lib/kiri/run/worker.js' : '/lib/pack/kiri-work.js', +}; + +const redirList = [ + "/kiri/", + "/mesh/" +]; + function handleVersion(req, res, next) { let vstr = oversion || dversion || version; - if (["/kiri/","/mesh/","/meta/"].indexOf(req.app.path) >= 0 && req.url.indexOf(vstr) < 0) { + if (!req.app.ispre && redirList.indexOf(req.app.path) === 0 && req.url.indexOf(vstr) < 0) { if (req.url.indexOf("?") > 0) { return http.redirect(res, `${req.url},ver:${vstr}`); } else { return http.redirect(res, `${req.url}?ver:${vstr}`); } + } else if (!debug) { + // in production serve packed bundles + let { path } = req.app; + let mapped = productionMap[path]; + if (mapped) { + req.url = mapped; + addCorsHeaders(req, res); + } + next(); } else { next(); } @@ -586,7 +376,20 @@ function handleOptions(req, res, next) { } } -function handleWasm(req, res, next) { +function serveCode(req, res, next) { + let { path } = req.app; + if (path.startsWith("/code/")) { + path = path.split('/')[2].split('.')[0]; + if (code[path]) { + res.setHeader('Content-Type', 'application/javascript'); + res.setHeader('Cache-Control', 'public, max-age=600'); + return res.end(code[path]); + } + } + next(); +} + +function serveWasm(req, res, next) { let file = req.app.path.split('/').pop(); let ext = (file || '').split('.')[1]; let path = PATH.join(dir,"src","wasm",file); @@ -609,61 +412,9 @@ function handleWasm(req, res, next) { } } -function handleCode(req, res, next) { - let key = req.app.path.split('/')[2].split('.')[0], - ck = code_src[key], - js = code[key]; - - if (!js) { - return http.reply404(req, res); - } - if (ck) { - let mpath = `${dir}/${ck.path}`; - let mod = lastmod(mpath); - if (mod > ck.mod) { - if (debug) { - js = code[ck.endpoint] = fs.readFileSync(mpath); - } else { - js = code[ck.endpoint] = minify(mpath); - } - ck.mod = mod; - } - } - - addCorsHeaders(req, res); - serveCode(req, res, { - code: js, - mtime: startTime - }); -} - -function serveCode(req, res, code) { - if (code.deny) { - return http.reply404(req, res); - } - - let imd = ifModifiedDate(req); - if (imd && code.mtime <= imd && !code.nocache) { - res.writeHead(304, "Not Modified"); - res.end(); - return; - } - - let cacheControl = code.nocache ? - 'private, max-age=0' : - 'public, max-age=600'; - - res.writeHead(200, { - 'Content-Type': 'application/javascript', - 'Cache-Control': cacheControl, - 'Last-Modified': new Date(code.mtime).toGMTString(), - }); - res.end(code.code); -} - // pack/concat device script strings to inject into /code/ scripts function generateDevices() { - let root = PATH.join(dir,"src","kiri-dev"); + let root = PATH.join(dir,"src","kiri","dev"); let devs = {}; fs.readdirSync(root).forEach(type => { let map = devs[type] = devs[type] || {}; @@ -674,130 +425,12 @@ function generateDevices() { map[deviceName] = JSON.parse(fs.readFileSync(PATH.join(root,type,device))); }); }); - synth.devices = `self.devices = ${JSON.stringify(devs)};`; + let dstr = JSON.stringify(devs); + util.mkdir(PATH.join(dir,"src","pack")); + fs.writeFileSync(PATH.join(dir,"src","pack","kiri-devs.js"), `export const devices = ${dstr};`); } -// pack/concat code modules served under "/code/" -function prepareScripts() { - generateDevices(); - for (let key of Object.keys(script)) { - code[key] = concatCode(script[key]); - } -} - -function concatCode(array) { - let code = []; - let direct = array.filter(f => f.charAt(0) !== '@'); - let inject = array.filter(f => f.charAt(0) === '@').map(f => f.substring(1)); - - synth.inject = "/* injection point */"; - - // in debug mode, the script should load dependent - // scripts instead of serving a complete bundle - if (debug) { - const code = [ - oversion ? `self.debug_version='${oversion}';self.enable_service=${serviceWorker};` : '', - 'self.debug=true;', - '(function() { let load = [ ' - ]; - direct.forEach(file => { - const vers = cachever[file] || oversion || dversion || version; - code.push(`"/${file.replace(/\\/g,'/')}?${vers}",`); - }); - code.push([ - ']; function load_next() {', - 'let file = load.shift();', - 'if (!file) return;', - 'if (!self.document) { importScripts(file); return load_next() }', - 'let s = document.createElement("script");', - 's.type = "text/javascript";', - 's.src = file;', - 's.onload = load_next;', - 'document.head.appendChild(s);', - '} load_next(); })();' - ].join('\n')); - inject.forEach(key => { - code.push(synth[key]); - }); - return code.join('\n'); - } - - direct.forEach(file => { - let cached = getCachedFile(file, path => { - return minify(PATH.join(dir,file)); - }); - if (oversion) { - cached = `self.debug_version='${oversion}';self.enable_service=${serviceWorker};` + cached; - } - code.push(cached); - }); - - inject.forEach(key => { - code.push(synth[key]); - }); - - return code.join(''); -} - -function getCachedFile(file, fn) { - let filePath = PATH.join(dir,file); - let cachePath = cacheDir + PATH.sep + file - .replace(/\//g,'_') - .replace(/\\/g,'_') - .replace(/:/g,'_'), - cached = fileCache[filePath], - now = time(); - - if (cached) { - if (now - cached.lastcheck > 60000) { - let smod = lastmod(filePath), - cmod = cached.mtime; - - if (!smod) { - throw "missing source file"; - } - if (smod > cmod) { - cached = null; - } else { - cached.lastcheck = now; - } - } - } - - if (!cached) { - let smod = lastmod(filePath), - cmod = lastmod(cachePath), - cacheData; - - if (cmod >= smod || (forceUseCache && cmod)) { - cacheData = fs.readFileSync(cachePath); - } else { - logger.log({update_cache:filePath}); - cacheData = fn(filePath); - // console.log(`NEW_CACHE_FILE: ${cachePath}`); - fs.writeFileSync(cachePath, cacheData); - } - - if (wrap[file]) { - // console.log('WRAP', filePath, cacheData.length); - cacheData = wrap_module(cacheData, wrap[file]); - } - - cached = { - data: cacheData, - mtime: cmod || now, - lastcheck: now - }; - - fileCache[filePath] = cached; - } - - // console.log('[*]', filePath, cached.data.length); - return cached.data; -} - -function minify(path) { - let code = fs.readFileSync(path); +function minify(code) { let mini = uglify.minify(code.toString(), { compress: { merge_vars: false, @@ -834,10 +467,10 @@ function addCorsHeaders(req, res) { if (req.headers['access-control-request-private-network'] === 'true') { res.setHeader('Access-Control-Allow-Private-Network', 'true'); } - if (!crossOrigin) { + // if (!crossOrigin) { res.setHeader("Cross-Origin-Opener-Policy", 'same-origin'); res.setHeader("Cross-Origin-Embedder-Policy", 'require-corp'); - } + // } res.setHeader("Allow", "GET,POST,OPTIONS"); } @@ -890,7 +523,9 @@ function fixedmap(prefix, map) { // HTTP 307 redirect function redir(path, type) { - return (req, res, next) => http.redirect(res, path, type); + return (req, res, next) => { + http.redirect(res, path, type); + } } // mangle request path @@ -913,26 +548,44 @@ function cookieValue(cookie,key) { } function rewriteHtmlVersion(req, res, next) { - if (["/kiri/","/mesh/","/meta/","/kiri/engine.html","/kiri/frame.html"].indexOf(req.app.path) >= 0) { + if ([ + "/kiri/", + "/mesh/", + "/lib/mesh/work.js", + "/lib/kiri/run/worker.js", + "/lib/kiri/run/minion.js", + "/lib/kiri/run/engine.js", + "/lib/kiri/run/frame.js" + ].indexOf(req.app.path) >= 0) { addCorsHeaders(req, res); - let real_write = res.write; - let real_end = res.end; - let mlen = '{{version}}'.length; - let vstr = oversion || dversion || version; - if (vstr.length < mlen) { - vstr = vstr.padStart(mlen,0); - } else if (vstr.length > mlen) { - vstr = vstr.substring(0,mlen); - } - res.write = function() { - arguments[0] = arguments[0].toString().replace(/{{version}}/g,vstr); - real_write.apply(res, arguments); + } + if ([ + "/lib/main/kiri.js", + "/lib/main/mesh.js" + ].indexOf(req.app.path) >= 0) { + addCorsHeaders(req, res); + // const data = append[req.app.path.split('/')[3].split('.')[0]]; + const data = append[req.app.path.split('.')[0].split('/').pop()]; + + if (!data) return next(); + if (debug) logger.log({ append: req.app.path, data: data.length }); + + const real_write = res.write; + const real_end = res.end; + let body = ''; + + res.write = function (chunk, encoding) { + body += chunk.toString(encoding); }; - res.end = function() { - if (arguments[0]) { - arguments[0] = arguments[0].toString().replace(/{{version}}/g,vstr); + + res.end = function (chunk, encoding) { + if (chunk) { + body += chunk.toString(encoding); } - real_end.apply(res, arguments); + body += data; + res.setHeader('Content-Length', Buffer.byteLength(body)); + real_write.call(res, body); + real_end.call(res); }; } diff --git a/bin/electron-pre.js b/bin/electron-pre.js index 7b8ecdaf..d25e6f8c 100644 --- a/bin/electron-pre.js +++ b/bin/electron-pre.js @@ -30,8 +30,8 @@ fs.copySync("mods", modsTmp, { dereference: true, filter:(src,dst) => { } }); // create minified asset cache -server({ - electron: true, - dryrun: true, - single: true -}); +// server({ +// electron: true, +// dryrun: true, +// single: true +// }); diff --git a/bin/esbuild.config.mjs b/bin/esbuild.config.mjs new file mode 100644 index 00000000..004c85bc --- /dev/null +++ b/bin/esbuild.config.mjs @@ -0,0 +1,106 @@ +import { build, transform } from 'esbuild'; +import fs from 'fs/promises'; +import glob from 'fast-glob'; + +// Get build mode from command line argument +const mode = process.argv[2] || 'dev'; +const isProd = mode === 'prod'; + +console.log(`Building in ${mode} mode...`); + +const MESH_OUTFILE = 'src/pack/mesh-main.js'; +const MESH_EXTRAS = [ ]; + +const KIRI_OUTFILE = 'src/pack/kiri-main.js'; +const KIRI_EXTRAS = [ ]; + +async function appendExtraModules(extras, outfile, minify = false) { + const files = await glob(extras); + if (files.length === 0) return; + + const transformed = await Promise.all( + files.map(async (file) => { + const code = await fs.readFile(file, 'utf8'); + if (!minify) return code; + + const result = await transform(code, { + minify: true, + loader: 'js', + target: 'es2020', + }); + return result.code; + }) + ); + + await fs.appendFile(outfile, transformed.join('')); + console.log(`Appended ${files.length} module(s) to ${outfile}`); +} + +const logOverride = { + 'duplicate-class-member': 'silent', + 'duplicate-object-key': 'silent', + 'direct-eval': 'silent' +}; + +const rec = { + bundle: true, + define: { 'process.env.NODE_ENV': `"${mode}"` }, + external: ['module'], + format: 'esm', + logOverride, + minify: isProd, // false for dev, true for prod + platform: 'browser', + sourcemap: !isProd, // true for dev, false for prod + target: 'es2020', +}; + +async function buildApp() { + try { + // Bundle mesh main app + await build(Object.assign({}, rec, { + entryPoints: [ 'src/main/mesh.js' ], + outfile: MESH_OUTFILE, + })); + + appendExtraModules(MESH_EXTRAS, MESH_OUTFILE, isProd); + + // Bundle mesh worker + await build(Object.assign({}, rec, { + entryPoints: [ 'src/mesh/work.js' ], + outfile: 'src/pack/mesh-work.js', + })); + + // Bundle kiri main app + await build(Object.assign({}, rec, { + entryPoints: [ 'src/main/kiri.js' ], + outfile: KIRI_OUTFILE, + })); + + appendExtraModules(KIRI_EXTRAS, KIRI_OUTFILE, isProd); + + // Bundle kiri worker + await build(Object.assign({}, rec, { + entryPoints: [ 'src/kiri/run/worker.js' ], + outfile: 'src/pack/kiri-work.js', + })); + + // Bundle kiri minion + await build(Object.assign({}, rec, { + entryPoints: [ 'src/kiri/run/minion.js' ], + outfile: 'src/pack/kiri-pool.js', + })); + + // Bundle kiri engine + await build(Object.assign({}, rec, { + entryPoints: [ 'src/kiri/run/engine.js' ], + outfile: 'src/pack/kiri-eng.js', + })); + + console.log(`Build completed successfully in ${mode} mode!`); + } catch (error) { + console.error('Build failed:', error); + process.exit(1); + } +} + +buildApp(); diff --git a/bin/install-pre.js b/bin/install-pre.js index 02866192..ee29d5e1 100644 --- a/bin/install-pre.js +++ b/bin/install-pre.js @@ -5,14 +5,14 @@ const path = require('path'); async function main() { console.log('npm pre running'); - const links = fs.readFileSync("links.csv") + const links = fs.readFileSync("conf/links.csv") .toString() .trim() .split('\n') .map(line => line.trim()) .map(line => line.split(',').map(v => v.trim())); - if (os.platform() === 'win32') + if (os.platform() === 'win32') { // convert links to the contents of the files/directories they reference for (let [link, target] of links) { const absoluteTarget = path.resolve(path.dirname(link), target); @@ -24,6 +24,9 @@ async function main() { console.error(`Error creating symlink: ${link} -> ${absoluteTarget}`, err); } } + } else { + console.log('skipping symlink conversion'); + } } main().catch(err => console.error('Error', err)); diff --git a/bin/mk-links.sh b/bin/mk-links.sh new file mode 100755 index 00000000..8dac7fe9 --- /dev/null +++ b/bin/mk-links.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +find src/ web/ -type l | while read link; do echo "$link,$(readlink "$link")"; done > conf/links.csv diff --git a/bin/webpack-jszip-bundle.js b/bin/webpack-jszip-bundle.js new file mode 100644 index 00000000..9552f32f --- /dev/null +++ b/bin/webpack-jszip-bundle.js @@ -0,0 +1,7 @@ +import * as JSZip from "../node_modules/jszip/dist/jszip.js"; + +export default JSZip; + +export { + JSZip +}; diff --git a/bin/webpack-jszip-esm.js b/bin/webpack-jszip-esm.js new file mode 100644 index 00000000..eb3c3616 --- /dev/null +++ b/bin/webpack-jszip-esm.js @@ -0,0 +1,38 @@ +const path = require('path'); + +module.exports = { + mode: 'production', + entry: path.resolve(__dirname, './webpack-jszip-bundle.js'), + output: { + path: path.resolve(__dirname, '../src/ext'), + filename: 'jszip-esm.js', + library: { + type: 'module' + }, + module: true + }, + experiments: { + outputModule: true + }, + resolve: { + extensions: ['.mjs', '.js'], + }, + module: { + rules: [ + { + test: /\.m?js$/, + resolve: { + fullySpecified: false, + }, + }, + ], + }, + optimization: { + minimize: false + }, + performance: { + hints: false, + maxAssetSize: 2 * 1024 * 1024, + maxEntrypointSize: 2 * 1024 * 1024, + }, +}; diff --git a/bin/webpack-three-bundle.js b/bin/webpack-three-bundle.js index 727d3756..2ff68516 100644 --- a/bin/webpack-three-bundle.js +++ b/bin/webpack-three-bundle.js @@ -8,9 +8,16 @@ import { LineGeometry } from '../node_modules/three/examples/jsm/lines/LineGeome import { LineSegments2 } from '../node_modules/three/examples/jsm/lines/LineSegments2.js'; import { LineSegmentsGeometry } from '../node_modules/three/examples/jsm/lines/LineSegmentsGeometry.js'; +import * as MeshBVHLib from '../node_modules/three-mesh-bvh/build/index.module.js'; + export { - THREE, SVGLoader, BufferGeometryUtils, + THREE, + SVGLoader, + BufferGeometryUtils, LineMaterial, - Line2, LineGeometry, - LineSegments2, LineSegmentsGeometry -}; + Line2, + LineGeometry, + LineSegments2, + LineSegmentsGeometry, + MeshBVHLib +}; \ No newline at end of file diff --git a/bin/webpack-three.js b/bin/webpack-three-esm.js similarity index 50% rename from bin/webpack-three.js rename to bin/webpack-three-esm.js index f6b28e4e..2eff4b47 100644 --- a/bin/webpack-three.js +++ b/bin/webpack-three-esm.js @@ -1,15 +1,18 @@ -const TerserPlugin = require("terser-webpack-plugin"); const path = require('path'); module.exports = { mode: 'production', - entry: "./bin/webpack-three-bundle.js", + entry: path.resolve(__dirname, './webpack-three-bundle.js'), output: { - path: path.resolve('src/ext'), + path: path.resolve(__dirname, '../src/ext'), filename: 'three.js', - library: 'ThreeBundle', - libraryTarget: 'umd', - globalObject: 'this' + library: { + type: 'module' + }, + module: true + }, + experiments: { + outputModule: true }, resolve: { extensions: ['.mjs', '.js'], @@ -25,21 +28,11 @@ module.exports = { ], }, optimization: { - minimize: false, - minimizer: [ - new TerserPlugin({ - extractComments: false, - terserOptions: { - format: { - comments: false, - }, - }, - }), - ], + minimize: false }, performance: { hints: false, maxAssetSize: 2 * 1024 * 1024, maxEntrypointSize: 2 * 1024 * 1024, }, -}; +}; \ No newline at end of file diff --git a/docusaurus.config.js b/conf/docusaurus.config.js similarity index 100% rename from docusaurus.config.js rename to conf/docusaurus.config.js diff --git a/links.csv b/conf/links.csv similarity index 93% rename from links.csv rename to conf/links.csv index 166efd31..4ab5141c 100644 --- a/links.csv +++ b/conf/links.csv @@ -5,7 +5,6 @@ src/ext/earcut.js,../../node_modules/earcut/src/earcut.js src/ext/tween.js,../../node_modules/@tweenjs/tween.js/src/Tween.js src/ext/jszip.js,../../node_modules/jszip/dist/jszip.js src/wasm/manifold.wasm,../../node_modules/manifold-3d/manifold.wasm -src/kiri-dev/fdm/GridBot.Two.json,GridBot.One.json src/kiri/lang-en.js,../../web/kiri/lang/en.js web/fon2,../node_modules/bootstrap-icons/font/ web/kiri/lang/pl.js,pl-pl.js diff --git a/prettier.config.js b/conf/prettier.config.js similarity index 100% rename from prettier.config.js rename to conf/prettier.config.js diff --git a/contributing.md b/contributing.md index 96ab12b2..fd3ca445 100644 --- a/contributing.md +++ b/contributing.md @@ -25,7 +25,7 @@ This is a community driven project, and we welcome any contributions you'd like - Open the developer console - Run the following code: `kiri.api.conf.get().device` - Right click on the object and select `Copy object` -- Make a new file in the `src/kiri-dev/` directory, with the name of your machine, no spaces or special characters, and a `.json` extension. +- Make a new file in the `src/kiri/dev/` directory, with the name of your machine, no spaces or special characters, and a `.json` extension. - Paste the copied object into the new file, and save it. - Publish your changes to a git repo - Submit a [pull request](https://github.com/GridSpace/grid-apps/compare) \ No newline at end of file diff --git a/mods/bambu/bambu.js b/mods/bambu/bambu.js index 2eb1ac49..2a74ebe7 100644 --- a/mods/bambu/bambu.js +++ b/mods/bambu/bambu.js @@ -2,9 +2,8 @@ self.kiri.load(api => { console.log('BAMBU MODULE RUNNING'); - const { kiri, moto } = self; - const { ui } = kiri; - const h = moto.webui; + const { uc: ui } = api; + const { $, h } = api.web; const defams = ";; DEFINE BAMBU-AMS "; const readonly = true; const stock_colors = Object.values({ @@ -565,7 +564,7 @@ self.kiri.load(api => { function monitoring() { let mon = selected?.rec?.serial ?? ''; - ui.setVisible($('bbl_connect'), select.value !== '' && monitors.indexOf(mon) < 0); + ui.setVisible($('bbl_connect'), select?.value && monitors.indexOf(mon) < 0); return mon ? true : false; } @@ -661,7 +660,7 @@ self.kiri.load(api => { } }); - api.event.on("init-done", function() { + api.event.on("load-done", function() { if (init) { return; } diff --git a/mods/electron/electron.js b/mods/electron/electron.js index 4db3f3c1..bbd19230 100644 --- a/mods/electron/electron.js +++ b/mods/electron/electron.js @@ -1,13 +1,15 @@ -if (self.kiri) -self.kiri.load(api => { - console.log('ELECTRON MODULE RUNNING'); - api.electron = {}; - api.event.on('init-done', () => { - $('app-name-text').innerText = "More Info"; - $('top-sep').style.display = 'flex'; +if (self.kiri && !self.kiri.electron) { + self.kiri.electron = {}; + self.kiri.load(api => { + console.log('ELECTRON MODULE RUNNING'); + api.event.on('load-done', () => { + $('app-name-text').innerText = "More Info"; + $('top-sep').style.display = 'flex'; + }); + api.stats.set('kiri', api.version + 'e'); }); - api.stats.set('kiri', self.kiri.version + 'e'); -}); -if (self.mesh && self.mesh.api) { - self.mesh.api.electron = {}; +} + +if (self.mesh && self.mesh) { + self.mesh.electron = {}; } diff --git a/notes.md b/notes.md index acd628ab..a7036e77 100644 --- a/notes.md +++ b/notes.md @@ -2,8 +2,6 @@ ## `C` cosmetic, `F` functional, `P` performance, `B` bug fix -* `F` experiment with structuredClone(), SharedArrayBuffer, createImageBitmap() -* `F` test #private fields, array.at() for negative indices * `F` option to hide platform when viewing from below # Kiri:Moto @@ -14,7 +12,6 @@ * `P` refactor vertex replacement and widget update matrix tracking * `P` duplicate objects should share same slice data unless rotated or scaled -* `P` allow selection to be decimated on demand (context menu?) * `P` move all persisted / workspace settings/data to IndexedDB (LS limitations) * `P` faster ray intersect https://github.com/gkjohnson/three-mesh-bvh/ * `P` try material clipping planes for slice range selection @@ -26,7 +23,7 @@ * `F` complete and expose grouping feature * `F` add svgnest-like arrange algorithm * `F` date column and sorting in recent files list -* `F` warn if part hanging in negative Z space or off bed in general +* `F` highlight part outside workspace bounds (like can neg Z shader) # FDM @@ -34,6 +31,7 @@ * `B` multi-extruder rendering of raft fails to offset the rest of the print * `B` multi-extruder purge blocks fail to generate properly for rafts +* `F` better thin-wall (like arachne) * `F` add scarf seams * `F` add 'random' and 'seam' start point options * `F` convert ranges to z offsets while continuing to show layer # @@ -52,12 +50,6 @@ * `P` extruder + filament max flow rate cap in planner * `P` solid fill the tops of supports for down facing flats -# FDM - SLA - -# `P` common support area detection (fork new fdm code) -* `P` prioritize supports by length of unsupported span. mandatory when circularity > X -* or % area of inner to outer poly is high (making it a thin shell) - # FDM - BELT * `B` auto bed resizing leaves the origin in the wrong place at first @@ -69,7 +61,6 @@ # CAM -* `B` roughing leave stock should also increase Z slightly (sqrt(2) * leave) * `B` rapid moves should be max of terrain zmax and last cut layer height (roughing) * `B` feed rate for next tool set before tool change (push/pop feed rates?) * `B` tab cuts cause loss of direction, parenting, depth, and it's hard to fix @@ -84,18 +75,14 @@ * `F` add `match faces` option in `outline` operation * `F` add {progress} substitution and maybe {time-remaining} if can be calc'd * `F` import and follow 2D paths (conformed like pocket contours) -* `F` add user-defined origin (issue #28) -* `F` intelligently turn circle hole pocket clear into spiral down * `F` add `plunge max` to contouring that can override z feed limit * `F` add lead-in milling (requires adding clamp / no go areas) * `F` add linear clearing strategy * `F` add adaptive clearing strategy -* `F` add ease-down support to trace op * `F` add support for tapered ball mills * `F` change color of line selection in trace op when not a closed poly * `F` limit cut depth to flute length of selected tool (or warn) * `F` validate muti-part layout and spacing exceeds largest outside tool diameter -* `F` parameterize dropping close points in prep.js. ensure long segments remain straight * `F` trapezoidal tabs in Z * `F` ease-in and ease-out especially on tab cut-out start/stop * `F` maintain several part orientations + op chains in a single profile @@ -104,14 +91,8 @@ * `P` outer corner arc moves * `P` log Z interpolation for contour XYZ moves * `P` option to start with the smallest poly by area on layer change -* `P` refactor slicing engine to add minion threading -* `P` port arc code from FDM export to CAM export * `P` redo all path route / planning in prepare to account for terrain before camOut -# SLA - -* `P` blit layers w/ 3JS ortho camera to canvas - # Laser * `F` add PLT / HP-GL output format (https://en.wikipedia.org/wiki/HP-GL) @@ -124,8 +105,9 @@ # Mesh:Tool -* font to path with https://github.com/paulzi/svg-text-to-path * add undo/redo +* add TextGeometry +* font to path with https://github.com/paulzi/svg-text-to-path * click to repair normals based on a known good (selected) face * send to Kiri:Moto workspace (or update model vertices in place) * better z snap using just vertexes from face intersected @@ -134,6 +116,5 @@ * add flatten/crush op: for z bottoms (or surfaces?) * allow setting model/group origin for scale/rotate * fix mirror to work with groups (just models currently) -* bounding box toggle should be global, not selection * add analyze results dialog * remove/hide auto-repair function diff --git a/package.json b/package.json index b2659a6f..112e16e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "grid-apps", - "version": "4.2.0", + "version": "4.3.0", "description": "grid.space 3d slicing & modeling tools", "author": "Stewart Allen ", "license": "MIT", @@ -32,11 +32,12 @@ "slicer" ], "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" }, "dependencies": { "@fortawesome/fontawesome-free": "^6.1.1", - "@gridspace/app-server": "^0.0.17", + "@gridspace/app-server": "^0.0.18", + "@gridspace/basic-ftp": "^5.0.5", "@gridspace/net-level-client": "^0.2.3", "@tracespace/parser": "^5.0.0-next.0", "@tweenjs/tween.js": "^16.6.0", @@ -45,15 +46,18 @@ "buffer-crc32": "^0.2.13", "compression": "^1.7.4", "connect": "^3.7.0", - "earcut": "^2.2.3", + "earcut": "^3.0.1", "express-useragent": "^1.0.13", + "fast-glob": "^3.3.3", + "i": "^0.3.7", "jszip": "^3.7.1", - "manifold-3d": "^2.5.1", + "manifold-3d": "^3.1.1", "moment": "^2.29.4", + "npm": "^11.4.2", "prettier": "^3.5.3", "react-responsive-carousel": "^3.2.23", "serve-static": "^1.14.1", - "three": "^0.174.0", + "three": "^0.178.0", "three-mesh-bvh": "^0.7.6", "uglify-js": "3.14.5", "validator": ">=13.7.0", @@ -63,25 +67,21 @@ "@docusaurus/core": "^3.7.0", "@docusaurus/preset-classic": "^3.7.0", "@electron/notarize": "^3.0.0", + "copy-webpack-plugin": "^12.0.2", "docusaurus-lunr-search": "^3.6.0", "dotenv": "latest", "electron": "^35.0.1", "electron-builder": "^24.9.1", + "esbuild": "^0.25.5", "fs-extra": "^11.2.0", + "imports-loader": "^5.0.0", "node-fetch": "^3.3.2", "terser-webpack-plugin": "^5.3.10", "webpack": "^5.92.1", "webpack-cli": "^5.1.4" }, "scripts": { - "setup": "npm i && cd mods && npm i", - "dev": "gs-app-server --debug", - "prod": "gs-app-server", - "prod-dryrun": "gs-app-server --dryrun", - "start": "npm run prebuild && electron .", - "start-dev": "npm run prebuild && electron . --devel", - "start-dbg": "npm run prebuild && electron . --debugg", - "start-ddb": "npm run prebuild && electron . --devel --debugg", + "setup": "npm i && npm run clean && npm run dev -- --dryrun && npm run webpack-ext", "build": "npm run prebuild && electron-builder", "build-nopublish": "npm run prebuild && electron-builder --publish never", "build-debug": "npm run prebuild && DEBUG=electron-builder electron-builder", @@ -91,16 +91,28 @@ "build-win-arm": "npm run build -- --win --arm64", "build-mac": "npm run build -- --mac --arm64", "build-mac-intel": "npm run build -- --mac --x64", - "mklinks": "find src web -type l | xargs -I{} sh -c 'echo \"{},$(readlink {})\"' > links.csv", + "clean": "rm -rf build data dist src/pack tmp src.old web.old", + "docs-dev": "docusaurus start --config conf/docusaurus.config.js", + "docs-build": "docusaurus build --config conf/docusaurus.config.js", + "docs-serve": "docusaurus serve --config conf/docusaurus.config.js", + "docs-check": "prettier --config ./conf/prettier.config.js ./docs --check", + "dev": "npm run webpack-ext && gs-app-server --debug --single", + "prod": "npm run webpack-ext && npm run webpack-src prod && gs-app-server", + "prod-pack": "npm run webpack-ext && npm run webpack-src prod", + "prod-dryrun": "gs-app-server --dryrun", + "start": "npm run prebuild prod && electron .", + "start-dev": "npm run prebuild prod && electron . --devel", + "start-dbg": "npm run prebuild && electron . --debugg", + "start-ddb": "npm run prebuild && electron . --devel --debugg", + "mk-links": "find src web -type l | xargs -I{} sh -c 'echo \"{},$(readlink {})\"' > conf/links.csv", "mac-verify": "spctl --assess -vv --type install dist/*/*.app", - "clear-cache": "rm -rf data/cache/* dist/ tmp/*", - "prebuild": "node bin/electron-pre.js", + "prebuild": "npm run setup && npm run webpack-src prod && node bin/electron-pre.js", "postbuild": "node bin/electron-post.js", - "preinstall": "node bin/install-pre.js && npx webpack --config bin/webpack-three.js", - "docs-dev": "docusaurus start", - "docs-build": "docusaurus build", - "docs-serve": "docusaurus serve", - "docs-check": "prettier ./docs --check" + "preinstall": "node bin/install-pre.js", + "webpack-ext": "npm run webpack-three && npm run webpack-zip", + "webpack-src": "node bin/esbuild.config.mjs", + "webpack-three": "npx webpack --config bin/webpack-three-esm.js", + "webpack-zip": "npx webpack --config bin/webpack-jszip-esm.js" }, "main": "app-el.js", "build": { @@ -172,8 +184,12 @@ "nsis": { "oneClick": false, "allowToChangeInstallationDirectory": true, + "differentialPackage": false, "allowElevation": true }, + "dmg": { + "writeUpdateInfo": false + }, "mac": { "icon": "bin/GS.icns", "category": "public.app-category.utilities", @@ -182,8 +198,7 @@ "entitlements": "bin/electron-entitlements.plist", "entitlementsInherit": "bin/electron-entitlements.plist", "target": [ - "dmg", - "zip" + "dmg" ] }, "linux": { @@ -193,5 +208,6 @@ ] }, "afterSign": "bin/electron-notarize.js" - } + }, + "sideEffects": true } diff --git a/readme.md b/readme.md index b810a2c8..9c7d2497 100644 --- a/readme.md +++ b/readme.md @@ -92,6 +92,22 @@ npm run setup npm run dev ``` +## TrueNAS Via YAML + +`Apps > Discover Apps > Install via YAML` + +``` +services: + kirimoto: + build: + context: https://github.com/GridSpace/grid-apps.git#refs/tags/latest + dockerfile: ./src/dock/Dockerfile + ports: + - "8080:8080" +``` + +## For any default install... + Then open a browser to [localhost:8080/kiri](http://localhost:8080/kiri) # Windows Developers diff --git a/src/add/array.js b/src/add/array.js index 93c9b8d3..7c6324cf 100644 --- a/src/add/array.js +++ b/src/add/array.js @@ -1,18 +1,14 @@ /** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */ -"use strict"; - // extend Array, String, Number, Math -gapp.register("add.array", [], (root, exports) => { - -let AP = {}; +const AP = {}; if (!AP.flat) { AP.flat = function() { try { return [].concat.apply([], this); } catch (e) { - // console.log({flat_error: e}); + // console.log({ flat_error: e, array: this }); let out = []; for (let e of this.slice()) { out.push(...e); @@ -216,12 +212,11 @@ Array.handle = function(fn) { } }; -for (let i in AP) { - Object.defineProperty(Array.prototype, i, { - value: AP[i], - enumerable: false - }); -} +// Apply all methods to Array.prototype +Object.assign(Array.prototype, AP); + +// Export for ES modules +export { AP as arrayPrototype }; Float32Array.prototype.toShared = function() { const newvert = new Float32Array(new SharedArrayBuffer(this.buffer.byteLength)); @@ -265,5 +260,3 @@ Number.prototype.round = function(digits) { const pow = Math.pow(10, digits || 3); return Math.round(this.valueOf() * pow) / pow; }; - -}); 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/cli/kiri-cam-process.json b/src/cli/kiri-cam-process.json index 867aaafc..9c5a6604 100644 --- a/src/cli/kiri-cam-process.json +++ b/src/cli/kiri-cam-process.json @@ -54,6 +54,21 @@ "camDrillLift": 2, "camDrillingOn": false, "camRegisterSpeed": 1000, + "camHelicalTool": 1000, + "camHelicalSpindle": 1000, + "camHelicalDownSpeed": 250, + "camHelicalSpeed": 1000, + "camHelicalDown": 5, + "camHelicalBottomFinish":true, + "camHelicalThru": 0, + "camHelicalOffset": "auto", + "camHelicalForceStartAngle": false, + "camHelicalStartAngle": 0, + "camHelicalOffsetOverride": 0, + "camHelicalEntry": false, + "camHelicalEntryOffset":0, + "camHelicalReverse": false, + "camHelicalClockwise": true, "camFlipAxis": "X", "camFlipOther": "", "camTabsWidth": 20, diff --git a/src/cli/kiri-source.json b/src/cli/kiri-source.json index 2aca4448..0d346a9a 100644 --- a/src/cli/kiri-source.json +++ b/src/cli/kiri-source.json @@ -23,37 +23,37 @@ "geo/wasm", "data/local", "load/stl", - "kiri/conf", - "kiri/utils", - "kiri/slice", - "kiri/consts", - "kiri/client", - "kiri/widget", - "kiri/layers", - "kiri-mode/cam/driver", - "kiri-mode/cam/export", - "kiri-mode/cam/ops", - "kiri-mode/cam/prepare", - "kiri-mode/cam/slice", - "kiri-mode/cam/slicer", - "kiri-mode/cam/tool", - "kiri-mode/cam/topo", - "kiri-mode/fdm/driver", - "kiri-mode/fdm/export", - "kiri-mode/fdm/fill", - "kiri-mode/fdm/post", - "kiri-mode/fdm/prepare", - "kiri-mode/fdm/slice", - "kiri-mode/laser/driver", - "kiri-mode/sla/driver", - "kiri-mode/sla/x_cxdlp", - "kiri-mode/sla/x_photon", - "kiri-mode/sla/export", - "kiri-mode/sla/slice", - "kiri/codec", - "kiri/print", - "kiri/pack", - "kiri-run/worker", - "kiri-run/engine", + "kiri/core/conf", + "kiri/core/utils", + "kiri/core/slice", + "kiri/core/consts", + "kiri/core/client", + "kiri/core/widget", + "kiri/core/layers", + "kiri/mode/cam/driver", + "kiri/mode/cam/export", + "kiri/mode/cam/ops", + "kiri/mode/cam/prepare", + "kiri/mode/cam/slice", + "kiri/mode/cam/slicer", + "kiri/mode/cam/tool", + "kiri/mode/cam/topo", + "kiri/mode/fdm/driver", + "kiri/mode/fdm/export", + "kiri/mode/fdm/fill", + "kiri/mode/fdm/post", + "kiri/mode/fdm/prepare", + "kiri/mode/fdm/slice", + "kiri/mode/laser/driver", + "kiri/mode/sla/driver", + "kiri/mode/sla/x_cxdlp", + "kiri/mode/sla/x_photon", + "kiri/mode/sla/export", + "kiri/mode/sla/slice", + "kiri/core/codec", + "kiri/core/print", + "kiri/core/pack", + "kiri/run/worker", + "kiri/run/engine", "main/kiri" -] +] \ No newline at end of file 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/src/ext/clip.js b/src/ext/clip.js deleted file mode 100755 index 347f6605..00000000 --- a/src/ext/clip.js +++ /dev/null @@ -1,6986 +0,0 @@ -// rev 452 -/******************************************************************************** - * * - * Author : Angus Johnson * - * Version : 6.1.3a * - * Date : 22 January 2014 * - * Website : http://www.angusj.com * - * Copyright : Angus Johnson 2010-2014 * - * * - * License: * - * Use, modification & distribution is subject to Boost Software License Ver 1. * - * http://www.boost.org/LICENSE_1_0.txt * - * * - * Attributions: * - * The code in this library is an extension of Bala Vatti's clipping algorithm: * - * "A generic solution to polygon clipping" * - * Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63. * - * http://portal.acm.org/citation.cfm?id=129906 * - * * - * Computer graphics and geometric modeling: implementation and algorithms * - * By Max K. Agoston * - * Springer; 1 edition (January 4, 2005) * - * http://books.google.com/books?q=vatti+clipping+agoston * - * * - * See also: * - * "Polygon Offsetting by Computing Winding Numbers" * - * Paper no. DETC2005-85513 pp. 565-575 * - * ASME 2005 International Design Engineering Technical Conferences * - * and Computers and Information in Engineering Conference (IDETC/CIE2005) * - * September 24-28, 2005 , Long Beach, California, USA * - * http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf * - * * - *******************************************************************************/ -/******************************************************************************* - * * - * Author : Timo * - * Version : 6.1.3.2 * - * Date : 1 February 2014 * - * * - * This is a translation of the C# Clipper library to Javascript. * - * Int128 struct of C# is implemented using JSBN of Tom Wu. * - * Because Javascript lacks support for 64-bit integers, the space * - * is a little more restricted than in C# version. * - * * - * C# version has support for coordinate space: * - * +-4611686018427387903 ( sqrt(2^127 -1)/2 ) * - * while Javascript version has support for space: * - * +-4503599627370495 ( sqrt(2^106 -1)/2 ) * - * * - * Tom Wu's JSBN proved to be the fastest big integer library: * - * http://jsperf.com/big-integer-library-test * - * * - * This class can be made simpler when (if ever) 64-bit integer support comes. * - * * - *******************************************************************************/ -/******************************************************************************* - * * - * Basic JavaScript BN library - subset useful for RSA encryption. * - * http://www-cs-students.stanford.edu/~tjw/jsbn/ * - * Copyright (c) 2005 Tom Wu * - * All Rights Reserved. * - * See "LICENSE" for details: * - * http://www-cs-students.stanford.edu/~tjw/jsbn/LICENSE * - * * - *******************************************************************************/ -(function () -{ - "use strict"; - // disable modules when conflicts with others - var use_modules = false; - //use_int32: When enabled 32bit ints are used instead of 64bit ints. This - //improve performance but coordinate values are limited to the range +/- 46340 - var use_int32 = false; - //use_xyz: adds a Z member to IntPoint. Adds a minor cost to performance. - var use_xyz = false; - //UseLines: Enables line clipping. Adds a very minor cost to performance. - var use_lines = true; - //use_deprecated: Enables support for the obsolete OffsetPaths() function - //which has been replace with the ClipperOffset class. - var use_deprecated = false; - - var ClipperLib = {}; - var isNode = false; - if (use_modules && typeof module !== 'undefined' && module.exports) - { - module.exports = ClipperLib; - isNode = true; - } - else - { - self.ClipperLib = ClipperLib; - } - var navigator_appName; - if (!isNode) - { - var nav = navigator.userAgent.toString().toLowerCase(); - navigator_appName = navigator.appName; - } - else - { - var nav = "chrome"; // Node.js uses Chrome's V8 engine - navigator_appName = "Netscape"; // Firefox, Chrome and Safari returns "Netscape", so Node.js should also - } - // Browser test to speedup performance critical functions - var browser = {}; - if (nav.indexOf("chrome") != -1 && nav.indexOf("chromium") == -1) browser.chrome = 1; - else browser.chrome = 0; - if (nav.indexOf("chromium") != -1) browser.chromium = 1; - else browser.chromium = 0; - if (nav.indexOf("safari") != -1 && nav.indexOf("chrome") == -1 && nav.indexOf("chromium") == -1) browser.safari = 1; - else browser.safari = 0; - if (nav.indexOf("firefox") != -1) browser.firefox = 1; - else browser.firefox = 0; - if (nav.indexOf("firefox/17") != -1) browser.firefox17 = 1; - else browser.firefox17 = 0; - if (nav.indexOf("firefox/15") != -1) browser.firefox15 = 1; - else browser.firefox15 = 0; - if (nav.indexOf("firefox/3") != -1) browser.firefox3 = 1; - else browser.firefox3 = 0; - if (nav.indexOf("opera") != -1) browser.opera = 1; - else browser.opera = 0; - if (nav.indexOf("msie 10") != -1) browser.msie10 = 1; - else browser.msie10 = 0; - if (nav.indexOf("msie 9") != -1) browser.msie9 = 1; - else browser.msie9 = 0; - if (nav.indexOf("msie 8") != -1) browser.msie8 = 1; - else browser.msie8 = 0; - if (nav.indexOf("msie 7") != -1) browser.msie7 = 1; - else browser.msie7 = 0; - if (nav.indexOf("msie ") != -1) browser.msie = 1; - else browser.msie = 0; - ClipperLib.biginteger_used = null; - // Copyright (c) 2005 Tom Wu - // All Rights Reserved. - // See "LICENSE" for details. - // Basic JavaScript BN library - subset useful for RSA encryption. - // Bits per digit - var dbits; - // JavaScript engine analysis - var canary = 0xdeadbeefcafe; - var j_lm = ((canary & 0xffffff) == 0xefcafe); - // (public) Constructor - function BigInteger(a, b, c) - { - // This test variable can be removed, - // but at least for performance tests it is useful piece of knowledge - // This is the only ClipperLib related variable in BigInteger library - ClipperLib.biginteger_used = 1; - if (a != null) - if ("number" == typeof a && "undefined" == typeof (b)) this.fromInt(a); // faster conversion - else if ("number" == typeof a) this.fromNumber(a, b, c); - else if (b == null && "string" != typeof a) this.fromString(a, 256); - else this.fromString(a, b); - } - // return new, unset BigInteger - function nbi() - { - return new BigInteger(null); - } - // am: Compute w_j += (x*this_i), propagate carries, - // c is initial carry, returns final carry. - // c < 3*dvalue, x < 2*dvalue, this_i < dvalue - // We need to select the fastest one that works in this environment. - // am1: use a single mult and divide to get the high bits, - // max digit bits should be 26 because - // max internal value = 2*dvalue^2-2*dvalue (< 2^53) - function am1(i, x, w, j, c, n) - { - while (--n >= 0) - { - var v = x * this[i++] + w[j] + c; - c = Math.floor(v / 0x4000000); - w[j++] = v & 0x3ffffff; - } - return c; - } - // am2 avoids a big mult-and-extract completely. - // Max digit bits should be <= 30 because we do bitwise ops - // on values up to 2*hdvalue^2-hdvalue-1 (< 2^31) - function am2(i, x, w, j, c, n) - { - var xl = x & 0x7fff, - xh = x >> 15; - while (--n >= 0) - { - var l = this[i] & 0x7fff; - var h = this[i++] >> 15; - var m = xh * l + h * xl; - l = xl * l + ((m & 0x7fff) << 15) + w[j] + (c & 0x3fffffff); - c = (l >>> 30) + (m >>> 15) + xh * h + (c >>> 30); - w[j++] = l & 0x3fffffff; - } - return c; - } - // Alternately, set max digit bits to 28 since some - // browsers slow down when dealing with 32-bit numbers. - function am3(i, x, w, j, c, n) - { - var xl = x & 0x3fff, - xh = x >> 14; - while (--n >= 0) - { - var l = this[i] & 0x3fff; - var h = this[i++] >> 14; - var m = xh * l + h * xl; - l = xl * l + ((m & 0x3fff) << 14) + w[j] + c; - c = (l >> 28) + (m >> 14) + xh * h; - w[j++] = l & 0xfffffff; - } - return c; - } - if (j_lm && (navigator_appName == "Microsoft Internet Explorer")) - { - BigInteger.prototype.am = am2; - dbits = 30; - } - else if (j_lm && (navigator_appName != "Netscape")) - { - BigInteger.prototype.am = am1; - dbits = 26; - } - else - { // Mozilla/Netscape seems to prefer am3 - BigInteger.prototype.am = am3; - dbits = 28; - } - BigInteger.prototype.DB = dbits; - BigInteger.prototype.DM = ((1 << dbits) - 1); - BigInteger.prototype.DV = (1 << dbits); - var BI_FP = 52; - BigInteger.prototype.FV = Math.pow(2, BI_FP); - BigInteger.prototype.F1 = BI_FP - dbits; - BigInteger.prototype.F2 = 2 * dbits - BI_FP; - // Digit conversions - var BI_RM = "0123456789abcdefghijklmnopqrstuvwxyz"; - var BI_RC = new Array(); - var rr, vv; - rr = "0".charCodeAt(0); - for (vv = 0; vv <= 9; ++vv) BI_RC[rr++] = vv; - rr = "a".charCodeAt(0); - for (vv = 10; vv < 36; ++vv) BI_RC[rr++] = vv; - rr = "A".charCodeAt(0); - for (vv = 10; vv < 36; ++vv) BI_RC[rr++] = vv; - - function int2char(n) - { - return BI_RM.charAt(n); - } - - function intAt(s, i) - { - var c = BI_RC[s.charCodeAt(i)]; - return (c == null) ? -1 : c; - } - // (protected) copy this to r - function bnpCopyTo(r) - { - for (var i = this.t - 1; i >= 0; --i) r[i] = this[i]; - r.t = this.t; - r.s = this.s; - } - // (protected) set from integer value x, -DV <= x < DV - function bnpFromInt(x) - { - this.t = 1; - this.s = (x < 0) ? -1 : 0; - if (x > 0) this[0] = x; - else if (x < -1) this[0] = x + this.DV; - else this.t = 0; - } - // return bigint initialized to value - function nbv(i) - { - var r = nbi(); - r.fromInt(i); - return r; - } - // (protected) set from string and radix - function bnpFromString(s, b) - { - var k; - if (b == 16) k = 4; - else if (b == 8) k = 3; - else if (b == 256) k = 8; // byte array - else if (b == 2) k = 1; - else if (b == 32) k = 5; - else if (b == 4) k = 2; - else - { - this.fromRadix(s, b); - return; - } - this.t = 0; - this.s = 0; - var i = s.length, - mi = false, - sh = 0; - while (--i >= 0) - { - var x = (k == 8) ? s[i] & 0xff : intAt(s, i); - if (x < 0) - { - if (s.charAt(i) == "-") mi = true; - continue; - } - mi = false; - if (sh == 0) - this[this.t++] = x; - else if (sh + k > this.DB) - { - this[this.t - 1] |= (x & ((1 << (this.DB - sh)) - 1)) << sh; - this[this.t++] = (x >> (this.DB - sh)); - } - else - this[this.t - 1] |= x << sh; - sh += k; - if (sh >= this.DB) sh -= this.DB; - } - if (k == 8 && (s[0] & 0x80) != 0) - { - this.s = -1; - if (sh > 0) this[this.t - 1] |= ((1 << (this.DB - sh)) - 1) << sh; - } - this.clamp(); - if (mi) BigInteger.ZERO.subTo(this, this); - } - // (protected) clamp off excess high words - function bnpClamp() - { - var c = this.s & this.DM; - while (this.t > 0 && this[this.t - 1] == c)--this.t; - } - // (public) return string representation in given radix - function bnToString(b) - { - if (this.s < 0) return "-" + this.negate().toString(b); - var k; - if (b == 16) k = 4; - else if (b == 8) k = 3; - else if (b == 2) k = 1; - else if (b == 32) k = 5; - else if (b == 4) k = 2; - else return this.toRadix(b); - var km = (1 << k) - 1, - d, m = false, - r = "", - i = this.t; - var p = this.DB - (i * this.DB) % k; - if (i-- > 0) - { - if (p < this.DB && (d = this[i] >> p) > 0) - { - m = true; - r = int2char(d); - } - while (i >= 0) - { - if (p < k) - { - d = (this[i] & ((1 << p) - 1)) << (k - p); - d |= this[--i] >> (p += this.DB - k); - } - else - { - d = (this[i] >> (p -= k)) & km; - if (p <= 0) - { - p += this.DB; - --i; - } - } - if (d > 0) m = true; - if (m) r += int2char(d); - } - } - return m ? r : "0"; - } - // (public) -this - function bnNegate() - { - var r = nbi(); - BigInteger.ZERO.subTo(this, r); - return r; - } - // (public) |this| - function bnAbs() - { - return (this.s < 0) ? this.negate() : this; - } - // (public) return + if this > a, - if this < a, 0 if equal - function bnCompareTo(a) - { - var r = this.s - a.s; - if (r != 0) return r; - var i = this.t; - r = i - a.t; - if (r != 0) return (this.s < 0) ? -r : r; - while (--i >= 0) - if ((r = this[i] - a[i]) != 0) return r; - return 0; - } - // returns bit length of the integer x - function nbits(x) - { - var r = 1, - t; - if ((t = x >>> 16) != 0) - { - x = t; - r += 16; - } - if ((t = x >> 8) != 0) - { - x = t; - r += 8; - } - if ((t = x >> 4) != 0) - { - x = t; - r += 4; - } - if ((t = x >> 2) != 0) - { - x = t; - r += 2; - } - if ((t = x >> 1) != 0) - { - x = t; - r += 1; - } - return r; - } - // (public) return the number of bits in "this" - function bnBitLength() - { - if (this.t <= 0) return 0; - return this.DB * (this.t - 1) + nbits(this[this.t - 1] ^ (this.s & this.DM)); - } - // (protected) r = this << n*DB - function bnpDLShiftTo(n, r) - { - var i; - for (i = this.t - 1; i >= 0; --i) r[i + n] = this[i]; - for (i = n - 1; i >= 0; --i) r[i] = 0; - r.t = this.t + n; - r.s = this.s; - } - // (protected) r = this >> n*DB - function bnpDRShiftTo(n, r) - { - for (var i = n; i < this.t; ++i) r[i - n] = this[i]; - r.t = Math.max(this.t - n, 0); - r.s = this.s; - } - // (protected) r = this << n - function bnpLShiftTo(n, r) - { - var bs = n % this.DB; - var cbs = this.DB - bs; - var bm = (1 << cbs) - 1; - var ds = Math.floor(n / this.DB), - c = (this.s << bs) & this.DM, - i; - for (i = this.t - 1; i >= 0; --i) - { - r[i + ds + 1] = (this[i] >> cbs) | c; - c = (this[i] & bm) << bs; - } - for (i = ds - 1; i >= 0; --i) r[i] = 0; - r[ds] = c; - r.t = this.t + ds + 1; - r.s = this.s; - r.clamp(); - } - // (protected) r = this >> n - function bnpRShiftTo(n, r) - { - r.s = this.s; - var ds = Math.floor(n / this.DB); - if (ds >= this.t) - { - r.t = 0; - return; - } - var bs = n % this.DB; - var cbs = this.DB - bs; - var bm = (1 << bs) - 1; - r[0] = this[ds] >> bs; - for (var i = ds + 1; i < this.t; ++i) - { - r[i - ds - 1] |= (this[i] & bm) << cbs; - r[i - ds] = this[i] >> bs; - } - if (bs > 0) r[this.t - ds - 1] |= (this.s & bm) << cbs; - r.t = this.t - ds; - r.clamp(); - } - // (protected) r = this - a - function bnpSubTo(a, r) - { - var i = 0, - c = 0, - m = Math.min(a.t, this.t); - while (i < m) - { - c += this[i] - a[i]; - r[i++] = c & this.DM; - c >>= this.DB; - } - if (a.t < this.t) - { - c -= a.s; - while (i < this.t) - { - c += this[i]; - r[i++] = c & this.DM; - c >>= this.DB; - } - c += this.s; - } - else - { - c += this.s; - while (i < a.t) - { - c -= a[i]; - r[i++] = c & this.DM; - c >>= this.DB; - } - c -= a.s; - } - r.s = (c < 0) ? -1 : 0; - if (c < -1) r[i++] = this.DV + c; - else if (c > 0) r[i++] = c; - r.t = i; - r.clamp(); - } - // (protected) r = this * a, r != this,a (HAC 14.12) - // "this" should be the larger one if appropriate. - function bnpMultiplyTo(a, r) - { - var x = this.abs(), - y = a.abs(); - var i = x.t; - r.t = i + y.t; - while (--i >= 0) r[i] = 0; - for (i = 0; i < y.t; ++i) r[i + x.t] = x.am(0, y[i], r, i, 0, x.t); - r.s = 0; - r.clamp(); - if (this.s != a.s) BigInteger.ZERO.subTo(r, r); - } - // (protected) r = this^2, r != this (HAC 14.16) - function bnpSquareTo(r) - { - var x = this.abs(); - var i = r.t = 2 * x.t; - while (--i >= 0) r[i] = 0; - for (i = 0; i < x.t - 1; ++i) - { - var c = x.am(i, x[i], r, 2 * i, 0, 1); - if ((r[i + x.t] += x.am(i + 1, 2 * x[i], r, 2 * i + 1, c, x.t - i - 1)) >= x.DV) - { - r[i + x.t] -= x.DV; - r[i + x.t + 1] = 1; - } - } - if (r.t > 0) r[r.t - 1] += x.am(i, x[i], r, 2 * i, 0, 1); - r.s = 0; - r.clamp(); - } - // (protected) divide this by m, quotient and remainder to q, r (HAC 14.20) - // r != q, this != m. q or r may be null. - function bnpDivRemTo(m, q, r) - { - var pm = m.abs(); - if (pm.t <= 0) return; - var pt = this.abs(); - if (pt.t < pm.t) - { - if (q != null) q.fromInt(0); - if (r != null) this.copyTo(r); - return; - } - if (r == null) r = nbi(); - var y = nbi(), - ts = this.s, - ms = m.s; - var nsh = this.DB - nbits(pm[pm.t - 1]); // normalize modulus - if (nsh > 0) - { - pm.lShiftTo(nsh, y); - pt.lShiftTo(nsh, r); - } - else - { - pm.copyTo(y); - pt.copyTo(r); - } - var ys = y.t; - var y0 = y[ys - 1]; - if (y0 == 0) return; - var yt = y0 * (1 << this.F1) + ((ys > 1) ? y[ys - 2] >> this.F2 : 0); - var d1 = this.FV / yt, - d2 = (1 << this.F1) / yt, - e = 1 << this.F2; - var i = r.t, - j = i - ys, - t = (q == null) ? nbi() : q; - y.dlShiftTo(j, t); - if (r.compareTo(t) >= 0) - { - r[r.t++] = 1; - r.subTo(t, r); - } - BigInteger.ONE.dlShiftTo(ys, t); - t.subTo(y, y); // "negative" y so we can replace sub with am later - while (y.t < ys) y[y.t++] = 0; - while (--j >= 0) - { - // Estimate quotient digit - var qd = (r[--i] == y0) ? this.DM : Math.floor(r[i] * d1 + (r[i - 1] + e) * d2); - if ((r[i] += y.am(0, qd, r, j, 0, ys)) < qd) - { // Try it out - y.dlShiftTo(j, t); - r.subTo(t, r); - while (r[i] < --qd) r.subTo(t, r); - } - } - if (q != null) - { - r.drShiftTo(ys, q); - if (ts != ms) BigInteger.ZERO.subTo(q, q); - } - r.t = ys; - r.clamp(); - if (nsh > 0) r.rShiftTo(nsh, r); // Denormalize remainder - if (ts < 0) BigInteger.ZERO.subTo(r, r); - } - // (public) this mod a - function bnMod(a) - { - var r = nbi(); - this.abs().divRemTo(a, null, r); - if (this.s < 0 && r.compareTo(BigInteger.ZERO) > 0) a.subTo(r, r); - return r; - } - // Modular reduction using "classic" algorithm - function Classic(m) - { - this.m = m; - } - - function cConvert(x) - { - if (x.s < 0 || x.compareTo(this.m) >= 0) return x.mod(this.m); - else return x; - } - - function cRevert(x) - { - return x; - } - - function cReduce(x) - { - x.divRemTo(this.m, null, x); - } - - function cMulTo(x, y, r) - { - x.multiplyTo(y, r); - this.reduce(r); - } - - function cSqrTo(x, r) - { - x.squareTo(r); - this.reduce(r); - } - Classic.prototype.convert = cConvert; - Classic.prototype.revert = cRevert; - Classic.prototype.reduce = cReduce; - Classic.prototype.mulTo = cMulTo; - Classic.prototype.sqrTo = cSqrTo; - // (protected) return "-1/this % 2^DB"; useful for Mont. reduction - // justification: - // xy == 1 (mod m) - // xy = 1+km - // xy(2-xy) = (1+km)(1-km) - // x[y(2-xy)] = 1-k^2m^2 - // x[y(2-xy)] == 1 (mod m^2) - // if y is 1/x mod m, then y(2-xy) is 1/x mod m^2 - // should reduce x and y(2-xy) by m^2 at each step to keep size bounded. - // JS multiply "overflows" differently from C/C++, so care is needed here. - function bnpInvDigit() - { - if (this.t < 1) return 0; - var x = this[0]; - if ((x & 1) == 0) return 0; - var y = x & 3; // y == 1/x mod 2^2 - y = (y * (2 - (x & 0xf) * y)) & 0xf; // y == 1/x mod 2^4 - y = (y * (2 - (x & 0xff) * y)) & 0xff; // y == 1/x mod 2^8 - y = (y * (2 - (((x & 0xffff) * y) & 0xffff))) & 0xffff; // y == 1/x mod 2^16 - // last step - calculate inverse mod DV directly; - // assumes 16 < DB <= 32 and assumes ability to handle 48-bit ints - y = (y * (2 - x * y % this.DV)) % this.DV; // y == 1/x mod 2^dbits - // we really want the negative inverse, and -DV < y < DV - return (y > 0) ? this.DV - y : -y; - } - // Montgomery reduction - function Montgomery(m) - { - this.m = m; - this.mp = m.invDigit(); - this.mpl = this.mp & 0x7fff; - this.mph = this.mp >> 15; - this.um = (1 << (m.DB - 15)) - 1; - this.mt2 = 2 * m.t; - } - // xR mod m - function montConvert(x) - { - var r = nbi(); - x.abs().dlShiftTo(this.m.t, r); - r.divRemTo(this.m, null, r); - if (x.s < 0 && r.compareTo(BigInteger.ZERO) > 0) this.m.subTo(r, r); - return r; - } - // x/R mod m - function montRevert(x) - { - var r = nbi(); - x.copyTo(r); - this.reduce(r); - return r; - } - // x = x/R mod m (HAC 14.32) - function montReduce(x) - { - while (x.t <= this.mt2) // pad x so am has enough room later - x[x.t++] = 0; - for (var i = 0; i < this.m.t; ++i) - { - // faster way of calculating u0 = x[i]*mp mod DV - var j = x[i] & 0x7fff; - var u0 = (j * this.mpl + (((j * this.mph + (x[i] >> 15) * this.mpl) & this.um) << 15)) & x.DM; - // use am to combine the multiply-shift-add into one call - j = i + this.m.t; - x[j] += this.m.am(0, u0, x, i, 0, this.m.t); - // propagate carry - while (x[j] >= x.DV) - { - x[j] -= x.DV; - x[++j]++; - } - } - x.clamp(); - x.drShiftTo(this.m.t, x); - if (x.compareTo(this.m) >= 0) x.subTo(this.m, x); - } - // r = "x^2/R mod m"; x != r - function montSqrTo(x, r) - { - x.squareTo(r); - this.reduce(r); - } - // r = "xy/R mod m"; x,y != r - function montMulTo(x, y, r) - { - x.multiplyTo(y, r); - this.reduce(r); - } - Montgomery.prototype.convert = montConvert; - Montgomery.prototype.revert = montRevert; - Montgomery.prototype.reduce = montReduce; - Montgomery.prototype.mulTo = montMulTo; - Montgomery.prototype.sqrTo = montSqrTo; - // (protected) true iff this is even - function bnpIsEven() - { - return ((this.t > 0) ? (this[0] & 1) : this.s) == 0; - } - // (protected) this^e, e < 2^32, doing sqr and mul with "r" (HAC 14.79) - function bnpExp(e, z) - { - if (e > 0xffffffff || e < 1) return BigInteger.ONE; - var r = nbi(), - r2 = nbi(), - g = z.convert(this), - i = nbits(e) - 1; - g.copyTo(r); - while (--i >= 0) - { - z.sqrTo(r, r2); - if ((e & (1 << i)) > 0) z.mulTo(r2, g, r); - else - { - var t = r; - r = r2; - r2 = t; - } - } - return z.revert(r); - } - // (public) this^e % m, 0 <= e < 2^32 - function bnModPowInt(e, m) - { - var z; - if (e < 256 || m.isEven()) z = new Classic(m); - else z = new Montgomery(m); - return this.exp(e, z); - } - // protected - BigInteger.prototype.copyTo = bnpCopyTo; - BigInteger.prototype.fromInt = bnpFromInt; - BigInteger.prototype.fromString = bnpFromString; - BigInteger.prototype.clamp = bnpClamp; - BigInteger.prototype.dlShiftTo = bnpDLShiftTo; - BigInteger.prototype.drShiftTo = bnpDRShiftTo; - BigInteger.prototype.lShiftTo = bnpLShiftTo; - BigInteger.prototype.rShiftTo = bnpRShiftTo; - BigInteger.prototype.subTo = bnpSubTo; - BigInteger.prototype.multiplyTo = bnpMultiplyTo; - BigInteger.prototype.squareTo = bnpSquareTo; - BigInteger.prototype.divRemTo = bnpDivRemTo; - BigInteger.prototype.invDigit = bnpInvDigit; - BigInteger.prototype.isEven = bnpIsEven; - BigInteger.prototype.exp = bnpExp; - // public - BigInteger.prototype.toString = bnToString; - BigInteger.prototype.negate = bnNegate; - BigInteger.prototype.abs = bnAbs; - BigInteger.prototype.compareTo = bnCompareTo; - BigInteger.prototype.bitLength = bnBitLength; - BigInteger.prototype.mod = bnMod; - BigInteger.prototype.modPowInt = bnModPowInt; - // "constants" - BigInteger.ZERO = nbv(0); - BigInteger.ONE = nbv(1); - // Copyright (c) 2005-2009 Tom Wu - // All Rights Reserved. - // See "LICENSE" for details. - // Extended JavaScript BN functions, required for RSA private ops. - // Version 1.1: new BigInteger("0", 10) returns "proper" zero - // Version 1.2: square() API, isProbablePrime fix - // (public) - function bnClone() - { - var r = nbi(); - this.copyTo(r); - return r; - } - // (public) return value as integer - function bnIntValue() - { - if (this.s < 0) - { - if (this.t == 1) return this[0] - this.DV; - else if (this.t == 0) return -1; - } - else if (this.t == 1) return this[0]; - else if (this.t == 0) return 0; - // assumes 16 < DB < 32 - return ((this[1] & ((1 << (32 - this.DB)) - 1)) << this.DB) | this[0]; - } - // (public) return value as byte - function bnByteValue() - { - return (this.t == 0) ? this.s : (this[0] << 24) >> 24; - } - // (public) return value as short (assumes DB>=16) - function bnShortValue() - { - return (this.t == 0) ? this.s : (this[0] << 16) >> 16; - } - // (protected) return x s.t. r^x < DV - function bnpChunkSize(r) - { - return Math.floor(Math.LN2 * this.DB / Math.log(r)); - } - // (public) 0 if this == 0, 1 if this > 0 - function bnSigNum() - { - if (this.s < 0) return -1; - else if (this.t <= 0 || (this.t == 1 && this[0] <= 0)) return 0; - else return 1; - } - // (protected) convert to radix string - function bnpToRadix(b) - { - if (b == null) b = 10; - if (this.signum() == 0 || b < 2 || b > 36) return "0"; - var cs = this.chunkSize(b); - var a = Math.pow(b, cs); - var d = nbv(a), - y = nbi(), - z = nbi(), - r = ""; - this.divRemTo(d, y, z); - while (y.signum() > 0) - { - r = (a + z.intValue()).toString(b).substr(1) + r; - y.divRemTo(d, y, z); - } - return z.intValue().toString(b) + r; - } - // (protected) convert from radix string - function bnpFromRadix(s, b) - { - this.fromInt(0); - if (b == null) b = 10; - var cs = this.chunkSize(b); - var d = Math.pow(b, cs), - mi = false, - j = 0, - w = 0; - for (var i = 0; i < s.length; ++i) - { - var x = intAt(s, i); - if (x < 0) - { - if (s.charAt(i) == "-" && this.signum() == 0) mi = true; - continue; - } - w = b * w + x; - if (++j >= cs) - { - this.dMultiply(d); - this.dAddOffset(w, 0); - j = 0; - w = 0; - } - } - if (j > 0) - { - this.dMultiply(Math.pow(b, j)); - this.dAddOffset(w, 0); - } - if (mi) BigInteger.ZERO.subTo(this, this); - } - // (protected) alternate constructor - function bnpFromNumber(a, b, c) - { - if ("number" == typeof b) - { - // new BigInteger(int,int,RNG) - if (a < 2) this.fromInt(1); - else - { - this.fromNumber(a, c); - if (!this.testBit(a - 1)) // force MSB set - this.bitwiseTo(BigInteger.ONE.shiftLeft(a - 1), op_or, this); - if (this.isEven()) this.dAddOffset(1, 0); // force odd - while (!this.isProbablePrime(b)) - { - this.dAddOffset(2, 0); - if (this.bitLength() > a) this.subTo(BigInteger.ONE.shiftLeft(a - 1), this); - } - } - } - else - { - // new BigInteger(int,RNG) - var x = new Array(), - t = a & 7; - x.length = (a >> 3) + 1; - b.nextBytes(x); - if (t > 0) x[0] &= ((1 << t) - 1); - else x[0] = 0; - this.fromString(x, 256); - } - } - // (public) convert to bigendian byte array - function bnToByteArray() - { - var i = this.t, - r = new Array(); - r[0] = this.s; - var p = this.DB - (i * this.DB) % 8, - d, k = 0; - if (i-- > 0) - { - if (p < this.DB && (d = this[i] >> p) != (this.s & this.DM) >> p) - r[k++] = d | (this.s << (this.DB - p)); - while (i >= 0) - { - if (p < 8) - { - d = (this[i] & ((1 << p) - 1)) << (8 - p); - d |= this[--i] >> (p += this.DB - 8); - } - else - { - d = (this[i] >> (p -= 8)) & 0xff; - if (p <= 0) - { - p += this.DB; - --i; - } - } - if ((d & 0x80) != 0) d |= -256; - if (k == 0 && (this.s & 0x80) != (d & 0x80))++k; - if (k > 0 || d != this.s) r[k++] = d; - } - } - return r; - } - - function bnEquals(a) - { - return (this.compareTo(a) == 0); - } - - function bnMin(a) - { - return (this.compareTo(a) < 0) ? this : a; - } - - function bnMax(a) - { - return (this.compareTo(a) > 0) ? this : a; - } - // (protected) r = this op a (bitwise) - function bnpBitwiseTo(a, op, r) - { - var i, f, m = Math.min(a.t, this.t); - for (i = 0; i < m; ++i) r[i] = op(this[i], a[i]); - if (a.t < this.t) - { - f = a.s & this.DM; - for (i = m; i < this.t; ++i) r[i] = op(this[i], f); - r.t = this.t; - } - else - { - f = this.s & this.DM; - for (i = m; i < a.t; ++i) r[i] = op(f, a[i]); - r.t = a.t; - } - r.s = op(this.s, a.s); - r.clamp(); - } - // (public) this & a - function op_and(x, y) - { - return x & y; - } - - function bnAnd(a) - { - var r = nbi(); - this.bitwiseTo(a, op_and, r); - return r; - } - // (public) this | a - function op_or(x, y) - { - return x | y; - } - - function bnOr(a) - { - var r = nbi(); - this.bitwiseTo(a, op_or, r); - return r; - } - // (public) this ^ a - function op_xor(x, y) - { - return x ^ y; - } - - function bnXor(a) - { - var r = nbi(); - this.bitwiseTo(a, op_xor, r); - return r; - } - // (public) this & ~a - function op_andnot(x, y) - { - return x & ~y; - } - - function bnAndNot(a) - { - var r = nbi(); - this.bitwiseTo(a, op_andnot, r); - return r; - } - // (public) ~this - function bnNot() - { - var r = nbi(); - for (var i = 0; i < this.t; ++i) r[i] = this.DM & ~this[i]; - r.t = this.t; - r.s = ~this.s; - return r; - } - // (public) this << n - function bnShiftLeft(n) - { - var r = nbi(); - if (n < 0) this.rShiftTo(-n, r); - else this.lShiftTo(n, r); - return r; - } - // (public) this >> n - function bnShiftRight(n) - { - var r = nbi(); - if (n < 0) this.lShiftTo(-n, r); - else this.rShiftTo(n, r); - return r; - } - // return index of lowest 1-bit in x, x < 2^31 - function lbit(x) - { - if (x == 0) return -1; - var r = 0; - if ((x & 0xffff) == 0) - { - x >>= 16; - r += 16; - } - if ((x & 0xff) == 0) - { - x >>= 8; - r += 8; - } - if ((x & 0xf) == 0) - { - x >>= 4; - r += 4; - } - if ((x & 3) == 0) - { - x >>= 2; - r += 2; - } - if ((x & 1) == 0)++r; - return r; - } - // (public) returns index of lowest 1-bit (or -1 if none) - function bnGetLowestSetBit() - { - for (var i = 0; i < this.t; ++i) - if (this[i] != 0) return i * this.DB + lbit(this[i]); - if (this.s < 0) return this.t * this.DB; - return -1; - } - // return number of 1 bits in x - function cbit(x) - { - var r = 0; - while (x != 0) - { - x &= x - 1; - ++r; - } - return r; - } - // (public) return number of set bits - function bnBitCount() - { - var r = 0, - x = this.s & this.DM; - for (var i = 0; i < this.t; ++i) r += cbit(this[i] ^ x); - return r; - } - // (public) true iff nth bit is set - function bnTestBit(n) - { - var j = Math.floor(n / this.DB); - if (j >= this.t) return (this.s != 0); - return ((this[j] & (1 << (n % this.DB))) != 0); - } - // (protected) this op (1<>= this.DB; - } - if (a.t < this.t) - { - c += a.s; - while (i < this.t) - { - c += this[i]; - r[i++] = c & this.DM; - c >>= this.DB; - } - c += this.s; - } - else - { - c += this.s; - while (i < a.t) - { - c += a[i]; - r[i++] = c & this.DM; - c >>= this.DB; - } - c += a.s; - } - r.s = (c < 0) ? -1 : 0; - if (c > 0) r[i++] = c; - else if (c < -1) r[i++] = this.DV + c; - r.t = i; - r.clamp(); - } - // (public) this + a - function bnAdd(a) - { - var r = nbi(); - this.addTo(a, r); - return r; - } - // (public) this - a - function bnSubtract(a) - { - var r = nbi(); - this.subTo(a, r); - return r; - } - // (public) this * a - function bnMultiply(a) - { - var r = nbi(); - this.multiplyTo(a, r); - return r; - } - // (public) this^2 - function bnSquare() - { - var r = nbi(); - this.squareTo(r); - return r; - } - // (public) this / a - function bnDivide(a) - { - var r = nbi(); - this.divRemTo(a, r, null); - return r; - } - // (public) this % a - function bnRemainder(a) - { - var r = nbi(); - this.divRemTo(a, null, r); - return r; - } - // (public) [this/a,this%a] - function bnDivideAndRemainder(a) - { - var q = nbi(), - r = nbi(); - this.divRemTo(a, q, r); - return new Array(q, r); - } - // (protected) this *= n, this >= 0, 1 < n < DV - function bnpDMultiply(n) - { - this[this.t] = this.am(0, n - 1, this, 0, 0, this.t); - ++this.t; - this.clamp(); - } - // (protected) this += n << w words, this >= 0 - function bnpDAddOffset(n, w) - { - if (n == 0) return; - while (this.t <= w) this[this.t++] = 0; - this[w] += n; - while (this[w] >= this.DV) - { - this[w] -= this.DV; - if (++w >= this.t) this[this.t++] = 0; - ++this[w]; - } - } - // A "null" reducer - function NullExp() - {} - - function nNop(x) - { - return x; - } - - function nMulTo(x, y, r) - { - x.multiplyTo(y, r); - } - - function nSqrTo(x, r) - { - x.squareTo(r); - } - NullExp.prototype.convert = nNop; - NullExp.prototype.revert = nNop; - NullExp.prototype.mulTo = nMulTo; - NullExp.prototype.sqrTo = nSqrTo; - // (public) this^e - function bnPow(e) - { - return this.exp(e, new NullExp()); - } - // (protected) r = lower n words of "this * a", a.t <= n - // "this" should be the larger one if appropriate. - function bnpMultiplyLowerTo(a, n, r) - { - var i = Math.min(this.t + a.t, n); - r.s = 0; // assumes a,this >= 0 - r.t = i; - while (i > 0) r[--i] = 0; - var j; - for (j = r.t - this.t; i < j; ++i) r[i + this.t] = this.am(0, a[i], r, i, 0, this.t); - for (j = Math.min(a.t, n); i < j; ++i) this.am(0, a[i], r, i, 0, n - i); - r.clamp(); - } - // (protected) r = "this * a" without lower n words, n > 0 - // "this" should be the larger one if appropriate. - function bnpMultiplyUpperTo(a, n, r) - { - --n; - var i = r.t = this.t + a.t - n; - r.s = 0; // assumes a,this >= 0 - while (--i >= 0) r[i] = 0; - for (i = Math.max(n - this.t, 0); i < a.t; ++i) - r[this.t + i - n] = this.am(n - i, a[i], r, 0, 0, this.t + i - n); - r.clamp(); - r.drShiftTo(1, r); - } - // Barrett modular reduction - function Barrett(m) - { - // setup Barrett - this.r2 = nbi(); - this.q3 = nbi(); - BigInteger.ONE.dlShiftTo(2 * m.t, this.r2); - this.mu = this.r2.divide(m); - this.m = m; - } - - function barrettConvert(x) - { - if (x.s < 0 || x.t > 2 * this.m.t) return x.mod(this.m); - else if (x.compareTo(this.m) < 0) return x; - else - { - var r = nbi(); - x.copyTo(r); - this.reduce(r); - return r; - } - } - - function barrettRevert(x) - { - return x; - } - // x = x mod m (HAC 14.42) - function barrettReduce(x) - { - x.drShiftTo(this.m.t - 1, this.r2); - if (x.t > this.m.t + 1) - { - x.t = this.m.t + 1; - x.clamp(); - } - this.mu.multiplyUpperTo(this.r2, this.m.t + 1, this.q3); - this.m.multiplyLowerTo(this.q3, this.m.t + 1, this.r2); - while (x.compareTo(this.r2) < 0) x.dAddOffset(1, this.m.t + 1); - x.subTo(this.r2, x); - while (x.compareTo(this.m) >= 0) x.subTo(this.m, x); - } - // r = x^2 mod m; x != r - function barrettSqrTo(x, r) - { - x.squareTo(r); - this.reduce(r); - } - // r = x*y mod m; x,y != r - function barrettMulTo(x, y, r) - { - x.multiplyTo(y, r); - this.reduce(r); - } - Barrett.prototype.convert = barrettConvert; - Barrett.prototype.revert = barrettRevert; - Barrett.prototype.reduce = barrettReduce; - Barrett.prototype.mulTo = barrettMulTo; - Barrett.prototype.sqrTo = barrettSqrTo; - // (public) this^e % m (HAC 14.85) - function bnModPow(e, m) - { - var i = e.bitLength(), - k, r = nbv(1), - z; - if (i <= 0) return r; - else if (i < 18) k = 1; - else if (i < 48) k = 3; - else if (i < 144) k = 4; - else if (i < 768) k = 5; - else k = 6; - if (i < 8) - z = new Classic(m); - else if (m.isEven()) - z = new Barrett(m); - else - z = new Montgomery(m); - // precomputation - var g = new Array(), - n = 3, - k1 = k - 1, - km = (1 << k) - 1; - g[1] = z.convert(this); - if (k > 1) - { - var g2 = nbi(); - z.sqrTo(g[1], g2); - while (n <= km) - { - g[n] = nbi(); - z.mulTo(g2, g[n - 2], g[n]); - n += 2; - } - } - var j = e.t - 1, - w, is1 = true, - r2 = nbi(), - t; - i = nbits(e[j]) - 1; - while (j >= 0) - { - if (i >= k1) w = (e[j] >> (i - k1)) & km; - else - { - w = (e[j] & ((1 << (i + 1)) - 1)) << (k1 - i); - if (j > 0) w |= e[j - 1] >> (this.DB + i - k1); - } - n = k; - while ((w & 1) == 0) - { - w >>= 1; - --n; - } - if ((i -= n) < 0) - { - i += this.DB; - --j; - } - if (is1) - { // ret == 1, don't bother squaring or multiplying it - g[w].copyTo(r); - is1 = false; - } - else - { - while (n > 1) - { - z.sqrTo(r, r2); - z.sqrTo(r2, r); - n -= 2; - } - if (n > 0) z.sqrTo(r, r2); - else - { - t = r; - r = r2; - r2 = t; - } - z.mulTo(r2, g[w], r); - } - while (j >= 0 && (e[j] & (1 << i)) == 0) - { - z.sqrTo(r, r2); - t = r; - r = r2; - r2 = t; - if (--i < 0) - { - i = this.DB - 1; - --j; - } - } - } - return z.revert(r); - } - // (public) gcd(this,a) (HAC 14.54) - function bnGCD(a) - { - var x = (this.s < 0) ? this.negate() : this.clone(); - var y = (a.s < 0) ? a.negate() : a.clone(); - if (x.compareTo(y) < 0) - { - var t = x; - x = y; - y = t; - } - var i = x.getLowestSetBit(), - g = y.getLowestSetBit(); - if (g < 0) return x; - if (i < g) g = i; - if (g > 0) - { - x.rShiftTo(g, x); - y.rShiftTo(g, y); - } - while (x.signum() > 0) - { - if ((i = x.getLowestSetBit()) > 0) x.rShiftTo(i, x); - if ((i = y.getLowestSetBit()) > 0) y.rShiftTo(i, y); - if (x.compareTo(y) >= 0) - { - x.subTo(y, x); - x.rShiftTo(1, x); - } - else - { - y.subTo(x, y); - y.rShiftTo(1, y); - } - } - if (g > 0) y.lShiftTo(g, y); - return y; - } - // (protected) this % n, n < 2^26 - function bnpModInt(n) - { - if (n <= 0) return 0; - var d = this.DV % n, - r = (this.s < 0) ? n - 1 : 0; - if (this.t > 0) - if (d == 0) r = this[0] % n; - else - for (var i = this.t - 1; i >= 0; --i) r = (d * r + this[i]) % n; - return r; - } - // (public) 1/this % m (HAC 14.61) - function bnModInverse(m) - { - var ac = m.isEven(); - if ((this.isEven() && ac) || m.signum() == 0) return BigInteger.ZERO; - var u = m.clone(), - v = this.clone(); - var a = nbv(1), - b = nbv(0), - c = nbv(0), - d = nbv(1); - while (u.signum() != 0) - { - while (u.isEven()) - { - u.rShiftTo(1, u); - if (ac) - { - if (!a.isEven() || !b.isEven()) - { - a.addTo(this, a); - b.subTo(m, b); - } - a.rShiftTo(1, a); - } - else if (!b.isEven()) b.subTo(m, b); - b.rShiftTo(1, b); - } - while (v.isEven()) - { - v.rShiftTo(1, v); - if (ac) - { - if (!c.isEven() || !d.isEven()) - { - c.addTo(this, c); - d.subTo(m, d); - } - c.rShiftTo(1, c); - } - else if (!d.isEven()) d.subTo(m, d); - d.rShiftTo(1, d); - } - if (u.compareTo(v) >= 0) - { - u.subTo(v, u); - if (ac) a.subTo(c, a); - b.subTo(d, b); - } - else - { - v.subTo(u, v); - if (ac) c.subTo(a, c); - d.subTo(b, d); - } - } - if (v.compareTo(BigInteger.ONE) != 0) return BigInteger.ZERO; - if (d.compareTo(m) >= 0) return d.subtract(m); - if (d.signum() < 0) d.addTo(m, d); - else return d; - if (d.signum() < 0) return d.add(m); - else return d; - } - var lowprimes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997]; - var lplim = (1 << 26) / lowprimes[lowprimes.length - 1]; - // (public) test primality with certainty >= 1-.5^t - function bnIsProbablePrime(t) - { - var i, x = this.abs(); - if (x.t == 1 && x[0] <= lowprimes[lowprimes.length - 1]) - { - for (i = 0; i < lowprimes.length; ++i) - if (x[0] == lowprimes[i]) return true; - return false; - } - if (x.isEven()) return false; - i = 1; - while (i < lowprimes.length) - { - var m = lowprimes[i], - j = i + 1; - while (j < lowprimes.length && m < lplim) m *= lowprimes[j++]; - m = x.modInt(m); - while (i < j) - if (m % lowprimes[i++] == 0) return false; - } - return x.millerRabin(t); - } - // (protected) true if probably prime (HAC 4.24, Miller-Rabin) - function bnpMillerRabin(t) - { - var n1 = this.subtract(BigInteger.ONE); - var k = n1.getLowestSetBit(); - if (k <= 0) return false; - var r = n1.shiftRight(k); - t = (t + 1) >> 1; - if (t > lowprimes.length) t = lowprimes.length; - var a = nbi(); - for (var i = 0; i < t; ++i) - { - //Pick bases at random, instead of starting at 2 - a.fromInt(lowprimes[Math.floor(Math.random() * lowprimes.length)]); - var y = a.modPow(r, this); - if (y.compareTo(BigInteger.ONE) != 0 && y.compareTo(n1) != 0) - { - var j = 1; - while (j++ < k && y.compareTo(n1) != 0) - { - y = y.modPowInt(2, this); - if (y.compareTo(BigInteger.ONE) == 0) return false; - } - if (y.compareTo(n1) != 0) return false; - } - } - return true; - } - // protected - BigInteger.prototype.chunkSize = bnpChunkSize; - BigInteger.prototype.toRadix = bnpToRadix; - BigInteger.prototype.fromRadix = bnpFromRadix; - BigInteger.prototype.fromNumber = bnpFromNumber; - BigInteger.prototype.bitwiseTo = bnpBitwiseTo; - BigInteger.prototype.changeBit = bnpChangeBit; - BigInteger.prototype.addTo = bnpAddTo; - BigInteger.prototype.dMultiply = bnpDMultiply; - BigInteger.prototype.dAddOffset = bnpDAddOffset; - BigInteger.prototype.multiplyLowerTo = bnpMultiplyLowerTo; - BigInteger.prototype.multiplyUpperTo = bnpMultiplyUpperTo; - BigInteger.prototype.modInt = bnpModInt; - BigInteger.prototype.millerRabin = bnpMillerRabin; - // public - BigInteger.prototype.clone = bnClone; - BigInteger.prototype.intValue = bnIntValue; - BigInteger.prototype.byteValue = bnByteValue; - BigInteger.prototype.shortValue = bnShortValue; - BigInteger.prototype.signum = bnSigNum; - BigInteger.prototype.toByteArray = bnToByteArray; - BigInteger.prototype.equals = bnEquals; - BigInteger.prototype.min = bnMin; - BigInteger.prototype.max = bnMax; - BigInteger.prototype.and = bnAnd; - BigInteger.prototype.or = bnOr; - BigInteger.prototype.xor = bnXor; - BigInteger.prototype.andNot = bnAndNot; - BigInteger.prototype.not = bnNot; - BigInteger.prototype.shiftLeft = bnShiftLeft; - BigInteger.prototype.shiftRight = bnShiftRight; - BigInteger.prototype.getLowestSetBit = bnGetLowestSetBit; - BigInteger.prototype.bitCount = bnBitCount; - BigInteger.prototype.testBit = bnTestBit; - BigInteger.prototype.setBit = bnSetBit; - BigInteger.prototype.clearBit = bnClearBit; - BigInteger.prototype.flipBit = bnFlipBit; - BigInteger.prototype.add = bnAdd; - BigInteger.prototype.subtract = bnSubtract; - BigInteger.prototype.multiply = bnMultiply; - BigInteger.prototype.divide = bnDivide; - BigInteger.prototype.remainder = bnRemainder; - BigInteger.prototype.divideAndRemainder = bnDivideAndRemainder; - BigInteger.prototype.modPow = bnModPow; - BigInteger.prototype.modInverse = bnModInverse; - BigInteger.prototype.pow = bnPow; - BigInteger.prototype.gcd = bnGCD; - BigInteger.prototype.isProbablePrime = bnIsProbablePrime; - // JSBN-specific extension - BigInteger.prototype.square = bnSquare; - var Int128 = BigInteger; - // BigInteger interfaces not implemented in jsbn: - // BigInteger(int signum, byte[] magnitude) - // double doubleValue() - // float floatValue() - // int hashCode() - // long longValue() - // static BigInteger valueOf(long val) - // Helper functions to make BigInteger functions callable with two parameters - // as in original C# Clipper - Int128.prototype.IsNegative = function () - { - if (this.compareTo(Int128.ZERO) == -1) return true; - else return false; - }; - Int128.op_Equality = function (val1, val2) - { - if (val1.compareTo(val2) == 0) return true; - else return false; - }; - Int128.op_Inequality = function (val1, val2) - { - if (val1.compareTo(val2) != 0) return true; - else return false; - }; - Int128.op_GreaterThan = function (val1, val2) - { - if (val1.compareTo(val2) > 0) return true; - else return false; - }; - Int128.op_LessThan = function (val1, val2) - { - if (val1.compareTo(val2) < 0) return true; - else return false; - }; - Int128.op_Addition = function (lhs, rhs) - { - return new Int128(lhs).add(new Int128(rhs)); - }; - Int128.op_Subtraction = function (lhs, rhs) - { - return new Int128(lhs).subtract(new Int128(rhs)); - }; - Int128.Int128Mul = function (lhs, rhs) - { - return new Int128(lhs).multiply(new Int128(rhs)); - }; - Int128.op_Division = function (lhs, rhs) - { - return lhs.divide(rhs); - }; - Int128.prototype.ToDouble = function () - { - return parseFloat(this.toString()); // This could be something faster - }; - // end of Int128 section - /* - // Uncomment the following two lines if you want to use Int128 outside ClipperLib - if (typeof(document) !== "undefined") window.Int128 = Int128; - else self.Int128 = Int128; - */ - // --------------------------------------------- - // Here starts the actual Clipper library: - // Helper function to support Inheritance in Javascript - if (typeof (Inherit) == 'undefined') - { - var Inherit = function (ce, ce2) - { - var p; - if (typeof (Object.getOwnPropertyNames) == 'undefined') - { - for (p in ce2.prototype) - if (typeof (ce.prototype[p]) == 'undefined' || ce.prototype[p] == Object.prototype[p]) ce.prototype[p] = ce2.prototype[p]; - for (p in ce2) - if (typeof (ce[p]) == 'undefined') ce[p] = ce2[p]; - ce.$baseCtor = ce2; - } - else - { - var props = Object.getOwnPropertyNames(ce2.prototype); - for (var i = 0; i < props.length; i++) - if (typeof (Object.getOwnPropertyDescriptor(ce.prototype, props[i])) == 'undefined') Object.defineProperty(ce.prototype, props[i], Object.getOwnPropertyDescriptor(ce2.prototype, props[i])); - for (p in ce2) - if (typeof (ce[p]) == 'undefined') ce[p] = ce2[p]; - ce.$baseCtor = ce2; - } - }; - } - ClipperLib.Path = function () - { - return []; - }; - ClipperLib.Paths = function () - { - return []; // Was previously [[]], but caused problems when pushed - }; - // Preserves the calling way of original C# Clipper - // Is essential due to compatibility, because DoublePoint is public class in original C# version - ClipperLib.DoublePoint = function () - { - var a = arguments; - this.X = 0; - this.Y = 0; - // public DoublePoint(DoublePoint dp) - // public DoublePoint(IntPoint ip) - if (a.length == 1) - { - this.X = a[0].X; - this.Y = a[0].Y; - } - else if (a.length == 2) - { - this.X = a[0]; - this.Y = a[1]; - } - }; // This is internal faster function when called without arguments - ClipperLib.DoublePoint0 = function () - { - this.X = 0; - this.Y = 0; - }; - // This is internal faster function when called with 1 argument (dp or ip) - ClipperLib.DoublePoint1 = function (dp) - { - this.X = dp.X; - this.Y = dp.Y; - }; - // This is internal faster function when called with 2 arguments (x and y) - ClipperLib.DoublePoint2 = function (x, y) - { - this.X = x; - this.Y = y; - }; - // PolyTree & PolyNode start - // ------------------------------- - ClipperLib.PolyNode = function () - { - this.m_Parent = null; - this.m_polygon = new ClipperLib.Path(); - this.m_Index = 0; - this.m_jointype = 0; - this.m_endtype = 0; - this.m_Childs = []; - this.IsOpen = false; - }; - ClipperLib.PolyNode.prototype.IsHoleNode = function () - { - var result = true; - var node = this.m_Parent; - while (node !== null) - { - result = !result; - node = node.m_Parent; - } - return result; - }; - ClipperLib.PolyNode.prototype.ChildCount = function () - { - return this.m_Childs.length; - }; - ClipperLib.PolyNode.prototype.Contour = function () - { - return this.m_polygon; - }; - ClipperLib.PolyNode.prototype.AddChild = function (Child) - { - var cnt = this.m_Childs.length; - this.m_Childs.push(Child); - Child.m_Parent = this; - Child.m_Index = cnt; - }; - ClipperLib.PolyNode.prototype.GetNext = function () - { - if (this.m_Childs.length > 0) - return this.m_Childs[0]; - else - return this.GetNextSiblingUp(); - }; - ClipperLib.PolyNode.prototype.GetNextSiblingUp = function () - { - if (this.m_Parent === null) - return null; - else if (this.m_Index == this.m_Parent.m_Childs.length - 1) - return this.m_Parent.GetNextSiblingUp(); - else - return this.m_Parent.m_Childs[this.m_Index + 1]; - }; - ClipperLib.PolyNode.prototype.Childs = function () - { - return this.m_Childs; - }; - ClipperLib.PolyNode.prototype.Parent = function () - { - return this.m_Parent; - }; - ClipperLib.PolyNode.prototype.IsHole = function () - { - return this.IsHoleNode(); - }; - // PolyTree : PolyNode - ClipperLib.PolyTree = function () - { - this.m_AllPolys = []; - ClipperLib.PolyNode.call(this); - }; - ClipperLib.PolyTree.prototype.Clear = function () - { - for (var i = 0, ilen = this.m_AllPolys.length; i < ilen; i++) - this.m_AllPolys[i] = null; - this.m_AllPolys.length = 0; - this.m_Childs.length = 0; - }; - ClipperLib.PolyTree.prototype.GetFirst = function () - { - if (this.m_Childs.length > 0) - return this.m_Childs[0]; - else - return null; - }; - ClipperLib.PolyTree.prototype.Total = function () - { - return this.m_AllPolys.length; - }; - Inherit(ClipperLib.PolyTree, ClipperLib.PolyNode); - // ------------------------------- - // PolyTree & PolyNode end - ClipperLib.Math_Abs_Int64 = ClipperLib.Math_Abs_Int32 = ClipperLib.Math_Abs_Double = function (a) - { - return Math.abs(a); - }; - ClipperLib.Math_Max_Int32_Int32 = function (a, b) - { - return Math.max(a, b); - }; - /* - ----------------------------------- - cast_32 speedtest: http://jsperf.com/truncate-float-to-integer/2 - ----------------------------------- - */ - if (browser.msie || browser.opera || browser.safari) ClipperLib.Cast_Int32 = function (a) - { - return a | 0; - }; - else ClipperLib.Cast_Int32 = function (a) - { // eg. browser.chrome || browser.chromium || browser.firefox - return~~ a; - }; - /* - -------------------------- - cast_64 speedtests: http://jsperf.com/truncate-float-to-integer - Chrome: bitwise_not_floor - Firefox17: toInteger (typeof test) - IE9: bitwise_or_floor - IE7 and IE8: to_parseint - Chromium: to_floor_or_ceil - Firefox3: to_floor_or_ceil - Firefox15: to_floor_or_ceil - Opera: to_floor_or_ceil - Safari: to_floor_or_ceil - -------------------------- - */ - if (browser.chrome) ClipperLib.Cast_Int64 = function (a) - { - if (a < -2147483648 || a > 2147483647) - return a < 0 ? Math.ceil(a) : Math.floor(a); - else return~~ a; - }; - else if (browser.firefox && typeof (Number.toInteger) == "function") ClipperLib.Cast_Int64 = function (a) - { - return Number.toInteger(a); - }; - else if (browser.msie7 || browser.msie8) ClipperLib.Cast_Int64 = function (a) - { - return parseInt(a, 10); - }; - else if (browser.msie) ClipperLib.Cast_Int64 = function (a) - { - if (a < -2147483648 || a > 2147483647) - return a < 0 ? Math.ceil(a) : Math.floor(a); - return a | 0; - }; - // eg. browser.chromium || browser.firefox || browser.opera || browser.safari - else ClipperLib.Cast_Int64 = function (a) - { - return a < 0 ? Math.ceil(a) : Math.floor(a); - }; - ClipperLib.Clear = function (a) - { - a.length = 0; - }; - //ClipperLib.MaxSteps = 64; // How many steps at maximum in arc in BuildArc() function - ClipperLib.PI = 3.141592653589793; - ClipperLib.PI2 = 2 * 3.141592653589793; - ClipperLib.IntPoint = function () - { - var a = arguments, - alen = a.length; - this.X = 0; - this.Y = 0; - if (use_xyz) - { - this.Z = 0; - if (alen == 3) // public IntPoint(cInt x, cInt y, cInt z = 0) - { - this.X = a[0]; - this.Y = a[1]; - this.Z = a[2]; - } - else if (alen == 2) // public IntPoint(cInt x, cInt y) - { - this.X = a[0]; - this.Y = a[1]; - this.Z = 0; - } - else if (alen == 1) - { - if (a[0] instanceof ClipperLib.DoublePoint) // public IntPoint(DoublePoint dp) - { - var dp = a[0]; - this.X = ClipperLib.Clipper.Round(dp.X); - this.Y = ClipperLib.Clipper.Round(dp.Y); - this.Z = 0; - } - else // public IntPoint(IntPoint pt) - { - var pt = a[0]; - if (typeof (pt.Z) == "undefined") pt.Z = 0; - this.X = pt.X; - this.Y = pt.Y; - this.Z = pt.Z; - } - } - else // public IntPoint() - { - this.X = 0; - this.Y = 0; - this.Z = 0; - } - } - else // if (!use_xyz) - { - if (alen == 2) // public IntPoint(cInt X, cInt Y) - { - this.X = a[0]; - this.Y = a[1]; - } - else if (alen == 1) - { - if (a[0] instanceof ClipperLib.DoublePoint) // public IntPoint(DoublePoint dp) - { - var dp = a[0]; - this.X = ClipperLib.Clipper.Round(dp.X); - this.Y = ClipperLib.Clipper.Round(dp.Y); - } - else // public IntPoint(IntPoint pt) - { - var pt = a[0]; - this.X = pt.X; - this.Y = pt.Y; - } - } - else // public IntPoint(IntPoint pt) - { - this.X = 0; - this.Y = 0; - } - } - }; - ClipperLib.IntPoint.op_Equality = function (a, b) - { - //return a == b; - return a.X == b.X && a.Y == b.Y; - }; - ClipperLib.IntPoint.op_Inequality = function (a, b) - { - //return a != b; - return a.X != b.X || a.Y != b.Y; - }; - /* - ClipperLib.IntPoint.prototype.Equals = function (obj) - { - if (obj === null) - return false; - if (obj instanceof ClipperLib.IntPoint) - { - var a = Cast(obj, ClipperLib.IntPoint); - return (this.X == a.X) && (this.Y == a.Y); - } - else - return false; - }; -*/ - if (use_xyz) - { - ClipperLib.IntPoint0 = function () - { - this.X = 0; - this.Y = 0; - this.Z = 0; - }; - ClipperLib.IntPoint1 = function (pt) - { - this.X = pt.X; - this.Y = pt.Y; - this.Z = pt.Z; - }; - ClipperLib.IntPoint1dp = function (dp) - { - this.X = ClipperLib.Clipper.Round(dp.X); - this.Y = ClipperLib.Clipper.Round(dp.Y); - this.Z = 0; - }; - ClipperLib.IntPoint2 = function (x, y) - { - this.X = x; - this.Y = y; - this.Z = 0; - }; - ClipperLib.IntPoint3 = function (x, y, z) - { - this.X = x; - this.Y = y; - this.Z = z; - }; - } - else // if (!use_xyz) - { - ClipperLib.IntPoint0 = function () - { - this.X = 0; - this.Y = 0; - }; - ClipperLib.IntPoint1 = function (pt) - { - this.X = pt.X; - this.Y = pt.Y; - }; - ClipperLib.IntPoint1dp = function (dp) - { - this.X = ClipperLib.Clipper.Round(dp.X); - this.Y = ClipperLib.Clipper.Round(dp.Y); - }; - ClipperLib.IntPoint2 = function (x, y) - { - this.X = x; - this.Y = y; - }; - } - ClipperLib.IntRect = function () - { - var a = arguments, - alen = a.length; - if (alen == 4) // function (l, t, r, b) - { - this.left = a[0]; - this.top = a[1]; - this.right = a[2]; - this.bottom = a[3]; - } - else if (alen == 1) // function (ir) - { - this.left = ir.left; - this.top = ir.top; - this.right = ir.right; - this.bottom = ir.bottom; - } - else // function () - { - this.left = 0; - this.top = 0; - this.right = 0; - this.bottom = 0; - } - }; - ClipperLib.IntRect0 = function () - { - this.left = 0; - this.top = 0; - this.right = 0; - this.bottom = 0; - }; - ClipperLib.IntRect1 = function (ir) - { - this.left = ir.left; - this.top = ir.top; - this.right = ir.right; - this.bottom = ir.bottom; - }; - ClipperLib.IntRect4 = function (l, t, r, b) - { - this.left = l; - this.top = t; - this.right = r; - this.bottom = b; - }; - ClipperLib.ClipType = { - ctIntersection: 0, - ctUnion: 1, - ctDifference: 2, - ctXor: 3 - }; - ClipperLib.PolyType = { - ptSubject: 0, - ptClip: 1 - }; - ClipperLib.PolyFillType = { - pftEvenOdd: 0, - pftNonZero: 1, - pftPositive: 2, - pftNegative: 3 - }; - ClipperLib.JoinType = { - jtSquare: 0, - jtRound: 1, - jtMiter: 2 - }; - ClipperLib.EndType = { - etOpenSquare: 0, - etOpenRound: 1, - etOpenButt: 2, - etClosedLine: 3, - etClosedPolygon: 4 - }; - if (use_deprecated) - ClipperLib.EndType_ = { - etSquare: 0, - etRound: 1, - etButt: 2, - etClosed: 3 - }; - ClipperLib.EdgeSide = { - esLeft: 0, - esRight: 1 - }; - ClipperLib.Direction = { - dRightToLeft: 0, - dLeftToRight: 1 - }; - ClipperLib.TEdge = function () - { - this.Bot = new ClipperLib.IntPoint(); - this.Curr = new ClipperLib.IntPoint(); - this.Top = new ClipperLib.IntPoint(); - this.Delta = new ClipperLib.IntPoint(); - this.Dx = 0; - this.PolyTyp = ClipperLib.PolyType.ptSubject; - this.Side = ClipperLib.EdgeSide.esLeft; - this.WindDelta = 0; - this.WindCnt = 0; - this.WindCnt2 = 0; - this.OutIdx = 0; - this.Next = null; - this.Prev = null; - this.NextInLML = null; - this.NextInAEL = null; - this.PrevInAEL = null; - this.NextInSEL = null; - this.PrevInSEL = null; - }; - ClipperLib.IntersectNode = function () - { - this.Edge1 = null; - this.Edge2 = null; - this.Pt = new ClipperLib.IntPoint(); - }; - ClipperLib.MyIntersectNodeSort = function () {}; - ClipperLib.MyIntersectNodeSort.Compare = function (node1, node2) - { - return (node2.Pt.Y - node1.Pt.Y); - }; - ClipperLib.LocalMinima = function () - { - this.Y = 0; - this.LeftBound = null; - this.RightBound = null; - this.Next = null; - }; - ClipperLib.Scanbeam = function () - { - this.Y = 0; - this.Next = null; - }; - ClipperLib.OutRec = function () - { - this.Idx = 0; - this.IsHole = false; - this.IsOpen = false; - this.FirstLeft = null; - this.Pts = null; - this.BottomPt = null; - this.PolyNode = null; - }; - ClipperLib.OutPt = function () - { - this.Idx = 0; - this.Pt = new ClipperLib.IntPoint(); - this.Next = null; - this.Prev = null; - }; - ClipperLib.Join = function () - { - this.OutPt1 = null; - this.OutPt2 = null; - this.OffPt = new ClipperLib.IntPoint(); - }; - ClipperLib.ClipperBase = function () - { - this.m_MinimaList = null; - this.m_CurrentLM = null; - this.m_edges = new Array(); - this.m_UseFullRange = false; - this.m_HasOpenPaths = false; - this.PreserveCollinear = false; - this.m_MinimaList = null; - this.m_CurrentLM = null; - this.m_UseFullRange = false; - this.m_HasOpenPaths = false; - }; - // Ranges are in original C# too high for Javascript (in current state 2013 september): - // protected const double horizontal = -3.4E+38; - // internal const cInt loRange = 0x3FFFFFFF; // = 1073741823 = sqrt(2^63 -1)/2 - // internal const cInt hiRange = 0x3FFFFFFFFFFFFFFFL; // = 4611686018427387903 = sqrt(2^127 -1)/2 - // So had to adjust them to more suitable for Javascript. - // If JS some day supports truly 64-bit integers, then these ranges can be as in C# - // and biginteger library can be more simpler (as then 128bit can be represented as two 64bit numbers) - ClipperLib.ClipperBase.horizontal = -9007199254740992; //-2^53 - ClipperLib.ClipperBase.Skip = -2; - ClipperLib.ClipperBase.Unassigned = -1; - ClipperLib.ClipperBase.tolerance = 1E-20; - if (use_int32) - { - ClipperLib.ClipperBase.loRange = 46340; - ClipperLib.ClipperBase.hiRange = 46340; - } - else - { - ClipperLib.ClipperBase.loRange = 47453132; // sqrt(2^53 -1)/2 - ClipperLib.ClipperBase.hiRange = 4503599627370495; // sqrt(2^106 -1)/2 - } - ClipperLib.ClipperBase.near_zero = function (val) - { - return (val > -ClipperLib.ClipperBase.tolerance) && (val < ClipperLib.ClipperBase.tolerance); - }; - ClipperLib.ClipperBase.IsHorizontal = function (e) - { - return e.Delta.Y === 0; - }; - ClipperLib.ClipperBase.prototype.PointIsVertex = function (pt, pp) - { - var pp2 = pp; - do { - if (ClipperLib.IntPoint.op_Equality(pp2.Pt, pt)) - return true; - pp2 = pp2.Next; - } - while (pp2 != pp) - return false; - }; - ClipperLib.ClipperBase.prototype.PointOnLineSegment = function (pt, linePt1, linePt2, UseFullRange) - { - if (UseFullRange) - return ((pt.X == linePt1.X) && (pt.Y == linePt1.Y)) || - ((pt.X == linePt2.X) && (pt.Y == linePt2.Y)) || - (((pt.X > linePt1.X) == (pt.X < linePt2.X)) && - ((pt.Y > linePt1.Y) == (pt.Y < linePt2.Y)) && - (Int128.op_Equality(Int128.Int128Mul((pt.X - linePt1.X), (linePt2.Y - linePt1.Y)), - Int128.Int128Mul((linePt2.X - linePt1.X), (pt.Y - linePt1.Y))))); - else - return ((pt.X == linePt1.X) && (pt.Y == linePt1.Y)) || ((pt.X == linePt2.X) && (pt.Y == linePt2.Y)) || (((pt.X > linePt1.X) == (pt.X < linePt2.X)) && ((pt.Y > linePt1.Y) == (pt.Y < linePt2.Y)) && ((pt.X - linePt1.X) * (linePt2.Y - linePt1.Y) == (linePt2.X - linePt1.X) * (pt.Y - linePt1.Y))); - }; - ClipperLib.ClipperBase.prototype.PointOnPolygon = function (pt, pp, UseFullRange) - { - var pp2 = pp; - while (true) - { - if (this.PointOnLineSegment(pt, pp2.Pt, pp2.Next.Pt, UseFullRange)) - return true; - pp2 = pp2.Next; - if (pp2 == pp) - break; - } - return false; - }; - ClipperLib.ClipperBase.prototype.SlopesEqual = ClipperLib.ClipperBase.SlopesEqual = function () - { - var a = arguments, - alen = a.length; - var e1, e2, pt1, pt2, pt3, pt4, UseFullRange; - if (alen == 3) // function (e1, e2, UseFullRange) - { - e1 = a[0]; - e2 = a[1]; - UseFullRange = a[2]; - if (UseFullRange) - return Int128.op_Equality(Int128.Int128Mul(e1.Delta.Y, e2.Delta.X), Int128.Int128Mul(e1.Delta.X, e2.Delta.Y)); - else - return ClipperLib.Cast_Int64((e1.Delta.Y) * (e2.Delta.X)) == ClipperLib.Cast_Int64((e1.Delta.X) * (e2.Delta.Y)); - } - else if (alen == 4) // function (pt1, pt2, pt3, UseFullRange) - { - pt1 = a[0]; - pt2 = a[1]; - pt3 = a[2]; - UseFullRange = a[3]; - if (UseFullRange) - return Int128.op_Equality(Int128.Int128Mul(pt1.Y - pt2.Y, pt2.X - pt3.X), Int128.Int128Mul(pt1.X - pt2.X, pt2.Y - pt3.Y)); - else - return ClipperLib.Cast_Int64((pt1.Y - pt2.Y) * (pt2.X - pt3.X)) - ClipperLib.Cast_Int64((pt1.X - pt2.X) * (pt2.Y - pt3.Y)) === 0; - } - else // function (pt1, pt2, pt3, pt4, UseFullRange) - { - pt1 = a[0]; - pt2 = a[1]; - pt3 = a[2]; - pt4 = a[3]; - UseFullRange = a[4]; - if (UseFullRange) - return Int128.op_Equality(Int128.Int128Mul(pt1.Y - pt2.Y, pt3.X - pt4.X), Int128.Int128Mul(pt1.X - pt2.X, pt3.Y - pt4.Y)); - else - return ClipperLib.Cast_Int64((pt1.Y - pt2.Y) * (pt3.X - pt4.X)) - ClipperLib.Cast_Int64((pt1.X - pt2.X) * (pt3.Y - pt4.Y)) === 0; - } - }; - ClipperLib.ClipperBase.SlopesEqual3 = function (e1, e2, UseFullRange) - { - if (UseFullRange) - return Int128.op_Equality(Int128.Int128Mul(e1.Delta.Y, e2.Delta.X), Int128.Int128Mul(e1.Delta.X, e2.Delta.Y)); - else - return ClipperLib.Cast_Int64((e1.Delta.Y) * (e2.Delta.X)) == ClipperLib.Cast_Int64((e1.Delta.X) * (e2.Delta.Y)); - }; - ClipperLib.ClipperBase.SlopesEqual4 = function (pt1, pt2, pt3, UseFullRange) - { - if (UseFullRange) - return Int128.op_Equality(Int128.Int128Mul(pt1.Y - pt2.Y, pt2.X - pt3.X), Int128.Int128Mul(pt1.X - pt2.X, pt2.Y - pt3.Y)); - else - return ClipperLib.Cast_Int64((pt1.Y - pt2.Y) * (pt2.X - pt3.X)) - ClipperLib.Cast_Int64((pt1.X - pt2.X) * (pt2.Y - pt3.Y)) === 0; - }; - ClipperLib.ClipperBase.SlopesEqual5 = function (pt1, pt2, pt3, pt4, UseFullRange) - { - if (UseFullRange) - return Int128.op_Equality(Int128.Int128Mul(pt1.Y - pt2.Y, pt3.X - pt4.X), Int128.Int128Mul(pt1.X - pt2.X, pt3.Y - pt4.Y)); - else - return ClipperLib.Cast_Int64((pt1.Y - pt2.Y) * (pt3.X - pt4.X)) - ClipperLib.Cast_Int64((pt1.X - pt2.X) * (pt3.Y - pt4.Y)) === 0; - }; - ClipperLib.ClipperBase.prototype.Clear = function () - { - this.DisposeLocalMinimaList(); - for (var i = 0, ilen = this.m_edges.length; i < ilen; ++i) - { - for (var j = 0, jlen = this.m_edges[i].length; j < jlen; ++j) - this.m_edges[i][j] = null; - ClipperLib.Clear(this.m_edges[i]); - } - ClipperLib.Clear(this.m_edges); - this.m_UseFullRange = false; - this.m_HasOpenPaths = false; - }; - ClipperLib.ClipperBase.prototype.DisposeLocalMinimaList = function () - { - while (this.m_MinimaList !== null) - { - var tmpLm = this.m_MinimaList.Next; - this.m_MinimaList = null; - this.m_MinimaList = tmpLm; - } - this.m_CurrentLM = null; - }; - ClipperLib.ClipperBase.prototype.RangeTest = function (Pt, useFullRange) - { - if (useFullRange.Value) - { - if (Pt.X > ClipperLib.ClipperBase.hiRange || Pt.Y > ClipperLib.ClipperBase.hiRange || -Pt.X > ClipperLib.ClipperBase.hiRange || -Pt.Y > ClipperLib.ClipperBase.hiRange) - ClipperLib.Error("Coordinate outside allowed range in RangeTest()."); - } - else if (Pt.X > ClipperLib.ClipperBase.loRange || Pt.Y > ClipperLib.ClipperBase.loRange || -Pt.X > ClipperLib.ClipperBase.loRange || -Pt.Y > ClipperLib.ClipperBase.loRange) - { - useFullRange.Value = true; - this.RangeTest(Pt, useFullRange); - } - }; - ClipperLib.ClipperBase.prototype.InitEdge = function (e, eNext, ePrev, pt) - { - e.Next = eNext; - e.Prev = ePrev; - //e.Curr = pt; - e.Curr.X = pt.X; - e.Curr.Y = pt.Y; - e.OutIdx = -1; - }; - ClipperLib.ClipperBase.prototype.InitEdge2 = function (e, polyType) - { - if (e.Curr.Y >= e.Next.Curr.Y) - { - //e.Bot = e.Curr; - e.Bot.X = e.Curr.X; - e.Bot.Y = e.Curr.Y; - //e.Top = e.Next.Curr; - e.Top.X = e.Next.Curr.X; - e.Top.Y = e.Next.Curr.Y; - } - else - { - //e.Top = e.Curr; - e.Top.X = e.Curr.X; - e.Top.Y = e.Curr.Y; - //e.Bot = e.Next.Curr; - e.Bot.X = e.Next.Curr.X; - e.Bot.Y = e.Next.Curr.Y; - } - this.SetDx(e); - e.PolyTyp = polyType; - }; - ClipperLib.ClipperBase.prototype.FindNextLocMin = function (E) - { - var E2; - for (;;) - { - while (ClipperLib.IntPoint.op_Inequality(E.Bot, E.Prev.Bot) || ClipperLib.IntPoint.op_Equality(E.Curr, E.Top)) - E = E.Next; - if (E.Dx != ClipperLib.ClipperBase.horizontal && E.Prev.Dx != ClipperLib.ClipperBase.horizontal) - break; - while (E.Prev.Dx == ClipperLib.ClipperBase.horizontal) - E = E.Prev; - E2 = E; - while (E.Dx == ClipperLib.ClipperBase.horizontal) - E = E.Next; - if (E.Top.Y == E.Prev.Bot.Y) - continue; - //ie just an intermediate horz. - if (E2.Prev.Bot.X < E.Bot.X) - E = E2; - break; - } - return E; - }; - ClipperLib.ClipperBase.prototype.ProcessBound = function (E, IsClockwise) - { - var EStart = E, - Result = E; - var Horz; - var StartX; - if (E.Dx == ClipperLib.ClipperBase.horizontal) - { - //it's possible for adjacent overlapping horz edges to start heading left - //before finishing right, so ... - if (IsClockwise) - StartX = E.Prev.Bot.X; - else - StartX = E.Next.Bot.X; - if (E.Bot.X != StartX) - this.ReverseHorizontal(E); - } - if (Result.OutIdx != ClipperLib.ClipperBase.Skip) - { - if (IsClockwise) - { - while (Result.Top.Y == Result.Next.Bot.Y && Result.Next.OutIdx != ClipperLib.ClipperBase.Skip) - Result = Result.Next; - if (Result.Dx == ClipperLib.ClipperBase.horizontal && Result.Next.OutIdx != ClipperLib.ClipperBase.Skip) - { - //nb: at the top of a bound, horizontals are added to the bound - //only when the preceding edge attaches to the horizontal's left vertex - //unless a Skip edge is encountered when that becomes the top divide - Horz = Result; - while (Horz.Prev.Dx == ClipperLib.ClipperBase.horizontal) - Horz = Horz.Prev; - if (Horz.Prev.Top.X == Result.Next.Top.X) - { - if (!IsClockwise) - Result = Horz.Prev; - } - else if (Horz.Prev.Top.X > Result.Next.Top.X) - Result = Horz.Prev; - } - while (E != Result) - { - E.NextInLML = E.Next; - if (E.Dx == ClipperLib.ClipperBase.horizontal && E != EStart && E.Bot.X != E.Prev.Top.X) - this.ReverseHorizontal(E); - E = E.Next; - } - if (E.Dx == ClipperLib.ClipperBase.horizontal && E != EStart && E.Bot.X != E.Prev.Top.X) - this.ReverseHorizontal(E); - Result = Result.Next; - //move to the edge just beyond current bound - } - else - { - while (Result.Top.Y == Result.Prev.Bot.Y && Result.Prev.OutIdx != ClipperLib.ClipperBase.Skip) - Result = Result.Prev; - if (Result.Dx == ClipperLib.ClipperBase.horizontal && Result.Prev.OutIdx != ClipperLib.ClipperBase.Skip) - { - Horz = Result; - while (Horz.Next.Dx == ClipperLib.ClipperBase.horizontal) - Horz = Horz.Next; - if (Horz.Next.Top.X == Result.Prev.Top.X) - { - if (!IsClockwise) - Result = Horz.Next; - } - else if (Horz.Next.Top.X > Result.Prev.Top.X) - Result = Horz.Next; - } - while (E != Result) - { - E.NextInLML = E.Prev; - if (E.Dx == ClipperLib.ClipperBase.horizontal && E != EStart && E.Bot.X != E.Next.Top.X) - this.ReverseHorizontal(E); - E = E.Prev; - } - if (E.Dx == ClipperLib.ClipperBase.horizontal && E != EStart && E.Bot.X != E.Next.Top.X) - this.ReverseHorizontal(E); - Result = Result.Prev; - //move to the edge just beyond current bound - } - } - if (Result.OutIdx == ClipperLib.ClipperBase.Skip) - { - //if edges still remain in the current bound beyond the skip edge then - //create another LocMin and call ProcessBound once more - E = Result; - if (IsClockwise) - { - while (E.Top.Y == E.Next.Bot.Y) - E = E.Next; - //don't include top horizontals when parsing a bound a second time, - //they will be contained in the opposite bound ... - while (E != Result && E.Dx == ClipperLib.ClipperBase.horizontal) - E = E.Prev; - } - else - { - while (E.Top.Y == E.Prev.Bot.Y) - E = E.Prev; - while (E != Result && E.Dx == ClipperLib.ClipperBase.horizontal) - E = E.Next; - } - if (E == Result) - { - if (IsClockwise) - Result = E.Next; - else - Result = E.Prev; - } - else - { - //there are more edges in the bound beyond result starting with E - if (IsClockwise) - E = Result.Next; - else - E = Result.Prev; - var locMin = new ClipperLib.LocalMinima(); - locMin.Next = null; - locMin.Y = E.Bot.Y; - locMin.LeftBound = null; - locMin.RightBound = E; - locMin.RightBound.WindDelta = 0; - Result = this.ProcessBound(locMin.RightBound, IsClockwise); - this.InsertLocalMinima(locMin); - } - } - return Result; - }; - ClipperLib.ClipperBase.prototype.AddPath = function (pg, polyType, Closed) - { - if (use_lines) - { - if (!Closed && polyType == ClipperLib.PolyType.ptClip) - ClipperLib.Error("AddPath: Open paths must be subject."); - } - else - { - if (!Closed) - ClipperLib.Error("AddPath: Open paths have been disabled."); - } - var highI = pg.length - 1; - if (Closed) - while (highI > 0 && (ClipperLib.IntPoint.op_Equality(pg[highI], pg[0]))) - --highI; - while (highI > 0 && (ClipperLib.IntPoint.op_Equality(pg[highI], pg[highI - 1]))) - --highI; - if ((Closed && highI < 2) || (!Closed && highI < 1)) - return false; - //create a new edge array ... - var edges = new Array(); - for (var i = 0; i <= highI; i++) - edges.push(new ClipperLib.TEdge()); - var IsFlat = true; - //1. Basic (first) edge initialization ... - - //edges[1].Curr = pg[1]; - edges[1].Curr.X = pg[1].X; - edges[1].Curr.Y = pg[1].Y; - - var $1 = {Value: this.m_UseFullRange}; - this.RangeTest(pg[0], $1); - this.m_UseFullRange = $1.Value; - - $1.Value = this.m_UseFullRange; - this.RangeTest(pg[highI], $1); - this.m_UseFullRange = $1.Value; - - this.InitEdge(edges[0], edges[1], edges[highI], pg[0]); - this.InitEdge(edges[highI], edges[0], edges[highI - 1], pg[highI]); - for (var i = highI - 1; i >= 1; --i) - { - $1.Value = this.m_UseFullRange; - this.RangeTest(pg[i], $1); - this.m_UseFullRange = $1.Value; - - this.InitEdge(edges[i], edges[i + 1], edges[i - 1], pg[i]); - } - - var eStart = edges[0]; - //2. Remove duplicate vertices, and (when closed) collinear edges ... - var E = eStart, - eLoopStop = eStart; - for (;;) - { - if (ClipperLib.IntPoint.op_Equality(E.Curr, E.Next.Curr) && (Closed || E.Next != eStart)) - { - if (E == E.Next) - break; - if (E == eStart && !Closed) - eStart = E.Next; - E = this.RemoveEdge(E); - eLoopStop = E; - continue; - } - if (E.Prev == E.Next) - break; - else if (Closed && ClipperLib.ClipperBase.SlopesEqual(E.Prev.Curr, E.Curr, E.Next.Curr, this.m_UseFullRange) && (!this.PreserveCollinear || !this.Pt2IsBetweenPt1AndPt3(E.Prev.Curr, E.Curr, E.Next.Curr))) - { - //Collinear edges are allowed for open paths but in closed paths - //the default is to merge adjacent collinear edges into a single edge. - //However, if the PreserveCollinear property is enabled, only overlapping - //collinear edges (ie spikes) will be removed from closed paths. - if (E == eStart) - eStart = E.Next; - E = this.RemoveEdge(E); - E = E.Prev; - eLoopStop = E; - continue; - } - E = E.Next; - if (E == eLoopStop) - break; - } - if ((!Closed && (E == E.Next)) || (Closed && (E.Prev == E.Next))) - return false; - if (!Closed) - { - this.m_HasOpenPaths = true; - eStart.Prev.OutIdx = ClipperLib.ClipperBase.Skip; - } - //3. Do second stage of edge initialization ... - var eHighest = eStart; - E = eStart; - do { - this.InitEdge2(E, polyType); - E = E.Next; - if (IsFlat && E.Curr.Y != eStart.Curr.Y) - IsFlat = false; - } - while (E != eStart) - //4. Finally, add edge bounds to LocalMinima list ... - //Totally flat paths must be handled differently when adding them - //to LocalMinima list to avoid endless loops etc ... - if (IsFlat) - { - if (Closed) - return false; - E.Prev.OutIdx = ClipperLib.ClipperBase.Skip; - if (E.Prev.Bot.X < E.Prev.Top.X) - this.ReverseHorizontal(E.Prev); - var locMin = new ClipperLib.LocalMinima(); - locMin.Next = null; - locMin.Y = E.Bot.Y; - locMin.LeftBound = null; - locMin.RightBound = E; - locMin.RightBound.Side = ClipperLib.EdgeSide.esRight; - locMin.RightBound.WindDelta = 0; - while (E.Next.OutIdx != ClipperLib.ClipperBase.Skip) - { - E.NextInLML = E.Next; - if (E.Bot.X != E.Prev.Top.X) - this.ReverseHorizontal(E); - E = E.Next; - } - this.InsertLocalMinima(locMin); - this.m_edges.push(edges); - return true; - } - this.m_edges.push(edges); - var clockwise; - var EMin = null; - for (;;) - { - E = this.FindNextLocMin(E); - if (E == EMin) - break; - else if (EMin == null) - EMin = E; - //E and E.Prev now share a local minima (left aligned if horizontal). - //Compare their slopes to find which starts which bound ... - var locMin = new ClipperLib.LocalMinima(); - locMin.Next = null; - locMin.Y = E.Bot.Y; - if (E.Dx < E.Prev.Dx) - { - locMin.LeftBound = E.Prev; - locMin.RightBound = E; - clockwise = false; - //Q.nextInLML = Q.prev - } - else - { - locMin.LeftBound = E; - locMin.RightBound = E.Prev; - clockwise = true; - //Q.nextInLML = Q.next - } - locMin.LeftBound.Side = ClipperLib.EdgeSide.esLeft; - locMin.RightBound.Side = ClipperLib.EdgeSide.esRight; - if (!Closed) - locMin.LeftBound.WindDelta = 0; - else if (locMin.LeftBound.Next == locMin.RightBound) - locMin.LeftBound.WindDelta = -1; - else - locMin.LeftBound.WindDelta = 1; - locMin.RightBound.WindDelta = -locMin.LeftBound.WindDelta; - E = this.ProcessBound(locMin.LeftBound, clockwise); - var E2 = this.ProcessBound(locMin.RightBound, !clockwise); - if (locMin.LeftBound.OutIdx == ClipperLib.ClipperBase.Skip) - locMin.LeftBound = null; - else if (locMin.RightBound.OutIdx == ClipperLib.ClipperBase.Skip) - locMin.RightBound = null; - this.InsertLocalMinima(locMin); - if (!clockwise) - E = E2; - } - return true; - }; - ClipperLib.ClipperBase.prototype.AddPaths = function (ppg, polyType, closed) - { - // console.log("-------------------------------------------"); - // console.log(JSON.stringify(ppg)); - var result = false; - for (var i = 0, ilen = ppg.length; i < ilen; ++i) - if (this.AddPath(ppg[i], polyType, closed)) - result = true; - return result; - }; - //------------------------------------------------------------------------------ - ClipperLib.ClipperBase.prototype.Pt2IsBetweenPt1AndPt3 = function (pt1, pt2, pt3) - { - if ((ClipperLib.IntPoint.op_Equality(pt1, pt3)) || (ClipperLib.IntPoint.op_Equality(pt1, pt2)) || - (ClipperLib.IntPoint.op_Equality(pt3, pt2))) - return false; - else if (pt1.X != pt3.X) - return (pt2.X > pt1.X) == (pt2.X < pt3.X); - else - return (pt2.Y > pt1.Y) == (pt2.Y < pt3.Y); - }; - ClipperLib.ClipperBase.prototype.RemoveEdge = function (e) - { - //removes e from double_linked_list (but without removing from memory) - e.Prev.Next = e.Next; - e.Next.Prev = e.Prev; - var result = e.Next; - e.Prev = null; //flag as removed (see ClipperBase.Clear) - return result; - }; - ClipperLib.ClipperBase.prototype.SetDx = function (e) - { - e.Delta.X = (e.Top.X - e.Bot.X); - e.Delta.Y = (e.Top.Y - e.Bot.Y); - if (e.Delta.Y === 0) e.Dx = ClipperLib.ClipperBase.horizontal; - else e.Dx = (e.Delta.X) / (e.Delta.Y); - }; - ClipperLib.ClipperBase.prototype.InsertLocalMinima = function (newLm) - { - if (this.m_MinimaList === null) - { - this.m_MinimaList = newLm; - } - else if (newLm.Y >= this.m_MinimaList.Y) - { - newLm.Next = this.m_MinimaList; - this.m_MinimaList = newLm; - } - else - { - var tmpLm = this.m_MinimaList; - while (tmpLm.Next !== null && (newLm.Y < tmpLm.Next.Y)) - tmpLm = tmpLm.Next; - newLm.Next = tmpLm.Next; - tmpLm.Next = newLm; - } - }; - ClipperLib.ClipperBase.prototype.PopLocalMinima = function () - { - if (this.m_CurrentLM === null) - return; - this.m_CurrentLM = this.m_CurrentLM.Next; - }; - ClipperLib.ClipperBase.prototype.ReverseHorizontal = function (e) - { - //swap horizontal edges' top and bottom x's so they follow the natural - //progression of the bounds - ie so their xbots will align with the - //adjoining lower edge. [Helpful in the ProcessHorizontal() method.] - var tmp = e.Top.X; - e.Top.X = e.Bot.X; - e.Bot.X = tmp; - if (use_xyz) - { - tmp = e.Top.Z; - e.Top.Z = e.Bot.Z; - e.Bot.Z = tmp; - } - }; - ClipperLib.ClipperBase.prototype.Reset = function () - { - this.m_CurrentLM = this.m_MinimaList; - if (this.m_CurrentLM == null) - return; - //ie nothing to process - //reset all edges ... - var lm = this.m_MinimaList; - while (lm != null) - { - var e = lm.LeftBound; - if (e != null) - { - //e.Curr = e.Bot; - e.Curr.X = e.Bot.X; - e.Curr.Y = e.Bot.Y; - e.Side = ClipperLib.EdgeSide.esLeft; - e.OutIdx = ClipperLib.ClipperBase.Unassigned; - } - e = lm.RightBound; - if (e != null) - { - //e.Curr = e.Bot; - e.Curr.X = e.Bot.X; - e.Curr.Y = e.Bot.Y; - e.Side = ClipperLib.EdgeSide.esRight; - e.OutIdx = ClipperLib.ClipperBase.Unassigned; - } - lm = lm.Next; - } - }; - ClipperLib.Clipper = function (InitOptions) // public Clipper(int InitOptions = 0) - { - if (typeof (InitOptions) == "undefined") InitOptions = 0; - this.m_PolyOuts = null; - this.m_ClipType = ClipperLib.ClipType.ctIntersection; - this.m_Scanbeam = null; - this.m_ActiveEdges = null; - this.m_SortedEdges = null; - this.m_IntersectList = null; - this.m_IntersectNodeComparer = null; - this.m_ExecuteLocked = false; - this.m_ClipFillType = ClipperLib.PolyFillType.pftEvenOdd; - this.m_SubjFillType = ClipperLib.PolyFillType.pftEvenOdd; - this.m_Joins = null; - this.m_GhostJoins = null; - this.m_UsingPolyTree = false; - this.ReverseSolution = false; - this.StrictlySimple = false; - ClipperLib.ClipperBase.call(this); - this.m_Scanbeam = null; - this.m_ActiveEdges = null; - this.m_SortedEdges = null; - this.m_IntersectList = new Array(); - this.m_IntersectNodeComparer = ClipperLib.MyIntersectNodeSort.Compare; - this.m_ExecuteLocked = false; - this.m_UsingPolyTree = false; - this.m_PolyOuts = new Array(); - this.m_Joins = new Array(); - this.m_GhostJoins = new Array(); - this.ReverseSolution = (1 & InitOptions) !== 0; - this.StrictlySimple = (2 & InitOptions) !== 0; - this.PreserveCollinear = (4 & InitOptions) !== 0; - if (use_xyz) - { - this.ZFillFunction = null; // function (IntPoint vert1, IntPoint vert2, ref IntPoint intersectPt); - } - }; - ClipperLib.Clipper.ioReverseSolution = 1; - ClipperLib.Clipper.ioStrictlySimple = 2; - ClipperLib.Clipper.ioPreserveCollinear = 4; - - ClipperLib.Clipper.prototype.Clear = function () - { - if (this.m_edges.length === 0) - return; - //avoids problems with ClipperBase destructor - this.DisposeAllPolyPts(); - ClipperLib.ClipperBase.prototype.Clear.call(this); - }; - - ClipperLib.Clipper.prototype.DisposeScanbeamList = function () - { - while (this.m_Scanbeam !== null) - { - var sb2 = this.m_Scanbeam.Next; - this.m_Scanbeam = null; - this.m_Scanbeam = sb2; - } - }; - ClipperLib.Clipper.prototype.Reset = function () - { - ClipperLib.ClipperBase.prototype.Reset.call(this); - this.m_Scanbeam = null; - this.m_ActiveEdges = null; - this.m_SortedEdges = null; - - var lm = this.m_MinimaList; - while (lm !== null) - { - this.InsertScanbeam(lm.Y); - lm = lm.Next; - } - }; - ClipperLib.Clipper.prototype.InsertScanbeam = function (Y) - { - if (this.m_Scanbeam === null) - { - this.m_Scanbeam = new ClipperLib.Scanbeam(); - this.m_Scanbeam.Next = null; - this.m_Scanbeam.Y = Y; - } - else if (Y > this.m_Scanbeam.Y) - { - var newSb = new ClipperLib.Scanbeam(); - newSb.Y = Y; - newSb.Next = this.m_Scanbeam; - this.m_Scanbeam = newSb; - } - else - { - var sb2 = this.m_Scanbeam; - while (sb2.Next !== null && (Y <= sb2.Next.Y)) - sb2 = sb2.Next; - if (Y == sb2.Y) - return; - //ie ignores duplicates - var newSb = new ClipperLib.Scanbeam(); - newSb.Y = Y; - newSb.Next = sb2.Next; - sb2.Next = newSb; - } - }; - // ************************************ - ClipperLib.Clipper.prototype.Execute = function () - { - var a = arguments, - alen = a.length, - ispolytree = a[1] instanceof ClipperLib.PolyTree; - if (alen == 4 && !ispolytree) // function (clipType, solution, subjFillType, clipFillType) - { - var clipType = a[0], - solution = a[1], - subjFillType = a[2], - clipFillType = a[3]; - if (this.m_ExecuteLocked) - return false; - if (this.m_HasOpenPaths) - ClipperLib.Error("Error: PolyTree struct is need for open path clipping."); - this.m_ExecuteLocked = true; - ClipperLib.Clear(solution); - this.m_SubjFillType = subjFillType; - this.m_ClipFillType = clipFillType; - this.m_ClipType = clipType; - this.m_UsingPolyTree = false; - try - { - var succeeded = this.ExecuteInternal(); - //build the return polygons ... - if (succeeded) this.BuildResult(solution); - } - finally - { - this.DisposeAllPolyPts(); - this.m_ExecuteLocked = false; - } - return succeeded; - } - else if (alen == 4 && ispolytree) // function (clipType, polytree, subjFillType, clipFillType) - { - var clipType = a[0], - polytree = a[1], - subjFillType = a[2], - clipFillType = a[3]; - if (this.m_ExecuteLocked) - return false; - this.m_ExecuteLocked = true; - this.m_SubjFillType = subjFillType; - this.m_ClipFillType = clipFillType; - this.m_ClipType = clipType; - this.m_UsingPolyTree = true; - try - { - var succeeded = this.ExecuteInternal(); - //build the return polygons ... - if (succeeded) this.BuildResult2(polytree); - } - finally - { - this.DisposeAllPolyPts(); - this.m_ExecuteLocked = false; - } - return succeeded; - } - else if (alen == 2 && !ispolytree) // function (clipType, solution) - { - var clipType = a[0], - solution = a[1]; - return this.Execute(clipType, solution, ClipperLib.PolyFillType.pftEvenOdd, ClipperLib.PolyFillType.pftEvenOdd); - } - else if (alen == 2 && ispolytree) // function (clipType, polytree) - { - var clipType = a[0], - polytree = a[1]; - return this.Execute(clipType, polytree, ClipperLib.PolyFillType.pftEvenOdd, ClipperLib.PolyFillType.pftEvenOdd); - } - }; - ClipperLib.Clipper.prototype.FixHoleLinkage = function (outRec) - { - //skip if an outermost polygon or - //already already points to the correct FirstLeft ... - if (outRec.FirstLeft === null || (outRec.IsHole != outRec.FirstLeft.IsHole && outRec.FirstLeft.Pts !== null)) - return; - var orfl = outRec.FirstLeft; - while (orfl !== null && ((orfl.IsHole == outRec.IsHole) || orfl.Pts === null)) - orfl = orfl.FirstLeft; - outRec.FirstLeft = orfl; - }; - ClipperLib.Clipper.prototype.ExecuteInternal = function () - { - try - { - this.Reset(); - if (this.m_CurrentLM === null) - return false; - var botY = this.PopScanbeam(); - do { - this.InsertLocalMinimaIntoAEL(botY); - ClipperLib.Clear(this.m_GhostJoins); - this.ProcessHorizontals(false); - if (this.m_Scanbeam === null) - break; - var topY = this.PopScanbeam(); - //console.log("botY:" + botY + ", topY:" + topY); - if (!this.ProcessIntersections(botY, topY)) - return false; - this.ProcessEdgesAtTopOfScanbeam(topY); - botY = topY; - } - while (this.m_Scanbeam !== null || this.m_CurrentLM !== null) - //fix orientations ... - for (var i = 0, ilen = this.m_PolyOuts.length; i < ilen; i++) - { - var outRec = this.m_PolyOuts[i]; - if (outRec.Pts === null || outRec.IsOpen) - continue; - if ((outRec.IsHole ^ this.ReverseSolution) == (this.Area(outRec) > 0)) - this.ReversePolyPtLinks(outRec.Pts); - } - this.JoinCommonEdges(); - for (var i = 0, ilen = this.m_PolyOuts.length; i < ilen; i++) - { - var outRec = this.m_PolyOuts[i]; - if (outRec.Pts !== null && !outRec.IsOpen) - this.FixupOutPolygon(outRec); - } - if (this.StrictlySimple) - this.DoSimplePolygons(); - return true; - } - finally - { - ClipperLib.Clear(this.m_Joins); - ClipperLib.Clear(this.m_GhostJoins); - } - }; - ClipperLib.Clipper.prototype.PopScanbeam = function () - { - var Y = this.m_Scanbeam.Y; - var sb2 = this.m_Scanbeam; - this.m_Scanbeam = this.m_Scanbeam.Next; - sb2 = null; - return Y; - }; - ClipperLib.Clipper.prototype.DisposeAllPolyPts = function () - { - for (var i = 0, ilen = this.m_PolyOuts.length; i < ilen; ++i) - this.DisposeOutRec(i); - ClipperLib.Clear(this.m_PolyOuts); - }; - ClipperLib.Clipper.prototype.DisposeOutRec = function (index) - { - var outRec = this.m_PolyOuts[index]; - if (outRec.Pts !== null) - this.DisposeOutPts(outRec.Pts); - outRec = null; - this.m_PolyOuts[index] = null; - }; - ClipperLib.Clipper.prototype.DisposeOutPts = function (pp) - { - if (pp === null) - return; - var tmpPp = null; - pp.Prev.Next = null; - while (pp !== null) - { - tmpPp = pp; - pp = pp.Next; - tmpPp = null; - } - }; - ClipperLib.Clipper.prototype.AddJoin = function (Op1, Op2, OffPt) - { - var j = new ClipperLib.Join(); - j.OutPt1 = Op1; - j.OutPt2 = Op2; - //j.OffPt = OffPt; - j.OffPt.X = OffPt.X; - j.OffPt.Y = OffPt.Y; - this.m_Joins.push(j); - }; - ClipperLib.Clipper.prototype.AddGhostJoin = function (Op, OffPt) - { - var j = new ClipperLib.Join(); - j.OutPt1 = Op; - //j.OffPt = OffPt; - j.OffPt.X = OffPt.X; - j.OffPt.Y = OffPt.Y; - this.m_GhostJoins.push(j); - }; - if (use_xyz) - { - ClipperLib.Clipper.prototype.SetZ = function (pt, e) - { - pt.Z = 0; - if (this.ZFillFunction !== null) - { - //put the 'preferred' point as first parameter ... - if (e.OutIdx < 0) - this.ZFillFunction(e.Bot, e.Top, pt); //outside a path so presume entering - else - this.ZFillFunction(e.Top, e.Bot, pt); //inside a path so presume exiting - } - }; - //------------------------------------------------------------------------------ - } - ClipperLib.Clipper.prototype.InsertLocalMinimaIntoAEL = function (botY) - { - while (this.m_CurrentLM !== null && (this.m_CurrentLM.Y == botY)) - { - var lb = this.m_CurrentLM.LeftBound; - var rb = this.m_CurrentLM.RightBound; - this.PopLocalMinima(); - var Op1 = null; - if (lb === null) - { - this.InsertEdgeIntoAEL(rb, null); - this.SetWindingCount(rb); - if (this.IsContributing(rb)) - Op1 = this.AddOutPt(rb, rb.Bot); - } - else if (rb == null) - { - this.InsertEdgeIntoAEL(lb, null); - this.SetWindingCount(lb); - if (this.IsContributing(lb)) - Op1 = this.AddOutPt(lb, lb.Bot); - this.InsertScanbeam(lb.Top.Y); - } - else - { - this.InsertEdgeIntoAEL(lb, null); - this.InsertEdgeIntoAEL(rb, lb); - this.SetWindingCount(lb); - rb.WindCnt = lb.WindCnt; - rb.WindCnt2 = lb.WindCnt2; - if (this.IsContributing(lb)) - Op1 = this.AddLocalMinPoly(lb, rb, lb.Bot); - this.InsertScanbeam(lb.Top.Y); - } - if (rb != null) - { - if (ClipperLib.ClipperBase.IsHorizontal(rb)) - this.AddEdgeToSEL(rb); - else - this.InsertScanbeam(rb.Top.Y); - } - if (lb == null || rb == null) continue; - //if output polygons share an Edge with a horizontal rb, they'll need joining later ... - if (Op1 !== null && ClipperLib.ClipperBase.IsHorizontal(rb) && this.m_GhostJoins.length > 0 && rb.WindDelta !== 0) - { - for (var i = 0, ilen = this.m_GhostJoins.length; i < ilen; i++) - { - //if the horizontal Rb and a 'ghost' horizontal overlap, then convert - //the 'ghost' join to a real join ready for later ... - var j = this.m_GhostJoins[i]; - if (this.HorzSegmentsOverlap(j.OutPt1.Pt, j.OffPt, rb.Bot, rb.Top)) - this.AddJoin(j.OutPt1, Op1, j.OffPt); - } - } - if (lb.OutIdx >= 0 && lb.PrevInAEL !== null && - lb.PrevInAEL.Curr.X == lb.Bot.X && - lb.PrevInAEL.OutIdx >= 0 && - ClipperLib.ClipperBase.SlopesEqual(lb.PrevInAEL, lb, this.m_UseFullRange) && - lb.WindDelta !== 0 && lb.PrevInAEL.WindDelta !== 0) - { - var Op2 = this.AddOutPt(lb.PrevInAEL, lb.Bot); - this.AddJoin(Op1, Op2, lb.Top); - } - if (lb.NextInAEL != rb) - { - if (rb.OutIdx >= 0 && rb.PrevInAEL.OutIdx >= 0 && - ClipperLib.ClipperBase.SlopesEqual(rb.PrevInAEL, rb, this.m_UseFullRange) && - rb.WindDelta !== 0 && rb.PrevInAEL.WindDelta !== 0) - { - var Op2 = this.AddOutPt(rb.PrevInAEL, rb.Bot); - this.AddJoin(Op1, Op2, rb.Top); - } - var e = lb.NextInAEL; - if (e !== null) - while (e != rb) - { - //nb: For calculating winding counts etc, IntersectEdges() assumes - //that param1 will be to the right of param2 ABOVE the intersection ... - this.IntersectEdges(rb, e, lb.Curr, false); - //order important here - e = e.NextInAEL; - } - } - } - }; - ClipperLib.Clipper.prototype.InsertEdgeIntoAEL = function (edge, startEdge) - { - if (this.m_ActiveEdges === null) - { - edge.PrevInAEL = null; - edge.NextInAEL = null; - this.m_ActiveEdges = edge; - } - else if (startEdge === null && this.E2InsertsBeforeE1(this.m_ActiveEdges, edge)) - { - edge.PrevInAEL = null; - edge.NextInAEL = this.m_ActiveEdges; - this.m_ActiveEdges.PrevInAEL = edge; - this.m_ActiveEdges = edge; - } - else - { - if (startEdge === null) - startEdge = this.m_ActiveEdges; - while (startEdge.NextInAEL !== null && !this.E2InsertsBeforeE1(startEdge.NextInAEL, edge)) - startEdge = startEdge.NextInAEL; - edge.NextInAEL = startEdge.NextInAEL; - if (startEdge.NextInAEL !== null) - startEdge.NextInAEL.PrevInAEL = edge; - edge.PrevInAEL = startEdge; - startEdge.NextInAEL = edge; - } - }; - ClipperLib.Clipper.prototype.E2InsertsBeforeE1 = function (e1, e2) - { - if (e2.Curr.X == e1.Curr.X) - { - if (e2.Top.Y > e1.Top.Y) - return e2.Top.X < ClipperLib.Clipper.TopX(e1, e2.Top.Y); - else - return e1.Top.X > ClipperLib.Clipper.TopX(e2, e1.Top.Y); - } - else - return e2.Curr.X < e1.Curr.X; - }; - ClipperLib.Clipper.prototype.IsEvenOddFillType = function (edge) - { - if (edge.PolyTyp == ClipperLib.PolyType.ptSubject) - return this.m_SubjFillType == ClipperLib.PolyFillType.pftEvenOdd; - else - return this.m_ClipFillType == ClipperLib.PolyFillType.pftEvenOdd; - }; - ClipperLib.Clipper.prototype.IsEvenOddAltFillType = function (edge) - { - if (edge.PolyTyp == ClipperLib.PolyType.ptSubject) - return this.m_ClipFillType == ClipperLib.PolyFillType.pftEvenOdd; - else - return this.m_SubjFillType == ClipperLib.PolyFillType.pftEvenOdd; - }; - ClipperLib.Clipper.prototype.IsContributing = function (edge) - { - var pft, pft2; - if (edge.PolyTyp == ClipperLib.PolyType.ptSubject) - { - pft = this.m_SubjFillType; - pft2 = this.m_ClipFillType; - } - else - { - pft = this.m_ClipFillType; - pft2 = this.m_SubjFillType; - } - switch (pft) - { - case ClipperLib.PolyFillType.pftEvenOdd: - if (edge.WindDelta === 0 && edge.WindCnt != 1) - return false; - break; - case ClipperLib.PolyFillType.pftNonZero: - if (Math.abs(edge.WindCnt) != 1) - return false; - break; - case ClipperLib.PolyFillType.pftPositive: - if (edge.WindCnt != 1) - return false; - break; - default: - if (edge.WindCnt != -1) - return false; - break; - } - switch (this.m_ClipType) - { - case ClipperLib.ClipType.ctIntersection: - switch (pft2) - { - case ClipperLib.PolyFillType.pftEvenOdd: - case ClipperLib.PolyFillType.pftNonZero: - return (edge.WindCnt2 !== 0); - case ClipperLib.PolyFillType.pftPositive: - return (edge.WindCnt2 > 0); - default: - return (edge.WindCnt2 < 0); - } - case ClipperLib.ClipType.ctUnion: - switch (pft2) - { - case ClipperLib.PolyFillType.pftEvenOdd: - case ClipperLib.PolyFillType.pftNonZero: - return (edge.WindCnt2 === 0); - case ClipperLib.PolyFillType.pftPositive: - return (edge.WindCnt2 <= 0); - default: - return (edge.WindCnt2 >= 0); - } - case ClipperLib.ClipType.ctDifference: - if (edge.PolyTyp == ClipperLib.PolyType.ptSubject) - switch (pft2) - { - case ClipperLib.PolyFillType.pftEvenOdd: - case ClipperLib.PolyFillType.pftNonZero: - return (edge.WindCnt2 === 0); - case ClipperLib.PolyFillType.pftPositive: - return (edge.WindCnt2 <= 0); - default: - return (edge.WindCnt2 >= 0); - } - else - switch (pft2) - { - case ClipperLib.PolyFillType.pftEvenOdd: - case ClipperLib.PolyFillType.pftNonZero: - return (edge.WindCnt2 !== 0); - case ClipperLib.PolyFillType.pftPositive: - return (edge.WindCnt2 > 0); - default: - return (edge.WindCnt2 < 0); - } - case ClipperLib.ClipType.ctXor: - if (edge.WindDelta === 0) - switch (pft2) - { - case ClipperLib.PolyFillType.pftEvenOdd: - case ClipperLib.PolyFillType.pftNonZero: - return (edge.WindCnt2 === 0); - case ClipperLib.PolyFillType.pftPositive: - return (edge.WindCnt2 <= 0); - default: - return (edge.WindCnt2 >= 0); - } - else - return true; - } - return true; - }; - ClipperLib.Clipper.prototype.SetWindingCount = function (edge) - { - var e = edge.PrevInAEL; - //find the edge of the same polytype that immediately preceeds 'edge' in AEL - while (e !== null && ((e.PolyTyp != edge.PolyTyp) || (e.WindDelta === 0))) - e = e.PrevInAEL; - if (e === null) - { - edge.WindCnt = (edge.WindDelta === 0 ? 1 : edge.WindDelta); - edge.WindCnt2 = 0; - e = this.m_ActiveEdges; - //ie get ready to calc WindCnt2 - } - else if (edge.WindDelta === 0 && this.m_ClipType != ClipperLib.ClipType.ctUnion) - { - edge.WindCnt = 1; - edge.WindCnt2 = e.WindCnt2; - e = e.NextInAEL; - //ie get ready to calc WindCnt2 - } - else if (this.IsEvenOddFillType(edge)) - { - //EvenOdd filling ... - if (edge.WindDelta === 0) - { - //are we inside a subj polygon ... - var Inside = true; - var e2 = e.PrevInAEL; - while (e2 !== null) - { - if (e2.PolyTyp == e.PolyTyp && e2.WindDelta !== 0) - Inside = !Inside; - e2 = e2.PrevInAEL; - } - edge.WindCnt = (Inside ? 0 : 1); - } - else - { - edge.WindCnt = edge.WindDelta; - } - edge.WindCnt2 = e.WindCnt2; - e = e.NextInAEL; - //ie get ready to calc WindCnt2 - } - else - { - //nonZero, Positive or Negative filling ... - if (e.WindCnt * e.WindDelta < 0) - { - //prev edge is 'decreasing' WindCount (WC) toward zero - //so we're outside the previous polygon ... - if (Math.abs(e.WindCnt) > 1) - { - //outside prev poly but still inside another. - //when reversing direction of prev poly use the same WC - if (e.WindDelta * edge.WindDelta < 0) - edge.WindCnt = e.WindCnt; - else - edge.WindCnt = e.WindCnt + edge.WindDelta; - } - else - edge.WindCnt = (edge.WindDelta === 0 ? 1 : edge.WindDelta); - } - else - { - //prev edge is 'increasing' WindCount (WC) away from zero - //so we're inside the previous polygon ... - if (edge.WindDelta === 0) - edge.WindCnt = (e.WindCnt < 0 ? e.WindCnt - 1 : e.WindCnt + 1); - else if (e.WindDelta * edge.WindDelta < 0) - edge.WindCnt = e.WindCnt; - else - edge.WindCnt = e.WindCnt + edge.WindDelta; - } - edge.WindCnt2 = e.WindCnt2; - e = e.NextInAEL; - //ie get ready to calc WindCnt2 - } - //update WindCnt2 ... - if (this.IsEvenOddAltFillType(edge)) - { - //EvenOdd filling ... - while (e != edge) - { - if (e.WindDelta !== 0) - edge.WindCnt2 = (edge.WindCnt2 === 0 ? 1 : 0); - e = e.NextInAEL; - } - } - else - { - //nonZero, Positive or Negative filling ... - while (e != edge) - { - edge.WindCnt2 += e.WindDelta; - e = e.NextInAEL; - } - } - }; - ClipperLib.Clipper.prototype.AddEdgeToSEL = function (edge) - { - //SEL pointers in PEdge are reused to build a list of horizontal edges. - //However, we don't need to worry about order with horizontal edge processing. - if (this.m_SortedEdges === null) - { - this.m_SortedEdges = edge; - edge.PrevInSEL = null; - edge.NextInSEL = null; - } - else - { - edge.NextInSEL = this.m_SortedEdges; - edge.PrevInSEL = null; - this.m_SortedEdges.PrevInSEL = edge; - this.m_SortedEdges = edge; - } - }; - ClipperLib.Clipper.prototype.CopyAELToSEL = function () - { - var e = this.m_ActiveEdges; - this.m_SortedEdges = e; - while (e !== null) - { - e.PrevInSEL = e.PrevInAEL; - e.NextInSEL = e.NextInAEL; - e = e.NextInAEL; - } - }; - ClipperLib.Clipper.prototype.SwapPositionsInAEL = function (edge1, edge2) - { - //check that one or other edge hasn't already been removed from AEL ... - if (edge1.NextInAEL == edge1.PrevInAEL || edge2.NextInAEL == edge2.PrevInAEL) - return; - if (edge1.NextInAEL == edge2) - { - var next = edge2.NextInAEL; - if (next !== null) - next.PrevInAEL = edge1; - var prev = edge1.PrevInAEL; - if (prev !== null) - prev.NextInAEL = edge2; - edge2.PrevInAEL = prev; - edge2.NextInAEL = edge1; - edge1.PrevInAEL = edge2; - edge1.NextInAEL = next; - } - else if (edge2.NextInAEL == edge1) - { - var next = edge1.NextInAEL; - if (next !== null) - next.PrevInAEL = edge2; - var prev = edge2.PrevInAEL; - if (prev !== null) - prev.NextInAEL = edge1; - edge1.PrevInAEL = prev; - edge1.NextInAEL = edge2; - edge2.PrevInAEL = edge1; - edge2.NextInAEL = next; - } - else - { - var next = edge1.NextInAEL; - var prev = edge1.PrevInAEL; - edge1.NextInAEL = edge2.NextInAEL; - if (edge1.NextInAEL !== null) - edge1.NextInAEL.PrevInAEL = edge1; - edge1.PrevInAEL = edge2.PrevInAEL; - if (edge1.PrevInAEL !== null) - edge1.PrevInAEL.NextInAEL = edge1; - edge2.NextInAEL = next; - if (edge2.NextInAEL !== null) - edge2.NextInAEL.PrevInAEL = edge2; - edge2.PrevInAEL = prev; - if (edge2.PrevInAEL !== null) - edge2.PrevInAEL.NextInAEL = edge2; - } - if (edge1.PrevInAEL === null) - this.m_ActiveEdges = edge1; - else if (edge2.PrevInAEL === null) - this.m_ActiveEdges = edge2; - }; - ClipperLib.Clipper.prototype.SwapPositionsInSEL = function (edge1, edge2) - { - if (edge1.NextInSEL === null && edge1.PrevInSEL === null) - return; - if (edge2.NextInSEL === null && edge2.PrevInSEL === null) - return; - if (edge1.NextInSEL == edge2) - { - var next = edge2.NextInSEL; - if (next !== null) - next.PrevInSEL = edge1; - var prev = edge1.PrevInSEL; - if (prev !== null) - prev.NextInSEL = edge2; - edge2.PrevInSEL = prev; - edge2.NextInSEL = edge1; - edge1.PrevInSEL = edge2; - edge1.NextInSEL = next; - } - else if (edge2.NextInSEL == edge1) - { - var next = edge1.NextInSEL; - if (next !== null) - next.PrevInSEL = edge2; - var prev = edge2.PrevInSEL; - if (prev !== null) - prev.NextInSEL = edge1; - edge1.PrevInSEL = prev; - edge1.NextInSEL = edge2; - edge2.PrevInSEL = edge1; - edge2.NextInSEL = next; - } - else - { - var next = edge1.NextInSEL; - var prev = edge1.PrevInSEL; - edge1.NextInSEL = edge2.NextInSEL; - if (edge1.NextInSEL !== null) - edge1.NextInSEL.PrevInSEL = edge1; - edge1.PrevInSEL = edge2.PrevInSEL; - if (edge1.PrevInSEL !== null) - edge1.PrevInSEL.NextInSEL = edge1; - edge2.NextInSEL = next; - if (edge2.NextInSEL !== null) - edge2.NextInSEL.PrevInSEL = edge2; - edge2.PrevInSEL = prev; - if (edge2.PrevInSEL !== null) - edge2.PrevInSEL.NextInSEL = edge2; - } - if (edge1.PrevInSEL === null) - this.m_SortedEdges = edge1; - else if (edge2.PrevInSEL === null) - this.m_SortedEdges = edge2; - }; - ClipperLib.Clipper.prototype.AddLocalMaxPoly = function (e1, e2, pt) - { - this.AddOutPt(e1, pt); - if (e2.WindDelta == 0) this.AddOutPt(e2, pt); - if (e1.OutIdx == e2.OutIdx) - { - e1.OutIdx = -1; - e2.OutIdx = -1; - } - else if (e1.OutIdx < e2.OutIdx) - this.AppendPolygon(e1, e2); - else - this.AppendPolygon(e2, e1); - }; - ClipperLib.Clipper.prototype.AddLocalMinPoly = function (e1, e2, pt) - { - var result; - var e, prevE; - if (ClipperLib.ClipperBase.IsHorizontal(e2) || (e1.Dx > e2.Dx)) - { - result = this.AddOutPt(e1, pt); - e2.OutIdx = e1.OutIdx; - e1.Side = ClipperLib.EdgeSide.esLeft; - e2.Side = ClipperLib.EdgeSide.esRight; - e = e1; - if (e.PrevInAEL == e2) - prevE = e2.PrevInAEL; - else - prevE = e.PrevInAEL; - } - else - { - result = this.AddOutPt(e2, pt); - e1.OutIdx = e2.OutIdx; - e1.Side = ClipperLib.EdgeSide.esRight; - e2.Side = ClipperLib.EdgeSide.esLeft; - e = e2; - if (e.PrevInAEL == e1) - prevE = e1.PrevInAEL; - else - prevE = e.PrevInAEL; - } - if (prevE !== null && prevE.OutIdx >= 0 && (ClipperLib.Clipper.TopX(prevE, pt.Y) == ClipperLib.Clipper.TopX(e, pt.Y)) && ClipperLib.ClipperBase.SlopesEqual(e, prevE, this.m_UseFullRange) && (e.WindDelta !== 0) && (prevE.WindDelta !== 0)) - { - var outPt = this.AddOutPt(prevE, pt); - this.AddJoin(result, outPt, e.Top); - } - return result; - }; - ClipperLib.Clipper.prototype.CreateOutRec = function () - { - var result = new ClipperLib.OutRec(); - result.Idx = -1; - result.IsHole = false; - result.IsOpen = false; - result.FirstLeft = null; - result.Pts = null; - result.BottomPt = null; - result.PolyNode = null; - this.m_PolyOuts.push(result); - result.Idx = this.m_PolyOuts.length - 1; - return result; - }; - ClipperLib.Clipper.prototype.AddOutPt = function (e, pt) - { - var ToFront = (e.Side == ClipperLib.EdgeSide.esLeft); - if (e.OutIdx < 0) - { - var outRec = this.CreateOutRec(); - outRec.IsOpen = (e.WindDelta === 0); - var newOp = new ClipperLib.OutPt(); - outRec.Pts = newOp; - newOp.Idx = outRec.Idx; - //newOp.Pt = pt; - newOp.Pt.X = pt.X; - newOp.Pt.Y = pt.Y; - newOp.Next = newOp; - newOp.Prev = newOp; - if (!outRec.IsOpen) - this.SetHoleState(e, outRec); - if (use_xyz) - { - if (ClipperLib.IntPoint.op_Equality(pt, e.Bot)) - { - //newOp.Pt = e.Bot; - newOp.Pt.X = e.Bot.X; - newOp.Pt.Y = e.Bot.Y; - newOp.Pt.Z = e.Bot.Z; - } - else if (ClipperLib.IntPoint.op_Equality(pt, e.Top)) - { - //newOp.Pt = e.Top; - newOp.Pt.X = e.Top.X; - newOp.Pt.Y = e.Top.Y; - newOp.Pt.Z = e.Top.Z; - } - else - this.SetZ(newOp.Pt, e); - } - e.OutIdx = outRec.Idx; - //nb: do this after SetZ ! - return newOp; - } - else - { - var outRec = this.m_PolyOuts[e.OutIdx]; - //OutRec.Pts is the 'Left-most' point & OutRec.Pts.Prev is the 'Right-most' - var op = outRec.Pts; - if (ToFront && ClipperLib.IntPoint.op_Equality(pt, op.Pt)) - return op; - else if (!ToFront && ClipperLib.IntPoint.op_Equality(pt, op.Prev.Pt)) - return op.Prev; - var newOp = new ClipperLib.OutPt(); - newOp.Idx = outRec.Idx; - //newOp.Pt = pt; - newOp.Pt.X = pt.X; - newOp.Pt.Y = pt.Y; - newOp.Next = op; - newOp.Prev = op.Prev; - newOp.Prev.Next = newOp; - op.Prev = newOp; - if (ToFront) - outRec.Pts = newOp; - if (use_xyz) - { - if (ClipperLib.IntPoint.op_Equality(pt, e.Bot)) - { - //newOp.Pt = e.Bot; - newOp.Pt.X = e.Bot.X; - newOp.Pt.Y = e.Bot.Y; - newOp.Pt.Z = e.Bot.Z; - } - else if (ClipperLib.IntPoint.op_Equality(pt, e.Top)) - { - //newOp.Pt = e.Top; - newOp.Pt.X = e.Top.X; - newOp.Pt.Y = e.Top.Y; - newOp.Pt.Z = e.Top.Z; - } - else - this.SetZ(newOp.Pt, e); - } - return newOp; - } - }; - ClipperLib.Clipper.prototype.SwapPoints = function (pt1, pt2) - { - var tmp = new ClipperLib.IntPoint(pt1.Value); - //pt1.Value = pt2.Value; - pt1.Value.X = pt2.Value.X; - pt1.Value.Y = pt2.Value.Y; - //pt2.Value = tmp; - pt2.Value.X = tmp.X; - pt2.Value.Y = tmp.Y; - }; - ClipperLib.Clipper.prototype.HorzSegmentsOverlap = function (Pt1a, Pt1b, Pt2a, Pt2b) - { - //precondition: both segments are horizontal - if ((Pt1a.X > Pt2a.X) == (Pt1a.X < Pt2b.X)) - return true; - else if ((Pt1b.X > Pt2a.X) == (Pt1b.X < Pt2b.X)) - return true; - else if ((Pt2a.X > Pt1a.X) == (Pt2a.X < Pt1b.X)) - return true; - else if ((Pt2b.X > Pt1a.X) == (Pt2b.X < Pt1b.X)) - return true; - else if ((Pt1a.X == Pt2a.X) && (Pt1b.X == Pt2b.X)) - return true; - else if ((Pt1a.X == Pt2b.X) && (Pt1b.X == Pt2a.X)) - return true; - else - return false; - }; - ClipperLib.Clipper.prototype.InsertPolyPtBetween = function (p1, p2, pt) - { - var result = new ClipperLib.OutPt(); - //result.Pt = pt; - result.Pt.X = pt.X; - result.Pt.Y = pt.Y; - if (p2 == p1.Next) - { - p1.Next = result; - p2.Prev = result; - result.Next = p2; - result.Prev = p1; - } - else - { - p2.Next = result; - p1.Prev = result; - result.Next = p1; - result.Prev = p2; - } - return result; - }; - ClipperLib.Clipper.prototype.SetHoleState = function (e, outRec) - { - var isHole = false; - var e2 = e.PrevInAEL; - while (e2 !== null) - { - if (e2.OutIdx >= 0 && e2.WindDelta != 0) - { - isHole = !isHole; - if (outRec.FirstLeft === null) - outRec.FirstLeft = this.m_PolyOuts[e2.OutIdx]; - } - e2 = e2.PrevInAEL; - } - if (isHole) - outRec.IsHole = true; - }; - ClipperLib.Clipper.prototype.GetDx = function (pt1, pt2) - { - if (pt1.Y == pt2.Y) - return ClipperLib.ClipperBase.horizontal; - else - return (pt2.X - pt1.X) / (pt2.Y - pt1.Y); - }; - ClipperLib.Clipper.prototype.FirstIsBottomPt = function (btmPt1, btmPt2) - { - var p = btmPt1.Prev; - while ((ClipperLib.IntPoint.op_Equality(p.Pt, btmPt1.Pt)) && (p != btmPt1)) - p = p.Prev; - var dx1p = Math.abs(this.GetDx(btmPt1.Pt, p.Pt)); - p = btmPt1.Next; - while ((ClipperLib.IntPoint.op_Equality(p.Pt, btmPt1.Pt)) && (p != btmPt1)) - p = p.Next; - var dx1n = Math.abs(this.GetDx(btmPt1.Pt, p.Pt)); - p = btmPt2.Prev; - while ((ClipperLib.IntPoint.op_Equality(p.Pt, btmPt2.Pt)) && (p != btmPt2)) - p = p.Prev; - var dx2p = Math.abs(this.GetDx(btmPt2.Pt, p.Pt)); - p = btmPt2.Next; - while ((ClipperLib.IntPoint.op_Equality(p.Pt, btmPt2.Pt)) && (p != btmPt2)) - p = p.Next; - var dx2n = Math.abs(this.GetDx(btmPt2.Pt, p.Pt)); - return (dx1p >= dx2p && dx1p >= dx2n) || (dx1n >= dx2p && dx1n >= dx2n); - }; - ClipperLib.Clipper.prototype.GetBottomPt = function (pp) - { - var dups = null; - var p = pp.Next; - while (p != pp) - { - if (p.Pt.Y > pp.Pt.Y) - { - pp = p; - dups = null; - } - else if (p.Pt.Y == pp.Pt.Y && p.Pt.X <= pp.Pt.X) - { - if (p.Pt.X < pp.Pt.X) - { - dups = null; - pp = p; - } - else - { - if (p.Next != pp && p.Prev != pp) - dups = p; - } - } - p = p.Next; - } - if (dups !== null) - { - //there appears to be at least 2 vertices at bottomPt so ... - while (dups != p) - { - if (!this.FirstIsBottomPt(p, dups)) - pp = dups; - dups = dups.Next; - while (ClipperLib.IntPoint.op_Inequality(dups.Pt, pp.Pt)) - dups = dups.Next; - } - } - return pp; - }; - ClipperLib.Clipper.prototype.GetLowermostRec = function (outRec1, outRec2) - { - //work out which polygon fragment has the correct hole state ... - if (outRec1.BottomPt === null) - outRec1.BottomPt = this.GetBottomPt(outRec1.Pts); - if (outRec2.BottomPt === null) - outRec2.BottomPt = this.GetBottomPt(outRec2.Pts); - var bPt1 = outRec1.BottomPt; - var bPt2 = outRec2.BottomPt; - if (bPt1.Pt.Y > bPt2.Pt.Y) - return outRec1; - else if (bPt1.Pt.Y < bPt2.Pt.Y) - return outRec2; - else if (bPt1.Pt.X < bPt2.Pt.X) - return outRec1; - else if (bPt1.Pt.X > bPt2.Pt.X) - return outRec2; - else if (bPt1.Next == bPt1) - return outRec2; - else if (bPt2.Next == bPt2) - return outRec1; - else if (this.FirstIsBottomPt(bPt1, bPt2)) - return outRec1; - else - return outRec2; - }; - ClipperLib.Clipper.prototype.Param1RightOfParam2 = function (outRec1, outRec2) - { - do { - outRec1 = outRec1.FirstLeft; - if (outRec1 == outRec2) - return true; - } - while (outRec1 !== null) - return false; - }; - ClipperLib.Clipper.prototype.GetOutRec = function (idx) - { - var outrec = this.m_PolyOuts[idx]; - while (outrec != this.m_PolyOuts[outrec.Idx]) - outrec = this.m_PolyOuts[outrec.Idx]; - return outrec; - }; - ClipperLib.Clipper.prototype.AppendPolygon = function (e1, e2) - { - //get the start and ends of both output polygons ... - var outRec1 = this.m_PolyOuts[e1.OutIdx]; - var outRec2 = this.m_PolyOuts[e2.OutIdx]; - var holeStateRec; - if (this.Param1RightOfParam2(outRec1, outRec2)) - holeStateRec = outRec2; - else if (this.Param1RightOfParam2(outRec2, outRec1)) - holeStateRec = outRec1; - else - holeStateRec = this.GetLowermostRec(outRec1, outRec2); - var p1_lft = outRec1.Pts; - var p1_rt = p1_lft.Prev; - var p2_lft = outRec2.Pts; - var p2_rt = p2_lft.Prev; - var side; - //join e2 poly onto e1 poly and delete pointers to e2 ... - if (e1.Side == ClipperLib.EdgeSide.esLeft) - { - if (e2.Side == ClipperLib.EdgeSide.esLeft) - { - //z y x a b c - this.ReversePolyPtLinks(p2_lft); - p2_lft.Next = p1_lft; - p1_lft.Prev = p2_lft; - p1_rt.Next = p2_rt; - p2_rt.Prev = p1_rt; - outRec1.Pts = p2_rt; - } - else - { - //x y z a b c - p2_rt.Next = p1_lft; - p1_lft.Prev = p2_rt; - p2_lft.Prev = p1_rt; - p1_rt.Next = p2_lft; - outRec1.Pts = p2_lft; - } - side = ClipperLib.EdgeSide.esLeft; - } - else - { - if (e2.Side == ClipperLib.EdgeSide.esRight) - { - //a b c z y x - this.ReversePolyPtLinks(p2_lft); - p1_rt.Next = p2_rt; - p2_rt.Prev = p1_rt; - p2_lft.Next = p1_lft; - p1_lft.Prev = p2_lft; - } - else - { - //a b c x y z - p1_rt.Next = p2_lft; - p2_lft.Prev = p1_rt; - p1_lft.Prev = p2_rt; - p2_rt.Next = p1_lft; - } - side = ClipperLib.EdgeSide.esRight; - } - outRec1.BottomPt = null; - if (holeStateRec == outRec2) - { - if (outRec2.FirstLeft != outRec1) - outRec1.FirstLeft = outRec2.FirstLeft; - outRec1.IsHole = outRec2.IsHole; - } - outRec2.Pts = null; - outRec2.BottomPt = null; - outRec2.FirstLeft = outRec1; - var OKIdx = e1.OutIdx; - var ObsoleteIdx = e2.OutIdx; - e1.OutIdx = -1; - //nb: safe because we only get here via AddLocalMaxPoly - e2.OutIdx = -1; - var e = this.m_ActiveEdges; - while (e !== null) - { - if (e.OutIdx == ObsoleteIdx) - { - e.OutIdx = OKIdx; - e.Side = side; - break; - } - e = e.NextInAEL; - } - outRec2.Idx = outRec1.Idx; - }; - ClipperLib.Clipper.prototype.ReversePolyPtLinks = function (pp) - { - if (pp === null) - return; - var pp1; - var pp2; - pp1 = pp; - do { - pp2 = pp1.Next; - pp1.Next = pp1.Prev; - pp1.Prev = pp2; - pp1 = pp2; - } - while (pp1 != pp) - }; - ClipperLib.Clipper.SwapSides = function (edge1, edge2) - { - var side = edge1.Side; - edge1.Side = edge2.Side; - edge2.Side = side; - }; - ClipperLib.Clipper.SwapPolyIndexes = function (edge1, edge2) - { - var outIdx = edge1.OutIdx; - edge1.OutIdx = edge2.OutIdx; - edge2.OutIdx = outIdx; - }; - ClipperLib.Clipper.prototype.IntersectEdges = function (e1, e2, pt, protect) - { - //e1 will be to the left of e2 BELOW the intersection. Therefore e1 is before - //e2 in AEL except when e1 is being inserted at the intersection point ... - var e1stops = !protect && e1.NextInLML === null && - e1.Top.X == pt.X && e1.Top.Y == pt.Y; - var e2stops = !protect && e2.NextInLML === null && - e2.Top.X == pt.X && e2.Top.Y == pt.Y; - var e1Contributing = (e1.OutIdx >= 0); - var e2Contributing = (e2.OutIdx >= 0); - if (use_lines) - { - //if either edge is on an OPEN path ... - if (e1.WindDelta === 0 || e2.WindDelta === 0) - { - //ignore subject-subject open path intersections UNLESS they - //are both open paths, AND they are both 'contributing maximas' ... - if (e1.WindDelta === 0 && e2.WindDelta === 0) - { - if ((e1stops || e2stops) && e1Contributing && e2Contributing) - this.AddLocalMaxPoly(e1, e2, pt); - } - //if intersecting a subj line with a subj poly ... - else if (e1.PolyTyp == e2.PolyTyp && - e1.WindDelta != e2.WindDelta && this.m_ClipType == ClipperLib.ClipType.ctUnion) - { - if (e1.WindDelta === 0) - { - if (e2Contributing) - { - this.AddOutPt(e1, pt); - if (e1Contributing) - e1.OutIdx = -1; - } - } - else - { - if (e1Contributing) - { - this.AddOutPt(e2, pt); - if (e2Contributing) - e2.OutIdx = -1; - } - } - } - else if (e1.PolyTyp != e2.PolyTyp) - { - if ((e1.WindDelta === 0) && Math.abs(e2.WindCnt) == 1 && - (this.m_ClipType != ClipperLib.ClipType.ctUnion || e2.WindCnt2 === 0)) - { - this.AddOutPt(e1, pt); - if (e1Contributing) - e1.OutIdx = -1; - } - else if ((e2.WindDelta === 0) && (Math.abs(e1.WindCnt) == 1) && - (this.m_ClipType != ClipperLib.ClipType.ctUnion || e1.WindCnt2 === 0)) - { - this.AddOutPt(e2, pt); - if (e2Contributing) - e2.OutIdx = -1; - } - } - if (e1stops) - if (e1.OutIdx < 0) - this.DeleteFromAEL(e1); - else - ClipperLib.Error("Error intersecting polylines"); - if (e2stops) - if (e2.OutIdx < 0) - this.DeleteFromAEL(e2); - else - ClipperLib.Error("Error intersecting polylines"); - return; - } - } - //update winding counts... - //assumes that e1 will be to the Right of e2 ABOVE the intersection - if (e1.PolyTyp == e2.PolyTyp) - { - if (this.IsEvenOddFillType(e1)) - { - var oldE1WindCnt = e1.WindCnt; - e1.WindCnt = e2.WindCnt; - e2.WindCnt = oldE1WindCnt; - } - else - { - if (e1.WindCnt + e2.WindDelta === 0) - e1.WindCnt = -e1.WindCnt; - else - e1.WindCnt += e2.WindDelta; - if (e2.WindCnt - e1.WindDelta === 0) - e2.WindCnt = -e2.WindCnt; - else - e2.WindCnt -= e1.WindDelta; - } - } - else - { - if (!this.IsEvenOddFillType(e2)) - e1.WindCnt2 += e2.WindDelta; - else - e1.WindCnt2 = (e1.WindCnt2 === 0) ? 1 : 0; - if (!this.IsEvenOddFillType(e1)) - e2.WindCnt2 -= e1.WindDelta; - else - e2.WindCnt2 = (e2.WindCnt2 === 0) ? 1 : 0; - } - var e1FillType, e2FillType, e1FillType2, e2FillType2; - if (e1.PolyTyp == ClipperLib.PolyType.ptSubject) - { - e1FillType = this.m_SubjFillType; - e1FillType2 = this.m_ClipFillType; - } - else - { - e1FillType = this.m_ClipFillType; - e1FillType2 = this.m_SubjFillType; - } - if (e2.PolyTyp == ClipperLib.PolyType.ptSubject) - { - e2FillType = this.m_SubjFillType; - e2FillType2 = this.m_ClipFillType; - } - else - { - e2FillType = this.m_ClipFillType; - e2FillType2 = this.m_SubjFillType; - } - var e1Wc, e2Wc; - switch (e1FillType) - { - case ClipperLib.PolyFillType.pftPositive: - e1Wc = e1.WindCnt; - break; - case ClipperLib.PolyFillType.pftNegative: - e1Wc = -e1.WindCnt; - break; - default: - e1Wc = Math.abs(e1.WindCnt); - break; - } - switch (e2FillType) - { - case ClipperLib.PolyFillType.pftPositive: - e2Wc = e2.WindCnt; - break; - case ClipperLib.PolyFillType.pftNegative: - e2Wc = -e2.WindCnt; - break; - default: - e2Wc = Math.abs(e2.WindCnt); - break; - } - if (e1Contributing && e2Contributing) - { - if (e1stops || e2stops || (e1Wc !== 0 && e1Wc != 1) || (e2Wc !== 0 && e2Wc != 1) || - (e1.PolyTyp != e2.PolyTyp && this.m_ClipType != ClipperLib.ClipType.ctXor)) - this.AddLocalMaxPoly(e1, e2, pt); - else - { - this.AddOutPt(e1, pt); - this.AddOutPt(e2, pt); - ClipperLib.Clipper.SwapSides(e1, e2); - ClipperLib.Clipper.SwapPolyIndexes(e1, e2); - } - } - else if (e1Contributing) - { - if (e2Wc === 0 || e2Wc == 1) - { - this.AddOutPt(e1, pt); - ClipperLib.Clipper.SwapSides(e1, e2); - ClipperLib.Clipper.SwapPolyIndexes(e1, e2); - } - } - else if (e2Contributing) - { - if (e1Wc === 0 || e1Wc == 1) - { - this.AddOutPt(e2, pt); - ClipperLib.Clipper.SwapSides(e1, e2); - ClipperLib.Clipper.SwapPolyIndexes(e1, e2); - } - } - else if ((e1Wc === 0 || e1Wc == 1) && - (e2Wc === 0 || e2Wc == 1) && !e1stops && !e2stops) - { - //neither edge is currently contributing ... - var e1Wc2, e2Wc2; - switch (e1FillType2) - { - case ClipperLib.PolyFillType.pftPositive: - e1Wc2 = e1.WindCnt2; - break; - case ClipperLib.PolyFillType.pftNegative: - e1Wc2 = -e1.WindCnt2; - break; - default: - e1Wc2 = Math.abs(e1.WindCnt2); - break; - } - switch (e2FillType2) - { - case ClipperLib.PolyFillType.pftPositive: - e2Wc2 = e2.WindCnt2; - break; - case ClipperLib.PolyFillType.pftNegative: - e2Wc2 = -e2.WindCnt2; - break; - default: - e2Wc2 = Math.abs(e2.WindCnt2); - break; - } - if (e1.PolyTyp != e2.PolyTyp) - this.AddLocalMinPoly(e1, e2, pt); - else if (e1Wc == 1 && e2Wc == 1) - switch (this.m_ClipType) - { - case ClipperLib.ClipType.ctIntersection: - if (e1Wc2 > 0 && e2Wc2 > 0) - this.AddLocalMinPoly(e1, e2, pt); - break; - case ClipperLib.ClipType.ctUnion: - if (e1Wc2 <= 0 && e2Wc2 <= 0) - this.AddLocalMinPoly(e1, e2, pt); - break; - case ClipperLib.ClipType.ctDifference: - if (((e1.PolyTyp == ClipperLib.PolyType.ptClip) && (e1Wc2 > 0) && (e2Wc2 > 0)) || - ((e1.PolyTyp == ClipperLib.PolyType.ptSubject) && (e1Wc2 <= 0) && (e2Wc2 <= 0))) - this.AddLocalMinPoly(e1, e2, pt); - break; - case ClipperLib.ClipType.ctXor: - this.AddLocalMinPoly(e1, e2, pt); - break; - } - else - ClipperLib.Clipper.SwapSides(e1, e2); - } - if ((e1stops != e2stops) && - ((e1stops && (e1.OutIdx >= 0)) || (e2stops && (e2.OutIdx >= 0)))) - { - ClipperLib.Clipper.SwapSides(e1, e2); - ClipperLib.Clipper.SwapPolyIndexes(e1, e2); - } - //finally, delete any non-contributing maxima edges ... - if (e1stops) - this.DeleteFromAEL(e1); - if (e2stops) - this.DeleteFromAEL(e2); - }; - ClipperLib.Clipper.prototype.DeleteFromAEL = function (e) - { - var AelPrev = e.PrevInAEL; - var AelNext = e.NextInAEL; - if (AelPrev === null && AelNext === null && (e != this.m_ActiveEdges)) - return; - //already deleted - if (AelPrev !== null) - AelPrev.NextInAEL = AelNext; - else - this.m_ActiveEdges = AelNext; - if (AelNext !== null) - AelNext.PrevInAEL = AelPrev; - e.NextInAEL = null; - e.PrevInAEL = null; - }; - ClipperLib.Clipper.prototype.DeleteFromSEL = function (e) - { - var SelPrev = e.PrevInSEL; - var SelNext = e.NextInSEL; - if (SelPrev === null && SelNext === null && (e != this.m_SortedEdges)) - return; - //already deleted - if (SelPrev !== null) - SelPrev.NextInSEL = SelNext; - else - this.m_SortedEdges = SelNext; - if (SelNext !== null) - SelNext.PrevInSEL = SelPrev; - e.NextInSEL = null; - e.PrevInSEL = null; - }; - ClipperLib.Clipper.prototype.UpdateEdgeIntoAEL = function (e) - { - if (e.NextInLML === null) - ClipperLib.Error("UpdateEdgeIntoAEL: invalid call"); - var AelPrev = e.PrevInAEL; - var AelNext = e.NextInAEL; - e.NextInLML.OutIdx = e.OutIdx; - if (AelPrev !== null) - AelPrev.NextInAEL = e.NextInLML; - else - this.m_ActiveEdges = e.NextInLML; - if (AelNext !== null) - AelNext.PrevInAEL = e.NextInLML; - e.NextInLML.Side = e.Side; - e.NextInLML.WindDelta = e.WindDelta; - e.NextInLML.WindCnt = e.WindCnt; - e.NextInLML.WindCnt2 = e.WindCnt2; - e = e.NextInLML; - // e.Curr = e.Bot; - e.Curr.X = e.Bot.X; - e.Curr.Y = e.Bot.Y; - e.PrevInAEL = AelPrev; - e.NextInAEL = AelNext; - if (!ClipperLib.ClipperBase.IsHorizontal(e)) - this.InsertScanbeam(e.Top.Y); - return e; - }; - ClipperLib.Clipper.prototype.ProcessHorizontals = function (isTopOfScanbeam) - { - var horzEdge = this.m_SortedEdges; - while (horzEdge !== null) - { - this.DeleteFromSEL(horzEdge); - this.ProcessHorizontal(horzEdge, isTopOfScanbeam); - horzEdge = this.m_SortedEdges; - } - }; - ClipperLib.Clipper.prototype.GetHorzDirection = function (HorzEdge, $var) - { - if (HorzEdge.Bot.X < HorzEdge.Top.X) - { - $var.Left = HorzEdge.Bot.X; - $var.Right = HorzEdge.Top.X; - $var.Dir = ClipperLib.Direction.dLeftToRight; - } - else - { - $var.Left = HorzEdge.Top.X; - $var.Right = HorzEdge.Bot.X; - $var.Dir = ClipperLib.Direction.dRightToLeft; - } - }; - ClipperLib.Clipper.prototype.PrepareHorzJoins = function (horzEdge, isTopOfScanbeam) - { - //get the last Op for this horizontal edge - //the point may be anywhere along the horizontal ... - var outPt = this.m_PolyOuts[horzEdge.OutIdx].Pts; - if (horzEdge.Side != ClipperLib.EdgeSide.esLeft) - outPt = outPt.Prev; - //First, match up overlapping horizontal edges (eg when one polygon's - //intermediate horz edge overlaps an intermediate horz edge of another, or - //when one polygon sits on top of another) ... - //for (var i = 0, ilen = this.m_GhostJoins.length; i < ilen; ++i) { - // var j = this.m_GhostJoins[i]; - // if (this.HorzSegmentsOverlap(j.OutPt1.Pt, j.OffPt, horzEdge.Bot, horzEdge.Top)) - // this.AddJoin(j.OutPt1, outPt, j.OffPt); - //} - - //Also, since horizontal edges at the top of one SB are often removed from - //the AEL before we process the horizontal edges at the bottom of the next, - //we need to create 'ghost' Join records of 'contrubuting' horizontals that - //we can compare with horizontals at the bottom of the next SB. - if (isTopOfScanbeam) - if (ClipperLib.IntPoint.op_Equality(outPt.Pt, horzEdge.Top)) - this.AddGhostJoin(outPt, horzEdge.Bot); - else - this.AddGhostJoin(outPt, horzEdge.Top); - }; - ClipperLib.Clipper.prototype.ProcessHorizontal = function (horzEdge, isTopOfScanbeam) - { - var $var = {Dir: null, Left: null, Right: null}; - this.GetHorzDirection(horzEdge, $var); - var dir = $var.Dir; - var horzLeft = $var.Left; - var horzRight = $var.Right; - - var eLastHorz = horzEdge, - eMaxPair = null; - while (eLastHorz.NextInLML !== null && ClipperLib.ClipperBase.IsHorizontal(eLastHorz.NextInLML)) - eLastHorz = eLastHorz.NextInLML; - if (eLastHorz.NextInLML === null) - eMaxPair = this.GetMaximaPair(eLastHorz); - for (;;) - { - var IsLastHorz = (horzEdge == eLastHorz); - var e = this.GetNextInAEL(horzEdge, dir); - while (e !== null) - { - //Break if we've got to the end of an intermediate horizontal edge ... - //nb: Smaller Dx's are to the right of larger Dx's ABOVE the horizontal. - if (e.Curr.X == horzEdge.Top.X && horzEdge.NextInLML !== null && e.Dx < horzEdge.NextInLML.Dx) - break; - var eNext = this.GetNextInAEL(e, dir); - //saves eNext for later - if ((dir == ClipperLib.Direction.dLeftToRight && e.Curr.X <= horzRight) || (dir == ClipperLib.Direction.dRightToLeft && e.Curr.X >= horzLeft)) - { - - if (horzEdge.OutIdx >= 0 && horzEdge.WindDelta != 0) - this.PrepareHorzJoins(horzEdge, isTopOfScanbeam); - - //so far we're still in range of the horizontal Edge but make sure - //we're at the last of consec. horizontals when matching with eMaxPair - if (e == eMaxPair && IsLastHorz) - { - if (dir == ClipperLib.Direction.dLeftToRight) - this.IntersectEdges(horzEdge, e, e.Top, false); - else - this.IntersectEdges(e, horzEdge, e.Top, false); - if (eMaxPair.OutIdx >= 0) - ClipperLib.Error("ProcessHorizontal error"); - return; - } - else if (dir == ClipperLib.Direction.dLeftToRight) - { - var Pt = new ClipperLib.IntPoint(e.Curr.X, horzEdge.Curr.Y); - this.IntersectEdges(horzEdge, e, Pt, true); - } - else - { - var Pt = new ClipperLib.IntPoint(e.Curr.X, horzEdge.Curr.Y); - this.IntersectEdges(e, horzEdge, Pt, true); - } - this.SwapPositionsInAEL(horzEdge, e); - } - else if ((dir == ClipperLib.Direction.dLeftToRight && e.Curr.X >= horzRight) || (dir == ClipperLib.Direction.dRightToLeft && e.Curr.X <= horzLeft)) - break; - e = eNext; - } - //end while - if (horzEdge.OutIdx >= 0 && horzEdge.WindDelta !== 0) - this.PrepareHorzJoins(horzEdge, isTopOfScanbeam); - if (horzEdge.NextInLML !== null && ClipperLib.ClipperBase.IsHorizontal(horzEdge.NextInLML)) - { - horzEdge = this.UpdateEdgeIntoAEL(horzEdge); - if (horzEdge.OutIdx >= 0) - this.AddOutPt(horzEdge, horzEdge.Bot); - - var $var = {Dir: dir, Left: horzLeft, Right: horzRight}; - this.GetHorzDirection(horzEdge, $var); - dir = $var.Dir; - horzLeft = $var.Left; - horzRight = $var.Right; - } - else - break; - } - //end for (;;) - if (horzEdge.NextInLML !== null) - { - if (horzEdge.OutIdx >= 0) - { - var op1 = this.AddOutPt(horzEdge, horzEdge.Top); - horzEdge = this.UpdateEdgeIntoAEL(horzEdge); - if (horzEdge.WindDelta === 0) - return; - //nb: HorzEdge is no longer horizontal here - var ePrev = horzEdge.PrevInAEL; - var eNext = horzEdge.NextInAEL; - if (ePrev !== null && ePrev.Curr.X == horzEdge.Bot.X && - ePrev.Curr.Y == horzEdge.Bot.Y && ePrev.WindDelta !== 0 && - (ePrev.OutIdx >= 0 && ePrev.Curr.Y > ePrev.Top.Y && - ClipperLib.ClipperBase.SlopesEqual(horzEdge, ePrev, this.m_UseFullRange))) - { - var op2 = this.AddOutPt(ePrev, horzEdge.Bot); - this.AddJoin(op1, op2, horzEdge.Top); - } - else if (eNext !== null && eNext.Curr.X == horzEdge.Bot.X && - eNext.Curr.Y == horzEdge.Bot.Y && eNext.WindDelta !== 0 && - eNext.OutIdx >= 0 && eNext.Curr.Y > eNext.Top.Y && - ClipperLib.ClipperBase.SlopesEqual(horzEdge, eNext, this.m_UseFullRange)) - { - var op2 = this.AddOutPt(eNext, horzEdge.Bot); - this.AddJoin(op1, op2, horzEdge.Top); - } - } - else horzEdge = this.UpdateEdgeIntoAEL(horzEdge); - } - else if (eMaxPair !== null) - { - if (eMaxPair.OutIdx >= 0) - { - if (dir == ClipperLib.Direction.dLeftToRight) - this.IntersectEdges(horzEdge, eMaxPair, horzEdge.Top, false); - else - this.IntersectEdges(eMaxPair, horzEdge, horzEdge.Top, false); - if (eMaxPair.OutIdx >= 0) - ClipperLib.Error("ProcessHorizontal error"); - } - else - { - this.DeleteFromAEL(horzEdge); - this.DeleteFromAEL(eMaxPair); - } - } - else - { - if (horzEdge.OutIdx >= 0) - this.AddOutPt(horzEdge, horzEdge.Top); - this.DeleteFromAEL(horzEdge); - } - }; - ClipperLib.Clipper.prototype.GetNextInAEL = function (e, Direction) - { - return Direction == ClipperLib.Direction.dLeftToRight ? e.NextInAEL : e.PrevInAEL; - }; - ClipperLib.Clipper.prototype.IsMinima = function (e) - { - return e !== null && (e.Prev.NextInLML != e) && (e.Next.NextInLML != e); - }; - ClipperLib.Clipper.prototype.IsMaxima = function (e, Y) - { - return (e !== null && e.Top.Y == Y && e.NextInLML === null); - }; - ClipperLib.Clipper.prototype.IsIntermediate = function (e, Y) - { - return (e.Top.Y == Y && e.NextInLML !== null); - }; - ClipperLib.Clipper.prototype.GetMaximaPair = function (e) - { - var result = null; - if ((ClipperLib.IntPoint.op_Equality(e.Next.Top, e.Top)) && e.Next.NextInLML === null) - result = e.Next; - else if ((ClipperLib.IntPoint.op_Equality(e.Prev.Top, e.Top)) && e.Prev.NextInLML === null) - result = e.Prev; - if (result !== null && (result.OutIdx == -2 || (result.NextInAEL == result.PrevInAEL && !ClipperLib.ClipperBase.IsHorizontal(result)))) - return null; - return result; - }; - ClipperLib.Clipper.prototype.ProcessIntersections = function (botY, topY) - { - if (this.m_ActiveEdges == null) - return true; - try - { - this.BuildIntersectList(botY, topY); - if (this.m_IntersectList.length == 0) - return true; - if (this.m_IntersectList.length == 1 || this.FixupIntersectionOrder()) - this.ProcessIntersectList(); - else - return false; - } - catch ($$e2) - { - this.m_SortedEdges = null; - this.m_IntersectList.length = 0; - ClipperLib.Error("ProcessIntersections error"); - } - this.m_SortedEdges = null; - return true; - }; - ClipperLib.Clipper.prototype.BuildIntersectList = function (botY, topY) - { - if (this.m_ActiveEdges === null) - return; - //prepare for sorting ... - var e = this.m_ActiveEdges; - //console.log(JSON.stringify(JSON.decycle( e ))); - this.m_SortedEdges = e; - while (e !== null) - { - e.PrevInSEL = e.PrevInAEL; - e.NextInSEL = e.NextInAEL; - e.Curr.X = ClipperLib.Clipper.TopX(e, topY); - e = e.NextInAEL; - } - //bubblesort ... - var isModified = true; - while (isModified && this.m_SortedEdges !== null) - { - isModified = false; - e = this.m_SortedEdges; - while (e.NextInSEL !== null) - { - var eNext = e.NextInSEL; - var pt = new ClipperLib.IntPoint(); - //console.log("e.Curr.X: " + e.Curr.X + " eNext.Curr.X" + eNext.Curr.X); - if (e.Curr.X > eNext.Curr.X) - { - if (!this.IntersectPoint(e, eNext, pt) && e.Curr.X > eNext.Curr.X + 1) - { - //console.log("e.Curr.X: "+JSON.stringify(JSON.decycle( e.Curr.X ))); - //console.log("eNext.Curr.X+1: "+JSON.stringify(JSON.decycle( eNext.Curr.X+1))); - ClipperLib.Error("Intersection error"); - } - if (pt.Y > botY) - { - pt.Y = botY; - if (Math.abs(e.Dx) > Math.abs(eNext.Dx)) - pt.X = ClipperLib.Clipper.TopX(eNext, botY); - else - pt.X = ClipperLib.Clipper.TopX(e, botY); - } - var newNode = new ClipperLib.IntersectNode(); - newNode.Edge1 = e; - newNode.Edge2 = eNext; - //newNode.Pt = pt; - newNode.Pt.X = pt.X; - newNode.Pt.Y = pt.Y; - this.m_IntersectList.push(newNode); - this.SwapPositionsInSEL(e, eNext); - isModified = true; - } - else - e = eNext; - } - if (e.PrevInSEL !== null) - e.PrevInSEL.NextInSEL = null; - else - break; - } - this.m_SortedEdges = null; - }; - ClipperLib.Clipper.prototype.EdgesAdjacent = function (inode) - { - return (inode.Edge1.NextInSEL == inode.Edge2) || (inode.Edge1.PrevInSEL == inode.Edge2); - }; - ClipperLib.Clipper.IntersectNodeSort = function (node1, node2) - { - //the following typecast is safe because the differences in Pt.Y will - //be limited to the height of the scanbeam. - return (node2.Pt.Y - node1.Pt.Y); - }; - ClipperLib.Clipper.prototype.FixupIntersectionOrder = function () - { - //pre-condition: intersections are sorted bottom-most first. - //Now it's crucial that intersections are made only between adjacent edges, - //so to ensure this the order of intersections may need adjusting ... - this.m_IntersectList.sort(this.m_IntersectNodeComparer); - this.CopyAELToSEL(); - var cnt = this.m_IntersectList.length; - for (var i = 0; i < cnt; i++) - { - if (!this.EdgesAdjacent(this.m_IntersectList[i])) - { - var j = i + 1; - while (j < cnt && !this.EdgesAdjacent(this.m_IntersectList[j])) - j++; - if (j == cnt) - return false; - var tmp = this.m_IntersectList[i]; - this.m_IntersectList[i] = this.m_IntersectList[j]; - this.m_IntersectList[j] = tmp; - } - this.SwapPositionsInSEL(this.m_IntersectList[i].Edge1, this.m_IntersectList[i].Edge2); - } - return true; - }; - ClipperLib.Clipper.prototype.ProcessIntersectList = function () - { - for (var i = 0, ilen = this.m_IntersectList.length; i < ilen; i++) - { - var iNode = this.m_IntersectList[i]; - this.IntersectEdges(iNode.Edge1, iNode.Edge2, iNode.Pt, true); - this.SwapPositionsInAEL(iNode.Edge1, iNode.Edge2); - } - this.m_IntersectList.length = 0; - }; - /* - -------------------------------- - Round speedtest: http://jsperf.com/fastest-round - -------------------------------- - */ - var R1 = function (a) - { - return a < 0 ? Math.ceil(a - 0.5) : Math.round(a) - }; - var R2 = function (a) - { - return a < 0 ? Math.ceil(a - 0.5) : Math.floor(a + 0.5) - }; - var R3 = function (a) - { - return a < 0 ? -Math.round(Math.abs(a)) : Math.round(a) - }; - var R4 = function (a) - { - if (a < 0) - { - a -= 0.5; - return a < -2147483648 ? Math.ceil(a) : a | 0; - } - else - { - a += 0.5; - return a > 2147483647 ? Math.floor(a) : a | 0; - } - }; - if (browser.msie) ClipperLib.Clipper.Round = R1; - else if (browser.chromium) ClipperLib.Clipper.Round = R3; - else if (browser.safari) ClipperLib.Clipper.Round = R4; - else ClipperLib.Clipper.Round = R2; // eg. browser.chrome || browser.firefox || browser.opera - ClipperLib.Clipper.TopX = function (edge, currentY) - { - //if (edge.Bot == edge.Curr) alert ("edge.Bot = edge.Curr"); - //if (edge.Bot == edge.Top) alert ("edge.Bot = edge.Top"); - if (currentY == edge.Top.Y) - return edge.Top.X; - return edge.Bot.X + ClipperLib.Clipper.Round(edge.Dx * (currentY - edge.Bot.Y)); - }; - ClipperLib.Clipper.prototype.IntersectPoint = function (edge1, edge2, ip) - { - ip.X = 0; - ip.Y = 0; - var b1, b2; - //nb: with very large coordinate values, it's possible for SlopesEqual() to - //return false but for the edge.Dx value be equal due to double precision rounding. - if (ClipperLib.ClipperBase.SlopesEqual(edge1, edge2, this.m_UseFullRange) || edge1.Dx == edge2.Dx) - { - if (edge2.Bot.Y > edge1.Bot.Y) - { - ip.X = edge2.Bot.X; - ip.Y = edge2.Bot.Y; - } - else - { - ip.X = edge1.Bot.X; - ip.Y = edge1.Bot.Y; - } - return false; - } - else if (edge1.Delta.X === 0) - { - ip.X = edge1.Bot.X; - if (ClipperLib.ClipperBase.IsHorizontal(edge2)) - { - ip.Y = edge2.Bot.Y; - } - else - { - b2 = edge2.Bot.Y - (edge2.Bot.X / edge2.Dx); - ip.Y = ClipperLib.Clipper.Round(ip.X / edge2.Dx + b2); - } - } - else if (edge2.Delta.X === 0) - { - ip.X = edge2.Bot.X; - if (ClipperLib.ClipperBase.IsHorizontal(edge1)) - { - ip.Y = edge1.Bot.Y; - } - else - { - b1 = edge1.Bot.Y - (edge1.Bot.X / edge1.Dx); - ip.Y = ClipperLib.Clipper.Round(ip.X / edge1.Dx + b1); - } - } - else - { - b1 = edge1.Bot.X - edge1.Bot.Y * edge1.Dx; - b2 = edge2.Bot.X - edge2.Bot.Y * edge2.Dx; - var q = (b2 - b1) / (edge1.Dx - edge2.Dx); - ip.Y = ClipperLib.Clipper.Round(q); - if (Math.abs(edge1.Dx) < Math.abs(edge2.Dx)) - ip.X = ClipperLib.Clipper.Round(edge1.Dx * q + b1); - else - ip.X = ClipperLib.Clipper.Round(edge2.Dx * q + b2); - } - if (ip.Y < edge1.Top.Y || ip.Y < edge2.Top.Y) - { - if (edge1.Top.Y > edge2.Top.Y) - { - ip.Y = edge1.Top.Y; - ip.X = ClipperLib.Clipper.TopX(edge2, edge1.Top.Y); - return ip.X < edge1.Top.X; - } - else - ip.Y = edge2.Top.Y; - if (Math.abs(edge1.Dx) < Math.abs(edge2.Dx)) - ip.X = ClipperLib.Clipper.TopX(edge1, ip.Y); - else - ip.X = ClipperLib.Clipper.TopX(edge2, ip.Y); - } - return true; - }; - ClipperLib.Clipper.prototype.ProcessEdgesAtTopOfScanbeam = function (topY) - { - var e = this.m_ActiveEdges; - while (e !== null) - { - //1. process maxima, treating them as if they're 'bent' horizontal edges, - // but exclude maxima with horizontal edges. nb: e can't be a horizontal. - var IsMaximaEdge = this.IsMaxima(e, topY); - if (IsMaximaEdge) - { - var eMaxPair = this.GetMaximaPair(e); - IsMaximaEdge = (eMaxPair === null || !ClipperLib.ClipperBase.IsHorizontal(eMaxPair)); - } - if (IsMaximaEdge) - { - var ePrev = e.PrevInAEL; - this.DoMaxima(e); - if (ePrev === null) - e = this.m_ActiveEdges; - else - e = ePrev.NextInAEL; - } - else - { - //2. promote horizontal edges, otherwise update Curr.X and Curr.Y ... - if (this.IsIntermediate(e, topY) && ClipperLib.ClipperBase.IsHorizontal(e.NextInLML)) - { - e = this.UpdateEdgeIntoAEL(e); - if (e.OutIdx >= 0) - this.AddOutPt(e, e.Bot); - this.AddEdgeToSEL(e); - } - else - { - e.Curr.X = ClipperLib.Clipper.TopX(e, topY); - e.Curr.Y = topY; - } - if (this.StrictlySimple) - { - var ePrev = e.PrevInAEL; - if ((e.OutIdx >= 0) && (e.WindDelta !== 0) && ePrev !== null && - (ePrev.OutIdx >= 0) && (ePrev.Curr.X == e.Curr.X) && - (ePrev.WindDelta !== 0)) - { - var op = this.AddOutPt(ePrev, e.Curr); - var op2 = this.AddOutPt(e, e.Curr); - this.AddJoin(op, op2, e.Curr); - //StrictlySimple (type-3) join - } - } - e = e.NextInAEL; - } - } - //3. Process horizontals at the Top of the scanbeam ... - this.ProcessHorizontals(true); - //4. Promote intermediate vertices ... - e = this.m_ActiveEdges; - while (e !== null) - { - if (this.IsIntermediate(e, topY)) - { - var op = null; - if (e.OutIdx >= 0) - op = this.AddOutPt(e, e.Top); - e = this.UpdateEdgeIntoAEL(e); - //if output polygons share an edge, they'll need joining later ... - var ePrev = e.PrevInAEL; - var eNext = e.NextInAEL; - if (ePrev !== null && ePrev.Curr.X == e.Bot.X && - ePrev.Curr.Y == e.Bot.Y && op !== null && - ePrev.OutIdx >= 0 && ePrev.Curr.Y > ePrev.Top.Y && - ClipperLib.ClipperBase.SlopesEqual(e, ePrev, this.m_UseFullRange) && - (e.WindDelta !== 0) && (ePrev.WindDelta !== 0)) - { - var op2 = this.AddOutPt(ePrev, e.Bot); - this.AddJoin(op, op2, e.Top); - } - else if (eNext !== null && eNext.Curr.X == e.Bot.X && - eNext.Curr.Y == e.Bot.Y && op !== null && - eNext.OutIdx >= 0 && eNext.Curr.Y > eNext.Top.Y && - ClipperLib.ClipperBase.SlopesEqual(e, eNext, this.m_UseFullRange) && - (e.WindDelta !== 0) && (eNext.WindDelta !== 0)) - { - var op2 = this.AddOutPt(eNext, e.Bot); - this.AddJoin(op, op2, e.Top); - } - } - e = e.NextInAEL; - } - }; - ClipperLib.Clipper.prototype.DoMaxima = function (e) - { - var eMaxPair = this.GetMaximaPair(e); - if (eMaxPair === null) - { - if (e.OutIdx >= 0) - this.AddOutPt(e, e.Top); - this.DeleteFromAEL(e); - return; - } - var eNext = e.NextInAEL; - var use_lines = true; - while (eNext !== null && eNext != eMaxPair) - { - this.IntersectEdges(e, eNext, e.Top, true); - this.SwapPositionsInAEL(e, eNext); - eNext = e.NextInAEL; - } - if (e.OutIdx == -1 && eMaxPair.OutIdx == -1) - { - this.DeleteFromAEL(e); - this.DeleteFromAEL(eMaxPair); - } - else if (e.OutIdx >= 0 && eMaxPair.OutIdx >= 0) - { - this.IntersectEdges(e, eMaxPair, e.Top, false); - } - else if (use_lines && e.WindDelta === 0) - { - if (e.OutIdx >= 0) - { - this.AddOutPt(e, e.Top); - e.OutIdx = -1; - } - this.DeleteFromAEL(e); - if (eMaxPair.OutIdx >= 0) - { - this.AddOutPt(eMaxPair, e.Top); - eMaxPair.OutIdx = -1; - } - this.DeleteFromAEL(eMaxPair); - } - else - ClipperLib.Error("DoMaxima error"); - }; - ClipperLib.Clipper.ReversePaths = function (polys) - { - for (var i = 0, len = polys.length; i < len; i++) - polys[i].reverse(); - }; - ClipperLib.Clipper.Orientation = function (poly) - { - return ClipperLib.Clipper.Area(poly) >= 0; - }; - ClipperLib.Clipper.prototype.PointCount = function (pts) - { - if (pts === null) - return 0; - var result = 0; - var p = pts; - do { - result++; - p = p.Next; - } - while (p != pts) - return result; - }; - ClipperLib.Clipper.prototype.BuildResult = function (polyg) - { - ClipperLib.Clear(polyg); - for (var i = 0, ilen = this.m_PolyOuts.length; i < ilen; i++) - { - var outRec = this.m_PolyOuts[i]; - if (outRec.Pts === null) - continue; - var p = outRec.Pts.Prev; - var cnt = this.PointCount(p); - if (cnt < 2) - continue; - var pg = new Array(cnt); - for (var j = 0; j < cnt; j++) - { - pg[j] = p.Pt; - p = p.Prev; - } - polyg.push(pg); - } - }; - ClipperLib.Clipper.prototype.BuildResult2 = function (polytree) - { - polytree.Clear(); - //add each output polygon/contour to polytree ... - //polytree.m_AllPolys.set_Capacity(this.m_PolyOuts.length); - for (var i = 0, ilen = this.m_PolyOuts.length; i < ilen; i++) - { - var outRec = this.m_PolyOuts[i]; - var cnt = this.PointCount(outRec.Pts); - if ((outRec.IsOpen && cnt < 2) || (!outRec.IsOpen && cnt < 3)) - continue; - this.FixHoleLinkage(outRec); - var pn = new ClipperLib.PolyNode(); - polytree.m_AllPolys.push(pn); - outRec.PolyNode = pn; - pn.m_polygon.length = cnt; - var op = outRec.Pts.Prev; - for (var j = 0; j < cnt; j++) - { - pn.m_polygon[j] = op.Pt; - op = op.Prev; - } - } - //fixup PolyNode links etc ... - //polytree.m_Childs.set_Capacity(this.m_PolyOuts.length); - for (var i = 0, ilen = this.m_PolyOuts.length; i < ilen; i++) - { - var outRec = this.m_PolyOuts[i]; - if (outRec.PolyNode === null) - continue; - else if (outRec.IsOpen) - { - outRec.PolyNode.IsOpen = true; - polytree.AddChild(outRec.PolyNode); - } - else if (outRec.FirstLeft !== null && outRec.FirstLeft.PolyNode != null) - outRec.FirstLeft.PolyNode.AddChild(outRec.PolyNode); - else - polytree.AddChild(outRec.PolyNode); - } - }; - ClipperLib.Clipper.prototype.FixupOutPolygon = function (outRec) - { - //FixupOutPolygon() - removes duplicate points and simplifies consecutive - //parallel edges by removing the middle vertex. - var lastOK = null; - outRec.BottomPt = null; - var pp = outRec.Pts; - for (;;) - { - if (pp.Prev == pp || pp.Prev == pp.Next) - { - this.DisposeOutPts(pp); - outRec.Pts = null; - return; - } - //test for duplicate points and collinear edges ... - if ((ClipperLib.IntPoint.op_Equality(pp.Pt, pp.Next.Pt)) || (ClipperLib.IntPoint.op_Equality(pp.Pt, pp.Prev.Pt)) || - (ClipperLib.ClipperBase.SlopesEqual(pp.Prev.Pt, pp.Pt, pp.Next.Pt, this.m_UseFullRange) && - (!this.PreserveCollinear || !this.Pt2IsBetweenPt1AndPt3(pp.Prev.Pt, pp.Pt, pp.Next.Pt)))) - { - lastOK = null; - var tmp = pp; - pp.Prev.Next = pp.Next; - pp.Next.Prev = pp.Prev; - pp = pp.Prev; - tmp = null; - } - else if (pp == lastOK) - break; - else - { - if (lastOK === null) - lastOK = pp; - pp = pp.Next; - } - } - outRec.Pts = pp; - }; - ClipperLib.Clipper.prototype.DupOutPt = function (outPt, InsertAfter) - { - var result = new ClipperLib.OutPt(); - //result.Pt = outPt.Pt; - result.Pt.X = outPt.Pt.X; - result.Pt.Y = outPt.Pt.Y; - result.Idx = outPt.Idx; - if (InsertAfter) - { - result.Next = outPt.Next; - result.Prev = outPt; - outPt.Next.Prev = result; - outPt.Next = result; - } - else - { - result.Prev = outPt.Prev; - result.Next = outPt; - outPt.Prev.Next = result; - outPt.Prev = result; - } - return result; - }; - ClipperLib.Clipper.prototype.GetOverlap = function (a1, a2, b1, b2, $val) - { - if (a1 < a2) - { - if (b1 < b2) - { - $val.Left = Math.max(a1, b1); - $val.Right = Math.min(a2, b2); - } - else - { - $val.Left = Math.max(a1, b2); - $val.Right = Math.min(a2, b1); - } - } - else - { - if (b1 < b2) - { - $val.Left = Math.max(a2, b1); - $val.Right = Math.min(a1, b2); - } - else - { - $val.Left = Math.max(a2, b2); - $val.Right = Math.min(a1, b1); - } - } - return $val.Left < $val.Right; - }; - ClipperLib.Clipper.prototype.JoinHorz = function (op1, op1b, op2, op2b, Pt, DiscardLeft) - { - var Dir1 = (op1.Pt.X > op1b.Pt.X ? ClipperLib.Direction.dRightToLeft : ClipperLib.Direction.dLeftToRight); - var Dir2 = (op2.Pt.X > op2b.Pt.X ? ClipperLib.Direction.dRightToLeft : ClipperLib.Direction.dLeftToRight); - if (Dir1 == Dir2) - return false; - //When DiscardLeft, we want Op1b to be on the Left of Op1, otherwise we - //want Op1b to be on the Right. (And likewise with Op2 and Op2b.) - //So, to facilitate this while inserting Op1b and Op2b ... - //when DiscardLeft, make sure we're AT or RIGHT of Pt before adding Op1b, - //otherwise make sure we're AT or LEFT of Pt. (Likewise with Op2b.) - if (Dir1 == ClipperLib.Direction.dLeftToRight) - { - while (op1.Next.Pt.X <= Pt.X && - op1.Next.Pt.X >= op1.Pt.X && op1.Next.Pt.Y == Pt.Y) - op1 = op1.Next; - if (DiscardLeft && (op1.Pt.X != Pt.X)) - op1 = op1.Next; - op1b = this.DupOutPt(op1, !DiscardLeft); - if (ClipperLib.IntPoint.op_Inequality(op1b.Pt, Pt)) - { - op1 = op1b; - //op1.Pt = Pt; - op1.Pt.X = Pt.X; - op1.Pt.Y = Pt.Y; - op1b = this.DupOutPt(op1, !DiscardLeft); - } - } - else - { - while (op1.Next.Pt.X >= Pt.X && - op1.Next.Pt.X <= op1.Pt.X && op1.Next.Pt.Y == Pt.Y) - op1 = op1.Next; - if (!DiscardLeft && (op1.Pt.X != Pt.X)) - op1 = op1.Next; - op1b = this.DupOutPt(op1, DiscardLeft); - if (ClipperLib.IntPoint.op_Inequality(op1b.Pt, Pt)) - { - op1 = op1b; - //op1.Pt = Pt; - op1.Pt.X = Pt.X; - op1.Pt.Y = Pt.Y; - op1b = this.DupOutPt(op1, DiscardLeft); - } - } - if (Dir2 == ClipperLib.Direction.dLeftToRight) - { - while (op2.Next.Pt.X <= Pt.X && - op2.Next.Pt.X >= op2.Pt.X && op2.Next.Pt.Y == Pt.Y) - op2 = op2.Next; - if (DiscardLeft && (op2.Pt.X != Pt.X)) - op2 = op2.Next; - op2b = this.DupOutPt(op2, !DiscardLeft); - if (ClipperLib.IntPoint.op_Inequality(op2b.Pt, Pt)) - { - op2 = op2b; - //op2.Pt = Pt; - op2.Pt.X = Pt.X; - op2.Pt.Y = Pt.Y; - op2b = this.DupOutPt(op2, !DiscardLeft); - } - } - else - { - while (op2.Next.Pt.X >= Pt.X && - op2.Next.Pt.X <= op2.Pt.X && op2.Next.Pt.Y == Pt.Y) - op2 = op2.Next; - if (!DiscardLeft && (op2.Pt.X != Pt.X)) - op2 = op2.Next; - op2b = this.DupOutPt(op2, DiscardLeft); - if (ClipperLib.IntPoint.op_Inequality(op2b.Pt, Pt)) - { - op2 = op2b; - //op2.Pt = Pt; - op2.Pt.X = Pt.X; - op2.Pt.Y = Pt.Y; - op2b = this.DupOutPt(op2, DiscardLeft); - } - } - if ((Dir1 == ClipperLib.Direction.dLeftToRight) == DiscardLeft) - { - op1.Prev = op2; - op2.Next = op1; - op1b.Next = op2b; - op2b.Prev = op1b; - } - else - { - op1.Next = op2; - op2.Prev = op1; - op1b.Prev = op2b; - op2b.Next = op1b; - } - return true; - }; - ClipperLib.Clipper.prototype.JoinPoints = function (j, outRec1, outRec2) - { - var op1 = j.OutPt1, - op1b = new ClipperLib.OutPt(); - var op2 = j.OutPt2, - op2b = new ClipperLib.OutPt(); - //There are 3 kinds of joins for output polygons ... - //1. Horizontal joins where Join.OutPt1 & Join.OutPt2 are a vertices anywhere - //along (horizontal) collinear edges (& Join.OffPt is on the same horizontal). - //2. Non-horizontal joins where Join.OutPt1 & Join.OutPt2 are at the same - //location at the Bottom of the overlapping segment (& Join.OffPt is above). - //3. StrictlySimple joins where edges touch but are not collinear and where - //Join.OutPt1, Join.OutPt2 & Join.OffPt all share the same point. - var isHorizontal = (j.OutPt1.Pt.Y == j.OffPt.Y); - if (isHorizontal && (ClipperLib.IntPoint.op_Equality(j.OffPt, j.OutPt1.Pt)) && (ClipperLib.IntPoint.op_Equality(j.OffPt, j.OutPt2.Pt))) - { - //Strictly Simple join ... - op1b = j.OutPt1.Next; - while (op1b != op1 && (ClipperLib.IntPoint.op_Equality(op1b.Pt, j.OffPt))) - op1b = op1b.Next; - var reverse1 = (op1b.Pt.Y > j.OffPt.Y); - op2b = j.OutPt2.Next; - while (op2b != op2 && (ClipperLib.IntPoint.op_Equality(op2b.Pt, j.OffPt))) - op2b = op2b.Next; - var reverse2 = (op2b.Pt.Y > j.OffPt.Y); - if (reverse1 == reverse2) - return false; - if (reverse1) - { - op1b = this.DupOutPt(op1, false); - op2b = this.DupOutPt(op2, true); - op1.Prev = op2; - op2.Next = op1; - op1b.Next = op2b; - op2b.Prev = op1b; - j.OutPt1 = op1; - j.OutPt2 = op1b; - return true; - } - else - { - op1b = this.DupOutPt(op1, true); - op2b = this.DupOutPt(op2, false); - op1.Next = op2; - op2.Prev = op1; - op1b.Prev = op2b; - op2b.Next = op1b; - j.OutPt1 = op1; - j.OutPt2 = op1b; - return true; - } - } - else if (isHorizontal) - { - //treat horizontal joins differently to non-horizontal joins since with - //them we're not yet sure where the overlapping is. OutPt1.Pt & OutPt2.Pt - //may be anywhere along the horizontal edge. - op1b = op1; - while (op1.Prev.Pt.Y == op1.Pt.Y && op1.Prev != op1b && op1.Prev != op2) - op1 = op1.Prev; - while (op1b.Next.Pt.Y == op1b.Pt.Y && op1b.Next != op1 && op1b.Next != op2) - op1b = op1b.Next; - if (op1b.Next == op1 || op1b.Next == op2) - return false; - //a flat 'polygon' - op2b = op2; - while (op2.Prev.Pt.Y == op2.Pt.Y && op2.Prev != op2b && op2.Prev != op1b) - op2 = op2.Prev; - while (op2b.Next.Pt.Y == op2b.Pt.Y && op2b.Next != op2 && op2b.Next != op1) - op2b = op2b.Next; - if (op2b.Next == op2 || op2b.Next == op1) - return false; - //a flat 'polygon' - //Op1 -. Op1b & Op2 -. Op2b are the extremites of the horizontal edges - - var $val = {Left: null, Right: null}; - if (!this.GetOverlap(op1.Pt.X, op1b.Pt.X, op2.Pt.X, op2b.Pt.X, $val)) - return false; - var Left = $val.Left; - var Right = $val.Right; - - //DiscardLeftSide: when overlapping edges are joined, a spike will created - //which needs to be cleaned up. However, we don't want Op1 or Op2 caught up - //on the discard Side as either may still be needed for other joins ... - var Pt = new ClipperLib.IntPoint(); - var DiscardLeftSide; - if (op1.Pt.X >= Left && op1.Pt.X <= Right) - { - //Pt = op1.Pt; - Pt.X = op1.Pt.X; - Pt.Y = op1.Pt.Y; - DiscardLeftSide = (op1.Pt.X > op1b.Pt.X); - } - else if (op2.Pt.X >= Left && op2.Pt.X <= Right) - { - //Pt = op2.Pt; - Pt.X = op2.Pt.X; - Pt.Y = op2.Pt.Y; - DiscardLeftSide = (op2.Pt.X > op2b.Pt.X); - } - else if (op1b.Pt.X >= Left && op1b.Pt.X <= Right) - { - //Pt = op1b.Pt; - Pt.X = op1b.Pt.X; - Pt.Y = op1b.Pt.Y; - DiscardLeftSide = op1b.Pt.X > op1.Pt.X; - } - else - { - //Pt = op2b.Pt; - Pt.X = op2b.Pt.X; - Pt.Y = op2b.Pt.Y; - DiscardLeftSide = (op2b.Pt.X > op2.Pt.X); - } - j.OutPt1 = op1; - j.OutPt2 = op2; - return this.JoinHorz(op1, op1b, op2, op2b, Pt, DiscardLeftSide); - } - else - { - //nb: For non-horizontal joins ... - // 1. Jr.OutPt1.Pt.Y == Jr.OutPt2.Pt.Y - // 2. Jr.OutPt1.Pt > Jr.OffPt.Y - //make sure the polygons are correctly oriented ... - op1b = op1.Next; - while ((ClipperLib.IntPoint.op_Equality(op1b.Pt, op1.Pt)) && (op1b != op1)) - op1b = op1b.Next; - var Reverse1 = ((op1b.Pt.Y > op1.Pt.Y) || !ClipperLib.ClipperBase.SlopesEqual(op1.Pt, op1b.Pt, j.OffPt, this.m_UseFullRange)); - if (Reverse1) - { - op1b = op1.Prev; - while ((ClipperLib.IntPoint.op_Equality(op1b.Pt, op1.Pt)) && (op1b != op1)) - op1b = op1b.Prev; - if ((op1b.Pt.Y > op1.Pt.Y) || !ClipperLib.ClipperBase.SlopesEqual(op1.Pt, op1b.Pt, j.OffPt, this.m_UseFullRange)) - return false; - } - op2b = op2.Next; - while ((ClipperLib.IntPoint.op_Equality(op2b.Pt, op2.Pt)) && (op2b != op2)) - op2b = op2b.Next; - var Reverse2 = ((op2b.Pt.Y > op2.Pt.Y) || !ClipperLib.ClipperBase.SlopesEqual(op2.Pt, op2b.Pt, j.OffPt, this.m_UseFullRange)); - if (Reverse2) - { - op2b = op2.Prev; - while ((ClipperLib.IntPoint.op_Equality(op2b.Pt, op2.Pt)) && (op2b != op2)) - op2b = op2b.Prev; - if ((op2b.Pt.Y > op2.Pt.Y) || !ClipperLib.ClipperBase.SlopesEqual(op2.Pt, op2b.Pt, j.OffPt, this.m_UseFullRange)) - return false; - } - if ((op1b == op1) || (op2b == op2) || (op1b == op2b) || - ((outRec1 == outRec2) && (Reverse1 == Reverse2))) - return false; - if (Reverse1) - { - op1b = this.DupOutPt(op1, false); - op2b = this.DupOutPt(op2, true); - op1.Prev = op2; - op2.Next = op1; - op1b.Next = op2b; - op2b.Prev = op1b; - j.OutPt1 = op1; - j.OutPt2 = op1b; - return true; - } - else - { - op1b = this.DupOutPt(op1, true); - op2b = this.DupOutPt(op2, false); - op1.Next = op2; - op2.Prev = op1; - op1b.Prev = op2b; - op2b.Next = op1b; - j.OutPt1 = op1; - j.OutPt2 = op1b; - return true; - } - } - }; - ClipperLib.Clipper.GetBounds = function (paths) - { - var i = 0, - cnt = paths.length; - while (i < cnt && paths[i].length == 0) i++; - if (i == cnt) return new ClipperLib.IntRect(0, 0, 0, 0); - var result = new ClipperLib.IntRect(); - result.left = paths[i][0].X; - result.right = result.left; - result.top = paths[i][0].Y; - result.bottom = result.top; - for (; i < cnt; i++) - for (var j = 0, jlen = paths[i].length; j < jlen; j++) - { - if (paths[i][j].X < result.left) result.left = paths[i][j].X; - else if (paths[i][j].X > result.right) result.right = paths[i][j].X; - if (paths[i][j].Y < result.top) result.top = paths[i][j].Y; - else if (paths[i][j].Y > result.bottom) result.bottom = paths[i][j].Y; - } - return result; - } - ClipperLib.Clipper.prototype.GetBounds2 = function (ops) - { - var opStart = ops; - var result = new ClipperLib.IntRect(); - result.left = ops.Pt.X; - result.right = ops.Pt.X; - result.top = ops.Pt.Y; - result.bottom = ops.Pt.Y; - ops = ops.Next; - while (ops != opStart) - { - if (ops.Pt.X < result.left) - result.left = ops.Pt.X; - if (ops.Pt.X > result.right) - result.right = ops.Pt.X; - if (ops.Pt.Y < result.top) - result.top = ops.Pt.Y; - if (ops.Pt.Y > result.bottom) - result.bottom = ops.Pt.Y; - ops = ops.Next; - } - return result; - }; - - ClipperLib.Clipper.PointInPolygon = function (pt, path) - { - //returns 0 if false, +1 if true, -1 if pt ON polygon boundary - //http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.88.5498&rep=rep1&type=pdf - var result = 0, - cnt = path.length; - if (cnt < 3) - return 0; - var ip = path[0]; - for (var i = 1; i <= cnt; ++i) - { - var ipNext = (i == cnt ? path[0] : path[i]); - if (ipNext.Y == pt.Y) - { - if ((ipNext.X == pt.X) || (ip.Y == pt.Y && ((ipNext.X > pt.X) == (ip.X < pt.X)))) - return -1; - } - if ((ip.Y < pt.Y) != (ipNext.Y < pt.Y)) - { - if (ip.X >= pt.X) - { - if (ipNext.X > pt.X) - result = 1 - result; - else - { - var d = (ip.X - pt.X) * (ipNext.Y - pt.Y) - (ipNext.X - pt.X) * (ip.Y - pt.Y); - if (d == 0) - return -1; - else if ((d > 0) == (ipNext.Y > ip.Y)) - result = 1 - result; - } - } - else - { - if (ipNext.X > pt.X) - { - var d = (ip.X - pt.X) * (ipNext.Y - pt.Y) - (ipNext.X - pt.X) * (ip.Y - pt.Y); - if (d == 0) - return -1; - else if ((d > 0) == (ipNext.Y > ip.Y)) - result = 1 - result; - } - } - } - ip = ipNext; - } - return result; - }; - - ClipperLib.Clipper.prototype.PointInPolygon = function (pt, op) - { - //returns 0 if false, +1 if true, -1 if pt ON polygon boundary - //http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.88.5498&rep=rep1&type=pdf - var result = 0; - var startOp = op; - for (;;) - { - var poly0x = op.Pt.X, - poly0y = op.Pt.Y; - var poly1x = op.Next.Pt.X, - poly1y = op.Next.Pt.Y; - if (poly1y == pt.Y) - { - if ((poly1x == pt.X) || (poly0y == pt.Y && ((poly1x > pt.X) == (poly0x < pt.X)))) - return -1; - } - if ((poly0y < pt.Y) != (poly1y < pt.Y)) - { - if (poly0x >= pt.X) - { - if (poly1x > pt.X) - result = 1 - result; - else - { - var d = (poly0x - pt.X) * (poly1y - pt.Y) - (poly1x - pt.X) * (poly0y - pt.Y); - if (d == 0) - return -1; - if ((d > 0) == (poly1y > poly0y)) - result = 1 - result; - } - } - else - { - if (poly1x > pt.X) - { - var d = (poly0x - pt.X) * (poly1y - pt.Y) - (poly1x - pt.X) * (poly0y - pt.Y); - if (d == 0) - return -1; - if ((d > 0) == (poly1y > poly0y)) - result = 1 - result; - } - } - } - op = op.Next; - if (startOp == op) - break; - } - return result; - }; - - ClipperLib.Clipper.prototype.Poly2ContainsPoly1 = function (outPt1, outPt2) - { - var op = outPt1; - do { - var res = this.PointInPolygon(op.Pt, outPt2); - if (res >= 0) - return res != 0; - op = op.Next; - } - while (op != outPt1) - return true; - }; - ClipperLib.Clipper.prototype.FixupFirstLefts1 = function (OldOutRec, NewOutRec) - { - for (var i = 0, ilen = this.m_PolyOuts.length; i < ilen; i++) - { - var outRec = this.m_PolyOuts[i]; - if (outRec.Pts !== null && outRec.FirstLeft == OldOutRec) - { - if (this.Poly2ContainsPoly1(outRec.Pts, NewOutRec.Pts)) - outRec.FirstLeft = NewOutRec; - } - } - }; - ClipperLib.Clipper.prototype.FixupFirstLefts2 = function (OldOutRec, NewOutRec) - { - for (var $i2 = 0, $t2 = this.m_PolyOuts, $l2 = $t2.length, outRec = $t2[$i2]; $i2 < $l2; $i2++, outRec = $t2[$i2]) - if (outRec.FirstLeft == OldOutRec) - outRec.FirstLeft = NewOutRec; - }; - ClipperLib.Clipper.ParseFirstLeft = function (FirstLeft) - { - while (FirstLeft != null && FirstLeft.Pts == null) - FirstLeft = FirstLeft.FirstLeft; - return FirstLeft; - }; - ClipperLib.Clipper.prototype.JoinCommonEdges = function () - { - for (var i = 0, ilen = this.m_Joins.length; i < ilen; i++) - { - var join = this.m_Joins[i]; - var outRec1 = this.GetOutRec(join.OutPt1.Idx); - var outRec2 = this.GetOutRec(join.OutPt2.Idx); - if (outRec1.Pts == null || outRec2.Pts == null) - continue; - //get the polygon fragment with the correct hole state (FirstLeft) - //before calling JoinPoints() ... - var holeStateRec; - if (outRec1 == outRec2) - holeStateRec = outRec1; - else if (this.Param1RightOfParam2(outRec1, outRec2)) - holeStateRec = outRec2; - else if (this.Param1RightOfParam2(outRec2, outRec1)) - holeStateRec = outRec1; - else - holeStateRec = this.GetLowermostRec(outRec1, outRec2); - - if (!this.JoinPoints(join, outRec1, outRec2)) continue; - - if (outRec1 == outRec2) - { - //instead of joining two polygons, we've just created a new one by - //splitting one polygon into two. - outRec1.Pts = join.OutPt1; - outRec1.BottomPt = null; - outRec2 = this.CreateOutRec(); - outRec2.Pts = join.OutPt2; - //update all OutRec2.Pts Idx's ... - this.UpdateOutPtIdxs(outRec2); - //We now need to check every OutRec.FirstLeft pointer. If it points - //to OutRec1 it may need to point to OutRec2 instead ... - if (this.m_UsingPolyTree) - for (var j = 0, jlen = this.m_PolyOuts.length; j < jlen - 1; j++) - { - var oRec = this.m_PolyOuts[j]; - if (oRec.Pts == null || ClipperLib.Clipper.ParseFirstLeft(oRec.FirstLeft) != outRec1 || oRec.IsHole == outRec1.IsHole) - continue; - if (this.Poly2ContainsPoly1(oRec.Pts, join.OutPt2)) - oRec.FirstLeft = outRec2; - } - if (this.Poly2ContainsPoly1(outRec2.Pts, outRec1.Pts)) - { - //outRec2 is contained by outRec1 ... - outRec2.IsHole = !outRec1.IsHole; - outRec2.FirstLeft = outRec1; - //fixup FirstLeft pointers that may need reassigning to OutRec1 - if (this.m_UsingPolyTree) - this.FixupFirstLefts2(outRec2, outRec1); - if ((outRec2.IsHole ^ this.ReverseSolution) == (this.Area(outRec2) > 0)) - this.ReversePolyPtLinks(outRec2.Pts); - } - else if (this.Poly2ContainsPoly1(outRec1.Pts, outRec2.Pts)) - { - //outRec1 is contained by outRec2 ... - outRec2.IsHole = outRec1.IsHole; - outRec1.IsHole = !outRec2.IsHole; - outRec2.FirstLeft = outRec1.FirstLeft; - outRec1.FirstLeft = outRec2; - //fixup FirstLeft pointers that may need reassigning to OutRec1 - if (this.m_UsingPolyTree) - this.FixupFirstLefts2(outRec1, outRec2); - if ((outRec1.IsHole ^ this.ReverseSolution) == (this.Area(outRec1) > 0)) - this.ReversePolyPtLinks(outRec1.Pts); - } - else - { - //the 2 polygons are completely separate ... - outRec2.IsHole = outRec1.IsHole; - outRec2.FirstLeft = outRec1.FirstLeft; - //fixup FirstLeft pointers that may need reassigning to OutRec2 - if (this.m_UsingPolyTree) - this.FixupFirstLefts1(outRec1, outRec2); - } - } - else - { - //joined 2 polygons together ... - outRec2.Pts = null; - outRec2.BottomPt = null; - outRec2.Idx = outRec1.Idx; - outRec1.IsHole = holeStateRec.IsHole; - if (holeStateRec == outRec2) - outRec1.FirstLeft = outRec2.FirstLeft; - outRec2.FirstLeft = outRec1; - //fixup FirstLeft pointers that may need reassigning to OutRec1 - if (this.m_UsingPolyTree) - this.FixupFirstLefts2(outRec2, outRec1); - } - } - }; - ClipperLib.Clipper.prototype.UpdateOutPtIdxs = function (outrec) - { - var op = outrec.Pts; - do { - op.Idx = outrec.Idx; - op = op.Prev; - } - while (op != outrec.Pts) - }; - ClipperLib.Clipper.prototype.DoSimplePolygons = function () - { - var i = 0; - while (i < this.m_PolyOuts.length) - { - var outrec = this.m_PolyOuts[i++]; - var op = outrec.Pts; - if (op === null) - continue; - do //for each Pt in Polygon until duplicate found do ... - { - var op2 = op.Next; - while (op2 != outrec.Pts) - { - if ((ClipperLib.IntPoint.op_Equality(op.Pt, op2.Pt)) && op2.Next != op && op2.Prev != op) - { - //split the polygon into two ... - var op3 = op.Prev; - var op4 = op2.Prev; - op.Prev = op4; - op4.Next = op; - op2.Prev = op3; - op3.Next = op2; - outrec.Pts = op; - var outrec2 = this.CreateOutRec(); - outrec2.Pts = op2; - this.UpdateOutPtIdxs(outrec2); - if (this.Poly2ContainsPoly1(outrec2.Pts, outrec.Pts)) - { - //OutRec2 is contained by OutRec1 ... - outrec2.IsHole = !outrec.IsHole; - outrec2.FirstLeft = outrec; - } - else if (this.Poly2ContainsPoly1(outrec.Pts, outrec2.Pts)) - { - //OutRec1 is contained by OutRec2 ... - outrec2.IsHole = outrec.IsHole; - outrec.IsHole = !outrec2.IsHole; - outrec2.FirstLeft = outrec.FirstLeft; - outrec.FirstLeft = outrec2; - } - else - { - //the 2 polygons are separate ... - outrec2.IsHole = outrec.IsHole; - outrec2.FirstLeft = outrec.FirstLeft; - } - op2 = op; - //ie get ready for the next iteration - } - op2 = op2.Next; - } - op = op.Next; - } - while (op != outrec.Pts) - } - }; - ClipperLib.Clipper.Area = function (poly) - { - var cnt = poly.length; - if (cnt < 3) - return 0; - var a = 0; - for (var i = 0, j = cnt - 1; i < cnt; ++i) - { - a += (poly[j].X + poly[i].X) * (poly[j].Y - poly[i].Y); - j = i; - } - return -a * 0.5; - }; - ClipperLib.Clipper.prototype.Area = function (outRec) - { - var op = outRec.Pts; - if (op == null) - return 0; - var a = 0; - do { - a = a + (op.Prev.Pt.X + op.Pt.X) * (op.Prev.Pt.Y - op.Pt.Y); - op = op.Next; - } - while (op != outRec.Pts) - return a * 0.5; - }; - if (use_deprecated) - { - ClipperLib.Clipper.OffsetPaths = function (polys, delta, jointype, endtype, MiterLimit) - { - var result = new ClipperLib.Paths(); - var co = new ClipperLib.ClipperOffset(MiterLimit, MiterLimit); - co.AddPaths(polys, jointype, endtype); - co.Execute(result, delta); - return result; - }; - } - ClipperLib.Clipper.SimplifyPolygon = function (poly, fillType) - { - var result = new Array(); - var c = new ClipperLib.Clipper(0); - c.StrictlySimple = true; - c.AddPath(poly, ClipperLib.PolyType.ptSubject, true); - c.Execute(ClipperLib.ClipType.ctUnion, result, fillType, fillType); - return result; - }; - ClipperLib.Clipper.SimplifyPolygons = function (polys, fillType) - { - if (typeof (fillType) == "undefined") fillType = ClipperLib.PolyFillType.pftEvenOdd; - var result = new Array(); - var c = new ClipperLib.Clipper(0); - c.StrictlySimple = true; - c.AddPaths(polys, ClipperLib.PolyType.ptSubject, true); - c.Execute(ClipperLib.ClipType.ctUnion, result, fillType, fillType); - return result; - }; - ClipperLib.Clipper.DistanceSqrd = function (pt1, pt2) - { - var dx = (pt1.X - pt2.X); - var dy = (pt1.Y - pt2.Y); - return (dx * dx + dy * dy); - }; - ClipperLib.Clipper.DistanceFromLineSqrd = function (pt, ln1, ln2) - { - //The equation of a line in general form (Ax + By + C = 0) - //given 2 points (x¹,y¹) & (x²,y²) is ... - //(y¹ - y²)x + (x² - x¹)y + (y² - y¹)x¹ - (x² - x¹)y¹ = 0 - //A = (y¹ - y²); B = (x² - x¹); C = (y² - y¹)x¹ - (x² - x¹)y¹ - //perpendicular distance of point (x³,y³) = (Ax³ + By³ + C)/Sqrt(A² + B²) - //see http://en.wikipedia.org/wiki/Perpendicular_distance - var A = ln1.Y - ln2.Y; - var B = ln2.X - ln1.X; - var C = A * ln1.X + B * ln1.Y; - C = A * pt.X + B * pt.Y - C; - return (C * C) / (A * A + B * B); - }; - ClipperLib.Clipper.SlopesNearCollinear = function (pt1, pt2, pt3, distSqrd) - { - return ClipperLib.Clipper.DistanceFromLineSqrd(pt2, pt1, pt3) < distSqrd; - }; - ClipperLib.Clipper.PointsAreClose = function (pt1, pt2, distSqrd) - { - var dx = pt1.X - pt2.X; - var dy = pt1.Y - pt2.Y; - return ((dx * dx) + (dy * dy) <= distSqrd); - }; - //------------------------------------------------------------------------------ - ClipperLib.Clipper.ExcludeOp = function (op) - { - var result = op.Prev; - result.Next = op.Next; - op.Next.Prev = result; - result.Idx = 0; - return result; - }; - ClipperLib.Clipper.CleanPolygon = function (path, distance) - { - if (typeof (distance) == "undefined") distance = 1.415; - //distance = proximity in units/pixels below which vertices will be stripped. - //Default ~= sqrt(2) so when adjacent vertices or semi-adjacent vertices have - //both x & y coords within 1 unit, then the second vertex will be stripped. - var cnt = path.length; - if (cnt == 0) - return new Array(); - var outPts = new Array(cnt); - for (var i = 0; i < cnt; ++i) - outPts[i] = new ClipperLib.OutPt(); - for (var i = 0; i < cnt; ++i) - { - outPts[i].Pt = path[i]; - outPts[i].Next = outPts[(i + 1) % cnt]; - outPts[i].Next.Prev = outPts[i]; - outPts[i].Idx = 0; - } - var distSqrd = distance * distance; - var op = outPts[0]; - while (op.Idx == 0 && op.Next != op.Prev) - { - if (ClipperLib.Clipper.PointsAreClose(op.Pt, op.Prev.Pt, distSqrd)) - { - op = ClipperLib.Clipper.ExcludeOp(op); - cnt--; - } - else if (ClipperLib.Clipper.PointsAreClose(op.Prev.Pt, op.Next.Pt, distSqrd)) - { - ClipperLib.Clipper.ExcludeOp(op.Next); - op = ClipperLib.Clipper.ExcludeOp(op); - cnt -= 2; - } - else if (ClipperLib.Clipper.SlopesNearCollinear(op.Prev.Pt, op.Pt, op.Next.Pt, distSqrd)) - { - op = ClipperLib.Clipper.ExcludeOp(op); - cnt--; - } - else - { - op.Idx = 1; - op = op.Next; - } - } - if (cnt < 3) - cnt = 0; - var result = new Array(cnt); - for (var i = 0; i < cnt; ++i) - { - result[i] = new ClipperLib.IntPoint(op.Pt); - op = op.Next; - } - outPts = null; - return result; - }; - ClipperLib.Clipper.CleanPolygons = function (polys, distance) - { - var result = new Array(polys.length); - for (var i = 0, ilen = polys.length; i < ilen; i++) - result[i] = ClipperLib.Clipper.CleanPolygon(polys[i], distance); - return result; - }; - ClipperLib.Clipper.Minkowski = function (pattern, path, IsSum, IsClosed) - { - var delta = (IsClosed ? 1 : 0); - var polyCnt = pattern.length; - var pathCnt = path.length; - var result = new Array(); - if (IsSum) - for (var i = 0; i < pathCnt; i++) - { - var p = new Array(polyCnt); - for (var j = 0, jlen = pattern.length, ip = pattern[j]; j < jlen; j++, ip = pattern[j]) - p[j] = new ClipperLib.IntPoint(path[i].X + ip.X, path[i].Y + ip.Y); - result.push(p); - } - else - for (var i = 0; i < pathCnt; i++) - { - var p = new Array(polyCnt); - for (var j = 0, jlen = pattern.length, ip = pattern[j]; j < jlen; j++, ip = pattern[j]) - p[j] = new ClipperLib.IntPoint(path[i].X - ip.X, path[i].Y - ip.Y); - result.push(p); - } - var quads = new Array(); - for (var i = 0; i < pathCnt - 1 + delta; i++) - for (var j = 0; j < polyCnt; j++) - { - var quad = new Array(); - quad.push(result[i % pathCnt][j % polyCnt]); - quad.push(result[(i + 1) % pathCnt][j % polyCnt]); - quad.push(result[(i + 1) % pathCnt][(j + 1) % polyCnt]); - quad.push(result[i % pathCnt][(j + 1) % polyCnt]); - if (!ClipperLib.Clipper.Orientation(quad)) - quad.reverse(); - quads.push(quad); - } - var c = new ClipperLib.Clipper(0); - c.AddPaths(quads, ClipperLib.PolyType.ptSubject, true); - c.Execute(ClipperLib.ClipType.ctUnion, result, ClipperLib.PolyFillType.pftNonZero, ClipperLib.PolyFillType.pftNonZero); - return result; - }; - - ClipperLib.Clipper.MinkowskiSum = function () - { - var a = arguments, - alen = a.length; - if (alen == 3) // MinkowskiSum(Path pattern, path, pathIsClosed) - { - var pattern = a[0], - path = a[1], - pathIsClosed = a[2]; - return ClipperLib.Clipper.Minkowski(pattern, path, true, pathIsClosed); - } - else if (alen == 4) // MinkowskiSum(pattern, paths, pathFillType, pathIsClosed) - { - var pattern = a[0], - paths = a[1], - pathFillType = a[2], - pathIsClosed = a[3]; - var c = new ClipperLib.Clipper(), - tmp; - for (var i = 0, ilen = paths.length; i < ilen; ++i) - { - var tmp = ClipperLib.Clipper.Minkowski(pattern, paths[i], true, pathIsClosed); - c.AddPaths(tmp, ClipperLib.PolyType.ptSubject, true); - } - if (pathIsClosed) c.AddPaths(paths, ClipperLib.PolyType.ptClip, true); - var solution = new ClipperLib.Paths(); - c.Execute(ClipperLib.ClipType.ctUnion, solution, pathFillType, pathFillType); - return solution; - } - }; - - ClipperLib.Clipper.MinkowskiDiff = function (pattern, path, pathIsClosed) - { - return ClipperLib.Clipper.Minkowski(pattern, path, false, pathIsClosed); - }; - - ClipperLib.Clipper.PolyTreeToPaths = function (polytree) - { - var result = new Array(); - //result.set_Capacity(polytree.get_Total()); - ClipperLib.Clipper.AddPolyNodeToPaths(polytree, ClipperLib.Clipper.NodeType.ntAny, result); - return result; - }; - ClipperLib.Clipper.AddPolyNodeToPaths = function (polynode, nt, paths) - { - var match = true; - switch (nt) - { - case ClipperLib.Clipper.NodeType.ntOpen: - return; - case ClipperLib.Clipper.NodeType.ntClosed: - match = !polynode.IsOpen; - break; - default: - break; - } - if (polynode.m_polygon.length > 0 && match) - paths.push(polynode.m_polygon); - for (var $i3 = 0, $t3 = polynode.Childs(), $l3 = $t3.length, pn = $t3[$i3]; $i3 < $l3; $i3++, pn = $t3[$i3]) - ClipperLib.Clipper.AddPolyNodeToPaths(pn, nt, paths); - }; - ClipperLib.Clipper.OpenPathsFromPolyTree = function (polytree) - { - var result = new ClipperLib.Paths(); - //result.set_Capacity(polytree.ChildCount()); - for (var i = 0, ilen = polytree.ChildCount(); i < ilen; i++) - if (polytree.Childs()[i].IsOpen) - result.push(polytree.Childs()[i].m_polygon); - return result; - }; - ClipperLib.Clipper.ClosedPathsFromPolyTree = function (polytree) - { - var result = new ClipperLib.Paths(); - //result.set_Capacity(polytree.Total()); - ClipperLib.Clipper.AddPolyNodeToPaths(polytree, ClipperLib.Clipper.NodeType.ntClosed, result); - return result; - }; - Inherit(ClipperLib.Clipper, ClipperLib.ClipperBase); - ClipperLib.Clipper.NodeType = { - ntAny: 0, - ntOpen: 1, - ntClosed: 2 - }; - ClipperLib.ClipperOffset = function (miterLimit, arcTolerance) - { - if (typeof (miterLimit) == "undefined") miterLimit = 2; - if (typeof (arcTolerance) == "undefined") arcTolerance = ClipperLib.ClipperOffset.def_arc_tolerance; - this.m_destPolys = new ClipperLib.Paths(); - this.m_srcPoly = new ClipperLib.Path(); - this.m_destPoly = new ClipperLib.Path(); - this.m_normals = new Array(); - this.m_delta = 0; - this.m_sinA = 0; - this.m_sin = 0; - this.m_cos = 0; - this.m_miterLim = 0; - this.m_StepsPerRad = 0; - this.m_lowest = new ClipperLib.IntPoint(); - this.m_polyNodes = new ClipperLib.PolyNode(); - this.MiterLimit = miterLimit; - this.ArcTolerance = arcTolerance; - this.m_lowest.X = -1; - }; - ClipperLib.ClipperOffset.two_pi = 6.28318530717959; - ClipperLib.ClipperOffset.def_arc_tolerance = 0.25; - ClipperLib.ClipperOffset.prototype.Clear = function () - { - ClipperLib.Clear(this.m_polyNodes.Childs()); - this.m_lowest.X = -1; - }; - ClipperLib.ClipperOffset.Round = ClipperLib.Clipper.Round; - ClipperLib.ClipperOffset.prototype.AddPath = function (path, joinType, endType) - { - var highI = path.length - 1; - if (highI < 0) - return; - var newNode = new ClipperLib.PolyNode(); - newNode.m_jointype = joinType; - newNode.m_endtype = endType; - //strip duplicate points from path and also get index to the lowest point ... - if (endType == ClipperLib.EndType.etClosedLine || endType == ClipperLib.EndType.etClosedPolygon) - while (highI > 0 && ClipperLib.IntPoint.op_Equality(path[0], path[highI])) - highI--; - //newNode.m_polygon.set_Capacity(highI + 1); - newNode.m_polygon.push(path[0]); - var j = 0, - k = 0; - for (var i = 1; i <= highI; i++) - if (ClipperLib.IntPoint.op_Inequality(newNode.m_polygon[j], path[i])) - { - j++; - newNode.m_polygon.push(path[i]); - if (path[i].Y > newNode.m_polygon[k].Y || (path[i].Y == newNode.m_polygon[k].Y && path[i].X < newNode.m_polygon[k].X)) - k = j; - } - if ((endType == ClipperLib.EndType.etClosedPolygon && j < 2) || (endType != ClipperLib.EndType.etClosedPolygon && j < 0)) - return; - this.m_polyNodes.AddChild(newNode); - //if this path's lowest pt is lower than all the others then update m_lowest - if (endType != ClipperLib.EndType.etClosedPolygon) - return; - if (this.m_lowest.X < 0) - this.m_lowest = new ClipperLib.IntPoint(0, k); - else - { - var ip = this.m_polyNodes.Childs()[this.m_lowest.X].m_polygon[this.m_lowest.Y]; - if (newNode.m_polygon[k].Y > ip.Y || (newNode.m_polygon[k].Y == ip.Y && newNode.m_polygon[k].X < ip.X)) - this.m_lowest = new ClipperLib.IntPoint(this.m_polyNodes.ChildCount() - 1, k); - } - }; - ClipperLib.ClipperOffset.prototype.AddPaths = function (paths, joinType, endType) - { - for (var i = 0, ilen = paths.length; i < ilen; i++) - this.AddPath(paths[i], joinType, endType); - }; - ClipperLib.ClipperOffset.prototype.FixOrientations = function () - { - //fixup orientations of all closed paths if the orientation of the - //closed path with the lowermost vertex is wrong ... - if (this.m_lowest.X >= 0 && !ClipperLib.Clipper.Orientation(this.m_polyNodes.Childs()[this.m_lowest.X].m_polygon)) - { - for (var i = 0; i < this.m_polyNodes.ChildCount(); i++) - { - var node = this.m_polyNodes.Childs()[i]; - if (node.m_endtype == ClipperLib.EndType.etClosedPolygon || (node.m_endtype == ClipperLib.EndType.etClosedLine && ClipperLib.Clipper.Orientation(node.m_polygon))) - node.m_polygon.reverse(); - } - } - else - { - for (var i = 0; i < this.m_polyNodes.ChildCount(); i++) - { - var node = this.m_polyNodes.Childs()[i]; - if (node.m_endtype == ClipperLib.EndType.etClosedLine && !ClipperLib.Clipper.Orientation(node.m_polygon)) - node.m_polygon.reverse(); - } - } - }; - ClipperLib.ClipperOffset.GetUnitNormal = function (pt1, pt2) - { - var dx = (pt2.X - pt1.X); - var dy = (pt2.Y - pt1.Y); - if ((dx == 0) && (dy == 0)) - return new ClipperLib.DoublePoint(0, 0); - var f = 1 / Math.sqrt(dx * dx + dy * dy); - dx *= f; - dy *= f; - return new ClipperLib.DoublePoint(dy, -dx); - }; - ClipperLib.ClipperOffset.prototype.DoOffset = function (delta) - { - this.m_destPolys = new Array(); - this.m_delta = delta; - //if Zero offset, just copy any CLOSED polygons to m_p and return ... - if (ClipperLib.ClipperBase.near_zero(delta)) - { - //this.m_destPolys.set_Capacity(this.m_polyNodes.ChildCount); - for (var i = 0; i < this.m_polyNodes.ChildCount(); i++) - { - var node = this.m_polyNodes.Childs()[i]; - if (node.m_endtype == ClipperLib.EndType.etClosedPolygon) - this.m_destPolys.push(node.m_polygon); - } - return; - } - //see offset_triginometry3.svg in the documentation folder ... - if (this.MiterLimit > 2) - this.m_miterLim = 2 / (this.MiterLimit * this.MiterLimit); - else - this.m_miterLim = 0.5; - var y; - if (this.ArcTolerance <= 0) - y = ClipperLib.ClipperOffset.def_arc_tolerance; - else if (this.ArcTolerance > Math.abs(delta) * ClipperLib.ClipperOffset.def_arc_tolerance) - y = Math.abs(delta) * ClipperLib.ClipperOffset.def_arc_tolerance; - else - y = this.ArcTolerance; - //see offset_triginometry2.svg in the documentation folder ... - var steps = 3.14159265358979 / Math.acos(1 - y / Math.abs(delta)); - this.m_sin = Math.sin(ClipperLib.ClipperOffset.two_pi / steps); - this.m_cos = Math.cos(ClipperLib.ClipperOffset.two_pi / steps); - this.m_StepsPerRad = steps / ClipperLib.ClipperOffset.two_pi; - if (delta < 0) - this.m_sin = -this.m_sin; - //this.m_destPolys.set_Capacity(this.m_polyNodes.ChildCount * 2); - for (var i = 0; i < this.m_polyNodes.ChildCount(); i++) - { - var node = this.m_polyNodes.Childs()[i]; - this.m_srcPoly = node.m_polygon; - var len = this.m_srcPoly.length; - if (len == 0 || (delta <= 0 && (len < 3 || node.m_endtype != ClipperLib.EndType.etClosedPolygon))) - continue; - this.m_destPoly = new Array(); - if (len == 1) - { - if (node.m_jointype == ClipperLib.JoinType.jtRound) - { - var X = 1, - Y = 0; - for (var j = 1; j <= steps; j++) - { - this.m_destPoly.push(new ClipperLib.IntPoint(ClipperLib.ClipperOffset.Round(this.m_srcPoly[0].X + X * delta), ClipperLib.ClipperOffset.Round(this.m_srcPoly[0].Y + Y * delta))); - var X2 = X; - X = X * this.m_cos - this.m_sin * Y; - Y = X2 * this.m_sin + Y * this.m_cos; - } - } - else - { - var X = -1, - Y = -1; - for (var j = 0; j < 4; ++j) - { - this.m_destPoly.push(new ClipperLib.IntPoint(ClipperLib.ClipperOffset.Round(this.m_srcPoly[0].X + X * delta), ClipperLib.ClipperOffset.Round(this.m_srcPoly[0].Y + Y * delta))); - if (X < 0) - X = 1; - else if (Y < 0) - Y = 1; - else - X = -1; - } - } - this.m_destPolys.push(this.m_destPoly); - continue; - } - //build m_normals ... - this.m_normals.length = 0; - //this.m_normals.set_Capacity(len); - for (var j = 0; j < len - 1; j++) - this.m_normals.push(ClipperLib.ClipperOffset.GetUnitNormal(this.m_srcPoly[j], this.m_srcPoly[j + 1])); - if (node.m_endtype == ClipperLib.EndType.etClosedLine || node.m_endtype == ClipperLib.EndType.etClosedPolygon) - this.m_normals.push(ClipperLib.ClipperOffset.GetUnitNormal(this.m_srcPoly[len - 1], this.m_srcPoly[0])); - else - this.m_normals.push(new ClipperLib.DoublePoint(this.m_normals[len - 2])); - if (node.m_endtype == ClipperLib.EndType.etClosedPolygon) - { - var k = len - 1; - for (var j = 0; j < len; j++) - k = this.OffsetPoint(j, k, node.m_jointype); - this.m_destPolys.push(this.m_destPoly); - } - else if (node.m_endtype == ClipperLib.EndType.etClosedLine) - { - var k = len - 1; - for (var j = 0; j < len; j++) - k = this.OffsetPoint(j, k, node.m_jointype); - this.m_destPolys.push(this.m_destPoly); - this.m_destPoly = new Array(); - //re-build m_normals ... - var n = this.m_normals[len - 1]; - for (var j = len - 1; j > 0; j--) - this.m_normals[j] = new ClipperLib.DoublePoint(-this.m_normals[j - 1].X, -this.m_normals[j - 1].Y); - this.m_normals[0] = new ClipperLib.DoublePoint(-n.X, -n.Y); - k = 0; - for (var j = len - 1; j >= 0; j--) - k = this.OffsetPoint(j, k, node.m_jointype); - this.m_destPolys.push(this.m_destPoly); - } - else - { - var k = 0; - for (var j = 1; j < len - 1; ++j) - k = this.OffsetPoint(j, k, node.m_jointype); - var pt1; - if (node.m_endtype == ClipperLib.EndType.etOpenButt) - { - var j = len - 1; - pt1 = new ClipperLib.IntPoint(ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].X + this.m_normals[j].X * delta), ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].Y + this.m_normals[j].Y * delta)); - this.m_destPoly.push(pt1); - pt1 = new ClipperLib.IntPoint(ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].X - this.m_normals[j].X * delta), ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].Y - this.m_normals[j].Y * delta)); - this.m_destPoly.push(pt1); - } - else - { - var j = len - 1; - k = len - 2; - this.m_sinA = 0; - this.m_normals[j] = new ClipperLib.DoublePoint(-this.m_normals[j].X, -this.m_normals[j].Y); - if (node.m_endtype == ClipperLib.EndType.etOpenSquare) - this.DoSquare(j, k); - else - this.DoRound(j, k); - } - //re-build m_normals ... - for (var j = len - 1; j > 0; j--) - this.m_normals[j] = new ClipperLib.DoublePoint(-this.m_normals[j - 1].X, -this.m_normals[j - 1].Y); - this.m_normals[0] = new ClipperLib.DoublePoint(-this.m_normals[1].X, -this.m_normals[1].Y); - k = len - 1; - for (var j = k - 1; j > 0; --j) - k = this.OffsetPoint(j, k, node.m_jointype); - if (node.m_endtype == ClipperLib.EndType.etOpenButt) - { - pt1 = new ClipperLib.IntPoint(ClipperLib.ClipperOffset.Round(this.m_srcPoly[0].X - this.m_normals[0].X * delta), ClipperLib.ClipperOffset.Round(this.m_srcPoly[0].Y - this.m_normals[0].Y * delta)); - this.m_destPoly.push(pt1); - pt1 = new ClipperLib.IntPoint(ClipperLib.ClipperOffset.Round(this.m_srcPoly[0].X + this.m_normals[0].X * delta), ClipperLib.ClipperOffset.Round(this.m_srcPoly[0].Y + this.m_normals[0].Y * delta)); - this.m_destPoly.push(pt1); - } - else - { - k = 1; - this.m_sinA = 0; - if (node.m_endtype == ClipperLib.EndType.etOpenSquare) - this.DoSquare(0, 1); - else - this.DoRound(0, 1); - } - this.m_destPolys.push(this.m_destPoly); - } - } - }; - ClipperLib.ClipperOffset.prototype.Execute = function () - { - var a = arguments, - ispolytree = a[0] instanceof ClipperLib.PolyTree; - if (!ispolytree) // function (solution, delta) - { - var solution = a[0], - delta = a[1]; - ClipperLib.Clear(solution); - this.FixOrientations(); - this.DoOffset(delta); - //now clean up 'corners' ... - var clpr = new ClipperLib.Clipper(0); - clpr.AddPaths(this.m_destPolys, ClipperLib.PolyType.ptSubject, true); - if (delta > 0) - { - clpr.Execute(ClipperLib.ClipType.ctUnion, solution, ClipperLib.PolyFillType.pftPositive, ClipperLib.PolyFillType.pftPositive); - } - else - { - var r = ClipperLib.Clipper.GetBounds(this.m_destPolys); - var outer = new ClipperLib.Path(); - outer.push(new ClipperLib.IntPoint(r.left - 10, r.bottom + 10)); - outer.push(new ClipperLib.IntPoint(r.right + 10, r.bottom + 10)); - outer.push(new ClipperLib.IntPoint(r.right + 10, r.top - 10)); - outer.push(new ClipperLib.IntPoint(r.left - 10, r.top - 10)); - clpr.AddPath(outer, ClipperLib.PolyType.ptSubject, true); - clpr.ReverseSolution = true; - clpr.Execute(ClipperLib.ClipType.ctUnion, solution, ClipperLib.PolyFillType.pftNegative, ClipperLib.PolyFillType.pftNegative); - if (solution.length > 0) - solution.splice(0, 1); - } - //console.log(JSON.stringify(solution)); - } - else // function (polytree, delta) - { - var solution = a[0], - delta = a[1]; - solution.Clear(); - this.FixOrientations(); - this.DoOffset(delta); - //now clean up 'corners' ... - var clpr = new ClipperLib.Clipper(0); - clpr.AddPaths(this.m_destPolys, ClipperLib.PolyType.ptSubject, true); - if (delta > 0) - { - clpr.Execute(ClipperLib.ClipType.ctUnion, solution, ClipperLib.PolyFillType.pftPositive, ClipperLib.PolyFillType.pftPositive); - } - else - { - var r = ClipperLib.Clipper.GetBounds(this.m_destPolys); - var outer = new ClipperLib.Path(); - outer.push(new ClipperLib.IntPoint(r.left - 10, r.bottom + 10)); - outer.push(new ClipperLib.IntPoint(r.right + 10, r.bottom + 10)); - outer.push(new ClipperLib.IntPoint(r.right + 10, r.top - 10)); - outer.push(new ClipperLib.IntPoint(r.left - 10, r.top - 10)); - clpr.AddPath(outer, ClipperLib.PolyType.ptSubject, true); - clpr.ReverseSolution = true; - clpr.Execute(ClipperLib.ClipType.ctUnion, solution, ClipperLib.PolyFillType.pftNegative, ClipperLib.PolyFillType.pftNegative); - //remove the outer PolyNode rectangle ... - if (solution.ChildCount() == 1 && solution.Childs()[0].ChildCount() > 0) - { - var outerNode = solution.Childs()[0]; - //solution.Childs.set_Capacity(outerNode.ChildCount); - solution.Childs()[0] = outerNode.Childs()[0]; - for (var i = 1; i < outerNode.ChildCount(); i++) - solution.AddChild(outerNode.Childs()[i]); - } - else - solution.Clear(); - } - } - }; - ClipperLib.ClipperOffset.prototype.OffsetPoint = function (j, k, jointype) - { - this.m_sinA = (this.m_normals[k].X * this.m_normals[j].Y - this.m_normals[j].X * this.m_normals[k].Y); - if (this.m_sinA < 0.00005 && this.m_sinA > -0.00005) - return k; - else if (this.m_sinA > 1) - this.m_sinA = 1.0; - else if (this.m_sinA < -1) - this.m_sinA = -1.0; - if (this.m_sinA * this.m_delta < 0) - { - this.m_destPoly.push(new ClipperLib.IntPoint(ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].X + this.m_normals[k].X * this.m_delta), - ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].Y + this.m_normals[k].Y * this.m_delta))); - this.m_destPoly.push(new ClipperLib.IntPoint(this.m_srcPoly[j])); - this.m_destPoly.push(new ClipperLib.IntPoint(ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].X + this.m_normals[j].X * this.m_delta), - ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].Y + this.m_normals[j].Y * this.m_delta))); - } - else - switch (jointype) - { - case ClipperLib.JoinType.jtMiter: - { - var r = 1 + (this.m_normals[j].X * this.m_normals[k].X + this.m_normals[j].Y * this.m_normals[k].Y); - if (r >= this.m_miterLim) - this.DoMiter(j, k, r); - else - this.DoSquare(j, k); - break; - } - case ClipperLib.JoinType.jtSquare: - this.DoSquare(j, k); - break; - case ClipperLib.JoinType.jtRound: - this.DoRound(j, k); - break; - } - k = j; - return k; - }; - ClipperLib.ClipperOffset.prototype.DoSquare = function (j, k) - { - var dx = Math.tan(Math.atan2(this.m_sinA, - this.m_normals[k].X * this.m_normals[j].X + this.m_normals[k].Y * this.m_normals[j].Y) / 4); - this.m_destPoly.push(new ClipperLib.IntPoint( - ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].X + this.m_delta * (this.m_normals[k].X - this.m_normals[k].Y * dx)), - ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].Y + this.m_delta * (this.m_normals[k].Y + this.m_normals[k].X * dx)))); - this.m_destPoly.push(new ClipperLib.IntPoint( - ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].X + this.m_delta * (this.m_normals[j].X + this.m_normals[j].Y * dx)), - ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].Y + this.m_delta * (this.m_normals[j].Y - this.m_normals[j].X * dx)))); - }; - ClipperLib.ClipperOffset.prototype.DoMiter = function (j, k, r) - { - var q = this.m_delta / r; - this.m_destPoly.push(new ClipperLib.IntPoint( - ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].X + (this.m_normals[k].X + this.m_normals[j].X) * q), - ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].Y + (this.m_normals[k].Y + this.m_normals[j].Y) * q))); - }; - ClipperLib.ClipperOffset.prototype.DoRound = function (j, k) - { - var a = Math.atan2(this.m_sinA, - this.m_normals[k].X * this.m_normals[j].X + this.m_normals[k].Y * this.m_normals[j].Y); - var steps = ClipperLib.Cast_Int32(ClipperLib.ClipperOffset.Round(this.m_StepsPerRad * Math.abs(a))); - var X = this.m_normals[k].X, - Y = this.m_normals[k].Y, - X2; - for (var i = 0; i < steps; ++i) - { - this.m_destPoly.push(new ClipperLib.IntPoint( - ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].X + X * this.m_delta), - ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].Y + Y * this.m_delta))); - X2 = X; - X = X * this.m_cos - this.m_sin * Y; - Y = X2 * this.m_sin + Y * this.m_cos; - } - this.m_destPoly.push(new ClipperLib.IntPoint( - ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].X + this.m_normals[j].X * this.m_delta), - ClipperLib.ClipperOffset.Round(this.m_srcPoly[j].Y + this.m_normals[j].Y * this.m_delta))); - }; - ClipperLib.Error = function (message) - { - try - { - throw new Error(message); - } - catch (err) - { - alert(err.message); - } - }; - // --------------------------------- - // JS extension by Timo 2013 - ClipperLib.JS = {}; - ClipperLib.JS.AreaOfPolygon = function (poly, scale) - { - if (!scale) scale = 1; - return ClipperLib.Clipper.Area(poly) / (scale * scale); - }; - ClipperLib.JS.AreaOfPolygons = function (poly, scale) - { - if (!scale) scale = 1; - var area = 0; - for (var i = 0; i < poly.length; i++) - { - area += ClipperLib.Clipper.Area(poly[i]); - } - return area / (scale * scale); - }; - ClipperLib.JS.BoundsOfPath = function (path, scale) - { - return ClipperLib.JS.BoundsOfPaths([path], scale); - }; - ClipperLib.JS.BoundsOfPaths = function (paths, scale) - { - if (!scale) scale = 1; - var bounds = ClipperLib.Clipper.GetBounds(paths); - bounds.left /= scale; - bounds.bottom /= scale; - bounds.right /= scale; - bounds.top /= scale; - return bounds; - }; - // Clean() joins vertices that are too near each other - // and causes distortion to offsetted polygons without cleaning - ClipperLib.JS.Clean = function (polygon, delta) - { - if (!(polygon instanceof Array)) return []; - var isPolygons = polygon[0] instanceof Array; - var polygon = ClipperLib.JS.Clone(polygon); - if (typeof delta != "number" || delta === null) - { - ClipperLib.Error("Delta is not a number in Clean()."); - return polygon; - } - if (polygon.length === 0 || (polygon.length == 1 && polygon[0].length === 0) || delta < 0) return polygon; - if (!isPolygons) polygon = [polygon]; - var k_length = polygon.length; - var len, poly, result, d, p, j, i; - var results = []; - for (var k = 0; k < k_length; k++) - { - poly = polygon[k]; - len = poly.length; - if (len === 0) continue; - else if (len < 3) - { - result = poly; - results.push(result); - continue; - } - result = poly; - d = delta * delta; - //d = Math.floor(c_delta * c_delta); - p = poly[0]; - j = 1; - for (i = 1; i < len; i++) - { - if ((poly[i].X - p.X) * (poly[i].X - p.X) + - (poly[i].Y - p.Y) * (poly[i].Y - p.Y) <= d) - continue; - result[j] = poly[i]; - p = poly[i]; - j++; - } - p = poly[j - 1]; - if ((poly[0].X - p.X) * (poly[0].X - p.X) + - (poly[0].Y - p.Y) * (poly[0].Y - p.Y) <= d) - j--; - if (j < len) - result.splice(j, len - j); - if (result.length) results.push(result); - } - if (!isPolygons && results.length) results = results[0]; - else if (!isPolygons && results.length === 0) results = []; - else if (isPolygons && results.length === 0) results = [ - [] - ]; - return results; - } - // Make deep copy of Polygons or Polygon - // so that also IntPoint objects are cloned and not only referenced - // This should be the fastest way - ClipperLib.JS.Clone = function (polygon) - { - if (!(polygon instanceof Array)) return []; - if (polygon.length === 0) return []; - else if (polygon.length == 1 && polygon[0].length === 0) return [[]]; - var isPolygons = polygon[0] instanceof Array; - if (!isPolygons) polygon = [polygon]; - var len = polygon.length, - plen, i, j, result; - var results = new Array(len); - for (i = 0; i < len; i++) - { - plen = polygon[i].length; - result = new Array(plen); - for (j = 0; j < plen; j++) - { - result[j] = { - X: polygon[i][j].X, - Y: polygon[i][j].Y - }; - } - results[i] = result; - } - if (!isPolygons) results = results[0]; - return results; - }; - // Removes points that doesn't affect much to the visual appearance. - // If middle point is at or under certain distance (tolerance) of the line segment between - // start and end point, the middle point is removed. - ClipperLib.JS.Lighten = function (polygon, tolerance) - { - if (!(polygon instanceof Array)) return []; - if (typeof tolerance != "number" || tolerance === null) - { - ClipperLib.Error("Tolerance is not a number in Lighten().") - return ClipperLib.JS.Clone(polygon); - } - if (polygon.length === 0 || (polygon.length == 1 && polygon[0].length === 0) || tolerance < 0) - { - return ClipperLib.JS.Clone(polygon); - } - if (!(polygon[0] instanceof Array)) polygon = [polygon]; - var i, j, poly, k, poly2, plen, A, B, P, d, rem, addlast; - var bxax, byay, l, ax, ay; - var len = polygon.length; - var toleranceSq = tolerance * tolerance; - var results = []; - for (i = 0; i < len; i++) - { - poly = polygon[i]; - plen = poly.length; - if (plen == 0) continue; - for (k = 0; k < 1000000; k++) // could be forever loop, but wiser to restrict max repeat count - { - poly2 = []; - plen = poly.length; - // the first have to added to the end, if first and last are not the same - // this way we ensure that also the actual last point can be removed if needed - if (poly[plen - 1].X != poly[0].X || poly[plen - 1].Y != poly[0].Y) - { - addlast = 1; - poly.push( - { - X: poly[0].X, - Y: poly[0].Y - }); - plen = poly.length; - } - else addlast = 0; - rem = []; // Indexes of removed points - for (j = 0; j < plen - 2; j++) - { - A = poly[j]; // Start point of line segment - P = poly[j + 1]; // Middle point. This is the one to be removed. - B = poly[j + 2]; // End point of line segment - ax = A.X; - ay = A.Y; - bxax = B.X - ax; - byay = B.Y - ay; - if (bxax !== 0 || byay !== 0) // To avoid Nan, when A==P && P==B. And to avoid peaks (A==B && A!=P), which have lenght, but not area. - { - l = ((P.X - ax) * bxax + (P.Y - ay) * byay) / (bxax * bxax + byay * byay); - if (l > 1) - { - ax = B.X; - ay = B.Y; - } - else if (l > 0) - { - ax += bxax * l; - ay += byay * l; - } - } - bxax = P.X - ax; - byay = P.Y - ay; - d = bxax * bxax + byay * byay; - if (d <= toleranceSq) - { - rem[j + 1] = 1; - j++; // when removed, transfer the pointer to the next one - } - } - // add all unremoved points to poly2 - poly2.push( - { - X: poly[0].X, - Y: poly[0].Y - }); - for (j = 1; j < plen - 1; j++) - if (!rem[j]) poly2.push( - { - X: poly[j].X, - Y: poly[j].Y - }); - poly2.push( - { - X: poly[plen - 1].X, - Y: poly[plen - 1].Y - }); - // if the first point was added to the end, remove it - if (addlast) poly.pop(); - // break, if there was not anymore removed points - if (!rem.length) break; - // else continue looping using poly2, to check if there are points to remove - else poly = poly2; - } - plen = poly2.length; - // remove duplicate from end, if needed - if (poly2[plen - 1].X == poly2[0].X && poly2[plen - 1].Y == poly2[0].Y) - { - poly2.pop(); - } - if (poly2.length > 2) // to avoid two-point-polygons - results.push(poly2); - } - if (!(polygon[0] instanceof Array)) results = results[0]; - if (typeof (results) == "undefined") results = [ - [] - ]; - return results; - } - ClipperLib.JS.PerimeterOfPath = function (path, closed, scale) - { - if (typeof (path) == "undefined") return 0; - var sqrt = Math.sqrt; - var perimeter = 0.0; - var p1, p2, p1x = 0.0, - p1y = 0.0, - p2x = 0.0, - p2y = 0.0; - var j = path.length; - if (j < 2) return 0; - if (closed) - { - path[j] = path[0]; - j++; - } - while (--j) - { - p1 = path[j]; - p1x = p1.X; - p1y = p1.Y; - p2 = path[j - 1]; - p2x = p2.X; - p2y = p2.Y; - perimeter += sqrt((p1x - p2x) * (p1x - p2x) + (p1y - p2y) * (p1y - p2y)); - } - if (closed) path.pop(); - return perimeter / scale; - }; - ClipperLib.JS.PerimeterOfPaths = function (paths, closed, scale) - { - if (!scale) scale = 1; - var perimeter = 0; - for (var i = 0; i < paths.length; i++) - { - perimeter += ClipperLib.JS.PerimeterOfPath(paths[i], closed, scale); - } - return perimeter; - }; - ClipperLib.JS.ScaleDownPath = function (path, scale) - { - var i, p; - if (!scale) scale = 1; - i = path.length; - while (i--) - { - p = path[i]; - p.X = p.X / scale; - p.Y = p.Y / scale; - } - }; - ClipperLib.JS.ScaleDownPaths = function (paths, scale) - { - var i, j, p, round = Math.round; - if (!scale) scale = 1; - i = paths.length; - while (i--) - { - j = paths[i].length; - while (j--) - { - p = paths[i][j]; - p.X = p.X / scale; - p.Y = p.Y / scale; - } - } - }; - ClipperLib.JS.ScaleUpPath = function (path, scale) - { - var i, p, round = Math.round; - if (!scale) scale = 1; - i = path.length; - while (i--) - { - p = path[i]; - p.X = round(p.X * scale); - p.Y = round(p.Y * scale); - } - }; - ClipperLib.JS.ScaleUpPaths = function (paths, scale) - { - var i, j, p, round = Math.round; - if (!scale) scale = 1; - i = paths.length; - while (i--) - { - j = paths[i].length; - while (j--) - { - p = paths[i][j]; - p.X = round(p.X * scale); - p.Y = round(p.Y * scale); - } - } - }; - ClipperLib.ExPolygons = function () - { - return []; - } - ClipperLib.ExPolygon = function () - { - this.outer = null; - this.holes = null; - }; - ClipperLib.JS.AddOuterPolyNodeToExPolygons = function (polynode, expolygons) - { - var ep = new ClipperLib.ExPolygon(); - ep.outer = polynode.Contour(); - var childs = polynode.Childs(); - var ilen = childs.length; - ep.holes = new Array(ilen); - var node, n, i, j, childs2, jlen; - for (i = 0; i < ilen; i++) - { - node = childs[i]; - ep.holes[i] = node.Contour(); - //Add outer polygons contained by (nested within) holes ... - for (j = 0, childs2 = node.Childs(), jlen = childs2.length; j < jlen; j++) - { - n = childs2[j]; - ClipperLib.JS.AddOuterPolyNodeToExPolygons(n, expolygons); - } - } - expolygons.push(ep); - }; - ClipperLib.JS.ExPolygonsToPaths = function (expolygons) - { - var a, i, alen, ilen; - var paths = new ClipperLib.Paths(); - for (a = 0, alen = expolygons.length; a < alen; a++) - { - paths.push(expolygons[a].outer); - for (i = 0, ilen = expolygons[a].holes.length; i < ilen; i++) - { - paths.push(expolygons[a].holes[i]); - } - } - return paths; - } - ClipperLib.JS.PolyTreeToExPolygons = function (polytree) - { - var expolygons = new ClipperLib.ExPolygons(); - var node, i, childs, ilen; - for (i = 0, childs = polytree.Childs(), ilen = childs.length; i < ilen; i++) - { - node = childs[i]; - ClipperLib.JS.AddOuterPolyNodeToExPolygons(node, expolygons); - } - return expolygons; - }; -})(); diff --git a/src/ext/clip2.esm.js b/src/ext/clip2.esm.js new file mode 100644 index 00000000..1c9bed06 --- /dev/null +++ b/src/ext/clip2.esm.js @@ -0,0 +1,7 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +// Import the original Clipper library +import './clip2.js'; + +// Re-export the ClipperLib object +export const ClipperLib = self.ClipperLib; \ No newline at end of file diff --git a/src/ext/fsave.js b/src/ext/fsave.js deleted file mode 100644 index 75d71ab0..00000000 --- a/src/ext/fsave.js +++ /dev/null @@ -1,270 +0,0 @@ -/* FileSaver.js - * A saveAs() FileSaver implementation. - * 1.1.20151003 - * - * By Eli Grey, http://eligrey.com - * License: MIT - * See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md - */ - -/*global self */ -/*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */ - -/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ - -var saveAs = saveAs || (function(view) { - "use strict"; - // IE <10 is explicitly unsupported - if (typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) { - return; - } - var - doc = view.document - // only get URL when necessary in case Blob.js hasn't overridden it yet - , get_URL = function() { - return view.URL || view.webkitURL || view; - } - , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a") - , can_use_save_link = "download" in save_link - , click = function(node) { - var event = new MouseEvent("click"); - node.dispatchEvent(event); - } - , is_safari = /Version\/[\d\.]+.*Safari/.test(navigator.userAgent) - , webkit_req_fs = view.webkitRequestFileSystem - , req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem - , throw_outside = function(ex) { - (view.setImmediate || view.setTimeout)(function() { - throw ex; - }, 0); - } - , force_saveable_type = "application/octet-stream" - , fs_min_size = 0 - // See https://code.google.com/p/chromium/issues/detail?id=375297#c7 and - // https://github.com/eligrey/FileSaver.js/commit/485930a#commitcomment-8768047 - // for the reasoning behind the timeout and revocation flow - , arbitrary_revoke_timeout = 500 // in ms - , revoke = function(file) { - var revoker = function() { - if (typeof file === "string") { // file is an object URL - get_URL().revokeObjectURL(file); - } else { // file is a File - file.remove(); - } - }; - if (view.chrome) { - revoker(); - } else { - setTimeout(revoker, arbitrary_revoke_timeout); - } - } - , dispatch = function(filesaver, event_types, event) { - event_types = [].concat(event_types); - var i = event_types.length; - while (i--) { - var listener = filesaver["on" + event_types[i]]; - if (typeof listener === "function") { - try { - listener.call(filesaver, event || filesaver); - } catch (ex) { - throw_outside(ex); - } - } - } - } - , auto_bom = function(blob) { - // prepend BOM for UTF-8 XML and text/* types (including HTML) - if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) { - return new Blob(["\ufeff", blob], {type: blob.type}); - } - return blob; - } - , FileSaver = function(blob, name, no_auto_bom) { - if (!no_auto_bom) { - blob = auto_bom(blob); - } - // First try a.download, then web filesystem, then object URLs - var - filesaver = this - , type = blob.type - , blob_changed = false - , object_url - , target_view - , dispatch_all = function() { - dispatch(filesaver, "writestart progress write writeend".split(" ")); - } - // on any filesys errors revert to saving with object URLs - , fs_error = function() { - if (target_view && is_safari && typeof FileReader !== "undefined") { - // Safari doesn't allow downloading of blob urls - var reader = new FileReader(); - reader.onloadend = function() { - var base64Data = reader.result; - target_view.location.href = "data:attachment/file" + base64Data.slice(base64Data.search(/[,;]/)); - filesaver.readyState = filesaver.DONE; - dispatch_all(); - }; - reader.readAsDataURL(blob); - filesaver.readyState = filesaver.INIT; - return; - } - // don't create more object URLs than needed - if (blob_changed || !object_url) { - object_url = get_URL().createObjectURL(blob); - } - if (target_view) { - target_view.location.href = object_url; - } else { - var new_tab = view.open(object_url, "_blank"); - if (new_tab == undefined && is_safari) { - //Apple do not allow window.open, see http://bit.ly/1kZffRI - view.location.href = object_url - } - } - filesaver.readyState = filesaver.DONE; - dispatch_all(); - revoke(object_url); - } - , abortable = function(func) { - return function() { - if (filesaver.readyState !== filesaver.DONE) { - return func.apply(this, arguments); - } - }; - } - , create_if_not_found = {create: true, exclusive: false} - , slice - ; - filesaver.readyState = filesaver.INIT; - if (!name) { - name = "download"; - } - if (can_use_save_link) { - object_url = get_URL().createObjectURL(blob); - setTimeout(function() { - save_link.href = object_url; - save_link.download = name; - click(save_link); - dispatch_all(); - revoke(object_url); - filesaver.readyState = filesaver.DONE; - }); - return; - } - // Object and web filesystem URLs have a problem saving in Google Chrome when - // viewed in a tab, so I force save with application/octet-stream - // http://code.google.com/p/chromium/issues/detail?id=91158 - // Update: Google errantly closed 91158, I submitted it again: - // https://code.google.com/p/chromium/issues/detail?id=389642 - if (view.chrome && type && type !== force_saveable_type) { - slice = blob.slice || blob.webkitSlice; - blob = slice.call(blob, 0, blob.size, force_saveable_type); - blob_changed = true; - } - // Since I can't be sure that the guessed media type will trigger a download - // in WebKit, I append .download to the filename. - // https://bugs.webkit.org/show_bug.cgi?id=65440 - if (webkit_req_fs && name !== "download") { - name += ".download"; - } - if (type === force_saveable_type || webkit_req_fs) { - target_view = view; - } - if (!req_fs) { - fs_error(); - return; - } - fs_min_size += blob.size; - req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) { - fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) { - var save = function() { - dir.getFile(name, create_if_not_found, abortable(function(file) { - file.createWriter(abortable(function(writer) { - writer.onwriteend = function(event) { - target_view.location.href = file.toURL(); - filesaver.readyState = filesaver.DONE; - dispatch(filesaver, "writeend", event); - revoke(file); - }; - writer.onerror = function() { - var error = writer.error; - if (error.code !== error.ABORT_ERR) { - fs_error(); - } - }; - "writestart progress write abort".split(" ").forEach(function(event) { - writer["on" + event] = filesaver["on" + event]; - }); - writer.write(blob); - filesaver.abort = function() { - writer.abort(); - filesaver.readyState = filesaver.DONE; - }; - filesaver.readyState = filesaver.WRITING; - }), fs_error); - }), fs_error); - }; - dir.getFile(name, {create: false}, abortable(function(file) { - // delete file if it already exists - file.remove(); - save(); - }), abortable(function(ex) { - if (ex.code === ex.NOT_FOUND_ERR) { - save(); - } else { - fs_error(); - } - })); - }), fs_error); - }), fs_error); - } - , FS_proto = FileSaver.prototype - , saveAs = function(blob, name, no_auto_bom) { - return new FileSaver(blob, name, no_auto_bom); - } - ; - // IE 10+ (native saveAs) - if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) { - return function(blob, name, no_auto_bom) { - if (!no_auto_bom) { - blob = auto_bom(blob); - } - return navigator.msSaveOrOpenBlob(blob, name || "download"); - }; - } - - FS_proto.abort = function() { - var filesaver = this; - filesaver.readyState = filesaver.DONE; - dispatch(filesaver, "abort"); - }; - FS_proto.readyState = FS_proto.INIT = 0; - FS_proto.WRITING = 1; - FS_proto.DONE = 2; - - FS_proto.error = - FS_proto.onwritestart = - FS_proto.onprogress = - FS_proto.onwrite = - FS_proto.onabort = - FS_proto.onerror = - FS_proto.onwriteend = - null; - - return saveAs; - }( - typeof self !== "undefined" && self - || typeof window !== "undefined" && window - || this.content - )); -// `self` is undefined in Firefox for Android content script context -// while `this` is nsIContentFrameMessageManager -// with an attribute `content` that corresponds to the window - -if (typeof module !== "undefined" && module.exports) { - module.exports.saveAs = saveAs; -} else if ((typeof define !== "undefined" && define !== null) && (define.amd != null)) { - define([], function() { - return saveAs; - }); -} 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/src/ext/pngjs.esm.js b/src/ext/pngjs.esm.js new file mode 100644 index 00000000..1da89a23 --- /dev/null +++ b/src/ext/pngjs.esm.js @@ -0,0 +1,7 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +// Import the original PNG library +import './pngjs.js'; + +// Re-export the PNG object +export const PNG = self.png ? self.png.PNG : null; \ No newline at end of file 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 2a098c26..f49138d1 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,19 +116,10 @@ function indexVertices(pos) { } let Instance; - -ext.manifold.then(mod => { - mod.default({ - locateFile() { return "/wasm/manifold.wasm" } - }).then(inst => { +manifold({ + locateFile() { return "../wasm/manifold.wasm" } +}).then(inst => { inst.setup(); Instance = inst; CSG.Instance = () => { return Instance }; - }); -}); - -gapp.overlay(base, { - CSG -}); - }); diff --git a/src/geo/gyroid.js b/src/geo/gyroid.js index b56ca25c..0b9d1b1f 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,11 +194,10 @@ function filter(poly, inc) { } e1 = el; } - nupoly.push(poly[poly.length-1]); return nupoly; } -function distTo(a, b, dir) { +export function distTo(a, b, dir) { let dx = a.x - b.x; let dy = a.y - b.y; // bias distance by prevailing direction of discovery to join stragglers @@ -212,9 +205,3 @@ function distTo(a, b, dir) { if (dir === 'td') dy = dy / 2; return Math.sqrt(dx * dx + dy * dy); } - -gapp.overlay(base, { - gyroid: { slice } -}) - -}); 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..4e638505 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(arcdivs <= 2){ + return [start.clone(), end.clone()]; } 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 +560,7 @@ function arcToPath( start, end,arcdivs=24,opts) { center.y = pr2.y; center.r = radius; } else { - console.log({malfomed_arc: {radius,center, clockwise, start, end}}); + console.log({ malfomed_arc: { radius, center, clockwise, start, end } }); } //deltas @@ -587,29 +575,19 @@ function arcToPath( start, end,arcdivs=24,opts) { 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 step = (samePoint? (Math.PI*2)*(clockwise? -1 : 1) : ad) / steps; let zStart = start.z; - let zStep = dz / numPoints; - let rot = a1 + step; + let zStep = -dz / steps; + let rot = a1; //unused deltas let da = Math.abs(a1 - a2); let dd = Math.sqrt(dx * dx + dy * dy); - // LOG({index, da, dd, first: pos, last: rec, center, a1, a2, ad, step, steps, rot, line}); - // G0G1(false, [`X${center.x}`, `Y${center.y}`, `E1`]); - - // under 1 degree arc and 5mm, convert to straight line - // if (da < 0.005 && dd < 5) { - // G0G1(false, [`X${end.x}`, `Y${end.y}`, `E1`]); - // return ; - // } - let arr = [] // point accumulator - 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 +601,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 +620,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..8cc0f14d 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); } @@ -848,6 +843,7 @@ class Polygon { * scale polygon around origin */ scale(scale, round) { + this.area2 = undefined; let x, y, z; if (typeof(scale) === 'number') { x = y = z = scale; @@ -1078,6 +1074,7 @@ class Polygon { * append point to polygon and return point */ push(p) { + this.area2 = undefined; // clone any point belonging to another polygon if (p.poly) p = p.clone(); p.poly = this; @@ -1122,6 +1119,14 @@ class Polygon { return this.first().isEqual(this.last()); } + fixClosed() { + if (this.appearsClosed()) { + this.points.pop(); + this.open = false; + } + return this; + } + setClockwise() { if (!this.isClockwise()) this.reverse(); return this; @@ -1143,7 +1148,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 +1210,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, @@ -1496,7 +1562,12 @@ class Polygon { * @returns {number} 0.0 - 1.0 from flat to perfectly circular */ circularity() { - return (4 * Math.PI * this.area()) / util.sqr(this.perimeter()); + try { + return (4 * Math.PI * this.area()) / util.sqr(this.perimeter()); + } catch (e) { + console.log(this.perimeter(), e); + return 0; + } } circularityDeep() { @@ -2003,7 +2074,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 +2389,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 +2398,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..6d4d8128 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= min) { + if (poly.area() >= min && poly.length > 2) { to.push(poly); out.push(poly); } @@ -440,63 +435,63 @@ function subtract(setA, setB, outA, outB, z, minArea, opt = {}) { * @param {Polygon[]} polys * @returns {Polygon[]} */ - function union(polys, minarea, all, opt = {}) { - if (polys.length < 2) return polys; - let lpre = length(polys); +export function union(polys, minarea, all, opt = {}) { + if (polys.length < 2) return polys; + let lpre = length(polys); - if (opt.wasm && geo.wasm) { - let min = minarea ?? 0.01; - // let deepLength = polys.map(p => p.deepLength).reduce((a,v) => a+v); - // if (deepLength < 15000) - try { - let out = geo.wasm.js.union(polys, polys[0].getZ()).filter(p => p.area() > min); - opt.changes = length(out) - lpre; - return out; - } catch (e) { - console.log({union_fail: polys, minarea, all}); - } - } + if (opt.wasm && geo.wasm) { + let min = minarea ?? 0.01; + // let deepLength = polys.map(p => p.deepLength).reduce((a,v) => a+v); + // if (deepLength < 15000) + try { + let out = geo.wasm.js.union(polys, polys[0].getZ()).filter(p => p.area() > min); + opt.changes = length(out) - lpre; + return out; + } catch (e) { + console.log({union_fail: polys, minarea, all}); + } + } - let out = polys.slice(), i, j, union, uset = [], a, b; + let out = polys.slice(), i, j, union, uset = [], a, b; - outer: for (i=0; i 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..72c7578f 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'; +import { newPolygon } from './polygon.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 = 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/src/kiri-mode/cam/animate.js b/src/kiri-mode/cam/animate.js deleted file mode 100644 index 7a5de7e4..00000000 --- a/src/kiri-mode/cam/animate.js +++ /dev/null @@ -1,648 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -"use strict"; - -// dep: kiri-mode.cam.driver -// dep: kiri-mode.cam.animate2 -gapp.register("kiri-mode.cam.animate", [], (root, exports) => { - -const { kiri } = root; -const { driver } = kiri; -const { CAM } = driver; -const asLines = false; -const asPoints = false; - -// ---( CLIENT FUNCTIONS )--- - -// tint points below z=0 with red -function add_red_neg_z(material) { - material.onBeforeCompile = (shader) => { - shader.vertexShader = shader.vertexShader.replace( - `#include `, - ` - #include - vWorldPosition = vec3(transformed); - ` - ); - - shader.vertexShader = ` - varying vec3 vWorldPosition; - ` + shader.vertexShader; - - shader.fragmentShader = ` - varying vec3 vWorldPosition; - ` + shader.fragmentShader; - - shader.fragmentShader = shader.fragmentShader.replace( - `#include `, - ` - #include - if (vWorldPosition.z < 0.0) { - gl_FragColor.rgb += vec3(0.5, 0.0, 0.0); // Add red tint - } - ` - ); - }; - return material; -} - -kiri.load(() => { - if (!kiri.client) { - return; - } - - let meshes = {}, - button = {}, - label = {}, - unitScale = 1, - speedValues = [ 1, 2, 4, 8, 32 ], - speedPauses = [ 30, 20, 10, 5, 0 ], - speedNames = [ "1x", "2x", "4x", "8x", "!!" ], - speedMax = speedValues.length - 1, - speedIndex = 0, - speed, - color = 0, - material, - origin, - posOffset = { x:0, y:0, z:0 }; - - const { moto } = root; - const { space } = moto; - const { api } = kiri; - - function animate_clear(api) { - let { anim } = api.ui; - moto.space.platform.showGridBelow(true); - kiri.client.animate_cleanup(); - Object.keys(meshes).forEach(id => deleteMesh(id)); - toggleStock(undefined,true,false); - api.uc.setVisible(anim.laba, false); - api.uc.setVisible(anim.vala, false); -} - - function animate(api, delay) { - let alert = api.alerts.show("building animation"); - let settings = api.conf.get(); - kiri.client.animate_setup(settings, data => { - checkMeshCommands(data); - if (!(data && data.mesh_add)) { - return; - } - - let { anim } = api.ui; - Object.assign(button, { - replay: anim.replay, - play: anim.play, - step: anim.step, - pause: anim.pause, - speed: anim.speed, - trans: anim.trans, - model: anim.model, - shade: anim.shade - }); - Object.assign(label, { - progress: anim.progress, - speed: anim.labspd, - x: anim.valx, - y: anim.valy, - z: anim.valz, - }); - - origin = settings.origin; - speedIndex = api.local.getInt('cam.anim.speed') || 0; - updateSpeed(); - setTimeout(step, delay || 0); - - button.replay.onclick = replay; - button.play.onclick = play; - button.step.onclick = step; - button.pause.onclick = pause; - button.speed.onclick = fast; - button.trans.onclick = toggleTrans; - button.model.onclick = toggleModel; - button.shade.onclick = toggleStock; - button.play.style.display = ''; - button.pause.style.display = 'none'; - - api.event.emit('animate', 'CAM'); - api.alerts.hide(alert); - moto.space.platform.showGridBelow(false); - toggleTrans(0,api.local.getBoolean('cam.anim.trans', true)); - toggleModel(0,api.local.getBoolean('cam.anim.model', false)); - toggleStock(0,api.local.getBoolean('cam.anim.stock', false)); - }); - } - - gapp.overlay(kiri.client, { - animate(data, ondone) { - kiri.client.send("animate", data, ondone); - }, - - animate_setup(settings, ondone) { - color = settings.controller.dark ? 0x48607B : 0x607FA4; - unitScale = settings.controller.units === 'in' ? 1/25.4 : 1; - let flatShading = true, - transparent = true, - opacity = 0.9, - side = THREE.DoubleSide; - material = new THREE.MeshPhongMaterial({ - flatShading, - transparent, - opacity, - color, - side - }); - add_red_neg_z(material); - kiri.client.send("animate_setup", {settings}, ondone); - }, - - animate_cleanup(data, ondone) { - kiri.client.send("animate_cleanup", data, ondone); - } - }); - - gapp.overlay(CAM, { - animate, - animate_clear - }); - - function meshAdd(id, ind, pos, sab) { - const geo = new THREE.BufferGeometry(); - if (sab) { - // use array buffer shared with worker - pos = new Float32Array(sab); - } - geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); - if (ind.length) { - geo.setIndex(new THREE.BufferAttribute(new Uint32Array(ind), 1)); - } - let mesh; - if (asPoints) { - const mat = new THREE.PointsMaterial({ - transparent: true, - opacity: 0.75, - color: 0x888888, - size: 0.3 - }); - mesh = new THREE.Points(geo, mat); - } else if (asLines) { - const mat = new THREE.LineBasicMaterial({ - transparent: true, - opacity: 0.75, - color - }); - mesh = new THREE.LineSegments(geo, mat); - } else { - geo.computeVertexNormals(); - mesh = new THREE.Mesh(geo, material); - mesh.renderOrder = -10; - } - space.world.add(mesh); - meshes[id] = mesh; - } - - function meshUpdates(id) { - const mesh = meshes[id]; - if (!mesh) { - return; // animate cancelled - } - mesh.geometry.attributes.position.needsUpdate = true; - space.update(); - } - - function deleteMesh(id) { - space.world.remove(meshes[id]); - delete meshes[id]; - } - - function toggleModel(ev,bool) { - api.local.toggle('cam.anim.model', bool); - api.widgets.all().forEach(w => w.toggleVisibility(bool)); - } - - function toggleStock(ev,bool,set) { - set !== false && api.local.toggle('cam.anim.stock', bool); - return api.event.emit('cam.stock.toggle', bool ?? undefined); - } - - function toggleTrans(ev,bool) { - bool = api.local.toggle('cam.anim.trans', bool); - material.transparent = bool; - material.needsUpdate = true; - } - - function step() { - updateSpeed(); - kiri.client.animate({speed, steps: 1}, handleGridUpdate); - } - - function play(opts) { - const { steps } = opts; - updateSpeed(); - if (steps !== 1) { - button.play.style.display = 'none'; - button.pause.style.display = ''; - } - kiri.client.animate({ - speed, - steps: steps || Infinity, - pause: speedPauses[speedIndex] - }, handleGridUpdate); - } - - function fast(opts) { - const { steps } = opts; - updateSpeed(1); - button.play.style.display = 'none'; - button.pause.style.display = ''; - kiri.client.animate({ - speed, - steps: steps || Infinity, - pause: speedPauses[speedIndex] - }, handleGridUpdate); - } - - function pause() { - button.play.style.display = ''; - button.pause.style.display = 'none'; - kiri.client.animate({speed: 0}, handleGridUpdate); - } - - function handleGridUpdate(data) { - checkMeshCommands(data); - if (data && data.progress) { - label.progress.value = (data.progress * 100).toFixed(1); - } - } - - function updateSpeed(inc = 0) { - if (inc === Infinity) { - speedIndex = speedMax; - } else if (inc > 0) { - speedIndex = (speedIndex + inc) % speedValues.length; - } - api.local.set('cam.anim.speed', speedIndex); - speed = speedValues[speedIndex]; - label.speed.value = speedNames[speedIndex]; - } - - function replay() { - animate_clear(api); - setTimeout(() => { - animate(api, 50); - }, 250); - } - - function checkMeshCommands(data) { - if (!data) { - return; - } - if (data.mesh_add) { - const { id, ind, pos, offset, sab } = data.mesh_add; - meshAdd(id, ind, pos, sab); - space.refresh(); - if (offset) { - posOffset = offset; - } - } - if (data.mesh_del) { - deleteMesh(data.mesh_del); - } - if (data.mesh_move) { - const { id, pos } = data.mesh_move; - const mesh = meshes[id]; - if (mesh) { - mesh.position.x = pos.x; - mesh.position.y = pos.y; - mesh.position.z = pos.z; - space.update(); - } - label.x.value = (pos.x - origin.x).toFixed(2); - label.y.value = (pos.y + origin.y).toFixed(2); - label.z.value = (pos.z - origin.z).toFixed(2); - } - if (data.mesh_update) { - meshUpdates(data.id); - } - } - -}); - -// ---( WORKER FUNCTIONS )--- - -kiri.load(() => { - if (!kiri.worker) { - return; - } - - let stock, center, grid, gridX, gridY, rez; - let path, pathIndex, tool, tools, last, toolID = 1; - - kiri.worker.animate_setup = function(data, send) { - const { settings } = data; - const { process } = settings; - const print = worker.print; - const density = parseInt(settings.controller.animesh) * 1000; - - pathIndex = 0; - path = print.output.flat(); - tools = settings.tools; - stock = settings.stock; - rez = 1/Math.sqrt(density/(stock.x * stock.y)); - - - //destructure arcs into path points - path = path.map(o=> - o.arcPoints - ? [ - ...o.arcPoints.map(point=> - ({ - ...o, - point - }) - ), - o - ] - : o - ) - .flat(); - - - const step = rez; - const stepsX = Math.floor(stock.x / step); - const stepsY = Math.floor(stock.y / step); - const { pos, ind, sab } = createGrid(stepsX, stepsY, stock, step, true); - const offset = { - x: process.camOriginCenter ? 0 : stock.x / 2, - y: process.camOriginCenter ? 0 : stock.y / 2, - z: process.camOriginTop ? -stock.z : 0 - } - - grid = pos; - gridX = stepsX; - gridY = stepsY; - - tool = null; - last = null; - animating = false; - animateClear = false; - - center = Object.assign({}, stock.center); - center.z -= stock.z / 2; - - send.data({ mesh_add: { id: 0, ind, offset, sab } }, [ ]); // sab not transferrable - send.data({ mesh_move: { id: 0, pos: center } }); - send.done(); - }; - - kiri.worker.animate = function(data, send) { - renderPause = data.pause || renderPause; - renderSpeed = data.speed || 0; - if (animating) { - return send.done(); - } - renderSteps = data.steps || 1; - renderDone = false; - animating = renderSpeed > 0; - renderPath(send); - }; - - kiri.worker.animate_cleanup = function(data, send) { - if (animating) { - animateClear = true; - } - }; - - function createGrid(stepsX, stepsY, size, step, stock) { - const gridPoints = stepsX * stepsY; - const sab = new SharedArrayBuffer(gridPoints * 3 * 4) - const pos = new Float32Array(sab); - const ind = []; - const ox = size.x / 2; - const oy = size.y / 2; - let ex = stepsX - 1; - let ey = stepsY - 1; - - // initialize grid points - for (let x=0, ai=0; x 0) ind.appendAll([ - (stepsY * x) + (y - 1), - (stepsY * x) + (y ) - ]); - if (x > 0) ind.appendAll([ - (stepsY * (x - 1)) + y, - (stepsY * (x )) + y - ]); - } else { - if (x > 0 && y > 0) { - let v0 = stepsY * (x - 1) + y - 1; - let v1 = stepsY * (x - 0) + y - 1; - let v2 = stepsY * (x - 0) + y; - let v3 = stepsY * (x - 1) + y; - ind.appendAll([ - v0, v1, v2, v0, v2, v3 - ]); - } - } - } - } - - return { pos, ind, sab }; - } - - let animateClear = false; - let animating = false; - let renderDist = 0; - let renderDone = false; - let renderPause = 10; - let renderSteps = 0; - let renderSpeed = 0; - let skipMove = null; - let toolUpdate; - let depth = 0; - - // send latest tool position and progress bar - function renderUpdate(send) { - if (toolUpdate) { - send.data(toolUpdate); - } - send.data({ progress: pathIndex / path.length, id: 0, mesh_update: 1 }); - } - - function renderPath(send) { - if (renderDone) { - return; - } - - if (renderSteps-- === 0) { - animating = false; - renderPath(send); - return; - } - - if (animating === false || animateClear || renderSpeed === 0) { - renderUpdate(send); - renderDone = true; - animating = false; - animateClear = false; - send.done(); - return; - } - - let next = path[pathIndex]; - while (next && next.type === 'laser') { - last = next; - next = path[++pathIndex]; - } - - if (!next) { - animating = false; - renderPath(send); - return; - } - pathIndex++; - - const firstTool = !tool && next.tool; - const toolChange = !firstTool && (tool.getID() !== next.tool.getID()); - if (firstTool || toolChange) { - // on real tool change, go to safe Z first - if (tool && last.point) { - let pos = last.point = { - x: last.point.x, - y: last.point.y, - z: stock.z - }; - send.data({ mesh_move: { toolID, pos }}); - } - updateTool(next.tool, send); - } - - const id = toolID; - const rezstep = rez; - if (last) { - const lp = last.point, np = next.point; - last = next; - // dwell ops have no point - if (!np || !lp) { - return renderPath(send); - } - const dx = np.x - lp.x, dy = np.y - lp.y, dz = np.z - lp.z; - const dist = Math.sqrt(dx*dx + dy*dy + dz*dz); - renderDist += dist; - - // skip moves that are less than grid resolution - if (renderDist < rezstep) { - renderPath(send); - return; - } - - const md = Math.max(Math.abs(dx), Math.abs(dy), Math.abs(dz)); - const st = Math.ceil(md / rezstep); - const mx = dx / st, my = dy / st, mz = dz / st; - const moves = []; - for (let i=0, x=lp.x, y=lp.y, z=lp.z; i= renderSpeed) || (depth > 600)) { - renderDist = depth = 0; - renderUpdate(send); - setTimeout(() => { - renderMoves(id, moves, send, index); - }, renderPause); - return; - } - } - depth++; - renderPath(send); - } - - // update stock mesh to reflect tool tip geometry at given XYZ position - function deformMesh(pos, send) { - const prof = tool.profile; - const { size, pix } = tool.profileDim; - const mid = Math.floor(pix / 2); - const rx = Math.floor((pos.x + stock.x / 2 - size / 2 - center.x) / rez); - const ry = Math.floor((pos.y + stock.y / 2 - size / 2 - center.y) / rez); - let upos = 0; - // deform mesh to lowest point on tool profile - for (let i=0, il=prof.length; i < il; ) { - const dx = mid + prof[i++]; - const dy = mid + prof[i++]; - const dz = prof[i++]; - const gx = rx + dx; - const gy = ry + dy; - - if (gx < 0|| gy < 0 || gx > gridX-1 || gy > gridY-1) { - continue; - } - - const gi = gx * gridY + gy; - const iz = gi * 3 + 2; - const cz = grid[iz]; - const tz = tool.pos.z - dz; - if (tz < cz) { - upos++; - grid[iz] = tz; - } - } - } - - function updateTool(toolobj, send) { - if (tool) { - send.data({ mesh_del: toolID }); - } - tool = new CAM.Tool({ tools }, toolobj.getID()); - tool.generateProfile(rez); - const flen = tool.fluteLength() || 15; - const slen = tool.shaftLength() || 15; - // const frad = tool.fluteDiameter() / 2; - const prof = tool.profile; - const { size, pix } = tool.profileDim; - const { pos, ind, sab } = createGrid(pix, pix, {x:size, y:size, z:flen+slen}, rez); - const mid = Math.floor(pix/2); - // deform mesh to fit tool profile - for (let i=0, il=prof.length; i < il; ) { - const dx = mid + prof[i++]; - const dy = mid + prof[i++]; - const dz = prof[i++]; - pos[(dx * pix + dy) * 3 + 2] = -dz; - } - send.data({ mesh_add: { id:++toolID, ind, sab }}); - } - -}); - -}); diff --git a/src/kiri-mode/cam/animate2.js b/src/kiri-mode/cam/animate2.js deleted file mode 100644 index c2d2618b..00000000 --- a/src/kiri-mode/cam/animate2.js +++ /dev/null @@ -1,670 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -"use strict"; - -// dep: geo.csg -// dep: kiri-mode.cam.driver -gapp.register("kiri-mode.cam.animate2", [], (root) => { - -const { kiri } = root; -const { driver } = kiri; -const { CAM } = driver; - -// ---( CLIENT FUNCTIONS )--- - -kiri.load(() => { - if (!kiri.client) { - return; - } - - let meshes = {}, - button = {}, - label = {}, - material, - speedPauses = [ 0, 0, 0, 0, 0 ], - speedValues = [ 1, 2, 4, 8, 32 ], - speedNames = [ "1x", "2x", "4x", "8x", "!!" ], - speedMax = speedValues.length - 1, - speedIndex = 0, - speed, - origin, - color = 0; - - const { moto } = root; - const { space } = moto; - const { api } = kiri; - - function animate_clear2(api) { - let { anim } = api.ui; - kiri.client.animate_cleanup2(); - Object.keys(meshes).forEach(id => deleteMesh(id)); - api.widgets.setAxisIndex(0); - api.uc.setVisible(anim.laba, true); - api.uc.setVisible(anim.vala, true); - anim.vala.value = "0.0"; -} - - function animate2(api, delay) { - let alert = api.alerts.show("building animation"); - let settings = api.conf.get(); - kiri.client.animate_setup2(settings, data => { - handleUpdate(data); - if (data) { - return; - } - - let { anim } = api.ui; - Object.assign(button, { - replay: anim.replay, - play: anim.play, - step: anim.step, - pause: anim.pause, - speed: anim.speed, - trans: anim.trans, - model: anim.model, - shade: anim.shade - }); - Object.assign(label, { - progress: anim.progress, - speed: anim.labspd, - x: anim.valx, - y: anim.valy, - z: anim.valz, - a: anim.vala - }); - - updateSpeed(0); - setTimeout(step, delay || 0); - toggleTrans(undefined, false); - origin = settings.origin; - - button.replay.onclick = replay; - button.play.onclick = play; - button.step.onclick = step; - button.pause.onclick = pause; - button.speed.onclick = fast; - button.trans.onclick = toggleTrans; - button.model.onclick = toggleModel; - button.shade.onclick = toggleStock; - button.play.style.display = ''; - button.pause.style.display = 'none'; - - api.event.emit('animate', 'CAM'); - api.alerts.hide(alert); - }); - } - - gapp.overlay(kiri.client, { - animate2(data, ondone) { - kiri.client.send("animate2", data, ondone); - }, - - animate_setup2(settings, ondone) { - color = settings.controller.dark ? 0x888888 : 0; - material = new THREE.MeshMatcapMaterial({ - flatShading: true, - transparent: false, - opacity: 0.9, - color: 0x888888, - side: THREE.DoubleSide - }); - kiri.client.send("animate_setup2", {settings}, ondone); - }, - - animate_cleanup2(data, ondone) { - kiri.client.send("animate_cleanup2", data, ondone); - } - }); - - gapp.overlay(CAM, { - animate2, - animate_clear2 - }); - - function meshAdd(id, ind, pos, ilen, plen) { - const geo = new THREE.BufferGeometry(); - const pa = plen ? pos.subarray(0, plen * 3) : pos; - const ia = ilen ? ind.subarray(0, ilen) : ind; - geo.setAttribute('position', new THREE.BufferAttribute(pa, 3)); - geo.setIndex(new THREE.BufferAttribute(ia, 1)); - const mesh = new THREE.Mesh(geo, material); - mesh.pos = pos; - mesh.ind = ind; - space.world.add(mesh); - meshes[id] = mesh; - } - - function meshUpdate(id, ind, pos, ilen, plen) { - const mesh = meshes[id]; - if (!mesh) { - return; // animate cancelled - } - const geo = mesh.geometry; - mesh.pos = pos || mesh.pos; - mesh.ind = ind || mesh.ind; - geo.setAttribute('position', new THREE.BufferAttribute(mesh.pos.subarray(0, plen * 3), 3)); - geo.setIndex(new THREE.BufferAttribute(mesh.ind.subarray(0, ilen), 1)); - geo.attributes.position.needsUpdate = true; - geo.index.needsUpdate = true; - space.update(); - } - - function deleteMesh(id) { - space.world.remove(meshes[id]); - delete meshes[id]; - } - - function toggleModel(ev,bool) { - api.local.toggle('cam.anim.model', bool); - api.widgets.all().forEach(w => w.toggleVisibility(bool)); - } - - function toggleStock(ev,bool,set) { - set !== false && api.local.toggle('cam.anim.stock', bool); - return api.event.emit('cam.stock.toggle', bool ?? undefined); - } - - function toggleTrans(ev,bool) { - bool = api.local.toggle('cam.anim.trans', bool); - material.transparent = bool; - material.needsUpdate = true; - } - - function step() { - updateSpeed(); - kiri.client.animate2({speed, steps: 1}, handleUpdate); - } - - function play(opts) { - const { steps } = opts; - updateSpeed(); - if (steps !== 1) { - button.play.style.display = 'none'; - button.pause.style.display = ''; - } - kiri.client.animate2({ - speed, - steps: steps || Infinity, - pause: speedPauses[speedIndex] - }, handleUpdate); - } - - function fast(opts) { - const { steps } = opts; - updateSpeed(1); - button.play.style.display = 'none'; - button.pause.style.display = ''; - kiri.client.animate2({ - speed, - steps: steps || Infinity, - pause: speedPauses[speedIndex] - }, handleUpdate); - } - - function pause() { - button.play.style.display = ''; - button.pause.style.display = 'none'; - kiri.client.animate2({speed: 0}, handleUpdate); - } - - function handleUpdate(data) { - if (!data) { - return; - } - if (data.mesh_add) { - const { id, ind, pos, ilen, plen } = data.mesh_add; - meshAdd(id, ind, pos, ilen, plen); - space.refresh(); - } - if (data.mesh_del) { - deleteMesh(data.mesh_del); - } - if (data.mesh_move) { - const { id, pos } = data.mesh_move; - const mesh = meshes[id]; - if (mesh) { - mesh.position.x = pos.x; - mesh.position.y = pos.y; - mesh.position.z = pos.z; - space.refresh(); - } - label.x.value = (pos.x - origin.x).toFixed(2); - label.y.value = (pos.y + origin.y).toFixed(2); - label.z.value = (pos.z - origin.z).toFixed(2); - } - if (data.stock_index !== undefined) { - api.widgets.setAxisIndex(data.stock_index); - label.a.value = -data.stock_index.toFixed(1); - } - if (data.mesh_index) { - const { id, index } = data.mesh_index; - const mesh = meshes[id]; - if (mesh) { - mesh.rotation.x = (Math.PI / 180) * index; - space.refresh(); - } - } - if (data.mesh_update) { - const { id, ind, pos, ilen, plen } = data.mesh_update; - meshUpdate(id, ind, pos, ilen, plen); - } - if (data && data.progress) { - label.progress.value = (data.progress * 100).toFixed(1); - } - } - - function updateSpeed(inc = 0) { - if (inc === Infinity) { - speedIndex = speedMax; - } else if (inc > 0) { - speedIndex = (speedIndex + inc) % speedValues.length; - } - api.local.set('cam.anim.speed', speedIndex); - speed = speedValues[speedIndex]; - label.speed.value = speedNames[speedIndex]; - } - - function replay() { - animate_clear2(api); - setTimeout(() => { - animate2(api, 50); - }, 250); - } -}); - -// ---( WORKER FUNCTIONS )--- -const { CSG } = root.base; - -let nextMeshID = 1; - -class Stock { - constructor(x, y, z) { - this.id = nextMeshID++; - this.vbuf = undefined; - this.ibuf = undefined; - this.ilen = 0; - this.sends = 0; - this.newbuf = true; - this.subtracts = 0; - this.mesh = CSG.Instance().Manifold.cube([x, y, z], true); - } - - send(send) { - const newbuf = this.newbuf; - const action = this.sends++ === 0 ? 'mesh_add' : 'mesh_update'; - send.data({ [action]: { - id: this.id, - ind: newbuf ? this.ibuf : undefined, - pos: newbuf ? this.vbuf : undefined, - ilen: this.ilen, - plen: this.plen - } }); - this.newbuf = false; - this.sends++; - // console.log({ send: this.id, newbuf, action }); - } - - translate(x, y, z) { - // console.log({ translate: this.id, x, y, z }); - const oldmesh = this.mesh; - this.mesh = this.mesh.translate(x, y, z); - this.bounds = CSG.toBox3(this.mesh); - oldmesh.delete(); - return this; - } - - updateMesh(updates) { - if (this.sends > 0 && this.subtracts === 0) { - return; - } - const subs = this.subtracts; - this.subtracts = 0; - let start = Date.now(); - let mesh = this.mesh.getMesh(); - this.sharedVertexBuffer(mesh.numVert * 3); - this.sharedIndexBuffer(mesh.numTri * 3); - this.vbuf.set(mesh.vertProperties); - this.ibuf.set(mesh.triVerts); - let sub = Date.now(); - updates.push(this); - if (false) console.log({ - update: this.id, - time: sub - start, - subs, - mssub: ((sub - start) / subs).round(3) - }); - } - - subtractTool(toolMesh) { - const oldmesh = this.mesh; - this.mesh = this.mesh.subtract(toolMesh.mesh); - oldmesh.delete(); - this.subtracts++; - } - - sharedVertexBuffer(size) { - const old = this.vbuf; - this.plen = size; - if (old && old.length >= size) { - // console.log({svb: this.id, reuse: size, old: old.length}); - return old; - } - // let buf = new Float32Array(new SharedArrayBuffer(size * 4)); - let buf = new Float32Array(new SharedArrayBuffer(size * 4 + 1024 * 1024)); - // console.log({new_svb: this.id, size, buf}); - this.newbuf = true; - return this.vbuf = buf; - } - - sharedIndexBuffer(size) { - const old = this.ibuf; - this.ilen = size; - if (old && old.length >= size) { - // console.log({sib: this.id, reuse: size, old: old.length}); - return old; - } - // let buf = new Uint32Array(new SharedArrayBuffer(size * 4)); - let buf = new Uint32Array(new SharedArrayBuffer(size * 4 + 1024 * 1024)); - // console.log({new_sib: this.id, size, buf}); - this.newbuf = true; - return this.ibuf = buf; - } -} - -kiri.load(() => { - if (!kiri.worker) { - return; - } - - let stock, center, rez; - let path, pathIndex, tool, tools, last; - - let stockZ; - let stockIndexMsg = false; - let stockSlices; - let stockIndex; - let startTime; - - let toolID = -1; - let toolMesh; - let toolRadius; - let toolUpdateMsg; - - let animateClear = false; - let animating = false; - let moveDist = 0; - let renderDist = 0; - let renderDone = false; - let renderPause = 10; - let renderSteps = 0; - let renderSpeed = 0; - let indexCount = 0; - let updates = 0; - - kiri.worker.animate_setup2 = function(data, send) { - const { settings } = data; - const { process } = settings; - const print = worker.print; - const density = parseInt(settings.controller.animesh) * 1000; - const isIndexed = process.camStockIndexed; - - pathIndex = 0; - path = print.output.flat(); - tools = settings.tools; - stock = settings.stock; - rez = 1/Math.sqrt(density/(stock.x * stock.y)); - - tool = null; - last = null; - animating = false; - animateClear = false; - stockIndex = 0; - indexCount = 0; - startTime = 0; - updates = 0; - stockZ = isIndexed ? 0 : stock.z; - - stockSlices = []; - const { x, y, z } = stock; - const sliceCount = parseInt(settings.controller.animesh || 2000) / 100; - const sliceWidth = stock.x / sliceCount; - for (let i=0; i 0; - startTime = startTime || Date.now(); - renderPath(send); - }; - - kiri.worker.animate_cleanup2 = function(data, send) { - if (animating) { - animateClear = true; - } - }; - - function renderPath(send) { - if (renderDone) { - return; - } - - if (renderSteps-- === 0) { - animating = false; - renderPath(send); - return; - } - - if (animating === false || animateClear || renderSpeed === 0) { - renderUpdate(send); - renderDone = true; - animating = false; - animateClear = false; - send.done(); - return; - } - - let next = path[pathIndex]; - while (next && next.type === 'laser') { - last = next; - next = path[++pathIndex]; - } - - if (!next) { - console.log('animation completed in ', ((Date.now() - startTime)/1000).round(2)); - animating = false; - renderPath(send); - return; - } - pathIndex++; - // console.log(next.point.z); - - if (next.tool && (!tool || tool.getID() !== next.tool.getID())) { - // on real tool change, go to safe Z first - if (tool && last.point) { - let pos = last.point = { - x: last.point.x, - y: last.point.y, - z: stock.z - }; - toolMove(pos); - send.data(toolUpdateMsg); - } - toolUpdate(next.tool.getID(), send); - } - - const id = toolID; - const rezstep = rez * 2; - if (last) { - const lp = last.point, np = next.point; - last = next; - // dwell ops have no point - if (!np || !lp) { - return renderPath(send); - } - let dx = np.x - lp.x, - dy = np.y - lp.y, - dz = np.z - lp.z, - da = Math.abs((np.a || 0) - (lp.a || 0)), - dr = (da / 360) * (2 * Math.PI * Math.max(np.z, lp.z)), - dist = Math.sqrt(dx*dx + dy*dy + dz*dz + dr*dr); - - moveDist += dist; - - // skip moves that are less than grid resolution - if (moveDist < rezstep) { - // console.log('skip', moveDist, rezstep, next); - renderPath(send); - return; - } - - const md = Math.max(Math.abs(dx), Math.abs(dy), Math.abs(dz), dr); - const st = Math.ceil(md / rezstep); - const mx = dx / st, my = dy / st, mz = dz / st; - // const sd = Math.sqrt(mx*mx + my*my + mz*mz + dr*dr); - const sd = Math.sqrt(mx*mx + my*my + Math.min(1,mz*mz) + dr*dr); - const moves = []; - for (let i=0, x=lp.x, y=lp.y, z=lp.z; i= renderSpeed) { - renderDist = 0; - renderUpdate(send); - setTimeout(() => { - renderMoves(id, moves, send, index + 1); - }, renderPause); - return; - } - } - renderPath(send); - } - - // send latest tool position and progress bar - function renderUpdate(send) { - const updated = [] - for (let slice of stockSlices) { - slice.updateMesh(updated); - } - for (let slice of updated) { - slice.send(send); - } - if (toolUpdateMsg) { - send.data(toolUpdateMsg); - } - if (stockIndexMsg) { - send.data({ stock_index: stockIndex }); - for (let slice of stockSlices) { - send.data({ mesh_index: { id: slice.id, index: -stockIndex } }); - } - stockIndexMsg = false; - } - send.data({ progress: pathIndex / path.length }); - updates++; - } - - // move tool mesh animation space, update client - function toolMove(pos) { - toolUpdateMsg = { mesh_move: { id: toolID, pos: { x: pos.x, y: pos.y, z: pos.z } } }; - if (toolMesh.mesh) { - toolMesh.mesh.delete(); - } - toolMesh.mesh = toolMesh.root.translate(pos.x, pos.y, pos.z); - if (pos.a !== undefined) { - let tmp = toolMesh.mesh.rotate([ pos.a, 0, 0 ]); - toolMesh.mesh.delete(); - toolMesh.mesh = tmp; - if (pos.a !== stockIndex) { - stockIndexMsg = true; - stockIndex = pos.a; - } - } - toolMesh.bounds = CSG.toBox3(toolMesh.mesh); - } - - // delete old tool mesh, generate tool mesh, send to client - function toolUpdate(toolid, send) { - if (tool) { - send.data({ mesh_del: toolID }); - } - tool = new CAM.Tool({ tools }, toolid); - const Instance = CSG.Instance(); - const slen = tool.shaftLength() || 15; - const srad = tool.shaftDiameter() / 2; - const flen = tool.fluteLength() || 15; - const frad = toolRadius = tool.fluteDiameter() / 2; - const tlen = slen + flen; // total tool length - let { cylinder, sphere } = Instance.Manifold; - let mesh; - if (tool.isBallMill()) { - mesh = cylinder(tlen - frad * 2, frad, frad, 20, true) - .add(sphere(frad, 20).translate(0, 0, -(tlen - frad * 2)/2)) - .add(cylinder(slen, srad, srad, 20, true).translate(0, 0, flen/2)); - } else if (tool.isTaperMill()) { - const trad = Math.max(tool.tipDiameter() / 2, 0.001); - mesh = cylinder(slen, srad, srad, 20, true).translate(0, 0, slen/2) - .add(cylinder(flen, trad, frad, 20, true).translate(0, 0, -flen/2)); - } else { - mesh = cylinder(tlen, frad, frad, 20, true) - .add(cylinder(slen, srad, srad, 20, true).translate(0, 0, flen/2)); - } - mesh = mesh.translate(0, 0, (tlen - stockZ) / 2); - const raw = mesh.getMesh(); - const vertex = raw.vertProperties; - const index = raw.triVerts; - toolMesh = { root: mesh, index, vertex, bounds: CSG.toBox3(mesh) }; - send.data({ mesh_add: { id:--toolID, ind: index, pos: vertex }}); - } - -}); - -}); diff --git a/src/kiri-mode/cam/client.js b/src/kiri-mode/cam/client.js deleted file mode 100644 index 5e798913..00000000 --- a/src/kiri-mode/cam/client.js +++ /dev/null @@ -1,2315 +0,0 @@ -/** 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', - offset: 'camRegisterOffset', - 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"}), - offset: UC.newInput(LANG.cd_rego_s, {title:LANG.cd_rego_l, convert:UC.toFloat, units:true, }), - 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 register = opType === "register"; - const drillOp = opType === "drill"; - const drillRegister = register && op.rec.axis !== '-' - - console.log({drillRegister},op.rec) - - const drillPath = drillOp || drillRegister; - - if( !drillPath && tool.type == "drill"){ - alerts.show(`Warning: Drills should not be used for non-drill operations.`) - } - else if ( drillPath && 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/kiri-mode/cam/driver.js deleted file mode 100644 index 44220e29..00000000 --- a/src/kiri-mode/cam/driver.js +++ /dev/null @@ -1,189 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -"use strict"; - -// dep: main.kiri -// use: kiri.codec -// use: mesh.tool -gapp.register("kiri-mode.cam.driver", [], (root, exports) => { - -const { kiri } = root; -const { driver } = kiri; -const CAM = driver.CAM = {}; - -CAM.process = { - LEVEL: 1, - ROUGH: 2, - OUTLINE: 3, - CONTOUR_X: 4, - CONTOUR_Y: 5, - TRACE: 6, - DRILL: 7 -}; - -// defer loading until kiri.client and kiri.worker exist -kiri.load(api => { - - if (kiri.client) { - CAM.surface_prep = function(index, ondone) { - kiri.client.sync(); - const settings = api.conf.get(); - kiri.client.send("cam_surfaces", { settings, index }, output => { - ondone(output); - }); - }; - - CAM.surface_show = function(widget) { - widget.selectFaces(Object.values(widget._surfaces).flat()); - }; - - CAM.surface_toggle = function(widget, face, radians, ondone) { - let surfaces = widget._surfaces = widget._surfaces || {}; - for (let [root, faces] of Object.entries(surfaces)) { - if (faces.contains(face)) { - // delete this face group - delete surfaces[root]; - CAM.surface_show(widget); - ondone(Object.keys(surfaces).map(v => parseInt(v))); - return; - } - } - kiri.client.send("cam_surface_find", { id: widget.id, face, radians }, faces => { - if (faces.length) { - surfaces[face] = faces; - CAM.surface_show(widget); - } - ondone(Object.keys(surfaces).map(v => parseInt(v))); - }); - }; - - CAM.surface_clear = function(widget) { - widget.selectFaces([]); - widget._surfaces = {}; - }; - - CAM.traces = function(ondone, single) { - kiri.client.sync(); - const settings = api.conf.get(); - const widgets = api.widgets.map(); - kiri.client.send("cam_traces", { settings, single }, output => { - const ids = []; - kiri.codec.decode(output).forEach(rec => { - ids.push(rec.id); - widgets[rec.id].traces = rec.traces; - }); - ondone(ids); - }); - }; - - CAM.traces_clear = function(ondone) { - kiri.client.send("cam_traces_clear", {}, () => { - // console.log({clear_traces: true}); - }); - }; - - - CAM.holes = function(indiv,rec,onProgress,onDone) { - kiri.client.sync(); - const settings = api.conf.get(); - return new Promise((res,rej)=>{ - kiri.client.send("cam_holes", { settings, rec, indiv }, output => { - let out = kiri.codec.decode(output) - if(out.progress != undefined){ - //if a progress message, - onProgress(out.progress,out.msg) - }else{ - api.hide.alert(alert) - onDone(out) - res(out) - } - }); - }) - } - } - - if (kiri.worker) { - CAM.surface_prep = function(widget, index) { - if (!widget.tool) { - let tool = widget.tool = new mesh.tool(); - let translate = index ? true : false; - tool.index(widget.getGeoVertices({ unroll: true, translate })); - } - }; - - CAM.surface_find = function(widget, faces, radians) { - CAM.surface_prep(widget); - return widget.tool.findConnectedSurface(faces, radians || 0, 0.0); - }; - - kiri.worker.cam_surfaces = function(data, send) { - const { settings, index } = data; - const widgets = Object.values(kiri.worker.cache); - for (let widget of widgets) { - if (index) { - widget.setIndexed(true); - widget.setAxisIndex(-index); - } else { - widget.setIndexed(false); - widget.setAxisIndex(0); - } - CAM.surface_prep(widget, index); - } - send.done({}); - }; - - kiri.worker.cam_surface_find = function(data, send) { - const { id, face, radians } = data; - const widget = kiri.worker.cache[id]; - const faces = CAM.surface_find(widget, [face], radians); - send.done(faces); - } - - kiri.worker.cam_traces = async function(data, send) { - const { settings, single } = data; - const widgets = Object.values(kiri.worker.cache); - const fresh = []; - for (let widget of widgets) { - if (await CAM.traces(settings, widget, single)) { - fresh.push(widget); - } - } - // const fresh = widgets.filter(widget => CAM.traces(settings, widget, single)); - send.done(kiri.codec.encode(fresh.map(widget => { return { - id: widget.id, - traces: widget.traces, - } } ))); - }; - - kiri.worker.cam_traces_clear = function(data, send) { - for (let widget of Object.values(kiri.worker.cache)) { - delete widget.traces; - delete widget.topo; - } - send.done({}); - }; - - kiri.worker.cam_holes = async function(data, send) { - const { settings, indiv, rec } = data; - const widgets = Object.values(kiri.worker.cache); - const fresh = []; - - for (let [i,widget] of widgets.entries() ) { - if (await CAM.holes(settings, widget, indiv, rec, - ( prog, msg )=>{ send.data({progress: (i/widgets.length)+(prog/widgets.length),msg})} - )){ - fresh.push(widget); - } - } - // const fresh = widgets.filter(widget => CAM.traces(settings, widget, single)); - send.done(kiri.codec.encode(fresh.map(widget => { return { - id: widget.id, - holes: widget.drills, - shadowed:widget.shadowedDrills - } } ))); - } - } - -}); - -}); diff --git a/src/kiri-mode/cam/ops.js b/src/kiri-mode/cam/ops.js deleted file mode 100644 index 1b7a687c..00000000 --- a/src/kiri-mode/cam/ops.js +++ /dev/null @@ -1,1957 +0,0 @@ -/** 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.01)); - 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); - - if (!process.camOutlinePocket) { - cutdir = !cutdir; - } - - // 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 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); - } - 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); - } - 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, - toolZ = widget.track.pos.z, - boundMinX = bounds.min.x, - boundMaxX = bounds.max.x, - boundMinY = bounds.min.y, - boundMaxY = bounds.max.y, - toolOffset = tool.fluteDiameter() * 2, - centerX = (boundMinX + boundMaxX) / 2, - centerY = (boundMinY + boundMaxY) / 2, - cutDepth = op.thru || zThru || 0, - pathPoints = [], - stockToSurfaceOffset = stock.z - bounds.max.z, - startZ = bounds.max.z + stockToSurfaceOffset + toolZ, - endZ = toolZ - cutDepth, - cutOffset = op.offset; - - if (!(stock.x && stock.y && stock.z)) { - return; - } - - switch (op.axis.toLowerCase()) { - case "x": - if (op.points == 3) { - pathPoints.push(newPoint(boundMinX - cutOffset, centerY, 0)); - pathPoints.push(newPoint(boundMaxX + cutOffset, centerY - toolOffset, 0)); - pathPoints.push(newPoint(boundMaxX + cutOffset, centerY + toolOffset, 0)); - } else { - pathPoints.push(newPoint(boundMinX - cutOffset, centerY, 0)); - pathPoints.push(newPoint(boundMaxX + cutOffset, centerY, 0)); - } - break; - case "y": - if (op.points == 3) { - pathPoints.push(newPoint(centerX, boundMinY - cutOffset, 0)); - pathPoints.push(newPoint(centerX - toolOffset, boundMaxY + cutOffset, 0)); - pathPoints.push(newPoint(centerX + toolOffset, boundMaxY + cutOffset, 0)); - } else { - pathPoints.push(newPoint(centerX, boundMinY - cutOffset, 0)); - pathPoints.push(newPoint(centerX, boundMaxY + cutOffset, 0)); - } - break; - case "-": - let halfOffset = toolOffset / 2, - loopMinX = boundMinX - cutOffset, - loopMaxX = boundMaxX + cutOffset, - loopMinY = boundMinY - cutOffset - halfOffset, - loopMaxY = boundMaxY + cutOffset + halfOffset, - deltaX = (loopMaxX - loopMinX - halfOffset) / 4, - deltaY = (loopMaxY - loopMinY - halfOffset * 3) / 4, - poly, currentPoint, currentZ; - function start(z) { - currentZ = z; - currentPoint = { x: loopMinX + halfOffset * 0.5, y: loopMinY + halfOffset * 1.5 }; - poly = newPolygon().add(currentPoint.x, currentPoint.y, z); - } - function move(dx, dy) { - currentPoint.x += dx; - currentPoint.y += dy; - poly.add(currentPoint.x, currentPoint.y, currentZ); - } - function rept(count, step, fn) { - while (count-- > 0) { - fn(step, count === 0); - step = -step; - } - } - for (let z of base.util.lerp(startZ, endZ, op.down)) { - let slice = newSlice(z); - addSlices(slice); - sliceOut.push(slice); - start(z); - rept(4, halfOffset, oy => { - move(0, -oy); - move(deltaX, 0); - }); - rept(4, halfOffset, ox => { - move(ox, 0); - move(0, deltaY); - }); - rept(4, halfOffset, oy => { - move(0, oy); - move(-deltaX, 0); - }); - rept(4, halfOffset, ox => { - move(-ox, 0); - move(0, -deltaY); - }); - 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 (pathPoints.length) { - let slice = newSlice(0,null), polys = []; - pathPoints.forEach(point => { - polys.push(newPolygon() - .append(point.clone().setZ(startZ)) - .append(point.clone().setZ(endZ))); - }); - 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/src/kiri-mode/cam/tools.js b/src/kiri-mode/cam/tools.js deleted file mode 100644 index 7a7ba8c9..00000000 --- a/src/kiri-mode/cam/tools.js +++ /dev/null @@ -1,353 +0,0 @@ -/** 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/drag/driver.js b/src/kiri-mode/drag/driver.js deleted file mode 100644 index 50b6953e..00000000 --- a/src/kiri-mode/drag/driver.js +++ /dev/null @@ -1,14 +0,0 @@ -/** 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/src/kiri-mode/fdm/driver.js b/src/kiri-mode/fdm/driver.js deleted file mode 100644 index 7ab55ca9..00000000 --- a/src/kiri-mode/fdm/driver.js +++ /dev/null @@ -1,85 +0,0 @@ -/** 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/src/kiri-mode/sla/client.js b/src/kiri-mode/sla/client.js deleted file mode 100644 index c65aa6a4..00000000 --- a/src/kiri-mode/sla/client.js +++ /dev/null @@ -1,101 +0,0 @@ -/** 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/src/kiri-mode/wjet/driver.js b/src/kiri-mode/wjet/driver.js deleted file mode 100644 index 8e4bf4b3..00000000 --- a/src/kiri-mode/wjet/driver.js +++ /dev/null @@ -1,14 +0,0 @@ -/** 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/src/kiri-run/minion.js b/src/kiri-run/minion.js deleted file mode 100644 index 3a7da0a1..00000000 --- a/src/kiri-run/minion.js +++ /dev/null @@ -1,175 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -"use strict"; - -// dep: moto.license -// dep: moto.broker -// dep: geo.base -// dep: geo.polygons -// dep: geo.slicer -// dep: geo.wasm -// dep: kiri.codec -// dep: kiri-mode.fdm.post -// dep: kiri-mode.cam.topo -// dep: kiri-mode.cam.topo4 -// use: kiri-mode.cam.slicer -// dep: ext.clip2 -gapp.register("kiri-run.minion", [], (root, exports) => { - -const { base, kiri } = root; -const { polygons } = base; -const { codec } = kiri; - -const POLY = polygons; -const clib = self.ClipperLib; -const ctyp = clib.ClipType; -const ptyp = clib.PolyType; -const cfil = clib.PolyFillType; - -let cache = self.cache = {}; -let name = "unknown"; - -// catch clipper alerts and convert to console messages -self.alert = function(o) { - console.log(o); -}; - -self.onmessage = function(msg) { - let data = msg.data; - let cmd = data.cmd; - (funcs[cmd] || funcs.bad)(data, data.seq, cmd); -}; - -function reply(msg, direct) { - self.postMessage(msg, direct); -} - -function log() { - console.log(`[${name}]`, ...arguments); -} - -const funcs = self.minion = { - label(data, seq) { - name = data.name; - }, - - config: data => { - if (data.base) { - Object.assign(base.config, data.base); - } else { - log({invalid: data}); - } - }, - - union: (data, seq) => { - if (!(data.polys && data.polys.length)) { - reply({ seq, union: codec.encode([]) }); - return; - } - let state = { zeros: [] }; - let polys = codec.decode(data.polys); - let union = POLY.union(polys, data.minarea || 0, true); - reply({ seq, union: codec.encode(union) }, state.zeros); - }, - - topShells: (data, seq) => { - let top = codec.decode(data.top, {full: true}); - let {z, count, offset1, offsetN, fillOffset, opt} = data; - kiri.driver.FDM.doTopShells(z, top, count, offset1, offsetN, fillOffset, opt); - let state = { zeros: [] }; - reply({ seq, top: codec.encode(top, {full: true}) }, state.zeros); - }, - - fill: (data, seq) => { - let polys = codec.decode(data.polys); - let { angle, spacing, minLen, maxLen } = data; - let fill = POLY.fillArea(polys, angle, spacing, [], minLen, maxLen); - let arr = new Float32Array(fill.length * 4); - for (let i=0, p=0; p { - const clip = new clib.Clipper(); - const ctre = new clib.PolyTree(); - const clips = []; - const M = base.config.clipper; - - let lines = data.lines.map(array => { - return codec.decodePointArray2D(array, data.z, (X, Y) => { return {X: X*M, Y: Y*M} }) - }); - let polys = data.polys.map(array => { - return codec.decodePointArray2D(array, data.z, (X, Y) => { return {X: X*M, Y: Y*M} }) - }); - - clip.AddPaths(lines, ptyp.ptSubject, false); - clip.AddPaths(polys, ptyp.ptClip, true); - - const state = { zeros: [] }; - if (clip.Execute(ctyp.ctIntersection, ctre, cfil.pftNonZero, cfil.pftEvenOdd)) { - for (let node of ctre.m_AllPolys) { - clips.push(codec.encode(POLY.fromClipperNode(node, data.z), state.zeros)); - } - } - - reply({ seq, clips }, state.zeros); - }, - - sliceZ: (data, seq) => { - let { z, points, options } = data; - let i = 0, p = 0, realp = new Array(points.length / 3); - while (i < points.length) { - realp[p++] = base.newPoint(points[i++], points[i++], points[i++]).round(3); - } - let state = { zero: [] }; - let output = []; - base.sliceZ(z, realp, { - ...options, - each(out) { output.push(out) } - }).then(() => { - for (let rec of output) { - // lines do not pass codec properly (for now) - delete rec.lines; - } - reply({ seq, output: codec.encode(output) }, state.zeros); - }); - }, - - putCache: msg => { - const { key, data } = msg; - // log({ minion_putCache: key, data }); - if (data) { - cache[key] = data; - } else { - delete cache[key]; - } - }, - - clearCache: msg => { - for (let key in cache) { - delete cache[key]; - } - }, - - wasm: data => { - if (data.enable) { - base.wasm_ctrl.enable(); - } else { - base.wasm_ctrl.disable(); - } - }, - - bad: (data, seq, cmd) => { - reply({ seq, error: `invalid command (${cmd})` }); - } -}; - -moto.broker.publish("minion.started", { funcs, cache, reply, log }); - -}); diff --git a/src/kiri/api.js b/src/kiri/api.js deleted file mode 100644 index 9dae1981..00000000 --- a/src/kiri/api.js +++ /dev/null @@ -1,149 +0,0 @@ -/** 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/src/kiri/alerts.js b/src/kiri/core/alerts.js similarity index 89% rename from src/kiri/alerts.js rename to src/kiri/core/alerts.js index 8ed32689..5cf3c46d 100644 --- a/src/kiri/alerts.js +++ b/src/kiri/core/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/core/api.js b/src/kiri/core/api.js new file mode 100644 index 00000000..c66292a3 --- /dev/null +++ b/src/kiri/core/api.js @@ -0,0 +1,189 @@ +/** 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 } from './consts.js'; +import { device, devices } from './devices.js'; +import { catalog, dialog, event, group, help, hide, image } from './main.js'; +import { modal, mode, probe, process, show, space, util, view } from './main.js'; +import { local as dataLocal } from '../../data/local.js'; +import { noop, ajax, o2js, js2o } from './utils.js'; +import { functions } from './function.js'; +import { platform } from './platform.js'; +import { selection } from './selection.js'; +import { stats } from './stats.js'; +import { newWidget } from './widget.js'; +import { widgets } from './widgets.js'; +import { updateTool } from '../mode/cam/tools.js'; +import { beta, version } from '../../moto/license.js'; +import { space as SPACE } from '../../moto/space.js'; + +import { LANG } from './lang.js'; +import { LOCAL, SETUP, SECURE } from './main.js'; +import { UI } from './ui.js'; + +import web from '../../moto/webui.js'; + +let UC = UI.prefix('kiri').inputAction(settings.conf.update), + und = undefined, + clone = Object.clone, + isHover = false, + feature = { + seed: true, // seed profiles on first use + meta: true, // show selected widget metadata + frame: true, // receive frame events + alert_event: false, // emit alerts as events instead of display + controls: true, // show or not side menus + device_filter: und, // function to limit devices shown + drop_group: und, // optional array to group multi drop + drop_layout: true, // layout on new drop + hoverAdds: false, // when true only searches widget additions + on_key: und, // function override default key handlers + on_key2: [], // allows for multiple key handlers + on_load: und, // function override file drop loads + on_add_stl: und, // legacy override stl drop loads + on_mouse_up: und, // function intercepts mouse up select + on_mouse_down: und, // function intercepts mouse down + work_alerts: true, // allow disabling work progress alerts + pmode: consts.PMODES.SPEED, // preview modes + // hover: false, // when true fires mouse hover events + get hover() { + return isHover; + }, + set hover(b) { + isHover = b; + broker.publish("feature.hover", b); + } + }, + busyVal = 0, + busy = { + val() { return busyVal }, + inc() { api.event.emit("busy", ++busyVal) }, + dec() { api.event.emit("busy", --busyVal) } + }, + onkey = (fn) => { + api.feature.on_key2.push(fn); + }, + doit = { + undo: noop, // do.js + redo: noop // do.js + }, + devel = { + xray(layers, raw) { + let proc = api.conf.get().process, + size = proc.sliceHeight || proc.slaSlice || 1, + base = (proc.firstSliceHeight || size); + layers = Array.isArray(layers) ? layers : [layers]; + proc.xray = layers.map(l => raw ? l : base + l * size - size / 2); + proc.xrayi = layers.slice(); + api.function.slice(); + } + }, + local = { + get: (key) => localGet(key), + getItem: (key) => localGet(key), + getInt: (key) => parseInt(localGet(key)), + getFloat: (key) => parseFloat(localGet(key)), + getBoolean: (key, def = true) => { + let val = localGet(key); + return val === true || val === 'true' || val === def; + }, + toggle: (key, val, def) => localSet(key, val ?? !api.local.getBoolean(key, def)), + put: (key, val) => localSet(key, val), + set: (key, val) => localSet(key, val), + setItem: (key, val) => localSet(key, val), + }, + tweak = { + line_precision(v) { api.work.config({ base: { clipperClean: v } }) }, + gcode_decimals(v) { api.work.config({ base: { gcode_decimals: v } }) } + }; + +function clip(text) { + navigator.clipboard + .writeText(text) + .catch(err => console.error('Clipboard Error:', err)); +} + +function localGet(key) { + let sloc = api.conf.get().local; + return sloc[key] || api.sdb[key]; +} + +function localSet(key, val) { + let sloc = api.conf.get().local; + sloc[key] = api.sdb[key] = val; + return val; +} + +export const api = { + ajax, + beta, + alerts, + busy, + catalog, + client: work, + clip, + clone, + color, + conf: settings.conf, + const: { LANG, LOCAL, SETUP, SECURE, STACKS, ...consts }, + devel, + device, + devices, + dialog, + doit, + event, + feature, + function: functions, + group, + help, + hide, + image, + js2o, + language: LANG, + lists, + local, + modal, + mode, + new: { + widget: newWidget + }, + noop, + o2js, + onkey, + platform, + probe, + process, + sdb: dataLocal, + selection, + settings, + show, + space, + SPACE, + stacks: STACKS, + stats, + tool: { + update: updateTool + }, + tweak, + uc: UC, + ui: {}, + util, + var: { + layer_lo: 0, + layer_hi: 0, + layer_max: 0 + }, + version, + view, + web, + widgets, + work, +}; + +// allow widget to straddle client / worker FOR NOW +self.kiri_api = api; diff --git a/src/kiri/core/boxes.js b/src/kiri/core/boxes.js new file mode 100644 index 00000000..7e6cbaed --- /dev/null +++ b/src/kiri/core/boxes.js @@ -0,0 +1,60 @@ +import { space } from '../../moto/space.js'; + +const { BoxGeometry, Matrix4, MeshPhongMaterial, Quaternion, Vector3, Mesh } = THREE; +const boxes = []; + +let lastBox; + +export function clearboxes() { + boxes.length = 0; +} + +export function getlastbox() { + return lastBox; +} + +export function delbox(name) { + const old = boxes[name]; + if (old) { + old.groupTo.remove(old); + } +} + +export function addbox(point, color, name, dim = {x:1,y:1,z:1,rz:0}, opt = {}) { + delbox(name); + + const box = new Mesh( + new BoxGeometry(dim.x, dim.y, dim.z), + new MeshPhongMaterial({ + transparent: true, + opacity: opt.opacity || 0.5, + color + }) + ); + + if (name) { + boxes[name] = box; + } + + box.position.x = point.x; + box.position.y = point.y; + box.position.z = point.z; + + lastBox = { point, dim }; + + const group = opt.group || space.scene; + group.add(box); + box.groupTo = group; + + if (dim.rz) { + opt.rotate = new Quaternion().setFromAxisAngle(new Vector3(0,0,1), dim.rz); + } + if (opt.rotate) { + opt.matrix = new Matrix4().makeRotationFromQuaternion(opt.rotate); + } + if (opt.matrix) { + box.geometry.applyMatrix4(opt.matrix); + } + + return box; +} diff --git a/src/kiri/client.js b/src/kiri/core/client.js similarity index 82% rename from src/kiri/client.js rename to src/kiri/core/client.js index 82157c71..a6a5a116 100644 --- a/src/kiri/client.js +++ b/src/kiri/core/client.js @@ -1,27 +1,22 @@ /** 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'; +import { codec } from './codec.js'; +import { Widget } from './widget.js'; +import { newPrint } from './print.js'; // this code runs in kiri's main loop -let loc = self.location, - host = loc.hostname, - port = loc.port, - proto = loc.protocol, - debug = self.debug === true, +let debug = self.debug === true, time = Date.now, seqid = 1, syncd = {}, running = {}, worker = null, minions = false, - restarting = false - // occ = new Worker("/kiri/ext/occ-worker.js", {type:"module"}), - ; + restarting = false, + workpath, + poolpath; /** * @param {Function} fn name of function in kiri.worker @@ -52,13 +47,23 @@ function send(fn, data, onreply, zerocopy) { } // code is running in the browser / client context -const client = exports({ +export const client = { send: send, + setWorkPath(path) { + workpath = path; + return client; + }, + + setPoolPath(path) { + poolpath = path; + return client; + }, + pool: { start() { minions = true; - send("pool_start", {}, noop); + send("pool_start", { url: poolpath }, noop); }, stop() { @@ -70,9 +75,21 @@ const client = exports({ newWorker() { if (self.createWorker) { return self.createWorker(); + } else if (self.kiri_work) { + const work = atob(self.kiri_work); + const blob = new Blob([ work ], { type: 'application/javascript' }); + return new Worker(URL.createObjectURL(blob)); } else { - let _ = debug ? '_' : ''; - return new Worker(`/code/kiri_work.js?${_}${gapp.version}`); + let worker = new Worker(workpath || api.const.PATHS.work, { type: 'module' }); + worker.onerror = (error) => { + console.log({ WORKER_ERROR: error }); + error.preventDefault(); + }; + worker.onmessageerror = (error) => { + console.log({ WORKER_MESSAGE_ERROR: error }); + error.preventDefault(); + }; + return worker; } }, @@ -85,6 +102,10 @@ const client = exports({ return current > 0; }, + start() { + client.restart(); + }, + restart() { // prevent re-entry from cancel callback if (restarting) { @@ -108,7 +129,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], @@ -157,7 +178,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])) { @@ -181,7 +202,7 @@ const client = exports({ send("rotate", { settings }, reply => { if (reply.group) { // collect post-rotation data for slice/preview renders - for (let widget of kiri.Widget.Groups.forid(reply.group)) { + for (let widget of Widget.Groups.forid(reply.group)) { widget.belt = reply.belt; } } else if (callback) { @@ -241,7 +262,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); } }); }, @@ -262,10 +283,10 @@ const client = exports({ y: -origin.y, z: origin.z }; - const output = kiri.newPrint().parseSVG(code, offset); + const output = newPrint().parseSVG(code, offset); send("parse_svg", output, reply => { if (reply.parsed) { - done(kiri.codec.decode(reply.parsed)); + done(codec.decode(reply.parsed)); } }); return; @@ -275,7 +296,7 @@ const client = exports({ progress(reply.progress); } if (reply.parsed) { - done(kiri.codec.decode(reply.parsed), reply.maxSpeed, reply.minSpeed); + done(codec.decode(reply.parsed), reply.maxSpeed, reply.minSpeed); } }); }, @@ -330,9 +351,8 @@ const client = exports({ // console.log({ clearCache_reply: reply }); }); } -}); +}; -// start worker -client.restart(); +self.kiri_client = client; -}); +export default client; diff --git a/src/kiri/codec.js b/src/kiri/core/codec.js similarity index 89% rename from src/kiri/codec.js rename to src/kiri/core/codec.js index db11d16c..06c9f896 100644 --- a/src/kiri/codec.js +++ b/src/kiri/core/codec.js @@ -1,32 +1,14 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { Layers } from './layers.js'; +import { newPoint } from '../../geo/point.js'; +import { Polygon, newPolygon } from '../../geo/polygon.js'; +import { Slice, newSlice, Top, newTop } from './slice.js'; +import { Widget, newWidget } from './widget.js'; -// dep: main.kiri -// dep: geo.base -// dep: geo.polygon -// dep: kiri.widget -// dep: kiri.slice -// dep: kiri.layers -gapp.register("kiri.codec", [], (root, exports) => { - -const { base, kiri } = root; const decoders = {}; const freeMem = true; -const codec = exports({ - undef: undefined, - encode: encode, - decode: decode, - registerDecoder: registerDecoder, - allocFloat32Array: allocFloat32Array, - encodePointArray, - decodePointArray, - encodePointArray2D, - decodePointArray2D, - toCodable -}); - const TYPE = { WIDGET: 100, SLICE: 200, @@ -210,7 +192,7 @@ function decodePointArray(array) { const points = new Array(length / 3); for (let vid=0, pid=0; vid < length; ) { - points[pid++] = base.newPoint(array[vid++], array[vid++], array[vid++]); + points[pid++] = newPoint(array[vid++], array[vid++], array[vid++]); } return points; @@ -225,7 +207,7 @@ function decodePointArray2D(array, z, fn) { for (let vid=0, pid=0; vid < length; ) { points[pid++] = fn ? fn(array[vid++], array[vid++]) : - base.newPoint(array[vid++], array[vid++], z); + newPoint(array[vid++], array[vid++], z); } return points; @@ -233,7 +215,7 @@ function decodePointArray2D(array, z, fn) { // ----- Widget Codec ----- -kiri.Widget.prototype.encode = function(state) { +Widget.prototype.encode = function(state) { const json = state._json_; const geo = this.getGeoVertices(); const coded = { @@ -252,7 +234,7 @@ registerDecoder(TYPE.WIDGET, function(v, state) { const id = v.id, group = v.group || id, track = v.track || undefined, - widget = kiri.newWidget(id, kiri.Widget.Groups.forid(group)); + widget = newWidget(id, Widget.Groups.forid(group)); widget.loadVertices(v.json ? v.geo.toFloat32() : v.geo); widget.saved = Date.now(); @@ -266,7 +248,7 @@ registerDecoder(TYPE.WIDGET, function(v, state) { // ----- Slice Codec ----- -kiri.Slice.prototype.encode = function(state) { +Slice.prototype.encode = function(state) { const rv = { type: TYPE.SLICE, z: this.z, @@ -279,7 +261,7 @@ kiri.Slice.prototype.encode = function(state) { }; registerDecoder(TYPE.SLICE, function(v, state) { - let slice = kiri.newSlice(v.z, state.mesh ? state.mesh.newGroup() : null); + let slice = newSlice(v.z, state.mesh ? state.mesh.newGroup() : null); slice.index = v.index; slice.layers = decode(v.layers, state) @@ -289,7 +271,7 @@ registerDecoder(TYPE.SLICE, function(v, state) { // ----- Slice.Top Codec ----- -kiri.Top.prototype.encode = function(state) { +Top.prototype.encode = function(state) { let obj = { type: TYPE.TOP, poly: encode(this.poly, state) @@ -307,7 +289,7 @@ kiri.Top.prototype.encode = function(state) { }; registerDecoder(TYPE.TOP, function(v, state) { - let top = kiri.newTop(decode(v.poly, state)); + let top = newTop(decode(v.poly, state)); if (state.full) { // top.gaps = decode(v.gaps, state); top.last = decode(v.last, state); @@ -320,7 +302,7 @@ registerDecoder(TYPE.TOP, function(v, state) { // ----- Polygon Codec ----- -base.Polygon.prototype.encode = function(state) { +Polygon.prototype.encode = function(state) { if (!state.poly) state.poly = {}; let cached = state.poly[this.id]; @@ -352,10 +334,10 @@ registerDecoder(TYPE.POLY, function(v, state) { const array = v.array; const length = array.length; - const poly = base.newPolygon(); + const poly = newPolygon(); for (let vid = 0; vid < length; ) { - poly.push(base.newPoint(array[vid++], array[vid++], array[vid++])); + poly.push(newPoint(array[vid++], array[vid++], array[vid++])); } state.poly[v.id] = poly; @@ -382,7 +364,7 @@ function encodeLayerPolys(polys, state) { }); } -kiri.Layers.prototype.encode = function(state) { +Layers.prototype.encode = function(state) { let zeros = state.zeros; let enc = { type: TYPE.LAYERS, @@ -419,7 +401,7 @@ kiri.Layers.prototype.encode = function(state) { }; registerDecoder(TYPE.LAYERS, function(v, state) { - const render = new kiri.Layers(); + const render = new Layers(); const { layers } = v; for (let i=0; i -- 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; @@ -215,12 +207,11 @@ const renamed = { drillDwell: "camDrillDwell", drillLift: "camDrillLift", drillingOn: "camDrillingOn", - camPocketOnlyFinish: "camOutlinePocket", camWideCutout: "camOutlineWide", outputClockwise: "camConventional" }; -const conf = exports({ +export const conf = { // --------------- helper functions normalize, device_from_code, @@ -431,9 +422,10 @@ const conf = exports({ camLevelTool: 1000, camLevelSpindle: 1000, camLevelOver: 0.75, + camLevelStepZ: 0, camLevelSpeed: 1000, camLevelDown: 0, - camLevelOver: 0.5, + camLevelInset: 0.5, camLevelStock: true, camRoughTool: 1000, camRoughSpindle: 1000, @@ -494,7 +486,7 @@ const conf = exports({ camTracePlunge: 200, camTraceOffOver: 0, camTraceDogbone: false, - camTraceMerge: true, + camTraceMerge: false, camTraceLines: false, camTraceZTop: 0, camTraceZBottom: 0, @@ -527,6 +519,21 @@ const conf = exports({ camRegisterSpeed: 1000, camRegisterThru: 5, camRegisterOffset: 10, + camHelicalTool: 1000, + camHelicalSpindle: 1000, + camHelicalDownSpeed: 250, + camHelicalSpeed: 1000, + camHelicalDown: 5, + camHelicalBottomFinish: true, + camHelicalThru: 0, + camHelicalOffset: "auto", + camHelicalForceStartAngle: false, + camHelicalStartAngle: 0, + camHelicalOffsetOverride: 0, + camHelicalEntry: false, + camHelicalEntryOffset:0, + camHelicalReverse: false, + camHelicalClockwise: true, camFlipAxis: "X", camFlipOther: "", camLaserEnable: ["M321"], @@ -580,7 +587,8 @@ const conf = exports({ outputInvertY: false, camExpertFast: false, camTrueShadow: false, - camArcTolerance: 0.006, + camArcEnabled: false, + camArcTolerance: 0.15, camArcResolution: 5, camForceZMax: false, camFirstZMax: false, @@ -973,7 +981,7 @@ const conf = exports({ id: genID(), ver: CVER } -}); +}; const settings = conf.template; @@ -992,5 +1000,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/core/consts.js similarity index 92% rename from src/kiri/consts.js rename to src/kiri/core/consts.js index 5ba2d52f..a332391e 100644 --- a/src/kiri/consts.js +++ b/src/kiri/core/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, @@ -94,6 +90,11 @@ const LISTS = { { name: "inside" }, { name: "outside" } ], + helicaloff:[ + { name: "auto" }, + { name: "inside" }, + { name: "outside" }, + ], zanchor: [ { name: "top" }, { name: "middle" }, @@ -145,16 +146,22 @@ const PMODES = { TOOLS: 2 }; +const PATHS = { + work: "../lib/kiri/run/worker.js", + pool: "./minion.js" +}; + const SEED = 'kiri-seed'; -exports({ - PMODES, +export let consts = { COLOR, LISTS, MODES, + PATHS, + PMODES, VIEWS, SEED, beltfact: Math.cos(Math.PI / 4) -}); +}; -}); +export { COLOR, LISTS, MODES, VIEWS, PATHS, PMODES, SEED }; diff --git a/src/kiri/conv.js b/src/kiri/core/conv.js similarity index 93% rename from src/kiri/conv.js rename to src/kiri/core/conv.js index 6408bd82..bd191694 100644 --- a/src/kiri/conv.js +++ b/src/kiri/core/conv.js @@ -13,7 +13,7 @@ const gapp = self.gapp; eval( fs.readFileSync("src/add/array.js").toString() ); eval( fs.readFileSync("src/data/local.js").toString() ); -eval( fs.readFileSync("src/kiri/conf.js").toString() ); +eval( fs.readFileSync("src/kiri/core/conf.js").toString() ); gapp.main(undefined, undefined, root => { let kiri = root.kiri; diff --git a/src/kiri/core/devices.js b/src/kiri/core/devices.js new file mode 100644 index 00000000..0c508851 --- /dev/null +++ b/src/kiri/core/devices.js @@ -0,0 +1,382 @@ +/** 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/kiri-devs.js'; +import { settings, conf as setconf } from './settings.js'; + +export const device = { + clone: cloneDevice, + code: currentDeviceCode, + get: currentDeviceName, + set: selectDevice, + isBelt +}; + +export const devices = { + show: showDevices, + select: selectDevice, + refresh: updateDeviceList, + update_laser_state: updateLaserState +}; + +function isBelt() { + return setconf.get().device.bedBelt; +} + +function currentDeviceName() { + return setconf.get().filter[api.mode.get()]; +} + +function currentDeviceCode() { + return setconf.get().devices[currentDeviceName()]; +} + +function getModeDevices() { + return Object.keys(devlist[ api.mode.get_lower() ]).sort(); +} + +export function showDevices() { + settings.sync.get().then(_showDevices); +} + +function _showDevices() { + updateDeviceList(); + api.modal.show('setup'); +} + +function updateDeviceList() { + renderDevices(getModeDevices()); +} + +function updateDeviceName(newname) { + let selected = api.device.get(), + devs = setconf.get().devices; + if (newname !== selected) { + devs[newname] = devs[selected]; + delete devs[selected]; + selectDevice(newname); + updateDeviceList(); + } +} + +function putLocalDevice(devicename, obj) { + setconf.get().devices[devicename] = obj; + setconf.save(); +} + +function removeLocalDevice(devicename) { + delete setconf.get().devices[devicename]; + setconf.save(); + settings.sync.put(); +} + +function isLocalDevice(devicename) { + return setconf.get().devices[devicename] ? true : false; +} + +function getSelectedDevice() { + return api.device.get(); +} + +function selectDevice(devicename) { + if (isLocalDevice(devicename)) { + setDeviceCode(setconf.get().devices[devicename], devicename); + } else { + let code = devlist[api.mode.get_lower()][devicename]; + if (code) { + setDeviceCode(code, devicename); + } + } +} + +// only for local filters +function cloneDevice() { + let name = `${getSelectedDevice().replace(/\./g,' ')}`; + let code = api.clone(setconf.get().device); + code.mode = api.mode.get(); + if (name.toLowerCase().indexOf('my ') >= 0) { + name = `${name} copy`; + } else { + name = `My ${name}`; + } + putLocalDevice(name, code); + setDeviceCode(code, name); + settings.sync.put(); +} + +function updateLaserState() { + const dev = setconf.get().device; + $('laser-on').style.display = dev.useLaser ? 'flex' : 'none'; + $('laser-off').style.display = dev.useLaser ? 'flex' : 'none'; +} + +function setDeviceCode(code, devicename) { + api.event.emit('device.select', devicename); + try { + if (typeof(code) === 'string') code = js2o(code) || {}; + + let mode = api.mode.get(), + lmode = mode.toLowerCase(), + current = setconf.get(), + local = isLocalDevice(devicename), + dev = current.device = conf.device_from_code(code,mode), + dproc = current.devproc[devicename], // last process name for this device + newdev = dproc === undefined, // first time device is selected + predev = current.filter[mode], // previous device selection + chgdev = predev !== devicename; // device is changing + + // fill missing device fields + conf.fill_cull_once(dev, conf.defaults[lmode].d); + + // first time device use, add any print profiles and set to default if present + if (code.profiles) { + for (let profile of code.profiles) { + let profname = profile.processName; + // if no saved profile by that name for this mode... + if (!current.sproc[mode][profname]) { + console.log('adding profile', profname, 'to', mode); + current.sproc[mode][profname] = profile; + } + // if it's a new device, seed the new profile name as last profile + if (newdev && !current.devproc[devicename]) { + console.log('setting default profile for new device', devicename, 'to', profname); + current.devproc[devicename] = dproc = profname; + } + } + } + + dev.new = false; + dev.deviceName = devicename; + + let { platform, ui, uc } = api; + + ui.deviceBelt.checked = dev.bedBelt; + ui.deviceRound.checked = dev.bedRound; + ui.deviceOrigin.checked = dev.ctOriginCenter || dev.originCenter || dev.bedRound; + ui.fwRetract.checked = dev.fwRetract; + + // add extruder selection buttons + if (dev.extruders) { + let ext = api.lists.extruders = []; + dev.internal = 0; + for (let i=0; i { + 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/core/do.js similarity index 93% rename from src/kiri/do.js rename to src/kiri/core/do.js index 6989c2be..b3b998c6 100644 --- a/src/kiri/do.js +++ b/src/kiri/core/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/core/export.js similarity index 96% rename from src/kiri/export.js rename to src/kiri/core/export.js index f217f5f7..ae3c50cf 100644 --- a/src/kiri/export.js +++ b/src/kiri/core/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 '../mode/laser/driver.js'; +import { SLA as sla_client } from '../mode/sla/client.js'; let printSeq = parseInt(local['kiri-print-seq'] || local['print-seq'] || "0") + 1; @@ -29,7 +19,7 @@ function localSet(key, val) { return api.local.set(key, val); } -function exportFile(options) { +export function exportFile(options) { let mode = api.mode.get(); let names = api.widgets.all().map(w => w.meta ? w.meta.file : undefined) .filter(v => v) @@ -53,7 +43,7 @@ function callExport(callback, mode, names) { let gcode = []; let section = []; let sections = { }; - kiri.client.export(api.conf.get(), (line) => { + client.export(api.conf.get(), (line) => { if (typeof line !== 'string') { if (line.section) { sections[line.section] = section = []; @@ -75,7 +65,7 @@ function callExport(callback, mode, names) { } function callExportLaser(options, names) { - kiri.client.export(api.conf.get(), (line) => { + client.export(api.conf.get(), (line) => { // engine export uses lines // console.log({unexpected_line: line}); }, (output, error) => { @@ -89,14 +79,14 @@ function callExportLaser(options, names) { } function callExportSLA(options, names) { - kiri.client.export(api.conf.get(), (line) => { + client.export(api.conf.get(), (line) => { api.show.progress(line.progress, "exporting"); }, (output, error) => { api.show.progress(0); if (error) { api.show.alert(error, 5); } else { - kiri.driver.SLA.printDownload(output, api, names); + sla_client.printDownload(output, api, names); } }); } @@ -107,7 +97,7 @@ function exportLaserDialog(data, names) { const fileroot = names[0] || "laser"; const filename = `${fileroot}-${(printSeq.toString().padStart(3,"0"))}`; const settings = api.conf.get(); - const driver = kiri.driver.LASER; + const driver = laser_driver; function download_svg() { api.util.download( @@ -207,7 +197,7 @@ function exportGCodeDialog(gcode, sections, info, names) { ajax.onreadystatechange = function() { if (ajax.readyState === 4) { let status = ajax.status; - stats.add(`ua_${api.mode.get_lower()}_print_octo_${status}`); + api.stats.add(`ua_${api.mode.get_lower()}_print_octo_${status}`); if (status >= 200 && status < 300) { api.modal.hide(); } else { @@ -278,11 +268,11 @@ function exportGCodeDialog(gcode, sections, info, names) { ) .then(t => t.text()) .then(t => { - stats.add(`ua_${api.mode.get_lower()}_print_local_ok`); + api.stats.add(`ua_${api.mode.get_lower()}_print_local_ok`); console.log({grid_spool_said: t}); }) .catch(e => { - stats.add(`ua_${api.mode.get_lower()}_print_local_err`); + api.stats.add(`ua_${api.mode.get_lower()}_print_local_err`); console.log({grid_local_spool_error: e}); }) .finally(() => { @@ -394,7 +384,7 @@ function exportGCodeDialog(gcode, sections, info, names) { xhtr.onreadystatechange = function() { if (xhtr.readyState === 4) { let status = xhtr.status; - stats.add(`ua_${api.mode.get_lower()}_print_grid_${status}`); + api.stats.add(`ua_${api.mode.get_lower()}_print_grid_${status}`); if (status >= 200 && status < 300) { let json = js2o(xhtr.responseText); gridhost_tracker(host,json.key); @@ -481,7 +471,7 @@ function exportGCodeDialog(gcode, sections, info, names) { $('print-filament').style.display = fdm ? '' : 'none'; $('print-filename').value = filename; $('print-filesize').value = util.comma(info.bytes); - $('print-filament').value = Math.round(info.distance); + $('print-length').value = Math.round(info.distance); if (set.controller.devel) { $('code-preview-textarea').style.height = "30em"; } @@ -511,7 +501,7 @@ function exportGCodeDialog(gcode, sections, info, names) { }) } } - kiri.client.zip(files, progress => { + client.zip(files, progress => { api.show.progress(progress.percent/100, "generating zip files"); }, 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/core/files.js similarity index 96% rename from src/kiri/files.js rename to src/kiri/core/files.js index ab878c95..0067915e 100644 --- a/src/kiri/files.js +++ b/src/kiri/core/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/core/frame.js similarity index 92% rename from src/kiri/frame.js rename to src/kiri/core/frame.js index 396135ae..601f5cf6 100644 --- a/src/kiri/frame.js +++ b/src/kiri/core/frame.js @@ -1,19 +1,12 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; - -// use: load.stl -// use: load.svg -// use: kiri.api -// use: kiri.platform -// use: kiri.settings -// use: kiri.widget -gapp.register("kiri.frame", [], (root, exports) => { +import { api } from './api.js'; +import { load } from '../../load/file.js'; +import { newWidget } from './widget.js'; +import { VIEWS } from './consts.js'; // add frame message api listener window.addEventListener('message', msg => { - const { load, kiri, moto } = self; - const { api, newWidget } = kiri; const { conf, event, feature, platform, settings, show } = api; if (!feature.frame) return; @@ -126,5 +119,3 @@ window.addEventListener('message', msg => { show.progress(data.progress, data.message); } }); - -}); diff --git a/src/kiri/function.js b/src/kiri/core/function.js similarity index 81% rename from src/kiri/function.js rename to src/kiri/core/function.js index 8c68fd50..b17eb4dd 100644 --- a/src/kiri/function.js +++ b/src/kiri/core/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,16 +125,43 @@ 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 = slicing.length > 1 ? + `${widget.id}_${segNumber++}_${lastMsg}` : + `${segNumber++}_${lastMsg}` + segtimes[key] = mark - startTime; + } + lastMsg = msg; + startTime = mark; + } + // on update + if (update >= 0) { + track[widget.id] = (update || 0) * factor; + totalProgress = 0; + for (let w of slicing) { + totalProgress += (track[w.id] || 0); + } + show.progress(offset + (totalProgress / slicing.length) * scale, msg); + } + } + + function ondone(sliced, error) { let mark = Date.now(); // update UI info if (sliced) { // update segment time if (lastMsg) { - segtimes[`${widget.id}_${segNumber++}_${lastMsg}`] = mark - startTime; + let key = slicing.length > 1 ? + `${widget.id}_${segNumber++}_${lastMsg}` : + `${segNumber++}_${lastMsg}` + segtimes[`${key}`] = mark - startTime; } event.emit('slice', settings.mode); } @@ -169,28 +182,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() { @@ -200,7 +240,10 @@ function prepareSlices(callback, scale = 1, offset = 0) { }; for (let widget of slicing) { // on done - segtimes[`${widget.id}_${segNumber++}_draw`] = widget.render(widget.stack); + let key = slicing.length > 1 ? + `${widget.id}_${segNumber++}_draw` : + `${segNumber++}_draw` + segtimes[`${key}`] = widget.render(widget.stack); // rotate stack for belt beds if (widget.belt) { widget.stack.obj.rotate(widget.belt); @@ -247,9 +290,7 @@ function prepareSlices(callback, scale = 1, offset = 0) { } function preparePreview(callback, scale = 1, offset = 0) { - 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 +346,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 +448,7 @@ function prepareExport() { } api.event.emit("function.export", {mode: settings.mode}); complete.export = true; - kiri.export(...argsave); + exportFile(...argsave); } function cancelWorker() { @@ -417,8 +458,7 @@ function cancelWorker() { } function parseCode(code, type) { - const { conf, event, show, widgets, view } = api; - const { stacks } = kiri; + const { conf, event, show, stacks, widgets, view } = api; const settings = conf.get(); event.emit("code.load", {code, type}); @@ -441,8 +481,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 +495,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/core/init.js b/src/kiri/core/init.js new file mode 100644 index 00000000..65703282 --- /dev/null +++ b/src/kiri/core/init.js @@ -0,0 +1,1879 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $, $c } from '../../moto/webui.js'; +import { api } from './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 './consts.js'; +import { beta, version } from '../../moto/license.js'; +import { settings as set_ctrl } from './settings.js'; +import { init as initCAM } from '../mode/cam/client.js'; +import { init as initDRAG } from '../mode/drag/client.js'; +import { init as initFDM } from '../mode/fdm/client.js'; +import { init as initLaser } from '../mode/laser/client.js'; +import { init as initSLA } from '../mode/sla/client.js'; +import { init as initWEDM } from '../mode/wedm/client.js'; +import { init as initWJET } from '../mode/wjet/client.js'; +import { menu as menuCAM } from '../mode/cam/init-menu.js'; +import { menu as menuFDM } from '../mode/fdm/init-menu.js'; +import { menu as menuLaser } from '../mode/laser/init-menu.js'; +import { menu as menuSLA } from '../mode/sla/init-menu.js'; + +let { CAM, SLA, FDM, LASER, DRAG, WJET, WEDM } = MODES, + { client, catalog, platform, selection, stats } = 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 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 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(); +} + +api.event.on('click.boolean', onBooleanClick); + +function onButtonClick(ev) { + let target = ev.target; + while (target && target.tagName !== 'BUTTON') { + target = target.parentNode; + } + api.event.emit("button.click", target); +} + +api.event.on('click.button', onButtonClick); + +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 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; + + 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'), + + /** 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 */ + ...menuFDM(), + + /** CAM Settings */ + ...menuCAM(), + + /** LASER/DRAG/WJET/WEDM cut tool Settings */ + ...menuLaser(), + + /** SLA SETTINGS */ + ...menuSLA(), + + layers: uc.setGroup($("layers")), + + settingsName: $('settingsName'), + settingsSave: $('settingsSave'), + }); + + // run client mode initializations right after UI is bound + // MUST run after main ui menu inits since they rely on passed state + // that createPopOps disrupts (should be refactored) + initCAM(); + initDRAG(); + initFDM(); + initLaser(); + initSLA(); + initWEDM(); + initWJET(); + + // override old style settings two-button menu + ui.settingsSave.onclick = () => { + settingsSave(undefined, ui.settingsName.value); + }; + + // 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('async', true); + 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; + $('mesh-split').onclick = selection.isolateBodies; + $('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 but preserve version root (when present) + let wlp = WIN.location.pathname; + let kio = wlp.indexOf('/kiri/'); + if (kio >= 0) { + history.replaceState({}, '', wlp.substring(0,kio + 6)); + } + + // 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() { + client.start(); + init_lang(); + return api; +} diff --git a/src/kiri/core/lang-en.js b/src/kiri/core/lang-en.js new file mode 120000 index 00000000..7d861b86 --- /dev/null +++ b/src/kiri/core/lang-en.js @@ -0,0 +1 @@ +../../../web/kiri/lang/en.js \ No newline at end of file diff --git a/src/kiri/lang.js b/src/kiri/core/lang.js similarity index 93% rename from src/kiri/lang.js rename to src/kiri/core/lang.js index 8b72c10e..187f590b 100644 --- a/src/kiri/lang.js +++ b/src/kiri/core/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/core/layers.js similarity index 92% rename from src/kiri/layers.js rename to src/kiri/core/layers.js index bbf05ac5..faf15d5f 100644 --- a/src/kiri/layers.js +++ b/src/kiri/core/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 */ + +import './frame.js'; + +import lang from './lang.js'; +import STACKS from './stacks.js'; + +import { $ } from '../../moto/webui.js'; +import { api } from './api.js'; +import { broker } from '../../moto/broker.js'; +import { consts } from './consts.js'; +import { Index } from '../../data/index.js'; +import { local as dataLocal } from '../../data/local.js'; +import { openFiles } from './files.js'; +import { platform } from './platform.js'; +import { selection } from './selection.js'; +import { settings } from './settings.js'; +import { showDevices } from './devices.js'; +import { space as SPACE } from '../../moto/space.js'; +import { stats } from './stats.js'; +import { noop, utils } from './utils.js'; +import { version } from '../../moto/license.js'; +import { Widget, newWidget } from './widget.js'; +import { showTools } from '../mode/cam/tools.js'; + +let { parseOpt, o2js, js2o, ls2o } = utils, + { COLOR, MODES, VIEWS } = consts, + LANG = lang.current, + WIN = self.window, + DOC = self.document, + LOC = self.location, + SETUP = parseOpt(LOC.search.substring(1)), + SECURE = isSecure(LOC.protocol), + LOCAL = self.debug && !SETUP.remote, + EVENT = broker, + SDB = dataLocal, + FILES = openFiles(new Index(SETUP.d ? SETUP.d[0] : 'kiri')), + clone = Object.clone, + MODE = MODES.FDM, + viewMode = VIEWS.ARRANGE, + autoSaveTimer = null, + inits = parseInt(SDB.getItem('kiri-init') || stats.get('init') || 0) + 1; + +// update version and init count +SDB.setItem('kiri-init', inits); +stats.set('init', inits); +stats.set('kiri', version); + +// allow widget to straddle client / worker FOR NOW +self.kiri_catalog = FILES; + +export const dialog = { + show: showModal, + hide: hideModal, + update_process_list: updateProcessList +}; + +export const help = { + show: showHelp, + file: showHelpFile +}; + +export const event = { + on(t,l) { return EVENT.on(t,l) }, + emit(t,m,o) { return EVENT.publish(t,m,o) }, + bind(t,m,o) { return EVENT.bind(t,m,o) }, + alerts(clr) { api.alerts.update(clr) }, + import: loadFile, + settings: triggerSettingsEvent +}; + +export const group = { + merge: groupMerge, + split: groupSplit, +}; + +export const hide = { + alert(rec, recs) { api.alerts.hide(...arguments) }, + import: noop, + slider: hideSlider +}; + +export const image = { + dialog: loadImageDialog, + convert: loadImageConvert +}; + +export const modal = { + show: showModal, + hide: hideModal, + visible: modalShowing +}; + +export const mode = { + get_id() { return MODE }, + get_lower: getModeLower, + get: getMode, + set: setMode, + switch: switchMode, + set_expert: noop, + is_fdm() { return MODE === MODES.FDM }, + is_cam() { return MODE === MODES.CAM }, + is_sla() { return MODE === MODES.SLA }, + is_drag() { return MODE === MODES.DRAG }, + is_wedm() { return MODE === MODES.WEDM }, + is_wjet() { return MODE === MODES.WJET }, + is_laser() { return MODE === MODES.LASER }, + is_2d() { return false || + api.mode.is_drag() || + api.mode.is_wedm() || + api.mode.is_wjet() || + api.mode.is_laser() + } +}; + +export const probe = { + live: "https://live.grid.space", + grid: noop, + local: noop +}; + +export const process = { + code: currentProcessCode, + get: currentProcessName +}; + +export const show = { + alert() { return api.alerts.show(...arguments) }, + controls: setControlsVisible, + devices: showDevices, + import() { api.ui.import.style.display = '' }, + layer: setVisibleLayer, + local: showLocal, + progress: setProgress, + slices: showSlices, + tools: showTools +}; + +export const space = { + reload, + auto_save, + restore: restoreWorkspace, + clear: clearWorkspace, + save: saveWorkspace, + set_focus: setFocus, + update: SPACE.update, + is_dark() { return settings.ctrl().dark } +}; + +export const util = { + isSecure, + download: downloadBlob, + ui2rec() { api.conf.update_from(...arguments) }, + rec2ui() { api.conf.update_fields(...arguments) }, + b64enc(obj) { return base64js.fromByteArray(new TextEncoder().encode(JSON.stringify(obj))) }, + b64dec(obj) { return JSON.parse(new TextDecoder().decode(base64js.toByteArray(obj))) } +}; + +export const view = { + get() { return viewMode }, + set() { setViewMode(...arguments) }, + set_arrange() { api.view.set(VIEWS.ARRANGE) }, + set_slice() { api.view.set(VIEWS.SLICE) }, + set_preview() { api.view.set(VIEWS.PREVIEW) }, + set_animate() { api.view.set(VIEWS.ANIMATE) }, + is_arrange() { return viewMode === VIEWS.ARRANGE }, + is_slice() { return viewMode === VIEWS.SLICE }, + is_preview() { return viewMode === VIEWS.PREVIEW }, + is_animate() { return viewMode === VIEWS.ANIMATE }, + update_stack_labels: updateStackLabelState, + update_slider_max: updateSliderMax, + update_slider: updateSlider, + update_speeds: updateSpeeds, + hide_slices: hideSlices, + snapshot: null, + edges: setEdges, + unit_scale: unitScale, + wireframe: setWireframe +}; + +// add show() to catalog for API +FILES.show = showCatalog; + +// patch broker for api backward compatibility +EVENT.on = (topic, listener) => { + EVENT.subscribe(topic, listener); + return EVENT; +}; + +function updateStackLabelState() { + const settings = api.conf.get(); + // match label checkboxes to preference + for (let label of api.stacks.getLabels()) { + let check = `${settings.mode}-${api.view.get()}-${label}`; + api.stacks.setVisible(label, settings.labels[check] !== false); + } +} + +function setFocus(sel, point) { + if (point) { + SPACE.platform.setCenter(point.x, point.z, point.y); + SPACE.view.setFocus(new THREE.Vector3(point.x, point.y, point.z)); + return; + } + if (sel === undefined) { + sel = api.widgets.all(); + } else if (!Array.isArray) { + sel = [ sel ]; + } else if (sel.length === 0) { + sel = api.widgets.all(); + } + let pos = { x:0, y:0, z:0 }; + for (let widget of sel) { + pos.x += widget.track.pos.x; + pos.y += widget.track.pos.y; + pos.z += widget.track.pos.z; + } + if (sel.length) { + pos.x /= sel.length; + pos.y /= sel.length; + pos.z /= sel.length; + } + let cam_index = api.conf.get().process.camStockIndexed || false; + let focus_z = cam_index ? 0 : platform.top_z() / 2; + SPACE.platform.setCenter(pos.x, -pos.y, focus_z); + SPACE.view.setFocus(new THREE.Vector3(pos.x, focus_z, -pos.y)); +} + +function reload() { + api.event.emit('reload'); + do_reload(100); +} + +function do_reload(time) { + // allow time for async saves to complete and busy to to to zero + setTimeout(() => { + if (api.busy.val() === 0) { + LOC.reload(); + } else { + console.log(`reload deferred on busy=${api.busy.val()}`); + do_reload(250); + } + }, time || 100); +} + +function auto_save() { + if (!settings.ctrl().autoSave) { + return; + } + clearTimeout(autoSaveTimer); + autoSaveTimer = setTimeout(() => { + api.space.save(true); + }, 1000); +} + +/** ****************************************************************** + * Utility Functions + ******************************************************************* */ + +function unitScale() { + return api.mode.is_cam() && settings.ctrl().units === 'in' ? 25.4 : 1; +} + +function triggerSettingsEvent() { + api.event.emit('settings', settings.get()); +} + +function isSecure(proto) { + return proto.toLowerCase().indexOf("https") === 0; +} + +function setProgress(value = 0, msg) { + value = (value * 100).round(4); + api.ui.progress.width = value+'%'; + if (self.debug) { + // console.log(msg, value.round(2)); + api.ui.prostatus.style.display = 'flex'; + if (msg) { + api.ui.prostatus.innerHTML = msg; + } else { + api.ui.prostatus.innerHTML = ''; + } + } +} + +function bound(v,min,max) { + return Math.max(min,Math.min(max,v)); +} + +function updateSpeeds(maxSpeed, minSpeed) { + const { ui } = api; + ui.speeds.style.display = + maxSpeed && + settings.mode !== 'SLA' && + settings.mode !== 'LASER' && + viewMode === VIEWS.PREVIEW && + ui.showSpeeds.checked ? 'block' : ''; + if (maxSpeed) { + const colors = []; + for (let i = 0; i <= maxSpeed; i += maxSpeed / 20) { + colors.push(Math.round(Math.max(i, 1))); + } + api.client.colors(colors, maxSpeed, speedColors => { + const list = []; + Object.keys(speedColors).map(v => parseInt(v)).sort((a, b) => b - a).forEach(speed => { + const color = speedColors[speed]; + const hex = color.toString(16).padStart(6, 0); + const r = (color >> 16) & 0xff; + const g = (color >> 8) & 0xff; + const b = (color >> 0) & 0xff; + const style = `background-color:#${hex}`; + list.push(``); + }); + 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; + } + const LANG = api.language.current; + $('kiri-version').innerHTML = `${LANG.version} ${version}`; + showModal('help'); + api.event.emit('help.show', local); +} + +function showLocal() { + showModal('local'); + api.probe.local((err,data) => { + let devc = 0; + let bind = []; + let html = ['']; + 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/src/kiri/pack.js b/src/kiri/core/pack.js similarity index 94% rename from src/kiri/pack.js rename to src/kiri/core/pack.js index 9d1a1121..2d35e626 100644 --- a/src/kiri/pack.js +++ b/src/kiri/core/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/core/platform.js similarity index 97% rename from src/kiri/platform.js rename to src/kiri/core/platform.js index b5eb769c..194d1f2a 100644 --- a/src/kiri/platform.js +++ b/src/kiri/core/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, newWidget } from './widget.js'; +import { Packer } from './pack.js'; -// dep: moto.space -// dep: moto.webui -// dep: kiri.api -// dep: kiri.consts -// dep: kiri.utils -// dep: kiri.widget -// dep: load.stl -// dep: load.obj -// dep: load.3mf -// dep: load.gbr -// use: kiri-mode.cam.tool -gapp.register("kiri.platform", [], (root, exports) => { - -const { base, kiri, moto, load } = root; -const { api, consts, driver, utils, newWidget, Widget } = kiri; -const { ajax, js2o } = utils; -const { space } = moto; -const { util } = base; -const { COLOR, MODES } = consts; +import { COLOR, MODES } from './consts.js'; +import { THREE } from '../../ext/three.js'; const V0 = new THREE.Vector3(0,0,0); @@ -525,7 +515,7 @@ function platformDelete(widget, defer) { api.event.emit('widget.delete', widget); return; } - kiri.client.clear(widget); + api.client.clear(widget); api.widgets.remove(widget); api.selection.remove(widget); Widget.Groups.remove(widget); @@ -719,12 +709,12 @@ function platformLayout() { mp = [sz.x, sz.y], ms = [mp[0] / 2, mp[1] / 2], c = Widget.Groups.blocks().sort(), - p = new kiri.Pack(ms[0], ms[1], gap).fit(c); + p = new Packer(ms[0], ms[1], gap).fit(c); while (!p.packed) { ms[0] *= 1.1; ms[1] *= 1.1; - p = new kiri.Pack(ms[0], ms[1], gap).fit(c); + p = new Packer(ms[0], ms[1], gap).fit(c); } for (i = 0; i < c.length; i++) { @@ -809,7 +799,7 @@ function platformLoadFiles(files, group) { if (api.conf.get().controller.devel) { api.event.emit('cam.parse.gerber', { data: text }); } else { - kiri.client.gerber2mesh(text, progress => { + api.client.gerber2mesh(text, progress => { api.show.progress(progress, "converting"); }, vertices => { api.show.progress(0); @@ -847,7 +837,7 @@ function platformLoadFiles(files, group) { api.function.parse(data.textDecode('utf-8'), 'gcode'); load_dec(); } else if (issvg) { - loadSVGDialog(opt => { + loadSVGDialog(opt => { group = group || []; let svg = load.SVG.parse(data.textDecode('utf-8'), opt); let ind = 0; @@ -921,7 +911,7 @@ function fitDeviceToWidgets() { } // extend API (api.platform) -const platform = Object.assign(api.platform, { +export const platform = { fit: fitDeviceToWidgets, add: platformAdd, changed: platformChanged, @@ -950,6 +940,4 @@ const platform = Object.assign(api.platform, { show_volume: space.platform.showVolume, top_z() { return topZ }, clear() { api.space.clear(); api.space.save(true) } -}); - -}); +}; diff --git a/src/kiri/print.js b/src/kiri/core/print.js similarity index 97% rename from src/kiri/print.js rename to src/kiri/core/print.js index 61f90e55..6be1665d 100644 --- a/src/kiri/print.js +++ b/src/kiri/core/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}); } @@ -619,10 +613,15 @@ class Print { scope.belt = belt; if (scope.debugE) { - console.log({ bounds, print_time: time.round(2) }); + console.log({ + bounds, + minf, + maxf, + print_time: time.round(2), + output: scope.output + }); } - console.log(scope.output) done({ output: scope.output }); } } @@ -646,8 +645,8 @@ class Output { const { type, center, arcPoints } = (options ?? {}); //speed, tool, type, center, arcPoints - this.point = point; // point to emit - this.emit = emit; // emit (feed for printers, power for lasers, cut for cam) + this.point = point; + this.emit = Number(emit); //convert bools into 0/1 this.speed = speed; this.tool = tool; this.type = type; @@ -680,9 +679,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/core/render.js similarity index 90% rename from src/kiri/render.js rename to src/kiri/core/render.js index 0e0c1090..0567d53d 100644 --- a/src/kiri/render.js +++ b/src/kiri/core/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/core/selection.js similarity index 86% rename from src/kiri/selection.js rename to src/kiri/core/selection.js index c889582b..8349cd26 100644 --- a/src/kiri/selection.js +++ b/src/kiri/core/selection.js @@ -1,16 +1,9 @@ /** 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'; +import { tool as MeshTool } from '../../mesh/tool.js'; const selectedMeshes = []; @@ -214,6 +207,39 @@ function merge() { api.platform.select(w); } +/** + * Isolates the bodies within the selected widgets. If no widgets are selected, + * it processes all available widgets. For each widget, it checks if a tool is + * associated; if not, it creates a new MeshTool and indexes the vertices. + * The function identifies isolated bodies within each widget. If a widget has + * a single body, it remains selected. Otherwise, it creates new widgets for + * each isolated body and selects them while deleting the original widget. + */ +function isolateBodies(){ + let sel = widgets(); + console.log('isolate', sel); + if (sel.length === 0) { + sel = api.widgets.all(); + } + for(let widget of sel){ + if (!widget.tool) { + let tool = widget.tool = new MeshTool(); + tool.index(widget.getGeoVertices({ unroll: true, translate:true })); + } + let bodies = widget.tool.isolateBodies() + if(bodies.length ==1){ + api.platform.select(widget); + }else{ + console.log(bodies,Array.from(bodies.entries())) + for(let [i,verts] of bodies.entries()){ + let w = api.platform.load_verts([], verts, `isolate ${i} of ${ widget.anno.file}`); + api.platform.select(w) + } + api.platform.delete(widget); + } + } +} + function exportWidgets(format = "stl") { let sel = widgets(); if (sel.length === 0) { @@ -296,9 +322,10 @@ function setDisabled(bool) { } // extend API (api.selection) -const selection = Object.assign(api.selection, { +export const selection = { move, merge, + isolateBodies, scale, rotate, mirror, @@ -320,6 +347,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/core/settings.js similarity index 95% rename from src/kiri/settings.js rename to src/kiri/core/settings.js index 768251d1..1831126e 100644 --- a/src/kiri/settings.js +++ b/src/kiri/core/settings.js @@ -1,23 +1,20 @@ /** 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'; +import { JSZip } from '../../ext/jszip-esm.js'; -// dep: kiri.api -// dep: kiri.conf -// dep: kiri.utils -// dep: moto.space -// dep: data.local -// use: kiri.widgets -// use: ext.base64 -gapp.register("kiri.settings", (root, exports) => { - -const { data, kiri, moto, noop } = self; -const { api, conf, consts, utils } = kiri; -const { space } = moto; -const { local } = data; -const { clone } = Object; const { areEqual, ls2o, js2o } = utils; const { COLOR } = consts; +const { clone } = Object; + const localFilterKey ='kiri-gcode-filters'; const localFilters = js2o(local.getItem(localFilterKey)) || []; @@ -225,7 +222,7 @@ function updateSettingsFromFields(setrec, uirec = api.ui, changes) { } // given a settings region, update values of matching bound UI fields -function updateFieldsFromSettings(setrec, uirec = api.ui, trace) { +function updateFieldsFromSettings(setrec, uirec = api.ui, opt = {}) { if (!setrec) { updateFieldsFromSettings(settings.device); updateFieldsFromSettings(settings.process); @@ -233,6 +230,9 @@ function updateFieldsFromSettings(setrec, uirec = api.ui, trace) { updateExtruderFields(settings.device); return; } + if (opt.trace) { + console.log({ setrec, uirec }); + } for (let key in setrec) { if (!setrec.hasOwnProperty(key)) { continue; @@ -242,6 +242,9 @@ function updateFieldsFromSettings(setrec, uirec = api.ui, trace) { continue; } let uie = uirec[key], typ = uie ? uie.type : null; + if (opt.trace) { + console.log({ key, uie, typ, val }); + } if (typ === 'text') { if (uie.setv) { uie.setv(val); @@ -275,6 +278,8 @@ function updateFieldsFromSettings(setrec, uirec = api.ui, trace) { } else { uie.value = ''; } + } else if (opt.trace) { + console.log('skipped', { key, val, typ }); } } } @@ -506,7 +511,7 @@ function settingsExport(opts = {}) { const widgets = api.widgets.all(); const 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 +520,7 @@ function settingsExport(opts = {}) { } const xprt = { settings: setn, - version: kiri.version, + version: version, screen: shot, space: space.info, note: note, @@ -600,7 +605,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) { @@ -655,7 +660,7 @@ function settingsImportUrl(url, ask) { function settingsImportZip(data, ask) { let alert = api.show.alert("Importing Workspace"); - JSZip.loadAsync(data).then(zip => { + new JSZip().loadAsync(data).then(zip => { for (let [key,value] of Object.entries(zip.files)) { if (key === "workspace.json") { value.async("string").then(json => { @@ -760,9 +765,14 @@ function setEnableWASM(bool) { api.event.emit("set.assembly", bool); } -// extend API (api.conf) -Object.assign(api.conf, { - dbo: () => { return ls2o('ws-settings') }, +// merged api for backward compatibility +const apiSet = { + dbo() { return ls2o('ws-settings') }, + dev() { return settings.device }, + proc() { return settings.process }, + ctrl() { return settings.controller }, + mode() { return settings.mode }, + prof() { return settings.sproc[settings.mode] }, get: getSettings, put: putSettings, load: loadSettings, @@ -777,25 +787,26 @@ Object.assign(api.conf, { restore: restoreSettings, export: settingsExport, import: settingsImport, -}); - -// extend API (api.settings) -Object.assign(api.settings, { - get: getSettings, - import: settingsImport, import_zip: settingsImportZip, import_url: settingsImportUrl, import_prusa: settingsPrusaConvert, - dev() { return settings.device }, - proc() { return settings.process }, - ctrl() { return settings.controller }, - mode() { return settings.mode }, - prof() { return settings.sproc[settings.mode] }, sync: { async get() {}, async put() {}, status: false } -}); +}; -}); +// for backward compatibility +apiSet.conf = apiSet; +apiSet.set = apiSet; +apiSet.settings = apiSet; + +export default apiSet; + +// for backward compatibility +export { + apiSet as conf, + apiSet as set, + apiSet as settings +}; diff --git a/src/kiri/slice.js b/src/kiri/core/slice.js similarity index 95% rename from src/kiri/slice.js rename to src/kiri/core/slice.js index c858c082..934f7394 100644 --- a/src/kiri/slice.js +++ b/src/kiri/core/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 { Layers } from './layers.js'; +import { newPoint } from '../../geo/point.js'; +import { polygons as POLY } from '../../geo/polygons.js'; let tracker; @@ -41,7 +35,7 @@ class Slice { */ output() { if (this.layers) return this.layers; - let layers = this.layers = new kiri.Layers(); + let layers = this.layers = new Layers(); if (tracker) { layers.setRotation(-tracker.rotation || 0); } @@ -150,7 +144,7 @@ class Slice { } else { // create top object from object bundle passed back by slicePost() let top = new Top(data.poly); - top.thin_fill = data.thin_fill ? data.thin_fill.map(p => base.newPoint(p.x,p.y,p.z)) : undefined; + top.thin_fill = data.thin_fill ? data.thin_fill.map(p => newPoint(p.x,p.y,p.z)) : undefined; top.fill_lines = data.fill_lines; top.fill_sparse = data.fill_sparse; top.fill_off = data.fill_off; @@ -251,12 +245,10 @@ function newSlice(z, view) { return new Slice(z, view); } -gapp.overlay(kiri, { +export { Top, Slice, newTop, newSlice, setSliceTracker -}); - -}); +}; diff --git a/src/kiri/stack.js b/src/kiri/core/stack.js similarity index 98% rename from src/kiri/stack.js rename to src/kiri/core/stack.js index f4866822..74a47d5b 100644 --- a/src/kiri/stack.js +++ b/src/kiri/core/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/core/stacks.js similarity index 87% rename from src/kiri/stacks.js rename to src/kiri/core/stacks.js index 9e9adf22..a45f862e 100644 --- a/src/kiri/stacks.js +++ b/src/kiri/core/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/core/stats.js similarity index 75% rename from src/kiri/stats.js rename to src/kiri/core/stats.js index abec28ff..d7fcb1a9 100644 --- a/src/kiri/stats.js +++ b/src/kiri/core/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/core/tools.js similarity index 96% rename from src/kiri/tools.js rename to src/kiri/core/tools.js index ef19635d..9ab3a079 100644 --- a/src/kiri/tools.js +++ b/src/kiri/core/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/core/ui.js b/src/kiri/core/ui.js new file mode 100644 index 00000000..b0fcee75 --- /dev/null +++ b/src/kiri/core/ui.js @@ -0,0 +1,865 @@ +/** 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, + toDegsFloat, + isSticky, + setSticky, + newBoolean, + newButton, + newBlank, + newDiv, + newElement, + newExpand, + endExpand, + newGCode, + newGroup, + newLabel, + newInput, + newValue, + newRange, + newRow, + newSelect, + newText, + setGroup, + addUnits, + setUnits, + confirm, + prompt, + alert, + onBlur, + setEnabled(el, bool) { + if (bool) { + el.removeAttribute('disabled'); + } else { + el.setAttribute('disabled',''); + } + }, + setVisible(el, bool) { + UI.setClass(el, 'hide', !bool); + }, + setClass(el, clazz, bool) { + if (bool) { + el.classList.add(clazz); + } else { + el.classList.remove(clazz); + } + } +}; + +function setHidden(map) { + hidden = map; + refresh(); + for (let ctrl of Object.values(heads)) { + ctrl.update(); + } +} + +function onBlur(obj, fn) { + if (Array.isArray(obj)) { + for (let o of obj) onBlur(o, fn); + return; + } + obj.addEventListener('blur', fn); +} + +function alert(message) { + return confirm(message, {ok:true}); +} + +function prompt(message, value) { + return confirm(message, {ok:true, cancel:undefined}, value); +} + +function confirm(message, buttons, input, opt = {}) { + return new Promise((resolve, reject) => { + let { feature } = api; + let onkey_save = feature.on_key; + feature.on_key = key => { + // console.log({ eat_key: key }); + return true; + }; + let dialog = $('dialog'); + let btns = buttons || { + "yes": true, + "no": false + }; + let rnd = Date.now().toString(36); + let html = [ + `
` + ]; + 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 toDegsFloat(){ + let nv = this.value !== '' ? parseFloat(this.value) : null; + if (nv !== null && this.bound) nv = this.bound(nv); + nv = (nv+360)% 360; // bound the val to 0-359.99 + if (this.setv) { + return this.setv(nv); + } else { + this.value = nv; + } + return nv; +} + +function raw() { + return this.value !== '' ? this.value : null; +} + +function setUnits(v) { + if (v !== units) { + units = v; + refresh(); + } +} + +function newLabel(text, opt = {}) { + let label = DOC.createElement('label'); + label.appendChild(DOC.createTextNode(text)); + label.setAttribute("class", "noselect"); + if (opt.class) opt.class.split(' ').forEach(cl => { + label.classList.add(cl); + }) + return label; +} + +function newValue(size = 6, opt = {}) { + let value = DOC.createElement('input'); + value.setAttribute("size", size); + value.setAttribute("readonly", ''); + value.setAttribute("class", "nooutline noselect"); + if (opt.class) opt.class.split(' ').forEach(cl => { + value.classList.add(cl); + }) + return value; +} + +function addId(el, opt = {}) { + if (opt.id) { + el.setAttribute("id", opt.id); + } +} + +function safecall(fn) { + try { + return fn(); + } catch (e) { + // console.log({ safecall_error: e }); + return false; + } +} + +function addModeControls(el, opt = {}) { + el.__opt = opt; + el.showMe = function() { + if (opt.trace) console.log({ showMe: el }); + el.classList.remove('hide'); + }; + el.hideMe = function() { + if (opt.trace) console.log({ hideMe: el }); + el.classList.add('hide'); + }; + el.setVisible = function(show) { + if (opt.trace) console.log({ setVisible: show }); + if (show) el.showMe(); + else el.hideMe(); + }; + el.setMode = function(mode) { + let hidn = hidden[el._group] === true; + let xprt = opt.expert === undefined || (opt.expert === lastExpert); + let show = opt.show ? safecall(opt.show) : true; + let disp = opt.visible ? opt.visible() : true; + let hmod = el.hasMode(mode); + if (opt.trace) console.log({ setMode: mode, xprt, show, disp, hmod, modes:el.modes }); + if (opt.manual) return; + el.setVisible(!hidn && hmod && show && xprt && disp); + } + el.hasMode = function(mode) { + return (el.modes.length === 0) || + (el.modes.contains && el.modes.contains(mode)) || + (el.modes === mode); + } + el.modes = opt.modes || []; + hasModes.push(el); +} + +function newDiv(opt = {}) { + let div = DOC.createElement(opt.tag || 'div'); + addModeControls(div, opt); + (opt.addto || addTo).appendChild(div); + if (opt.addto) lastDiv = addTo = div; + if (opt.class) div.setAttribute('class', opt.class); + if (opt.group !== false) { + lastGroup?.push(div); + div._group = groupName; + } else { + lastGroup = undefined; + groupName = undefined; + } + return div; +} + +function newExpand(label, opt = {}, opteach = {}) { + let div = DOC.createElement('details'); + div.setAttribute('class', opt.class || 'f-col'); + addModeControls(div, opt); + + let summary = DOC.createElement('summary'); + summary.setAttribute('class', opt.class || 'var-row'); + summary.innerHTML = ``; + + div.appendChild( summary ); + div.collapse = () => { + div.removeAttribute('open'); + }; + + lastAddTo = addTo; + addTo.appendChild(div); + addTo = div; + + return div; +} + +function endExpand() { + addTo = lastAddTo; + return addTo; +} + +function isSticky() { + return groupSticky; +} + +function setSticky(bool) { + groupSticky = bool; +} + +function newGCode(label, options) { + let opt = options || {}, + btn = DOC.createElement("button"), + txt = DOC.createElement("textarea"), + area = opt.area; + + txt.setAttribute("wrap", "off"); + txt.setAttribute("spellcheck", "false"); + txt.setAttribute("style", "resize: none"); + txt.onblur = bindTo || inputAction; + txt.button = btn; + + btn.setAttribute("class", "basis-50"); + btn.appendChild(DOC.createTextNode(label)); + btn.setAttribute("title", opt.title || undefined); + btn.onclick = function(ev) { + ev.stopPropagation(); + if (ev.target === txt) { + // drop clicks on TextArea + ev.target.focus(); + } else { + let fc = area.firstChild; + if (fc) area.removeChild(fc); + area.appendChild(txt); + txt.scrollTop = 0; + txt.scrollLeft = 0; + txt.selectionEnd = 0; + let rows = txt.value.split('\n'); + let cols = 0; + rows.forEach(row => { + cols = Math.max(cols, row.length); + }); + let showing = btn === lastBtn; + if (lastTxt) { + lastTxt.classList.remove('txt-sel'); + } + if (lastBtn) { + lastBtn.classList.remove('btn-sel'); + } + if (!showing) { + btn.classList.add('btn-sel'); + lastTxt = btn; + lastBtn = btn; + txt.focus(); + } else { + inputAction(); + } + } + }; + + addModeControls(btn, opt); + + return txt; +} + +function newText(label, options) { + let opt = options || {}, + inline = opt.row, + row = inline ? lastDiv : newDiv(options), + btn = DOC.createElement("button"), + pop = DOC.createElement("div"), + txt = DOC.createElement("textarea"), + area = opt.area; + + txt.setAttribute("wrap", "off"); + txt.setAttribute("spellcheck", "false"); + txt.setAttribute("style", "resize: none"); + txt.onblur = inputAction; + + btn.appendChild(DOC.createTextNode(inline ? label : "edit")); + + btn.onclick = function(ev) { + ev.stopPropagation(); + if (ev.target === txt) { + // drop clicks on TextArea + ev.target.focus(); + } else { + let fc = area.firstChild; + if (fc) area.removeChild(fc); + area.appendChild(txt); + // first time, button click / show + btn.parentNode.onclick = btn.onclick; + txt.scrollTop = 0; + txt.scrollLeft = 0; + txt.selectionEnd = 0; + let rows = txt.value.split('\n'); + let cols = 0; + rows.forEach(row => { + cols = Math.max(cols, row.length); + }); + + let showing = pop === lastPop; + if (lastBtn) { + lastBtn.classList.remove('btn-sel'); + } + if (lastTxt) { + lastTxt.classList.remove('txt-sel'); + } + if (!showing) { + row.classList.add('txt-sel'); + pop.style.display = "flex"; + lastPop = pop; + lastTxt = row; + txt.focus(); + } else { + inputAction(); + } + } + }; + addModeControls(btn, opt); + addId(btn, opt); + + if (!inline) { + row.appendChild(newLabel(label)); + row.setAttribute("class", "var-row"); + } + row.appendChild(btn); + if (opt.title) row.setAttribute("title", options.title); + if (row.setVisible) { + btn.setVisible = row.setVisible; + } + + return txt; +} + +function newInput(label, opt = {}) { + let row = newDiv(opt), + hide = opt.hide, + size = opt.size || 5, + height = opt.height || 0, + ip = height > 1 ? DOC.createElement('textarea') : DOC.createElement('input'), + action = opt.action || bindTo || inputAction; + + row.appendChild(newLabel(label)); + row.appendChild(ip); + row.setAttribute("class", opt.class || "var-row"); + if (height > 1) { + ip.setAttribute("cols", size); + ip.setAttribute("rows", height); + ip.setAttribute("wrap", "off"); + } else { + if (Number.isInteger(size)) { + ip.setAttribute("size", size); + } else { + ip.setAttribute("style", `width:${size}`); + } + } + ip.setAttribute("type", "text"); + ip.setAttribute("spellcheck", "false"); + row.style.display = hide ? 'none' : ''; + if (opt.disabled) ip.setAttribute("disabled", "true"); + if (opt.title) row.setAttribute("title", opt.title); + if (opt.convert) ip.convert = opt.convert.bind(ip); + if (opt.bound) ip.bound = opt.bound; + if (opt.action) action = opt.action; + ip.addEventListener('focus', function(event) { + setSticky(true); + }); + if (action) { + ip.addEventListener('keydown', function(event) { + let key = event.key; + if ( + opt.text || + (key >= '0' && key <= '9') || + key === '.' || + key === '-' || + key === 'Backspace' || + key === 'Delete' || + key === 'ArrowLeft' || + key === 'ArrowRight' || + key === 'Tab' || + event.metaKey || + event.ctrlKey || + (key === ',' && options.comma) + ) { + return; + } + event.preventDefault(); + event.stopPropagation(); + }); + ip.addEventListener('keyup', function(event) { + if (event.keyCode === 13) { + ip.blur(); + } + }); + ip.addEventListener('blur', function(event) { + setSticky(false); + lastChange = ip; + action(event); + if (opt.trigger) { + refresh(opt.trigger === 1 || opt.trigger === true); + } + }); + if (opt.units) { + addUnits(ip, opt.round || 3); + } + } + if (!ip.convert) ip.convert = raw.bind(ip); + ip.setVisible = row.setVisible; + + return ip; +} + +function addUnits(input, round) { + setters.push(input); + input.setv = function(value) { + if (typeof(value) === 'number') { + input.real = value; + input.value = (value / units).round(round); + } else { + input.value = value; + } + // console.log({ value }, input.real, input.value ); + return input.real; + }; + return input; +} + +function newRange(label, options) { + let row = newDiv(options), + ip = DOC.createElement('input'), + hide = options && options.hide, + action = bindTo || inputAction; + + if (label) row.appendChild(newLabel(label)); + row.appendChild(ip); + row.setAttribute("class", "var-row"); + ip.setAttribute("type", "range"); + ip.setAttribute("min", (options && options.min ? options.min : 0)); + ip.setAttribute("max", (options && options.max ? options.max : 100)); + ip.setAttribute("value", 0); + row.style.display = hide ? 'none' : ''; + if (options) { + if (options.title) { + ip.setAttribute("title", options.title); + row.setAttribute("title", options.title); + } + if (options.action) action = options.action; + } + ip.setVisible = row.setVisible; + + return ip; +} + +function newSelect(label, options = {}, source) { + let row = newDiv(options), + ip = DOC.createElement('select'), + hide = options && options.hide, + action = bindTo || inputAction; + + row.appendChild(newLabel(label)); + row.appendChild(ip); + if (Array.isArray(source)) { + ip._source = source; + } else { + row.setAttribute("source", source || "tools"); + } + row.setAttribute("class", "var-row"); + row.style.display = hide ? 'none' : ''; + if (options.convert) ip.convert = options.convert.bind(ip); + if (options.disabled) ip.setAttribute("disabled", "true"); + if (options.title) row.setAttribute("title", options.title); + if (options.action) action = options.action; + ip.setVisible = row.setVisible; + ip.onchange = function(ev) { + lastChange = ip; + action(); + if (options.trigger) { + refresh(); + } + }; + ip.onclick = (ev) => { + groupSticky = true; + }; + // because firefox + ip.onmouseenter = (ev) => { + groupSticky = true; + }; + + return ip; +} + +function newBoolean(label, action = bindTo, opt = {}) { + let row = newDiv(opt), + ip = DOC.createElement('input'), + hide = opt.hide; + + if (label) { + row.appendChild(newLabel(label)); + } + row.appendChild(ip); + row.setAttribute("class", "var-row"); + row.style.display = hide ? 'none' : ''; + ip.setAttribute("type", "checkbox"); + ip.checked = false; + if (opt.disabled) { + ip.setAttribute("disabled", "true"); + } + if (opt.title) { + ip.setAttribute("title", opt.title); + row.setAttribute("title", opt.title); + } + if (action) { + ip.onclick = function(ev) { + action(ip); + if (opt.trigger) { + refresh(); + } + }; + } + ip.setVisible = row.setVisible; + + return ip; +} + +function newBlank(options) { + let opt = options || {}, + row = newDiv(opt), + hide = opt.hide; + + row.isBlank = true; + row.style.display = hide ? 'none' : ''; + + if (!opt.driven) { + row.setAttribute("class", "var-row"); + } + + if (opt.class) { + opt.class.split(' ').forEach(ce => { + row.classList.add(ce); + }); + } + + return row; +} + +// unlike other elements, does not auto-add to a row +function newButton(label, action, opt = {}) { + let b = DOC.createElement('button'); + + b.onclick = function() { + switch (typeof action) { + case "string": + api.event.emit(action); + break; + case "function": + action(...arguments); + break; + } + }; + + if (opt.class) { + opt.class.split(' ').forEach(ce => { + b.classList.add(ce); + }); + } + if (opt.icon) { + let d = DOC.createElement('div'); + d.innerHTML = opt.icon; + b.appendChild(d); + } + if (opt.title) { + b.setAttribute('title', opt.title); + } + if (label) { + b.appendChild(DOC.createTextNode(label)); + } + + addModeControls(b, opt); + addId(b, opt); + + return b; +} + +function newRow(children, options) { + let row = addCollapsableElement((options && options.noadd) ? null : addTo); + if (children) children.forEach(function (c) { row.appendChild(c) }); + addModeControls(row, options); + if (options && options.class) { + options.class.split(' ').forEach(ce => { + row.classList.add(ce); + }); + } + return row; +} diff --git a/src/kiri/utils.js b/src/kiri/core/utils.js similarity index 85% rename from src/kiri/utils.js rename to src/kiri/core/utils.js index d75db786..128220cf 100644 --- a/src/kiri/utils.js +++ b/src/kiri/core/utils.js @@ -1,8 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; - -gapp.register("kiri.utils", [], (root, exports) => { +import { ajax as moto_ajax } from '../../moto/ajax.js'; +import { local as dataLocal } from '../../data/local.js'; function parseOpt(ov) { let opt = {}, kv, kva; @@ -28,7 +27,7 @@ function encodeOpt(opt) { } function ajax(url, fn, rt, po, hd) { - return moto.ajax.new(fn, rt).request(url, po, hd); + return moto_ajax.new(fn, rt).request(url, po, hd); } function o2js(o,def) { @@ -46,7 +45,7 @@ function js2o(s,def) { function ls2o(key,def) { // defer ref b/c it may run in a worker - return js2o(root.data.local.getItem(key),def); + return js2o(dataLocal.getItem(key),def); } // split 24 bit color into [ r, g, b ] @@ -99,11 +98,14 @@ function trackFn(fn, name) { } } -exports({ +function noop() {} + +export { trackFn, areEqual, parseOpt, encodeOpt, + noop, ajax, o2js, js2o, @@ -111,6 +113,19 @@ exports({ avgc, rgb, a2c -}); +}; -}); +export let utils = { + trackFn, + areEqual, + parseOpt, + encodeOpt, + noop, + ajax, + o2js, + js2o, + ls2o, + avgc, + rgb, + a2c +}; diff --git a/src/kiri/widget.js b/src/kiri/core/widget.js similarity index 85% rename from src/kiri/widget.js rename to src/kiri/core/widget.js index c55df903..7e0cd9cf 100644 --- a/src/kiri/widget.js +++ b/src/kiri/core/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/core/widgets.js similarity index 87% rename from src/kiri/widgets.js rename to src/kiri/core/widgets.js index 58d939cd..56c1e9af 100644 --- a/src/kiri/widgets.js +++ b/src/kiri/core/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/kiri-dev/cam/Any.Generic.Grbl.json b/src/kiri/dev/cam/Any.Generic.Grbl.json similarity index 100% rename from src/kiri-dev/cam/Any.Generic.Grbl.json rename to src/kiri/dev/cam/Any.Generic.Grbl.json diff --git a/src/kiri-dev/cam/Any.Generic.LinuxCNC.json b/src/kiri/dev/cam/Any.Generic.LinuxCNC.json similarity index 100% rename from src/kiri-dev/cam/Any.Generic.LinuxCNC.json rename to src/kiri/dev/cam/Any.Generic.LinuxCNC.json diff --git a/src/kiri-dev/cam/Any.Generic.TinyG.json b/src/kiri/dev/cam/Any.Generic.TinyG.json similarity index 100% rename from src/kiri-dev/cam/Any.Generic.TinyG.json rename to src/kiri/dev/cam/Any.Generic.TinyG.json diff --git a/src/kiri-dev/cam/Carbide3D.Nomad.883Pro.json b/src/kiri/dev/cam/Carbide3D.Nomad.883Pro.json similarity index 100% rename from src/kiri-dev/cam/Carbide3D.Nomad.883Pro.json rename to src/kiri/dev/cam/Carbide3D.Nomad.883Pro.json diff --git a/src/kiri-dev/cam/Carbide3D.Shapeoko.3.json b/src/kiri/dev/cam/Carbide3D.Shapeoko.3.json similarity index 100% rename from src/kiri-dev/cam/Carbide3D.Shapeoko.3.json rename to src/kiri/dev/cam/Carbide3D.Shapeoko.3.json diff --git a/src/kiri-dev/cam/Carbide3D.Shapeoko.XL.json b/src/kiri/dev/cam/Carbide3D.Shapeoko.XL.json similarity index 100% rename from src/kiri-dev/cam/Carbide3D.Shapeoko.XL.json rename to src/kiri/dev/cam/Carbide3D.Shapeoko.XL.json diff --git a/src/kiri-dev/cam/Carbide3D.Shapeoko.XXL.json b/src/kiri/dev/cam/Carbide3D.Shapeoko.XXL.json similarity index 100% rename from src/kiri-dev/cam/Carbide3D.Shapeoko.XXL.json rename to src/kiri/dev/cam/Carbide3D.Shapeoko.XXL.json diff --git a/src/kiri-dev/cam/Genmitsu.3018.json b/src/kiri/dev/cam/Genmitsu.3018.json similarity index 100% rename from src/kiri-dev/cam/Genmitsu.3018.json rename to src/kiri/dev/cam/Genmitsu.3018.json diff --git a/src/kiri-dev/cam/Genmitsu.PROVerXL.4030.json b/src/kiri/dev/cam/Genmitsu.PROVerXL.4030.json similarity index 100% rename from src/kiri-dev/cam/Genmitsu.PROVerXL.4030.json rename to src/kiri/dev/cam/Genmitsu.PROVerXL.4030.json diff --git a/src/kiri-dev/cam/Inventables.Carvey.Any.json b/src/kiri/dev/cam/Inventables.Carvey.Any.json similarity index 100% rename from src/kiri-dev/cam/Inventables.Carvey.Any.json rename to src/kiri/dev/cam/Inventables.Carvey.Any.json diff --git a/src/kiri-dev/cam/Inventables.XCarve.1000mm.json b/src/kiri/dev/cam/Inventables.XCarve.1000mm.json similarity index 100% rename from src/kiri-dev/cam/Inventables.XCarve.1000mm.json rename to src/kiri/dev/cam/Inventables.XCarve.1000mm.json diff --git a/src/kiri-dev/cam/Inventables.XCarve.500mm.json b/src/kiri/dev/cam/Inventables.XCarve.500mm.json similarity index 100% rename from src/kiri-dev/cam/Inventables.XCarve.500mm.json rename to src/kiri/dev/cam/Inventables.XCarve.500mm.json diff --git a/src/kiri-dev/cam/Langmuir.MR-1.json b/src/kiri/dev/cam/Langmuir.MR-1.json similarity index 100% rename from src/kiri-dev/cam/Langmuir.MR-1.json rename to src/kiri/dev/cam/Langmuir.MR-1.json diff --git a/src/kiri-dev/cam/Makera.Carvera.json b/src/kiri/dev/cam/Makera.Carvera.json similarity index 100% rename from src/kiri-dev/cam/Makera.Carvera.json rename to src/kiri/dev/cam/Makera.Carvera.json diff --git a/src/kiri-dev/cam/PrintNC.GrblHAL.json b/src/kiri/dev/cam/PrintNC.GrblHAL.json similarity index 100% rename from src/kiri-dev/cam/PrintNC.GrblHAL.json rename to src/kiri/dev/cam/PrintNC.GrblHAL.json diff --git a/src/kiri-dev/cam/RatRig.StrongHold.ONE.750x750mm.json b/src/kiri/dev/cam/RatRig.StrongHold.ONE.750x750mm.json similarity index 100% rename from src/kiri-dev/cam/RatRig.StrongHold.ONE.750x750mm.json rename to src/kiri/dev/cam/RatRig.StrongHold.ONE.750x750mm.json diff --git a/src/kiri-dev/cam/Roland.MDX-50.json b/src/kiri/dev/cam/Roland.MDX-50.json similarity index 100% rename from src/kiri-dev/cam/Roland.MDX-50.json rename to src/kiri/dev/cam/Roland.MDX-50.json diff --git a/src/kiri-dev/cam/ShopBot.Basic.json b/src/kiri/dev/cam/ShopBot.Basic.json similarity index 100% rename from src/kiri-dev/cam/ShopBot.Basic.json rename to src/kiri/dev/cam/ShopBot.Basic.json diff --git a/src/kiri-dev/cam/Sienci.AltMill.json b/src/kiri/dev/cam/Sienci.AltMill.json similarity index 100% rename from src/kiri-dev/cam/Sienci.AltMill.json rename to src/kiri/dev/cam/Sienci.AltMill.json diff --git a/src/kiri-dev/cam/Sienci.LongMill.MK2.json b/src/kiri/dev/cam/Sienci.LongMill.MK2.json similarity index 100% rename from src/kiri-dev/cam/Sienci.LongMill.MK2.json rename to src/kiri/dev/cam/Sienci.LongMill.MK2.json diff --git a/src/kiri-dev/cam/Sienci.LongMill.json b/src/kiri/dev/cam/Sienci.LongMill.json similarity index 100% rename from src/kiri-dev/cam/Sienci.LongMill.json rename to src/kiri/dev/cam/Sienci.LongMill.json diff --git a/src/kiri-dev/cam/Sienci.Mill.One.json b/src/kiri/dev/cam/Sienci.Mill.One.json similarity index 100% rename from src/kiri-dev/cam/Sienci.Mill.One.json rename to src/kiri/dev/cam/Sienci.Mill.One.json diff --git a/src/kiri-dev/cam/Snapmaker.Original.json b/src/kiri/dev/cam/Snapmaker.Original.json similarity index 100% rename from src/kiri-dev/cam/Snapmaker.Original.json rename to src/kiri/dev/cam/Snapmaker.Original.json diff --git a/src/kiri-dev/cam/Techno.Davinci.CNC.json b/src/kiri/dev/cam/Techno.Davinci.CNC.json similarity index 100% rename from src/kiri-dev/cam/Techno.Davinci.CNC.json rename to src/kiri/dev/cam/Techno.Davinci.CNC.json diff --git a/src/kiri-dev/cam/Tormach.24R.json b/src/kiri/dev/cam/Tormach.24R.json similarity index 100% rename from src/kiri-dev/cam/Tormach.24R.json rename to src/kiri/dev/cam/Tormach.24R.json diff --git a/src/kiri-dev/cam/V1Engineering.Lowrider3.json b/src/kiri/dev/cam/V1Engineering.Lowrider3.json similarity index 100% rename from src/kiri-dev/cam/V1Engineering.Lowrider3.json rename to src/kiri/dev/cam/V1Engineering.Lowrider3.json diff --git a/src/kiri-dev/cam/V1Engineering.MPCNC.json b/src/kiri/dev/cam/V1Engineering.MPCNC.json similarity index 100% rename from src/kiri-dev/cam/V1Engineering.MPCNC.json rename to src/kiri/dev/cam/V1Engineering.MPCNC.json diff --git a/src/kiri-dev/drag/Any.Generic.DragKnife.json b/src/kiri/dev/drag/Any.Generic.DragKnife.json similarity index 100% rename from src/kiri-dev/drag/Any.Generic.DragKnife.json rename to src/kiri/dev/drag/Any.Generic.DragKnife.json diff --git a/src/kiri-dev/fdm/Any.Generic.Klipper.json b/src/kiri/dev/fdm/Any.Generic.Klipper.json similarity index 100% rename from src/kiri-dev/fdm/Any.Generic.Klipper.json rename to src/kiri/dev/fdm/Any.Generic.Klipper.json diff --git a/src/kiri-dev/fdm/Any.Generic.Marlin.json b/src/kiri/dev/fdm/Any.Generic.Marlin.json similarity index 100% rename from src/kiri-dev/fdm/Any.Generic.Marlin.json rename to src/kiri/dev/fdm/Any.Generic.Marlin.json diff --git a/src/kiri-dev/fdm/Anycubic.Chiron.json b/src/kiri/dev/fdm/Anycubic.Chiron.json similarity index 100% rename from src/kiri-dev/fdm/Anycubic.Chiron.json rename to src/kiri/dev/fdm/Anycubic.Chiron.json diff --git a/src/kiri-dev/fdm/Anycubic.Kobra.2.json b/src/kiri/dev/fdm/Anycubic.Kobra.2.json similarity index 100% rename from src/kiri-dev/fdm/Anycubic.Kobra.2.json rename to src/kiri/dev/fdm/Anycubic.Kobra.2.json diff --git a/src/kiri-dev/fdm/Anycubic.Kobra.json b/src/kiri/dev/fdm/Anycubic.Kobra.json similarity index 100% rename from src/kiri-dev/fdm/Anycubic.Kobra.json rename to src/kiri/dev/fdm/Anycubic.Kobra.json diff --git a/src/kiri-dev/fdm/Anycubic.Predator.json b/src/kiri/dev/fdm/Anycubic.Predator.json similarity index 100% rename from src/kiri-dev/fdm/Anycubic.Predator.json rename to src/kiri/dev/fdm/Anycubic.Predator.json diff --git a/src/kiri-dev/fdm/Anycubic.i3.Mega.json b/src/kiri/dev/fdm/Anycubic.i3.Mega.json similarity index 100% rename from src/kiri-dev/fdm/Anycubic.i3.Mega.json rename to src/kiri/dev/fdm/Anycubic.i3.Mega.json diff --git a/src/kiri-dev/fdm/Atom.v2.json b/src/kiri/dev/fdm/Atom.v2.json similarity index 100% rename from src/kiri-dev/fdm/Atom.v2.json rename to src/kiri/dev/fdm/Atom.v2.json diff --git a/src/kiri-dev/fdm/Bambu.A1.json b/src/kiri/dev/fdm/Bambu.A1.json similarity index 100% rename from src/kiri-dev/fdm/Bambu.A1.json rename to src/kiri/dev/fdm/Bambu.A1.json diff --git a/src/kiri-dev/fdm/Bambu.P1S.json b/src/kiri/dev/fdm/Bambu.P1S.json similarity index 100% rename from src/kiri-dev/fdm/Bambu.P1S.json rename to src/kiri/dev/fdm/Bambu.P1S.json diff --git a/src/kiri-dev/fdm/Creality.CR-10S-Pro.json b/src/kiri/dev/fdm/Creality.CR-10S-Pro.json similarity index 100% rename from src/kiri-dev/fdm/Creality.CR-10S-Pro.json rename to src/kiri/dev/fdm/Creality.CR-10S-Pro.json diff --git a/src/kiri-dev/fdm/Creality.CR-10S.json b/src/kiri/dev/fdm/Creality.CR-10S.json similarity index 100% rename from src/kiri-dev/fdm/Creality.CR-10S.json rename to src/kiri/dev/fdm/Creality.CR-10S.json diff --git a/src/kiri-dev/fdm/Creality.CR-30.json b/src/kiri/dev/fdm/Creality.CR-30.json similarity index 100% rename from src/kiri-dev/fdm/Creality.CR-30.json rename to src/kiri/dev/fdm/Creality.CR-30.json diff --git a/src/kiri-dev/fdm/Creality.Ender.3.json b/src/kiri/dev/fdm/Creality.Ender.3.json similarity index 100% rename from src/kiri-dev/fdm/Creality.Ender.3.json rename to src/kiri/dev/fdm/Creality.Ender.3.json diff --git a/src/kiri-dev/fdm/Creality.Ender.5.json b/src/kiri/dev/fdm/Creality.Ender.5.json similarity index 100% rename from src/kiri-dev/fdm/Creality.Ender.5.json rename to src/kiri/dev/fdm/Creality.Ender.5.json diff --git a/src/kiri-dev/fdm/Creality.Ender.6.json b/src/kiri/dev/fdm/Creality.Ender.6.json similarity index 100% rename from src/kiri-dev/fdm/Creality.Ender.6.json rename to src/kiri/dev/fdm/Creality.Ender.6.json diff --git a/src/kiri-dev/fdm/Creality.K1.json b/src/kiri/dev/fdm/Creality.K1.json similarity index 100% rename from src/kiri-dev/fdm/Creality.K1.json rename to src/kiri/dev/fdm/Creality.K1.json diff --git a/src/kiri-dev/fdm/Creality.Sermoon.D1.json b/src/kiri/dev/fdm/Creality.Sermoon.D1.json similarity index 100% rename from src/kiri-dev/fdm/Creality.Sermoon.D1.json rename to src/kiri/dev/fdm/Creality.Sermoon.D1.json diff --git a/src/kiri-dev/fdm/Cubicon.Single+.json b/src/kiri/dev/fdm/Cubicon.Single+.json similarity index 100% rename from src/kiri-dev/fdm/Cubicon.Single+.json rename to src/kiri/dev/fdm/Cubicon.Single+.json diff --git a/src/kiri-dev/fdm/Cubicon.Style.json b/src/kiri/dev/fdm/Cubicon.Style.json similarity index 100% rename from src/kiri-dev/fdm/Cubicon.Style.json rename to src/kiri/dev/fdm/Cubicon.Style.json diff --git a/src/kiri-dev/fdm/Dagoma.Disco.Ult.V2.json b/src/kiri/dev/fdm/Dagoma.Disco.Ult.V2.json similarity index 100% rename from src/kiri-dev/fdm/Dagoma.Disco.Ult.V2.json rename to src/kiri/dev/fdm/Dagoma.Disco.Ult.V2.json diff --git a/src/kiri-dev/fdm/DeltaMaker.2T.json b/src/kiri/dev/fdm/DeltaMaker.2T.json similarity index 100% rename from src/kiri-dev/fdm/DeltaMaker.2T.json rename to src/kiri/dev/fdm/DeltaMaker.2T.json diff --git a/src/kiri-dev/fdm/DeltaMaker.Original.json b/src/kiri/dev/fdm/DeltaMaker.Original.json similarity index 100% rename from src/kiri-dev/fdm/DeltaMaker.Original.json rename to src/kiri/dev/fdm/DeltaMaker.Original.json diff --git a/src/kiri-dev/fdm/FLSUN.QQ-S.json b/src/kiri/dev/fdm/FLSUN.QQ-S.json similarity index 100% rename from src/kiri-dev/fdm/FLSUN.QQ-S.json rename to src/kiri/dev/fdm/FLSUN.QQ-S.json diff --git a/src/kiri-dev/fdm/Flashforge.Finder.json b/src/kiri/dev/fdm/Flashforge.Finder.json similarity index 100% rename from src/kiri-dev/fdm/Flashforge.Finder.json rename to src/kiri/dev/fdm/Flashforge.Finder.json diff --git a/src/kiri-dev/fdm/Flashforge.Guider.II.json b/src/kiri/dev/fdm/Flashforge.Guider.II.json similarity index 100% rename from src/kiri-dev/fdm/Flashforge.Guider.II.json rename to src/kiri/dev/fdm/Flashforge.Guider.II.json diff --git a/src/kiri-dev/fdm/FolgerTech.FT5.json b/src/kiri/dev/fdm/FolgerTech.FT5.json similarity index 100% rename from src/kiri-dev/fdm/FolgerTech.FT5.json rename to src/kiri/dev/fdm/FolgerTech.FT5.json diff --git a/src/kiri-dev/fdm/GridBot.One.json b/src/kiri/dev/fdm/GridBot.One.json similarity index 100% rename from src/kiri-dev/fdm/GridBot.One.json rename to src/kiri/dev/fdm/GridBot.One.json diff --git a/src/kiri-dev/fdm/GridBot.Two.json b/src/kiri/dev/fdm/GridBot.Two.json similarity index 100% rename from src/kiri-dev/fdm/GridBot.Two.json rename to src/kiri/dev/fdm/GridBot.Two.json diff --git a/src/kiri-dev/fdm/IMADE3D.JellyBOX.1.N0.4.json b/src/kiri/dev/fdm/IMADE3D.JellyBOX.1.N0.4.json similarity index 100% rename from src/kiri-dev/fdm/IMADE3D.JellyBOX.1.N0.4.json rename to src/kiri/dev/fdm/IMADE3D.JellyBOX.1.N0.4.json diff --git a/src/kiri-dev/fdm/IMADE3D.JellyBOX.2.N0.4.json b/src/kiri/dev/fdm/IMADE3D.JellyBOX.2.N0.4.json similarity index 100% rename from src/kiri-dev/fdm/IMADE3D.JellyBOX.2.N0.4.json rename to src/kiri/dev/fdm/IMADE3D.JellyBOX.2.N0.4.json diff --git a/src/kiri-dev/fdm/IMADE3D.JellyBOX.3.N0.4.json b/src/kiri/dev/fdm/IMADE3D.JellyBOX.3.N0.4.json similarity index 100% rename from src/kiri-dev/fdm/IMADE3D.JellyBOX.3.N0.4.json rename to src/kiri/dev/fdm/IMADE3D.JellyBOX.3.N0.4.json diff --git a/src/kiri-dev/fdm/IMADE3D.JellyBOX.4.N0.4.json b/src/kiri/dev/fdm/IMADE3D.JellyBOX.4.N0.4.json similarity index 100% rename from src/kiri-dev/fdm/IMADE3D.JellyBOX.4.N0.4.json rename to src/kiri/dev/fdm/IMADE3D.JellyBOX.4.N0.4.json diff --git a/src/kiri-dev/fdm/IMADE3D.JellyBOX.4.N0.6.json b/src/kiri/dev/fdm/IMADE3D.JellyBOX.4.N0.6.json similarity index 100% rename from src/kiri-dev/fdm/IMADE3D.JellyBOX.4.N0.6.json rename to src/kiri/dev/fdm/IMADE3D.JellyBOX.4.N0.6.json diff --git a/src/kiri-dev/fdm/Leapfrog.CreatrHS.json b/src/kiri/dev/fdm/Leapfrog.CreatrHS.json similarity index 100% rename from src/kiri-dev/fdm/Leapfrog.CreatrHS.json rename to src/kiri/dev/fdm/Leapfrog.CreatrHS.json diff --git a/src/kiri-dev/fdm/MakerGear.M2.json b/src/kiri/dev/fdm/MakerGear.M2.json similarity index 100% rename from src/kiri-dev/fdm/MakerGear.M2.json rename to src/kiri/dev/fdm/MakerGear.M2.json diff --git a/src/kiri-dev/fdm/Makerbot.Replicator2.json b/src/kiri/dev/fdm/Makerbot.Replicator2.json similarity index 100% rename from src/kiri-dev/fdm/Makerbot.Replicator2.json rename to src/kiri/dev/fdm/Makerbot.Replicator2.json diff --git a/src/kiri/dev/fdm/My.Bambu.P1S b/src/kiri/dev/fdm/My.Bambu.P1S new file mode 100644 index 00000000..4aee21dd --- /dev/null +++ b/src/kiri/dev/fdm/My.Bambu.P1S @@ -0,0 +1,535 @@ +{ + "mode": "FDM", + "internal": 0, + "bedHeight": 2.5, + "bedWidth": 250, + "bedDepth": 250, + "bedRound": false, + "maxHeight": 250, + "originCenter": false, + "gcodeFan": [ + "M106 P1 S{fan_speed}" + ], + "gcodeTrack": [ + "M73 P{progress}" + ], + "gcodeLayer": [ + "; CHANGE_LAYER", + "; layer num/total_layer_count: {layer}/{layers}", + "M622.1 S1 ; for prev firware, default turned on", + "M1002 judge_flag timelapse_record_flag", + "M622 J1", + "M971 S11 C10 O0 ; timelapse without wipe tower", + "M623", + "M73 L{layer}", + "M991 S0 P{layer-1} ; notify layer change", + ";; IF { layer == 0 }", + "M204 S500", + ";; ELSE", + "M204 S5000", + ";; END" + ], + "gcodePre": [ + ";; DEFINE BAMBU-AMS 2,0,1", + "; ===== HEADER =====================================================================", + "M73 P0 R14 ; Initial progress and time remaining", + "M201 X20000 Y20000 Z500 E5000 ; Set max acceleration", + "M203 X500 Y500 Z20 E30 ; Set max feedrates", + "M204 P20000 R5000 T20000 ; Set acceleration", + "M205 X9.00 Y9.00 Z3.00 E2.50 ; Set advanced settings", + "M106 S0 ; Set part fan speed to 0", + "M106 P2 S0 ; Turn off aux fan", + ";===== machine: P1S ========================", + "M104 S75 ; Set extruder temp to turn on HB fan, prevent filament oozing", + "M710 A1 S255 ; Turn on MC fan by default (P1S)", + ";===== Reset machine status =================", + "M290 X40 Y40 Z2.6666666 ; Set baby stepping offsets", + "G91 ; Set to relative positioning", + "M17 Z0.4 ; Lower Z-motor current", + "G380 S2 Z30 F300 ; Lower hotbed (G380 like G38)", + "G380 S2 Z-25 F300 ; Continue to lower bed", + "G1 Z5 F300 ; Raise bed by 5mm", + "G90 ; Set to absolute positioning", + "M17 X1.2 Y1.2 Z0.75 ; Reset motor current to default", + "M960 S5 P1 ; Turn on logo lamp", + "M220 S100 ; Reset feedrate to 100%", + "M221 S100 ; Reset flowrate to 100%", + "M73.2 R1.0 ; Reset remaining time magnitude", + "M1002 set_gcode_claim_speed_level : 5 ; Set G-code claim speed level", + "M221 X0 Y0 Z0 ; Turn off soft endstop to prevent logic issues", + "G29.1 Z0 ; Clear Z-trim value", + "M204 S10000 ; Initialize acceleration to 10 m/s^2", + ";===== Heatbed preheat ====================", + "M1002 gcode_claim_action : 2 ; Claim action for preheating", + "M140 S{bed_temp} ; Set bed temperature to 65°C", + "M190 S{bed_temp} ; Wait for bed temperature to reach 65°C", + ";===== Turn on fans to prevent PLA jamming =====", + "M106 P3 S180 ; set chamber fan speed to prevent PLA jamming", + "M106 P2 S100 ; Turn on aux fan to cool toolhead", + ";===== Prepare print temperature and material =====", + "M104 S{temp} ; Set extruder temp to 220°C", + "G91 ; Set to relative positioning", + "G0 Z10 F1200 ; Raise Z by 10mm", + "G90 ; Set to absolute positioning", + "G28 X ; Home X axis", + "M975 S1 ; Turn on feature (guess: vibration suppression)", + "M73 P31 R9 ; Update progress and time remaining", + "G1 X60 F12000 ; Fast move to X60", + "G1 Y245 ; Move to Y245", + "G1 Y265 F3000 ; Move to Y265 slowly", + "M620 M ; Custom command (guess: material management)", + "M620 S{tool}A ; Switch material if AMS present", + "M109 S{temp} ; Wait for extruder to reach 220°C", + "G1 X120 F12000 ; Fast move to X120", + "G1 X20 Y50 F12000 ; Fast move to X20 Y50", + "G1 Y-3 ; Move down Y-axis by 3", + "T{tool} ; Select starting tool", + "G1 X54 F12000 ; Fast move to X54", + "G1 Y265 ; Move to Y265", + "M400 ; Wait for all moves to finish", + "M621 S{tool}A ; Custom command", + "M620.1 E F523.843 T240 ; Extrude command with custom parameters", + "M412 S1 ; Turn on filament runout detection", + "M109 S250 ; Set nozzle to common flush temp", + "M106 P1 S0 ; Turn off nozzle fan", + "G92 E0 ; Reset extruder position", + "G1 E50 F200 ; Extrude 50mm of filament", + "M400 ; Wait for moves to finish", + "M104 S{temp} ; Set nozzle temperature to 220°C", + "G92 E0 ; Reset extruder position", + "G1 E50 F200 ; Extrude 50mm of filament", + "M400 ; Wait for moves to finish", + "M106 P1 S255 ; Turn on nozzle fan to full speed", + "G92 E0 ; Reset extruder position", + "G1 E5 F300 ; Extrude 5mm of filament", + "M109 S200 ; Drop nozzle temperature to 200°C to shrink filament", + "G92 E0 ; Reset extruder position", + "M73 P33 R9 ; Update progress and time remaining", + "G1 E-0.5 F300 ; Retract filament by 0.5mm", + "M73 P35 R9 ; Update progress and time remaining", + "G1 X70 F9000 ; Fast move to X70", + "G1 X76 F15000 ; Fast move to X76", + "G1 X65 F15000 ; Fast move to X65", + "G1 X76 F15000 ; Repeat movements to shake and clean nozzle", + "G1 X65 F15000 ; Shake to put down garbage", + "G1 X80 F6000 ; Move to X80", + "G1 X95 F15000 ; Fast move to X95", + "G1 X80 F15000 ; Return to X80", + "G1 X165 F15000 ; Wipe and shake", + "M400 ; Wait for all moves to finish", + "M106 P1 S0 ; Turn off nozzle fan", + ";===== Prepare print temperature and material end =====", + ";===== Wipe nozzle ===============================", + "M1002 gcode_claim_action : 14 ; Claim action for nozzle wipe", + "M975 S1 ; Turn on feature (guess: vibration suppression)", + "M106 S255 ; Turn on part fan to full speed", + "G1 X65 Y230 F18000 ; Fast move to start of wipe", + "G1 Y264 F6000 ; Wipe along Y-axis", + "M109 S200 ; Set nozzle temperature to 200°C", + "G1 X100 F18000 ; Wipe first pass", + "G0 X135 Y253 F20000 ; Move to exposed steel surface edge", + "G28 Z P0 T300 ; Home Z with low precision, permit 300°C temperature", + "G29.2 S0 ; Turn off ABL (auto bed leveling)", + "G0 Z5 F20000 ; Raise Z by 5mm", + "G1 X60 Y265 ; Move to X60 Y265", + "G92 E0 ; Reset extruder position", + "G1 E-0.5 F300 ; Retract more", + "G1 X100 F5000 ; Second wipe pass", + "G1 X70 F15000 ; Repeat movements for further wiping", + "G1 X100 F5000", + "G1 X70 F15000", + "G1 X100 F5000", + "G1 X70 F15000", + "G1 X90 F5000", + "G0 X128 Y261 Z-1.5 F20000 ; Move to exposed steel surface, stop nozzle", + "M104 S140 ; Set temp down to heatbed acceptable", + "M106 S255 ; Turn on part fan (G28 turned off fan)", + "M221 S ; Push soft endstop status", + "M221 Z0 ; Turn off Z axis endstop", + "G0 Z0.5 F20000 ; Raise Z by 0.5mm", + "G0 X125 Y259.5 Z-1.01 ; Move to wiping position", + "G0 X131 F211", + "G0 X124", + "G0 Z0.5 F20000", + "G0 X125 Y262.5", + "G0 Z-1.01", + "G0 X131 F211", + "G0 X124", + "G0 Z0.5 F20000", + "G0 X125 Y260.0", + "G0 Z-1.01", + "G0 X131 F211", + "G0 X124", + "G0 Z0.5 F20000", + "G0 X125 Y262.0", + "G0 Z-1.01", + "G0 X131 F211", + "G0 X124", + "G0 Z0.5 F20000", + "G0 X125 Y260.5", + "G0 Z-1.01", + "G0 X131 F211", + "G0 X124", + "G0 Z0.5 F20000", + "G0 X125 Y261.5", + "G0 Z-1.01", + "G0 X131 F211", + "G0 X124", + "G0 Z0.5 F20000", + "G0 X125 Y261.0", + "G0 Z-1.01", + "G0 X131 F211", + "G0 X124", + "G0 X128", + "G2 I0.5 J0 F300 ; Arc movements for final cleaning", + "M73 P36 R8 ; Update progress and time remaining", + "G2 I0.5 J0 F300", + "G2 I0.5 J0 F300", + "G2 I0.5 J0 F300", + "M109 S140 ; Wait for nozzle temp to reach heatbed acceptable temp", + "G2 I0.5 J0 F3000 ; More arc movements", + "G2 I0.5 J0 F3000", + "G2 I0.5 J0 F3000", + "G2 I0.5 J0 F3000", + "M221 R ; Pop softend status", + "G1 Z10 F1200 ; Raise Z by 10mm", + "M400 ; Wait for all moves to finish", + "G1 Z10 ; Raise Z by 10mm", + "G1 F30000 ; Set fast feedrate", + "G1 X230 Y15 ; Move to X230 Y15", + "G29.2 S1 ; Turn on ABL", + "M106 S0 ; Turn off part fan, too noisy", + ";===== Wipe nozzle end ================================", + ";===== Bed leveling ==================================", + "M1002 judge_flag g29_before_print_flag ; Judge flag before leveling", + "M622 J1", + " M1002 gcode_claim_action : 1 ; Claim action for bed leveling", + " G29 A X115.2 Y115.2 I25.6 J25.6 ; Perform bed leveling", + " M400 ; Wait for all moves to finish", + " M500 ; Save calibration data", + "M623", + ";===== Bed leveling end ================================", + ";===== Home after wipe mouth =========================", + "M1002 judge_flag g29_before_print_flag ; Judge flag before homing", + "M622 J0", + " M1002 gcode_claim_action : 13 ; Claim action for homing", + " G28 ; Home all axes", + "M623", + ";===== Home after wipe mouth end =======================", + "M975 S1 ; Turn on vibration suppression", + ";===== Turn on fans to prevent PLA jamming =====", + "M106 P3 S180 ; set chamber fan speed to Prevent PLA jamming", + "M106 P2 S100 ; Turn on aux fan to cool toolhead", + "M104 S{temp} ; Set extruder temp to 220°C earlier, reduce wait time", + ";===== Mech mode fast check ==========================", + "G1 X128 Y128 Z10 F20000 ; Move to center for check", + "M400 P200 ; Wait and perform check", + "M970.3 Q1 A7 B30 C80 H15 K0 ; Custom command (mechanical check)", + "M974 Q1 S2 P0 ; Custom command", + "G1 X128 Y128 Z10 F20000 ; Move again to center for check", + "M400 P200 ; Wait and perform another check", + "M970.3 Q0 A7 B30 C90 Q0 H15 K0 ; Custom command", + "M974 Q0 S2 P0 ; Custom command", + "M975 S1 ; Turn on feature (vibration suppression)", + "G1 F30000 ; Set fast feedrate", + "M73 P37 R8 ; Update progress and time remaining", + "G1 X230 Y15 ; Move to X230 Y15", + "G28 X ; Re-home X and Y", + ";===== Mech mode fast check end =========================", + ";===== Nozzle load line ===============================", + "M975 S1 ; Turn on vibration suppression", + "G90 ; Set to absolute positioning", + "M83 ; Set extruder to relative mode", + "T1000 ; Custom command", + "G1 X18.0 Y1.0 Z0.8 F18000 ; Move to start position", + "M109 S{temp} ; Set extruder temp to 220°C", + "G1 Z0.2 ; Lower Z to 0.2mm", + "G0 E2 F300 ; Extrude 2mm of filament", + "G0 X240 E25 F5539.96 ; Move to X240 while extruding", + "G0 Y15 E1.166 F1384.99 ; Move along Y while extruding", + "G0 X239.5 ; Move to X239.5", + "G0 E0.2 ; Extrude 0.2mm of filament", + "G0 Y1.5 E1.166 ; Move along Y while extruding", + "G0 X18 E25 F5539.96 ; Return to start position while extruding", + "M400 ; Wait for all moves to finish", + ";===== For Textured PEI Plate ========================", + ";G29.1 Z-0.04 ; Adjust Z-offset for textured PEI plate", + ";===== Turn off light and wait extrude temperature =====", + "M1002 gcode_claim_action : 0 ; Claim action", + "M106 S0 ; Turn off part fan", + "M106 P2 S0 ; Turn off aux fan", + "M106 P3 S0 ; Turn off chamber fan", + "M975 S1 ; Turn on mechanical mode suppression", + "G90 ; Set to absolute positioning", + "G21 ; Set units to millimeters", + "M83 ; Use relative distances for extrusion", + "M106 P3 S200 ; Set chamber fan speed to 200 (prevents PLA jamming)", + "M981 S1 P20000 ; Open spaghetti detector" + ], + "gcodePost": [ + "; ===== FOOTER =====================================================================", + "M106 S0 ; Turn off part cooling fan", + "M106 P2 S0 ; Turn off auxiliary cooling fan", + "M981 S0 P20000 ; Close spaghetti detector", + "M106 P3 S0 ; Turn off chamber fan", + "M400 ; Wait for buffer to clear", + "G92 E0 ; Zero the extruder", + "G1 E-0.8 F1800 ; Retract filament by 0.8mm", + "G91 ; relative moves", + "G1 Z5 F900 ; drop bed 5mm", + "G90 ; absolute moves", + "G1 X65 Y245 F12000 ; Move to safe position X65 Y245", + "G1 Y265 F3000 ; Move to Y265 slowly", + "G1 X65 Y245 F12000 ; Move back to X65 Y245", + "G1 Y265 F3000 ; Move to Y265 slowly again", + "M140 S0 ; Turn off heated bed", + "M106 S0 ; Turn off part cooling fan", + "M106 P2 S0 ; Turn off auxiliary cooling fan", + "M106 P3 S0 ; Turn off chamber cooling fan", + "G1 X100 F12000 ; Wipe nozzle", + "M620 S255 ; Pull back filament to AMS", + "G1 X20 Y50 F12000 ; Move to X20 Y50", + "G1 Y-3 ; Move down Y-axis by 3", + "T255 ; Tool change command (educated guess)", + "G1 X65 F12000 ; Move to X65", + "G1 Y265 ; Move to Y265", + "G1 X100 F12000 ; Wipe nozzle again", + "M621 S255 ; Custom command (educated guess)", + "M104 S0 ; Turn off hotend", + "M622.1 S1 ; Enable feature (default for older firmware)", + "M1002 judge_flag timelapse_record_flag ; Check if timelapse recording is needed", + "M622 J1 ; Enable timelapse", + " M400 ; Wait for all motions to complete", + " M991 S0 P-1 ; End smooth timelapse at safe position", + " M400 S3 ; Wait for last picture to be taken", + "M623 ; End of \"timelapse_record_flag\"", + "M400 ; Wait for all motions to complete", + "M17 S ; Engage stepper motors", + "M17 Z0.4 ; Lower Z motor current to reduce impact if obstruction exists", + "G1 Z{z_max} F600 ; Move Z-axis to 125.5mm", + "M400 P100 ; Wait for 100ms", + "M17 R ; Restore Z motor current", + "M220 S100 ; Reset feedrate magnitude to 100%", + "M201.2 K1.0 ; Reset acceleration magnitude", + "M73.2 R1.0 ; Reset remaining time magnitude", + "M1002 set_gcode_claim_speed_level : 0 ; Set G-code claim speed level to 0", + "M17 X0.8 Y0.8 Z0.5 ; Lower motor current to 45% power", + "M73 P100 R0 ; Set progress to 100% with 0 remaining time" + ], + "gcodeProc": "", + "gcodeFExt": "gcode", + "extruders": [ + { + "extFilament": 1.75, + "extNozzle": 0.6, + "extSelect": [ + "; BEGIN tool change from {last_tool} to {tool}", + "M620 S{tool}A ; prepare tool change", + "M204 S9000 ; Set acceleration to 9000 mm/s^2", + "M620.11 S0 ; Reset filament retraction state", + "M400 ; wait for moves to complete", + "M620.1 E F187 T240 ; set purge rate 187 temp 240", + "T{tool} ; initiate tool change", + "M620.1 E F374 T240 ; set purge rate 374 temp 240", + "M620.11 S0 ; Reset filament retraction state", + "G92 E0 ; reset extruder position", + "M400 ; wait for moves to complete", + " ; -- PURGE --", + "M109 S240 ; Set nozzle temperature to 240°C", + "G1 E23.7 F187 ; Extrude 23.7mm of filament at flowrate 187", + "G1 E0.914611 F50 ; Pulsatile extrusion", + "G1 E10.518 F187 ; Continue extrusion at flowrate 187", + "G1 E0.914611 F50 ; Pulsatile extrusion", + "G1 E10.518 F374 ; Continue extrusion at flowrate 374", + "G1 E0.914611 F50 ; Pulsatile extrusion", + "G1 E10.518 F374 ; Continue extrusion at flowrate 374", + "G1 E0.914611 F50 ; Pulsatile extrusion", + "G1 E10.518 F374 ; Continue extrusion at flowrate 374", + "G1 E-2 F1800 ; Retract filament by 2mm", + "G1 E2 F300 ; Recover retraction slowly", + "M400 ; Wait for all movements to complete", + "M109 S{temp} ; Set nozzle temperature back to print temp", + " ; -- SHAKE --", + "M106 P1 S255 ; Turn on nozzle cooling fan to full speed", + "M400 S3 ; Wait for all movements to complete", + "G1 X70 F5000 ; Move to X70", + "G1 X90 F3000 ; Move to X90", + "G1 Y255 F4000 ; Move to Y255", + "G1 X105 F5000 ; Move to X105", + "G1 Y265 ; Move to Y265", + "G1 X70 F10000 ; Rapid move to X70", + "G1 X100 F5000 ; Move to X100", + "G1 X70 F10000 ; Rapid move back to X70", + "G1 X100 F5000 ; Move to X100", + "G1 X70 F10000 ; Shake filament residue", + "G1 X80 F15000", + "G1 X60", + "G1 X80", + "G1 X60", + "G1 X80 ; Shake to put down garbage", + "G1 X100 F5000 ; Move to X100", + "G1 X165 F15000 ; Wipe and shake", + "G1 Y256 ; Move Y aside to prevent collision", + "M400 ; Wait for all movements to complete", + "M204 S10000 ; Set acceleration to 10000 mm/s^2", + "M621 S{tool}A ; complete tool change", + "; END tool change" + ], + "extOffsetX": 0, + "extOffsetY": 0, + "extDeselect": [] + }, + { + "extFilament": 1.75, + "extNozzle": 0.6, + "extSelect": [], + "extOffsetX": 0, + "extOffsetY": 0, + "extDeselect": [] + }, + { + "extFilament": 1.75, + "extNozzle": 0.6, + "extSelect": [], + "extOffsetX": 0, + "extOffsetY": 0, + "extDeselect": [] + }, + { + "extFilament": 1.75, + "extNozzle": 0.6, + "extSelect": [], + "extOffsetX": 0, + "extOffsetY": 0, + "extDeselect": [] + } + ], + "new": false, + "deviceName": "My Bambu P1S", + "bedBelt": false, + "fwRetract": false, + "filamentSource": "direct", + "extras": { + "bbl": { + "scrappy": { + "host": "10.10.2.5", + "code": "38373335", + "serial": "01S00A292500224" + }, + "luke": { + "host": "10.20.20.75", + "code": "20709223", + "serial": "01P09C482401767", + "modified": true + }, + "leia": { + "host": "10.20.20.76", + "code": "35129782", + "serial": "01P09C461603301", + "modified": true + }, + "vader": { + "host": "10.20.20.77", + "code": "17711811", + "serial": "01P00A413100516", + "modified": true + } + } + }, + "imageURL": "", + "deviceZMax": 0, + "gcodeTime": 1, + "gcodeFeature": [], + "profiles": [ + { + "processName": "Bambu Test", + "sliceHeight": 0.2, + "sliceShells": 2, + "sliceShellOrder": "in-out", + "sliceLayerStart": "last", + "sliceFillAngle": 45, + "sliceFillOverlap": 0.35, + "sliceFillSparse": 0.1, + "sliceFillType": "hex", + "sliceAdaptive": false, + "sliceMinHeight": 0, + "sliceSupportDensity": 0.5, + "sliceSupportOffset": 0.4, + "sliceSupportGap": 1, + "sliceSupportSize": 5, + "sliceSupportArea": 0.25, + "sliceSupportExtra": 0, + "sliceSupportAngle": 60, + "sliceSupportNozzle": 0, + "sliceSolidMinArea": 0, + "sliceBottomLayers": 2, + "sliceTopLayers": 3, + "firstLayerRate": 20, + "firstLayerPrintMult": 1, + "firstLayerYOffset": 0, + "firstLayerBrim": 0, + "firstLayerBeltLead": 0, + "firstLayerFanSpeed": 0, + "outputTemp": 220, + "outputBedTemp": 65, + "outputFanSpeed": 255, + "outputFeedrate": 120, + "outputFinishrate": 90, + "outputSeekrate": 200, + "outputShellMult": 1.2, + "outputFillMult": 1.2, + "outputSparseMult": 1.2, + "outputRetractDist": 2, + "outputRetractSpeed": 80, + "outputRetractWipe": 0, + "outputRetractDwell": 0, + "outputShortPoly": 100, + "outputMinSpeed": 5, + "outputCoastDist": 0, + "outputLayerRetract": false, + "zHopDistance": 0, + "antiBacklash": 0, + "sliceFillWidth": 1, + "sliceFillRate": 0, + "sliceSupportEnable": false, + "firstSliceHeight": 0.3, + "firstLayerFillRate": 80, + "firstLayerLineMult": 1, + "firstLayerNozzleTemp": 210, + "firstLayerBedTemp": 60, + "firstLayerBrimTrig": 0, + "firstLayerBrimGap": 0, + "outputRaft": false, + "outputRaftSpacing": 0.2, + "outputBrimCount": 0, + "outputBrimOffset": 2, + "outputPurgeTower": 0, + "outputInvertX": false, + "outputInvertY": false, + "arcTolerance": 0, + "ranges": [], + "sliceLineWidth": 0, + "sliceFillRepeat": 2, + "firstLayerBrimIn": 0, + "firstLayerBeltBump": 0, + "outputBeltFirst": false, + "outputLoops": 0, + "sliceFillGrow": 0, + "sliceSolidRate": 0, + "sliceSupportSpan": 5, + "sliceSupportOutline": true, + "firstLayerFlatten": 0, + "outputDraftShield": false, + "outputAvoidGaps": true, + "sliceDetectThin": "off", + "outputAlternating": false, + "sliceLayerStartX": 0, + "sliceLayerStartY": 0, + "sliceSupportGrow": 0, + "outputFanLayer": 1, + "outputNozzle": 0, + "sliceAngle": 45, + "sliceZInterleave": false + } + ] +} \ No newline at end of file diff --git a/src/kiri-dev/fdm/Prusa.i3.MK2S.json b/src/kiri/dev/fdm/Prusa.i3.MK2S.json similarity index 100% rename from src/kiri-dev/fdm/Prusa.i3.MK2S.json rename to src/kiri/dev/fdm/Prusa.i3.MK2S.json diff --git a/src/kiri-dev/fdm/Prusa.i3.MK3S+.json b/src/kiri/dev/fdm/Prusa.i3.MK3S+.json similarity index 100% rename from src/kiri-dev/fdm/Prusa.i3.MK3S+.json rename to src/kiri/dev/fdm/Prusa.i3.MK3S+.json diff --git a/src/kiri-dev/fdm/Prusa.mini.json b/src/kiri/dev/fdm/Prusa.mini.json similarity index 100% rename from src/kiri-dev/fdm/Prusa.mini.json rename to src/kiri/dev/fdm/Prusa.mini.json diff --git a/src/kiri-dev/fdm/Raise3D.N2.json b/src/kiri/dev/fdm/Raise3D.N2.json similarity index 100% rename from src/kiri-dev/fdm/Raise3D.N2.json rename to src/kiri/dev/fdm/Raise3D.N2.json diff --git a/src/kiri-dev/fdm/Rolohaun.Rook.MK1.LDO.json b/src/kiri/dev/fdm/Rolohaun.Rook.MK1.LDO.json similarity index 100% rename from src/kiri-dev/fdm/Rolohaun.Rook.MK1.LDO.json rename to src/kiri/dev/fdm/Rolohaun.Rook.MK1.LDO.json diff --git a/src/kiri-dev/fdm/SainSmart.Infi-20.json b/src/kiri/dev/fdm/SainSmart.Infi-20.json similarity index 100% rename from src/kiri-dev/fdm/SainSmart.Infi-20.json rename to src/kiri/dev/fdm/SainSmart.Infi-20.json diff --git a/src/kiri-dev/fdm/Snapmaker.Original.json b/src/kiri/dev/fdm/Snapmaker.Original.json similarity index 100% rename from src/kiri-dev/fdm/Snapmaker.Original.json rename to src/kiri/dev/fdm/Snapmaker.Original.json diff --git a/src/kiri-dev/fdm/Tina2.Basic.json b/src/kiri/dev/fdm/Tina2.Basic.json similarity index 100% rename from src/kiri-dev/fdm/Tina2.Basic.json rename to src/kiri/dev/fdm/Tina2.Basic.json diff --git a/src/kiri-dev/fdm/TypeA.Series1.json b/src/kiri/dev/fdm/TypeA.Series1.json similarity index 100% rename from src/kiri-dev/fdm/TypeA.Series1.json rename to src/kiri/dev/fdm/TypeA.Series1.json diff --git a/src/kiri-dev/fdm/TypeA.Series1Pro.json b/src/kiri/dev/fdm/TypeA.Series1Pro.json similarity index 100% rename from src/kiri-dev/fdm/TypeA.Series1Pro.json rename to src/kiri/dev/fdm/TypeA.Series1Pro.json diff --git a/src/kiri-dev/fdm/ULIO.3D.BRIX.json b/src/kiri/dev/fdm/ULIO.3D.BRIX.json similarity index 97% rename from src/kiri-dev/fdm/ULIO.3D.BRIX.json rename to src/kiri/dev/fdm/ULIO.3D.BRIX.json index 113f20a7..0a1aadf9 100644 --- a/src/kiri-dev/fdm/ULIO.3D.BRIX.json +++ b/src/kiri/dev/fdm/ULIO.3D.BRIX.json @@ -104,16 +104,16 @@ "sliceFillWidth": 1, "sliceFillRate": 0, "sliceSupportEnable": false, - "firstSliceHeight": 0.25, + "firstSliceHeight": 0.5, "firstLayerFillRate": 20, "firstLayerLineMult": 1, "firstLayerNozzleTemp": 0, "firstLayerBedTemp": 0, "firstLayerBrimTrig": 0, "firstLayerBrimGap": 0, - "outputRaft": true, + "outputRaft": false, "outputRaftSpacing": 0.2, - "outputBrimCount": 5, + "outputBrimCount": 0, "outputBrimOffset": 2, "outputPurgeTower": 0, "outputInvertX": false, diff --git a/src/kiri-dev/fdm/ULIO.3D.STEAM.json b/src/kiri/dev/fdm/ULIO.3D.STEAM.json similarity index 97% rename from src/kiri-dev/fdm/ULIO.3D.STEAM.json rename to src/kiri/dev/fdm/ULIO.3D.STEAM.json index b6239564..5270560e 100644 --- a/src/kiri-dev/fdm/ULIO.3D.STEAM.json +++ b/src/kiri/dev/fdm/ULIO.3D.STEAM.json @@ -104,16 +104,16 @@ "sliceFillWidth": 1, "sliceFillRate": 0, "sliceSupportEnable": false, - "firstSliceHeight": 0.25, + "firstSliceHeight": 0.5, "firstLayerFillRate": 20, "firstLayerLineMult": 1, "firstLayerNozzleTemp": 0, "firstLayerBedTemp": 0, "firstLayerBrimTrig": 0, "firstLayerBrimGap": 0, - "outputRaft": true, + "outputRaft": false, "outputRaftSpacing": 0.2, - "outputBrimCount": 5, + "outputBrimCount": 0, "outputBrimOffset": 2, "outputPurgeTower": 0, "outputInvertX": false, diff --git a/src/kiri-dev/fdm/Ultimaker.Ultimaker2.json b/src/kiri/dev/fdm/Ultimaker.Ultimaker2.json similarity index 100% rename from src/kiri-dev/fdm/Ultimaker.Ultimaker2.json rename to src/kiri/dev/fdm/Ultimaker.Ultimaker2.json diff --git a/src/kiri-dev/fdm/Wanhao.Duplicator_4s.json b/src/kiri/dev/fdm/Wanhao.Duplicator_4s.json similarity index 100% rename from src/kiri-dev/fdm/Wanhao.Duplicator_4s.json rename to src/kiri/dev/fdm/Wanhao.Duplicator_4s.json diff --git a/src/kiri-dev/fdm/Wanhao.Duplicator_5s.json b/src/kiri/dev/fdm/Wanhao.Duplicator_5s.json similarity index 100% rename from src/kiri-dev/fdm/Wanhao.Duplicator_5s.json rename to src/kiri/dev/fdm/Wanhao.Duplicator_5s.json diff --git a/src/kiri-dev/fdm/Wanhao.Duplicator_6.json b/src/kiri/dev/fdm/Wanhao.Duplicator_6.json similarity index 100% rename from src/kiri-dev/fdm/Wanhao.Duplicator_6.json rename to src/kiri/dev/fdm/Wanhao.Duplicator_6.json diff --git a/src/kiri-dev/fdm/Wanhao.Duplicator_i3.json b/src/kiri/dev/fdm/Wanhao.Duplicator_i3.json similarity index 100% rename from src/kiri-dev/fdm/Wanhao.Duplicator_i3.json rename to src/kiri/dev/fdm/Wanhao.Duplicator_i3.json diff --git a/src/kiri-dev/fdm/XYZ.daVinci.Jr.Pro.json b/src/kiri/dev/fdm/XYZ.daVinci.Jr.Pro.json similarity index 100% rename from src/kiri-dev/fdm/XYZ.daVinci.Jr.Pro.json rename to src/kiri/dev/fdm/XYZ.daVinci.Jr.Pro.json diff --git a/src/kiri-dev/fdm/XYZ.daVinci.Mini.w.json b/src/kiri/dev/fdm/XYZ.daVinci.Mini.w.json similarity index 100% rename from src/kiri-dev/fdm/XYZ.daVinci.Mini.w.json rename to src/kiri/dev/fdm/XYZ.daVinci.Mini.w.json diff --git a/src/kiri-dev/laser/Any.Generic.Laser.json b/src/kiri/dev/laser/Any.Generic.Laser.json similarity index 100% rename from src/kiri-dev/laser/Any.Generic.Laser.json rename to src/kiri/dev/laser/Any.Generic.Laser.json diff --git a/src/kiri-dev/laser/Glowforge.json b/src/kiri/dev/laser/Glowforge.json similarity index 100% rename from src/kiri-dev/laser/Glowforge.json rename to src/kiri/dev/laser/Glowforge.json diff --git a/src/kiri-dev/laser/Snapmaker.A250T.json b/src/kiri/dev/laser/Snapmaker.A250T.json similarity index 100% rename from src/kiri-dev/laser/Snapmaker.A250T.json rename to src/kiri/dev/laser/Snapmaker.A250T.json diff --git a/src/kiri-dev/laser/Snapmaker.A350T.json b/src/kiri/dev/laser/Snapmaker.A350T.json similarity index 100% rename from src/kiri-dev/laser/Snapmaker.A350T.json rename to src/kiri/dev/laser/Snapmaker.A350T.json diff --git a/src/kiri-dev/laser/Snapmaker.Original.json b/src/kiri/dev/laser/Snapmaker.Original.json similarity index 100% rename from src/kiri-dev/laser/Snapmaker.Original.json rename to src/kiri/dev/laser/Snapmaker.Original.json diff --git a/src/kiri-dev/laser/Xtool.P2.json b/src/kiri/dev/laser/Xtool.P2.json similarity index 100% rename from src/kiri-dev/laser/Xtool.P2.json rename to src/kiri/dev/laser/Xtool.P2.json diff --git a/src/kiri-dev/laser/xTool.D1.json b/src/kiri/dev/laser/xTool.D1.json similarity index 100% rename from src/kiri-dev/laser/xTool.D1.json rename to src/kiri/dev/laser/xTool.D1.json diff --git a/src/kiri-dev/laser/xTool.F1.Ultra.json b/src/kiri/dev/laser/xTool.F1.Ultra.json similarity index 100% rename from src/kiri-dev/laser/xTool.F1.Ultra.json rename to src/kiri/dev/laser/xTool.F1.Ultra.json diff --git a/src/kiri-dev/laser/xTool.F1.json b/src/kiri/dev/laser/xTool.F1.json similarity index 100% rename from src/kiri-dev/laser/xTool.F1.json rename to src/kiri/dev/laser/xTool.F1.json diff --git a/src/kiri-dev/laser/xTool.M1.json b/src/kiri/dev/laser/xTool.M1.json similarity index 100% rename from src/kiri-dev/laser/xTool.M1.json rename to src/kiri/dev/laser/xTool.M1.json diff --git a/src/kiri-dev/laser/xTool.P2S.json b/src/kiri/dev/laser/xTool.P2S.json similarity index 100% rename from src/kiri-dev/laser/xTool.P2S.json rename to src/kiri/dev/laser/xTool.P2S.json diff --git a/src/kiri-dev/laser/xTool.S1.json b/src/kiri/dev/laser/xTool.S1.json similarity index 100% rename from src/kiri-dev/laser/xTool.S1.json rename to src/kiri/dev/laser/xTool.S1.json diff --git a/src/kiri-dev/sla/Anycubic.Photon.S.json b/src/kiri/dev/sla/Anycubic.Photon.S.json similarity index 100% rename from src/kiri-dev/sla/Anycubic.Photon.S.json rename to src/kiri/dev/sla/Anycubic.Photon.S.json diff --git a/src/kiri-dev/sla/Anycubic.Photon.json b/src/kiri/dev/sla/Anycubic.Photon.json similarity index 100% rename from src/kiri-dev/sla/Anycubic.Photon.json rename to src/kiri/dev/sla/Anycubic.Photon.json diff --git a/src/kiri-dev/sla/Creality.Halot.Max.json b/src/kiri/dev/sla/Creality.Halot.Max.json similarity index 100% rename from src/kiri-dev/sla/Creality.Halot.Max.json rename to src/kiri/dev/sla/Creality.Halot.Max.json diff --git a/src/kiri-dev/sla/Creality.Halot.One.json b/src/kiri/dev/sla/Creality.Halot.One.json similarity index 100% rename from src/kiri-dev/sla/Creality.Halot.One.json rename to src/kiri/dev/sla/Creality.Halot.One.json diff --git a/src/kiri-dev/sla/Creality.Halot.Sky.json b/src/kiri/dev/sla/Creality.Halot.Sky.json similarity index 100% rename from src/kiri-dev/sla/Creality.Halot.Sky.json rename to src/kiri/dev/sla/Creality.Halot.Sky.json diff --git a/src/kiri-dev/wedm/Any.Generic.WireEDM.json b/src/kiri/dev/wedm/Any.Generic.WireEDM.json similarity index 100% rename from src/kiri-dev/wedm/Any.Generic.WireEDM.json rename to src/kiri/dev/wedm/Any.Generic.WireEDM.json diff --git a/src/kiri-dev/wedm/RackRobo.Betta.Wire.V1.json b/src/kiri/dev/wedm/RackRobo.Betta.Wire.V1.json similarity index 100% rename from src/kiri-dev/wedm/RackRobo.Betta.Wire.V1.json rename to src/kiri/dev/wedm/RackRobo.Betta.Wire.V1.json diff --git a/src/kiri-dev/wjet/Any.Generic.WaterJet.json b/src/kiri/dev/wjet/Any.Generic.WaterJet.json similarity index 100% rename from src/kiri-dev/wjet/Any.Generic.WaterJet.json rename to src/kiri/dev/wjet/Any.Generic.WaterJet.json diff --git a/src/kiri-dev/wjet/HydroBLADE.json b/src/kiri/dev/wjet/HydroBLADE.json similarity index 100% rename from src/kiri-dev/wjet/HydroBLADE.json rename to src/kiri/dev/wjet/HydroBLADE.json diff --git a/src/kiri/devices.js b/src/kiri/devices.js deleted file mode 100644 index e9055c51..00000000 --- a/src/kiri/devices.js +++ /dev/null @@ -1,393 +0,0 @@ -/** 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/src/kiri/init.js b/src/kiri/init.js deleted file mode 100644 index d489ed21..00000000 --- a/src/kiri/init.js +++ /dev/null @@ -1,2169 +0,0 @@ -/** 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/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/main.js b/src/kiri/main.js deleted file mode 100644 index d61eaefe..00000000 --- a/src/kiri/main.js +++ /dev/null @@ -1,984 +0,0 @@ -/** Copyright Stewart Allen -- 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/src/kiri/mode/cam/anim-2d-be.js b/src/kiri/mode/cam/anim-2d-be.js new file mode 100644 index 00000000..720c74b0 --- /dev/null +++ b/src/kiri/mode/cam/anim-2d-be.js @@ -0,0 +1,307 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +// WORKER BACK END ANIMATION CODE for 2D + +import { Tool } from './tool.js'; + +const asPoints = false; +const asLines = false; + +let stock, center, grid, gridX, gridY, rez; +let path, pathIndex, tool, tools, last, toolID = 1; + +export function init(worker) { + const { dispatch } = worker; + + dispatch.animate_setup = function(data, send) { + const { settings } = data; + const { process } = settings; + const print = worker.current.print; + const density = parseInt(settings.controller.animesh) * 1000; + + pathIndex = 0; + path = print.output.flat(); + tools = settings.tools; + stock = settings.stock; + rez = 1/Math.sqrt(density/(stock.x * stock.y)); + + // destructure arcs into path points + path = path.map(o => + o.arcPoints ? [ ...o.arcPoints.map(point => ({ ...o, point })), o ] : [ o ] + ).flat(); + + const step = rez; + const stepsX = Math.floor(stock.x / step); + const stepsY = Math.floor(stock.y / step); + const { pos, ind, sab } = createGrid(stepsX, stepsY, stock, step, true); + const offset = { + x: process.camOriginCenter ? 0 : stock.x / 2, + y: process.camOriginCenter ? 0 : stock.y / 2, + z: process.camOriginTop ? -stock.z : 0 + } + + grid = pos; + gridX = stepsX; + gridY = stepsY; + + tool = null; + last = null; + animating = false; + animateClear = false; + + center = Object.assign({}, stock.center); + center.z -= stock.z / 2; + + send.data({ mesh_add: { id: 0, ind, offset, sab } }, [ ]); // sab not transferrable + send.data({ mesh_move: { id: 0, pos: center } }); + send.done(); + }; + + dispatch.animate = function(data, send) { + renderPause = data.pause || renderPause; + renderSpeed = data.speed || 0; + if (animating) { + return send.done(); + } + renderSteps = data.steps || 1; + renderDone = false; + animating = renderSpeed > 0; + renderPath(send); + }; + + dispatch.animate_cleanup = function(data, send) { + if (animating) { + animateClear = true; + } + }; +} + +function createGrid(stepsX, stepsY, size, step, stock) { + const gridPoints = stepsX * stepsY; + const sab = new SharedArrayBuffer(gridPoints * 3 * 4) + const pos = new Float32Array(sab); + const ind = []; + const ox = size.x / 2; + const oy = size.y / 2; + let ex = stepsX - 1; + let ey = stepsY - 1; + + // initialize grid points + for (let x=0, ai=0; x 0) ind.appendAll([ + (stepsY * x) + (y - 1), + (stepsY * x) + (y ) + ]); + if (x > 0) ind.appendAll([ + (stepsY * (x - 1)) + y, + (stepsY * (x )) + y + ]); + } else { + if (x > 0 && y > 0) { + let v0 = stepsY * (x - 1) + y - 1; + let v1 = stepsY * (x - 0) + y - 1; + let v2 = stepsY * (x - 0) + y; + let v3 = stepsY * (x - 1) + y; + ind.appendAll([ + v0, v1, v2, v0, v2, v3 + ]); + } + } + } + } + + return { pos, ind, sab }; +} + +let animateClear = false; +let animating = false; +let renderDist = 0; +let renderDone = false; +let renderPause = 10; +let renderSteps = 0; +let renderSpeed = 0; +let skipMove = null; +let toolUpdate; +let depth = 0; + +// send latest tool position and progress bar +function renderUpdate(send) { + if (toolUpdate) { + send.data(toolUpdate); + } + send.data({ progress: pathIndex / path.length, id: 0, mesh_update: 1 }); +} + +function renderPath(send) { + if (renderDone) { + return; + } + + if (renderSteps-- === 0) { + animating = false; + renderPath(send); + return; + } + + if (animating === false || animateClear || renderSpeed === 0) { + renderUpdate(send); + renderDone = true; + animating = false; + animateClear = false; + send.done(); + return; + } + + let next = path[pathIndex]; + while (next && next.type === 'laser') { + last = next; + next = path[++pathIndex]; + } + + if (!next) { + animating = false; + renderPath(send); + return; + } + pathIndex++; + + const firstTool = !tool && next.tool; + const toolChange = !firstTool && (tool.getID() !== next.tool.getID()); + if (firstTool || toolChange) { + // on real tool change, go to safe Z first + if (tool && last.point) { + let pos = last.point = { + x: last.point.x, + y: last.point.y, + z: stock.z + }; + send.data({ mesh_move: { toolID, pos }}); + } + updateTool(next.tool, send); + } + + const id = toolID; + const rezstep = rez; + if (last) { + const lp = last.point, np = next.point; + last = next; + // dwell ops have no point + if (!np || !lp) { + return renderPath(send); + } + const dx = np.x - lp.x, dy = np.y - lp.y, dz = np.z - lp.z; + const dist = Math.sqrt(dx*dx + dy*dy + dz*dz); + renderDist += dist; + + // skip moves that are less than grid resolution + if (renderDist < rezstep) { + renderPath(send); + return; + } + + const md = Math.max(Math.abs(dx), Math.abs(dy), Math.abs(dz)); + const st = Math.ceil(md / rezstep); + const mx = dx / st, my = dy / st, mz = dz / st; + const moves = []; + for (let i=0, x=lp.x, y=lp.y, z=lp.z; i= renderSpeed) || (depth > 600)) { + renderDist = depth = 0; + renderUpdate(send); + setTimeout(() => { + renderMoves(id, moves, send, index); + }, renderPause); + return; + } + } + depth++; + renderPath(send); +} + +// update stock mesh to reflect tool tip geometry at given XYZ position +function deformMesh(pos, send) { + const prof = tool.profile; + const { size, pix } = tool.profileDim; + const mid = Math.floor(pix / 2); + const rx = Math.floor((pos.x + stock.x / 2 - size / 2 - center.x) / rez); + const ry = Math.floor((pos.y + stock.y / 2 - size / 2 - center.y) / rez); + let upos = 0; + // deform mesh to lowest point on tool profile + for (let i=0, il=prof.length; i < il; ) { + const dx = mid + prof[i++]; + const dy = mid + prof[i++]; + const dz = prof[i++]; + const gx = rx + dx; + const gy = ry + dy; + + if (gx < 0|| gy < 0 || gx > gridX-1 || gy > gridY-1) { + continue; + } + + const gi = gx * gridY + gy; + const iz = gi * 3 + 2; + const cz = grid[iz]; + const tz = tool.pos.z - dz; + if (tz < cz) { + upos++; + grid[iz] = tz; + } + } +} + +function updateTool(toolobj, send) { + if (tool) { + send.data({ mesh_del: toolID }); + } + tool = new Tool({ tools }, toolobj.getID()); + tool.generateProfile(rez); + const flen = tool.fluteLength() || 15; + const slen = tool.shaftLength() || 15; + // const frad = tool.fluteDiameter() / 2; + const prof = tool.profile; + const { size, pix } = tool.profileDim; + const { pos, ind, sab } = createGrid(pix, pix, {x:size, y:size, z:flen+slen}, rez); + const mid = Math.floor(pix/2); + // deform mesh to fit tool profile + for (let i=0, il=prof.length; i < il; ) { + const dx = mid + prof[i++]; + const dy = mid + prof[i++]; + const dz = prof[i++]; + pos[(dx * pix + dy) * 3 + 2] = -dz; + } + send.data({ mesh_add: { id:++toolID, ind, sab }}); +} diff --git a/src/kiri/mode/cam/anim-2d-fe.js b/src/kiri/mode/cam/anim-2d-fe.js new file mode 100644 index 00000000..5e13d220 --- /dev/null +++ b/src/kiri/mode/cam/anim-2d-fe.js @@ -0,0 +1,309 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +// UI FRONT END ANIMATION CODE for 2D + +import { api } from '../../core/api.js'; +import { space } from '../../../moto/space.js'; + +const { client } = api; + +const asLines = false; +const asPoints = false; + +let meshes = {}, + button = {}, + label = {}, + unitScale = 1, + speedValues = [ 1, 2, 4, 8, 32 ], + speedPauses = [ 30, 20, 10, 5, 0 ], + speedNames = [ "1x", "2x", "4x", "8x", "!!" ], + speedMax = speedValues.length - 1, + speedIndex = 0, + speed, + color = 0, + material, + origin, + posOffset = { x:0, y:0, z:0 }; + +export function animate_clear(api) { + let { anim } = api.ui; + space.platform.showGridBelow(true); + client.animate_cleanup(); + Object.keys(meshes).forEach(id => deleteMesh(id)); + toggleStock(undefined,true,false); + api.uc.setVisible(anim.laba, false); + api.uc.setVisible(anim.vala, false); +} + +export function animate(api, delay) { + let alert = api.alerts.show("building animation"); + let settings = api.conf.get(); + client.animate_setup(settings, data => { + checkMeshCommands(data); + if (!(data && data.mesh_add)) { + return; + } + + let { anim } = api.ui; + Object.assign(button, { + replay: anim.replay, + play: anim.play, + step: anim.step, + pause: anim.pause, + speed: anim.speed, + trans: anim.trans, + model: anim.model, + shade: anim.shade + }); + Object.assign(label, { + progress: anim.progress, + speed: anim.labspd, + x: anim.valx, + y: anim.valy, + z: anim.valz, + }); + + origin = settings.origin; + speedIndex = api.local.getInt('cam.anim.speed') || 0; + updateSpeed(); + setTimeout(step, delay || 0); + + button.replay.onclick = replay; + button.play.onclick = play; + button.step.onclick = step; + button.pause.onclick = pause; + button.speed.onclick = fast; + button.trans.onclick = toggleTrans; + button.model.onclick = toggleModel; + button.shade.onclick = toggleStock; + button.play.style.display = ''; + button.pause.style.display = 'none'; + + api.event.emit('animate', 'CAM'); + api.alerts.hide(alert); + space.platform.showGridBelow(false); + toggleTrans(0,api.local.getBoolean('cam.anim.trans', true)); + toggleModel(0,api.local.getBoolean('cam.anim.model', false)); + toggleStock(0,api.local.getBoolean('cam.anim.stock', false)); + }); +} + +Object.assign(client, { + animate(data, ondone) { + client.send("animate", data, ondone); + }, + + animate_setup(settings, ondone) { + color = settings.controller.dark ? 0x48607B : 0x607FA4; + unitScale = settings.controller.units === 'in' ? 1/25.4 : 1; + let flatShading = true, + transparent = true, + opacity = 0.9, + side = THREE.DoubleSide; + material = new THREE.MeshPhongMaterial({ + flatShading, + transparent, + opacity, + color, + side + }); + add_red_neg_z(material); + client.send("animate_setup", {settings}, ondone); + }, + + animate_cleanup(data, ondone) { + client.send("animate_cleanup", data, ondone); + } +}); + +function meshAdd(id, ind, pos, sab) { + const geo = new THREE.BufferGeometry(); + if (sab) { + // use array buffer shared with worker + pos = new Float32Array(sab); + } + geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); + if (ind.length) { + geo.setIndex(new THREE.BufferAttribute(new Uint32Array(ind), 1)); + } + let mesh; + if (asPoints) { + const mat = new THREE.PointsMaterial({ + transparent: true, + opacity: 0.75, + color: 0x888888, + size: 0.3 + }); + mesh = new THREE.Points(geo, mat); + } else if (asLines) { + const mat = new THREE.LineBasicMaterial({ + transparent: true, + opacity: 0.75, + color + }); + mesh = new THREE.LineSegments(geo, mat); + } else { + geo.computeVertexNormals(); + mesh = new THREE.Mesh(geo, material); + mesh.renderOrder = -10; + } + space.world.add(mesh); + meshes[id] = mesh; +} + +function meshUpdates(id) { + const mesh = meshes[id]; + if (!mesh) { + return; // animate cancelled + } + mesh.geometry.attributes.position.needsUpdate = true; + space.update(); +} + +function deleteMesh(id) { + space.world.remove(meshes[id]); + delete meshes[id]; +} + +function toggleModel(ev,bool) { + api.local.toggle('cam.anim.model', bool); + api.widgets.all().forEach(w => w.toggleVisibility(bool)); +} + +function toggleStock(ev,bool,set) { + set !== false && api.local.toggle('cam.anim.stock', bool); + return api.event.emit('cam.stock.toggle', bool ?? undefined); +} + +function toggleTrans(ev,bool) { + bool = api.local.toggle('cam.anim.trans', bool); + material.transparent = bool; + material.needsUpdate = true; +} + +function step() { + updateSpeed(); + client.animate({speed, steps: 1}, handleGridUpdate); +} + +function play(opts) { + const { steps } = opts; + updateSpeed(); + if (steps !== 1) { + button.play.style.display = 'none'; + button.pause.style.display = ''; + } + client.animate({ + speed, + steps: steps || Infinity, + pause: speedPauses[speedIndex] + }, handleGridUpdate); +} + +function fast(opts) { + const { steps } = opts; + updateSpeed(1); + button.play.style.display = 'none'; + button.pause.style.display = ''; + client.animate({ + speed, + steps: steps || Infinity, + pause: speedPauses[speedIndex] + }, handleGridUpdate); +} + +function pause() { + button.play.style.display = ''; + button.pause.style.display = 'none'; + client.animate({speed: 0}, handleGridUpdate); +} + +function handleGridUpdate(data) { + checkMeshCommands(data); + if (data && data.progress) { + label.progress.value = (data.progress * 100).toFixed(1); + } +} + +function updateSpeed(inc = 0) { + if (inc === Infinity) { + speedIndex = speedMax; + } else if (inc > 0) { + speedIndex = (speedIndex + inc) % speedValues.length; + } + api.local.set('cam.anim.speed', speedIndex); + speed = speedValues[speedIndex]; + label.speed.value = speedNames[speedIndex]; +} + +function replay() { + animate_clear(api); + setTimeout(() => { + animate(api, 50); + }, 250); +} + +function checkMeshCommands(data) { + if (!data) { + return; + } + if (data.mesh_add) { + const { id, ind, pos, offset, sab } = data.mesh_add; + meshAdd(id, ind, pos, sab); + space.refresh(); + if (offset) { + posOffset = offset; + } + } + if (data.mesh_del) { + deleteMesh(data.mesh_del); + } + if (data.mesh_move) { + const { id, pos } = data.mesh_move; + const mesh = meshes[id]; + if (mesh) { + mesh.position.x = pos.x; + mesh.position.y = pos.y; + mesh.position.z = pos.z; + space.update(); + } + label.x.value = (pos.x - origin.x).toFixed(2); + label.y.value = (pos.y + origin.y).toFixed(2); + label.z.value = (pos.z - origin.z).toFixed(2); + } + if (data.mesh_update) { + meshUpdates(data.id); + } +} + +// SHADER: tint points below z=0 with red +function add_red_neg_z(material) { + material.onBeforeCompile = (shader) => { + shader.vertexShader = shader.vertexShader.replace( + `#include `, + ` + #include + vWorldPosition = vec3(transformed); + ` + ); + + shader.vertexShader = ` + varying vec3 vWorldPosition; + ` + shader.vertexShader; + + shader.fragmentShader = ` + varying vec3 vWorldPosition; + ` + shader.fragmentShader; + + shader.fragmentShader = shader.fragmentShader.replace( + `#include `, + ` + #include + if (vWorldPosition.z < 0.0) { + gl_FragColor.rgb += vec3(0.5, 0.0, 0.0); // Add red tint + } + ` + ); + }; + return material; +} diff --git a/src/kiri/mode/cam/anim-3d-be.js b/src/kiri/mode/cam/anim-3d-be.js new file mode 100644 index 00000000..204e5b2a --- /dev/null +++ b/src/kiri/mode/cam/anim-3d-be.js @@ -0,0 +1,396 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +// WORKER BACK END ANIMATION CODE for 3D (indexed) + +import { CSG } from '../../../geo/csg.js'; +import { Tool } from './tool.js'; + +let nextMeshID = 1; +let stock, center, rez; +let path, pathIndex, tool, tools, last; + +let stockZ; +let stockIndexMsg = false; +let stockSlices; +let stockIndex; +let startTime; + +let toolID = -1; +let toolMesh; +let toolRadius; +let toolUpdateMsg; + +let animateClear = false; +let animating = false; +let moveDist = 0; +let renderDist = 0; +let renderDone = false; +let renderPause = 10; +let renderSteps = 0; +let renderSpeed = 0; +let indexCount = 0; +let updates = 0; + +export function init(worker) { + const { dispatch } = worker; + + dispatch.animate_setup2 = function (data, send) { + const { settings } = data; + const { process } = settings; + const print = worker.current.print; + const density = parseInt(settings.controller.animesh) * 1000; + const isIndexed = process.camStockIndexed; + + pathIndex = 0; + path = print.output.flat(); + tools = settings.tools; + stock = settings.stock; + rez = 1 / Math.sqrt(density / (stock.x * stock.y)); + + tool = null; + last = null; + animating = false; + animateClear = false; + stockIndex = 0; + indexCount = 0; + startTime = 0; + updates = 0; + stockZ = isIndexed ? 0 : stock.z; + + stockSlices = []; + const { x, y, z } = stock; + const sliceCount = parseInt(settings.controller.animesh || 2000) / 100; + const sliceWidth = stock.x / sliceCount; + for (let i = 0; i < sliceCount; i++) { + let xmin = -(x / 2) + (i * sliceWidth) + sliceWidth / 2; + let slice = new Stock(sliceWidth, y, z).translate(xmin, 0, 0); + stockSlices.push(slice); + slice.updateMesh([]); + slice.send(send); + // send({ mesh_move: { id: slice.id, pos: { x:0, y:0, z: stock.z/2} } }); + } + + send.done(); + }; + + dispatch.animate2 = function (data, send) { + renderPause = data.pause || renderPause; + renderSpeed = data.speed || 0; + if (animating) { + return send.done(); + } + renderSteps = data.steps || 1; + renderDone = false; + animating = renderSpeed > 0; + startTime = startTime || Date.now(); + renderPath(send); + }; + + dispatch.animate_cleanup2 = function (data, send) { + if (animating) { + animateClear = true; + } + }; +} + +function renderPath(send) { + if (renderDone) { + return; + } + + if (renderSteps-- === 0) { + animating = false; + renderPath(send); + return; + } + + if (animating === false || animateClear || renderSpeed === 0) { + renderUpdate(send); + renderDone = true; + animating = false; + animateClear = false; + send.done(); + return; + } + + let next = path[pathIndex]; + while (next && next.type === 'laser') { + last = next; + next = path[++pathIndex]; + } + + if (!next) { + console.log('animation completed in ', ((Date.now() - startTime) / 1000).round(2)); + animating = false; + renderPath(send); + return; + } + pathIndex++; + // console.log(next.point.z); + + if (next.tool && (!tool || tool.getID() !== next.tool.getID())) { + // on real tool change, go to safe Z first + if (tool && last.point) { + let pos = last.point = { + x: last.point.x, + y: last.point.y, + z: stock.z + }; + toolMove(pos); + send.data(toolUpdateMsg); + } + toolUpdate(next.tool.getID(), send); + } + + const id = toolID; + const rezstep = rez * 2; + if (last) { + const lp = last.point, np = next.point; + last = next; + // dwell ops have no point + if (!np || !lp) { + return renderPath(send); + } + let dx = np.x - lp.x, + dy = np.y - lp.y, + dz = np.z - lp.z, + da = Math.abs((np.a || 0) - (lp.a || 0)), + dr = (da / 360) * (2 * Math.PI * Math.max(np.z, lp.z)), + dist = Math.sqrt(dx * dx + dy * dy + dz * dz + dr * dr); + + moveDist += dist; + + // skip moves that are less than grid resolution + if (moveDist < rezstep) { + // console.log('skip', moveDist, rezstep, next); + renderPath(send); + return; + } + + const md = Math.max(Math.abs(dx), Math.abs(dy), Math.abs(dz), dr); + const st = Math.ceil(md / rezstep); + const mx = dx / st, my = dy / st, mz = dz / st; + // const sd = Math.sqrt(mx*mx + my*my + mz*mz + dr*dr); + const sd = Math.sqrt(mx * mx + my * my + Math.min(1, mz * mz) + dr * dr); + const moves = []; + for (let i = 0, x = lp.x, y = lp.y, z = lp.z; i < st; i++) { + moves.push({ x, y, z, a: lp.a, md: sd, dx, dy, dz }); + x += mx; + y += my; + z += mz; + } + moveDist = 0; + moves.push({ ...next.point, md: sd }); + renderMoves(id, moves, send); + } else { + last = next; + if (tool) { + toolMove(next.point); + } + renderPath(send); + } +} + +function renderMoves(id, moves, send, seed = 0) { + for (let index = seed; index < moves.length; index++) { + const pos = moves[index]; + if (!pos) { + throw `no pos @ ${index} of ${moves.length}`; + } + const { dx, dy, dz } = pos; + toolMove(pos); + // console.log('renderMoves', {id, moves, seed}); + let subs = 0; + if (dx || dy || dz < 0) + for (let slice of stockSlices) { + if (slice.bounds.intersectsBox(toolMesh.bounds)) { + slice.subtractTool(toolMesh); + subs++; + } + } + // console.log({ index, subs }); + renderDist += pos.md; + // pause renderer at specified offsets + if (renderSpeed && renderDist >= renderSpeed) { + renderDist = 0; + renderUpdate(send); + setTimeout(() => { + renderMoves(id, moves, send, index + 1); + }, renderPause); + return; + } + } + renderPath(send); +} + +// send latest tool position and progress bar +function renderUpdate(send) { + const updated = [] + for (let slice of stockSlices) { + slice.updateMesh(updated); + } + for (let slice of updated) { + slice.send(send); + } + if (toolUpdateMsg) { + send.data(toolUpdateMsg); + } + if (stockIndexMsg) { + send.data({ stock_index: stockIndex }); + for (let slice of stockSlices) { + send.data({ mesh_index: { id: slice.id, index: -stockIndex } }); + } + stockIndexMsg = false; + } + send.data({ progress: pathIndex / path.length }); + updates++; +} + +// move tool mesh animation space, update client +function toolMove(pos) { + toolUpdateMsg = { mesh_move: { id: toolID, pos: { x: pos.x, y: pos.y, z: pos.z } } }; + if (toolMesh.mesh) { + toolMesh.mesh.delete(); + } + toolMesh.mesh = toolMesh.root.translate(pos.x, pos.y, pos.z); + if (pos.a !== undefined) { + let tmp = toolMesh.mesh.rotate([pos.a, 0, 0]); + toolMesh.mesh.delete(); + toolMesh.mesh = tmp; + if (pos.a !== stockIndex) { + stockIndexMsg = true; + stockIndex = pos.a; + } + } + toolMesh.bounds = CSG.toBox3(toolMesh.mesh); +} + +// delete old tool mesh, generate tool mesh, send to client +function toolUpdate(toolid, send) { + if (tool) { + send.data({ mesh_del: toolID }); + } + tool = new Tool({ tools }, toolid); + const Instance = CSG.Instance(); + const slen = tool.shaftLength() || 15; + const srad = tool.shaftDiameter() / 2; + const flen = tool.fluteLength() || 15; + const frad = toolRadius = tool.fluteDiameter() / 2; + const tlen = slen + flen; // total tool length + let { cylinder, sphere } = Instance.Manifold; + let mesh; + if (tool.isBallMill()) { + mesh = cylinder(tlen - frad * 2, frad, frad, 20, true) + .add(sphere(frad, 20).translate(0, 0, -(tlen - frad * 2) / 2)) + .add(cylinder(slen, srad, srad, 20, true).translate(0, 0, flen / 2)); + } else if (tool.isTaperMill()) { + const trad = Math.max(tool.tipDiameter() / 2, 0.001); + mesh = cylinder(slen, srad, srad, 20, true).translate(0, 0, slen / 2) + .add(cylinder(flen, trad, frad, 20, true).translate(0, 0, -flen / 2)); + } else { + mesh = cylinder(tlen, frad, frad, 20, true) + .add(cylinder(slen, srad, srad, 20, true).translate(0, 0, flen / 2)); + } + mesh = mesh.translate(0, 0, (tlen - stockZ) / 2); + const raw = mesh.getMesh(); + const vertex = raw.vertProperties; + const index = raw.triVerts; + toolMesh = { root: mesh, index, vertex, bounds: CSG.toBox3(mesh) }; + send.data({ mesh_add: { id: --toolID, ind: index, pos: vertex } }); +} + +class Stock { + constructor(x, y, z) { + this.id = nextMeshID++; + this.vbuf = undefined; + this.ibuf = undefined; + this.ilen = 0; + this.sends = 0; + this.newbuf = true; + this.subtracts = 0; + this.mesh = CSG.Instance().Manifold.cube([x, y, z], true); + } + + send(send) { + const newbuf = this.newbuf; + const action = this.sends++ === 0 ? 'mesh_add' : 'mesh_update'; + send.data({ + [action]: { + id: this.id, + ind: newbuf ? this.ibuf : undefined, + pos: newbuf ? this.vbuf : undefined, + ilen: this.ilen, + plen: this.plen + } + }); + this.newbuf = false; + this.sends++; + // console.log({ send: this.id, newbuf, action }); + } + + translate(x, y, z) { + // console.log({ translate: this.id, x, y, z }); + const oldmesh = this.mesh; + this.mesh = this.mesh.translate(x, y, z); + this.bounds = CSG.toBox3(this.mesh); + oldmesh.delete(); + return this; + } + + updateMesh(updates) { + if (this.sends > 0 && this.subtracts === 0) { + return; + } + const subs = this.subtracts; + this.subtracts = 0; + let start = Date.now(); + let mesh = this.mesh.getMesh(); + this.sharedVertexBuffer(mesh.numVert * 3); + this.sharedIndexBuffer(mesh.numTri * 3); + this.vbuf.set(mesh.vertProperties); + this.ibuf.set(mesh.triVerts); + let sub = Date.now(); + updates.push(this); + if (false) console.log({ + update: this.id, + time: sub - start, + subs, + mssub: ((sub - start) / subs).round(3) + }); + } + + subtractTool(toolMesh) { + const oldmesh = this.mesh; + this.mesh = this.mesh.subtract(toolMesh.mesh); + oldmesh.delete(); + this.subtracts++; + } + + sharedVertexBuffer(size) { + const old = this.vbuf; + this.plen = size; + if (old && old.length >= size) { + // console.log({svb: this.id, reuse: size, old: old.length}); + return old; + } + // let buf = new Float32Array(new SharedArrayBuffer(size * 4)); + let buf = new Float32Array(new SharedArrayBuffer(size * 4 + 1024 * 1024)); + // console.log({new_svb: this.id, size, buf}); + this.newbuf = true; + return this.vbuf = buf; + } + + sharedIndexBuffer(size) { + const old = this.ibuf; + this.ilen = size; + if (old && old.length >= size) { + // console.log({sib: this.id, reuse: size, old: old.length}); + return old; + } + // let buf = new Uint32Array(new SharedArrayBuffer(size * 4)); + let buf = new Uint32Array(new SharedArrayBuffer(size * 4 + 1024 * 1024)); + // console.log({new_sib: this.id, size, buf}); + this.newbuf = true; + return this.ibuf = buf; + } +} diff --git a/src/kiri/mode/cam/anim-3d-fe.js b/src/kiri/mode/cam/anim-3d-fe.js new file mode 100644 index 00000000..3f31d97f --- /dev/null +++ b/src/kiri/mode/cam/anim-3d-fe.js @@ -0,0 +1,253 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +// UI FRONT END ANIMATION CODE for 3D (indexed) + +import { api } from '../../core/api.js'; +import { space } from '../../../moto/space.js'; + +const { client } = api; + +let meshes = {}, + button = {}, + label = {}, + material, + speedPauses = [0, 0, 0, 0, 0], + speedValues = [1, 2, 4, 8, 32], + speedNames = ["1x", "2x", "4x", "8x", "!!"], + speedMax = speedValues.length - 1, + speedIndex = 0, + speed, + origin, + color = 0; + +export function animate_clear2(api) { + let { anim } = api.ui; + client.animate_cleanup2(); + Object.keys(meshes).forEach(id => deleteMesh(id)); + api.widgets.setAxisIndex(0); + api.uc.setVisible(anim.laba, true); + api.uc.setVisible(anim.vala, true); + anim.vala.value = "0.0"; +} + +export function animate2(api, delay) { + let alert = api.alerts.show("building animation"); + let settings = api.conf.get(); + client.animate_setup2(settings, data => { + handleUpdate(data); + if (data) { + return; + } + + let { anim } = api.ui; + Object.assign(button, { + replay: anim.replay, + play: anim.play, + step: anim.step, + pause: anim.pause, + speed: anim.speed, + trans: anim.trans, + model: anim.model, + shade: anim.shade + }); + Object.assign(label, { + progress: anim.progress, + speed: anim.labspd, + x: anim.valx, + y: anim.valy, + z: anim.valz, + a: anim.vala + }); + + updateSpeed(0); + setTimeout(step, delay || 0); + toggleTrans(undefined, false); + origin = settings.origin; + + button.replay.onclick = replay; + button.play.onclick = play; + button.step.onclick = step; + button.pause.onclick = pause; + button.speed.onclick = fast; + button.trans.onclick = toggleTrans; + button.model.onclick = toggleModel; + button.shade.onclick = toggleStock; + button.play.style.display = ''; + button.pause.style.display = 'none'; + + api.event.emit('animate', 'CAM'); + api.alerts.hide(alert); + }); +} + +Object.assign(client, { + animate2(data, ondone) { + client.send("animate2", data, ondone); + }, + + animate_setup2(settings, ondone) { + color = settings.controller.dark ? 0x888888 : 0; + material = new THREE.MeshPhongMaterial({ + flatShading: true, + transparent: false, + opacity: 0.5, + color: 0x888888, + side: THREE.DoubleSide + }); + client.send("animate_setup2", { settings }, ondone); + }, + + animate_cleanup2(data, ondone) { + client.send("animate_cleanup2", data, ondone); + } +}); + +function meshAdd(id, ind, pos, ilen, plen) { + const geo = new THREE.BufferGeometry(); + const pa = plen ? pos.subarray(0, plen * 3) : pos; + const ia = ilen ? ind.subarray(0, ilen) : ind; + geo.setAttribute('position', new THREE.BufferAttribute(pa, 3)); + geo.setIndex(new THREE.BufferAttribute(ia, 1)); + const mesh = new THREE.Mesh(geo, material); + mesh.pos = pos; + mesh.ind = ind; + space.world.add(mesh); + meshes[id] = mesh; +} + +function meshUpdate(id, ind, pos, ilen, plen) { + const mesh = meshes[id]; + if (!mesh) { + return; // animate cancelled + } + const geo = mesh.geometry; + mesh.pos = pos || mesh.pos; + mesh.ind = ind || mesh.ind; + geo.setAttribute('position', new THREE.BufferAttribute(mesh.pos.subarray(0, plen * 3), 3)); + geo.setIndex(new THREE.BufferAttribute(mesh.ind.subarray(0, ilen), 1)); + geo.attributes.position.needsUpdate = true; + geo.index.needsUpdate = true; + space.update(); +} + +function deleteMesh(id) { + space.world.remove(meshes[id]); + delete meshes[id]; +} + +function toggleModel(ev, bool) { + api.local.toggle('cam.anim.model', bool); + api.widgets.all().forEach(w => w.toggleVisibility(bool)); +} + +function toggleStock(ev, bool, set) { + set !== false && api.local.toggle('cam.anim.stock', bool); + return api.event.emit('cam.stock.toggle', bool ?? undefined); +} + +function toggleTrans(ev, bool) { + bool = api.local.toggle('cam.anim.trans', bool); + material.transparent = bool; + material.needsUpdate = true; +} + +function step() { + updateSpeed(); + client.animate2({ speed, steps: 1 }, handleUpdate); +} + +function play(opts) { + const { steps } = opts; + updateSpeed(); + if (steps !== 1) { + button.play.style.display = 'none'; + button.pause.style.display = ''; + } + client.animate2({ + speed, + steps: steps || Infinity, + pause: speedPauses[speedIndex] + }, handleUpdate); +} + +function fast(opts) { + const { steps } = opts; + updateSpeed(1); + button.play.style.display = 'none'; + button.pause.style.display = ''; + client.animate2({ + speed, + steps: steps || Infinity, + pause: speedPauses[speedIndex] + }, handleUpdate); +} + +function pause() { + button.play.style.display = ''; + button.pause.style.display = 'none'; + client.animate2({ speed: 0 }, handleUpdate); +} + +function handleUpdate(data) { + if (!data) { + return; + } + if (data.mesh_add) { + const { id, ind, pos, ilen, plen } = data.mesh_add; + meshAdd(id, ind, pos, ilen, plen); + space.refresh(); + } + if (data.mesh_del) { + deleteMesh(data.mesh_del); + } + if (data.mesh_move) { + const { id, pos } = data.mesh_move; + const mesh = meshes[id]; + if (mesh) { + mesh.position.x = pos.x; + mesh.position.y = pos.y; + mesh.position.z = pos.z; + space.refresh(); + } + label.x.value = (pos.x - origin.x).toFixed(2); + label.y.value = (pos.y + origin.y).toFixed(2); + label.z.value = (pos.z - origin.z).toFixed(2); + } + if (data.stock_index !== undefined) { + api.widgets.setAxisIndex(data.stock_index); + label.a.value = -data.stock_index.toFixed(1); + } + if (data.mesh_index) { + const { id, index } = data.mesh_index; + const mesh = meshes[id]; + if (mesh) { + mesh.rotation.x = (Math.PI / 180) * index; + space.refresh(); + } + } + if (data.mesh_update) { + const { id, ind, pos, ilen, plen } = data.mesh_update; + meshUpdate(id, ind, pos, ilen, plen); + } + if (data && data.progress) { + label.progress.value = (data.progress * 100).toFixed(1); + } +} + +function updateSpeed(inc = 0) { + if (inc === Infinity) { + speedIndex = speedMax; + } else if (inc > 0) { + speedIndex = (speedIndex + inc) % speedValues.length; + } + api.local.set('cam.anim.speed', speedIndex); + speed = speedValues[speedIndex]; + label.speed.value = speedNames[speedIndex]; +} + +function replay() { + animate_clear2(api); + setTimeout(() => { + animate2(api, 50); + }, 250); +} diff --git a/src/kiri/mode/cam/cl-flip.js b/src/kiri/mode/cam/cl-flip.js new file mode 100644 index 00000000..427ae9db --- /dev/null +++ b/src/kiri/mode/cam/cl-flip.js @@ -0,0 +1,84 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from '../../core/api.js'; +import { env, opRender } from './client.js'; +import { CAM } from './driver-fe.js'; +import { clearTabs, restoreTabs } from './cl-tab.js'; +import { updateStock } from './cl-stock.js'; + +export function opFlip() { + api.view.set_arrange(); + let widgets = api.widgets.all(); + let { process } = env.current; + let { ops, op2 } = process; + // add flip singleton to b-side + let add2 = op2.length === 0; + let axis = env.poppedRec.axis; + env.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(); + api.client.clear(); + env.flipping = false; + process.ops = op2; + process.op2 = ops; + // flip camZBottom + if (env.poppedRec.invert && process.camZBottom && env.camZBottom) { + const maxZ = env.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 = env.poppedRec.axis; + op.invert = env.poppedRec.invert; + } + } + if (add2) { + opAdd(env.poppedRec); + } else { + opRender(); + } +} diff --git a/src/kiri/mode/cam/cl-helical.js b/src/kiri/mode/cam/cl-helical.js new file mode 100644 index 00000000..6894f0a6 --- /dev/null +++ b/src/kiri/mode/cam/cl-helical.js @@ -0,0 +1,63 @@ +import { api } from "../../core/api.js"; +import { CAM } from "./driver-fe.js"; +import { clearPops, env } from "./client.js"; + +let alert, lastWidget; +export let helicalOn = false; +export function selectHelical() { + if (helicalOn) { + return helicalDone(); + } + clearPops(); + alert = api.show.alert("analyzing surfaces...", 1000); + let cylinders = env.poppedRec.cylinders; + + CAM.surface_prep(false, () => { + api.hide.alert(alert); + alert = api.show.alert("[esc] cancels surface selection"); + for (let [wid, arr] of Object.entries(cylinders)) { + let widget = api.widgets.forid(wid); + if ( widget && arr?.length ) + for (let faceid of arr) { + CAM.cylinderToggle(widget, faceid, (faceids) => { + // cylinders[widget.id] = faceids; + }); + } + } + }); + helicalOn = env.hoveredOp; + helicalOn.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.cylinderToggle(widget, faceid, ({ faces, error }) => { + if ( error ) { + api.show.alert(error, 3000); + console.log( error ); + return; + } + cylinders[widget.id] = faces; + }); + }; +}; + +export function helicalDone(){ + if (!( helicalOn && env.poppedRec && env.poppedRec.cylinders )) { + return; + } + let { cylinders } = env.poppedRec; + for (let wid of Object.keys(cylinders)) { + let widget = api.widgets.forid(wid); + if (widget) { + CAM.cylinderClear(widget); + } else { + delete cylinders[wid]; + } + } + api.hide.alert(alert); + api.feature.on_mouse_up = undefined; + helicalOn.classList.remove("editing"); + helicalOn = false; +}; diff --git a/src/kiri/mode/cam/cl-hole.js b/src/kiri/mode/cam/cl-hole.js new file mode 100644 index 00000000..8ebf6e56 --- /dev/null +++ b/src/kiri/mode/cam/cl-hole.js @@ -0,0 +1,208 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from '../../core/api.js'; +import { env, clearPops } from './client.js'; +import { CAM } from './driver-fe.js'; +import { lastTrace, setLastTrace } from './cl-trace.js'; + +export let holeSelOn = false; +export let 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. + */ +export async function selectHoles(individual) { + // console.log("client individual selected",individual) + if (holeSelOn) { + return selectHolesDone(); + } + clearPops(); + let alert = api.show.alert("analyzing parts..."); + holeSelOn = env.hoveredOp; + holeSelOn.classList.add("editing"); + api.feature.hover = true; + api.feature.hoverAdds = true; + env.hover = selectHolesHover; + env.hoverUp = selectHolesHoverUp; + + const widgets = 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 => env.poppedRec.drills[widget.id] != undefined) + if (individual && meshesCached) { + // if any widget already has cached holes + // console.log("already has cached holes",env.poppedRec.drills) + api.hide.alert(alert); + api.widgets.for(widget => { + if (widget.adds) { + let drills = env.poppedRec.drills[widget.id]; + let ids = drills.map(hole => hole.meshId); + if (false && widget.adds.includes(mesh)) { + // mesh in the old code was a dead artifact + // so this code branch never actually ran + 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 alert2 = api.show.alert(""); + let found = await CAM.holes( + individual, + env.poppedRec, + (progress, msg) => { + alert2[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; + } + if (centers.length === 0) { + console.log("no drill holes found"); + return; + } + let shadow = centers.some(c => c.shadowed); + api.hide.alert(alert2); + if (shadow) { + alert2 = 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 + 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 + env.poppedRec.drills = env.poppedRec.drills ?? {}; + env.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); + }) + } + ); + if (!found || found.length === 0) { + api.hide.alert(alert); + api.hide.alert(alert2); + api.show.alert("no drill holes found"); + return; + } + } + //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); + api.widgets.opacity(0.8); +} + +export function selectHolesHover(data) { + //not used right now. may be useful in the future + +} + +export function selectHolesHoverUp(int, ev) { + if (!int) return; //if not a hole mesh return + let { object } = int; + selectHoleToggle(object); +} + +/** + * Toggle the selection of a hole mesh and update its color + * @param {Object3D} mesh - the hole mesh to toggle + */ +export function selectHoleToggle(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 + */ +export function 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. + */ +export function selectHolesDone() { + if (!holeSelOn) { + return; + } + env.func.unpop(); + holeSelOn.classList.remove("editing"); + holeSelOn = false; + api.widgets.opacity(1); + api.hide.alert(alert); + api.feature.hover = false; + api.feature.hoverAdds = false; + + api.widgets.for(widget => { + for (let add of widget.adds) { + add.visible = false + widget.mesh.remove(add); + } + }); +} + +export function unselectHoles(widget) { + if (!widget.holes) return + widget.holes.forEach(hole => { + hole.selected = false + }) +} diff --git a/src/kiri/mode/cam/cl-ops.js b/src/kiri/mode/cam/cl-ops.js new file mode 100644 index 00000000..18cfb92d --- /dev/null +++ b/src/kiri/mode/cam/cl-ops.js @@ -0,0 +1,646 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $, h } from '../../../moto/webui.js'; +import { api } from '../../core/api.js'; +import { env } from './client.js'; +import { Tool } from './tool.js'; +import { opFlip } from './cl-flip.js'; +import { selectHoles } from './cl-hole.js'; +import { selectHelical } from './cl-helical.js'; +import { surfaceAdd } from './cl-surface.js'; +import { traceAdd } from './cl-trace.js'; + +const { MODES } = api.const; +const { uc: UC } = api; +const { alerts, conf } = api; +const { current: LANG } = api.language; + +let seed = Date.now(); + +function hasIndexing() { + return env.isIndexed; +} + +function hasSpindle() { + return env.current.device.spindleMax > 0; +} + +function zTop() { + return api.conf.get().process.camZTop > 0; +} + +function zBottom() { + return api.conf.get().process.camZBottom > 0; +} + +// create custom gcode editor function +function gcodeEditor(label, field) { + return function () { + opGCode(label, field); + } +} + +function 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 = env.poppedRec[field] || []; + c_gcode.value = typeof (av) === 'string' ? av : av.join('\n'); + c_gcode.onkeyup = (el) => { + env.poppedRec[field] = c_gcode.value.trim().split('\n'); + }; + c_gcode.focus(); +} + +/** + * 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. + */ +export function createPopOp(type, map) { + let op = env.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] = env.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 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("~")) { + env.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' + ? env.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', env.poppedRec.note || '').then(note => { + if (note !== undefined && note !== null) { + env.poppedRec.note = op.x = note; + api.conf.save(); + } + opRender(); + }); + }; + const note = $(noteid); + if (note) note.innerText = env.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; +} + +export function createPopOps() { + createPopOp('level', { + tool: 'camLevelTool', + spindle: 'camLevelSpindle', + step: 'camLevelOver', + stepz: 'camLevelStepZ', + rate: 'camLevelSpeed', + down: 'camLevelDown', + inset: 'camLevelInset', + 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) }), + stepz: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: UC.toFloat, bound: UC.bound(0, 100.0) }), + rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: UC.toInt, units: true }), + down: UC.newInput(LANG.cc_loff_s, { title: LANG.cc_loff_l, convert: UC.toFloat, units: true }), + inset: UC.newInput(LANG.cc_lxyo_s, { title: LANG.cc_lxyo_l, convert: UC.toFloat, units: true, show: () => !env.popOp.level.rec.stock }), + 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: () => env.popOp.outline.rec.wide }), + steps: UC.newInput(LANG.cc_sovc_s, { title: LANG.cc_sovc_l, convert: UC.toInt, bound: UC.bound(1, 5), show: () => env.popOp.outline.rec.wide }), + 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 (!env.poppedRec) return false; + return env.poppedRec.mode === 'follow';// && env.poppedRec.offset && env.poppedRec.offset !== 'none'; + } + + function canDogBonesRev() { + return canDogBones() && env.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: () => (env.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) => env.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: () => env.poppedRec.offset !== "none" || env.poppedRec.mode === "clear" }), + sep: UC.newBlank({ class: "pop-sep", modes: MODES.CAM, xshow: zDogSep }), + merge: UC.newBoolean(LANG.co_merg_s, undefined, { title: LANG.co_merg_l, show: () => !env.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", 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: () => !env.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: () => !env.poppedRec.contour }), + refine: UC.newInput(LANG.cp_refi_s, { title: LANG.cp_refi_l, convert: UC.toInt, show: () => env.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: () => env.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: () => env.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", 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: () => !env.poppedRec.mark }), + mark: UC.newBoolean(LANG.cd_mark_s, undefined, { title: LANG.cd_mark_l, show: () => !env.poppedRec.fromTop }), + fromTop: UC.newBoolean(LANG.cd_ftop_s, undefined, { title: LANG.cd_ftop_l, show: () => !env.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: () => !env.poppedRec.mark }), + precision: UC.newInput(LANG.cd_prcn_s, { title: LANG.cd_prcn_l, convert: UC.toFloat, units: true, show: () => !env.poppedRec.mark }), + sep: UC.newBlank({ class: "pop-sep", }), + actions: UC.newRow([ + UC.newButton(LANG.select, () => selectHoles(true), { title: LANG.cd_seli_l }), + UC.newButton(LANG.cd_sela_s, () => 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', + offset: 'camRegisterOffset', + 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"}), + offset: UC.newInput(LANG.cd_rego_s, {title:LANG.cd_rego_l, convert:UC.toFloat, units:true, }), + thru: UC.newInput(LANG.cd_thru_s, {title:LANG.cd_thru_l, convert:UC.toFloat, units:true}), + }; + + createPopOp('helical', { + tool: 'camHelicalTool', + offset: 'camHelicalOffset', + spindle: 'camHelicalSpindle', + rate: 'camHelicalDownSpeed', + feed: 'camHelicalSpeed', + down: 'camHelicalDown', + finish: 'camHelicalBottomFinish', + startAng:'camHelicalStartAngle', + offOver: 'camHelicalOffsetOverride', + entry: 'camHelicalEntry', + entryOffset: 'camHelicalEntryOffset', + reverse: 'camHelicalReverse', + clockwise:'camHelicalClockwise', + thru: 'camHelicalThru', + forceStartAng:'camHelicalForceStartAngle', + fromTop: 'camHelicalFromStockTop', + }).inputs = { + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + offset: UC.newSelect(LANG.cc_offs_s, {title: LANG.cc_offs_l,}, "helicaloff"), + 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.toFloat, units:true}), + feed: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toFloat, units:true}), + down: UC.newInput(LANG.ch_sdwn_s, {title:LANG.ch_sdwn_l, convert:UC.toFloat, units:true}), + startAng: UC.newInput(LANG.ch_stra_s, {title:LANG.ch_stra_l, convert:UC.toDegsFloat, bound:UC.bound(-360,360),show:() => env.poppedRec.forceStartAng}), + offOver: UC.newInput(LANG.cc_offd_s, {title:LANG.cc_offd_l, convert:UC.toFloat, units:true, bound:UC.bound(0,Infinity)}), + sep: UC.newBlank({class:"pop-sep"}), + entry: UC.newBoolean(LANG.ch_entr_s,undefined, {title:LANG.ch_entr_l}), + entryOffset: UC.newInput(LANG.ch_ento_s, {title:LANG.ch_ento_l, convert:UC.toFloat, units:true, show:() => env.poppedRec.entry}), + reverse: UC.newBoolean(LANG.ch_rvrs_s,undefined, {title:LANG.ch_rvrs_l}), + clockwise:UC.newBoolean(LANG.ch_clkw_s,undefined, {title:LANG.ch_clkw_l}), + sep: UC.newBlank({class:"pop-sep"}), + finish: UC.newBoolean(LANG.ch_fini_s,undefined, { title:LANG.ch_fini_l ,show: ()=>!env.poppedRec.reverse}), + forceStartAng: UC.newBoolean(LANG.ch_fsta_s, undefined, {title:LANG.ch_fsta_l }), + fromTop: UC.newBoolean(LANG.cd_ftop_s,undefined, {title:LANG.cd_ftop_l}), + sep: UC.newBlank({class:"pop-sep"}), + thru: UC.newInput(LANG.cd_thru_s, {title:LANG.cd_thru_l, convert:UC.toFloat, units:true}), + actions: UC.newRow([ + UC.newButton(LANG.select, selectHelical, {title:LANG.cd_seli_l}), + ], {class:"ext-buttons f-col"}) + }; + + createPopOp('flip', { + axis: 'camFlipAxis', + invert: 'camFlipInvert' + }).inputs = { + axis: UC.newSelect(LANG.cd_axis, {}, "regaxis"), + sep: UC.newBlank({ class: "pop-sep", modes: MODES.CAM, 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, 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'); + } + + 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: () => !env.poppedRec.adapt }), + maxp: UC.newInput(LANG.cl_maxp_s, { title: LANG.cl_maxp_l, convert: UC.toFloat, bound: UC.bound(0, 1.0), show: () => env.poppedRec.adapt }), + minp: UC.newInput(LANG.cl_minp_s, { title: LANG.cl_minp_l, convert: UC.toFloat, bound: UC.bound(0, 1.0), show: () => env.poppedRec.adapt }), + maxz: UC.newInput(LANG.cl_maxz_s, { title: LANG.cl_maxz_l, convert: UC.toFloat, show: () => env.poppedRec.adapt }), + minz: UC.newInput(LANG.cl_minz_s, { title: LANG.cl_minz_l, convert: UC.toFloat, show: () => env.poppedRec.adapt }), + flatz: UC.newInput(LANG.cl_flaz_s, { title: LANG.cl_flaz_l, convert: UC.toFloat, show: () => env.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: () => env.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 = {}; +} diff --git a/src/kiri/mode/cam/cl-origin.js b/src/kiri/mode/cam/cl-origin.js new file mode 100644 index 00000000..5b63f8ac --- /dev/null +++ b/src/kiri/mode/cam/cl-origin.js @@ -0,0 +1,140 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from '../../core/api.js'; +import { env, clearPops } from './client.js'; +import { CAM } from './driver-fe.js'; +import { addbox, clearboxes } from '../../core/boxes.js'; + +export let originSelectOn = false; + +let { world } = api.SPACE; +let alert, lastHover, lastPoints; + +export function originSelect() { + if (originSelectOn) { + return originSelectDone(); + } + + clearPops(); + + alert = api.show.alert("analyzing parts...", 1000); + originSelectOn = true; + + api.feature.hover = true; + api.feature.hoverAdds = true; + env.hover = pointHover; + env.hoverUp = pointHoverUp; + + CAM.traces((ids) => { + api.hide.alert(alert); + alert = api.show.alert("select origin
[esc] to cancel", 1000); + let ends = {}; + let points = {}; + let color = new THREE.Color( 0xff0000 ); + function incpoint(widget, point) { + let rec = ends[point.key]; + if (rec && rec.count++ === 2) { + addpoint(widget, point); + } else if (!rec) { + rec = ends[point.key] = { count: 1 }; + } + } + function addpoint(widget, center) { + let box = addbox(center, color, undefined, undefined, { opacity: 1 }); + widget.adds.push(box); + world.add(box); + box._origin = center; + points[center.key] = { box, center }; + } + api.widgets.for(widget => { + let { pos, tzoff } = widget.track; + let polys = widget.traces.map(trace => { + return trace.fixClosed().clone().move(pos).move({ x:0, y:0, z: -tzoff }); + }); + for (let poly of polys) { + if (poly.circularity() >= 0.99) { + let center = poly.calcCircleCenter(); + if (points[center.key]) { + continue; + } + addpoint(widget, center); + } else if (poly.isOpen()) { + incpoint(widget, poly.first()); + incpoint(widget, poly.last()); + } + } + }); + lastPoints = Object.values(points); + }); +} + +export function originReset() { + let { process} = api.conf.get(); + process.camOriginOffX = 0; + process.camOriginOffY = 0; + process.camOriginOffZ = 0; + api.settings.update_fields(process); + api.platform.update_origin(); + api.conf.save(); + originSelectDone(); +} + +export function originSelectDone() { + if (!originSelectOn) { + return; + } + api.hide.alert(alert); + api.feature.hover = false; + api.feature.hoverAdds = false; + api.widgets.for(widget => { + for (let box of widget.adds) { + world.remove(box); + } + widget.adds.length = 0; + }); + clearboxes(); + originSelectOn = false; +} + +function pointHover(data) { + if (lastHover) { + let { color, colorSave } = lastHover.material[0] || lastHover.material; + color.r = colorSave.r; + color.g = colorSave.g; + color.b = colorSave.b; + } + lastHover = null; + if (data.type === 'platform') { + return; + } + if (!data.int.object._origin) { + return; + } + lastHover = data.int.object; + let material = lastHover.material[0] || lastHover.material; + let color = material.color; + material.colorSave = color.clone(); + color.setHex(0x00ff00); +} + +function pointHoverUp(int, ev) { + if (!int?.object?._origin) return; + let { _origin } = int.object; + let { process, stock } = api.conf.get(); + let { x, y, z } = _origin; + let { camOriginTop, camOriginCenter } = process; + if (camOriginTop) { + z = z - stock.z; + } + if (!camOriginCenter) { + x += stock.x / 2; + y += stock.y / 2; + } + process.camOriginOffX = x; + process.camOriginOffY = y; + process.camOriginOffZ = z; + api.settings.update_fields(process); + api.platform.update_origin(); + api.conf.save(); + originSelectDone(); +} diff --git a/src/kiri/mode/cam/cl-stock.js b/src/kiri/mode/cam/cl-stock.js new file mode 100644 index 00000000..dbb1777d --- /dev/null +++ b/src/kiri/mode/cam/cl-stock.js @@ -0,0 +1,142 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from '../../core/api.js'; +import { env, isDark } from './client.js'; +import { space as SPACE } from '../../../moto/space.js'; + +const { ui: UI } = api; + +let meshZTop, meshZBottom; + +export function updateStock() { + if (env.isAnimate) { + if (env.isIndexed) { + SPACE.world.remove(env.camStock); + env.camStock = undefined; + } + return; + } + + if (!env.isCamMode) { + SPACE.world.remove(meshZTop); + SPACE.world.remove(meshZBottom); + SPACE.world.remove(env.camStock); + env.camStock = null; + meshZTop = null; + meshZBottom = 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 (env.camStock) { + SPACE.world.remove(env.camStock); + env.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 + }); + env.camStock = new THREE.Mesh(geo, mat); + env.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); + env.camStock.lines = lines; + env.camStock.add(lines); + SPACE.world.add(env.camStock); + } + // fight z fighting in threejs + const { scale, position, lines } = env.camStock; + scale.x = x + 0.005; + scale.y = y + 0.005; + scale.z = z + 0.005; + position.x = center.x; + position.y = center.y; + position.z = center.z; + lines.material.color = new THREE.Color(isDark() ? 0x555555 : 0xaaaaaa); + } + + SPACE.world.remove(meshZTop); + 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 + }); + meshZTop = new THREE.Mesh(geo, mat); + meshZTop._max = max; + meshZTop.renderOrder = 1; + meshZTop.position.x = center.x; + meshZTop.position.y = center.y; + meshZTop.position.z = process.camZTop; + SPACE.world.add(meshZTop); + } else { + meshZTop = undefined; + } + + SPACE.world.remove(meshZBottom); + 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 + }); + meshZBottom = new THREE.Mesh(geo, mat); + meshZBottom._max = max; + meshZBottom.renderOrder = 1; + meshZBottom.position.x = center.x; + meshZBottom.position.y = center.y; + meshZBottom.position.z = process.camZBottom; + SPACE.world.add(meshZBottom); + } else { + meshZBottom = undefined; + } + + SPACE.update(); +} diff --git a/src/kiri/mode/cam/cl-surface.js b/src/kiri/mode/cam/cl-surface.js new file mode 100644 index 00000000..bdbbbcae --- /dev/null +++ b/src/kiri/mode/cam/cl-surface.js @@ -0,0 +1,65 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from '../../core/api.js'; +import { env, clearPops } from './client.js'; +import { CAM } from './driver-fe.js'; + +const DEG2RAD = Math.PI / 180; +const RAD2DEG = 180 / Math.PI; + +export let surfaceOn = false; + +let alert, lastWidget; + +export function surfaceAdd(ev) { + if (surfaceOn) { + return surfaceDone(); + } + clearPops(); + alert = api.show.alert("analyzing surfaces...", 1000); + let surfaces = env.poppedRec.surfaces; + let radians = env.poppedRec.follow * DEG2RAD; + CAM.surface_prep(env.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 = env.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; + }); + }; +} + +export function surfaceDone() { + if (!(surfaceOn && env.poppedRec && env.poppedRec.surfaces)) { + return; + } + let surfaces = env.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; +} diff --git a/src/kiri/mode/cam/cl-tab.js b/src/kiri/mode/cam/cl-tab.js new file mode 100644 index 00000000..8237065e --- /dev/null +++ b/src/kiri/mode/cam/cl-tab.js @@ -0,0 +1,227 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from '../../core/api.js'; +import { env, isDark } from './client.js'; +import { addbox, delbox, clearboxes } from '../../core/boxes.js'; +import { traceDone } from './cl-trace.js'; +import { space as SPACE } from '../../../moto/space.js'; + +let showTab, lastTab, tab, iw, ic; + +export 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(); +} + +export 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(); +} + +export 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 sz = stock.z || bounds.max.z; + const zto = sz - iw.track.top; + const zp = (env.camZBottom || camStockIndexed ? env.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(env.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 }; +} + +export 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; + } +} + +export 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); + }); + }); +} + +export 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; + } + clearboxes(); +} + +export 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(); + } + }); + }); +} + +export function tabHover(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); +} + +export function tabHoverUp(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 + (env.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(); +} + +export function tabAdd() { + traceDone(); + alert = api.show.alert("[esc] cancels tab editing"); + api.feature.hover = true; + env.hover = tabHover; + env.hoverUp = tabHoverUp; +} + +export function tabDone() { + delbox('tabb'); + clearboxes(); + api.hide.alert(alert); + api.feature.hover = false; + if (lastTab) { + lastTab.box.material.color.r = 0; + lastTab = null; + } +} + +export function tabClear() { + tabDone(); + api.widgets.all().forEach(widget => { + clearTabs(widget); + widget.saveState(); + }); + api.conf.save(); +} + +function boxColor() { + return isDark() ? 0x00ddff : 0x0000dd; +} + +function boxOpacity() { + return isDark() ? 0.75 : 0.6; +} diff --git a/src/kiri/mode/cam/cl-trace.js b/src/kiri/mode/cam/cl-trace.js new file mode 100644 index 00000000..25755670 --- /dev/null +++ b/src/kiri/mode/cam/cl-trace.js @@ -0,0 +1,181 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from '../../core/api.js'; +import { env, clearPops, isDark } from './client.js'; +import { CAM } from './driver-fe.js'; +import { Layers } from '../../core/layers.js'; +import { Stack } from '../../core/stack.js'; + +export let traceOn = false; +export let lastTrace; + +export function setLastTrace(trace) { + lastTrace = trace; +} + +export function traceAdd(ev) { + if (traceOn) { + return traceDone(); + } + clearPops(); + alert = api.show.alert("analyzing parts...", 1000); + traceOn = env.hoveredOp; + traceOn.classList.add("editing"); + api.feature.hover = true; + api.feature.hoverAdds = true; + env.hover = traceHover; + env.hoverUp = traceHoverUp; + CAM.traces((ids) => { + api.hide.alert(alert); + alert = api.show.alert("[esc] cancels trace editing"); + 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 = (env.poppedRec.areas[widget.id] || []); + let stack = new Stack(widget.mesh); + widget.trace_stack = stack; + widget.traces?.forEach(poly => { + let match = areas.filter(arr => poly.matches(arr)); + let layers = new 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 + api.widgets.for(widget => { + widget.setVisualState({ opacity: 0.25 }); + let areas = (env.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) { + traceToggle(mesh, true); + } + } else if (mesh.selected) { + traceToggle(mesh, true); + } + }); + }); + }, env.poppedRec.select === 'lines'); +} + +export function traceDone() { + if (!traceOn) { + return; + } + env.func.unpop(); + traceOn.classList.remove("editing"); + traceOn = false; + api.widgets.opacity(1); + api.hide.alert(alert); + api.feature.hover = false; + api.feature.hoverAdds = false; + api.widgets.for(widget => { + widget.restoreVisualState(); + if (widget.trace_stack) { + widget.trace_stack.hide(); + widget.adds.removeAll(widget.trace_stack.meshes); + } + }); +} + +export function traceHover(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); +} + +export function traceHoverUp(int, ev) { + if (!int) return; + let { object } = int; + 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)) { + traceToggle(add); + } + } + } +} + +export function traceToggle(obj, skip) { + let material = obj.material[0] || obj.material; + if (!material) return; + let { color, colorSave } = material; + let { widget, poly } = obj.trace; + let areas = env.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(); +} + +export function unselectTraces(widget, skip) { + if (widget.trace_stack) { + widget.trace_stack.meshes.forEach(mesh => { + if (mesh.selected) { + traceToggle(mesh, skip); + } + }); + } +} diff --git a/src/kiri/mode/cam/client.js b/src/kiri/mode/cam/client.js new file mode 100644 index 00000000..59fa9e10 --- /dev/null +++ b/src/kiri/mode/cam/client.js @@ -0,0 +1,913 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $ } from '../../../moto/webui.js'; +import { api } from '../../core/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 '../../core/layers.js'; +import { Stack } from '../../core/stack.js'; +import { updateStock } from './cl-stock.js'; +import { createPopOps } from './cl-ops.js'; +import { tabAdd, tabDone, tabClear, restoreTabs, rotateTabs, updateTabs, clearTabs } from './cl-tab.js'; +import { traceOn, traceDone, unselectTraces } from './cl-trace.js'; +import { holeSelOn, selectHolesDone, clearHolesRec } from './cl-hole.js'; +import { surfaceOn, surfaceDone } from './cl-surface.js'; +import { helicalOn, helicalDone } from './cl-helical.js'; +import { originSelectDone } from './cl-origin.js'; + +const DEG2RAD = Math.PI / 180; +const RAD2DEG = 180 / Math.PI; +const hasSharedArrays = self.SharedArrayBuffer ? true : false; + +const { VIEWS, STACKS } = api.const; +const { noop } = 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(); + originSelectDone(); + selectHolesDone(); + surfaceDone(); + tabDone(); + traceDone(); + selectHolesDone(); + helicalDone(); +} + +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 opAddHelical = () => { + traceDone(); + surfaceDone(); + let rec = env.popOp.helical.new(); + rec.cylinders = { /* 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 "helical": return opAddHelical(); + 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(); + } + if(helicalOn){ + helicalDone(); + } + 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(); + } + if(helicalOn){ + helicalDone(); + } + 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(helicalOn){ + helicalDone(); + } + 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/src/kiri/mode/cam/driver-be.js b/src/kiri/mode/cam/driver-be.js new file mode 100644 index 00000000..d4d6b39c --- /dev/null +++ b/src/kiri/mode/cam/driver-be.js @@ -0,0 +1,120 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { codec } from '../../core/codec.js'; +import { cam_slice, holes, traces } from './slice.js'; +import { cam_prepare } from './prepare.js'; +import { cam_export } from './export.js'; +import { tool as mesh_tool } from '../../../mesh/tool.js'; +import { cylinder_poly_find } from './slice.js'; + +import { init as init_2d } from './anim-2d-be.js'; +import { init as init_3d } from './anim-3d-be.js'; + +function surface_prep(widget, index) { + if (!widget.tool) { + let tool = widget.tool = new mesh_tool(); + let translate = index ? true : false; + tool.index(widget.getGeoVertices({ unroll: true, translate })); + } +}; + +function surface_find(widget, faces, radians) { + surface_prep(widget); + return widget.tool.findConnectedSurface(faces, radians || 0, 0.0); +}; + +function init(worker) { + + const { dispatch } = worker; + + init_2d(worker); + init_3d(worker); + + dispatch.cam_surfaces = function(data, send) { + const { settings, index } = data; + const widgets = Object.values(worker.cache); + for (let widget of widgets) { + if (index) { + widget.setIndexed(true); + widget.setAxisIndex(-index); + } else { + widget.setIndexed(false); + widget.setAxisIndex(0); + } + CAM.surface_prep(widget, index); + } + send.done({}); + }; + + dispatch.cam_surface_find = function(data, send) { + const { id, face, radians } = data; + const widget = worker.cache[id]; + const faces = surface_find(widget, [face], radians); + send.done(faces); + } + + dispatch.cam_traces = async function(data, send) { + const { settings, single } = data; + const widgets = Object.values(worker.cache); + const fresh = []; + for (let widget of widgets) { + if (await traces(settings, widget, single)) { + fresh.push(widget); + } + } + // const fresh = widgets.filter(widget => traces(settings, widget, single)); + send.done(codec.encode(fresh.map(widget => { return { + id: widget.id, + traces: widget.traces, + } } ))); + }; + + dispatch.cam_traces_clear = function(data, send) { + for (let widget of Object.values(worker.cache)) { + delete widget.traces; + delete widget.topo; + } + send.done({}); + }; + + dispatch.cam_holes = async function(data, send) { + const { settings, indiv, rec } = data; + const widgets = Object.values(worker.cache); + const fresh = []; + + for (let [i, widget] of widgets.entries()) { + let ret = await holes(settings, widget, indiv, rec, (prog, msg) => { + send.data({ progress: (i/widgets.length)+(prog/widgets.length), msg }); + }); + if (ret) { + fresh.push(widget); + } + } + + // const fresh = widgets.filter(widget => CAM.traces(settings, widget, single)); + send.done(codec.encode(fresh.map(widget => { return { + id: widget.id, + holes: widget.drills, + shadowed: widget.shadowedDrills + } } ))); + } + + dispatch.cam_cylinder_find = async function(data,send){ + const { id, face, settings } = data; + const widget = worker.cache[id]; + try{ + send.done(cylinder_poly_find(widget, face)); + }catch(error){ + send.done({error}); + } + } +} + +export const CAM = { + init, + surface_prep, + surface_find, + slice: cam_slice, + prepare: cam_prepare, + export: cam_export +}; diff --git a/src/kiri/mode/cam/driver-fe.js b/src/kiri/mode/cam/driver-fe.js new file mode 100644 index 00000000..dbb02a0e --- /dev/null +++ b/src/kiri/mode/cam/driver-fe.js @@ -0,0 +1,128 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from '../../core/api.js'; +import { codec } from '../../core/codec.js'; + +function surface_prep(index, ondone) { + api.client.sync(); + const settings = api.conf.get(); + api.client.send("cam_surfaces", { settings, index }, output => { + ondone(output); + }); +}; + +function surface_show(widget) { + widget.selectFaces(Object.values(widget._surfaces).flat()); +}; + +function cylinder_show(widget) { + widget.selectFaces(Object.values(widget._cylinders).flat()); +}; + +function surface_toggle(widget, face, radians, ondone) { + let surfaces = widget._surfaces = widget._surfaces || {}; + for (let [root, faces] of Object.entries(surfaces)) { + if (faces.contains(face)) { + // delete this face group + delete surfaces[root]; + surface_show(widget); + ondone(Object.keys(surfaces).map(v => parseInt(v))); + return; + } + } + api.client.send("cam_surface_find", { id: widget.id, face, radians }, faces => { + if (faces.length) { + surfaces[face] = faces; + surface_show(widget); + } + ondone(Object.keys(surfaces).map(v => parseInt(v))); + }); +}; + +function surface_clear(widget) { + widget.selectFaces([]); + widget._surfaces = {}; +}; + +function traces(ondone, single) { + api.client.sync(); + const settings = api.conf.get(); + const widgets = api.widgets.map(); + api.client.send("cam_traces", { settings, single }, output => { + const ids = []; + codec.decode(output).forEach(rec => { + ids.push(rec.id); + widgets[rec.id].traces = rec.traces; + }); + ondone(ids); + }); +}; + +function traces_clear(ondone) { + api.client.send("cam_traces_clear", {}, () => { + // console.log({clear_traces: true}); + }); +}; + +function holes(indiv, rec, onProgress, onDone) { + api.client.sync(); + const settings = api.conf.get(); + return new Promise((res,rej)=>{ + api.client.send("cam_holes", { settings, rec, indiv }, output => { + let out = codec.decode(output); + if (out.progress != undefined) { + //if a progress message, + onProgress(out.progress,out.msg) + } else { + api.hide.alert(alert); + onDone(out); + res(out); + } + }); + }) +}; + +function cylinderShow(onProgress,onDone){ + kiri.client.sync(); + const settings = api.conf.get(); +} + + function cylinderToggle(widget, face, onDone){ + let cyls = widget._cylinders = widget._cylinders || {}; + for (let [root, faces] of Object.entries(cyls)) { + if (faces.contains(face)) { // if a face group contains the selected face + // delete this face group + delete cyls[root]; + CAM.cylinder_show(widget); + onDone(Object.keys(cyls).map(v => parseInt(v))); + return; + } + } + //send + api.client.send("cam_cylinder_find", { id: widget.id, face }, ({faces,error}) => { + if (faces?.length) { + cyls[face] = faces; + CAM.cylinder_show(widget); + } + + onDone({faces:Object.keys(cyls).map(v => parseInt(v)),error}); + }); +} + + function cylinderClear(widget){ + widget.selectFaces([]); + widget._cylinders = {}; +} + +export const CAM = { + surface_prep, + surface_show, + cylinder_show, + cylinderToggle, + cylinderClear, + surface_toggle, + surface_clear, + traces, + traces_clear, + holes +}; diff --git a/src/kiri-mode/cam/export.js b/src/kiri/mode/cam/export.js similarity index 81% rename from src/kiri-mode/cam/export.js rename to src/kiri/mode/cam/export.js index 3ac5c2eb..5fe36a4e 100644 --- a/src/kiri-mode/cam/export.js +++ b/src/kiri/mode/cam/export.js @@ -1,28 +1,18 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { base } from '../../../geo/base.js'; +import { version } from '../../../moto/license.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 +32,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 +52,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 +74,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 +97,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 +124,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,7 +156,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) + */ function moveTo(out, opt = {}) { let laser = out.type === 'laser'; let newpos = out.point; @@ -193,7 +194,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++; } @@ -224,14 +225,16 @@ CAM.export = function(print, online) { moveTo({ // speed: Infinity, tool: out.tool, - point: { x: pos.x, y: pos.y, z: newpos.z, a: newpos.a } + point: { x: pos.x, y: pos.y, z: newpos.z, a: newpos.a }, + emit: 0, }, { dx: 0, dy: 0, dz: 1, time: 0 }); moveTo({ // speed: Infinity, tool: out.tool, - point: { x: newpos.x, y: newpos.y, z: newpos.z, a: pos.a } + point: { x: newpos.x, y: newpos.y, z: newpos.z, a: pos.a }, + emit: 1, }, { dx: 1, dy: 1, dz: 0, time: 0 }); @@ -239,14 +242,16 @@ CAM.export = function(print, online) { moveTo({ // speed: Infinity, tool: out.tool, - point: { x: newpos.x, y: newpos.y, z: pos.z, a: pos.a } + point: { x: newpos.x, y: newpos.y, z: pos.z, a: pos.a }, + emit: 0 }, { dx: 1, dy: 1, dz: 0, time: 0 }); moveTo({ // speed: Infinity, tool: out.tool, - point: { x: newpos.x, y: newpos.y, z: newpos.z, a: newpos.a } + point: { x: newpos.x, y: newpos.y, z: newpos.z, a: newpos.a }, + emit: 1 }, { dx: 0, dy: 0, dz: 1, time: 0 }); @@ -255,20 +260,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)) { @@ -281,7 +286,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; @@ -303,9 +308,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 @@ -314,9 +319,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), ";"]; } } @@ -388,7 +393,7 @@ CAM.export = function(print, online) { section('header'); if (!stripComments) { - append(`; Generated by Kiri:Moto ${kiri.version}`); + append(`; Generated by Kiri:Moto ${version}`); append(`; ${new Date().toString()}`); filterEmit(["; Bed left:{left} right:{right} top:{top} bottom:{bottom}"], consts); append(`; Stock X:${stock.x?.round(2)} Y:${stock.y?.round(2)} Z:${stock.z?.round(2)}`); @@ -512,5 +517,3 @@ CAM.export = function(print, online) { print.bytes = bytes + lines - 1; print.bounds = runbox; }; - -}); diff --git a/src/kiri/mode/cam/init-menu.js b/src/kiri/mode/cam/init-menu.js new file mode 100644 index 00000000..69becf60 --- /dev/null +++ b/src/kiri/mode/cam/init-menu.js @@ -0,0 +1,128 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from '../../core/api.js'; +import { originReset, originSelect } from './cl-origin.js'; + +let LANG = api.language.current; +let { CAM } = api.const.MODES, + { $ } = api.web, + { uc, ui } = api, + { bound, toInt, toFloat } = uc, + { newBlank, newButton, newBoolean, newGroup, newInput } = uc, + { newSelect, newLabel, newValue, newRow, newDiv } = uc, + driven = true, + separator = true, + trigger = true, + units = true + ; + +function onBooleanClick(el) { + api.event.emit('click.boolean', el); +} + +function onButtonClick(el) { + api.event.emit('click.button', el); +} + +function zAnchorSave() { + api.conf.update(); + api.platform.update_top_z(); +} + +export function menu() { + let anim = ui.anim = {}; + + return { + + /** Animation Bar */ + + _____: { + _____: newDiv({ addto: $('layer-animate'), set:true, group:false }), + 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,null,{icon:'',title:"transparency",class:"padleft"}), + anim.model = newButton(null,null,{icon:'',title:"show model"}), + anim.shade = newButton(null,null,{icon:'',title:"stock box"}), + ]) + }, + + /** Left Side Menu */ + + _____: 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}), + separator: newBlank({ class:"set-sep", driven }), + camOriginSelect: newRow([ + newButton("select", originSelect), + newButton("reset", originReset), + ], { class: "ext-buttons f-row" }), + _____: 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 }), + camArcEnabled: newBoolean(LANG.cx_arce_s, onBooleanClick, {title:LANG.cx_arce_l}), + camArcTolerance: newInput(LANG.cx_arct_s, {title:LANG.cx_arct_l, convert:toFloat, bound:bound(0,100), units, trigger, show:() => ui.camArcEnabled.checked}), + camArcResolution: newInput(LANG.cx_arcr_s, {title:LANG.cx_arcr_l, convert:toFloat, bound:bound(0,180), trigger, show:() => ui.camArcEnabled.checked}), + + }; +}; diff --git a/src/kiri/mode/cam/op-contour.js b/src/kiri/mode/cam/op-contour.js new file mode 100644 index 00000000..ae7e089a --- /dev/null +++ b/src/kiri/mode/cam/op-contour.js @@ -0,0 +1,129 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; +import { Tool } from './tool.js'; +import { generate as Topo } from './topo3.js'; +import { newPoint } from '../../../geo/point.js'; +import { tip2tipEmit } from '../../../geo/paths.js'; + +function createFilter(op) { + let filter = slices => slices; + let filterString = op.filter?.map(l => l.trim()).join('\n'); + if (filterString) { + try { + const obj = eval(`( ${filterString} )`); + let idx = 0; + if (obj && obj.slices) { + const nadd = []; + filter = function (slices) { + for (let slice of slices) { + if (obj.slices(slice, idx++)) { + nadd.push(slice); + } + } + 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, settings, updateToolDiams } = state; + let filter = createFilter(op); + let toolDiam = this.toolDiam = new Tool(settings, op.tool).fluteDiameter(); + updateToolDiams(toolDiam); + // 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 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; + 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); + } +} + +export { OpContour }; \ No newline at end of file diff --git a/src/kiri/mode/cam/op-drill.js b/src/kiri/mode/cam/op-drill.js new file mode 100644 index 00000000..eefde0cf --- /dev/null +++ b/src/kiri/mode/cam/op-drill.js @@ -0,0 +1,68 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; +import { Tool } from './tool.js'; +import { newPolygon } from '../../../geo/polygon.js'; +import { newSlice } from '../../core/slice.js'; +import { newPoint } from '../../../geo/point.js'; + +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 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()); + } +} + +export { OpDrill }; diff --git a/src/kiri/mode/cam/op-gcode.js b/src/kiri/mode/cam/op-gcode.js new file mode 100644 index 00000000..a3ed9bfb --- /dev/null +++ b/src/kiri/mode/cam/op-gcode.js @@ -0,0 +1,13 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; + +export class OpGCode extends CamOp { + constructor(state, op) { + super(state, op); + } + + prepare(ops, progress) { + ops.addGCode(this.op.gcode); + } +} diff --git a/src/kiri/mode/cam/op-helical.js b/src/kiri/mode/cam/op-helical.js new file mode 100644 index 00000000..82fc7dbc --- /dev/null +++ b/src/kiri/mode/cam/op-helical.js @@ -0,0 +1,278 @@ +import { arcToPath } from "../../../geo/paths.js"; +import { newPolygon } from "../../../geo/polygon.js"; +import { CamOp } from "./op.js"; +import { Tool } from "./tool.js"; +import { cylinder_poly_find } from "./slice.js"; +import { newPoint } from "../../../geo/point.js"; +import { newSlice } from "../../core/slice.js"; + +export class OpHelical extends CamOp { + constructor(state, op) { + super(state, op); + } + async slice(progress) { + let { op, state } = this; + let { settings, addSlices, widget, updateToolDiams, slicer } = state; + let { color } = state; + let { + cylinders, + forceStartAng, + startAng, + fromTop, + offOver, + offset, + down, + thru, + finish, + clockwise, + reverse, + entry, + entryOffset, + } = op; + let {stock} = settings + + let tool = new Tool(settings, op.tool), + toolDiam = tool.fluteDiameter(), + sliceOut = (this.sliceOut = []), + startAngle = forceStartAng ? startAng : 0; + + updateToolDiams(toolDiam); + // console.log({cylinders}) + let faces = cylinders[widget.id] ?? []; + if (faces.faces) faces = faces.faces; + let polys = []; + + //iterate over selected faces + for (let [i,face] of faces.entries()) { + //get poly info for the cylinder + let res + try{ + res = cylinder_poly_find( + widget, + face + ); + }catch(error){ + // don't quit if a cylinder errors out, just skip + console.error(error) + continue + } + let { zmin, zmax, diam, center, interior, faces } = res; + // console.log({ zmin, zmax, diam, center, interior }); + let radius = diam >> 1; + zmax = fromTop ? stock.z: zmax; + + let radAdd, + zBottom = zmin - thru, + numSegs = faces.length / 2, + poly = newPolygon().setOpen(); + + if (offOver) { + //apply offset override + if (offset == "auto") { + radAdd = interior ? -offOver : offOver; + } else if (offset == "inside") { + radAdd = -offOver; + } else if (offset == "outside") { + radAdd = offOver; + } + } else { + //otherwise, calculate based on offset type + if (offset == "auto") { + radAdd = (interior ? -toolDiam : toolDiam) / 2; + } else if (offset == "inside") { + radAdd = -toolDiam / 2; + } else if (offset == "outside") { + radAdd = toolDiam / 2; + } + } + radius += radAdd; + + if( radius <= 0 ){ + //if negative radius, skip this cylinder + console.log('negative radius', radius); + continue + } + + let startPoint = center + .clone() + .setZ(zmax) + .add( + newPoint( + Math.cos(startAngle) * radius, + Math.sin(startAngle) * radius, + 0 + ) + ); + + let currentZ = zmax; + for (;;) { + let bottom = currentZ - down; + if ( bottom > zBottom ) { + //if not at the bottom, do a whole helix + let start = startPoint.setZ(currentZ), + end = startPoint.clone().setZ(bottom); + // console.log(currentZ, bottom, [start.z, end.z]); + let path = arcToPath(start, end, numSegs, { + clockwise, + radius, + center, + }); + // console.log({ path, start, end, radius, center }); + poly.addPoints(path); + } else { + //if at the bottom, do a partial helix + let toBottom = currentZ - zBottom, + ofFull = toBottom / down, + ofCircle = ofFull * (2 * Math.PI), + cwMultiplier = clockwise ? -1 : 1, + finalAngle = startAngle + (ofCircle*cwMultiplier), + finalEnd = center + .clone() + .setZ(zBottom) + .add( + newPoint( + Math.cos(finalAngle) * radius, + Math.sin(finalAngle) * radius, + 0 + ) + ); + // console.log({down,toBottom,ofFull,ofCircle,startAngle,finalAngle,finalEnd}); + + //circle down to final point + poly.addPoints( + arcToPath(startPoint.setZ(currentZ), finalEnd, numSegs , { + clockwise, + radius, + center, + }) + ); + + //if starting at the bottom, reverse poly + if ( reverse ){ + poly.reverse(); + } + + if ( entry ){ + //get first z of poly to use for entry + let firstZ = poly.first().z; + //create new pointArray + let entry =[newPoint(0,0,0)]; + //add center point + let entryCenter = center.clone().setZ(firstZ) + + //if doing a curved enter, add more + if ( entryOffset > 0 ){ + if ( entryOffset > radius ){ + console.error("entryOffset must be less than radius of helix"); + }else{ + //calculate entry center + let lineOut = radius - entryOffset; + let centerOffset = (entryOffset**2 + 2*entryOffset*lineOut) / (2*(entryOffset + lineOut)); + //add entry points + entry.push(... + newPoint(0,-lineOut,0), + arcToPath( + newPoint(0,-lineOut,0), + newPoint(radius,0,0), + 128, + { + clockwise:false, + radius:lineOut, + center:newPoint(centerOffset,0,0) + } + ), + newPoint(radius,0,0), + ) + } + } + //for each point + entry = entry.map(p => { + //rotate by start angle + // add to hole center + // TODO: there's totally a way to do this with a matrix + p.y *= clockwise ? 1 : -1 + p.rotate(startAngle) + return p.add(entryCenter) + }) + //append to start of poly + poly = newPolygon().setOpen().addPoints(entry).addPoints(poly.points); + } + //if doing a finish pass, do a full circle at the bottom + if ( finish && !reverse ){ + poly.addPoints( + arcToPath(finalEnd, finalEnd, numSegs, { + clockwise, + radius, + center, + }), + ); + poly.append( + finalEnd.clone().setZ(zBottom), + ) + } + break; + } + currentZ = bottom; + } + + progress(i/faces.length, "Helical intrerpolation"); + polys.push(poly); + } + + let slice = newSlice(0); + + slice.camTrace = { tool: op.tool, rate: op.feed, plunge: op.rate }; + slice.camLines = polys; + + slice + .output() + .setLayer("Helical", { face: color, line: color }) + .addPolys(slice.camLines); + addSlices(slice); + sliceOut.push(slice); + } + + async prepare(ops, progress) { + let { op, state } = this; + let { settings, widget, addSlices, updateToolDiams } = state; + let { + setTool, + setSpindle, + polyEmit, + camOut, + setPrintPoint, + getPrintPoint, + } = ops; + let { tool, spindle, rate, feed } = op; + setTool(tool, feed, rate); + setSpindle(spindle); + + let [polys] = this.sliceOut.map((s) => s.camLines); + let pp = getPrintPoint(); + + polys = polys.slice(); + for (;;) { + let closestDist = Infinity, + closestI, + closest = null, + dist, + poly; + + for (let i = 0; i < polys.length; i++) { + if (!polys[i]) continue; + if ((dist = polys[i].first().distTo2D(pp)) < closestDist) { + closestDist = dist; + closest = polys[i]; + closestI = i; + } + } + + if (!closest) break; + poly = polys[closestI] + polys[closestI] = null; + //emit + pp = polyEmit(poly,0,1,poly.points[0]) + } + setPrintPoint(pp); + } +} diff --git a/src/kiri/mode/cam/op-index.js b/src/kiri/mode/cam/op-index.js new file mode 100644 index 00000000..d226aac0 --- /dev/null +++ b/src/kiri/mode/cam/op-index.js @@ -0,0 +1,40 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; + +export class OpIndex extends CamOp { + constructor(state, op) { + super(state, op); + } + + weight() { + return 0.1; + } + + 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); + } + } +} diff --git a/src/kiri/mode/cam/op-laser-off.js b/src/kiri/mode/cam/op-laser-off.js new file mode 100644 index 00000000..a474e933 --- /dev/null +++ b/src/kiri/mode/cam/op-laser-off.js @@ -0,0 +1,17 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; + +export 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); + } +} diff --git a/src/kiri/mode/cam/op-laser-on.js b/src/kiri/mode/cam/op-laser-on.js new file mode 100644 index 00000000..076009c9 --- /dev/null +++ b/src/kiri/mode/cam/op-laser-on.js @@ -0,0 +1,17 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; + +export 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); + } +} diff --git a/src/kiri/mode/cam/op-lathe.js b/src/kiri/mode/cam/op-lathe.js new file mode 100644 index 00000000..915ae7e1 --- /dev/null +++ b/src/kiri/mode/cam/op-lathe.js @@ -0,0 +1,119 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; +import { Tool } from './tool.js'; +import { generate as topo4_generate } from './topo4.js'; +import { newPoint } from '../../../geo/point.js'; + +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 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 topo4_generate({ + 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 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); + } +} + +export { OpLathe }; \ No newline at end of file diff --git a/src/kiri/mode/cam/op-level.js b/src/kiri/mode/cam/op-level.js new file mode 100644 index 00000000..50d3ddcb --- /dev/null +++ b/src/kiri/mode/cam/op-level.js @@ -0,0 +1,75 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; +import { Tool } from './tool.js'; +import { newPolygon } from '../../../geo/polygon.js'; +import { polygons as POLY } from '../../../geo/polygons.js'; +import { newSlice } from '../../core/slice.js'; +import { util } from '../../../geo/base.js'; + +class OpLevel extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let { op, state } = this; + let { settings, addSlices, updateToolDiams } = state; + let { zMax, ztOff, color, tshadow } = state; + let { stock } = settings; + + let toolDiam = new Tool(settings, op.tool).fluteDiameter(); + let stepOver = this.stepOver = toolDiam * op.step; + let zTop = zMax + ztOff; + let zBot = zTop - op.down; + let zList = op.stepz ? util.lerp(zTop, zBot, op.stepz) : [ zBot ]; + + 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.inset || 0))); + + POLY.fillArea(clear, 90, stepOver, points); + + let layers = this.layers = []; + for (let z of zList) { + let lines = []; + layers.push(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); + }); + newLayer(); + } + setPrintPoint(printPoint); + } +} + +export { OpLevel }; diff --git a/src/kiri/mode/cam/op-outline.js b/src/kiri/mode/cam/op-outline.js new file mode 100644 index 00000000..9daa9519 --- /dev/null +++ b/src/kiri/mode/cam/op-outline.js @@ -0,0 +1,299 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; +import { Tool } from './tool.js'; +import { newPolygon } from '../../../geo/polygon.js'; +import { newSlice } from '../../core/slice.js'; +import { polygons as POLY } from '../../../geo/polygons.js'; +import { util as base_util } from '../../../geo/base.js'; +import { poly2polyEmit } from '../../../geo/paths.js'; +import { newPoint } from '../../../geo/point.js'; + +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, shadowAt } = state; + let { process, stock } = settings; + + if (op.down <= 0) { + throw `invalid step down "${op.down}"`; + } + let toolDiam = this.toolDiam = new 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; + let lastShadowZ; + // 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 ? shadowAt(data.z, lastShadowZ) : 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)); + lastShadowZ = data.z; + }, 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) { + 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); + } +} + +export { OpOutline }; diff --git a/src/kiri/mode/cam/op-pocket.js b/src/kiri/mode/cam/op-pocket.js new file mode 100644 index 00000000..b3b19b2b --- /dev/null +++ b/src/kiri/mode/cam/op-pocket.js @@ -0,0 +1,285 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; +import { Tool } from './tool.js'; +import { generate as Topo } from './topo3.js'; +import { newPolygon } from '../../../geo/polygon.js'; +import { newSlice } from '../../core/slice.js'; +import { polygons as POLY } from '../../../geo/polygons.js'; +import { util as base_util } from '../../../geo/base.js'; +import { calc_normal, calc_vertex } from '../../../geo/paths.js'; +import { CAM } from './driver-be.js'; + +const DEG2RAG = Math.PI / 180; + +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 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 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; + } + } + } +} + +export { OpPocket }; diff --git a/src/kiri/mode/cam/op-register.js b/src/kiri/mode/cam/op-register.js new file mode 100644 index 00000000..cdffeb87 --- /dev/null +++ b/src/kiri/mode/cam/op-register.js @@ -0,0 +1,158 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; +import { Tool } from './tool.js'; +import { newPolygon } from '../../../geo/polygon.js'; +import { newSlice } from '../../core/slice.js'; +import { newPoint } from '../../../geo/point.js'; +import { util as base_util } from '../../../geo/base.js'; + +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, + toolZ = widget.track.pos.z, + boundMinX = bounds.min.x, + boundMaxX = bounds.max.x, + boundMinY = bounds.min.y, + boundMaxY = bounds.max.y, + toolOffset = tool.fluteDiameter() * 2, + centerX = (boundMinX + boundMaxX) / 2, + centerY = (boundMinY + boundMaxY) / 2, + cutDepth = op.thru || zThru || 0, + pathPoints = [], + stockToSurfaceOffset = stock.z - bounds.max.z, + startZ = bounds.max.z + stockToSurfaceOffset + toolZ, + endZ = toolZ - cutDepth, + cutOffset = op.offset; + + if (!(stock.x && stock.y && stock.z)) { + return; + } + + switch (op.axis.toLowerCase()) { + case "x": + if (op.points == 3) { + pathPoints.push(newPoint(boundMinX - cutOffset, centerY, 0)); + pathPoints.push(newPoint(boundMaxX + cutOffset, centerY - toolOffset, 0)); + pathPoints.push(newPoint(boundMaxX + cutOffset, centerY + toolOffset, 0)); + } else { + pathPoints.push(newPoint(boundMinX - cutOffset, centerY, 0)); + pathPoints.push(newPoint(boundMaxX + cutOffset, centerY, 0)); + } + break; + case "y": + if (op.points == 3) { + pathPoints.push(newPoint(centerX, boundMinY - cutOffset, 0)); + pathPoints.push(newPoint(centerX - toolOffset, boundMaxY + cutOffset, 0)); + pathPoints.push(newPoint(centerX + toolOffset, boundMaxY + cutOffset, 0)); + } else { + pathPoints.push(newPoint(centerX, boundMinY - cutOffset, 0)); + pathPoints.push(newPoint(centerX, boundMaxY + cutOffset, 0)); + } + break; + case "-": + let halfOffset = toolOffset / 2, + loopMinX = boundMinX - cutOffset, + loopMaxX = boundMaxX + cutOffset, + loopMinY = boundMinY - cutOffset - halfOffset, + loopMaxY = boundMaxY + cutOffset + halfOffset, + deltaX = (loopMaxX - loopMinX - halfOffset) / 4, + deltaY = (loopMaxY - loopMinY - halfOffset * 3) / 4, + poly, currentPoint, currentZ; + function start(z) { + currentZ = z; + currentPoint = { x: loopMinX + halfOffset * 0.5, y: loopMinY + halfOffset * 1.5 }; + poly = newPolygon().add(currentPoint.x, currentPoint.y, z); + } + function move(dx, dy) { + currentPoint.x += dx; + currentPoint.y += dy; + poly.add(currentPoint.x, currentPoint.y, currentZ); + } + function rept(count, step, fn) { + while (count-- > 0) { + fn(step, count === 0); + step = -step; + } + } + for (let z of base.util.lerp(startZ, endZ, op.down)) { + let slice = newSlice(z); + addSlices(slice); + sliceOut.push(slice); + start(z); + rept(4, halfOffset, oy => { + move(0, -oy); + move(deltaX, 0); + }); + rept(4, halfOffset, ox => { + move(ox, 0); + move(0, deltaY); + }); + rept(4, halfOffset, oy => { + move(0, oy); + move(-deltaX, 0); + }); + rept(4, halfOffset, ox => { + move(-ox, 0); + move(0, -deltaY); + }); + 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 (pathPoints.length) { + let slice = newSlice(0,null), polys = []; + pathPoints.forEach(point => { + polys.push(newPolygon() + .append(point.clone().setZ(startZ)) + .append(point.clone().setZ(endZ))); + }); + 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()); + } + } +} + +export { OpRegister }; diff --git a/src/kiri/mode/cam/op-rough.js b/src/kiri/mode/cam/op-rough.js new file mode 100644 index 00000000..06e2630e --- /dev/null +++ b/src/kiri/mode/cam/op-rough.js @@ -0,0 +1,320 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; +import { Tool } from './tool.js'; +import { newPolygon } from '../../../geo/polygon.js'; +import { newSlice } from '../../core/slice.js'; +import { polygons as POLY } from '../../../geo/polygons.js'; +import { util as base_util } from '../../../geo/base.js'; +import { poly2polyEmit } from '../../../geo/paths.js'; + +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 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 lsz; + 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, lsz) : shadow.clone(true); + data.slice.shadow = data.shadow; + slices.push(data.slice); + lsz = data.z; + 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") + }); + } +} + +export { OpRough }; diff --git a/src/kiri/mode/cam/op-shadow.js b/src/kiri/mode/cam/op-shadow.js new file mode 100644 index 00000000..5c782636 --- /dev/null +++ b/src/kiri/mode/cam/op-shadow.js @@ -0,0 +1,116 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; +import { polygons as POLY } from '../../../geo/polygons.js'; + +/** + * 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); + } + + weight() { + return 0.1; + } + + 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(); + } +} + +export { OpShadow }; diff --git a/src/kiri/mode/cam/op-trace.js b/src/kiri/mode/cam/op-trace.js new file mode 100644 index 00000000..6ec46dba --- /dev/null +++ b/src/kiri/mode/cam/op-trace.js @@ -0,0 +1,307 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; +import { Tool } from './tool.js'; +import { newPolygon } from '../../../geo/polygon.js'; +import { newSlice } from '../../core/slice.js'; +import { polygons as POLY } from '../../../geo/polygons.js'; +import { util as base_util } from '../../../geo/base.js'; +import { poly2polyEmit } from '../../../geo/paths.js'; +import { newPoint } from '../../../geo/point.js'; + +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 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) { + 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); + // for cases where the shadow IS the poly like + // with lettering without a bounding frame, clip + // will fail and we need to restore the matching poly + let subshadow = true; + for (let spo of shadow) { + if (poly.isInside(spo, 0.01)) { + subshadow = false; + clip = [ poly ]; + break; + } + } + if (subshadow) { + POLY.subtract([ poly ], shadow, clip, undefined, undefined, 0); + } + if (op.outline) { + POLY.clearInner(clip); + } + if (clip.length === 0) { + continue; + } + let count = 999; + let slice = newSliceOut(z); + 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); + if (debug && shadow) slice.output() + .setLayer("trace shadow", {line: 0xff8811}, false) + .addPolys(shadow) + if (debug) slice.output() + .setLayer("trace poly", {line: 0x1188ff}, false) + .addPolys([ poly ]) + slice.output() + .setLayer("trace", {line: color}, false) + .addPolys(slice.camLines) + 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 = {}; + polys = POLY.nest(polys); + 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); + } + } +} + +export { OpTrace }; diff --git a/src/kiri/mode/cam/op-xray.js b/src/kiri/mode/cam/op-xray.js new file mode 100644 index 00000000..ab147a3d --- /dev/null +++ b/src/kiri/mode/cam/op-xray.js @@ -0,0 +1,32 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { CamOp } from './op.js'; +import { newSlice } from '../../core/slice.js'; +import { Slicer } from './slicer.js'; + +class OpXRay extends CamOp { + constructor(state, op) { + super(state, op); + } + + async slice(progress) { + let { widget, addSlices } = this.state; + let slicer = new 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); + } +} + +export { OpXRay }; diff --git a/src/kiri/mode/cam/op.js b/src/kiri/mode/cam/op.js new file mode 100644 index 00000000..9b27734c --- /dev/null +++ b/src/kiri/mode/cam/op.js @@ -0,0 +1,20 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +export class CamOp { + constructor(state, op) { + this.state = state; + this.op = op + } + + type() { + return this.op.type; + } + + weight() { + return 1; + } + + async slice() { } + + prepare() { } +} diff --git a/src/kiri/mode/cam/ops.js b/src/kiri/mode/cam/ops.js new file mode 100644 index 00000000..003a7dcc --- /dev/null +++ b/src/kiri/mode/cam/ops.js @@ -0,0 +1,39 @@ +/** 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'; +import { OpHelical } from './op-helical.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, + "helical": OpHelical, + "laser on": OpLaserOn, + "laser off": OpLaserOff, + "gcode": OpGCode, + "index": OpIndex, + "flip": CamOp +}; diff --git a/src/kiri-mode/cam/prepare.js b/src/kiri/mode/cam/prepare.js similarity index 78% rename from src/kiri-mode/cam/prepare.js rename to src/kiri/mode/cam/prepare.js index eb13ec12..7c6ed901 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 '../../core/render.js'; +import { newPrint } from '../../core/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; @@ -99,6 +88,7 @@ function prepEach(widget, settings, print, firstPoint, update) { engageFactor = process.camFullEngage, arcTolerance = process.camArcTolerance, arcRes = toRadians(process.camArcResolution), + arcEnabled = process.camArcEnabled && arcTolerance > 0 && arcRes > 0, tolerance = 0, drillDown = 0, drillLift = 0, @@ -180,7 +170,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; @@ -188,6 +178,7 @@ function prepEach(widget, settings, print, firstPoint, update) { } feedRate = feed || feedRate || plunge; plungeRate = Math.min(feedRate || plunge, plunge || plungeRate || feedRate); + // console.log({ setTool: toolID, feed, plunge, plungeRate }); } function setLasering(bool, power = 0) { @@ -204,13 +195,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; @@ -252,15 +243,15 @@ function prepEach(widget, settings, print, firstPoint, update) { break; } } - camOut(point.clone().setZ(zmax)); - points.forEach(function(point, index) { + camOut(point.clone().setZ(zmax_outer)); + 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_outer), 0); newLayer(); } @@ -301,9 +292,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,18 +309,17 @@ 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, p.z + zadd - ) + ).setA(p.a ?? lastPoint?.a); } /** @@ -341,9 +331,8 @@ 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) { - - // console.log({point, emit, opts}) + function camOut(point, emit, opts) { + // console.trace({point, emit, opts}) let { center = {}, clockwise = true, @@ -355,7 +344,8 @@ function prepEach(widget, settings, print, firstPoint, update) { // console.log({radius}) const isArc = emit == 2 || emit == 3; - //apply widget movement pos + // apply widget movement pos + const pointA = point.a; point = applyWidgetMovement(point); // console.log(point.z); @@ -367,15 +357,13 @@ function prepEach(widget, settings, print, firstPoint, update) { let rate = feedRate * factor; // carry rotation forward when not overridden - if (point.a === undefined && lastPoint) { - point.a = lastPoint.a; - } else if (lastPoint && point.a !== undefined && lastPoint.a !== undefined) { - let DA = lastPoint.a - point.a; + if (lastPoint && pointA !== undefined && lastPoint.a !== undefined) { + let DA = lastPoint.a - pointA; let MZ = Math.max(lastPoint.z, point.z) // find arc length let AL = (Math.abs(DA) / 360) * (2 * Math.PI * MZ); if (AL >= 1) { - let lerp = base.util.lerp(lastPoint.a, point.a, 1); + let lerp = base.util.lerp(lastPoint.a, pointA, 1); // create interpolated point set for rendering and animation // console.log({ DA, MZ, AL }, lerp.length); for (let a of lerp) { @@ -386,7 +374,7 @@ function prepEach(widget, settings, print, firstPoint, update) { emit, rate, tool, - {type:"lerp"}, + { type: "lerp" }, ); } } @@ -413,7 +401,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 +423,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; @@ -474,21 +462,31 @@ function prepEach(widget, settings, print, firstPoint, update) { } // set new plunge rate - if (!lasering && deltaZ < -tolerance && !isLathe) { + if (!lasering && !isLathe && deltaZ < -tolerance) { let threshold = Math.min(deltaXY / 2, absDeltaZ), modifier = threshold / absDeltaZ; if (synthPlunge && threshold && modifier && deltaXY > tolerance) { // use modifier to speed up long XY move plunge rates // console.log('modifier', modifier); - rate = Math.round(plungeRate + ((feedRate - plungeRate) * modifier)); - cut = 1; + rate = Math.max( + plungeRate, + Math.round(plungeRate + ((feedRate - plungeRate) * modifier)) + ); } else { - rate = 1 / Math.hypot(deltaXY / feedRate , absDeltaZ / plungeRate ); + let L = Math.hypot(deltaXY, absDeltaZ); + let limXY = feedRate * L / deltaXY; + let limZ = plungeRate * L / Math.abs(deltaZ); + rate = Math.min(feedRate, limXY, limZ); + // let zps = len / absDeltaZ; + // rate = Math.max( + // plungeRate, + // 1 / Math.hypot(deltaXY / feedRate, absDeltaZ / plungeRate) + // ); + // console.log({ rate, deltaXY, deltaZ }); } } - - if(isArc) { + if (isArc) { layerOut.mode = currentOp; layerOut.spindle = spindle; lastPoint = layerPush( @@ -501,9 +499,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; @@ -547,8 +544,9 @@ function prepEach(widget, settings, print, firstPoint, update) { if (depthFirst) { depthData.push(polys); } else { - printPoint = poly2polyEmit(polys, printPoint, function(poly, index, count) { - poly.forEachPoint(function(point, pidx, points, offset) { + // 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) { // 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); @@ -560,14 +558,16 @@ 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) { + // get inside vals (the positive ones) let ins = depthData.map(a => a.filter(p => !isNeg(p.depth))); let itops = ins.map(level => { return POLY.nest(level.filter(poly => poly.depth === 0).clone()); }); + // get outside vals (the negative ones) let outs = depthData.map(a => a.filter(p => isNeg(p.depth))); let otops = outs.map(level => { return POLY.nest(level.filter(poly => poly.depth === 0).clone()); @@ -582,7 +582,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, @@ -638,7 +638,7 @@ function prepEach(widget, settings, print, firstPoint, update) { index++; } - return touch; + return touch; } // coming from a previous widget, use previous last point @@ -688,7 +688,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); @@ -717,15 +717,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 @@ -743,137 +740,143 @@ 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 - lineTolerance = 0.001; // do not consider points under 0.001mm for arcs + let arcQ = [], + arcMax = Infinity, // no max arc radius + lineTolerance = 0.001, // do not consider points under 0.001mm for arcs + zTolerance = 0.1; // allow zs of Points to be off by 0.001mm per radian of rotation 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); + if (arcEnabled) 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 (arcEnabled && 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; } - + + let endDelta = e1.distTo2D(e3) + if(endDelta < 0.01){ + console.log("circleCompleteFault") + let last = arcQ.peek() + drainQ(true) + arcQ.push(last) + return e3 + } + + let ddz = Math.abs((e3.z - e4.z) - (e4.z - e5.z)) // take second derivitive of z + let zFault = Math.abs(ddz) > zTolerance 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; // 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) - - if( Math.abs(angle) > arcRes){ + let angle = toRadians(arcQ[0].slopeTo(cc).angleDiff(arcQ[1].slopeTo(cc)).angle) + radFault = Math.abs(angle) > arcRes + if (radFault) { // if so, remove first point + + console.log("secondary radfault,",structuredClone(arcQ),{angle,arcRes,a,b}) 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 ) { - // 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 point is off-center, or too far from center, or too large of a radius + if (dc * arcQ.center.length / arcQ.rSum > arcTolerance || dist > cc.r || cc.r > arcMax || radFault || zFault) { + // let debug = [ dc * arcQ.center.length / arcQ.rSum > arcTolerance, dist > cc.r, cc.r > arcMax, radFault]; + // console.log("point off the arc,",structuredClone(arcQ),radFault,zFault,[dc * arcQ.center.length / arcQ.rSum > arcTolerance , dist > cc.r , cc.r > arcMax]); 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 { @@ -899,7 +902,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... @@ -909,26 +911,32 @@ function prepEach(widget, settings, print, firstPoint, update) { } return point; } - - function drainQ() { + /** + * Emits arcs and/or lines from the arcQ to the current point set. + * @param {boolean} forceCircle emits a single circle iven if poly is not closed. + */ + function drainQ(forceCircle = false) { + // console.trace("draining") 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 ); + let closed = poly.isClosed() + 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, @@ -936,32 +944,35 @@ function prepEach(widget, settings, print, firstPoint, update) { ) // console.log("draining") - - if(arcQ.length == poly.points.length ){ + if (closed && arcQ.length == poly.points.length || forceCircle ) { //if is a circle // generate circle // console.log("circle",{from, to,center}); - let arcPoints = arcToPath( from, from, arcPreviewRes,{ clockwise,center}) + to = forceCircle? from.clone().setZ(to.z) : from + + + let arcPoints = arcToPath(from, to, arcPreviewRes, { clockwise, center }) .map(applyWidgetMovement); - camOut(from,1); - camOut(from,gc,{ center:center.sub(from), clockwise, arcPoints}); - // lastPoint = to.clone(); - }else{ + + // console.log({arcPoints}) + + camOut(from, 1); + camOut(to, gc, { center: center.sub(from), clockwise, arcPoints }); + } 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(); } @@ -969,7 +980,10 @@ function prepEach(widget, settings, print, firstPoint, update) { arcQ.center = undefined; } - // newLayer(); + if (depthFirst) { + newLayer(); + } + return lastPoint; } @@ -1011,7 +1025,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; @@ -1077,7 +1091,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 '../../core/slice.js'; +import { ops as OPS } from './ops.js'; +import { Tool } from './tool.js'; +import { Slicer as cam_slicer } from './slicer.js'; +import { CAM } from './driver-be.js'; /** * DRIVER SLICE CONTRACT @@ -29,7 +18,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 = [], @@ -45,6 +34,15 @@ CAM.slice = async function(settings, widget, onupdate, ondone) { top_stock, top_part, top_gap, top_z, workarea; + axisRotation = axisIndex = undefined; + dark = settings.controller.dark; + color = dark ? 0xbbbbbb : 0; + minToolDiam = Infinity; + maxToolDiam = -Infinity; + tabs = widget.anno.tab; + unsafe = proc.camExpertFast; + units = settings.controller.units === 'in' ? 25.4 : 1; + // allow recomputing later if widget or settings changes const var_compute = () => { let { camStockX, camStockY, camStockZ, camStockOffset } = proc; @@ -57,9 +55,9 @@ CAM.slice = async function(settings, widget, onupdate, ondone) { x: camStockX, y: camStockY, z: camStockZ - } - track = widget.track; + }; ({ camZTop, camZBottom, camZThru } = proc); + track = widget.track; wztop = track.top; ztOff = isIndexed ? (stock.z - bounds.dim.z) / 2 : (stock.z - wztop); zbOff = isIndexed ? (stock.z - bounds.dim.z) / 2 : (wztop - track.box.d); @@ -68,14 +66,6 @@ CAM.slice = async function(settings, widget, onupdate, ondone) { zMax = bounds.max.z; zThru = camZThru; zTop = zMax + ztOff; - minToolDiam = Infinity; - maxToolDiam = -Infinity; - dark = !!settings.controller.dark; - color = dark ? 0xbbbbbb : 0; - tabs = widget.anno.tab; - unsafe = proc.camExpertFast; - units = settings.controller.units === 'in' ? 25.4 : 1; - axisRotation = axisIndex = undefined; part_size = bounds.dim; bottom_gap = zbOff; bottom_part = 0; @@ -105,15 +95,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; @@ -134,6 +124,10 @@ CAM.slice = async function(settings, widget, onupdate, ondone) { return error('no processes specified'); } + if (stock.x === 0 || stock.y === 0 || stock.z === 0) { + return error("one or more stock dimensions is zero
offset stock or set to non zero value"); + } + if (stock.x && stock.y && stock.z && !isIndexed) { if (stock.x + 0.00001 < bounds.max.x - bounds.min.x) { return error('stock X too small for part. resize stock or use offset stock'); @@ -149,7 +143,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(); @@ -165,6 +159,7 @@ CAM.slice = async function(settings, widget, onupdate, ondone) { tabs, cutTabs, cutPolys, + contourPolys, healPolys, shadowAt, slicer, @@ -187,12 +182,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)); }); } @@ -217,7 +212,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; @@ -229,7 +224,7 @@ CAM.slice = async function(settings, widget, onupdate, ondone) { } function setAxisIndex(degrees = 0, absolute = true) { - axisIndex = absolute ? degrees : axisIndex + degrees; + axisIndex = absolute ? degrees : (axisIndex || 0) + degrees; axisRotation = (Math.PI / 180) * axisIndex; widget.setAxisIndex(isIndexed ? -axisIndex : 0); return isIndexed ? -axisIndex : 0; @@ -253,7 +248,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) { @@ -265,17 +260,21 @@ 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); // 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 })); + if (activeOps.length === 0 || activeOps[0].type !== 'index') { + opList.push(new OPS.index(state, { type: "index", index: 0 })); + opTot += opList.peek().weight(); + + } } else { - opList.push(new CAM.OPS.shadow(state, { type: "shadow", silent: true })); + opList.push(new OPS.shadow(state, { type: "shadow", silent: true })); + opTot += opList.peek().weight(); } // determing # of steps and step weighting for progress bar @@ -340,7 +339,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() @@ -355,7 +354,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; @@ -365,11 +364,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(); @@ -378,8 +375,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) { @@ -429,7 +426,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); @@ -512,20 +509,137 @@ CAM.traces = async function(settings, widget, single) { return true; }; - /** - * Generate a list of holes in the model based on the given diameter. - * - * @param {Object} settings - settings object - * @param {Object} widget - widget object - * @param {boolean} individual - if true, drill holes individually - * @param {Object} rec - DrillOp record - * @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) { - let {tool,mark,precision} = rec //TODO: display some visual difference if mark is selected - let toolDiam = new CAM.Tool(settings,tool).fluteDiameter() +/** + * Returns an array of arrays of perpindicular triangles in the mesh. + * Each sub-array is a list of triangle data that are part of the same + * cylinder. + * + * @param {object} settings - settings object + * @param {object} widget - widget object + * @param {object} opts - options object + * @return {array} - array of arrays of triangle data + */ +export async function cylinders(settings, widget, opts){ + + let {} = opts ?? {} + + let {array:verts} = widget.mesh.geometry.attributes.position + let perpTriangles = [] + + //iterate over all triangles + for(let i = 0; i < verts.length; i+=9){ + let a = [verts[i], verts[i+1], verts[i+2]], + b = [verts[i+3], verts[i+4], verts[i+5]], + c = [verts[i+6], verts[i+7], verts[i+8]]; + + //calculate normal + let normal = new THREE.Vector3(a[0]-b[0], a[1]-b[1], a[2]-b[2]) + .cross(new THREE.Vector3(b[0]-c[0], b[1]-c[1], b[2]-c[2])) + .normalize() + + // if perpindicular normal, and at least 2 Zs are the same + if(normal.z.round(5) == 0 && ! (a[2] != b[2] && b[2] != c[2])){ + let minZ = Math.min(a[2], b[2], c[2]), + maxZ = Math.max(a[2], b[2], c[2]); + if(minZ == maxZ){ // all Zs are the same indicated malformed geometry + continue; + } + perpTriangles.push({...[a,b,c], normal,minZ,maxZ,i:index}) + } + + } + //map where zmax, zmin -> triangleData + let cylinderTriangles = new Map() + for(let t of perpTriangles){ + let hash = `${t.minZ.round(5)},${t.maxZ.round(5)}` + if(!cylinderTriangles.has(hash)){ + cylinderTriangles.set(hash,[]) + } + cylinderTriangles.get(hash).push(t) + } + + return Array.from(cylinderTriangles.values()) +} + + + /** + * Find all triangles that are part of the same cylinder as the given triangle + * @param {Widget} widget - widget object + * @param {number} face - index of face in triangle vertex data + * @return {array} - array of all triangle data that belong to the same cylinder + * @requires surface_prep must be called first + */ +export function cylinder_find (widget, face){ + CAM.surface_prep(widget,false); + return widget.tool.findCylinderSurface(face); +} + +export function cylinder_poly_find(widget, face){ + let faces = cylinder_find(widget, face); + let vert = widget.getGeoVertices({ unroll: true, translate: true }).map(v => v.round(4)); + let slicer = new cam_slicer(widget,{}) + let opts = { + dedup:false, + edges: false, + over:true + } + let firstOffset = faces[0]*9 + let [x1,y1,z1,x2,y2,z2,x3,y3,z3]= Array.from(vert.subarray(firstOffset,firstOffset+9)) + let zs = [z1,z2,z3].map(z => z.round(5)) + let zmin = Math.min(...zs); + let zmax = Math.max(...zs); + let zmid = (zmin+zmax)/2 + let cylVerts = new Float32Array(faces.length*9) + + let i = 0 + //for each cylinder face index + for(let index of faces){ + let off = index*9 + //copy the 9 triangle vertices floats in + for(let j = 0;j < 9;j++){ + cylVerts[i] = vert[off+j] + i++ + } + } + //slice the poly at the midpoint + let poly = slicer.sliceZ(zmid, cylVerts,opts)?.polys?.at(0) + if(!poly) throw "slicing returned no poly" + let circular = poly.circularity() > 0.98; + //throw error if not circular + if(!circular) throw "faces must be circular" + + //calculate circle properties + let area = poly.area(), + diam = Math.sqrt(area / Math.PI)* 2, + center = poly.calcCircleCenter(); + + //find direction using normal to center point + + let point = newPoint(x1, y1, z1), + delta = point.setZ(center.z).sub(center), + normal = new THREE.Vector3(x1-x2, y1-y2, z1-z2) + .cross(new THREE.Vector3(x2-x3, y2-y3, z2-z3)), + dotProd = normal.dot(delta), + interior = dotProd < 0 + + return {faces, zmin, zmax, poly, circular, area, diam, center,interior} +} + +/** + * Generate a list of holes in the model based on the given diameter. + * + * @param {Object} settings - settings object + * @param {Object} widget - widget object + * @param {boolean} individual - if true, drill holes individually + * @param {Object} rec - DrillOp record + * @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) { + + 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, @@ -543,52 +657,51 @@ CAM.holes = async function(settings, widget, individual, rec,onProgress) { // keeps the z bottom relative to the part when z align changes 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 = 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; } @@ -598,12 +711,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 @@ -614,41 +726,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) @@ -659,26 +771,45 @@ 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); } +function contourPolys(widget, polys) { + const raycaster = new THREE.Raycaster(); + raycaster.ray.direction.set(0, 0, -1); // ray pointing down Z + for (let poly of polys) { + for (let point of poly.points) { + 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; + } + } +} + function healPolys(noff) { + for (let p of noff) { + if (p.appearsClosed()) { + p.points.pop(); + p.setClosed(); + } + } 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, true); + + return polys; +} \ No newline at end of file diff --git a/src/kiri-mode/cam/slicer.js b/src/kiri/mode/cam/slicer.js similarity index 81% rename from src/kiri-mode/cam/slicer.js rename to src/kiri/mode/cam/slicer.js index ab4b4317..4ed66eff 100644 --- a/src/kiri-mode/cam/slicer.js +++ b/src/kiri/mode/cam/slicer.js @@ -1,44 +1,39 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { base, util } from '../../../geo/base.js'; +import { decode, decodePointArray } from '../../core/codec.js'; +import { newLine, newOrderedLine } from '../../../geo/line.js'; +import { newPoint } from '../../../geo/point.js'; +import { newSlice } from '../../core/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 zDecimal = 3; -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.threaded = this.minions && this.minions.running; this.setPoints(widget.getGeoVertices({ unroll: true, translate: true })); this.computeFeatures(); } } - get threaded() { - return this.options.threaded; - } - // zList = generate list of z vertices // zline = generate list of z vertices with coplanar lines // trace = find z coplanar trace lines @@ -72,19 +67,19 @@ class Slicer { const zList = this.zList; function countZ(z) { - z = z.round(5); + z = z.round(zDecimal); 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); + // points[i] = points[i].round(zDecimal); // } - 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++]); @@ -101,8 +96,8 @@ class Slicer { // use co-flat and co-line detection to adjust slice Z 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; + let zkey = p1.z.toFixed(zDecimal), + area = Math.abs(util.area2(p1, p2, p3)) / 2; if (!zFlat[zkey]) { zFlat[zkey] = area; } else { @@ -112,17 +107,17 @@ class Slicer { // detect zLine (curved region tops/bottoms) // in cam used for ball and v mill tracing if (p1.z === p2.z) { - let zkey = p1.z.toFixed(5); + let zkey = p1.z.toFixed(zDecimal); let zval = zLine[zkey]; zLine[zkey] = (zval || 0) + 1; } if (p2.z === p3.z) { - let zkey = p2.z.toFixed(5); + let zkey = p2.z.toFixed(zDecimal); let zval = zLine[zkey]; zLine[zkey] = (zval || 0) + 1; } if (p3.z === p1.z) { - let zkey = p3.z.toFixed(5); + let zkey = p3.z.toFixed(zDecimal); let zval = zLine[zkey]; zLine[zkey] = (zval || 0) + 1; } @@ -157,11 +152,11 @@ 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; } - const znorm = z.toFixed(5); + const znorm = z.toFixed(zDecimal); return this.zFlat[znorm] ? z + flatoff : z; }); @@ -185,13 +180,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 +214,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 +233,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 +248,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 +294,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 +331,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 +341,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 +365,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(zDecimal)).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))); + // return array.map(v => Math.abs(parseFloat(v.toFixed(zDecimal)))); + return array.map(v => parseFloat(v.toFixed(zDecimal))); } } @@ -474,7 +469,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; @@ -482,37 +477,3 @@ function makeZLine(phash, p1, p2, coplanar, edge) { Slicer.checkOverUnderOn = checkOverUnderOn; Slicer.intersectPoints = intersectPoints; - -kiri.cam_slicer = Slicer; - -moto.broker.subscribe("minion.started", msg => { - const { funcs, cache, reply, log } = msg; - - funcs.cam_slice_init = () => { - cache.slicer = new Slicer(); - }; - - funcs.cam_slice_cleanup = () => { - delete cache.slicer; - }; - - 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 }); - }).then(data => { - data.forEach(rec => { - rec.polys = encode(rec.polys); - if (rec.lines) { - const points = rec.lines.map(l => [l.p1, l.p2]).flat(); - rec.lines = encodePointArray(points); - } - }); - reply({ seq, slices: data }); - }); - }; -}); - -}); diff --git a/src/kiri-mode/cam/slicer2.js b/src/kiri/mode/cam/slicer_topo.js similarity index 92% rename from src/kiri-mode/cam/slicer2.js rename to src/kiri/mode/cam/slicer_topo.js index 89452dac..3fe74ca6 100644 --- a/src/kiri-mode/cam/slicer2.js +++ b/src/kiri/mode/cam/slicer_topo.js @@ -1,16 +1,13 @@ /** 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 { +/** Slicer used in Topo3 and Topo4 to find contour lines */ +export class Slicer { static intersectPoints = intersectPoints; static checkOverUnderOn = checkOverUnderOn; @@ -29,7 +26,7 @@ class Slicer { const max = options.max || Infinity; const len = position.length; const pts = this.points = []; - for (let i=0; i -- 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; +const HPI = Math.PI / 2; +const RAD2DEG = 180 / Math.PI; class Tool { constructor(settings, id, number) { @@ -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() { @@ -111,7 +102,7 @@ class Tool { return this.tool.type === "tapermill"; } - isDrill(){ + isDrill() { return this.tool.type === "drill"; } @@ -171,8 +162,8 @@ class Tool { // z_offset = (1 - Math.cos((dist_from_center / flute_radius_pix_float) * HPI)) * -flute_radius; } else if (taper && dist_from_center >= tip_radius_pix_float) {// if tapered and not in the flat tip radius z_offset = ((dist_from_center - tip_radius_pix_float) / tip_max_radius_offset) * -shaft_offset; - }else if(drill){ - z_offset = -dist_from_center /45; + } else if (drill) { + z_offset = -dist_from_center / 45; } toolOffset.push(dx, dy, z_offset); } else if (shaft_offset && larger_shaft && dist_from_center <= shaft_radius_pix_float) { @@ -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 new file mode 100644 index 00000000..7d8007d7 --- /dev/null +++ b/src/kiri/mode/cam/tools.js @@ -0,0 +1,344 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $ } from '../../../moto/webui.js'; +import { api } from '../../core/api.js'; +import { consts } from '../../core/consts.js'; +import { settings as setconf } from '../../core/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/src/kiri-mode/cam/topo.js b/src/kiri/mode/cam/topo3.js similarity index 82% rename from src/kiri-mode/cam/topo.js rename to src/kiri/mode/cam/topo3.js index 8d669010..84a54fce 100644 --- a/src/kiri-mode/cam/topo.js +++ b/src/kiri/mode/cam/topo3.js @@ -1,29 +1,17 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { codec } from '../../core/codec.js'; +import { newPoint } from '../../../geo/point.js'; +import { newPolygon } from '../../../geo/polygon.js'; +import { newSlice } from '../../core/slice.js'; +import { Slicer as topo_slicer } from './slicer_topo.js'; +import { polygons as POLY } from '../../../geo/polygons.js'; +import { Tool } from './tool.js'; -// 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; -const { CAM } = driver; -const { polygons, newLine, newSlope, newPoint, newPolygon } = base; - -const PRO = CAM.process; -const POLY = polygons; const RAD2DEG = 180 / Math.PI; const DEG2RAD = Math.PI / 180; -class Topo { +export class Topo { constructor() { } async generate(opt = {}) { @@ -49,8 +37,8 @@ class Topo { boundsY = maxY - minY, inside = contour.inside, density = 1 + (contour.reduction || 0), - resolution = tolerance ? tolerance : 1/Math.sqrt(animesh/(boundsX * boundsY)), - tool = new CAM.Tool(settings, contour.tool), + resolution = tolerance ? tolerance : 1 / Math.sqrt(animesh / (boundsX * boundsY)), + tool = new Tool(settings, contour.tool), toolOffset = tool.generateProfile(resolution).profile, toolDiameter = tool.fluteDiameter(), toolStep = toolDiameter * contour.step, //tool.contourOffset(contour.step), @@ -80,11 +68,11 @@ class Topo { }, data = topo.data, newslices = [], - tabsMax = tabs ? Math.max(...tabs.map(tab => tab.dim.z/2 + tab.pos.z)) : 0, + tabsMax = tabs ? Math.max(...tabs.map(tab => tab.dim.z / 2 + tab.pos.z)) : 0, tabsOn = tabs, tabHeight = Math.max(process.camTabsHeight + zBottom, tabsMax), clipTab = tabsOn ? [] : null, - clipTo = inside ? shadow : POLY.expand(shadow, toolDiameter/2 + resolution * 3), + clipTo = inside ? shadow : POLY.expand(shadow, toolDiameter / 2 + resolution * 3), partOff = inside ? 0 : toolDiameter / 2 + resolution, gridDelta = Math.floor(partOff / resolution), debug = false, @@ -104,8 +92,8 @@ class Topo { if (tabs) { clipTab.appendAll(tabs.map(tab => { - let ctab = POLY.expand([tab.poly], toolDiameter/2); - ctab.forEach(ct => ct.z = tab.dim.z/2 + tab.pos.z); + let ctab = POLY.expand([tab.poly], toolDiameter / 2); + ctab.forEach(ct => ct.z = tab.dim.z / 2 + tab.pos.z); return ctab; }).flat()); } @@ -203,13 +191,13 @@ class Topo { async raster(widget, params, onupdate) { const { resolution } = params; const { box } = params; - const { worker, minions } = kiri; + const { dispatch, minions } = self.kiri_worker; const vertices = widget.getGeoVertices({ unroll: true, translate: true }).toShared(); const range = { min: Infinity, max: -Infinity }; // swap XZ in shared array - for (let i=0,l=vertices.length; i 1) { - worker.putCache({ key: widget.id, data: vertices }, { done: data => { - // console.log({ put_cache_done: data }); - }}); + dispatch.putCache({ key: widget.id, data: vertices }, { + done: data => { + // console.log({ put_cache_done: data }); + } + }); let promises = slices.map(slice => { return new Promise(resolve => { @@ -268,21 +258,23 @@ class Topo { return box2; }); - worker.clearCache({}, { done: data => { - // console.log({ clear_cache_done: data }); - }}); + dispatch.clearCache({}, { + done: data => { + // console.log({ clear_cache_done: data }); + } + }); } else { // iterate over shards, merge output // const output = []; for (let slice of slices) { - new kiri.topo_slicer(slice.index) + new topo_slicer(slice.index) .setFromArray(vertices, slice) .slice(resolution) .map(rec => { - const slice = kiri.newSlice(rec.z); + const slice = newSlice(rec.z); slice.index = rec.index; slice.lines = rec.lines; for (let line of rec.lines) { @@ -308,7 +300,7 @@ class Topo { async contour(params, onupdate) { const trace = this.trace; - const concurrent = kiri.minions.running; + const concurrent = self.kiri_worker.minions.running; const { minX, maxX, minY, maxY, boundsX, boundsY, stepsX, stepsY } = params; const { gridDelta, resolution, density, partOff, toolStep, contourX, contourY } = params; @@ -352,8 +344,8 @@ class Topo { let promise = new Promise(resolve => { resolver = () => { // sort output slices (required for async) - slicesY.sort((a,b) => a.z - b.z); - slicesX.sort((a,b) => a.z - b.z); + slicesY.sort((a, b) => a.z - b.z); + slicesX.sort((a, b) => a.z - b.z); newslices.appendAll(slicesY); newslices.appendAll(slicesX); resolve(); @@ -381,7 +373,7 @@ class Topo { let slice = newSlice(gridx); slice.camLines = segments; slice.output() - .setLayer("contour y", {face: color, line: color}) + .setLayer("contour y", { face: color, line: color }) .addPolys(segments); slicesY.push(slice); } @@ -410,7 +402,7 @@ class Topo { let slice = newSlice(gridy); slice.camLines = segments; slice.output() - .setLayer("contour x", {face: color, line: color}) + .setLayer("contour x", { face: color, line: color }) .addPolys(segments); slicesX.push(slice); } @@ -433,7 +425,7 @@ class Topo { } -class Probe { +export class Probe { constructor(params) { @@ -443,7 +435,7 @@ class Probe { this.params = params; // return the touching z given topo x,y and a tool profile - const toolAtZ = this.toolAtZ = function(x,y) { + const toolAtZ = this.toolAtZ = function (x, y) { let sx = stepsX, sy = stepsY, xl = sx - 1, @@ -473,13 +465,13 @@ class Probe { // export z probe function const rx = stepsX / boundsX; const ry = stepsX / boundsX; - const toolAtXY = this.toolAtXY = function(px, py) { + const toolAtXY = this.toolAtXY = function (px, py) { px = Math.round(rx * (px - minX)); py = Math.round(ry * (py - minY)); return toolAtZ(px, py); }; - const zAtXY = this.zAtXY = function(px, py) { + const zAtXY = this.zAtXY = function (px, py) { let ix = Math.round(rx * (px - minX)); let iy = Math.round(ry * (py - minY)); return data[ix * stepsY + iy] || zMin; @@ -489,7 +481,7 @@ class Probe { } -class Trace { +export class Trace { constructor(probe, params) { @@ -508,11 +500,11 @@ class Trace { this.slice = slice = []; } - const newtrace = this.newtrace = function() { + const newtrace = this.newtrace = function () { trace = newPolygon().setOpen(); } - const end_poly = this.end_poly = function(point) { + const end_poly = this.end_poly = function (point) { if (latent) { trace.push(latent); } @@ -532,7 +524,7 @@ class Trace { } } - const log = function(map) { + const log = function (map) { for (let key in map) { const val = map[key]; if (typeof val === 'number') { @@ -542,7 +534,7 @@ class Trace { console.log(...arguments); } - const push_point = this.push_point = function(x, y, z) { + const push_point = this.push_point = function (x, y, z) { const newP = newPoint(x, y, z); const lastP = lastPP;//trace.last(); @@ -562,7 +554,7 @@ class Trace { if (curvesOnly) { const dv = contourX ? Math.abs(lastP.x - x) : Math.abs(lastP.y - y); // const dz = lastPP.z - z; - const angle = Math.atan2( Math.abs(dz), dv) * RAD2DEG; + const angle = Math.atan2(Math.abs(dz), dv) * RAD2DEG; if (angle > maxangle) { end_poly(); } @@ -580,7 +572,7 @@ class Trace { const object = this.object = this; this.inClip = function (clips, checkZ, point) { - for (let i=0; i { - then(kiri.codec.decode(data.slice)); + then(codec.decode(data.slice)); }); } else { this.crossY_sync(params, then); @@ -640,14 +631,14 @@ class Trace { } crossX(params, then) { - const { minions } = kiri; + const { minions } = self.kiri_worker; if (minions && this.cross.concurrent) { minions.queue({ cmd: "trace_x", params }, data => { - then(kiri.codec.decode(data.slice)); + then(codec.decode(data.slice)); }); } else { this.crossX_sync(params, then); @@ -662,7 +653,7 @@ class Trace { let { from, to, x, gridx, gridy } = params; const step = resolution * density; - const checkr = newPoint(0,0); + const checkr = newPoint(0, 0); newslice(); newtrace(); for (let y = from; y < to; y += step) { @@ -700,7 +691,7 @@ class Trace { let { from, to, y, gridx, gridy } = params; const step = resolution * density; - const checkr = newPoint(0,0); + const checkr = newPoint(0, 0); newslice(); newtrace(); for (let x = from; x < to; x += step) { @@ -732,7 +723,7 @@ class Trace { } } -function raster_slice(inputs) { +export function raster_slice(inputs) { const { lines, data, box, resolution, curvesOnly } = inputs; const { flatness, zMin, minY, maxY, stepsY, gridx } = inputs; const { slice } = inputs; @@ -747,7 +738,7 @@ function raster_slice(inputs) { // emit an array of valid line-pairs const len = lines.length; - outer: for (let i=0; i p2.y) { const tp = p1; p1 = p2; p2 = tp }; // eliminate if points "under" other lines - for (let j=0; j= i-1 && j <= i+1) continue; + if (j >= i - 1 && j <= i + 1) continue; let l2 = lines[j], p3 = l2.p1, p4 = l2.p2; // sort p3,p4 by y for comparison if (p3.y > p4.y) { const tp = p3; p3 = p4; p4 = tp }; // it's under the other line - if (Math.max(p1.z,p2.z) < Math.min(p3.z,p4.z)) { + if (Math.max(p1.z, p2.z) < Math.min(p3.z, p4.z)) { // it's inside the other line, too, so skip if (p1.y >= p3.y && p2.y <= p4.y) continue outer; } @@ -779,8 +770,8 @@ function raster_slice(inputs) { gridi = gridx * stepsY + gridy; gridv = data[gridi] || zMin; // strategy using raw lines (faster slice, but more lines) - for (i=0, il=points.length; i gridv || p2.z > gridv); // segment crosses grid y @@ -796,7 +787,7 @@ function raster_slice(inputs) { if (nz > gridv) { gridv = data[gridi] = Math.max(nz, zMin); if (slice) slice.output() - .setLayer("heights", {face: 0, line: 0}) + .setLayer("heights", { face: 0, line: 0 }) .addLine( newPoint(p1.x, y, 0), newPoint(p1.x, y, gridv) @@ -810,9 +801,9 @@ function raster_slice(inputs) { // remove flat lines when curvesOnly if (curvesOnly) { let nup = []; - for (let i=0, p=points, l=p.length; i= flatness) { nup.push(p1, p2); } @@ -823,77 +814,6 @@ function raster_slice(inputs) { return points; }; -CAM.Topo = async function(opt) { +export async function generate(opt) { return new Topo().generate(opt); -}; - -moto.broker.subscribe("minion.started", msg => { - const { funcs, cache, reply, log } = msg; - - funcs.topo_raster = (data, seq) => { - const { id, slice, params } = data; - const { resolution } = params; - const vertices = cache[id]; - const box = new THREE.Box2(); - new kiri.topo_slicer(slice.index) - .setFromArray(vertices, slice) - .slice(resolution) - .forEach(rec => { - const { z, index, lines } = rec; - - for (let line of lines) { - const { p1, p2 } = line; - if (!p1.swapped) { p1.swapXZ(); p1.swapped = true } - if (!p2.swapped) { p2.swapXZ(); p2.swapped = true } - } - - raster_slice({ - ...params, - box, - lines, - gridx: index - }); - }); - // only pass back bounds of rasters to be merged - reply({ seq, box }); - }; - - funcs.trace_init = data => { - const { cache } = self; - const { codec } = kiri; - data.cross.clipTo = codec.decode(data.cross.clipTo); - data.cross.clipTab = codec.decode(data.cross.clipTab); - const probe = new Probe(data.probe); - const trace = new Trace(probe, data.trace); - cache.trace = { - probe, - trace, - cross: data.cross - }; - trace.init(data.cross); - }; - - funcs.trace_y = (data, seq) => { - const { cache } = self; - const { trace } = cache.trace; - trace.crossY_sync(data.params, slice => { - slice = kiri.codec.encode(slice); - reply({ seq, slice }); - }); - }; - - funcs.trace_x = (data, seq) => { - const { cache } = self; - const { trace } = cache.trace; - trace.crossX_sync(data.params, slice => { - slice = kiri.codec.encode(slice); - reply({ seq, slice }); - }); - }; - - funcs.trace_cleanup = () => { - delete cache.trace; - }; -}); - -}); +} diff --git a/src/kiri-mode/cam/topo4.js b/src/kiri/mode/cam/topo4.js similarity index 77% rename from src/kiri-mode/cam/topo4.js rename to src/kiri/mode/cam/topo4.js index a0df0e50..5bd4f528 100644 --- a/src/kiri-mode/cam/topo4.js +++ b/src/kiri/mode/cam/topo4.js @@ -1,25 +1,13 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import { codec } from '../../core/codec.js'; +import { newPoint } from '../../../geo/point.js'; +import { newPolygon } from '../../../geo/polygon.js'; +import { sliceConnect } from '../../../geo/slicer.js'; +import { newSlice } from '../../core/slice.js'; +import { Tool } from './tool.js'; +import { Slicer as topo_slicer } from './slicer_topo.js'; -// 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; -const { CAM } = driver; -const { polygons, newPoint, newPolygon, sliceConnect } = base; - -const PRO = CAM.process; -const POLY = polygons; const RAD2DEG = 180 / Math.PI; const DEG2RAD = Math.PI / 180; @@ -29,7 +17,7 @@ function scale(fn, factor = 1, base = 0) { } } -class Topo4 { +export class Topo { constructor() { } async generate(opt = {}) { @@ -38,7 +26,7 @@ class Topo4 { let { controller, process } = settings; let axis = op.axis.toLowerCase(), - tool = new CAM.Tool(settings, op.tool), + tool = new Tool(settings, op.tool), bounds = widget.getBoundingBox().clone(), density = parseInt(controller.animesh || 100) * 2500, { min, max } = bounds, @@ -104,7 +92,7 @@ class Topo4 { const box = this.box = new THREE.Box2(); // swap XZ in shared array - for (let i=0, l=vertices.length; i 1) { + const { minions } = self.kiri_worker; + if (minions?.running > 1) { return await this.sliceMinions(onupdate); } else { return await this.sliceWorker(onupdate); @@ -154,16 +143,15 @@ class Topo4 { async sliceWorker(onupdate) { const { vertices, slices, resolution } = this; - const { codec } = kiri; let output = []; let complete = 0; for (let slice of slices) { - const recs = new kiri.topo_slicer(slice.index) + const recs = new topo_slicer(slice.index) .setFromArray(vertices, slice) .slice(resolution) .map(rec => { - const slice = kiri.newSlice(rec.z); + const slice = newSlice(rec.z); for (let line of rec.lines) { const { p1, p2 } = line; if (!p1.swapped) { p1.swapXZ(); p1.swapped = true } @@ -172,7 +160,7 @@ class Topo4 { slice.index = rec.index; slice.addTops(sliceConnect(rec.lines)); - const points = codec.encodePointArray(rec.lines.map(l => [ l.p1, l.p2 ]).flat()); + const points = codec.encodePointArray(rec.lines.map(l => [l.p1, l.p2]).flat()); const shared = new Float32Array(new SharedArrayBuffer(points.length * 4)); shared.set(points); slice.shared = shared; @@ -180,14 +168,13 @@ class Topo4 { return slice; }); output.appendAll(recs); - onupdate(++complete /slices.length); + onupdate(++complete / slices.length); } return output; } async sliceMinions(onupdate) { - const { codec } = kiri; const { queue, putCache, clearCache } = this; const { vertices, slices, resolution } = this; putCache("vertices", vertices); @@ -210,14 +197,15 @@ class Topo4 { .map(rec => newSlice(rec.z) .addTops(rec.polys) .setFields({ shared: rec.shared })) - .sort((a,b) => a.z - b.z); + .sort((a, b) => a.z - b.z); clearCache(); return output; } async lathe(onupdate) { - if (kiri.minions.running > 1) { + const { minions } = self.kiri_worker; + if (minions?.running > 1) { return await this.latheMinions(onupdate); } else { return await this.latheWorker(onupdate); @@ -238,7 +226,7 @@ class Topo4 { const lines = slice.lines; const plen = lines.length; const rec = { z: slice.z, lines: [] }; - for (let i = 0; i < plen; ) { + for (let i = 0; i < plen;) { ++i; // skip x which should match slice.z let py0 = lines[i++]; const pz0 = lines[i++]; @@ -260,7 +248,7 @@ class Topo4 { const rx = oslices[si].z; let mz = -Infinity; // iterate over tool offsets - for (let ti = 0; ti < tlen; ) { + for (let ti = 0; ti < tlen;) { // tool offset in grid units from present x (si) const xo = tool[ti++]; // x grid offset (slice) const yo = tool[ti++] * resolution; // y grid offset (mult rez to get real y) @@ -274,7 +262,7 @@ class Topo4 { const slice = oslices[ts]; const lines = slice.lines; const plen = lines.length; - for (let i = 0; i < plen; ) { + for (let i = 0; i < plen;) { ++i; // skip x which should match slice.z let py0 = lines[i++]; const pz0 = lines[i++]; @@ -308,7 +296,7 @@ class Topo4 { } async latheWorker(onupdate) { - const { sliced, tool, zoff, leave } = this; + const { sliced, tool, zoff, leave, linear } = this; const rota = this.angle * DEG2RAD; const steps = (Math.PI * 2) / rota; @@ -331,19 +319,32 @@ class Topo4 { onupdate(count / steps); } - count = recs[0].heights.length / 3; + count = linear ? recs.length : recs[0].heights.length / 3; + // count = recs[0].heights.length / 3; while (count-- > 0) { let slice = newSlice(count); - slice.camLines = [ newPolygon().setOpen() ]; + slice.camLines = [newPolygon().setOpen()]; paths.push(slice); } - for (let rec of recs) { - const { degrees, heights } = rec; - [...heights].group(3).forEach((a,i) => { - // progress each path 360 degrees to prevent A rolling backwards - paths[i].camLines[0].push( newPoint(a[0], a[1], a[2] + leave).setA(degrees + i * -360) ); + if (linear) { + recs.forEach((rec, i) => { + const { degrees, heights } = rec; + [...heights].group(3).forEach((a) => { + paths[i].camLines[0].push(newPoint(a[0], a[1], a[2] + leave).setA(degrees)); + }); + if (i % 2 === 1) { + paths[i].camLines[0].reverse(); + } }); + } else { + for (let rec of recs) { + const { degrees, heights } = rec; + [...heights].group(3).forEach((a, i) => { + // progress each path 360 degrees to prevent A rolling backwards + paths[i].camLines[0].push(newPoint(a[0], a[1], a[2] + leave).setA(degrees + i * -360)); + }); + } } for (let slice of paths) { @@ -357,7 +358,7 @@ class Topo4 { slice.camLines[0].push(repeat.clone().setA(repeat.a - 360)); slice.output() .setLayer("lathe", { line: this.lineColor }) - .addPoly(poly.clone().applyRotations().move({ z: -zoff, x:0, y:0 })); + .addPoly(poly.clone().applyRotations().move({ z: -zoff, x: 0, y: 0 })); } // console.log({ tool, slices, paths }); @@ -407,20 +408,18 @@ class Topo4 { await Promise.all(promises); recs.sort((a, b) => { return b.angle - a.angle }); - // let linear = true; - count = linear ? recs.length : recs[0].heights.length / 3; while (count-- > 0) { let slice = newSlice(count); - slice.camLines = [ newPolygon().setOpen() ]; + slice.camLines = [newPolygon().setOpen()]; paths.push(slice); } if (linear) { - recs.forEach((rec,i) => { + recs.forEach((rec, i) => { const { degrees, heights } = rec; [...heights].group(3).forEach((a) => { - paths[i].camLines[0].push( newPoint(a[0], a[1], a[2] + leave).setA(degrees) ); + paths[i].camLines[0].push(newPoint(a[0], a[1], a[2] + leave).setA(degrees)); }); if (i % 2 === 1) { paths[i].camLines[0].reverse(); @@ -429,9 +428,9 @@ class Topo4 { } else { for (let rec of recs) { const { degrees, heights } = rec; - [...heights].group(3).forEach((a,i) => { + [...heights].group(3).forEach((a, i) => { // progress each path 360 degrees to prevent A rolling backwards - paths[i].camLines[0].push( newPoint(a[0], a[1], a[2] + leave).setA(degrees + i * -360) ); + paths[i].camLines[0].push(newPoint(a[0], a[1], a[2] + leave).setA(degrees + i * -360)); }); } } @@ -449,7 +448,7 @@ class Topo4 { } slice.output() .setLayer("lathe", { line: this.lineColor }) - .addPoly(poly.clone().applyRotations().move({ z: -zoff, x:0, y:0 })); + .addPoly(poly.clone().applyRotations().move({ z: -zoff, x: 0, y: 0 })); } // console.log({ tool, slices, paths }); @@ -459,78 +458,27 @@ class Topo4 { } putCache(key, data) { - kiri.worker.putCache({ key, data }, { done: data => { } }); + const { dispatch } = self.kiri_worker; + dispatch.putCache({ key, data }, { done: data => { } }); } clearCache() { - kiri.worker.clearCache({}, { done: data => { } }); + const { dispatch } = self.kiri_worker; + dispatch.clearCache({}, { done: data => { } }); } queue(cmd, params) { + const { minions } = self.kiri_worker; return new Promise(resolve => { - kiri.minions.queue({ cmd, ...params }, resolve); + minions.queue({ cmd, ...params }, resolve); }); } } -function rotatePoints(lines, rot) { +export function rotatePoints(lines, rot) { new THREE.BufferAttribute(lines, 3).applyMatrix4(rot); } -CAM.Topo4 = function(opt) { - return new Topo4().generate(opt); +export async function generate(opt) { + return new Topo().generate(opt); }; - -moto.broker.subscribe("minion.started", msg => { - const { funcs, cache, reply, log } = msg; - const { codec } = kiri; - - funcs.topo4_slice = (data, seq) => { - const { slice, resolution } = data; - const vertices = cache.vertices; - const recs = new kiri.topo_slicer(slice.index) - .setFromArray(vertices, slice) - .slice(resolution) - .map(rec => { - const { z, index, lines } = rec; - - for (let line of lines) { - const { p1, p2 } = line; - if (!p1.swapped) { p1.swapXZ(); p1.swapped = true } - if (!p2.swapped) { p2.swapXZ(); p2.swapped = true } - } - - const points = codec.encodePointArray(lines.map(l => [ l.p1, l.p2 ]).flat()); - const shared = new Float32Array(new SharedArrayBuffer(points.length * 4)); - shared.set(points); - - return { - z, index, shared, - polys: codec.encode(sliceConnect(lines)), - }; - }); - // only pass back bounds of rasters to be merged - reply({ seq, recs }); - }; - - funcs.topo4_lathe = (data, seq) => { - const { angle } = data; - const { slices, tool } = cache.lathe; - - const axis = new THREE.Vector3(1, 0, 0); - const mrot = new THREE.Matrix4().makeRotationAxis(axis, -angle); - const stmp = slices.map(s => { - const lines = s.lines.slice(); - rotatePoints(lines, mrot); - return { z: s.z, lines } - }); - - const topo4 = Object.assign(new Topo4(), cache.lathe); - const heights = topo4.lathePath(stmp, tool); - - reply({ seq, heights }); - } - -}); - -}); diff --git a/src/kiri/mode/drag/client.js b/src/kiri/mode/drag/client.js new file mode 100644 index 00000000..0b17b4e1 --- /dev/null +++ b/src/kiri/mode/drag/client.js @@ -0,0 +1,5 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +export function init() { + // TODO +} diff --git a/src/kiri/mode/drag/driver.js b/src/kiri/mode/drag/driver.js new file mode 100644 index 00000000..a6b5c853 --- /dev/null +++ b/src/kiri/mode/drag/driver.js @@ -0,0 +1,13 @@ +/** 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/src/kiri-mode/fdm/client.js b/src/kiri/mode/fdm/client.js similarity index 90% rename from src/kiri-mode/fdm/client.js rename to src/kiri/mode/fdm/client.js index 6e7709db..a7471a5d 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 '../../core/api.js'; +import { conf } from '../../core/conf.js'; +import { util } from '../../../geo/base.js'; +import { client } from '../../core/client.js'; +import { addbox, delbox, getlastbox } from '../../core/boxes.js'; +import { Layers } from '../../core/layers.js'; +import { Stack } from '../../core/stack.js'; +import { Widget, newWidget } from '../../core/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/src/kiri/mode/fdm/driver-be.js b/src/kiri/mode/fdm/driver-be.js new file mode 100644 index 00000000..397670e2 --- /dev/null +++ b/src/kiri/mode/fdm/driver-be.js @@ -0,0 +1,44 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { util } from '../../../geo/base.js'; +import { fdm_slice } from './slice.js'; +import { fdm_prepare } from './prepare.js'; +import { fdm_export } from './export.js'; + +// 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 +function init(worker) { + + worker.dispatch.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, + } } ))); + }; + +} + +export const FDM = { + init, + extrudePerMM, + extrudeMM, + slice: fdm_slice, + prepare: fdm_prepare, + export: fdm_export +}; diff --git a/src/kiri/mode/fdm/driver.js b/src/kiri/mode/fdm/driver.js new file mode 100644 index 00000000..ea0deabd --- /dev/null +++ b/src/kiri/mode/fdm/driver.js @@ -0,0 +1,22 @@ +/** 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/src/kiri-mode/fdm/export.js b/src/kiri/mode/fdm/export.js similarity index 98% rename from src/kiri-mode/fdm/export.js rename to src/kiri/mode/fdm/export.js index 1570bb93..8481bdc0 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 similarity index 91% rename from src/kiri-mode/fdm/fill.js rename to src/kiri/mode/fdm/fill.js index 1ef29575..830dec06 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 { newPolygon } from '../../../geo/polygon.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 { @@ -124,7 +119,7 @@ function fillGyroid(target) { z: 0 } }); - polys.push(base.newPolygon().setOpen(true).addObj(points)); + polys.push(newPolygon().setOpen(true).addObj(points)); } } polys = connectOpenPolys(polys); @@ -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 { @@ -141,7 +136,7 @@ function fillGyroid(target) { z: 0 } }); - polys.push(base.newPolygon().setOpen(true).addObj(points)); + polys.push(newPolygon().setOpen(true).addObj(points)); } } polys = connectOpenPolys(polys); @@ -323,5 +318,3 @@ function fillTriangle(target) { target.emit(xp, bounds.max.y); } } - -}); diff --git a/src/kiri/mode/fdm/init-menu.js b/src/kiri/mode/fdm/init-menu.js new file mode 100644 index 00000000..bb34b97f --- /dev/null +++ b/src/kiri/mode/fdm/init-menu.js @@ -0,0 +1,188 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from '../../core/api.js'; + +let LANG = api.language.current; +let { FDM } = api.const.MODES, + { $ } = api.web, + { uc, ui } = api, + { bound, toInt, toFloat } = uc, + { newBlank, newButton, newBoolean, newGroup, newInput, newSelect, newRow } = uc, + driven = true, + hideable = true, + separator = true, + trigger = true + ; + +function onBooleanClick(el) { + api.event.emit('click.boolean', el); +} + +function onButtonClick(el) { + api.event.emit('click.button', el); +} + +function thinWallSave() { + let opt = ui.sliceDetectThin; + let level = opt.options[opt.selectedIndex]; + if (level) { + api.conf.get().process.sliceDetectThin = level.value; + api.conf.save(); + } +} + +function isMultiHead() { + let dev = api.conf.get().device; + return isNotBelt() && dev.extruders && dev.extruders.length > 1; +} + +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 zIntShow() { + return settings().controller.devel; +} + +function isBelt() { + return api.device.isBelt(); +} + +function isNotBelt() { + return !isBelt(); +} + +export function menu() { + + return { + + /** Left Side Menu */ + + _____: 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'), + + }; +} diff --git a/src/kiri-mode/fdm/post.js b/src/kiri/mode/fdm/post.js similarity index 94% rename from src/kiri-mode/fdm/post.js rename to src/kiri/mode/fdm/post.js index 5385f041..dd0edbc8 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 similarity index 98% rename from src/kiri-mode/fdm/prepare.js rename to src/kiri/mode/fdm/prepare.js index 42ffa6cd..c7a90638 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 '../../core/print.js'; +import { newSlice } from '../../core/slice.js'; +import { newWidget } from '../../core/widget.js'; +import { render } from '../../core/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 similarity index 96% rename from src/kiri-mode/fdm/slice.js rename to src/kiri/mode/fdm/slice.js index 45789e10..cec07284 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 '../../core/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/src/kiri/mode/laser/client.js b/src/kiri/mode/laser/client.js new file mode 100644 index 00000000..0b17b4e1 --- /dev/null +++ b/src/kiri/mode/laser/client.js @@ -0,0 +1,5 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +export function init() { + // TODO +} diff --git a/src/kiri-mode/laser/driver.js b/src/kiri/mode/laser/driver.js similarity index 94% rename from src/kiri-mode/laser/driver.js rename to src/kiri/mode/laser/driver.js index ee4909d2..50407301 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 '../../core/pack.js'; +import { newBounds } from '../../../geo/bounds.js'; +import { newPoint } from '../../../geo/point.js'; +import { newPrint } from '../../core/print.js'; +import { newSlopeFromAngle } from '../../../geo/slope.js'; +import { polygons as POLY } from '../../../geo/polygons.js'; +import { render } from '../../core/render.js'; +import { slicer } from '../../../geo/slicer.js'; +import { newSlice } from '../../core/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/laser/init-menu.js b/src/kiri/mode/laser/init-menu.js new file mode 100644 index 00000000..14d40eee --- /dev/null +++ b/src/kiri/mode/laser/init-menu.js @@ -0,0 +1,74 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from '../../core/api.js'; + +let LANG = api.language.current; +let { LASER, DRAG, WJET, WEDM } = api.const.MODES, + TWOD = [ LASER, DRAG, WJET, WEDM ], + TWONED = [ LASER, DRAG, WJET ], + { $ } = api.web, + { uc, ui } = api, + { bound, toInt, toFloat } = uc, + { newBlank, newButton, newBoolean, newGroup, newInput, newRow } = uc, + driven = true, + separator = true, + trigger = true + ; + +function onBooleanClick(el) { + api.event.emit('click.boolean', el); +} + +function onButtonClick(el) { + api.event.emit('click.button', el); +} + +export function menu() { + + return { + + /** Left Side Menu */ + + _____: 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 }), + + }; + +} diff --git a/src/kiri/mode/sla/client.js b/src/kiri/mode/sla/client.js new file mode 100644 index 00000000..0d1f849d --- /dev/null +++ b/src/kiri/mode/sla/client.js @@ -0,0 +1,86 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $ } from '../../../moto/webui.js'; +import { api } from '../../core/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/src/kiri-mode/sla/driver.js b/src/kiri/mode/sla/driver.js similarity index 71% rename from src/kiri-mode/sla/driver.js rename to src/kiri/mode/sla/driver.js index e0bc8488..1a160945 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 '../../core/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 similarity index 92% rename from src/kiri-mode/sla/export.js rename to 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/init-menu.js b/src/kiri/mode/sla/init-menu.js new file mode 100644 index 00000000..fa45d671 --- /dev/null +++ b/src/kiri/mode/sla/init-menu.js @@ -0,0 +1,58 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from '../../core/api.js'; + +let LANG = api.language.current; +let { SLA } = api.const.MODES, + { $ } = api.web, + { uc, ui } = api, + { bound, toInt, toFloat } = uc, + { newBoolean, newGroup, newInput, newSelect } = uc, + driven = true, + separator = true + ; + +function onBooleanClick(el) { + api.event.emit('click.boolean', el); +} + +export function menu() { + + return { + + /** Left Side Menu */ + + 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"), + + }; + +} diff --git a/src/kiri-mode/sla/slice.js b/src/kiri/mode/sla/slice.js similarity index 94% rename from src/kiri-mode/sla/slice.js rename to src/kiri/mode/sla/slice.js index c55e0aea..2c2e3d4e 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 '../../core/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 similarity index 98% rename from src/kiri-mode/sla/x_photon.js rename to 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/src/kiri-mode/wedm/driver.js b/src/kiri/mode/wedm/client.js similarity index 76% rename from src/kiri-mode/wedm/driver.js rename to src/kiri/mode/wedm/client.js index f94c7808..cf5db035 100644 --- a/src/kiri-mode/wedm/driver.js +++ b/src/kiri/mode/wedm/client.js @@ -1,21 +1,6 @@ /** 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; +import { api } from '../../core/api.js'; const state = { api: undefined, @@ -25,6 +10,45 @@ const state = { selecting: false }; +export function init() { + 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; + } + }); +} + function faceAdd() { if (state.selecting) return faceDone(); @@ -72,61 +96,3 @@ function faceDone() { 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/kiri/mode/wedm/driver.js b/src/kiri/mode/wedm/driver.js new file mode 100644 index 00000000..3342abfd --- /dev/null +++ b/src/kiri/mode/wedm/driver.js @@ -0,0 +1,21 @@ +/** 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/src/kiri/mode/wjet/client.js b/src/kiri/mode/wjet/client.js new file mode 100644 index 00000000..0b17b4e1 --- /dev/null +++ b/src/kiri/mode/wjet/client.js @@ -0,0 +1,5 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +export function init() { + // TODO +} diff --git a/src/kiri/mode/wjet/driver.js b/src/kiri/mode/wjet/driver.js new file mode 100644 index 00000000..b11c54fd --- /dev/null +++ b/src/kiri/mode/wjet/driver.js @@ -0,0 +1,13 @@ +/** 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/src/kiri-run/cli.js b/src/kiri/run/cli.js similarity index 100% rename from src/kiri-run/cli.js rename to src/kiri/run/cli.js diff --git a/src/kiri-run/engine.js b/src/kiri/run/engine.js similarity index 65% rename from src/kiri-run/engine.js rename to src/kiri/run/engine.js index d0350a17..abd69b2a 100644 --- a/src/kiri-run/engine.js +++ b/src/kiri/run/engine.js @@ -1,38 +1,41 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"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) => { - -const { kiri } = root; +import '../../add/array.js'; +import '../../add/class.js'; +import '../../add/three.js'; +import { api } from '../core/api.js'; +import { conf } from '../core/conf.js'; +import { client } from '../core/client.js'; +import { load } from '../../load/file.js'; +import { newWidget } from '../core/widget.js'; class Engine { - constructor() { - this.widget = kiri.newWidget(); + constructor({ workURL, poolURL } = {}) { + this.widget = newWidget(); this.settings = { mode: "FDM", controller: {}, render: false, filter: { FDM: "internal" }, - device: kiri.conf.defaults.fdm.d, // device profile - process: kiri.conf.defaults.fdm.p, // slicing settings - widget: { [ this.widget.id ]: {} } + device: conf.defaults.fdm.d, // device profile + process: conf.defaults.fdm.p, // slicing settings + widget: { [this.widget.id]: {} } }; - this.listener = () => {}; + this.listener = () => { }; + try { + client.setWorkPath(workURL); + client.setPoolPath(poolURL); + client.restart(); + } catch (error) { + console.log({ error }); + } } load(url) { return new Promise((accept, reject) => { try { new load.STL().load(url, vertices => { - this.listener({loaded: url, vertices}); + this.listener({ loaded: url, vertices }); this.widget.loadVertices(vertices).center(); accept(this); }); @@ -43,14 +46,14 @@ class Engine { } clear() { - kiri.api.platform.clear(); + api.platform.clear(); } parse(data) { return new Promise((accept, reject) => { try { let vertices = new load.STL().parse(data); - this.listener({parsed: data, vertices}); + this.listener({ parsed: data, vertices }); this.widget.loadVertices(vertices).center(); accept(this); } catch (error) { @@ -59,6 +62,14 @@ class Engine { }); } + setThreading(bool) { + if (bool) { + client.pool.start(); + } else { + client.pool.stop(); + } + } + setListener(listener) { this.listener = listener; return this; @@ -93,9 +104,9 @@ class Engine { let ctrl = this.settings.controller; Object.assign(ctrl, controller); if (ctrl.threaded) { - kiri.client.pool.start(); + client.pool.start(); } else { - kiri.client.pool.stop(); + client.pool.stop(); } return this; } @@ -142,11 +153,11 @@ class Engine { slice() { return new Promise((accept, reject) => { - kiri.client.clear(); - kiri.client.sync([ this.widget ]); - kiri.client.rotate(this.settings); - kiri.client.slice(this.settings, this.widget, msg => { - this.listener({slice:msg}); + client.clear(); + client.sync([this.widget]); + client.rotate(this.settings); + client.slice(this.settings, this.widget, msg => { + this.listener({ slice: msg }); if (msg.error) { reject(msg.error); } @@ -159,10 +170,10 @@ class Engine { prepare() { return new Promise((accept, reject) => { - kiri.client.prepare(this.settings, update => { - this.listener({prepare:{update}}); + client.prepare(this.settings, update => { + this.listener({ prepare: { update } }); }, done => { - this.listener({prepare:{done:true}}); + this.listener({ prepare: { done: true } }); accept(this); }); }); @@ -171,25 +182,21 @@ class Engine { export() { return new Promise((accept, reject) => { let output = []; - kiri.client.export(this.settings, segment => { + client.export(this.settings, segment => { if (typeof segment === 'string') { - this.listener({export:{segment}}); + this.listener({ export: { segment } }); output.push(segment); } }, done => { - this.listener({export:{done}}); + this.listener({ export: { done } }); accept(output.join('\r\n')); }); }); } } -function newEngine() { - return new Engine(); +export function newEngine() { + return new Engine(...arguments); } -gapp.overlay(kiri, { - newEngine -}); - -}); +export { Engine }; diff --git a/src/kiri-run/frame.js b/src/kiri/run/frame.js similarity index 99% rename from src/kiri-run/frame.js rename to src/kiri/run/frame.js index 02d06e62..9830b436 100644 --- a/src/kiri-run/frame.js +++ b/src/kiri/run/frame.js @@ -1,7 +1,5 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; - (function() { let KIRI = self.kiri = self.kiri || {}; let frame, cwin, onevent = {}, targetOrigin; diff --git a/src/kiri/run/minion.js b/src/kiri/run/minion.js new file mode 100644 index 00000000..ae1c2920 --- /dev/null +++ b/src/kiri/run/minion.js @@ -0,0 +1,321 @@ +/** 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'; + +import { base } from '../../geo/base.js'; +import { codec, encode, encodePointArray } from '../core/codec.js'; +import { doTopShells } from '../mode/fdm/post.js'; +import { newPoint } from '../../geo/point.js'; +import { polygons as POLY } from '../../geo/polygons.js'; +import { sliceZ, sliceConnect } from '../../geo/slicer.js'; +import { Slicer as cam_slicer } from '../mode/cam/slicer.js'; +import { Slicer as topo_slicer } from '../mode/cam/slicer_topo.js'; +import { Probe, Trace, raster_slice } from '../mode/cam/topo3.js'; +import { Topo as Topo4, rotatePoints } from '../mode/cam/topo4.js'; +import { wasm_ctrl } from '../../geo/wasm.js'; + +const clib = self.ClipperLib; +const ctyp = clib.ClipType; +const ptyp = clib.PolyType; +const cfil = clib.PolyFillType; + +let cache = self.cache = {}; +let name = "unknown"; + +// catch clipper alerts and convert to console messages +self.alert = function(o) { + console.log(o); +}; + +self.onmessage = function(msg) { + let { data } = msg; + let { cmd } = data; + debug('MINION.onmessage', { cmd, data }); + try { + (funcs[cmd] || funcs.invalid)(data, data.seq, cmd); + } catch (error) { + log('MINION.dispatch.error', error); + } +}; + +function reply(msg, direct) { + self.postMessage(msg, direct); +} + +function log() { + console.log(`[${name}]`, ...arguments); +} + +function debug() { + // console.log(`[${name}]`, ...arguments); +} + +let invalid = 0; +const funcs = self.minion = { + invalid(data, seq, cmd) { + if (invalid++ < 20) { + // prevent flooding console and killing session + console.error({ invalid_minion_command: cmd, data }); + } + reply({ seq, error: `invalid command (${cmd})` }); + }, + + label(data, seq) { + name = data.name; + self.kiri_minion = { name, cache, log }; + }, + + config(data) { + if (data.base) { + Object.assign(base.config, data.base); + } else { + log({invalid: data}); + } + }, + + union(data, seq) { + if (!(data.polys && data.polys.length)) { + reply({ seq, union: codec.encode([]) }); + return; + } + let state = { zeros: [] }; + let polys = codec.decode(data.polys); + let union = POLY.union(polys, data.minarea || 0, true); + reply({ seq, union: codec.encode(union) }, state.zeros); + }, + + topShells(data, seq) { + let top = codec.decode(data.top, {full: true}); + let {z, count, offset1, offsetN, fillOffset, opt} = data; + doTopShells(z, top, count, offset1, offsetN, fillOffset, opt); + let state = { zeros: [] }; + reply({ seq, top: codec.encode(top, {full: true}) }, state.zeros); + }, + + fill(data, seq) { + let polys = codec.decode(data.polys); + let { angle, spacing, minLen, maxLen } = data; + let fill = POLY.fillArea(polys, angle, spacing, [], minLen, maxLen); + let arr = new Float32Array(fill.length * 4); + for (let i=0, p=0; p { + return codec.decodePointArray2D(array, data.z, (X, Y) => { return {X: X*M, Y: Y*M} }) + }); + let polys = data.polys.map(array => { + return codec.decodePointArray2D(array, data.z, (X, Y) => { return {X: X*M, Y: Y*M} }) + }); + + clip.AddPaths(lines, ptyp.ptSubject, false); + clip.AddPaths(polys, ptyp.ptClip, true); + + const state = { zeros: [] }; + if (clip.Execute(ctyp.ctIntersection, ctre, cfil.pftNonZero, cfil.pftEvenOdd)) { + for (let node of ctre.m_AllPolys) { + clips.push(codec.encode(POLY.fromClipperNode(node, data.z), state.zeros)); + } + } + + reply({ seq, clips }, state.zeros); + }, + + sliceZ(data, seq) { + debug('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); + } + let state = { zero: [] }; + let output = []; + sliceZ(z, realp, { + ...options, + each(out) { output.push(out) } + }).then(() => { + for (let rec of output) { + // lines do not pass codec properly (for now) + delete rec.lines; + } + reply({ seq, output: codec.encode(output) }, state.zeros); + }); + }, + + putCache(msg) { + const { key, data } = msg; + // log({ minion_putCache: key, data }); + if (data) { + cache[key] = data; + } else { + delete cache[key]; + } + }, + + clearCache(msg) { + for (let key in cache) { + delete cache[key]; + } + }, + + wasm(data) { + if (data.enable) { + wasm_ctrl.enable(); + } else { + wasm_ctrl.disable(); + } + }, + + // CAM slice support + + cam_slice_init() { + cache.slicer = new cam_slicer(); + }, + + cam_slice_cleanup() { + delete cache.slicer; + }, + + cam_slice(data, seq) { + const { bucket, opt } = data; + cache.slicer.sliceBucket(bucket, opt, slice => { + // log({ slice }); + }).then(data => { + data.forEach(rec => { + rec.polys = encode(rec.polys); + if (rec.lines) { + const points = rec.lines.map(l => [l.p1, l.p2]).flat(); + rec.lines = encodePointArray(points); + } + }); + reply({ seq, slices: data }); + }); + }, + + // CAM Topo3 support + + topo_raster(data, seq) { + const { id, slice, params } = data; + const { resolution } = params; + const vertices = cache[id]; + const box = new THREE.Box2(); + new topo_slicer(slice.index) + .setFromArray(vertices, slice) + .slice(resolution) + .forEach(rec => { + const { z, index, lines } = rec; + + for (let line of lines) { + const { p1, p2 } = line; + if (!p1.swapped) { p1.swapXZ(); p1.swapped = true } + if (!p2.swapped) { p2.swapXZ(); p2.swapped = true } + } + + raster_slice({ + ...params, + box, + lines, + gridx: index + }); + }); + // only pass back bounds of rasters to be merged + reply({ seq, box }); + }, + + trace_init(data) { + data.cross.clipTo = codec.decode(data.cross.clipTo); + data.cross.clipTab = codec.decode(data.cross.clipTab); + const probe = new Probe(data.probe); + const trace = new Trace(probe, data.trace); + cache.trace = { + probe, + trace, + cross: data.cross + }; + trace.init(data.cross); + }, + + trace_y(data, seq) { + const { trace } = cache.trace; + trace.crossY_sync(data.params, slice => { + slice = codec.encode(slice); + reply({ seq, slice }); + }); + }, + + trace_x(data, seq) { + const { trace } = cache.trace; + trace.crossX_sync(data.params, slice => { + slice = codec.encode(slice); + reply({ seq, slice }); + }); + }, + + trace_cleanup() { + delete cache.trace; + }, + + // CAM Topo4 support + + topo4_slice(data, seq) { + const { slice, resolution } = data; + const vertices = cache.vertices; + const recs = new topo_slicer(slice.index) + .setFromArray(vertices, slice) + .slice(resolution) + .map(rec => { + const { z, index, lines } = rec; + + for (let line of lines) { + const { p1, p2 } = line; + if (!p1.swapped) { p1.swapXZ(); p1.swapped = true } + if (!p2.swapped) { p2.swapXZ(); p2.swapped = true } + } + + const points = codec.encodePointArray(lines.map(l => [ l.p1, l.p2 ]).flat()); + const shared = new Float32Array(new SharedArrayBuffer(points.length * 4)); + shared.set(points); + + return { + z, index, shared, + polys: codec.encode(sliceConnect(lines)), + }; + }); + // only pass back bounds of rasters to be merged + reply({ seq, recs }); + }, + + topo4_lathe(data, seq) { + const { angle } = data; + const { slices, tool } = cache.lathe; + + const axis = new THREE.Vector3(1, 0, 0); + const mrot = new THREE.Matrix4().makeRotationAxis(axis, -angle); + const stmp = slices.map(s => { + const lines = s.lines.slice(); + rotatePoints(lines, mrot); + return { z: s.z, lines } + }); + + const topo4 = Object.assign(new Topo4(), cache.lathe); + const heights = topo4.lathePath(stmp, tool); + + reply({ seq, heights }); + } +}; diff --git a/src/kiri-run/worker.js b/src/kiri/run/worker.js similarity index 83% rename from src/kiri-run/worker.js rename to src/kiri/run/worker.js index bcb463e4..97f22e17 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'; -// 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 { codec } from '../core/codec.js'; +import { consts } from '../core/consts.js'; +import { util } from '../../geo/base.js'; +import { newPoint } from '../../geo/point.js'; +import { polygons as POLY } from '../../geo/polygons.js'; +import { newPrint } from '../core/print.js'; +import { render } from '../core/render.js'; +import { wasm_ctrl } from '../../geo/wasm.js'; +import { version } from '../../moto/license.js'; +import { Widget, newWidget } from '../core/widget.js'; +import { load } from "../../load/png.js"; +import { JSZip } from '../../ext/jszip-esm.js'; + +import { CAM } from '../mode/cam/driver-be.js'; +import { DRAG } from '../mode/drag/driver.js'; +import { FDM } from '../mode/fdm/driver-be.js'; +import { LASER } from '../mode/laser/driver.js'; +import { SLA } from '../mode/sla/driver.js'; +import { WEDM } from '../mode/wedm/driver.js'; +import { WJET } from '../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 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,7 +52,10 @@ let debug = self.debug === true, minifns = {}, miniseq = 0; -kiri.version = gapp.version; +function debug() { + if (self.debug === true) + console.log(...arguments); +} // catch clipper alerts and convert to console messages self.alert = function(o) { @@ -76,8 +76,7 @@ function minhandler(msg) { } // for concurrent operations -const minwork = -kiri.minions = { +const minwork = { get concurrent() { return concurrent }, @@ -86,18 +85,25 @@ kiri.minions = { return minions.length; }, - start() { + start(poolpath) { if (minions.length || !concurrent) { 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(poolpath || consts.PATHS.pool, { type: 'module' }); + minion.onerror = (error) => { + debug({ MINION_ERROR: error }); + error.preventDefault(); + }; + minion.onmessageerror = (error) => { + debug({ 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}`); }, stop() { @@ -150,7 +156,7 @@ kiri.minions = { let arr = data.fill; let fill = []; for (let i=0; i { + 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 +442,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 +467,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 +490,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,31 +498,15 @@ 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"){ - let FExt = data.settings.device.gcodeFExt.toUpperCase(); - if(FExt === "DXF"){ - let dxf = driver.exportDXF(data.settings, current.print.output); - send.data({line:dxf}); - }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 - let gcode = driver.exportGCode(data.settings, current.print.output); - send.data({line:gcode}); - } - } - const { bounds, time, @@ -542,7 +539,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 +553,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 +563,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 +580,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 +691,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 +740,17 @@ 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 +}; -}); - -// load kiri modules -// kiri.load_exec(dispatch); +// initilize driver mode handlers +CAM.init(worker); +FDM.init(worker); +LASER.init(worker); +SLA.init(worker); diff --git a/src/kiri/ui.js b/src/kiri/ui.js deleted file mode 100644 index bb18c9e6..00000000 --- a/src/kiri/ui.js +++ /dev/null @@ -1,857 +0,0 @@ -/** 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/src/load/3mf.js b/src/load/3mf.js index c1d59bc3..bf392988 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 { JSZip } from '../ext/jszip-esm.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 new JSZip().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..88962c27 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,6 @@ function load_file(file, opt) { }); } -})(); +Object.assign(load_file, { SVG, OBJ, STL, TMF, GBR, PNG: pngLoad.PNG }); + +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..859f0344 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 '../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..26983b84 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) } @@ -29,7 +28,7 @@ function parseAsync(bin, opt) { * opt.base = base added thickness in mm */ function parse(bin, opt = {}) { - let img = new png.PNG(); + let img = new PNG(); let progress = opt.progress || noop; let ondone = opt.done || noop; img.parse(bin, (err, output) => { @@ -255,4 +254,4 @@ function parse(bin, opt = {}) { }); } -}); +function noop() {} diff --git a/src/load/stl.js b/src/load/stl.js index 866fd86a..d5db160b 100644 --- a/src/load/stl.js +++ b/src/load/stl.js @@ -1,268 +1,248 @@ /** 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; +export class STL { + constructor() { + this.vertices = null; + this.normals = null; + this.colors = null; } - function writeInt32(val) { - writer.setUint32(pos, val, true); - pos += 4; - } + load(url, callback, formdata, scale, credentials, headers) { + const stl = this; - function writeFloat(val) { - writer.setFloat32(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 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); - 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; + 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; + if (hasColors) { + colors[offset] = r; + colors[offset + 1] = g; + colors[offset + 2] = b; + } + offset += 3; } - 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 < vertices.length; i++) vToFloat32[i] = vertices[i]; + for (i = 0; i < normals.length; i++) nToFloat32[i] = normals[i]; + + this.vertices = vToFloat32; + this.normals = nToFloat32; + + return vToFloat32; + } + + convertToString(buf) { + if (typeof buf !== "string") { + let array_buffer = new Uint8Array(buf); + let str = ''; + for (let i = 0; i < buf.byteLength; i++) { + str += String.fromCharCode(array_buffer[i]); + } + return str; + } else { + return buf; } } - 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); + convertToBinary(buf) { + if (typeof buf === "string") { + let array_buffer = new Uint8Array(buf.length); + for (let i = 0; i < buf.length; i++) { + array_buffer[i] = buf.charCodeAt(i) & 0xff; + } + return array_buffer.buffer || array_buffer; + } else { + return buf; } } - - 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 { newPoint } from '../geo/point.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 +42,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 => newPoint(p.x, -p.y, 0))); if (poly.appearsClosed()) poly.points.pop(); if (type === 'polyline') poly.setOpen(true); poly._svg = { width, miter }; @@ -61,9 +55,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 +77,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/gapp.js b/src/main/gapp.js deleted file mode 100644 index 8ac1f19a..00000000 --- a/src/main/gapp.js +++ /dev/null @@ -1,151 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -"use strict"; - -/** node compatibility **/ - -if (typeof(self) === 'undefined') { - this.self = this; -} - -(function () { - -/** satisfy earcut, tween, etc **/ - -if (!self.module) self.module = { exports: {} }; - -/** debug and logging **/ - -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 -}; - -/** empty function. countless possible uses **/ - -self.noop = () => {}; - -/** grid app container **/ - -let gapp = self.gapp = self.gapp || {}; - -// modules to load -let mods = []; - -gapp.overlay = Object.assign; - -// extract function arguments grouped by type -function exargs(args) { - return { - funcs: args.filter(a => typeof a === 'function'), - arrays: args.filter(a => Array.isArray(a)), - strings: args.filter(a => typeof a === 'string'), - objects: args.filter(a => typeof a === 'object' && !Array.isArray(a)) - }; -} - -// prevent module load from terminating main/init chain -function safeFN(fn, name) { - return function() { - try { - return fn(...arguments); - } catch (error) { - if (error.stack) { - console.log(`[${name}]`, error.stack); - } else { - console.log({ register_fail: name, error }); - } - } - }; -} - -// register module without a load function -gapp.register = function() { - const args = exargs([...arguments]); - const objs = args.objects || {}; - const name = objs.name || objs.module || args.strings[0]; - const fn = objs.exec || args.funcs[0]; - const mod = { fn, name }; - mods.push(mod); -}; - -// perform dependency checks and run module load functions -gapp.main = function() { - const args = exargs([...arguments]); - const objs = args.objects[0] || {}; - const app = objs.app || args.strings[0]; - const post = objs.post || args.funcs[0]; - const pre = objs.pre || args.funcs[1]; - const root = self; - // optional fn to run before loading - if (pre) { - pre(root); - } - // load mods after checking if dependency is present - for (let mod of mods) { - let { fn, name } = mod; - // mods with no name can't be depended on, but are allowed - if (name) { - dbug.debug(`${app} | load | ${name}`); - } - // modules without functions allowed so they can be depended on - if (!fn) { - dbug.debug(`${app} | ${mod.name} | missing fn()`); - continue; - } - // create namespace path in root and execute module load function - let toks = name.split('.'); - let map = toks.pop(); - let path = root; - for (let tok of toks) { - if (!path[tok]) { - dbug.debug({creating_root: tok, for: name}); - path = path[tok] = {}; - } else { - path = path[tok]; - } - } - let tmp = path[map] || {}; - safeFN(fn, name)(root, exports => { - // map exports into module namespace when called - if (exports) { - return path[map] = Object.assign(tmp, exports); - } - }); - } - // force runtime error if gapp.load() called gapp.main() - mods = null; - // optional if not called inline with startup - if (post) { - post(root); - } -}; - -})(); diff --git a/src/main/kiri.js b/src/main/kiri.js index 69e5ba14..e21a7734 100644 --- a/src/main/kiri.js +++ b/src/main/kiri.js @@ -1,41 +1,49 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; +import '../add/array.js'; +import '../add/class.js'; +import '../add/three.js'; +import '../kiri/core/lang.js'; +import '../kiri/core/lang-en.js'; -gapp.main({ - app: "kiri", +import { run } from '../kiri/core/init.js'; - pre(root) { - let mods = root.kirimod = ( root.kirimod || [] ); +let 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); - } - }; - }, - - post(root) { - // complete module loading - root.kiri.load_exec(); +function safeExec(fn) { + try { + fn(kiri.api); + } catch (error) { + console.log('load error', fn, error); } -}); +} + +function checkReady() { + if (document.readyState === 'complete') { + kiri.api = run(); + self.$ = kiri.api.web.$; + for (let fn of load) { + safeExec(fn); + } + load = undefined; + kiri.api.event.emit('load-done', stats); + } +} + +self.kiri = { + load(fn) { + // console.log('KIRI LOAD', [...arguments]); + if (load) { + load.push(fn); + } else { + safeExec(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..aaae2bee 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,32 @@ 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 + let wlp = window.location.pathname; + let mio = wlp.indexOf('/mesh/'); + if (mio >= 0) { + history.replaceState({}, '', wlp.substring(0,mio + 6)); + } + + // for electron + self.mesh = { api }; } // 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 +135,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 +186,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 +199,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 +238,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 +302,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 +384,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 +400,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 +412,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 +425,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 +451,7 @@ function space_init(data) { opt); break; case modes.edge: - mesh.edges.select(); + meshEdges.select(); break; } } @@ -450,6 +470,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 +488,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 +504,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 +514,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 +522,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 +531,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 +609,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 +636,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 +664,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 +690,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 +699,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 +719,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 +784,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/meta.js b/src/main/meta.js deleted file mode 100644 index d381ef70..00000000 --- a/src/main/meta.js +++ /dev/null @@ -1,16 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -"use strict"; - -(function () { - - let meta = self.meta = self.meta || {}, - DOC = document; - - DOC.onreadystatechange = function() { - if (DOC.readyState === 'complete') { - // console.log('init'); - } - } - -})(); 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/src/mesh/handles.js b/src/mesh/handles.js new file mode 100644 index 00000000..e9e9ee23 --- /dev/null +++ b/src/mesh/handles.js @@ -0,0 +1,315 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { THREE } from '../ext/three.js'; +import { broker } from '../moto/broker.js'; +import { space as motoSpace } from '../moto/space.js'; +import { api as meshApi } from './api.js'; +import { util as meshUtil } from './util.js'; + +const { + BoxGeometry, + EdgesGeometry, + LineSegments, + MeshBasicMaterial, + LineBasicMaterial, + Group, + Vector3, + Quaternion, + Mesh +} = THREE; + +class TransformTool { + constructor() { + this.group = new Group(); + this.handles = []; + this.lines = []; // Add array to track connecting lines + this.mode = '2d'; // or '3d' + this.bounds = null; + this.handleSize = 2; + this.handleColor = 0x0088ff; + this.handleOpacity = 0.5; + this.enabled = true; // persisted state + this.visible = false; // selection state + + // Create materials + this.handleMaterial = new MeshBasicMaterial({ + color: this.handleColor, + transparent: true, + opacity: this.handleOpacity + }); + + this.edgeMaterial = new LineBasicMaterial({ + color: this.handleColor, + transparent: true, + opacity: 0.8 + }); + + // Add to scene + motoSpace.world.add(this.group); + this.group.visible = false; + + // Listen for selection changes + broker.listeners({ + selection_update: () => this.update(), + sketch_selections: () => this.update(), + selection_drag: () => this.update(), + selection_move: () => this.update(), + selection_scale: () => this.update(), + selection_rotate: () => this.update(), + selection_qrotate: () => this.update(), + sketch_render: () => this.update(), + history_undo: () => this.update(), + history_redo: () => this.update(), + move: () => this.update(), + rotate: () => this.update(), + qrotate: () => this.update(), + scale: () => this.update(), + }); + } + + setEnabled(enabled) { + this.enabled = enabled; + if (meshApi && meshApi.prefs) { + meshApi.prefs.map.space.bounds = enabled; + meshApi.prefs.save(); + } + this.updateVisibility(); + } + + setVisible(visible) { + this.visible = visible; + this.updateVisibility(); + } + + show() { + this.setVisible(true); + } + + hide() { + this.setVisible(false); + } + + toggleBounds() { + this.setEnabled(!this.enabled); + } + + updateVisibility() { + this.group.visible = this.enabled && this.visible; + } + + update() { + // Get current selection + const selection = meshApi.selection; + const sketch = selection.sketch(); + const models = selection.models(); + + // Hide if no selection + if (!sketch && !models.length) { + this.hide(); + return; + } + + // Show transform tool + this.show(); + + // Get bounds based on selection type + let bounds; + if (sketch) { + // For sketches, use 2D mode + this.mode = '2d'; + bounds = sketch.bounds; + } else { + // For models/groups, use 3D mode + this.mode = '3d'; + const objects = models; + bounds = meshUtil.bounds(objects); + } + + this.setBounds(bounds); + } + + setBounds(bounds, mode = '3d') { + this.mode = mode; + this.bounds = bounds; + this.updateHandles(); + } + + updateHandles() { + // Clear existing handles and lines + this.handles.forEach(handle => this.group.remove(handle)); + this.lines.forEach(line => this.group.remove(line)); + this.handles = []; + this.lines = []; + + if (!this.bounds) return; + + const { min, max } = this.bounds; + const corners = this.getCorners(min, max); + + // Create handles for each corner + corners.forEach(corner => { + const handle = this.createHandle(); + handle.position.copy(corner); + this.handles.push(handle); + this.group.add(handle); + }); + + // Add connecting lines between handles + if (this.mode === '2d') { + // For 2D mode, connect handles with horizontal and vertical lines + const lineGeometry = new THREE.BufferGeometry(); + const linePositions = []; + + // Horizontal lines + linePositions.push( + corners[0].x, corners[0].y, corners[0].z, // 0 to 1 + corners[1].x, corners[1].y, corners[1].z, + corners[2].x, corners[2].y, corners[2].z, // 2 to 3 + corners[3].x, corners[3].y, corners[3].z + ); + + // Vertical lines + linePositions.push( + corners[0].x, corners[0].y, corners[0].z, // 0 to 3 + corners[3].x, corners[3].y, corners[3].z, + corners[1].x, corners[1].y, corners[1].z, // 1 to 2 + corners[2].x, corners[2].y, corners[2].z + ); + + lineGeometry.setAttribute('position', new THREE.Float32BufferAttribute(linePositions, 3)); + const line = new THREE.LineSegments(lineGeometry, this.edgeMaterial); + this.lines.push(line); + this.group.add(line); + } else { + // For 3D mode, connect handles with axis-aligned lines + const lineGeometry = new THREE.BufferGeometry(); + const linePositions = []; + + // Bottom face (horizontal lines) + for (let i = 0; i < 4; i++) { + linePositions.push( + corners[i].x, corners[i].y, corners[i].z, + corners[(i + 1) % 4].x, corners[(i + 1) % 4].y, corners[(i + 1) % 4].z + ); + } + + // Top face (horizontal lines) + for (let i = 4; i < 8; i++) { + linePositions.push( + corners[i].x, corners[i].y, corners[i].z, + corners[4 + ((i - 3) % 4)].x, corners[4 + ((i - 3) % 4)].y, corners[4 + ((i - 3) % 4)].z + ); + } + + // Vertical edges + for (let i = 0; i < 4; i++) { + linePositions.push( + corners[i].x, corners[i].y, corners[i].z, + corners[i + 4].x, corners[i + 4].y, corners[i + 4].z + ); + } + + lineGeometry.setAttribute('position', new THREE.Float32BufferAttribute(linePositions, 3)); + const line = new THREE.LineSegments(lineGeometry, this.edgeMaterial); + this.lines.push(line); + this.group.add(line); + } + } + + getCorners(min, max) { + if (this.mode === '2d') { + // 2D mode - 4 corners in XY plane at z midline + const zMid = (min.z + max.z) / 2; + return [ + new Vector3(min.x, min.y, zMid), + new Vector3(max.x, min.y, zMid), + new Vector3(max.x, max.y, zMid), + new Vector3(min.x, max.y, zMid) + ]; + } else { + // 3D mode - 8 corners of bounding box + return [ + new Vector3(min.x, min.y, min.z), + new Vector3(max.x, min.y, min.z), + new Vector3(max.x, max.y, min.z), + new Vector3(min.x, max.y, min.z), + new Vector3(min.x, min.y, max.z), + new Vector3(max.x, min.y, max.z), + new Vector3(max.x, max.y, max.z), + new Vector3(min.x, max.y, max.z) + ]; + } + } + + createHandle() { + // Create a small box for the handle + const geometry = new BoxGeometry( + this.handleSize, + this.handleSize, + this.handleSize + ); + + // Create the handle mesh + const handle = new Mesh(geometry, this.handleMaterial); + + // Add wireframe edges + const edges = new EdgesGeometry(geometry); + const line = new LineSegments(edges, this.edgeMaterial); + handle.add(line); + + return handle; + } + + get object() { + return this.group; + } + + activeHandles() { + if (!this.enabled) return []; + // Return an array of objects wrapping each handle + // Each object: { mesh, index, position } + return this.handles.map((mesh, index) => ({ + mesh, + index, + position: mesh.position.clone(), + })); + } + + /** + * Given a handle (from activeHandles) and a drag {delta, offset}, + * compute the new center and size for the bounding box. + * Returns: { center: Vector3, size: Vector3 } + */ + computeDelta(handleObj, { delta, offset }) { + // Get current bounds + if (!this.bounds) return null; + const { min, max } = this.bounds; + const center = min.clone().add(max).multiplyScalar(0.5); + const size = max.clone().sub(min); + + // Which handle is being dragged? + const handleIndex = handleObj.index; + // Get all corners + const corners = this.getCorners(min, max); + // Copy corners so we can mutate + const newCorners = corners.map(c => c.clone()); + // Move the dragged corner by delta + newCorners[handleIndex].add(delta); + + // Compute new min/max from moved corners + let newMin = newCorners[0].clone(); + let newMax = newCorners[0].clone(); + for (let c of newCorners) { + newMin.min(c); + newMax.max(c); + } + const newCenter = newMin.clone().add(newMax).multiplyScalar(0.5); + const newSize = newMax.clone().sub(newMin); + return { center: newCenter, size: newSize }; + } +} + +// Create singleton instance +const handles = new TransformTool(); + +export { handles }; \ No newline at end of file diff --git a/src/mesh/history.js b/src/mesh/history.js new file mode 100644 index 00000000..770201b3 --- /dev/null +++ b/src/mesh/history.js @@ -0,0 +1,144 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { broker } from "../moto/broker.js"; +import { target } from "./sketch.js"; + +class History { + #pos = -1; + #stack = []; + #offset; // store final drag move + #target; // store sketch drag target + #tracking = false; + + constructor() { + broker.subscribe({ + + app_ready() { + this.#tracking = true + }, + + drag_start(selected) {}, + + drag(opt) { + this.#target = target(); + this.#offset = opt.offset; + }, + + drag_end(selected) { + // console.log({ + // drag_end: selected, + // offset: this.#offset, + // target: this.#target + // }); + const { x, y, z } = this.#offset; + broker.publish('move', { + target: this.#target, + set: selected, + dx: x, + dy: y, + dz: z + }); + }, + + move(data) { + const { target, set, dx, dy, dz } = data; + this.push('move', { + set, dx: -dx, dy: -dy, dz: -dz, target + }, { + set, dx, dy, dz, target + }); + }, + + rotate(data) { + const { set, dx, dy, dz } = data; + this.push('rotate', { + set, dx: -dx, dy: -dy, dz: -dz + }, { + set, dx, dy, dz + }); + }, + + scale(data) { + const { set, dx, dy, dz } = data; + this.push('scale', { + set, dx: 1/dx, dy: 1/dy, dz: 1/dz + }, { + set, dx, dy, dz + }); + }, + + }, this); + } + + push(type, undo, redo) { + this.#stack.length = ++this.#pos; + this.#stack.push({ + type, + undo, + redo + }); + } + + // update(redo, undo) { + // let peek = this.#stack.peek(); + // if (peek) { + // Object.assign(peek, { + // redo: redo || peek.redo, + // undo: undo || peek.undo + // }); + // } + // } + + do(type, data) { + // console.log({ type, data }); + switch (type) { + case 'move': { + let { set, dx, dy, dz, target } = data; + for (let grp of set) { + grp.move(dx, dy, dz, target); + } + break; + } + // case 'position': { + // for (let { model, pos } of data) { + // model.position(...pos); + // } + // break; + // } + case 'rotate': { + let { set, dx, dy, dz } = data; + for (let grp of set) { + grp.rotate(dx, dy, dz); + } + break; + } + case 'scale': { + let { set, dx, dy, dz } = data; + for (let grp of set) { + grp.scale(dx, dy, dz); + } + break; + } + default: + console.warn(`invalid record type: ${type}`); + } + } + + undo() { + if (this.#pos >= 0) { + const rec = this.#stack[this.#pos--]; + this.do(rec.type, rec.undo); + broker.publish("history_undo", rec); + } + } + + redo() { + if (this.#stack.length && this.#pos >= -1 && this.#pos < this.#stack.length - 1) { + const rec = this.#stack[++this.#pos]; + this.do(rec.type, rec.redo); + broker.publish("history_redo", rec); + } + } +} + +export const history = new History(); 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..73e0d687 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); } @@ -238,33 +233,37 @@ mesh.tool = class MeshTool { fn++; } } + //faces are the normals in groups of 6 [nx,] + this.indexed = { faces, sides, sideExt }; } +/** + * Retrieves the adjacent faces for a given face index. + * + * @param {number} face - The index of the face for which adjacent faces are to be found. + * @returns {number[]} An array of face indices that are adjacent to the given face. + */ + getAdjacentFaces(face) { const { faces, sides } = this.getIndex(); const foff = face * 6; 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,6 +303,86 @@ mesh.tool = class MeshTool { return faces; } + /** + * Finds all faces in a cylinder surface connected to the given face. + * + * @param {number} face - the face index to start from + * @return {number[]} face indices in the cylinder surface + * + * @throws {string} if the face has only one Z value + * @throws {string} if the face normal is not perpendicular to Z axis + * + * The algorithm works by: + * 1. Finding the two distinct Z values of the given face + * 2. Filtering adjacent faces to those with the same Z values + * 3. Filtering faces with normals perpendicular to Z axis + * 4. Recursively adding adjacent faces with the same Z values and normal perpendicular to Z axis + */ + findCylinderSurface(face){ + face = Number(face) + + const radianTolerance = 8 * (Math.PI/180); + const vertexTolerance = 1e-4; + //helper function + const dissimilar = (a,b,c) => Math.abs(a-b) > vertexTolerance && + Math.abs(b-c) > vertexTolerance && + Math.abs(a-c) > vertexTolerance; + + let found = {}, + out = [face], + {vertices} = this, + {faces} = this.getIndex(), + faceOffset = face*9, + zs = [2,5,8].map(i => vertices[faceOffset+i].round(5)), //get zs from face vertices + lowZ = Math.min(zs), + highZ = Math.max(zs), + checked = {}, + check = [face]; + + //check for errors, and throw if found + if(dissimilar(...zs)){ + throw `face must have only 2 Z values. found: ${zs.join(", ")}` + }else if(Math.abs( faces[face * 6 + 2] > 1e-6 )){ + throw "face's normal must be perpendicular to Z axis" + } + + found[face] = 1; + while (check.length) { + const face = check.shift(); + const froot = face * 6; + const vroot = face * 9 + + // filter faces to those perpendicular to +z axis + if ( Math.abs( faces[froot + 2] > 1e-6 )) continue; + //filter faces with verts don't share the z values of the first face + if( + !(vertices[vroot + 2] !== lowZ || vertices[vroot + 2] !== highZ) && + !(vertices[vroot + 5] !== lowZ || vertices[vroot + 5] !== highZ) && + !(vertices[vroot + 8] !== lowZ || vertices[vroot + 8] !== highZ) + ) continue; + + const fadj = this.getAdjacentFaces(face); + for (let f of fadj) { + if (found[f] || checked[f]) { + continue; + } + const aroot = f * 6; + let sum = 0; + for (let i=0; i<3; i++) { + sum += Math.pow(faces[froot + i] - faces[aroot + i], 2); + } + const fn = Math.sqrt(sum); + if (fn <= radianTolerance) { + out.push(f); + check.push(f) + checked[f] = 1; + found[f] = 1; + } + } + } + return out; + } + // given a list of faces (indices), return an array of closed // polylines that represent the outlines of each discrete island generateOutlines(list) { @@ -380,7 +459,6 @@ mesh.tool = class MeshTool { return outs; } - // depends on index() being run first isolateBodies() { const verts = this.checkVertices(this.vertices); const bodies = []; @@ -403,12 +481,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 +809,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 +817,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 +824,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 +832,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 +846,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 +923,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 +975,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()); +let 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..08ca8734 100644 --- a/src/moto/broker.js +++ b/src/moto/broker.js @@ -1,15 +1,13 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -"use strict"; - -gapp.register("moto.broker", [], (root, exports) => { - class Broker { + #debug = false; + #topics = {}; + #used = {}; + #send = {}; + #uuid = ((Math.random() * 0xffffffff) | 0).toString(36); 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 +15,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 @@ -35,6 +37,9 @@ class Broker { // creates topic if new topic // creates a send function if new topic subscribe(topic, listener) { + if (this.#debug) { + console.log({ subscribe: topic, listener }); + } if (Array.isArray(topic)) { for (let t of topic) { this.subscribe(t, listener); @@ -42,16 +47,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 +69,7 @@ class Broker { } unsubscribe(topic, listener) { - let channel = this.topics[topic]; + let channel = this.#topics[topic]; if (!channel) { return; } @@ -71,7 +79,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 +94,13 @@ class Broker { if (topic !== ".topic.publish") { this.publish(".topic.publish", {topic, message, options}); } + if (this.#debug && !topic.startsWith(".")) { + console.log({ publish: 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 +147,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..8aa5047d 100644 --- a/src/moto/space.js +++ b/src/moto/space.js @@ -1,137 +1,142 @@ /** 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, + FrontSide, +} = 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: FrontSide, + depthWrite: false, + }), + 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 +151,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..ceb3255b 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,22 @@ gapp.overlay(root, { } }); -}); +export { + $, + $d, + $h, + $c, + $C, + h, + estop +} + +export default { + $, + $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/web/kiri/engine.html b/web/kiri/engine.html index 9d8eb8c6..da417539 100644 --- a/web/kiri/engine.html +++ b/web/kiri/engine.html @@ -23,7 +23,6 @@ display: flex; flex-direction: row; } - #loadExample #examples { display:flex; flex-direction: row; @@ -87,8 +86,9 @@ } - - - +

Kiri:Moto Javascript Engine API

diff --git a/web/kiri/frame.html b/web/kiri/frame.html index b89ae43e..a0589e13 100644 --- a/web/kiri/frame.html +++ b/web/kiri/frame.html @@ -82,7 +82,7 @@ } - + - - + + + + + + + + + +
@@ -284,6 +286,7 @@
+
@@ -435,6 +438,7 @@
outline
contour
pocket
+
helical
@@ -551,7 +555,7 @@
-
+
-
+
shaft
-
-
+
+
flute
-
-
+
+
taper
-
+
@@ -624,7 +628,7 @@
-
+
diff --git a/web/kiri/lang/da.js b/web/kiri/lang/da.js index 63112b8c..c6607075 100644 --- a/web/kiri/lang/da.js +++ b/web/kiri/lang/da.js @@ -1,7 +1,7 @@ // Danish localization for Kiri:Moto // Will defer to English map for any missing key/value pairs -kiri.lang['da'] = -kiri.lang['da-dk'] = { +self.lang['da'] = +self.lang['da-dk'] = { version: "version", enable: "enable", settings: "indstillinger", diff --git a/web/kiri/lang/de-de.js b/web/kiri/lang/de-de.js index ce527cf9..82880d59 100644 --- a/web/kiri/lang/de-de.js +++ b/web/kiri/lang/de-de.js @@ -1,7 +1,7 @@ // German localization for Kiri:Moto // Will defer to English map for any missing key/value pairs -kiri.lang['de'] = -kiri.lang['de-de'] = { +self.lang['de'] = +self.lang['de-de'] = { // common keys and menus animate: "Animieren", // CAM animate button arrange: "Anordnen", // layout workspace objects diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index a8bc366e..bc713531 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -1,7 +1,7 @@ // English language map. Other localization maps will // defer to English map for any missing key/value pairs -self.kiri.lang['en'] = -self.kiri.lang['en-us'] = { +self.lang['en'] = +self.lang['en-us'] = { // common keys and menus animate: "animate", // CAM animate button arrange: "arrange", // layout workspace objects @@ -78,6 +78,7 @@ self.kiri.lang['en-us'] = { rc_xstl: "export STL", sb_info: ["print speed","in mm/s"], rc_merg: "merge object meshes", + rc_splt: "isolate bodies", // DEVICE MENU and related dialogs dm_sldt: "select a device type", @@ -612,6 +613,25 @@ self.kiri.lang['en-us'] = { cd_rego_s: "part offset", cd_rego_l: ["offset of hole center","from part bounding box","in workspace units"], + // CNC HELICAL tolpath + ch_menu: "helical", + ch_sdwn_s: "step down (pitch)", + ch_sdwn_l: ["z-distance","per revolution","in workspace units","(inverse of threads per unit)", "0 to disable"], + ch_fsta_s: "force start angle", + ch_fsta_l: ["override start angle","defaults to closest point to tool"], + ch_stra_s: "start angle", + ch_stra_l: ["start angle of helix","in degrees ccw of +x axis"], + ch_selc_l: ["select a cylinder face", "inner holes that are too small","will be ignored."], + ch_fini_s: "bottom finish pass", + ch_fini_l: ["trace a circle at bottom of helix","used for hole clearing"], + ch_entr_s: "enter from center", + ch_entr_l: ["start cut from center of cylinder","gently arcing in horisontaly", "instead of plunging"], + ch_ento_s: "center entry offset", + ch_ento_l: ["offset from path for curved entry","usually thread depth if threadmilling","in workspace units","0 for a linear entry"], + ch_rvrs_s: "reverse", + ch_rvrs_l: ["reverse helical z-axis direction","low-Z to high-Z when checked","useful for thread milling","not recomended for other operations"], + ch_clkw_s: "clockwise", + ch_clkw_l: ["reverse helical rotation around cylinder","clockwise when checked"], // CNC FLIP cf_menu: "flip", @@ -648,6 +668,10 @@ self.kiri.lang['en-us'] = { 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", @@ -863,6 +887,8 @@ self.kiri.lang['en-us'] = { 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_arce_s: "arc output", + cx_arce_l: ["EXPERIMENTAL: enable path to arc conversion"], 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", diff --git a/web/kiri/lang/es-es.js b/web/kiri/lang/es-es.js index 944cf279..f83941d5 100644 --- a/web/kiri/lang/es-es.js +++ b/web/kiri/lang/es-es.js @@ -1,7 +1,7 @@ // Spanish localization for Kiri:Moto // Will defer to English map for any missing key/value pairs -kiri.lang['es'] = -kiri.lang['es-es'] = { +self.lang['es'] = +self.lang['es-es'] = { // common keys and menus animate: "animar", // CAM animate button arrange: "organizar", // layout workspace objects diff --git a/web/kiri/lang/fr.js b/web/kiri/lang/fr.js index e30df48f..27536843 100644 --- a/web/kiri/lang/fr.js +++ b/web/kiri/lang/fr.js @@ -1,7 +1,7 @@ // French localization for Kiri:Moto // Will defer to English map for any missing key/value pairs -kiri.lang['fr'] = -kiri.lang['fr-fr'] = { +self.lang['fr'] = +self.lang['fr-fr'] = { // common keys and menus animate: "animer", // CAM animate button arrange: "organiser", // layout workspace objects diff --git a/web/kiri/lang/pl-pl.js b/web/kiri/lang/pl-pl.js index 2545e6e7..6dd105a3 100644 --- a/web/kiri/lang/pl-pl.js +++ b/web/kiri/lang/pl-pl.js @@ -1,6 +1,6 @@ // Polish. Język polski tłumaczony na zasadach własności publicznej -kiri.lang['pl'] = -kiri.lang['pl-pl'] = { +self.lang['pl'] = +self.lang['pl-pl'] = { // hasło i tłumaczenie animate: "animacja", // CAM animate button arrange: "rozmieść", // layout workspace objects diff --git a/web/kiri/lang/pt.js b/web/kiri/lang/pt.js index 21e152bf..e5524a12 100644 --- a/web/kiri/lang/pt.js +++ b/web/kiri/lang/pt.js @@ -1,7 +1,7 @@ // Portuguese localization for Kiri:Moto // Will defer to English map for any missing key/value pairs -kiri.lang['pt'] = -kiri.lang['pt-pt'] = { +self.lang['pt'] = +self.lang['pt-pt'] = { // common keys and menus animate: "animar", // CAM animate button arrange: "organizar", // layout workspace objects diff --git a/web/kiri/lang/zh.js b/web/kiri/lang/zh.js index c801d1e4..34e71397 100644 --- a/web/kiri/lang/zh.js +++ b/web/kiri/lang/zh.js @@ -1,6 +1,6 @@ // 英文。其他语言地图将遵循英语内容 // 映射缺少的键/值对 -kiri.lang['zh'] = { +self.lang['zh'] = { // 通用键及菜单 animate: "动画", // CAM动画按钮 arrange: "排布", // 对工作区对象排版 diff --git a/web/kiri/manifest.json b/web/kiri/manifest.json index 661bb6d5..ce4522be 100644 --- a/web/kiri/manifest.json +++ b/web/kiri/manifest.json @@ -1,5 +1,5 @@ { - "name": "Kiri:Moto 4.2.0", + "name": "Kiri:Moto 4.3.0", "short_name": "Kiri:Moto", "description": "Slicer for 3D printers, CNC mills, laser cutters and more", "start_url": "/kiri/", @@ -9,7 +9,7 @@ "background_color": "#ffffff", "orientation": "landscape-primary", "icons": [ - { "src": "/kiri/logo-cube-512.png", "type": "image/png", "sizes": "512x512" }, - { "src": "/kiri/logo-cube-144.png", "type": "image/png", "sizes": "144x144" } + { "src": "./logo-cube-512.png", "type": "image/png", "sizes": "512x512" }, + { "src": "./logo-cube-144.png", "type": "image/png", "sizes": "144x144" } ] } diff --git a/web/mesh/fiddle.html b/web/mesh/fiddle.html deleted file mode 100644 index 713bb88b..00000000 --- a/web/mesh/fiddle.html +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - - - - - - - diff --git a/web/mesh/index.css b/web/mesh/index.css index 298d6bd7..0c3dc2b4 100644 --- a/web/mesh/index.css +++ b/web/mesh/index.css @@ -10,7 +10,7 @@ @font-face { font-family: 'Russo One'; - src: url('/moto/russo-one.ttf'); + src: url('../moto/russo-one.ttf'); } /** defaults */ 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/web/meta/favicon-mobile.png b/web/meta/favicon-mobile.png deleted file mode 100644 index 66119682..00000000 Binary files a/web/meta/favicon-mobile.png and /dev/null differ diff --git a/web/meta/favicon.ico b/web/meta/favicon.ico deleted file mode 100644 index 5b0b30cb..00000000 Binary files a/web/meta/favicon.ico and /dev/null differ diff --git a/web/meta/index.css b/web/meta/index.css deleted file mode 100644 index 1997bec8..00000000 --- a/web/meta/index.css +++ /dev/null @@ -1,16 +0,0 @@ -a, a:hover, a:visited { - border: none; - outline: none; - color: inherit; - text-decoration: none; -} -body { - overflow: hidden; -} -body,div { - margin: 0; - padding: 0; - border: 0; - font-weight: normal; - font-family: sans-serif; -} \ No newline at end of file diff --git a/web/meta/index.html b/web/meta/index.html deleted file mode 100644 index 7654ca08..00000000 --- a/web/meta/index.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - Meta:Moto - - - - - - - - - - diff --git a/web/obj/pocket-test.stl b/web/obj/pocket-test.stl new file mode 100644 index 00000000..e873d7a1 --- /dev/null +++ b/web/obj/pocket-test.stl @@ -0,0 +1,198 @@ +solid + facet normal 0.000000e+00 -1.000000e+00 0.000000e+00 + outer loop + vertex 5.000000e+01 -5.000000e+01 0.000000e+00 + vertex -5.000000e+01 -5.000000e+01 1.800000e+01 + vertex -5.000000e+01 -5.000000e+01 0.000000e+00 + endloop + endfacet + facet normal 0.000000e+00 -1.000000e+00 0.000000e+00 + outer loop + vertex 5.000000e+01 -5.000000e+01 0.000000e+00 + vertex 5.000000e+01 -5.000000e+01 1.800000e+01 + vertex -5.000000e+01 -5.000000e+01 1.800000e+01 + endloop + endfacet + facet normal -1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex -5.000000e+01 -5.000000e+01 0.000000e+00 + vertex -5.000000e+01 5.000000e+01 1.800000e+01 + vertex -5.000000e+01 5.000000e+01 0.000000e+00 + endloop + endfacet + facet normal -1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex -5.000000e+01 -5.000000e+01 0.000000e+00 + vertex -5.000000e+01 -5.000000e+01 1.800000e+01 + vertex -5.000000e+01 5.000000e+01 1.800000e+01 + endloop + endfacet + facet normal 1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex 5.000000e+01 5.000000e+01 0.000000e+00 + vertex 5.000000e+01 -5.000000e+01 1.800000e+01 + vertex 5.000000e+01 -5.000000e+01 0.000000e+00 + endloop + endfacet + facet normal 1.000000e+00 0.000000e+00 -0.000000e+00 + outer loop + vertex 5.000000e+01 5.000000e+01 0.000000e+00 + vertex 5.000000e+01 5.000000e+01 1.800000e+01 + vertex 5.000000e+01 -5.000000e+01 1.800000e+01 + endloop + endfacet + facet normal 0.000000e+00 -0.000000e+00 -1.000000e+00 + outer loop + vertex 5.000000e+01 5.000000e+01 0.000000e+00 + vertex 5.000000e+01 -5.000000e+01 0.000000e+00 + vertex -5.000000e+01 -5.000000e+01 0.000000e+00 + endloop + endfacet + facet normal -0.000000e+00 0.000000e+00 -1.000000e+00 + outer loop + vertex 5.000000e+01 5.000000e+01 0.000000e+00 + vertex -5.000000e+01 -5.000000e+01 0.000000e+00 + vertex -5.000000e+01 5.000000e+01 0.000000e+00 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex 5.000000e+01 -5.000000e+01 1.800000e+01 + vertex 2.000000e+01 -2.000000e+01 1.800000e+01 + vertex -2.000000e+01 -2.000000e+01 1.800000e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex 5.000000e+01 -5.000000e+01 1.800000e+01 + vertex -2.000000e+01 -2.000000e+01 1.800000e+01 + vertex -5.000000e+01 -5.000000e+01 1.800000e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex -2.000000e+01 2.000000e+01 1.800000e+01 + vertex -5.000000e+01 5.000000e+01 1.800000e+01 + vertex -5.000000e+01 -5.000000e+01 1.800000e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex -2.000000e+01 2.000000e+01 1.800000e+01 + vertex -5.000000e+01 -5.000000e+01 1.800000e+01 + vertex -2.000000e+01 -2.000000e+01 1.800000e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex 5.000000e+01 5.000000e+01 1.800000e+01 + vertex 2.000000e+01 2.000000e+01 1.800000e+01 + vertex 2.000000e+01 -2.000000e+01 1.800000e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex 5.000000e+01 5.000000e+01 1.800000e+01 + vertex -2.000000e+01 2.000000e+01 1.800000e+01 + vertex 2.000000e+01 2.000000e+01 1.800000e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex 5.000000e+01 5.000000e+01 1.800000e+01 + vertex -5.000000e+01 5.000000e+01 1.800000e+01 + vertex -2.000000e+01 2.000000e+01 1.800000e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex 5.000000e+01 5.000000e+01 1.800000e+01 + vertex 2.000000e+01 -2.000000e+01 1.800000e+01 + vertex 5.000000e+01 -5.000000e+01 1.800000e+01 + endloop + endfacet + facet normal 0.000000e+00 1.000000e+00 0.000000e+00 + outer loop + vertex -5.000000e+01 5.000000e+01 0.000000e+00 + vertex 5.000000e+01 5.000000e+01 1.800000e+01 + vertex 5.000000e+01 5.000000e+01 0.000000e+00 + endloop + endfacet + facet normal 0.000000e+00 1.000000e+00 0.000000e+00 + outer loop + vertex -5.000000e+01 5.000000e+01 0.000000e+00 + vertex -5.000000e+01 5.000000e+01 1.800000e+01 + vertex 5.000000e+01 5.000000e+01 1.800000e+01 + endloop + endfacet + facet normal -1.000000e+00 -0.000000e+00 0.000000e+00 + outer loop + vertex 2.000000e+01 2.000000e+01 1.800000e+01 + vertex 2.000000e+01 -2.000000e+01 1.000000e+01 + vertex 2.000000e+01 -2.000000e+01 1.800000e+01 + endloop + endfacet + facet normal -1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex 2.000000e+01 2.000000e+01 1.000000e+01 + vertex 2.000000e+01 -2.000000e+01 1.000000e+01 + vertex 2.000000e+01 2.000000e+01 1.800000e+01 + endloop + endfacet + facet normal 0.000000e+00 -1.000000e+00 0.000000e+00 + outer loop + vertex -2.000000e+01 2.000000e+01 1.800000e+01 + vertex 2.000000e+01 2.000000e+01 1.000000e+01 + vertex 2.000000e+01 2.000000e+01 1.800000e+01 + endloop + endfacet + facet normal 0.000000e+00 -1.000000e+00 0.000000e+00 + outer loop + vertex -2.000000e+01 2.000000e+01 1.000000e+01 + vertex 2.000000e+01 2.000000e+01 1.000000e+01 + vertex -2.000000e+01 2.000000e+01 1.800000e+01 + endloop + endfacet + facet normal 1.000000e+00 -0.000000e+00 0.000000e+00 + outer loop + vertex -2.000000e+01 -2.000000e+01 1.800000e+01 + vertex -2.000000e+01 2.000000e+01 1.000000e+01 + vertex -2.000000e+01 2.000000e+01 1.800000e+01 + endloop + endfacet + facet normal 1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex -2.000000e+01 -2.000000e+01 1.000000e+01 + vertex -2.000000e+01 2.000000e+01 1.000000e+01 + vertex -2.000000e+01 -2.000000e+01 1.800000e+01 + endloop + endfacet + facet normal 0.000000e+00 1.000000e+00 0.000000e+00 + outer loop + vertex 2.000000e+01 -2.000000e+01 1.800000e+01 + vertex -2.000000e+01 -2.000000e+01 1.000000e+01 + vertex -2.000000e+01 -2.000000e+01 1.800000e+01 + endloop + endfacet + facet normal 0.000000e+00 1.000000e+00 -0.000000e+00 + outer loop + vertex 2.000000e+01 -2.000000e+01 1.000000e+01 + vertex -2.000000e+01 -2.000000e+01 1.000000e+01 + vertex 2.000000e+01 -2.000000e+01 1.800000e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex 2.000000e+01 2.000000e+01 1.000000e+01 + vertex -2.000000e+01 -2.000000e+01 1.000000e+01 + vertex 2.000000e+01 -2.000000e+01 1.000000e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex 2.000000e+01 2.000000e+01 1.000000e+01 + vertex -2.000000e+01 2.000000e+01 1.000000e+01 + vertex -2.000000e+01 -2.000000e+01 1.000000e+01 + endloop + endfacet +endsolid