/** Copyright 2014-2017 Stewart Allen -- All Rights Reserved */ "use strict"; self.kiri = (self.kiri || {}); self.kiri.version = exports.VERSION; self.kiri.copyright = exports.COPYRIGHT; self.kiri.license = exports.LICENSE; (function () { if (kiri.init) return; function genID() { while (true) { var k = Math.round(Math.random() * 9999999999).toString(36); if (k.length >= 4 && k.length <= 8) return k; } } var iOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent), // --------------- MODES = { FDM: 1, // fused deposition modeling (also FFF) LASER: 2, // laser cutting CAM: 3 // 3 axis milling/machining }, VIEWS = { ARRANGE: 1, SLICE: 2, PREVIEW: 3 }, // --------------- settings filters (for loading/saving) sf = { fdm:{ // fields permitted in FDM:Device d:{ bedWidth: 1, bedDepth: 1, bedHeight: 1, maxHeight: 1, extrudeAbs: 1, filamentSize: 1, nozzleSize: 1, gcodePre: 1, gcodePost: 1, gcodeProc: 1, gcodeFExt: 1, gcodeFan: 1, gcodeTrack: 1, gcodeLayer: 1 }, // fields permitted in FDM:Process p:{ processName: 1, sliceHeight: 1, sliceShells: 1, sliceShellSpacing: 1, sliceFillAngle: 1, sliceFillOverlap: 1, sliceFillSpacing: 1, sliceFillSparse: 1, sliceSupportEnable: 1, sliceSupportDensity: 1, sliceSupportOffset: 1, sliceSupportSize: 1, sliceSupportArea: 1, sliceSupportExtra: 1, sliceSupportSpan: 1, sliceSolidMinArea: 1, sliceSolidLayers: 1, sliceBottomLayers: 1, sliceTopLayers: 1, sliceVase: 1, firstSliceHeight: 1, firstLayerRate: 1, outputRaft: 1, outputTemp: 1, outputFanMax: 1, outputBedTemp: 1, outputFeedrate: 1, outputFinishrate: 1, outputSeekrate: 1, outputShellMult: 1, outputFillMult: 1, outputSparseMult: 1, outputRetractOver: 1, outputRetractDist: 1, outputRetractSpeed: 1, outputRetractDwell: 1, outputBrimCount: 1, outputBrimOffset: 1, outputShortPoly: 1, outputShortDistance: 1, outputShortFactor: 1, outputFinishFactor: 1, outputCooling: 1, outputOuterFirst: 1, detectThinWalls: 1, outputWipeSpeed: 1, outputWipeDistance: 1, outputClockwise: 1, outputOriginCenter: 1, outputInvertX: 1, outputInvertY: 1 } }, cam:{ // fields permitted in CAM:Device d:{ bedWidth: 1, bedDepth: 1, bedHeight: 1, spindleMax: 1, gcodePre: 1, gcodePost: 1, gcodeDwell: 1, gcodeChange: 1, gcodeFExt: 1, gcodeSpace: 1, gcodeStrip: 1 }, // fields permitted in CAM:Process p:{ processName: 1, roughingTool: 1, roughingSpindle: 1, roughingDown: 1, roughingOver: 1, roughingSpeed: 1, roughingPlunge: 1, roughingStock: 1, roughingOn: 1, finishingTool: 1, finishingSpindle: 1, finishingDown: 1, finishingOver: 1, finishingAngle: 1, finishingSpeed: 1, finishingPlunge: 1, finishingOn: 1, finishingXOn: 1, finishingYOn: 1, finishCurvesOnly: 1, drillTool: 1, drillSpindle: 1, drillDownSpeed: 1, drillDown: 1, drillDwell: 1, drillLift: 1, drillingOn: 1, camTabsWidth: 1, camTabsHeight: 1, camTabsOn: 1, camPocketOnly: 1, camDepthFirst: 0, camEaseDown: 1, camOriginTop: 1, camTolerance: 1, camZTopOffset: 1, camZBottom: 1, camZClearance: 1, camStockX: 1, camStockY: 1, camStockZ: 1, outputClockwise: 1, outputOriginCenter: 1, outputInvertX: 1, outputInvertY: 1 } }, laser: { // fields permitted in Laser:Device d:{ bedWidth: 1, bedDepth: 1, }, // fields permitted in Laser:Process p:{ processName: 1, laserOffset: 1, laserSliceHeight: 1, outputTileSpacing: 1, outputTileScaling: 1, outputLaserPower: 1, outputLaserSpeed: 1, outputOriginCenter: 1, outputInvertX: 1, outputInvertY: 1 } } }, // --------------- (default) settings = { // CAM only tools:[ { id: 1000, number: 1, type: "endmill", name: "end 1/4", metric: false, flute_diam: 0.25, flute_len: 0, shaft_diam: 0.25, shaft_len: 0 }, { id: 1001, number: 2, type: "endmill", name: "end 1/8", metric: false, flute_diam: 0.125, flute_len: 0, shaft_diam: 0.125, shaft_len: 0 }, { id: 1002, number: 3, type: "endmill", name: "end 1/16", metric: false, flute_diam: 0.0625, flute_len: 0, shaft_diam: 0.0625, shaft_len: 0 } ], // FDM/CAM/Laser device:{ bedWidth: 300, // FDM/CAM/Laser bedDepth: 175, // FDM/CAM/Laser bedHeight: 2.5, // display only (deprecate) maxHeight: 150, // FDM filamentSize: 1.75, // FDM nozzleSize: 0.4, // FDM spindleMax: 0, // CAM gcodePre: [], // FDM/CAM header script gcodePost: [], // FDM/CAM footer script gcodeProc: "", // FDM post processor script gcodeFan: "", // FDM fan command gcodeTrack: "", // FDM progress command gcodeLayer: "", // FDM layer output gcodeDwell: [], // CAM dwell script gcodeChange: [], // CAM tool change script gcodeFExt: "", // CAM file extension gcodeSpace: "", // CAM token spacing gcodeStrip: true // CAM strip comments }, // FDM/CAM/Laser process:{ processName: "default", // --- FDM --- sliceHeight: 0.25, sliceShells: 2, sliceShellSpacing: 1, sliceFillAngle: 45, sliceFillOverlap: 0.3, sliceFillSpacing: 1.0, sliceFillSparse: 0.5, sliceSupportEnable: false, sliceSupportDensity: 0.25, sliceSupportOffset: 1.0, sliceSupportSize: 4, sliceSupportArea: 1, sliceSupportExtra: 1, sliceSupportSpan: 6, sliceSolidMinArea: 0.1, sliceSolidLayers: 2, sliceBottomLayers: 2, sliceTopLayers: 2, sliceVase: false, firstSliceHeight: 0.25, firstLayerRate: 30, outputRaft: false, outputTemp: 220, outputFanMax: 255, outputBedTemp: 0, outputFeedrate: 100, outputFinishrate: 60, outputSeekrate: 150, outputShellMult: 1.1, outputFillMult: 1.2, outputSparseMult: 1.3, outputRetractOver: 5.0, outputRetractDist: 0.5, outputRetractSpeed: 20, outputRetractDwell: 50, outputWipeSpeed: 50, outputWipeDistance: 0, outputBrimCount: 2, outputBrimOffset: 3, outputShortPoly: 15.0, outputShortDistance: 5.0, outputShortFactor: 0.2, outputFinishFactor: 0, outputOuterFirst: false, detectThinWalls: false, outputCooling: true, // --- LASER --- laserOffset: 0.25, laserSliceHeight: 1, outputTileSpacing: 1, // LASER outputTileScaling: 1, // LASER outputLaserPower: 100, // LASER outputLaserSpeed: 1000, // LASER // --- CAM --- roughingTool: 1000, roughingSpindle: 1000, roughingDown: 2, roughingOver: 0.5, roughingSpeed: 1000, roughingPlunge: 250, roughingStock: 0, roughingOn: true, finishingTool: 1000, finishingSpindle: 1000, finishingDown: 3, finishingOver: 0.5, finishingAngle: 85, finishingSpeed: 800, finishingPlunge: 250, finishingOn: true, finishingXOn: true, finishingYOn: true, finishCurvesOnly: false, drillTool: 1000, drillSpindle: 1000, drillDownSpeed: 250, drillDown: 5, drillDwell: 250, drillLift: 2, drillingOn: false, camTabsWidth: 5, camTabsHeight: 5, camTabsOn: false, camPocketOnly: false, camDepthFirst: false, camEaseDown: false, camOriginTop: true, camTolerance: 0.15, camZTopOffset: 0, camZBottom: 0, camZClearance: 1, camStockX: 0, camStockY: 0, camStockZ: 0, outputClockwise: true, // --- shared FDM/Laser/CAM --- outputOriginCenter: true, outputInvertX: false, outputInvertY: false }, // saved processes by name sproc:{ FDM: {}, CAM: {}, LASER: {} }, // current process name cproc:{ FDM: null, CAM: null, LASER: null }, // cached device settings by mode cdev: { FDM: null, CAM: null }, // now they're called devices instead of gcode filters filter:{ FDM: "Any.Generic.Marlin", CAM: "Any.Generic.Grbl" }, // custom devices by name devices:{ }, layers:{ layerOutline: true, layerTrace: true, layerFacing: true, layerRough: true, layerFinish: true, layerFinishX: true, layerFinishY: true, layerDelta: false, layerSolid: false, layerSparse: true, layerFill: true, layerSupport: true, layerPrint: false }, synth: { // set in updateSettingsFromFields() fillOffsetMult: 0, diffOffsetMult: 0 }, controller:{ view: null, zoomSpeed: 1.0, reverseZoom: true }, mode: 'FDM', id: genID(), ver: 1 }, settingsDefault = settings, autoDecimate = true, // --------------- SELF = self, MOTO = moto, KIRI = SELF.kiri, BASE = SELF.base, UTIL = BASE.util, DBUG = BASE.debug, // --------------- WIN = SELF.window, DOC = SELF.document, LOC = SELF.location, SETUP = parseOpt(LOC.search.substring(1)), LOCAL = LOC.host.indexOf('localhost') === 0, SECURE = isSecure(LOC.protocol), // --------------- 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), // ACTIVE = 'kiri-active', // IDLE = 'kiri-idle', SEED = 'kiri-seed', // --------------- Widget = kiri.Widget, newWidget = kiri.newWidget, // --------------- UI = {}, UC = MOTO.ui.prefix('kiri').inputAction(updateSettings).hideAction(onControlResize), DEFMODE = SETUP.dm && SETUP.dm.length === 1 ? SETUP.dm[0] : 'FDM', STARTMODE = SETUP.sm && SETUP.sm.length === 1 ? SETUP.sm[0] : null, MODE = MODES.FDM, onEvent = {}, screenShot = null, currentPrint = null, selectedMeshes = [], localFilterKey ='kiri-gcode-filters', localFilters = js2o(SDB.getItem(localFilterKey)) || [], OCTOPRINT = null, // --------------- wireframe_color = 0x444444, wireframe_model_opacity = 0.25, widget_selected_color = 0xbbff00, widget_deselected_color = 0xffff00, widget_slicing_color = 0xffaaaa, widget_cam_preview_color = 0x0055bb, preview_opacity_cam = 1, preview_opacity = 0.0, model_opacity = 1.0, slicing_opacity = 0.5, sliced_opacity = 0.0, sliced_opacity_cam = 0.25, // --------------- printSeq = parseInt(SDB['print-seq'] || "0") + 1, catalogSize = 0, showLayerRange = 0, showLayerValue = 0, showLayerMax = 0, renderMode = 4, viewMode = VIEWS.ARRANGE, layoutOnAdd = true, deviceLock = false, local = SETUP.local, mouseMoved = false, camStock = null, camTopZ = 0; DBUG.enable(); if (SETUP.rm) renderMode = parseInt(SETUP.rm[0]); KIRI.api = { ui : UI, on : addOnEvent, sdb : SDB, o2js : o2js, js2o : js2o, ajax : ajax, help : showHelp, load : loadWidget, focus : takeFocus, stats : STATS, import : loadFile, catalog : CATALOG, getMode : getMode, setMode : setMode, showModal : showModal, hideModal : hideModal, showDialog : showDialog, hideDialog : hideDialog, showCatalog : showCatalog, addWidget : platformAdd, selectAll : platformSelectAll, selectNone : widgetDeselect, showProgress : setProgress, clearWorker : KIRI.work.clear, getSettings : getSettings, putSettings : putSettings, hideImport : function() { UI.import.style.display = 'none' }, mouse : { moved : function() { return mouseMoved }, movedSet : function(b) { mouseMoved = b } } }; /** ****************************************************************** * Stats accumulator ******************************************************************* */ function sendOnEvent(name, data) { if (name && onEvent[name]) onEvent[name](data); } function addOnEvent(name, handler) { if (name && typeof(name) === 'string' && typeof(handler) === 'function') { onEvent[name] = handler; } } function triggerSettingsEvent() { sendOnEvent('settings', settings); } function oremap(o,k,p) { try { var ov = o[k]; delete o[k]; o[p+(k.split('-')[1].toLowerCase())] = ov; } catch (e) { console.log(e) } } function Stats(db) { this.db = db; this.obj = js2o(db['stats'] || '{}'); var o = this.obj, k; for (k in o) { if (!o.hasOwnProperty(k)) continue; if (k.indexOf('slice-') === 0) oremap(o,k,'s-'); if (k.indexOf('prep-') === 0) oremap(o,k,'p-'); if (k.indexOf('export-') === 0) oremap(o,k,'e-'); if (k.indexOf('show-') === 0) oremap(o,k,'d-'); } } Stats.prototype.save = function() { this.db['stats'] = o2js(this.obj); return this; }; Stats.prototype.get = function(k) { return this.obj[k]; } Stats.prototype.set = function(k,v) { this.obj[k] = v; this.save(); return this; }; Stats.prototype.add = function(k,v) { this.obj[k] = (this.obj[k] || 0) + (v || 1); this.save(); return this; }; Stats.prototype.del = function(k) { delete this.obj[k]; this.save(); return this; } STATS.set('upgrade', kiri.version !== STATS.get('kiri') && STATS.get('init') > 0); STATS.set('kiri', kiri.version); /** ****************************************************************** * Utility Functions ******************************************************************* */ function isSecure(proto) { return proto.toLowerCase().indexOf("https") === 0; } function parseOpt(ov) { var opt = {}, kv, kva; ov.split(',').forEach(function(el) { kv = 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 set(to, from) { for (var k in from) { if (from.hasOwnProperty(k)) to[k] = from[k]; } } function cull(o, f) { for (var k in o) { if (!o.hasOwnProperty(k)) continue; if (!f.hasOwnProperty(k)) delete o[k]; } } 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(showLayerValue = bound(v, 0, showLayerMax)); } function meshArray() { var out = []; forWidgets(function(widget) { out.push(widget.mesh); }); return out; } function forWidgets(f) { WIDGETS.slice().forEach(function(widget) { f(widget); }); } function forSelectedWidgets(f) { var 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) { forWidgets(function(w) { w.toggleWireframe(color, opacity) }); SPACE.update(); } function updateSliderMax(set) { var max = 0; if (viewMode === VIEWS.PREVIEW && currentPrint) { max = currentPrint.getLayerCount(); } else { forWidgets(function(widget) { if (!widget.slices) return; max = Math.max(max, widget.slices.length); }); } max = Math.max(0, max - 1); showLayerMax = max; if (UI.layerID.convert() > max || showLayerValue > max) { showLayerValue = max; UI.layerID.value = max; UI.layerSlider.value = showLayerMax; } UI.layerSlider.max = max; if (set) { showLayerValue = showLayerMax; UI.layerSlider.value = showLayerMax; } } function hideSlices() { var showing = false; setOpacity(model_opacity); forWidgets(function(widget) { widget.setWireframe(false); showing = widget.hideSlices() || showing; }); clearPrint(); return showing; } function showSlice(index, range, layer) { return range ? (index <= layer && index > layer-range) : (index <= layer); MODE === MODES.CAM ? (index >= layer) : (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 = showLayerValue; } layer = bound(layer, 0, showLayerMax); UI.layerID.value = layer; UI.layerSlider.value = layer; var j, slice, slices, layers, range = UI.layerRange.checked ? UI.layerSpan.convert() || 1 : 0, print = UI.layerPrint.checked; if (MODE === MODES.CAM && showLayerRange !== range && range && layer === showLayerMax) { layer = 0; } showLayerRange = range; showLayerValue = layer; forWidgets(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) { for (j = 0; j < currentPrint.layerView.length; j++) { currentPrint.showLayer(j, print && showSlice(j, range, layer)); } } SPACE.update(); } function newButton(name,action) { var b = DOC.createElement("button"); b.appendChild(DOC.createTextNode(name)); b.onclick = action; return b; } function loadGCode(gcode) { setViewMode(VIEWS.PREVIEW); clearPrint(); setOpacity(0); currentPrint = kiri.newPrint(settings, []); currentPrint.parseGCode(gcode, settings.process.outputOriginCenter ? null : { x: -settings.device.bedWidth / 2, y: -settings.device.bedDepth / 2, }); currentPrint.render(); SPACE.platform.add(currentPrint.group); SPACE.update(); UI.layerPrint.checked = true; updateSliderMax(true); showSlices(); exportGCode(gcode); } function preparePrint(callback) { // kick off slicing it hasn't been done already for (var i=0; i < WIDGETS.length; i++) { if (!WIDGETS[i].slices || WIDGETS[i].isModified()) { prepareSlices(function() { preparePrint(callback) }); return; } } setViewMode(VIEWS.PREVIEW); clearPrint(); saveSettings(); if (MODE === MODES.CAM) { setOpacity(preview_opacity_cam); forWidgets(function(widget) { widget.setColor(widget_cam_preview_color); }); } else { setOpacity(preview_opacity); } currentPrint = kiri.newPrint(settings, WIDGETS); currentPrint.setup(true, function(update, status) { // on update setProgress(update, status); }, function() { setProgress(0); currentPrint.render(); // on done STATS.add('p-'+getMode().toLowerCase()); SPACE.platform.add(currentPrint.group); SPACE.update(); UI.layerPrint.checked = true; updateSliderMax(true); showSlices(); if (typeof(callback) === 'function') callback(); }) } function exportPrint() { if (!currentPrint) { preparePrint(exportPrint); return; } STATS.add('e-'+getMode().toLowerCase()); switch (settings.mode) { case 'LASER': return exportPrintLaser(); case 'FDM': return exportPrintGCODE(); case 'CAM': return exportPrintGCODE(); } } function exportPrintGCODE() { if (!currentPrint) { preparePrint(exportPrint); return; } currentPrint.exportGCode(true, function(gcode) { exportGCode(gcode); }); } function exportPrintLaser() { if (!currentPrint) { preparePrint(exportPrintLaser); return; } var filename = "laser-"+(new Date().getTime().toString(36)); function download_svg() { saveAs(new Blob( [currentPrint.exportSVG($('print-color').value)], {type:"application/octet-stream"}), $('print-filename').value + ".svg"); } function download_dxf() { saveAs(new Blob( [currentPrint.exportDXF()], {type:"application/octet-stream"}), $('print-filename').value + ".dxf"); } function download_gcode() { saveAs(new Blob( [currentPrint.exportLaserGCode()], {type:"application/octet-stream"}), $('print-filename').value + ".gcode"); } ajax("/kiri/output-laser.html", function(html) { UI.print.innerHTML = html; $('print-filename').value = filename; $('print-lines').value = currentPrint.lines; $('print-close').onclick = hideModal; $('print-svg').onclick = download_svg; $('print-dxf').onclick = download_dxf; $('print-lg').onclick = download_gcode; showModal('print'); }); } function exportGCode(gcode) { if (MODE === MODES.CAM) { KIRI.serial.setGCode(gcode, currentPrint.bounds); } SDB['print-seq'] = printSeq++; var pre = (MODE === MODES.CAM ? "cnc-" : "print-") + (printSeq.toString().padStart(3,"0")), filename = pre,// + (new Date().getTime().toString(36)), fileext = settings.device.gcodeFExt || "gcode", codeproc = settings.device.gcodeProc, octo_host, octo_apik, grid_host, grid_apik, grid_target; // run gcode post processor function (when supplied and valid) if (codeproc && self[codeproc]) { gcode = self[codeproc](gcode); } function getBlob() { return new Blob( [gcode], {type:"application/octet-stream"}); } function octoprint() { if (!(octo_host && octo_apik)) return; var form = new FormData(), ajax = new XMLHttpRequest(), host = octo_host.value.toLowerCase(), apik = octo_apik.value; if (host.indexOf("http") !== 0) { alert("host missing protocol (http:// or https://)"); return; } if (SECURE && !isSecure(host)) { alert("host must begin with 'https' on a secure site"); return; } SDB['octo-host'] = host.trim(); SDB['octo-apik'] = apik.trim(); filename = $('print-filename').value; form.append("file", getBlob(), filename+"."+fileext); ajax.onreadystatechange = function() { if (ajax.readyState === 4) { var status = ajax.status; STATS.add('op-'+status); if (status >= 200 && status < 300) { hideModal(); } else { alert("octoprint error\nstatus: "+status+"\nmessage: "+ajax.responseText); } } }; ajax.upload.addEventListener('progress', function(evt) { setProgress(Math.ceil(evt.loaded/evt.total), "sending"); }); ajax.open("POST", host+"/api/files/local"); ajax.setRequestHeader("X-Api-Key", apik); ajax.send(form); } function gridprint_tracker(host,key) { ajax(host+"/api/check?key="+key, function(data) { data = js2o(data); DBUG.log(data); if (!data.done && !data.error) setTimeout(function() { gridprint_tracker(host,key) }, 1000); }); } function gridprint() { if (!(grid_host && grid_apik)) return; var xhtr = new XMLHttpRequest(), host = grid_host.value, apik = grid_apik.value, target = grid_target.value; if (host.indexOf("http") !== 0) { alert("host missing protocol (http:// or https://)"); return; } if (host.indexOf("://") < 0) { alert("host:port malformed"); return; } if (SECURE && !isSecure(host)) { alert("host must begin with 'https' on a secure site"); return; } SDB['grid-host'] = host.trim(); SDB['grid-apik'] = apik.trim(); SDB['grid-target'] = target.trim(); xhtr.onreadystatechange = function() { if (xhtr.readyState === 4) { var status = xhtr.status; STATS.add('gp-'+status); if (status >= 200 && status < 300) { var json = js2o(xhtr.responseText); gridprint_tracker(host,json.key); ajax(host+"/api/wait?key="+json.key, function(data) { data = js2o(data); DBUG.log(data); alert("print to "+target+" @ "+host+" : "+data.status); }); } else { alert("grid:print error\nstatus: "+status+"\nmessage: "+xhtr.responseText); } setProgress(0); } }; xhtr.upload.addEventListener('progress', function(evt) { setProgress(Math.ceil(evt.loaded/evt.total), "sending"); }); filename = $('print-filename').value; xhtr.open("POST", host + "/api/print?" + "filename=" + filename + "&target=" + target + "&key=" + apik + "&time=" + Math.round(currentPrint.time) + "&length=" + Math.round(currentPrint.distance) + "&image=" + filename ); xhtr.setRequestHeader("Content-Type", "text/plain"); xhtr.send(screenShot ? [gcode,screenShot].join("\0") : gcode); hideModal(); } function download() { filename = $('print-filename').value; saveAs(getBlob(), filename + "." + fileext); } function pad(v) { v = v.toString(); return v.length < 2 ? '0' + v : v; } function calcWeight() { $('print-weight').value = ( UTIL.round( (Math.PI * UTIL.sqr(currentPrint.settings.device.filamentSize/2)) * currentPrint.distance * parseFloat($('print-weight').value || 1.25) / 1000, 2) ); } function calcTime() { var floor = Math.floor, time = floor(currentPrint.time), hours = floor(time / 3600), newtime = time - hours * 3600, mins = floor(newtime / 60), secs = newtime - mins * 60; $('mill-time').value = $('print-time').value = [pad(hours),pad(mins),pad(secs)].join(':'); } ajax("/kiri/output-gcode.html", function(html) { UI.print.innerHTML = html; $('print-close').onclick = hideModal; $('print-download').onclick = download; $('print-octoprint').onclick = octoprint; $('print-gridprint').onclick = gridprint; $('print-serial').onclick = showSerial; $('print-serial').style.display = MODE === MODES.CAM ? '' : 'none'; $('print-filament-row').style.display = MODE === MODES.FDM ? '' : 'none'; $('mill-info').style.display = MODE === MODES.CAM ? '' : 'none'; $('print-filename').value = filename; $('print-filesize').value = currentPrint.bytes; $('print-filament').value = Math.round(currentPrint.distance); calcTime(); calcWeight(); octo_host = $('octo-host'); octo_apik = $('octo-apik'); if (OCTOPRINT) { $('ophost').style.display = 'none'; $('opapik').style.display = 'none'; $('ophint').style.display = 'none'; $('send-to-gridprint').style.display = 'none'; } octo_host.value = SDB['octo-host'] || ''; octo_apik.value = SDB['octo-apik'] || ''; grid_host = $('grid-host'); grid_apik = $('grid-apik'); grid_target = $('grid-target'); grid_host.value = SDB['grid-host'] || ''; grid_apik.value = SDB['grid-apik'] || ''; grid_target.value = SDB['grid-target'] || ''; showModal('print'); }); } function clearWidgetCache() { hideSlices(); clearSlices(); clearPrint(); } function clearPrint() { if (currentPrint) { SPACE.platform.remove(currentPrint.group); currentPrint = null; } UI.layerPrint.checked = false; } function clearSlices() { forWidgets(function(widget) { widget.slices = null; }); } /** * incrementally slice all meshes then incrementally update them * * @param {Function} callback */ function prepareSlices(callback) { if (viewMode == VIEWS.ARRANGE) { screenShot = SPACE.screenshot(); screenShot = screenShot.substring(screenShot.indexOf(",")+1); } setViewMode(VIEWS.SLICE); var selectSave = selectedMeshes.slice(); clearPrint(); saveSettings(); widgetDeselect(); var firstMesh = true, countdown = WIDGETS.length, preserveMax = showLayerMax, preserveLayer = showLayerValue, totalProgress, track = {}; // require topo be sent back from worker for local printing settings.synth.sendTopo = false; setOpacity(slicing_opacity); // for each widget, slice forWidgets(function(widget) { var segtimes = {}, segNumber = 0, startTime, lastMsg; // skip non-selected widgets in CAM mode when any widget is selected if (MODE === MODES.CAM && selectSave.length > 0 && selectSave.indexOf(widget.mesh) < 0) return --countdown; widget.stats.progress = 0; widget.setColor(widget_slicing_color); widget.slice(settings, function(sliced, error) { var mark = UTIL.time(); // on done widget.render(renderMode, MODE === MODES.CAM); // clear wireframe widget.setWireframe(false, wireframe_color, wireframe_model_opacity); widget.setOpacity(settings.mode === 'CAM' ? sliced_opacity_cam : sliced_opacity); widget.setColor(widget_deselected_color); // update UI info if (sliced) { // update segment time if (lastMsg) segtimes[segNumber+"_"+lastMsg] = mark - startTime; DBUG.log(segtimes); STATS.add('s-'+getMode().toLowerCase()); updateSliderMax(true); if (preserveMax != showLayerMax) { preserveLayer = showLayerMax; } firstMesh = false; } // on the last exit, update ui and call the callback if (--countdown === 0 || error) { setProgress(0); showSlices(preserveLayer); if (callback && typeof callback === 'function') callback(); setOpacity(settings.mode === 'CAM' ? sliced_opacity_cam : sliced_opacity); } // update slider window onControlResize(); // handle slicing errors if (error) { setViewMode(VIEWS.ARRANGE); setOpacity(model_opacity); widgetDeselect(); alert(error); } }, function(update, msg) { if (msg !== lastMsg) { var mark = UTIL.time(); if (lastMsg) segtimes[segNumber+"_"+lastMsg] = mark - startTime; lastMsg = msg; startTime = mark; segNumber++; } // on update track[widget.id] = update; totalProgress = 0; forWidgets(function(w) { totalProgress += (track[w.id] || 0); }); setProgress(totalProgress / WIDGETS.length, msg); }, true); }); } function fillOffsetMult() { return 1.0-bound(settings.process.sliceFillOverlap, 0, 0.8); } function diffOffset() { return (settings.device.nozzleSize / 2) * fillOffsetMult(); } function meshUpdateInfo(mesh) { UI.selection.style.display = selectedWidgetCount() ? 'block' : 'none'; if (!mesh) { if (selectedMeshes.length === 0) { UI.selWidth.innerHTML = '0'; UI.selDepth.innerHTML = '0'; UI.selHeight.innerHTML = '0'; UI.scaleX.value = ''; UI.scaleY.value = ''; UI.scaleZ.value = ''; } return } UI.selWidth.innerHTML = UTIL.round(mesh.w,2); UI.selDepth.innerHTML = UTIL.round(mesh.h,2); UI.selHeight.innerHTML = UTIL.round(mesh.d,2); UI.scaleX.value = 1; UI.scaleY.value = 1; UI.scaleZ.value = 1; } function setOpacity(value) { forWidgets(function (w) { w.setOpacity(value) }); UI.viewModelOpacity.value = value * 100; SPACE.update(); } function moveSelection(x, y, z, abs) { forSelectedWidgets(function (w) { w.move(x, y, z, abs) }); SPACE.update(); } function scaleSelection(ev) { var dv = parseFloat(ev.target.value || 1); if (UI.scaleUniform.checked) { UI.scaleX.value = dv; UI.scaleY.value = dv; UI.scaleZ.value = dv; } var x = parseFloat(UI.scaleX.value || dv), y = parseFloat(UI.scaleY.value || dv), z = parseFloat(UI.scaleZ.value || dv); forSelectedWidgets(function (w) { w.scale(x,y,z); meshUpdateInfo(w.mesh); }); UI.scaleX.value = 1; UI.scaleY.value = 1; UI.scaleZ.value = 1; platformComputeMaxZ(); SPACE.update(); } function rotateSelection(x, y, z) { forSelectedWidgets(function (w) { w.rotate(x, y, z) }); platformComputeMaxZ(); SPACE.update(); } function boundsSelection() { var bounds = new THREE.Box3(); forSelectedWidgets(function(widget) { bounds.union(widget.mesh.getBoundingBox()); }); return bounds; } function platformComputeMaxZ() { var maxZ = 0; forWidgets(function(widget) { maxZ = Math.max(maxZ, widget.mesh.getBoundingBox().max.z); }); SPACE.platform.setMaxZ(maxZ); } function platformAdd(widget, shift, nolayout) { WIDGETS.push(widget); SPACE.platform.add(widget.mesh); widgetSelect(widget, shift); platformComputeMaxZ(); if (nolayout) return; if (layoutOnAdd) layoutPlatform(0); } function platformDelete(widget) { if (Array.isArray(widget)) { var mc = widget.slice(), i; for (i=0; i= 0); if (sel) { if (shift) { widgetDeselect(widget) } else if (selectedMeshes.length > 1) { widgetDeselect(); widgetSelect(widget, false); } } else { // prevent selection in slice view if (!mesh.material.visible) return; if (!shift) widgetDeselect(); selectedMeshes.push(mesh); widget.setColor(widget_selected_color); meshUpdateInfo(mesh); } SPACE.update(); } function selectedWidgetCount() { return viewMode === VIEWS.ARRANGE ? selectedMeshes.length : 0; } function widgetDeselect(widget) { if (viewMode !== VIEWS.ARRANGE) return; if (!widget) { forWidgets(function(widget) { widgetDeselect(widget); }); return; } var mesh = widget.mesh, si = selectedMeshes.indexOf(mesh), sel = (si >= 0); if (sel) selectedMeshes.splice(si,1); widget.setColor(widget_deselected_color); SPACE.update(); meshUpdateInfo(); } function layoutPlatform(event, space) { var layout = (viewMode === VIEWS.ARRANGE), modified = false, gap = space || (1 + settings.process.sliceSupportExtra), topZ = MODE === MODES.CAM ? camTopZ : 0; setViewMode(VIEWS.ARRANGE); hideSlices(); // do not layout when switching back from slice view if (!space && !layout) return SPACE.update(); forWidgets(function(w) { modified |= w.isModified(); }); // in CNC mode with >1 widget, force layout with spacing @ 1.5x largest tool diameter if (MODE === MODES.CAM && WIDGETS.length > 1) { var spacing = space || 1, proc = settings.process, CAM = KIRI.driver.CAM; if (proc.roughingOn) spacing = Math.max(spacing, CAM.getToolDiameter(settings, proc.roughingTool)); if (proc.finishingOn || proc.finishingXOn || proc.finishingYOn) spacing = Math.max(spacing, CAM.getToolDiameter(settings, proc.finishingTool)); gap = spacing * 1.5; } var i, m, sz = SPACE.platform.size(), mp = [sz.x, sz.y], ms = [mp[0] / 2, mp[1] / 2], mi = mp[0] > mp[1] ? [(mp[0] / mp[1]) * 10, 10] : [10, (mp[1] / mp[1]) * 10], c = meshArray().sort(function (a, b) { return (b.w * b.h) - (a.w * a.h) }), p = new MOTO.Pack(ms[0], ms[1], gap).fit(c); while (!p.packed) { ms[0] += mi[0]; ms[1] += mi[1]; p = new MOTO.Pack(ms[0], ms[1], gap).fit(c); } for (i = 0; i < c.length; i++) { m = c[i]; m.fit.x += m.w / 2 + p.pad; m.fit.y += m.h / 2 + p.pad; m.widget.move(p.max.w / 2 - m.fit.x, p.max.h / 2 - m.fit.y, 0, true); m.widget.setTopZ(topZ); m.material.visible = true; } SPACE.update(); } function loadWidget(url, onload) { if (url.toLowerCase().indexOf(".stl") > 0) { loadSTL(url, onload); } else { ajax(url, function(vertices) { vertices = js2o(vertices).toFloat32(); platformAdd(newWidget().loadVertices(vertices)); if (onload) onload(vertices); }); } } function loadSTL(url, onload) { new MOTO.STL().load(url, function(vertices) { platformAdd(newWidget().loadVertices(vertices)); if (onload) onload(vertices); }) } function resetSettings(force) { if (force || confirm('reset all values to system defaults?')) { settings = settingsDefault; updateFields(); }; } /** * fill in missing settings from default template to pick up new fields * that may have been recently added and expected in the code * * @param {Object} osrc * @param {Object} odst */ function fillMissingSettings(osrc, odst) { var key, val; for (key in osrc) { if (!osrc.hasOwnProperty(key)) continue; val = odst[key]; if (typeof val === 'undefined' || val === null || val === '') { odst[key] = osrc[key]; } else if (typeof osrc[key] === 'object') { fillMissingSettings(osrc[key], odst[key]); } } } /** * @returns {Object} */ function updateFieldsFromSettings(scope) { if (!scope) return console.trace("missing scope"); var key, val; fillMissingSettings(settingsDefault, settings); for (key in scope) { if (!scope.hasOwnProperty(key)) continue; val = scope[key]; if (UI.hasOwnProperty(key)) { var uie = UI[key], typ = uie ? uie.type : null; if (typ === 'text') { uie.value = val; } else if (typ === 'checkbox') { uie.checked = val; } else if (typ === 'select-one') { uie.innerHTML = ''; var chosen = null; settings.tools.forEach(function(tool,index) { if (val === tool.id) chosen = index+1; var opt = DOC.createElement('option'); opt.appendChild(DOC.createTextNode(tool.name)); opt.setAttribute('value', tool.id); uie.appendChild(opt); }); if (chosen) uie.selectedIndex = chosen; } } } return settings; } /** * @returns {Object} */ function updateSettingsFromFields(scope) { if (!scope) return console.trace("missing scope"); var key, changed = false; // for each key in scope object for (key in scope) { if (!scope.hasOwnProperty(key)) continue; if (UI.hasOwnProperty(key)) { var nval = null, uie = UI[key]; // skip empty UI values if (!uie || uie === '') continue; if (uie.type === 'text') { nval = UI[key].convert(); } else if (uie.type === 'checkbox') { nval = UI[key].checked; } else if (uie.type === 'select-one') { if (uie.selectedIndex > 0) { nval = parseInt(uie.options[uie.selectedIndex].value); } } if (scope[key] != nval) { scope[key] = nval; } } } settings.synth.fillOffsetMult = fillOffsetMult(); settings.synth.diffOffsetMult = diffOffset(); return settings; } function camUpdateWidgetZ() { var ztop = MODE === MODES.CAM ? camTopZ : 0; forWidgets(function(widget) { widget.setTopZ(ztop); }); } function updateCamStock() { var sd = settings.process; // create/inject cam stock if stock size other than default if (sd.camStockX && sd.camStockY && sd.camStockZ) { if (!camStock) { var geo = new THREE.BoxGeometry(1, 1, 1); var mat = new THREE.MeshBasicMaterial({ color: 0x777777, opacity: 0.2, transparent: true, side:THREE.DoubleSide }); var cube = new THREE.Mesh(geo, mat); SPACE.platform.add(cube); camStock = cube; } camStock.scale.x = sd.camStockX; camStock.scale.y = sd.camStockY; camStock.scale.z = sd.camStockZ; camStock.position.z = sd.camStockZ / 2; camStock.material.visible = settings.mode === 'CAM'; camTopZ = sd.camStockZ - sd.camZTopOffset; camUpdateWidgetZ(); SPACE.update(); } else if (camStock) { SPACE.platform.remove(camStock); SPACE.update(); camStock = null; camTopZ = 0; camUpdateWidgetZ(); } } function updateFields() { updateFieldsFromSettings(settings.device); updateFieldsFromSettings(settings.process); updateFieldsFromSettings(settings.layers); } function updateSettings() { updateSettingsFromFields(settings.device); updateSettingsFromFields(settings.process); updateSettingsFromFields(settings.layers, true); saveSettings(); updateCamStock(); } function saveSettings(nocam) { // remove settings invalid for a given mode (cleanup) cull(settings, settingsDefault); switch (settings.mode) { case 'FDM': cull(settings.device, sf.fdm.d); cull(settings.process, sf.fdm.p); break; case 'CAM': cull(settings.device, sf.cam.d); cull(settings.process, sf.cam.p); break; case 'LASER': cull(settings.device, sf.laser.d); cull(settings.process, sf.laser.p); settings.cdev.LASER = settings.device; break; } cull(settings.cdev.FDM, sf.fdm.d); cull(settings.cdev.CAM, sf.cam.d); // store camera view var view = SPACE.view.save(); if (view.left || view.up) settings.controller.view = view; SDB.setItem('ws-settings', JSON.stringify(settings)); } function saveWorkspace() { saveSettings(); var newWidgets = [], oldWidgets = js2o(SDB.getItem('ws-widgets'), []); forWidgets(function(widget) { newWidgets.push(widget.id); oldWidgets.remove(widget.id); widget.saveState(); }); SDB.setItem('ws-widgets', o2js(newWidgets)); oldWidgets.forEach(function(wid) { Widget.deleteFromState(wid); }); } function loadFiles(files) { for (var i=0; i 0, isgcode = lower.indexOf(".gcode") > 0 || lower.indexOf(".nc") > 0; reader.file = files[i]; reader.onloadend = function (e) { if (isstl) platformAdd( newWidget() .loadVertices(new MOTO.STL().parse(e.target.result)) .saveToCatalog(e.target.file.name) ); if (isgcode) loadGCode(e.target.result); }; reader.readAsBinaryString(reader.file); } } function loadFile() { $('load-file').onchange = function(event) { DBUG.log(event); loadFiles(event.target.files); }; $('load-file').click(); // alert("drag/drop STL files onto platform to import\nreload page to return to last saved state"); } // kiri api function getSettings() { return settings; } // kiri api function putSettings(newset) { settings = newset; saveSettings() restoreWorkspace(null, true); } function updatePlatformSize() { var dev = settings.device, width, depth, height = Math.round(Math.max(dev.bedHeight, dev.bedWidth/100, dev.bedDepth/100)); SPACE.platform.setGZOff(height/2 - 0.1); SPACE.platform.setSize( width = parseInt(dev.bedWidth), depth = parseInt(dev.bedDepth), height ); SPACE.platform.setHidden(width > 500 || depth > 500); } function restoreWorkspace(ondone, skipwidgets) { var loaded = 0, toload = ls2o('ws-widgets',[]), newset = ls2o('ws-settings'), camera = ls2o('ws-camera'), isFDM = (newset || settings).mode === 'FDM'; if (newset) { fillMissingSettings(settingsDefault, newset); settings = newset; // override camera from settings if (settings.controller.view) { camera = settings.controller.view; SDB.removeItem('ws-camera'); UI.reverseZoom.checked = settings.controller.reverseZoom; } // update/integrate old settings ["FDM","CAM","LASER"].forEach(function(mode) { var key = 'ws-settings-'+mode, oset = ls2o(key, settings); SDB.removeItem(key); SDB.removeItem('ws-camera-'+mode); // create default settings if missing if (!settings.cproc[mode]) { // merge bed into device set(oset.device, oset.bed); // merge output into process set(oset.process, oset.output); settings.cproc[mode] = "default"; settings.sproc[mode].default = oset.process; } // copy device into cdev cache if missing if (!settings.cdev[mode]) settings.cdev[mode] = oset.device; // prefer tools object from CAM settings if (mode === 'CAM' && oset.tools) { settings.tools = oset.tools; } // restore per-mode filter settings.filter[mode] = oset.filter[mode]; }); // merge custom filters from localstorage into settings localFilters.forEach(function(fname) { var fkey = "gcode-filter-"+fname, ov = ls2o(fkey); if (ov) settings.devices[fname] = ov; SDB.removeItem(fkey) }); SDB.removeItem(localFilterKey); // save updated settings saveSettings(); } updateFields(); updatePlatformSize(); updateCamStock(); SPACE.view.reset(); if (camera) SPACE.view.load(camera); else setTimeout(SPACE.view.home, 100); if (skipwidgets) return; forWidgets(function(widget) { platformDelete(widget); }); toload.forEach(function(widgetid) { Widget.loadFromState(widgetid, function(widget) { if (widget) platformAdd(widget, 0, isFDM); if (++loaded === toload.length) { widgetDeselect(); if (ondone) ondone(); } }, isFDM); }); return toload.length > 0; } function clearWorkspace() { // free up worker cache/mem KIRI.work.clear(); platformSelectAll(); platformDelete(selectedMeshes); } function modalShowing() { var showing = false; ["modal","catalog","devices","tools"].forEach(function(dialog) { showing = showing || UI[dialog].style.display === 'block'; }); return showing; } function showModal(which) { UI.modal.style.display = 'block'; ["print","help"].forEach(function(modal) { UI[modal].style.display = (modal === which ? 'block' : 'none'); }); } function hideDialog() { showDialog(null); } function showDialog(which, force) { ["catalog","devices","tools","settings"].forEach(function(dialog) { var style = UI[dialog].style; style.display = (dialog === which && (force || style.display !== 'block') ? 'block' : 'none'); }); } function showCatalog() { showDialog("catalog"); } function loadNamedSetting(e) { var name = e ? e.target.getAttribute("load") : settings.cproc[getMode()], load = settings.sproc[getMode()][name]; if (!load) return; for (var k in load) { if (!load.hasOwnProperty(k)) continue; settings.process[k] = load[k]; } settings.process.processName = name; settings.cproc[getMode()] = name; updateFields(); hideDialog(); updateSettings(); if (e) triggerSettingsEvent(); } function deleteNamedSetting(e) { var name = e.target.getAttribute("del"); delete settings.sproc[getMode()][name]; updateSettingsList(); saveSettings(); triggerSettingsEvent(); } function updateSettingsList() { var list = [], s = settings, sp = s.sproc[getMode()] || {}, table = UI.settingsList; table.innerHTML = ''; for (var k in sp) { if (sp.hasOwnProperty(k)) list.push(k); } list.sort().forEach(function(sk) { var row = DOC.createElement('div'), load = DOC.createElement('button'), del = DOC.createElement('button'), name = sk; load.setAttribute('load', sk); load.onclick = loadNamedSetting; load.appendChild(DOC.createTextNode(sk)); if (sk == settings.process.processName) { load.setAttribute('class', 'selected') } del.setAttribute('del', sk); del.setAttribute('title', "remove '"+sk+"'"); del.onclick = deleteNamedSetting; del.appendChild(DOC.createTextNode('x')); row.setAttribute("class", "flow-row"); row.appendChild(load); row.appendChild(del); table.appendChild(row); }); onControlResize(); } function showSettings() { updateSettingsList(); showDialog("settings"); } function onWindowResize() { onControlResize(); } function onControlResize() { var left = UI.ctrlLeft.getBoundingClientRect(), right = UI.ctrlRight.getBoundingClientRect(); UI.catalog.style.left = (left.width + 5) + 'px'; UI.devices.style.left = (left.width + 5) + 'px'; UI.tools.style.left = (left.width + 5) + 'px'; UI.settings.style.right = (right.width + 5) + 'px'; } function hideModal() { UI.modal.style.display = 'none'; } function showSerial(toggle) { if (MODE !== MODES.CAM) return; hideModal(); if (toggle === true) { KIRI.serial.toggle(); } else { KIRI.serial.show(); } STATS.add('d-sender'); } function showHelpLocal() { showHelp("/kiri/help-kiri.html"); } function showHelp(local) { hideDialog(); if (!local) { WIN.open("//wiki.grid.space/wiki/Kiri:Moto", "_help"); STATS.add('d-help'); return; } ajax(local, function(html) { UI.help.innerHTML = html; $('help-close').onclick = hideModal; $('kiri-version').innerHTML = 'version '+kiri.version+""; showModal('help'); STATS.add('d-help'); }); } function takeFocus(el) { DOC.activeElement.blur(); el = [ el || DOC.body, UI.ctrlLeft, UI.container, UI.assets, UI.control, UI.modeFDM, UI.reverseZoom, UI.viewModelOpacity, DOC.body ]; for (var es, i=0; i 0) showLayerValue--; else if (delta < 0) showLayerValue++; showSlices(); }); SPACE.platform.onMove(saveSettings); set(UI, { container: container, ctrlLeft: $('control-left'), ctrlRight: $('control-right'), layerView: $('layer-view'), layerSlider: $('layer-slider'), assets: assets, control: control, modal: $('modal'), print: $('print'), help: $('help'), devices: $('devices'), deviceAdd: $('device-add'), deviceDelete: $('device-del'), deviceSave: $('device-save'), deviceClose: $('device-close'), deviceSelect: $('device-select'), device: UC.newGroup("device", $('device')), deviceName: UC.newInput("name", {size:20}), setDeviceFilament: UC.newInput("filament", {title:"diameter in millimeters", convert:UC.toFloat, modes:FDM}), setDeviceNozzle: UC.newInput("nozzle", {title:"diameter in millimeters", convert:UC.toFloat, modes:FDM}), setDeviceWidth: UC.newInput("bed width", {title:"millimeters", convert:UC.toInt}), setDeviceDepth: UC.newInput("bed depth", {title:"millimeters", convert:UC.toInt}), setDeviceHeight: UC.newInput("max height", {title:"max build height\nin millimeters", convert:UC.toInt, modes:FDM}), setDeviceMaxSpindle: UC.newInput("max spindle rpm", {title:"max spindle speed\n0 to disable", convert:UC.toInt, modes:CAM}), setDeviceExtrusion: UC.newBoolean("extrusion absolute", onBooleanClick, {title:"extrusion moves absolute"}), setDeviceOrigin: UC.newBoolean("origin center", onBooleanClick, {title:"bed origin center"}), setDeviceOriginTop: UC.newBoolean("origin top", onBooleanClick, {title:"part z origin top", modes:CAM}), setDevice: UC.newGroup("gcode", $('device')), setDeviceFan: UC.newInput("fan power", {title:"set cooling fan power", modes:FDM, size:15}), setDeviceTrack: UC.newInput("progress", {title:"output on each % progress", modes:FDM, size:15}), setDeviceLayer: UC.newInput("layer", {title:"output at each layer change", modes:FDM, size:14, height: 2}), setDeviceToken: UC.newBoolean("token spacing", null, {title:"gcode token spacing", modes:CAM}), setDeviceStrip: UC.newBoolean("strip comments", null, {title:"strip gcode comments", modes:CAM}), setDeviceFExt: UC.newInput("file ext", {title:"file name exension", modes:CAM, size:5}), setDeviceDwell: UC.newInput("dwell", {title:"gcode dwell script", modes:CAM, size:14, height:2}), setDeviceChange: UC.newInput("tool change", {title:"tool change script", modes:CAM, size:14, height:2}), setDevicePre: UC.newInput("header", {title:"gcode header script", modes:FDM_CAM, size:14, height:3}), setDevicePost: UC.newInput("footer", {title:"gcode footer script", modes:FDM_CAM, size:14, height:3}), tools: $('tools'), toolsSave: $('tools-save'), toolsClose: $('tools-close'), toolSelect: $('tool-select'), toolAdd: $('tool-add'), toolDelete: $('tool-del'), toolType: $('tool-type'), toolName: $('tool-name'), toolNum: $('tool-num'), toolFluteDiam: $('tool-fdiam'), toolFluteLen: $('tool-flen'), toolShaftDiam: $('tool-sdiam'), toolShaftLen: $('tool-slen'), toolMetric: $('tool-metric'), catalog: $('catalog'), catalogBody: $('catalogBody'), catalogList: $('catalogList'), settings: $('settings'), settingsBody: $('settingsBody'), settingsList: $('settingsList'), layerID: $('layer-id'), layerSpan: $('layer-span'), layerRange: $('layer-range'), loading: $('loading').style, progress: $('progress').style, prostatus: $('prostatus'), selection: $('selection'), selWidth: $('sel_width'), selHeight: $('sel_height'), selDepth: $('sel_depth'), scaleX: $('scale_x'), scaleY: $('scale_y'), scaleZ: $('scale_z'), scaleUniform: $('scale_uni'), mode: UC.newGroup('mode', assets), modeTable: UC.newTableRow([ [ UI.modeFDM = UC.newButton("FDM Printing", function() { setMode('FDM',null,updatePlatformSize) }), ],[ UI.modeLASER = UC.newButton("Laser Cutting", function() { setMode('LASER',null,updatePlatformSize) }), ],[ UI.modeCAM = UC.newButton("CNC Milling", function() { setMode('CAM',null,updatePlatformSize) }, {id:"modeCAM"}), ] ]), system: UC.newGroup('setup'), sysTable: UC.newTableRow([ [ UI.setupDevices = UC.newButton("Devices", showDevices, {modes:FDM_CAM}) ],[ UI.setupTools = UC.newButton('Tools', showTools, {modes:CAM}), ],[ UI.helpButton = UC.newButton("Help", showHelp) ] ]), wsFunc: UC.newGroup('function'), wsFuncTable: UC.newTableRow([ [ UC.newButton("Import", function() { KIRI.api.import() }), UI.import = UC.newButton("+") ],[ UI.modeArrange = UC.newButton("Arrange", layoutPlatform), ],[ UI.modeSlice = UC.newButton("Slice", prepareSlices) ],[ UI.modePreview = UC.newButton("Preview", preparePrint), ],[ UI.modeExport = UC.newButton("Export", exportPrint) ] ]), workspace: UC.newGroup('platform'), wsTable: UC.newTableRow([ [ UI.saveButton = UC.newButton("Save", saveWorkspace), ],[ UC.newButton("Clear", clearWorkspace) ] ]), camera: UC.newGroup('view'), camTable: UC.newTableRow([ [ UC.newButton("home", SPACE.view.home), UC.newButton("reset", SPACE.view.reset) ],[ UC.newButton("top", SPACE.view.top), UC.newButton("front", SPACE.view.front), ],[ UC.newButton("left", SPACE.view.left), UC.newButton("right", SPACE.view.right) ] ]), viewOpt: UC.newTableRow([[ UI.reverseZoom = UC.newBoolean(null, invertMouse, {title:"invert mouse\nscroll zoom"}), UI.viewModelOpacity = UC.newRange(null, {title:"change model opacity"}) ]]), layers: UC.setGroup($("layers")), layerOutline: UC.newBoolean("outline", onBooleanClick, {modes:LOCAL ? ALL : FDM_LASER}), layerTrace: UC.newBoolean("trace", onBooleanClick, {modes:FDM_LASER}), layerFacing: UC.newBoolean("facing", onBooleanClick, {modes:CAM}), layerRough: UC.newBoolean("roughing", onBooleanClick, {modes:CAM}), layerFinish: UC.newBoolean("finishing", onBooleanClick, {modes:CAM}), layerFinishX: UC.newBoolean("finish x", onBooleanClick, {modes:CAM}), layerFinishY: UC.newBoolean("finish y", onBooleanClick, {modes:CAM}), layerDelta: UC.newBoolean("delta", onBooleanClick, {modes:FDM}), layerSolid: UC.newBoolean("solids", onBooleanClick, {modes:FDM}), layerFill: UC.newBoolean("solid fill", onBooleanClick, {modes:FDM}), layerSparse: UC.newBoolean("sparse fill", onBooleanClick, {modes:FDM}), layerSupport: UC.newBoolean("support", onBooleanClick, {modes:FDM}), layerPrint: UC.newBoolean("print", onBooleanClick), settingsGroup: UC.newGroup("settings", control), settingsTable: UC.newTableRow([ [ UI.settingsLoad = UC.newButton("load", settingsLoad), UI.settingsSave = UC.newButton("save", settingsSave) ] ]), platform: UC.newGroup("build area", control, {modes:LASER}), bedWidth: UC.newInput("width", {title:"millimeters", convert:UC.toInt, modes:LASER}), bedDepth: UC.newInput("depth", {title:"millimeters", convert:UC.toInt, modes:LASER}), process: UC.newGroup("process", control, {modes:FDM_LASER}), // 3d print sliceHeight: UC.newInput("slice height", {title:"millimeters", convert:UC.toFloat, modes:FDM}), sliceShells: UC.newInput("shell count", {convert:UC.toInt, modes:FDM}), sliceShellSpacing: UC.newInput("shell spacing", {title:"as a percentage of nozzle width\n< 1.0 causes shell overlap\nrecommended 0.85 - 1.0", convert:UC.toFloat, bound:UC.bound(0.5,1.0), modes:FDM}), sliceFillOverlap: UC.newInput("fill overlap", {title:"overlap with shell\nas % of nozzle width\nhigher bonds better\n0.0 - 1.0", convert:UC.toFloat, bound:UC.bound(0.0,2.0), modes:FDM}), sliceFillSpacing: UC.newInput("fill spacing", {title:"for solid fill areas\nas a percentage of nozzle width\n< 1.0 causes fill overlap\nrecommended 0.85 - 1.0", convert:UC.toFloat, bound:UC.bound(0.0,2.0), modes:FDM}), sliceFillAngle: UC.newInput("fill angle", {title:"base angle in degrees", convert:UC.toFloat, modes:FDM}), sliceFillSparse: UC.newInput("fill ratio", {title:"for infill areas\n0.0 - 1.0", convert:UC.toFloat, bound:UC.bound(0.0,1.0), modes:FDM}), sliceSolidMinArea: UC.newInput("solid area", {title:"minimum area (cm^2)\nrequired to keep solid\nmust be > 0.1", convert:UC.toFloat, modes:FDM}), sliceSolidLayers: UC.newInput("solid layers", {title:"flat area fill projections\nbased on layer deltas", convert:UC.toInt, modes:FDM}), sliceBottomLayers: UC.newInput("base layers", {title:"bottom solid layer count", convert:UC.toInt, modes:FDM}), sliceTopLayers: UC.newInput("top layers", {title:"top solid layer count", convert:UC.toInt, modes:FDM}), sliceVase: UC.newBoolean("vase mode", onBooleanClick, {modes:FDM}), // laser laserOffset: UC.newInput("cut offset", {title:"millimeters\nadjust for beam width", convert:UC.toFloat, modes:LASER}), laserSliceHeight: UC.newInput("layer height", {title:"millimeters\n0 = auto/detect", convert:UC.toFloat, modes:LASER}), // cam roughing: UC.newGroup("roughing", null, {modes:CAM}), roughingTool: UC.newSelectField("tool", {modes:CAM}), roughingSpindle: UC.newInput("spindle rpm", {title:"spindle speed rpm", convert:UC.toInt, modes:CAM}), roughingOver: UC.newInput("step over", {title:"0.1 - 1.0\npercentage of\ntool diameter", convert:UC.toFloat, bound:UC.bound(0.1,1.0), modes:CAM}), roughingDown: UC.newInput("step down", {title:"step down depth for\neach roughing pass\nin millimeters\n0 to disable", convert:UC.toFloat, modes:CAM}), roughingSpeed: UC.newInput("feed rate", {title:"max speed while cutting\nmillimeters / minute", convert:UC.toInt, modes:CAM}), roughingPlunge: UC.newInput("plunge rate", {title:"max speed on z axis\nmillimeters / minute", convert:UC.toInt, modes:CAM}), roughingStock: UC.newInput("leave stock", {title:"horizontal offset from vertical faces\nstock to leave for finishing pass\nin millimeters", convert:UC.toFloat, modes:CAM}), roughingOn: UC.newBoolean("enable", onBooleanClick, {modes:CAM}), // cam finishing: UC.newGroup("finishing", null, {modes:CAM}), finishingTool: UC.newSelectField("tool", {modes:CAM}), finishingSpindle: UC.newInput("spindle rpm", {title:"spindle speed rpm", convert:UC.toInt, modes:CAM}), finishingOver: UC.newInput("step over", {title:"0.05 - 1.0\npercentage of\ntool diameter", convert:UC.toFloat, bound:UC.bound(0.05,1.0), modes:CAM}), finishingDown: UC.newInput("step down", {title:"step down depth for\neach roughing pass\nin millimeters\n0 to disable", convert:UC.toFloat, modes:CAM}), finishingAngle: UC.newInput("max angle", {title:"angles greater than this\nare considered vertical", convert:UC.toFloat, bound:UC.bound(45,90), modes:CAM}), finishingSpeed: UC.newInput("feed rate", {title:"max speed while cutting\nmillimeters / minute", convert:UC.toInt, modes:CAM}), finishingPlunge: UC.newInput("plunge rate", {title:"max speed on z axis\nmillimeters / minute", convert:UC.toInt, modes:CAM}), finishingOn: UC.newBoolean("waterline", onBooleanClick, {title:"contour finishing\ndisabled when pocketing", modes:CAM}), finishingXOn: UC.newBoolean("linear x", onBooleanClick, {title:"linear x-axis finishing", modes:CAM}), finishingYOn: UC.newBoolean("linear y", onBooleanClick, {title:"linear y-axis finishing", modes:CAM}), finishCurvesOnly: UC.newBoolean("curves only", onBooleanClick, {title:"limit linear cleanup\nto curved surfaces\nto reduce time", modes:CAM}), // cam drilling: UC.newGroup("drilling", null, {modes:CAM}), drillTool: UC.newSelectField("tool", {modes:CAM}), drillSpindle: UC.newInput("spindle rpm", {title:"spindle speed rpm", convert:UC.toInt, modes:CAM}), drillDown: UC.newInput("plunge per", {title:"max plunge between\ndwell periods\nin millimeters\n0 to disable", convert:UC.toFloat, modes:CAM}), drillDownSpeed: UC.newInput("plunge rate", {title:"plunge rate\nin millimeters / minute\n0 to disable", convert:UC.toFloat, modes:CAM}), drillDwell: UC.newInput("dwell time", {title:"dwell time\nbetween plunges in\nin milliseconds", convert:UC.toFloat, modes:CAM}), drillLift: UC.newInput("drill lift", {title:"lift between plunges\nafter dwell period\nin millimeters\n0 to disable", convert:UC.toFloat, modes:CAM}), drillingOn: UC.newBoolean("enable", onBooleanClick, {modes:CAM}), // cam tabs camTabs: UC.newGroup("cutout tabs", null, {modes:CAM}), camTabsWidth: UC.newInput("width", {title:"width in millimeters\nperpendicular to part", convert:UC.toFloat, bound:UC.bound(1,100), modes:CAM}), camTabsHeight: UC.newInput("height", {title:"height in millimeters\nfrom part bottom", convert:UC.toFloat, bound:UC.bound(1,100), modes:CAM}), camTabsOn: UC.newBoolean("enable", onBooleanClick, {title:"enable or disable tabs\ntab generation skipped when\npocket only mode enabled", modes:CAM}), output: UC.newGroup("output"), outputTileSpacing: UC.newInput("spacing", {title:"millimeters\ndistance between layer output", convert:UC.toInt, modes:LASER}), outputTileScaling: UC.newInput("scaling", {title:"multiplier (0.1 to 100)", convert:UC.toInt, bound:UC.bound(0.1,100), modes:LASER}), outputLaserPower: UC.newInput("power", {title:"0 - 100 %", convert:UC.toInt, bound:UC.bound(1,100), modes:LASER}), outputLaserSpeed: UC.newInput("speed", {title:"millimeters / minute", convert:UC.toInt, modes:LASER}), outputBedTemp: UC.newInput("bed temp", {title:"degrees celsius", convert:UC.toInt, modes:FDM}), outputTemp: UC.newInput("nozzle temp", {title:"degrees celsius", convert:UC.toInt, modes:FDM}), outputFeedrate: UC.newInput("print speed", {title:"print move max speed\nmillimeters / minute", convert:UC.toInt, modes:FDM}), outputFinishrate: UC.newInput("finish speed", {title:"outermost shell speed\nmillimeters / minute", convert:UC.toInt, modes:FDM}), outputSeekrate: UC.newInput("move speed", {title:"non-print move speed\nmillimeters / minute", convert:UC.toInt, modes:FDM}), // outputShellSpeed: UC.newInput("shell speed", {title:"speed multiplier\n0.1 - 2.0", convert:UC.toFloat, bound:UC.bound(0.1,2.0), modes:FDM}), outputShellMult: UC.newInput("shell factor", {title:"extrusion multiplier\n0.0 - 2.0", convert:UC.toFloat, bound:UC.bound(0.0,2.0), modes:FDM}), outputFillMult: UC.newInput("solid factor", {title:"extrusion multiplier\n0.0 - 2.0", convert:UC.toFloat, bound:UC.bound(0.0,2.0), modes:FDM}), outputSparseMult: UC.newInput("infill factor", {title:"extrusion multiplier\n0.0 - 2.0", convert:UC.toFloat, bound:UC.bound(0.0,2.0), modes:FDM}), outputCooling: UC.newBoolean("cooling", onBooleanClick, {title: "enable cooling fan\nafter first layer", modes:FDM}), // cam camTolerance: UC.newInput("tolerance", {title:"surface precision\nin millimeters", convert:UC.toFloat, bound:UC.bound(0.05,1.0), modes:CAM}), camZTopOffset: UC.newInput("z top offset", {title:"offset from stock surface\nto top face of part\nin millimeters", convert:UC.toFloat, modes:CAM}), camZBottom: UC.newInput("z bottom", {title:"offset from part bottom\nto limit cutting depth\nin millimeters", convert:UC.toFloat, modes:CAM}), camZClearance: UC.newInput("z clearance", {title:"travel offset from z top\nin millimeters", convert:UC.toFloat, bound:UC.bound(1,100), modes:CAM}), camPocketOnly: UC.newBoolean("pocket only", onBooleanClick, {title:"constrain to\npart boundaries", modes:CAM}), // camEaseDown: UC.newBoolean("ease down", onBooleanClick, {title:"rough plunge cuts\nwill spiral down", modes:CAM}), camDepthFirst: UC.newBoolean("depth first", onBooleanClick, {title:"optimize pocket cuts\nwith depth priority", modes:CAM}), outputClockwise: UC.newBoolean("clockwise", onBooleanClick, {title:"waterline milling direction", modes:CAM}), // cam stock camStock: UC.newGroup("stock", null, {modes:CAM}), camStockX: UC.newInput("width", {title:"width in millimeters\n0 defaults to part size", convert:UC.toFloat, bound:UC.bound(0,9999), modes:CAM}), camStockY: UC.newInput("depth", {title:"depth in millimeters\n0 defaults to part size", convert:UC.toFloat, bound:UC.bound(0,9999), modes:CAM}), camStockZ: UC.newInput("height", {title:"depth in millimeters\n0 defaults to part size", convert:UC.toFloat, bound:UC.bound(0,9999), modes:CAM}), outputOriginCenter: UC.newBoolean("origin center", onBooleanClick, {modes:CAM_LASER}), camOriginTop: UC.newBoolean("origin top", onBooleanClick, {modes:CAM}), support: UC.newGroup("supports", null, {modes:FDM}), sliceSupportDensity: UC.newInput("density", {title:"0.0 - 1.0\nrecommended 0.15\n0 to disable", convert:UC.toFloat, bound:UC.bound(0.05,1.0), modes:FDM}), sliceSupportSize: UC.newInput("pillar size", {title:"width in millimeters", bound:UC.bound(1.0,200.0), convert:UC.toFloat, modes:FDM}), sliceSupportOffset: UC.newInput("part offset", {title:"millimeters\noffset from part", bound:UC.bound(0.0,200.0), convert:UC.toFloat, modes:FDM}), sliceSupportSpan: UC.newInput("max bridge", {title:"span length that\ntriggers support\nin millimeters", bound:UC.bound(0.0,200.0), convert:UC.toFloat, modes:FDM}), sliceSupportArea: UC.newInput("min area", {title:"min area for a\nsupport column\nin millimeters", bound:UC.bound(0.1,200.0), convert:UC.toFloat, modes:FDM}), sliceSupportExtra: UC.newInput("expand", {title:"expand support area\nbeyond part boundary\nin millimeters", bound:UC.bound(0.0,200.0), convert:UC.toFloat, modes:FDM}), sliceSupportEnable: UC.newBoolean("enable", onBooleanClick, {modes:FDM}), firstLayer: UC.newGroup("first layer", null, {modes:FDM}), firstSliceHeight: UC.newInput("slice height", {title:"in millimeters\nshould be >= slice height", convert:UC.toFloat, modes:FDM}), firstLayerRate: UC.newInput("print speed", {title:"print move max speed\nmillimeters / minute", convert:UC.toFloat, modes:FDM}), outputBrimCount: UC.newInput("skirt count", {title:"number of skirts", convert:UC.toInt, modes:FDM}), outputBrimOffset: UC.newInput("skirt offset", {title:"millimeters", convert:UC.toFloat, modes:FDM}), outputRaft: UC.newBoolean("raft", onBooleanClick, {title:"create a raft under the\nmodel for better adhesion", modes:FDM}), advanced: UC.newGroup("advanced", null, {modes:FDM}), // outputRetractOver: UC.newInput("retract over", {title:"move threshold that\ntriggers retraction\n0 to disable", convert:UC.toFloat, modes:FDM}), outputRetractDist: UC.newInput("retract dist", {title:"amount to retract filament", convert:UC.toFloat, modes:FDM}), outputRetractSpeed: UC.newInput("retract rate", {title:"speed of filament\nretraction in mm/s", convert:UC.toInt, modes:FDM}), outputRetractDwell: UC.newInput("retract dwell", {title:"time between re-engaging\nfilament and movement\nin milliseconds", convert:UC.toInt, modes:FDM}), outputWipeSpeed: UC.newInput("wipe speed", {title:"speed while wiping\nnozzle in mm/s", convert:UC.toInt, modes:FDM}), outputWipeDistance: UC.newInput("wipe distance", {title:"distance to wipe nozzle after\neach enclosed island completes\nin millimeters. 0 to disable", convert:UC.toInt, modes:FDM}), outputShortPoly: UC.newInput("short outline", {title:"poly perimeter length\ntriggers short slowdown\nin millimeters", bound:UC.bound(0,200), convert:UC.toFloat, modes:FDM}), outputShortDistance: UC.newInput("short segment", {title:"segment length cutoff\nfor short segments\nin millimeters", bound:UC.bound(0,200), convert:UC.toFloat, modes:FDM}), outputShortFactor: UC.newInput("short factor", {title:"max speed reduction factor\nfor short segments\nas % of print speed", bound:UC.bound(0.05,1), convert:UC.toFloat, modes:FDM}), outputFinishFactor: UC.newInput("finish factor", {title:"% of nozzle diameter to\nshorten finish path by\nvalues of 0-1", bound:UC.bound(0.0,1), convert:UC.toFloat, modes:FDM}), outputOuterFirst: UC.newBoolean("outer first", onBooleanClick, {title: "output outermost shell first", modes:FDM}), detectThinWalls: UC.newBoolean("thin wall", onBooleanClick, {title: "thin wall detection\nexperimental", modes:FDM}) }); function toolUpdate(a,b,c) { DBUG.log(['toolUpdate',a,b,c]) } function invertMouse() { settings.controller.reverseZoom = UI.reverseZoom.checked; SPACE.view.setZoom(settings.controller.reverseZoom, settings.controller.zoomSpeed); saveSettings(); } function onBooleanClick() { updateSettings(); showSlices(); } function clearSelectedText() { var sel = WIN.getSelection(); if (sel && sel.rangeCount) sel.collapseToStart(); } function keyUpHandler(evt) { switch (evt.keyCode) { // escape case 27: // clear text selection clearSelectedText(); // dismiss modals hideModal(); // deselect widgets widgetDeselect(); // hide all dialogs hideDialog(); // cancel slicing KIRI.work.cancel(); break; } return false; } function inputHasFocus() { var active = DOC.activeElement; return active && (active.nodeName === "INPUT" || active.nodeName === "TEXTAREA"); } function textAreaHasFocus() { var active = DOC.activeElement; return active && active.nodeName === "TEXTAREA"; } function inputSize() { return parseInt(DOC.activeElement.size); } function keyDownHandler(evt) { if (modalShowing()) return false; var move = evt.altKey ? 5 : 0, deg = move ? 0 : -Math.PI / (evt.shiftKey ? 36 : 2); switch (evt.keyCode) { case 8: // delete if (inputHasFocus()) return false; if (selectedMeshes.length > 0) { platformDelete(selectedMeshes); } evt.preventDefault(); break; case 37: // left arrow if (inputHasFocus()) return false; // if (selectedMeshes.length === 0) return; if (deg) rotateSelection(0, 0, -deg); if (move > 0) moveSelection(-move, 0, 0); evt.preventDefault(); break; case 39: // right arrow if (inputHasFocus()) return false; // if (selectedMeshes.length === 0) return; if (deg) rotateSelection(0, 0, deg); if (move > 0) moveSelection(move, 0, 0); evt.preventDefault(); break; case 38: // up arrow if (inputHasFocus()) return false; if (evt.metaKey) return setVisibleLayer(showLayerValue+1); // if (selectedMeshes.length === 0) return; if (deg) rotateSelection(deg, 0, 0); if (move > 0) moveSelection(0, move, 0); evt.preventDefault(); break; case 40: // down arrow if (inputHasFocus()) return false; if (evt.metaKey) return setVisibleLayer(showLayerValue-1); // if (selectedMeshes.length === 0) return; if (deg) rotateSelection(-deg, 0, 0); if (move > 0) moveSelection(0, -move, 0); evt.preventDefault(); break; case 65: // 'a' for select all if (evt.metaKey) { if (inputHasFocus()) return false; evt.preventDefault(); widgetDeselect(); platformSelectAll(); clearSelectedText(); } break; case 83: // 's' for save workspace if (evt.ctrlKey) { evt.preventDefault(); saveSettings(); log("settings saved"); } else if (evt.metaKey) { evt.preventDefault(); saveWorkspace(); } break; case 76: // 'l' for restore workspace if (evt.metaKey) { evt.preventDefault(); restoreWorkspace(); } break; } } function cca(c) { return c.charCodeAt(0); } function keyHandler(evt) { var handled = true, style, sel, i, m, bb, ncc = evt.charCode - 48; if (modalShowing()) return false; if (inputHasFocus() && (inputSize() > 10 || (ncc >= 0 && ncc <= 9))) { return false; } switch (evt.charCode) { case cca('`'): showSlices(0); break; case cca('0'): showSlices(showLayerMax); break; case cca('1'): // toggle control left if (evt.ctrlKey) { style = UI.ctrlLeft.style; style.display = style.display === 'none' ? 'block' : 'none'; } else { showSlices(showLayerMax/10); } break; case cca('2'): // toggle control right if (evt.ctrlKey) { style = UI.ctrlRight.style; style.display = style.display === 'none' ? 'block' : 'none'; } else { showSlices(showLayerMax*2/10); } break; case cca('3'): if (evt.ctrlKey) { style = !SPACE.platform.isHidden(); SPACE.platform.setHidden(style); SPACE.platform.showGrid(!style); SPACE.update(); } else { showSlices(showLayerMax*3/10); } break; case cca('4'): showSlices(showLayerMax*4/10); break; case cca('5'): showSlices(showLayerMax*5/10); break; case cca('6'): showSlices(showLayerMax*6/10); break; case cca('7'): showSlices(showLayerMax*7/10); break; case cca('8'): showSlices(showLayerMax*8/10); break; case cca('9'): showSlices(showLayerMax*9/10); break; case cca('?'): showHelpLocal(); break; case cca('Z'): // reset stored state if (confirm('clear all settings?')) { SDB.clear(); } break; case cca('C'): // refresh catalog CATALOG.refresh(); break; case cca('c'): // open control window showSerial(true); break; case cca('U'): // full settings url storeSettingsToServer(true); break; case cca('u'): // full settings url loadSettingsFromServer(prompt("settings id to load")); break; case cca('s'): // complete slice prepareSlices(); break; case cca('p'): // prepare print preparePrint(); break; case cca('P'): // position widget positionSelection(); break; case cca('R'): // position widget rotateInputSelection(); break; case cca('x'): // export print exportPrint(); break; case cca('e'): // devices showDevices(); break; case cca('o'): // tools showTools(); break; case cca('v'): // toggle single slice view mode UI.layerRange.checked = !UI.layerRange.checked; showSlices(); break; case cca('d'): // duplicate object sel = selectedMeshes.slice(); widgetDeselect(); for (i=0; i= 0) { UI.deviceSelect.selectedIndex = selectedIndex; selectDevice(selected); } else { UI.deviceSelect.selectedIndex = 0; selectDevice(devices[0]); } showDialog('devices', true); onWindowResize(); UI.deviceSelect.focus(); } function renderTools() { UI.toolSelect.innerHTML = ''; editTools.forEach(function(tool, index) { tool.order = index; var 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.toolMetric.checked = tool.metric; UI.toolType.selectedIndex = ( tool.type === 'endmill' ? 0 : tool.type === 'ballmill' ? 1 : -1 ); } function updateTool() { 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.metric = UI.toolMetric.checked; selectedTool.type = ['endmill','ballmill'][UI.toolType.selectedIndex]; renderTools(); UI.toolSelect.selectedIndex = selectedTool.order; editTools.changed = true; } function showTools() { if (MODE !== MODES.CAM) return; var selectedIndex = null; editTools = settings.tools.slice(); UI.toolsClose.onclick = function() { if (editTools.changed && !confirm("abandon changes?")) return; hideDialog(); }; UI.toolAdd.onclick = function() { editTools.push({ id: UTIL.time(), number: editTools.length, name: "new", type: "endmill", flute_diam: 0.25, flute_len: 1, shaft_diam: 0.25, shaft_len: 3, metric: false }); editTools.changed = true; renderTools(); UI.toolSelect.selectedIndex = editTools.length-1; selectTool(editTools[editTools.length-1]); }; UI.toolDelete.onclick = function() { editTools.remove(selectedTool); editTools.changed = true; renderTools(); }; UI.toolsSave.onclick = function() { editTools.changed = false; if (selectedTool) updateTool(); settings.tools = editTools; saveSettings(); updateFields(); hideDialog(); triggerSettingsEvent(); }; renderTools(); if (editTools.length > 0) { selectTool(editTools[0]); UI.toolSelect.selectedIndex = 0; } else { UI.toolAdd.onclick(); } showDialog('tools'); UI.toolSelect.focus(); } function showDevices() { if (MODE === MODES.LASER || deviceLock) return; setViewMode(VIEWS.ARRANGE); ajax("/api/filters-"+getMode().toLowerCase(), function(flvalue) { if (!flvalue) return; renderDevices(js2o(flvalue)); STATS.add('d-gcode'); }); } function dragOverHandler(evt) { evt.stopPropagation(); evt.preventDefault(); evt.dataTransfer.dropEffect = 'copy'; SPACE.platform.setColor(0x00ff00); } function dragLeave() { SPACE.platform.setColor(0x555555); } function dropHandler(evt) { evt.stopPropagation(); evt.preventDefault(); SPACE.platform.setColor(0x555555); var files = evt.dataTransfer.files, plate = files.length < 5 || confirm('add objects to table?'); if (plate) loadFiles(files); } function loadCatalogFile(e) { Widget.loadFromCatalog(e.target.getAttribute('load'), function(widget) { platformAdd(widget); hideDialog(); }); } function deleteCatalogFile(e) { CATALOG.deleteFile(e.target.getAttribute('del')); } function updateCatalog(files) { var table = UI.catalogList, list = []; table.innerHTML = ''; for (var 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 (var i=0; i 0; } }); function checkSeed(ondone) { // skip sample object load in onshape (or any script postload) if (!SDB[SEED] && !SETUP.s) { loadSTL("/obj/cube.stl", function(vert) { CATALOG.putFile("sample cube.stl", vert); SDB[SEED] = new Date().getTime(); platformComputeMaxZ(); SPACE.view.home(); setTimeout(saveWorkspace,500); ondone(); showHelpLocal(); }); return true; } return false; } function ondone() { widgetDeselect(); CATALOG.addFileListener(updateCatalog); SPACE.view.setZoom(settings.controller.reverseZoom, settings.controller.zoomSpeed); SPACE.platform.setZOff(0.2); if (SETUP.s) SETUP.s.forEach(function(lib) { var scr = DOC.createElement('script'); scr.setAttribute('async',true); scr.setAttribute('src','/code/'+lib+'.js'); DOC.body.appendChild(scr); STATS.add('l-'+lib); }); if (SETUP.ss) SETUP.ss.forEach(function(style) { var ss = DOC.createElement('link'); ss.setAttribute("type", "text/css"); ss.setAttribute("rel", "stylesheet"); ss.setAttribute("href", "/kiri/style-"+style+".css"); DOC.body.appendChild(ss); }); if (SETUP.v) SETUP.v.forEach(function(kv) { kv = kv.split('='); SDB.setItem(kv[0],kv[1]); }); // import octoprint settings from url if (SETUP.ophost) { OCTOPRINT = { host: SETUP.ophost[0], apik: SETUP.opkey ? SETUP.opkey[0] : '' }; SDB['octo-host'] = OCTOPRINT.host; SDB['octo-apik'] = OCTOPRINT.apik; console.log({octoprint:OCTOPRINT}); } // mode passed on url var SETMODE = SETUP.mode ? SETUP.mode[0] : null; // device name pass on url var DEVNAME = SETUP.dev ? SETUP.dev[0] : null; setMode(SETMODE || STARTMODE || settings.mode, SETMODE); setControlsVisible(true); updatePlatformSize(); takeFocus(); if (STATS.get('upgrade')) DBUG.log("kiri | version upgrade"); STATS.del('upgrade'); if (!SETUP.s) console.log("kiri | init main | "+kiri.version); STATS.set('seed', SDB[SEED]); STATS.add('init'); // updateActive(true); // init gcode sender KIRI.serial.init(); // place version number a couple of places to help users UI.helpButton.title = "version " + KIRI.version; // setup tab visibility watcher // DOC.addEventListener('visibilitychange', function() { document.title = document.hidden }); // ensure settings has gcode selectDevice(DEVNAME || currentDeviceName(), DEVNAME); // ensure field data propagation updateSettings(); // set initial layer slider size onControlResize(); // send init-done event sendOnEvent('init-done', STATS); // load settings provided in url hash loadSettingsFromServer(); } restoreWorkspace(ondone) || checkSeed(ondone) || ondone(); } // end init() SPACE.addEventListener(DOC, 'DOMContentLoaded', function () { init() }, false); SPACE.addEventListener(WIN, 'mousemove', function() { mouseMoved = true }); // prevent safari from exiting full screen mode DOC.onkeydown = function (evt) { if (evt.keyCode == 27) evt.preventDefault(); } // run optional module functions if (Array.isArray(self.kirimod)) { kirimod.forEach(function(mod) { mod(kiri.api) }); } })();