/** Copyright Stewart Allen -- All Rights Reserved */ "use strict"; self.kiri = (self.kiri || {}); self.kiri.version = exports.VERSION; self.kiri.license = exports.LICENSE; self.kiri.copyright = exports.COPYRIGHT; (function () { let iOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent), autoDecimate = true, // --------------- MOTO = moto, KIRI = self.kiri, BASE = self.base, UTIL = BASE.util, DBUG = BASE.debug, LANG = KIRI.lang.current, WIN = self.window, DOC = self.document, LOC = self.location, HOST = LOC.host.split(':'), SETUP = parseOpt(LOC.search.substring(1)), SECURE = isSecure(LOC.protocol), LOCAL = self.debug && !SETUP.remote, SDB = MOTO.KV, ODB = KIRI.odb = new MOTO.Storage(SETUP.d ? SETUP.d[0] : 'kiri'), SPACE = KIRI.space = MOTO.Space, WIDGETS = KIRI.widgets = [], CATALOG = KIRI.catalog = KIRI.openCatalog(ODB,autoDecimate), STATS = new Stats(SDB), SEED = 'kiri-seed', // --------------- CONF = KIRI.conf, MODES = CONF.MODES, VIEWS = CONF.VIEWS, clone = Object.clone, settings = clone(CONF.template), settingsDefault = clone(settings), // --------------- Widget = kiri.Widget, newWidget = kiri.newWidget, // --------------- UI = {}, UC = MOTO.ui.prefix('kiri').inputAction(updateSettings).hideAction(updateDialogLeft), MODE = MODES.FDM, onEvent = {}, currentPrint = null, selectedMeshes = [], localFilterKey ='kiri-gcode-filters', localFilters = js2o(SDB.getItem(localFilterKey)) || [], // --------------- renderMode = 4, viewMode = VIEWS.ARRANGE, layoutOnAdd = true, local = SETUP.local, camStock = null, camTopZ = 0, topZ = 0, showFavorites = SDB.getItem('dev-favorites') === 'true', alerts = [], grouping = false; if (SETUP.rm) renderMode = parseInt(SETUP.rm[0]); DBUG.enable(); // add show() to catalog for API CATALOG.show = showCatalog; const feature = { seed: true, controls: true }; const selection = { opacity: setOpacity, move: moveSelection, scale: scaleSelection, rotate: rotateSelection, meshes: function() { return selectedMeshes.slice() }, widgets: function() { return selectedMeshes.slice().map(m => m.widget) }, for_groups: forSelectedGroups, for_meshes: forSelectedMeshes, for_widgets: forSelectedWidgets }; const platform = { add: platformAdd, delete: platformDelete, layout: platformLayout, load: platformLoad, load_stl: platformLoadSTL, deselect: platformDeselect, select: platformSelect, select_all: platformSelectAll, selected_count: platformSelectedCount, compute_max_z: platformComputeMaxZ, update_origin: platformUpdateOrigin, update_bounds: platformUpdateBounds, update_stock: platformUpdateStock, update_size: platformUpdateSize, update_top_z: platformUpdateTopZ, update_selected: platformUpdateSelected, load_files: platformLoadFiles, group: platformGroup, group_done: platformGroupDone }; const color = { wireframe: 0x444444, wireframe_opacity: 0.25, selected: [ 0xbbff00, 0xbbee00, 0xbbdd00 ], deselected: [ 0xffff00, 0xffdd00, 0xffbb00 ], slicing: 0xffaaaa, preview_opacity: 0.0, model_opacity: 1.0, slicing_opacity: 0.5, sliced_opacity: 0.0, cam_preview: 0x0055bb, cam_preview_opacity: 0.25, cam_sliced_opacity: 0.25 }; const lists = { infill: [ { name: "vase" }, { name: "hex" }, { name: "grid" }, { name: "gyroid" }, { name: "triangle" }, { name: "linear" }, { name: "bubbles" } ], units: [ { name: "mm" }, { name: "in" } ], antialias: [ { name: "1", id: 1 }, { name: "2", id: 2 }, { name: "4", id: 4 }, { name: "8", id: 8 } ] }; const API = KIRI.api = { ui: UI, uc: UC, sdb: SDB, o2js: o2js, js2o: js2o, ajax: ajax, clone: clone, focus: setFocus, stats: STATS, catalog: CATALOG, conf: { get: getSettings, put: putSettings, load: loadSettings, save: saveSettings, show: showSettings, update: updateSettings, restore: restoreSettings, export: settingsExport, import: settingsImport }, color, const: { SEED, LANG, LOCAL, MODES, VIEWS, SETUP }, var: { layer_at: Infinity, layer_max: 0, layer_range: 0 }, device: { get: currentDeviceName, set: undefined // set during init }, dialog: { show: showDialog, hide: hideDialog, update: updateDialogLeft }, help: { show: showHelp, file: showHelpFile }, event: { on: addOnEvent, emit: sendOnEvent, import: loadFile, alerts: updateAlerts, settings: triggerSettingsEvent }, feature, function: { slice: prepareSlices, print: preparePrint, export: function() { KIRI.export() }, clear: clearWidgetCache }, hide: { import: function() { UI.import.style.display = 'none' } }, language: KIRI.lang, lists, modal: { show: showModal, hide: hideModal, visible: modalShowing }, mode: { get_lower: getModeLower, get_id: function() { return MODE }, get: getMode, set: setMode, switch: switchMode, set_expert: setExpert }, print: { get: function() { return currentPrint }, clear: clearPrint }, probe: { grid : function() { return false }, local : function() { return false } }, platform, selection, show: { alert: alert2, devices: undefined, // set during init progress: setProgress, controls: setControlsVisible, favorites: getShowFavorites, slices: showSlices, layer: setVisibleLayer, local: showLocal, import: function() { UI.import.style.display = '' } }, space: { reload: reload, restore: restoreWorkspace, clear: clearWorkspace, save: saveWorkspace, }, view: { get: function() { return viewMode }, set: setViewMode, update_fields: updateFields, wireframe: toggleWireframe, snapshot: null }, widgets: { new: newWidget, all: function() { return WIDGETS.slice() }, for: forAllWidgets, load: Widget.loadFromCatalog, meshes: meshArray, opacity: setOpacity }, work: KIRI.work }; function reload() { API.event.emit('reload'); // allow time for async saves to complete setTimeout(() => { LOC.reload() }, 50); } /** ****************************************************************** * Stats accumulator ******************************************************************* */ function Stats(db) { this.db = db; this.obj = js2o(this.db['stats'] || '{}'); let o = this.obj, k; for (k in o) { if (!o.hasOwnProperty(k)) continue; if (k === 'dn' || k.indexOf('-') > 0 || k.indexOf('_') > 0) { delete o[k]; } } } Stats.prototype.save = function(quiet) { this.db['stats'] = o2js(this.obj); if (!quiet) { API.event.emit('stats', this.obj); } return this; }; Stats.prototype.get = function(k) { return this.obj[k]; }; Stats.prototype.set = function(k,v,quiet) { this.obj[k] = v; this.save(quiet); return this; }; Stats.prototype.add = function(k,v,quiet) { this.obj[k] = (this.obj[k] || 0) + (v || 1); this.save(quiet); return this; }; Stats.prototype.del = function(k, quiet) { delete this.obj[k]; this.save(quiet); return this; }; 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); /** ****************************************************************** * Utility Functions ******************************************************************* */ function unitScale() { return MODE === MODES.CAM && settings.controller.units === 'in' ? 25.4 : 1; } function alert2(message, time) { if (message === undefined) { return updateAlerts(true); } alerts.push([message, Date.now(), time]); updateAlerts(); } function updateAlerts(clear) { if (clear) { alerts = []; } let now = Date.now(); // filter out by age alerts = alerts.filter(alert => { return (now - alert[1]) < ((alert[2] || 5) * 1000); }); // limit to 5 showing while (alerts.length > 5) { alerts.shift(); } // return if called before UI configured if (!UI.alert) { return; } if (alerts.length > 0) { UI.alert.text.innerHTML = alerts.map(v => ['
',v[0],'
'].join('')).join(''); UI.alert.dialog.style.display = 'flex'; } else { UI.alert.dialog.style.display = 'none'; } } function getShowFavorites(bool) { if (bool !== undefined) { SDB.setItem('dev-favorites', bool); showFavorites = bool; return bool; } return showFavorites; } function sendOnEvent(name, data) { if (name && onEvent[name]) onEvent[name].forEach(function(fn) { fn(data); }); } function addOnEvent(name, handler) { if (name && typeof(name) === 'string' && typeof(handler) === 'function') { onEvent[name] = onEvent[name] || []; onEvent[name].push(handler); } } function triggerSettingsEvent() { API.event.emit('settings', settings); } function isSecure(proto) { return proto.toLowerCase().indexOf("https") === 0; } function parseOpt(ov) { let opt = {}, kv, kva; // handle kiri legacy and proper url encoding better ov.replace(/&/g,',').split(',').forEach(function(el) { kv = decodeURIComponent(el).split(':'); if (kv.length === 2) { kva = opt[kv[0]] = opt[kv[0]] || []; kva.push(decodeURIComponent(kv[1])); } }); return opt; } function ajax(url, fn, rt, po, hd) { return new MOTO.Ajax(fn, rt).request(url, po, hd); } function o2js(o,def) { return o ? JSON.stringify(o) : def || null; } function js2o(s,def) { try { return s ? JSON.parse(s) : def || null; } catch (e) { console.log({malformed_json:s}); return def || null; } } function ls2o(key,def) { return js2o(SDB.getItem(key),def); } function setProgress(value, msg) { if (value) { value = UTIL.round(value*100,4); UI.loading.display = 'block'; UI.progress.width = value+'%'; if (msg) UI.prostatus.innerHTML = msg; } else { UI.loading.display = 'none'; } } function bound(v,min,max) { return Math.max(min,Math.min(max,v)); } function setVisibleLayer(v) { showSlices(API.var.layer_at = bound(v, 0, API.var.layer_max)); } function meshArray() { let out = []; forAllWidgets(function(widget) { out.push(widget.mesh); }); return out; } function forAllWidgets(f) { WIDGETS.slice().forEach(function(widget) { f(widget); }); } function forSelectedGroups(f) { let m = selectedMeshes; if (m.length === 0 && WIDGETS.length === 1) m = [ WIDGETS[0].mesh ]; let v = []; m.slice().forEach(function (mesh) { if (v.indexOf(mesh.widget.group) < 0) f(mesh.widget); v.push(mesh.widget.group); }); } function forSelectedWidgets(f) { let m = selectedMeshes; if (m.length === 0 && WIDGETS.length === 1) m = [ WIDGETS[0].mesh ]; m.slice().forEach(function (mesh) { f(mesh.widget) }); } function forSelectedMeshes(f) { selectedMeshes.slice().forEach(function (mesh) { f(mesh) }); } function toggleWireframe(color, opacity) { forAllWidgets(function(w) { w.toggleWireframe(color, opacity) }); SPACE.update(); } function updateSliderMax(set) { let max = 0; if (viewMode === VIEWS.PREVIEW && currentPrint) { max = currentPrint.getLayerCount(); } else { forAllWidgets(function(widget) { if (!widget.slices) return; max = Math.max(max, widget.slices.length); }); } max = Math.max(0, max - 1); API.var.layer_max = max; if (UI.layerID.convert() > max || API.var.layer_at > max) { API.var.layer_at = max; UI.layerID.value = max; UI.layerSlider.value = API.var.layer_max; } UI.layerSlider.max = max; if (set) { API.var.layer_at = API.var.layer_max; UI.layerSlider.value = API.var.layer_max; } } function hideSlices() { let showing = false; setOpacity(color.model_opacity); forAllWidgets(function(widget) { widget.setWireframe(false); showing = widget.hideSlices() || showing; }); clearPrint(); return showing; } function showSlice(index, range, layer) { if (range) { return index <= layer && index > layer-range; } else { return index <= layer; } } /** * hide or show slice-layers and their sub-elements * * @param {number} [layer] */ function showSlices(layer) { if (typeof(layer) === 'string' || typeof(layer) === 'number') { layer = parseInt(layer); } else { layer = API.var.layer_at; } layer = bound(layer, 0, API.var.layer_max); UI.layerID.value = layer; UI.layerSlider.value = layer; let j, slice, slices, layers, range = UI.layerRange.checked ? UI.layerSpan.convert() || 1 : 0, print = UI.layerPrint.checked, moves = UI.layerMoves.checked; if (MODE === MODES.CAM && API.var.layer_range !== range && range && layer === API.var.layer_max) { layer = 0; } API.var.layer_range = range; API.var.layer_at = layer; forAllWidgets(function(widget) { if (print) return widget.hideSlices(); slices = widget.slices; if (!slices) return; for (j = 0; j < slices.length; j++) { slice = slices[j]; slice.view.visible = showSlice(j, range, layer); layers = slice.layers; layers.outline.setVisible( MODE === MODES.CAM ? UI.layerOutline.checked && LOCAL : UI.layerOutline.checked ); layers.trace.setVisible( MODE === MODES.CAM ? UI.layerRough.checked : UI.layerTrace.checked ); layers.bridge.setVisible( MODE === MODES.CAM ? UI.layerFinishX.checked : UI.layerDelta.checked ); layers.flat.setVisible( MODE === MODES.CAM ? UI.layerFinishY.checked : UI.layerDelta.checked ); layers.solid.setVisible( MODE === MODES.CAM ? UI.layerFinish.checked : UI.layerSolid.checked ); layers.fill.setVisible( MODE === MODES.CAM ? UI.layerFacing.checked : UI.layerFill.checked ); layers.sparse.setVisible(UI.layerSparse.checked); layers.support.setVisible(UI.layerSupport.checked); } }); if (currentPrint) { let len = currentPrint.getLayerCount(); for (j = 0; j < len; j++) { currentPrint.showLayer(j, print && showSlice(j, range, layer), moves); } } UI.layerPrint.parentNode.style.display = currentPrint ? '' : 'none'; UI.layerMoves.parentNode.style.display = currentPrint ? '' : 'none'; SPACE.update(); } function loadCode(code, type) { setViewMode(VIEWS.PREVIEW); clearPrint(); setOpacity(0); currentPrint = kiri.newPrint(settings, []); let center = settings.process.outputOriginCenter; let origin = settings.origin; let offset = { x: origin.x, y: -origin.y, z: origin.z }; switch (type) { case 'svg': currentPrint.parseSVG(code, offset); break; default: currentPrint.parseGCode(code, offset); break; } currentPrint.render(); SPACE.platform.add(currentPrint.group); SPACE.update(); UI.layerPrint.checked = true; updateSliderMax(true); showSlices(); } function preparePrint(callback) { if (viewMode === VIEWS.PREVIEW) return; // kick off slicing it hasn't been done already for (let i=0; i < WIDGETS.length; i++) { if (!WIDGETS[i].slices || WIDGETS[i].isModified()) { prepareSlices(function() { if (!WIDGETS[i].slices || WIDGETS[i].isModified()) { alert2("nothing to print"); } else { preparePrint(callback); } }); return; } } setViewMode(VIEWS.PREVIEW); clearPrint(); API.conf.save(); if (MODE === MODES.CAM) { setOpacity(color.cam_preview_opacity); forAllWidgets(function(widget) { widget.setColor(color.cam_preview); }); } else { setOpacity(color.preview_opacity); } currentPrint = kiri.newPrint(settings, WIDGETS); currentPrint.setup(true, function(update, status) { setProgress(update, status); }, function() { if (!currentPrint) { return setViewMode(VIEWS.ARRANGE); } setProgress(0); setOpacity(0); currentPrint.render(); API.event.emit('print', getMode()); SPACE.platform.add(currentPrint.group); SPACE.update(); UI.layerPrint.checked = true; updateSliderMax(true); showSlices(); if (typeof(callback) === 'function') { callback(); } }); } function clearWidgetCache() { hideSlices(); clearSlices(); clearPrint(); } function clearPrint() { if (currentPrint) { SPACE.platform.remove(currentPrint.group); currentPrint = null; } UI.layerPrint.checked = false; } function clearSlices() { forAllWidgets(function(widget) { widget.slices = null; }); } /** * incrementally slice all meshes then incrementally update them * * @param {Function} callback */ function prepareSlices(callback) { if (viewMode == VIEWS.ARRANGE) { let snap = SPACE.screenshot(); API.view.snapshot = snap.substring(snap.indexOf(",")+1); KIRI.work.snap(API.view.snapshot); } clearPrint(); platform.deselect(); setViewMode(VIEWS.SLICE); API.conf.save(); let firstMesh = true, countdown = WIDGETS.length, preserveMax = API.var.layer_max, preserveLayer = API.var.layer_at, totalProgress, track = {}, now = UTIL.time(); // require topo be sent back from worker for local printing settings.synth.sendTopo = false; setOpacity(color.slicing_opacity); // for each widget, slice forAllWidgets(function(widget) { let segtimes = {}, segNumber = 0, errored = false, startTime, lastMsg; widget.stats.progress = 0; widget.setColor(color.slicing); widget.slice(settings, function(sliced, error) { let mark = UTIL.time(); // on done widget.render(renderMode, MODE === MODES.CAM); // clear wireframe widget.setWireframe(false, color.wireframe, color.wireframe_opacity); widget.setOpacity(settings.mode === 'CAM' ? color.cam_sliced_opacity : color.sliced_opacity); widget.setColor(color.deselected); // update UI info if (sliced) { // update segment time if (lastMsg) segtimes[segNumber+"_"+lastMsg] = mark - startTime; segtimes.total = UTIL.time() - now; DBUG.log(segtimes); API.event.emit('slice', getMode()); updateSliderMax(true); if (preserveMax != API.var.layer_max) { preserveLayer = API.var.layer_max; } firstMesh = false; } // on the last exit, update ui and call the callback if (--countdown === 0 || error || errored) { setProgress(0); showSlices(preserveLayer); setOpacity(settings.mode === 'CAM' ? color.cam_sliced_opacity : color.sliced_opacity); if (callback && typeof callback === 'function') { callback(); } } // update slider window API.dialog.update(); // handle slicing errors if (error && !errored) { errored = true; setViewMode(VIEWS.ARRANGE); setOpacity(color.model_opacity); platform.deselect(); alert2(error); } }, function(update, msg) { if (msg !== lastMsg) { let mark = UTIL.time(); if (lastMsg) segtimes[segNumber+"_"+lastMsg] = mark - startTime; lastMsg = msg; startTime = mark; segNumber++; } // on update track[widget.id] = update; totalProgress = 0; forAllWidgets(function(w) { totalProgress += (track[w.id] || 0); }); setProgress((totalProgress / WIDGETS.length), msg); }, true); }); } /** ****************************************************************** * Selection Functions ******************************************************************* */ function updateSelectedInfo() { let bounds = new THREE.Box3(), track; forSelectedMeshes(mesh => { bounds = bounds.union(mesh.getBoundingBox()); track = mesh.widget.track; }); if (bounds.min.x === Infinity) { if (selectedMeshes.length === 0) { UI.sizeX.value = 0; UI.sizeY.value = 0; UI.sizeZ.value = 0; UI.scaleX.value = 1; UI.scaleY.value = 1; UI.scaleZ.value = 1; } return; } let dx = bounds.max.x - bounds.min.x, dy = bounds.max.y - bounds.min.y, dz = bounds.max.z - bounds.min.z, scale = unitScale(); UI.sizeX.value = UI.sizeX.was = UTIL.round(dx/scale,2); UI.sizeY.value = UI.sizeY.was = UTIL.round(dy/scale,2); UI.sizeZ.value = UI.sizeZ.was = UTIL.round(dz/scale,2); UI.scaleX.value = UI.scaleX.was = track.scale.x; UI.scaleY.value = UI.scaleY.was = track.scale.y; UI.scaleZ.value = UI.scaleZ.was = track.scale.z; } function setOpacity(value) { forAllWidgets(function (w) { w.setOpacity(value) }); UI.modelOpacity.value = value * 100; SPACE.update(); } function moveSelection(x, y, z, abs) { forSelectedGroups(function (w) { w.move(x, y, z, abs) }); platform.update_stock(); SPACE.update(); } function scaleSelection() { let args = arguments; forSelectedGroups(function (w) { w.scale(...args); }); // skip update if last argument is strictly 'false' if ([...arguments].pop() === false) { return; } updateSelectedInfo(); platform.compute_max_z(); platform.update_stock(true); SPACE.update(); } function rotateSelection(x, y, z) { forSelectedGroups(function (w) { w.rotate(x, y, z); }); updateSelectedInfo(); platform.compute_max_z(); platform.update_stock(true); SPACE.update(); } /** ****************************************************************** * Platform Functions ******************************************************************* */ function platformUpdateOrigin() { platform.update_bounds(); let dev = settings.device; let proc = settings.process; let x = 0; let y = 0; let z = 0; if (MODE === MODES.CAM && proc.camOriginTop) { z = camTopZ + 0.01; if (!camStock) { z += proc.camZTopOffset; } } if (!proc.outputOriginCenter) { if (camStock) { x = (-camStock.scale.x / 2) + camStock.position.x; y = (camStock.scale.y / 2) - camStock.position.y; } else { if (MODE === MODES.LASER && proc.outputOriginBounds) { let b = settings.bounds; x = b.min.x, y = -b.min.y } else { x = -dev.bedWidth / 2; y = dev.bedDepth / 2; } } } else if (camStock) { x = camStock.position.x; y = -camStock.position.y; } settings.origin = {x, y, z}; if (settings.controller.showOrigin && MODE !== MODES.SLA) { SPACE.platform.setOrigin(x,y,z); } else { SPACE.platform.setOrigin(); } } function platformUpdateTopZ() { let alignTopOk = WIDGETS.length > 1 && settings.controller.alignTop; let camz = (MODE === MODES.CAM) && (settings.stock.z || alignTopOk); let ztop = camz ? camTopZ - settings.process.camZTopOffset : 0; forAllWidgets(function(widget) { widget.setTopZ(ztop); }); } function platformUpdateSize() { let frozen = SPACE.scene.freeze(true); let dev = settings.device, width, depth, height = Math.round(Math.max(dev.bedHeight, dev.bedWidth/100, dev.bedDepth/100)); SPACE.platform.setRound(dev.bedRound); SPACE.platform.setSize( width = parseInt(dev.bedWidth), depth = parseInt(dev.bedDepth), height = parseFloat(dev.bedHeight) ); SPACE.platform.setGZOff(height/2 - 0.1); platform.update_origin(); SPACE.scene.freeze(frozen); } function platformUpdateBounds() { let bounds = new THREE.Box3(); forAllWidgets(function(widget) { let wp = widget.track.pos; let wb = widget.mesh.getBoundingBox().clone(); wb.min.x += wp.x; wb.max.x += wp.x; wb.min.y += wp.y; wb.max.y += wp.y; bounds.union(wb); }); return settings.bounds = bounds; } function platformSelect(widget, shift) { if (viewMode !== VIEWS.ARRANGE) return; let mesh = widget.mesh, sel = (selectedMeshes.indexOf(mesh) >= 0); if (sel) { if (shift) { platform.deselect(widget) } else if (selectedMeshes.length > 1) { platform.deselect(); platform.select(widget, false); } } else { // prevent selection in slice view if (!mesh.material.visible) return; if (!shift) platform.deselect(); selectedMeshes.push(mesh); API.event.emit('widget.select', widget); widget.setColor(color.selected, settings); updateSelectedInfo(); } platformUpdateSelected(); SPACE.update(); } function platformSelectedCount() { return viewMode === VIEWS.ARRANGE ? selectedMeshes.length : 0; } function platformUpdateSelected() { UI.selection.style.display = platform.selected_count() ? 'inline' : 'none'; $('ext-sel').style.display = (MODE === MODES.FDM) ? 'inline-block' : 'none'; let extruders = settings.device.extruders; if (extruders) { for (let i=0; i| device | type | status | |
|---|---|---|---|
| ${r.device.name} | `); html.push(`${r.device.mode} | `); html.push(`${r.state} | `); html.push(``); html.push(` |