grid-apps-cmms/src/kiri/main.js
2021-12-27 10:14:21 -05:00

3360 lines
112 KiB
JavaScript

/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
(function () {
let MOTO = self.moto,
KIRI = self.kiri,
LOAD = self.load,
BASE = self.base,
UTIL = BASE.util,
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,
EVENT = KIRI.broker = new Broker(),
SDB = MOTO.KV,
ODB = KIRI.odb = new MOTO.Storage(SETUP.d ? SETUP.d[0] : 'kiri'),
// K3DB = KIRI.wdb = new MOTO.Storage('kiri3', { stores:["file","work"] }).init(),
SPACE = KIRI.space = MOTO.Space,
WIDGETS = KIRI.widgets = [],
CATALOG = KIRI.catalog = KIRI.openCatalog(ODB),
STATS = new Stats(SDB),
SEED = 'kiri-seed',
// ---------------
CONF = KIRI.conf,
MODES = CONF.MODES,
VIEWS = CONF.VIEWS,
clone = Object.clone,
settings = clone(CONF.template),
// ---------------
Widget = kiri.Widget,
newWidget = kiri.newWidget,
// ---------------
UI = {},
UC = KIRI.ui.prefix('kiri').inputAction(updateSettings),
MODE = MODES.FDM,
STACKS = KIRI.stacks,
DRIVER = undefined,
complete = {},
selectedMeshes = [],
localFilterKey ='kiri-gcode-filters',
localFilters = js2o(SDB.getItem(localFilterKey)) || [],
// ---------------
viewMode = VIEWS.ARRANGE,
local = SETUP.local,
topZD = 0,
topZ = 0,
busy = 0,
showFavorites = SDB.getItem('dev-favorites') === 'true',
alerts = [],
grouping = false,
saveTimer = null,
noop = () => {};
// add show() to catalog for API
CATALOG.show = showCatalog;
// patch broker for api backward compatibility
EVENT.on = (topic, listener) => {
EVENT.subscribe(topic, listener);
return EVENT;
};
const PMODES = {
SPEED: 1,
TOOLS: 2
};
const feature = {
seed: true, // seed profiles on first use
meta: true, // show selected widget metadata
alert_event: false, // emit alerts as events instead of display
controls: true, // show or not side menus
device_filter: undefined, // function to limit devices shown
drop_group: undefined, // optional array to group multi drop
drop_layout: true, // layout on new drop
hover: false, // when true fires mouse hover events
hoverAdds: false, // when true only searches widget additions
on_key: undefined, // function override default key handlers
on_load: undefined, // function override file drop loads
on_add_stl: undefined, // legacy override stl drop loads
work_alerts: true, // allow disabling work progress alerts
modes: [ "fdm", "sla", "cam", "laser" ], // enable device modes
pmode: PMODES.SPEED // preview modes
};
const selection = {
opacity: setOpacity,
move: moveSelection,
scale: scaleSelection,
rotate: rotateSelection,
mirror: mirrorSelection,
duplicate: duplicateSelection,
meshes: function() { return selectedMeshes.slice() },
widgets: function(orall) {
let sel = selectedMeshes.slice().map(m => m.widget);
return sel.length ? sel : orall ? WIDGETS.slice() : []
},
for_groups: forSelectedGroups,
for_meshes: forSelectedMeshes,
for_widgets: forSelectedWidgets,
update_bounds: updateSelectedBounds,
update_info: updateSelectedInfo,
delete: function() { platform.delete(selection.widgets()) },
export: exportSelection
};
const platform = {
add: platformAdd,
delete: platformDelete,
layout: platformLayout,
load: platformLoad,
load_stl: platformLoadSTL,
load_url: platformLoadURL,
deselect: platformDeselect,
select: platformSelect,
select_all: platformSelectAll,
selected_count: platformSelectedCount,
compute_max_z: platformComputeMaxZ,
update_origin: platformUpdateOrigin,
update_bounds: platformUpdateBounds,
update_size: platformUpdateSize,
update_top_z: platformUpdateTopZ,
update_selected: platformUpdateSelected,
update_speeds: updateSpeeds,
load_files: platformLoadFiles,
group: platformGroup,
group_done: platformGroupDone,
update: SPACE.platform.update,
set_font: SPACE.platform.setFont,
show_axes: SPACE.platform.showAxes,
show_volume: SPACE.platform.showVolume,
top_z: () => { return topZ },
clear: () => { clearWorkspace(); saveWorkspace(true) }
};
const color = {
wireframe: 0x444444,
wireframe_opacity: 0.25,
selected: [ 0xbbff00, 0xbbee00, 0xbbdd00, 0xbb9900 ],
deselected: [ 0xffff00, 0xffdd00, 0xffbb00, 0xff9900 ],
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 = {
shell: [
{ name: "in-out" },
{ name: "out-in" },
],
start: [
{ name: "last" },
{ name: "center" },
{ name: "origin" },
],
infill: [
{ name: "hex" },
{ name: "grid" },
// { name: "cubic" },
{ name: "linear" },
{ name: "triangle" },
{ name: "gyroid" },
{ name: "vase" }
],
units: [
{ name: "mm" },
{ name: "in" }
],
antialias: [
{ name: "1", id: 1 },
{ name: "2", id: 2 },
{ name: "4", id: 4 },
{ name: "8", id: 8 }
],
detail: [
{ name: "100" },
{ name: "75" },
{ name: "50" },
{ name: "25" },
],
linetype: [
{ name: "path" },
{ name: "flat" },
{ name: "line" }
],
filasrc: [
{ name: "direct" },
{ name: "palette3" }
],
animesh: [
{ name: "100" },
{ name: "200" },
{ name: "300" },
{ name: "400" },
{ name: "500" },
{ name: "600" },
{ name: "700" },
{ name: "800" },
{ name: "900" }
],
trace: [
{ name: "follow" },
{ name: "clear" }
],
traceoff: [
{ name: "center" },
{ name: "inside" },
{ name: "outside" }
],
zanchor: [
{ name: "top" },
{ name: "middle" },
{ name: "bottom" }
],
regaxis: [
{ name: "X" },
{ name: "Y" },
{ name: "-" }
],
regpoints: [
{ name: "2" },
{ name: "3" }
],
};
const tweak = {
line_precision: (v) => { API.work.config({base:{clipperClean: v}}) },
gcode_decimals: (v) => { API.work.config({base:{gcode_decimals: v}}) }
};
const devel = {
xray: (layers) => {
let proc = API.conf.get().process;
let size = proc.sliceHeight || 1;
layers = Array.isArray(layers) ? layers : [ layers ];
proc.xray = layers.map(l => l * size + size / 2);
API.function.slice();
}
};
const API = KIRI.api = {
ui: UI,
uc: UC,
sdb: SDB,
o2js: o2js,
js2o: js2o,
ajax: ajax,
clone: clone,
focus: () => {},
stats: STATS,
catalog: CATALOG,
busy: {
val: () => { return busy },
inc: () => { kiri.api.event.emit("busy", ++busy) },
dec: () => { kiri.api.event.emit("busy", --busy) }
},
conf: {
dbo: () => { return ls2o('ws-settings') },
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,
SECURE,
STACKS,
SPACE
},
devel,
doit: {
undo: noop, // set in do.js
redo: noop // set in do.js
},
var: {
layer_lo: 0,
layer_hi: 0,
layer_max: 0
},
device: {
code: currentDeviceCode,
get: currentDeviceName,
set: noop, // set during init
clone: noop // set during init
},
dialog: {
show: showModal,
hide: hideModal
},
help: {
show: showHelp,
file: showHelpFile
},
event: {
on: (t,l) => { return EVENT.on(t,l) },
emit: (t,m,o) => { EVENT.publish(t,m,o) },
bind: (t,m,o) => { return EVENT.bind(t,m,o) },
import: loadFile,
alerts: updateAlerts,
settings: triggerSettingsEvent
},
feature,
function: {
slice: prepareSlices,
print: preparePreview,
prepare: preparePreview,
animate: prepareAnimation,
export: prepareExport,
cancel: cancelWorker,
clear: KIRI.client.clear,
parse: loadCode
},
group: {
merge: groupMerge,
split: groupSplit,
},
hide: {
alert: function(rec,recs) { alert2cancel(rec,recs) },
import: function() { },
slider: hideSlider
},
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: noop
},
probe: {
live : "https://live.grid.space",
grid : function() { return false },
local : function() { return false }
},
process: {
code: currentProcessCode,
get: currentProcessName
},
platform,
selection,
show: {
alert: alert2,
devices: noop, // 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,
set_focus: setFocus,
update: SPACE.update
},
tweak,
util: {
isSecure,
ui2rec: updateSettingsFromFields,
rec2ui: updateFieldsFromSettings,
download: downloadBlob,
b64enc: obj => { return base64js.fromByteArray(new TextEncoder().encode(JSON.stringify(obj))) },
b64dec: obj => { return JSON.parse(new TextDecoder().decode(base64js.toByteArray(obj))) }
},
view: {
get: function() { return viewMode },
set: setViewMode,
update_slider: updateSlider,
update_fields: updateFields,
wireframe: setWireframe,
snapshot: null,
unit_scale: unitScale,
isArrange: function() { return viewMode === VIEWS.ARRANGE }
},
widgets: {
map: function() {
let map = {};
WIDGETS.forEach(widget => {
map[widget.id] = widget;
});
return map;
},
new: newWidget,
all: () => { return WIDGETS.slice() },
add: (widget) => { WIDGETS.push(widget) },
filter: (fn) => { WIDGETS = WIDGETS.filter(fn) },
for: forAllWidgets,
load: Widget.loadFromCatalog,
heal: healWidgets,
replace: replaceVertices,
meshes: meshArray,
opacity: setOpacity,
annotate: (id) => {
// add per widget annotation
// for things like extruder and supports
return WIDGETS.filter(w => w.id === id)[0].anno;
}
},
work: KIRI.work
};
function setFocus(widgets, point) {
if (point) {
SPACE.platform.setCenter(point.x, point.z, point.y);
SPACE.view.setFocus(new THREE.Vector3(point.x, point.y, point.z));
return;
}
if (widgets === undefined) {
widgets = WIDGETS;
} else if (!Array.isArray) {
widgets = [ widgets ];
} else if (widgets.length === 0) {
widgets = WIDGETS;
}
let pos = { x:0, y:0, z:0 };
for (let widget of widgets) {
pos.x += widget.track.pos.x;
pos.y += widget.track.pos.y;
pos.z += widget.track.pos.z;
}
if (widgets.length) {
pos.x /= widgets.length;
pos.y /= widgets.length;
pos.z /= widgets.length;
}
SPACE.platform.setCenter(pos.x, -pos.y, topZ / 2);
SPACE.view.setFocus(new THREE.Vector3(pos.x, topZ / 2, -pos.y));
}
function reload() {
API.event.emit('reload');
do_reload(100);
}
function do_reload(time) {
// allow time for async saves to complete and busy to to to zero
setTimeout(() => {
if (busy === 0) {
LOC.reload();
} else {
console.log(`reload deferred on busy=${busy}`);
do_reload(250);
}
}, time || 100);
}
function auto_save() {
if (!settings.controller.autoSave) {
return;
}
clearTimeout(saveTimer);
saveTimer = setTimeout(() => {
API.space.save(true);
}, 1000);
}
// frame message api
WIN.addEventListener('message', msg => {
let { origin, source, target, data } = msg;
if (source.window === target.window) return;
let send = source.window.postMessage;
if (data.mode) { API.mode.set(data.mode.toUpperCase()) }
if (data.view) { API.view.set(VIEWS[data.view.toUpperCase()]) }
if (data.function) { API.function[data.function.toLowerCase()]() }
if (data.event) {
API.event.on(data.event, (evd) => {
send({event: data.event, data: evd});
});
}
if (data.emit) API.event.emit(data.emit, data.message)
if (data.get) switch (data.get) {
case "mode": send({mode: settings.mode}); break;
case "device": send({device: settings.device}); break;
case "process": send({process: settings.process}); break;
default: send({all: settings}); break;
}
if (data.set) switch (data.set) {
case "features":
Object.assign(feature, data.features);
break;
case "device":
Object.assign(settings.device, data.options);
saveSettings();
break;
case "process":
Object.assign(settings.process, data.options);
saveSettings();
break;
}
if (data.parse) {
new moto.STL().parse(data.parse, vertices => {
let widget = newWidget().loadVertices(vertices);
platform.add(widget);
});
}
if (data.load) platformLoad(data.load);
if (data.clear) platform.clear();
if (data.alert) alert2(data.alert, data.time);
if (data.progress >= 0) setProgress(data.progress, data.message);
});
/** ******************************************************************
* 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 (['dn','lo','re'].indexOf(k) >= 0 || 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);
// remove version from url, preserve other settings
WIN.history.replaceState({},'','/kiri/' + encodeOpt(SETUP) + LOC.hash);
/** ******************************************************************
* Utility Functions
******************************************************************* */
function unitScale() {
return MODE === MODES.CAM && settings.controller.units === 'in' ? 25.4 : 1;
}
function alert2(message, time) {
if (message === undefined || message === null) {
return updateAlerts(true);
}
let rec = [message, Date.now(), time, true];
if (feature.alert_event) {
API.event.emit('alert', rec);
} else {
alerts.push(rec);
updateAlerts();
}
return rec;
}
function alert2cancel(rec,recs) {
if (Array.isArray(recs)) {
for (let r of recs) {
alert2cancel(r);
}
return;
}
if (feature.alert_event) {
API.event.emit('alert.cancel', rec);
return;
}
if (Array.isArray(rec)) {
rec[3] = false;
updateAlerts();
}
}
function updateAlerts(clear) {
if (clear) {
alerts = [];
}
let now = Date.now();
// filter out by age and active flag
alerts = alerts.filter(alert => {
return alert[3] && (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 => ['<p>',v[0],'</p>'].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 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 encodeOpt(opt) {
let out = [];
Object.keys(opt).forEach(key => {
if (key === 'ver') return;
let val = opt[key];
out.push(encodeURIComponent(key) + ":" + encodeURIComponent(val));
});
return out.length ? '?' + out.join(',') : '';
}
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 getOverlappingRanges(lo, hi) {
let ranges = [];
for (let range of settings.process.ranges) {
let in_lo = range.lo >= lo && range.lo <= hi;
let in_hi = range.hi >= lo && range.hi <= hi;
if (in_lo || in_hi) {
ranges.push(range);
}
}
return ranges;
}
// set process override values for a range
function updateRange(lo, hi, values, add) {
let ranges = settings.process.ranges;
let slices = {};
let min = lo;
let max = hi;
// special case for belt loops which should not be flattened
if (values.outputLoops) {
ranges.push({
lo, hi, fields: values
});
updateFieldsFromSettings(settings.process);
API.show.alert("update ranges", 2);
API.event.emit("range.updates", ranges);
return;
}
// just remove values from matching ranges
if (!add) {
for (let range of getOverlappingRanges(lo, hi)) {
for (let key of Object.keys(values)) {
delete range.fields[key];
}
if (Object.keys(range.fields).length === 0) {
let pos = ranges.indexOf(range);
if (pos >= 0) {
ranges.splice(pos,1);
}
}
}
API.event.emit("range.updates", ranges);
return;
}
// set aside belt loops and re-append later
// since we do not want to collapse/merge loops
let exclude = ranges.filter(r => r.fields.outputLoops);
ranges = ranges.filter(r => !r.fields.outputLoops);
// flatten ranges
ranges.push({lo, hi, fields: values});
for (let range of ranges) {
min = Math.min(range.lo, min);
max = Math.max(range.hi, max);
for (let i=range.lo; i<=range.hi; i++) {
let slice = slices[i];
if (!slice) {
slice = slices[i] = {};
}
for (let [key,val] of Object.entries(range.fields)) {
slice[key] = val;
}
}
}
// merge contiguous matching ranges
ranges = settings.process.ranges = [];
let range;
for (let i=min; i<=max; i++) {
let slice = slices[i];
if (slice && !range) {
range = {lo: i, hi: i, fields: slice};
} else if (slice && range && isEquals(range.fields, slice)) {
range.hi = i;
} else if (range) {
ranges.push(range);
if (slice) {
range = {lo: i, hi: i, fields: slice};
} else {
range = undefined;
}
}
}
ranges.push(range);
ranges.appendAll(exclude);
updateFieldsFromSettings(settings.process);
API.show.alert("update ranges", 2);
API.event.emit("range.updates", ranges);
}
let overrides = {};
// updates editable fields that are range dependent
function updateFieldsFromRange() {
return;
if (settings.mode !== 'FDM' || viewMode !== VIEWS.SLICE || !settings.process.ranges) {
let okeys = Object.keys(overrides);
if (okeys.length) {
updateFieldsFromSettings(overrides);
overrides = {};
}
return;
}
let match = 0;
let values = {};
let restores = Object.clone(overrides);
let { layer_lo, layer_hi } = API.var;
for (let range of getOverlappingRanges(API.var.layer_lo, API.var.layer_hi)) {
for (let key of Object.keys(range.fields)) {
values[key] = range.fields[key];
overrides[key] = settings.process[key];
delete restores[key];
match++;
}
}
if (match) {
updateFieldsFromSettings(values);
}
let rkeys = Object.keys(restores);
if (rkeys.length) {
updateFieldsFromSettings(restores);
for (let key of rkeys) {
delete overrides[key];
}
}
UC.refresh();
}
function updateSlider() {
API.event.emit("slider.set", {
start: (API.var.layer_lo / API.var.layer_max),
end: (API.var.layer_hi / API.var.layer_max)
});
updateFieldsFromRange();
}
function setVisibleLayer(h, l) {
h = h >= 0 ? h : API.var.layer_hi;
l = l >= 0 ? l : API.var.layer_lo;
API.var.layer_hi = bound(h, 0, API.var.layer_max);
API.var.layer_lo = bound(l, 0, h);
API.event.emit("slider.label");
updateSlider();
showSlices();
}
function meshArray() {
let out = [];
forAllWidgets(function(widget) {
if (!feature.hoverAdds) {
out.push(widget.mesh);
}
out.appendAll(widget.adds);
});
return out;
}
function replaceVertices(vertices) {
let widgets = API.selection.widgets(true);
if (!widgets.length) {
return;
}
function onload(vertices) {
for (let w of widgets) {
let track = Object.clone(w.track);
let { scale, rot, pos } = track;
let roto = w.roto.slice();
w.loadVertices(vertices);
for (let m of roto) {
w.mesh.geometry.applyMatrix4(m.clone());
}
w._scale(scale.x, scale.y, scale.z);
}
platform.update();
}
if (vertices) {
onload(vertices);
} else {
// dialog
$('load-file').onchange = function(event) {
LOAD.File.load(event.target.files[0])
.then(data => onload(data[0].mesh))
.catch(error => console.log({error}));
};
$('load-file').click();
}
}
function healWidgets() {
let widgets = API.widgets.all();
let marker;
if (widgets.length) {
marker = API.show.alert("Analyzing objects", 100000);
} else {
return;
}
setTimeout(() => {
Promise.all(widgets.map(w => w.heal())).then(mod => {
API.hide.alert(marker);
let healed = mod.filter(m => m).length;
if (healed) {
API.show.alert(`${healed} Object${healed ? 's':''} healed`);
} else {
API.show.alert('Nothing found to heal');
}
});
}, 1);
}
function forAllWidgets(f) {
WIDGETS.slice().forEach(function(widget) {
f(widget);
});
}
function forSelectedGroups(f) {
let groups = API.selection.widgets(true).map(w => w.group).uniq();
for (let group of groups) {
f(group[0]);
}
}
function forSelectedWidgets(f,noauto) {
let m = selectedMeshes;
if (m.length === 0 && WIDGETS.length === 1) {
m = noauto ? [] : [ WIDGETS[0].mesh ];
}
m.slice().forEach(function (mesh) { f(mesh.widget) });
}
function forSelectedMeshes(f) {
selectedMeshes.slice().forEach(function (mesh) { f(mesh) });
}
function setWireframe(bool, color, opacity) {
forAllWidgets(function(w) { w.setWireframe(bool, color, opacity) });
SPACE.update();
}
function updateSliderMax(set) {
let max = STACKS.getRange().tallest - 1;
API.var.layer_max = UI.sliderMax.innerText = max;
if (set || max < API.var.layer_hi) {
API.var.layer_hi = API.var.layer_max;
API.event.emit("slider.label");
updateSlider();
}
}
function hideSlices() {
STACKS.clear();
setOpacity(color.model_opacity);
forAllWidgets(function(widget) {
widget.setWireframe(false);
});
}
function setWidgetVisibility(bool) {
forAllWidgets(w => {
if (bool) {
w.show();
} else {
w.hide();
}
});
}
/**
* hide or show slice-layers and their sub-elements
*
* @param {number} [layer]
*/
function showSlices(layer) {
if (viewMode === VIEWS.ARRANGE) {
return;
}
showSlider();
if (typeof(layer) === 'string' || typeof(layer) === 'number') {
layer = parseInt(layer);
} else {
layer = API.var.layer_hi;
}
layer = bound(layer, 0, API.var.layer_max);
if (layer < API.var.layer_lo) API.var.layer_lo = layer;
API.var.layer_hi = layer;
API.event.emit("slider.label");
let cam = MODE === MODES.CAM,
sla = MODE === MODES.SLA,
hi = cam ? API.var.layer_max - API.var.layer_lo : API.var.layer_hi,
lo = cam ? API.var.layer_max - API.var.layer_hi : API.var.layer_lo;
updateSlider();
STACKS.setRange(API.var.layer_lo, API.var.layer_hi);
SPACE.update();
}
function cancelWorker() {
if (KIRI.work.isBusy()) {
KIRI.work.restart();
}
}
function showSlider() {
UI.layers.style.display = 'flex';
UI.slider.style.display = 'flex';
}
function hideSlider(andmenu) {
UI.layers.style.display = 'none';
UI.slider.style.display = 'none';
UI.speeds.style.display = 'none';
}
function updateSpeeds(maxSpeed, minSpeed) {
UI.speeds.style.display =
maxSpeed &&
settings.mode !== 'SLA' &&
settings.mode !== 'LASER' &&
viewMode === VIEWS.PREVIEW &&
UI.showSpeeds.checked ? 'block' : '';
if (maxSpeed) {
const colors = [];
for (let i=0; i<= maxSpeed; i += maxSpeed/20) {
colors.push(Math.round(Math.max(i,1)));
}
KIRI.client.colors(colors, maxSpeed, speedColors => {
const list = [];
Object.keys(speedColors).map(v => parseInt(v)).sort((a,b) => b-a).forEach(speed => {
const color = speedColors[speed];
const hex = color.toString(16).padStart(6,0);
const r = (color >> 16) & 0xff;
const g = (color >> 8) & 0xff;
const b = (color >> 0) & 0xff;
const style = `background-color:#${hex}`;
list.push(`<label style="${style}">${speed}</label>`);
});
UI.speedbar.innerHTML = list.join('');
});
API.event.emit('preview.speeds', {min: minSpeed, max: maxSpeed});
}
}
function prepareSlices(callback, scale = 1, offset = 0) {
if (viewMode == VIEWS.ARRANGE) {
let snap = SPACE.screenshot();
API.view.snapshot = snap.substring(snap.indexOf(",") + 1);
KIRI.work.snap(SPACE.screenshot2({width: 640}));
}
if (MODE === MODES.SLA && !callback) {
callback = preparePreview;
}
// force layout in belt mode when widget exceeds bed length
if (WIDGETS.length && settings.device.bedBelt) {
let doLayout = false;
for (let w of WIDGETS) {
let bb = w.getBoundingBox();
let yspan = bb.max.y - bb.min.y;
if (yspan > settings.device.bedDepth) {
doLayout = true;
}
}
if (doLayout) {
platformLayout();
}
}
let process = settings.process,
device = settings.device,
isBelt = device.bedBelt,
mode = settings.mode,
now = Date.now(),
totvert = 0,
track = {},
totalProgress;
// clear completion marks
complete = {};
hideSlider(true);
platform.deselect();
setViewMode(VIEWS.SLICE);
API.conf.save();
API.event.emit('slice.begin', getMode());
let slicing = WIDGETS.slice().filter(w => !w.track.ignore);
// determing this widgets % of processing time estimated by vertex count
for (let widget of slicing) {
totvert += widget.getVertices().count;
}
let defvert = totvert / slicing.length;
setOpacity(color.slicing_opacity);
let segtimes = {},
segNumber = 0,
errored = false,
startTime = Date.now(),
toSlice = slicing.slice(),
camOrLaser = mode === 'CAM' || mode === 'LASER',
extruders = {},
lastMsg;
for (let widget of toSlice) {
widget.stats.progress = 0;
widget.setColor(color.slicing);
extruders[widget.anno.extruder] = widget.anno.extruder;
}
// in multi-material belt mode, the anchor needs to be extended
// to allow room for the purge tower to be built. calculate here
extruders = Object.values(extruders);
if (isBelt && extruders.length > 1 && process.outputPurgeTower) {
process.beltAnchor = Math.max(
process.firstLayerBeltLead,
Math.sqrt(process.outputPurgeTower) * extruders.length * (1/Math.sqrt(2)));
} else {
process.beltAnchor = process.firstLayerBeltLead;
}
STACKS.clear();
if (isBelt) {
KIRI.client.clear();
}
KIRI.client.sync();
KIRI.client.rotate(settings);
sliceNext();
function sliceNext() {
if (toSlice.length) {
sliceWidget(toSlice.shift())
} else {
KIRI.client.sliceAll(settings, sliceDone);
}
}
function sliceWidget(widget) {
widget.stack = STACKS.create(widget.id, widget.mesh);
let factor = (widget.getVertices().count / defvert);
// compensate for zcut (widget moved through floor)
widget.stack.obj.view.position.z = widget.track.zcut || 0;
widget.slice(settings, function(sliced, error) {
widget.rotinfo = null;
let mark = Date.now();
// update UI info
if (sliced) {
// update segment time
if (lastMsg) {
segtimes[`${widget.id}_${segNumber++}_${lastMsg}`] = mark - startTime;
}
API.event.emit('slice', getMode());
}
// handle slicing errors
if (error && !errored) {
errored = true;
setViewMode(VIEWS.ARRANGE);
alert2(error, 5);
API.show.progress(0);
KIRI.client.restart();
API.event.emit('slice.error', error);
}
if (errored) {
// terminate slicing
sliceDone();
} else {
// start next widget slice
sliceNext();
}
}, function(update, msg) {
if (msg && msg !== lastMsg) {
let mark = Date.now();
if (lastMsg) {
segtimes[`${widget.id}_${segNumber++}_${lastMsg}`] = mark - startTime;
}
lastMsg = msg;
startTime = mark;
}
// on update
track[widget.id] = (update || 0) * factor;
totalProgress = 0;
for (let w of slicing) {
totalProgress += (track[w.id] || 0);
}
API.show.progress(offset + (totalProgress / WIDGETS.length) * scale, msg);
});
}
function sliceDone() {
let alert = null;
if (scale === 1 && feature.work_alerts && slicing.length) {
alert = API.show.alert("Rendering");
};
KIRI.client.unrotate(settings, () => {
for (let widget of slicing) {
// on done
segtimes[`${widget.id}_${segNumber++}_draw`] = widget.render(widget.stack);
// rotate stack for belt beds
if (widget.rotinfo) {
widget.stack.obj.rotate(widget.rotinfo);
}
if (scale === 1) {
// clear wireframe
widget.setWireframe(false, color.wireframe, color.wireframe_opacity);
widget.setOpacity(camOrLaser ? color.cam_sliced_opacity : color.sliced_opacity);
widget.setColor(color.deselected);
API.hide.alert(alert);
}
}
updateSliderMax(true);
setVisibleLayer(-1, 0);
if (scale === 1) {
updateStackLabelState();
}
});
if (scale === 1) {
API.show.progress(0);
}
// cause visuals to update
SPACE.scene.active();
// mark slicing complete for prep/preview
complete.slice = true;
API.event.emit('slice.end', getMode());
// print stats
segtimes.total = Date.now() - now;
console.log(segtimes);
if (callback && typeof callback === 'function') {
callback();
}
}
}
function preparePreview(callback, scale = 1, offset = 0) {
if (complete.preview === feature.pmode) {
if (settings.device.extruders.length > 1) {
if (++feature.pmode > 2) {
feature.pmode = 1;
}
} else {
if (callback) callback();
return;
}
}
if (!complete.slice) {
settings.render = false;
prepareSlices(() => {
preparePreview(callback, 0.25, 0.75);
}, 0.75);
return;
}
hideSlider(true);
let isCam = MODE === MODES.CAM, pMode = getMode();
setViewMode(VIEWS.PREVIEW);
API.conf.save();
API.event.emit('preview.begin', pMode);
if (isCam) {
setOpacity(color.cam_preview_opacity);
forAllWidgets(function(widget) {
widget.setColor(color.cam_preview);
});
} else if (offset === 0) {
setOpacity(color.preview_opacity);
}
let now = Date.now(),
segNumber = 0,
segtimes = {},
startTime,
lastMsg,
output = [];
// pass preview mode to worker
settings.pmode = feature.pmode;
settings.render = true;
KIRI.client.prepare(settings, function(progress, message, layer) {
if (layer) {
output.push(KIRI.codec.decode(layer));
}
if (message && message !== lastMsg) {
let mark = Date.now();
if (lastMsg) {
segtimes[`${segNumber++}_${lastMsg}`] = mark - startTime;
}
lastMsg = message;
startTime = mark;
}
API.show.progress(offset + progress * scale, message);
}, function (reply, maxSpeed, minSpeed) {
// handle worker errors
if (reply && reply.error) {
alert2(reply.error, 5);
setViewMode(VIEWS.ARRANGE);
API.event.emit('preview.error', reply.error);
API.show.progress(0);
SPACE.update();
return;
}
if (lastMsg) {
segtimes[`${segNumber++}_${lastMsg}`] = Date.now() - startTime;
}
API.show.progress(0);
if (!isCam) setOpacity(0);
if (output.length) {
let alert = feature.work_alerts ? API.show.alert("Rendering") : null;
startTime = Date.now();
STACKS.clear();
const stack = STACKS.create('print', SPACE.world)
output.forEach(layer => {
stack.add(layer);
});
// rotate stack for belt beds
if (settings.device.bedBelt && WIDGETS[0].rotinfo) {
let ri = WIDGETS[0].rotinfo;
ri.dz = 0;
ri.dy = settings.device.bedDepth / 2;
stack.obj.rotate(WIDGETS[0].rotinfo);
}
API.hide.alert(alert);
segtimes[`${segNumber}_draw`] = Date.now() - startTime;
}
// print stats
segtimes.total = Date.now() - now;
console.log(segtimes);
API.event.emit('print', pMode);
API.event.emit('preview.end', pMode);
SPACE.update();
updateSliderMax(true);
setVisibleLayer(-1, 0);
if (feature.pmode === PMODES.SPEED) {
updateSpeeds(maxSpeed, minSpeed);
} else {
updateSpeeds();
}
updateStackLabelState();
// mark preview complete for export
complete.preview = feature.pmode;
if (typeof(callback) === 'function') {
callback();
}
});
}
function prepareAnimation() {
API.event.emit("function.animate", {mode: settings.mode});
}
function prepareExport() {
const argsave = arguments;
if (!complete.preview) {
preparePreview(() => {
prepareExport(...argsave);
});
return;
}
API.event.emit("function.export", {mode: settings.mode});
complete.export = true;
KIRI.export(...argsave);
}
function updateStackLabelState() {
// match label checkboxes to preference
for (let label of STACKS.getLabels()) {
let check = `${settings.mode}-${viewMode}-${label}`;
STACKS.setVisible(label, settings.labels[check] !== false);
}
}
function loadCode(code, type) {
API.event.emit("code.load", {code, type});
setViewMode(VIEWS.PREVIEW);
setOpacity(0);
KIRI.client.parse({code, type, settings}, progress => {
API.show.progress(progress, "parsing");
}, (layers, maxSpeed, minSpeed) => {
API.show.progress(0);
STACKS.clear();
const stack = STACKS.create('parse', SPACE.platform.world);
layers.forEach(layer => stack.add(layer));
updateSliderMax(true);
updateSpeeds(maxSpeed, minSpeed);
showSlices();
updateStackLabelState();
SPACE.update();
API.event.emit("code.loaded", {code, type});
});
}
function loadImageDialog(image, name, force) {
if (!force && image.byteLength > 2500000) {
return UC.confirm("Large images may fail to import<br>Consider resizing under 1000 x 1000<br>Proceed with import?").then(ok => {
if (ok) {
loadImageDialog(image, name, true);
}
});
}
const opt = {pre: [
"<div class='f-col a-center'>",
" <h3>Image Conversion</h3>",
" <p class='t-just' style='width:300px;line-height:1.5em'>",
" This will create a 3D model from a 2D PNG image. Photos must",
" be blurred to be usable. Values from 0=off to 50=high are suggested.",
" Higher values incur more processing time.",
" </p>",
" <div class='f-row t-right'><table>",
" <tr><th>blur value</th><td><input id='png-blur' value='0' size='3'></td>",
" <th>&nbsp;invert image</th><td><input id='png-inv' type='checkbox'></td></tr>",
" <tr><th>base size</th><td><input id='png-base' value='0' size='3'></td>",
" <th>&nbsp;invert alpha</th><td><input id='alpha-inv' type='checkbox'></td></tr>",
" <tr><th>border size</th><td><input id='png-border' value='0' size='3'></td>",
" <th></th><td></td></tr>",
" </table></div>",
"</div>"
]};
UC.confirm(undefined, {convert:true, cancel:false}, undefined, opt).then((ok) => {
if (ok) {
loadImage(image, {
file: name,
blur: parseInt($('png-blur').value) || 0,
base: parseInt($('png-base').value) || 0,
border: parseInt($('png-border').value) || 0,
inv_image: $('png-inv').checked,
inv_alpha: $('alpha-inv').checked
});
}
});
}
function loadImage(image, opt = {}) {
let info = Object.assign({settings, png:image}, opt);
KIRI.client.image2mesh(info, progress => {
API.show.progress(progress, "converting");
}, output => {
API.show.progress(0);
let {bigv, verts, index} = output;
// let mat = new THREE.MeshPhongMaterial({
// shininess: 0x101010,
// specular: 0x101010,
// transparent: true,
// opacity: 1,
// color: 0x999999,
// side: THREE.DoubleSide
// });
//
// let geo = new THREE.BufferGeometry();
// geo.setAttribute('position', new THREE.BufferAttribute(bigv, 3));
// // geo.setIndex([...index]); // doesn't like the Uint32Array
// geo.computeFaceNormals();
// geo.computeVertexNormals();
//
// let mesh = new THREE.Mesh(geo, mat);
// mesh.castShadow = true;
// mesh.receiveShadow = true;
//
// SPACE.platform.world.add(mesh);
let widget = newWidget().loadVertices(bigv)
widget.meta.file = opt.file;
platform.add(widget);
});
}
/** ******************************************************************
* Selection Functions
******************************************************************* */
function groupMerge() {
Widget.Groups.merge(API.selection.widgets(true));
}
function groupSplit() {
Widget.Groups.split(API.selection.widgets(false));
}
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 = (dx / scale).round(2)
UI.sizeY.value = UI.sizeY.was = (dy / scale).round(2)
UI.sizeZ.value = UI.sizeZ.was = (dz / scale).round(2)
UI.scaleX.value = UI.scaleX.was = track.scale.x.round(2);
UI.scaleY.value = UI.scaleY.was = track.scale.y.round(2);
UI.scaleZ.value = UI.scaleZ.was = track.scale.z.round(2);
updateSelectedBounds();
}
function fitDeviceToWidgets() {
let maxy = 0;
forAllWidgets(function(widget) {
let wb = widget.mesh.getBoundingBox().clone();
maxy = Math.max(maxy, wb.max.y - wb.min.y);
});
let dev = settings.device;
if (maxy > dev.bedDepth) {
dev.bedDepthSave = dev.bedDepth;
dev.bedDepth = maxy + 10;
SPACE.platform.setSize(
parseInt(dev.bedWidth),
parseInt(dev.bedDepth),
parseFloat(dev.bedHeight),
parseFloat(dev.maxHeight)
);
SPACE.platform.update();
return true;
}
}
function updateSelectedBounds(widgets) {
// update bounds on selection for drag limiting
let isBelt = settings.device.bedBelt;
if (isBelt) {
if (fitDeviceToWidgets()) {
platform.update_origin();
SPACE.update();
}
}
let dvy = settings.device.bedDepth;
let dvx = settings.device.bedWidth;
let bounds_sel = new THREE.Box3();
if (!widgets) {
widgets = selectedMeshes.map(m => m.widget);
}
for (let widget of widgets) {
let wp = widget.track.pos;
let bx = widget.track.box;
let miny = wp.y - bx.h/2 + dvy/2;
let maxy = wp.y + bx.h/2 + dvy/2;
let minx = wp.x - bx.w/2 + dvx/2;
let maxx = wp.x + bx.w/2 + dvx/2;
// keep widget in bounds when rotated or scaled
let ylo = miny < 0;
let yhi = !isBelt && maxy > dvy
if (ylo && !yhi) {
widget.move(0, -miny, 0);
} else if (yhi && !ylo) {
widget.move(0, dvy - maxy, 0);
}
let xlo = minx < 0;
let xhi = maxx > dvx;
if (xlo && !xhi) {
widget.move(-minx, 0, 0);
} else if (xhi && !xlo) {
widget.move(dvx - maxx, 0, 0);
}
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_sel.union(wb);
}
settings.bounds_sel = bounds_sel;
}
function setOpacity(value) {
forAllWidgets(function (w) { w.setOpacity(value) });
SPACE.update();
}
function duplicateSelection() {
API.selection.for_widgets(function(widget) {
let mesh = widget.mesh;
let bb = mesh.getBoundingBox();
let ow = widget;
let nw = API.widgets.new().loadGeometry(mesh.geometry.clone());
nw.meta.file = ow.meta.file;
nw.meta.vertices = ow.meta.vertices;
nw.move(bb.max.x - bb.min.x + 1, 0, 0);
platform.add(nw,true);
nw.anno = ow.annotations();
API.event.emit("widget.duplicate", nw, ow);
});
}
function mirrorSelection() {
API.selection.for_widgets(function(widget) {
widget.mirror();
API.event.emit("widget.mirror", widget);
});
SPACE.update();
auto_save();
}
function moveSelection(x, y, z, abs) {
if (viewMode !== VIEWS.ARRANGE) return;
forSelectedGroups(function (w) {
w.move(x, y, z, abs);
});
updateSelectedBounds();
platformUpdateBounds();
API.event.emit('selection.move', {x, y, z, abs});
SPACE.update();
auto_save();
}
function scaleSelection() {
if (viewMode !== VIEWS.ARRANGE) return;
let args = arguments;
forSelectedGroups(function (w) {
w.scale(...args);
});
platform.compute_max_z();
updateSelectedBounds();
platformUpdateBounds();
API.event.emit('selection.scale', [...arguments]);
// skip update if last argument is strictly 'false'
if ([...arguments].pop() === false) {
return;
}
updateSelectedInfo();
SPACE.update();
auto_save();
}
function rotateSelection(x, y, z) {
if (viewMode !== VIEWS.ARRANGE) return;
forSelectedGroups(function (w) {
w.rotate(x, y, z);
API.event.emit('widget.rotate', {widget: w, x, y, z});
});
updateSelectedBounds();
platformUpdateBounds();
platform.compute_max_z();
API.event.emit('selection.rotate', {x, y, z});
updateSelectedInfo();
SPACE.update();
auto_save();
}
function exportSelection(format = "stl") {
let widgets = API.selection.widgets();
if (widgets.length === 0) {
widgets = API.widgets.all();
}
let facets = 0;
let outs = [];
widgets.forEach(widget => {
let mesh = widget.mesh;
let geo = mesh.geometry;
outs.push({geo, widget});
facets += geo.attributes.position.count;
});
if (format === "obj") {
let obj = [];
let vpad = 0;
for (let out of outs) {
let meta = out.widget.meta;
let name = meta.file || 'unnamed';
obj.push(`g ${name}`);
let { position } = out.geo.attributes;
let pvals = position.array;
for (let i=0, il=position.count; i<il; i += 3) {
let pi = i * position.itemSize;
obj.push(`v ${pvals[pi++]} ${pvals[pi++]} ${pvals[pi++]}`);
obj.push(`v ${pvals[pi++]} ${pvals[pi++]} ${pvals[pi++]}`);
obj.push(`v ${pvals[pi++]} ${pvals[pi++]} ${pvals[pi++]}`);
obj.push(`f ${i+1+vpad} ${i+2+vpad} ${i+3+vpad}`);
}
vpad += position.count;
}
return obj.join('\n');
}
let stl = new Uint8Array(80 + 4 + facets * 50);
let dat = new DataView(stl.buffer);
let pos = 84;
dat.setInt32(80, facets, true);
for (let out of outs) {
let { position } = out.geo.attributes;
let pvals = position.array;
for (let i=0, il=position.count; i<il; i += 3) {
let pi = i * position.itemSize;
let p0 = new THREE.Vector3(pvals[pi++], pvals[pi++], pvals[pi++]);
let p1 = new THREE.Vector3(pvals[pi++], pvals[pi++], pvals[pi++]);
let p2 = new THREE.Vector3(pvals[pi++], pvals[pi++], pvals[pi++]);
let norm = THREE.computeFaceNormal(p0, p1, p2);
let xo = 0, yo = 0, zo = 0;
if (outs.length > 1) {
let {x, y, z} = out.widget.track.pos;
xo = x;
yo = y;
zo = z;
}
dat.setFloat32(pos + 0, norm.x, true);
dat.setFloat32(pos + 4, norm.y, true);
dat.setFloat32(pos + 8, norm.z, true);
dat.setFloat32(pos + 12, p0.x + xo, true);
dat.setFloat32(pos + 16, p0.y + yo, true);
dat.setFloat32(pos + 20, p0.z + zo, true);
dat.setFloat32(pos + 24, p1.x + xo, true);
dat.setFloat32(pos + 28, p1.y + yo, true);
dat.setFloat32(pos + 32, p1.z + zo, true);
dat.setFloat32(pos + 36, p2.x + xo, true);
dat.setFloat32(pos + 40, p2.y + yo, true);
dat.setFloat32(pos + 44, p2.z + zo, true);
pos += 50;
}
}
return stl;
}
/** ******************************************************************
* Platform Functions
******************************************************************* */
function platformUpdateOrigin() {
platform.update_bounds();
let dev = settings.device;
let proc = settings.process;
let ruler = settings.controller.showRulers;
let stock = settings.stock;
let stockCenter = stock.center || {};
let hasStock = stock.x && stock.y && stock.z;
let isBelt = dev.bedBelt;
let center = MODE === MODES.FDM ? dev.originCenter || dev.bedRound :
MODE === MODES.SLA ? false :
MODE === MODES.CAM ? proc.outputOriginCenter :
dev.originCenter || proc.outputOriginCenter;
let x = 0;
let y = 0;
let z = 0;
if (MODE === MODES.CAM && proc.camOriginTop) {
z = (hasStock ? stock.z : topZ) + 0.01;
}
if (!center) {
if (hasStock) {
x = (-stock.x / 2) + stockCenter.x;
y = (stock.y / 2) - stockCenter.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 (hasStock) {
x = stockCenter.x;
y = -stockCenter.y;
} else if (isBelt) {
y = dev.bedDepth / 2;
}
settings.origin = {x, y, z};
SPACE.platform.setRulers(ruler, ruler, 1/unitScale(), 'X', isBelt ? 'Z' : 'Y');
if (settings.controller.showOrigin && MODE !== MODES.SLA) {
SPACE.platform.setOrigin(x,y,z,true);
} else {
SPACE.platform.setOrigin(x,y,z,false);
}
}
function platformUpdateTopZ(zdelta) {
// preserve topZD and re-use when not supplied as argument
topZD = zdelta !== undefined ? zdelta : topZD;
let stock = settings.stock;
let hasStock = stock.x && stock.y && stock.z;
forAllWidgets(function(widget) {
if (MODE === MODES.CAM) {
let bounds = widget.getBoundingBox();
let wzmax = bounds.max.z;
let topz = hasStock ? stock.z : topZ;
let zdelta = settings.process.camZOffset || 0;
switch (settings.process.camZAnchor) {
case 'top':
widget.setTopZ(stock.z - zdelta);
break;
case 'middle':
widget.setTopZ(stock.z - (stock.z - wzmax) / 2);
break;
case 'bottom':
widget.setTopZ(wzmax + zdelta);
break;
}
} else {
widget.setTopZ(0);
}
});
}
function platformUpdateSize(updateDark = true) {
let dev = settings.device,
isBelt = dev.bedBelt,
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),
parseFloat(dev.maxHeight)
);
let proc = settings.process,
ctrl = settings.controller,
ruler = ctrl.showRulers,
unitMM = ctrl.units === 'mm',
gridMajor = unitMM ? 25 : 25.4,
gridMinor = unitMM ? 5 : 25.4 / 10;
if (updateDark) {
if (ctrl.dark) {
SPACE.platform.setGrid(gridMajor, gridMinor, 0x999999, 0x333333);
SPACE.platform.opacity(0.8);
SPACE.sky.setColor(0);
DOC.body.classList.add('dark');
} else {
SPACE.platform.setGrid(gridMajor, gridMinor, 0x999999, 0xcccccc);
SPACE.platform.opacity(0.3);
SPACE.sky.setColor(0xffffff);
DOC.body.classList.remove('dark');
}
}
SPACE.platform.setRulers(ruler, ruler, 1 / unitScale(), 'X', isBelt ? 'Z' : 'Y');
// SPACE.platform.setGZOff(height/2 - 0.1);
platform.update_origin();
}
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, recurse = true) {
if (viewMode !== VIEWS.ARRANGE) {
return;
}
// apply select to entire group
if (recurse && widget && widget.group.length > 1) {
for (let w of widget.group) {
platformSelect(w, true, false);
}
return;
}
let mesh = widget.mesh,
sel = (selectedMeshes.indexOf(mesh) >= 0);
if (sel) {
if (shift) {
platform.deselect(widget, recurse)
} else if (selectedMeshes.length > 1) {
platform.deselect(undefined, recurse);
platform.select(widget, false, recurse);
}
} else {
// prevent selection in slice view
if (!mesh.material.visible) return;
if (!shift) platform.deselect(undefined, recurse);
selectedMeshes.push(mesh);
API.event.emit('widget.select', widget);
widget.setColor(color.selected);
updateSelectedInfo();
}
platformUpdateSelected();
SPACE.update();
}
function platformSelectedCount() {
return viewMode === VIEWS.ARRANGE ? selectedMeshes.length : 0;
}
function platformUpdateSelected() {
let selreal = selection.widgets();
let selwid = selection.widgets(true);
let selcount = selwid.length;
let extruders = settings.device.extruders;
UI.trash.style.display = selreal.length ? 'flex' : '';
if (selcount) {
UI.nozzle.classList.add('lt-active');
if (feature.meta && selcount === 1) {
let sel = selwid[0];
let name = sel.meta.file || sel.meta.url;
if (name) {
name = name
.toLowerCase()
.replace(/_/g,' ')
.replace(/.png/,'')
.replace(/.stl/,'');
let sp = name.indexOf('/');
if (sp >= 0) {
name = name.substring(sp + 1);
}
UI.mesh.name.innerText = name;
}
UI.mesh.points.innerText = sel.meta.vertices;
UI.mesh.faces.innerText = sel.meta.vertices / 3;
} else {
UI.mesh.name.innerText = `[${selcount}]`;
UI.mesh.points.innerText = '-';
UI.mesh.faces.innerText = '-';
}
} else {
UI.mesh.name.innerText = '[0]';
UI.mesh.points.innerText = '-';
UI.mesh.faces.innerText = '-';
UI.nozzle.classList.remove('lt-active');
}
UI.nozzle.style.display = extruders && extruders.length > 1 ? 'flex' : '';
if (extruders) {
for (let i=0; i<extruders.length; i++) {
let b = $(`sel-ext-${i}`);
if (b) b.classList.remove('pop-sel');
}
forSelectedWidgets(w => {
w.setColor(color.selected);
let ext = API.widgets.annotate(w.id).extruder || 0;
let b = $(`sel-ext-${ext}`);
if (b) b.classList.add('pop-sel');
w.saveState();
}, true);
}
}
function platformDeselect(widget, recurse = true) {
if (viewMode !== VIEWS.ARRANGE) {
// don't de-select and re-color widgets in,
// for example, sliced or preview modes
return;
}
// apply deselect to entire group
if (recurse && widget && widget.group.length > 1) {
for (let w of widget.group) {
platformDeselect(w, false);
}
return;
}
if (!widget) {
forAllWidgets(function(widget) {
platform.deselect(widget);
});
return;
}
let mesh = widget.mesh,
si = selectedMeshes.indexOf(mesh),
sel = (si >= 0);
if (sel) {
selectedMeshes.splice(si,1);
API.event.emit('widget.deselect', widget);
}
widget.setColor(color.deselected);
platformUpdateSelected();
SPACE.update();
updateSelectedInfo();
}
function platformLoad(url, onload) {
if (url.toLowerCase().indexOf(".stl") > 0) {
platform.load_stl(url, onload);
} else {
ajax(url, function(vertices) {
vertices = js2o(vertices).toFloat32();
let widget = newWidget().loadVertices(vertices);
widget.meta.url = url;
platform.add(widget);
if (onload) onload(vertices, widget);
});
}
}
function platformLoadSTL(url, onload, formdata) {
let scale = 1 / unitScale();
new LOAD.STL().load(url, function(vertices, filename) {
if (vertices) {
let widget = newWidget().loadVertices(vertices);
widget.meta.file = filename;
platform.add(widget);
if (onload) {
onload(vertices, widget);
}
}
}, formdata, scale);
}
function platformLoadURL(url, options = {}) {
platform.group();
LOAD.URL.load(url, options).then((objects) => {
let widgets = [];
for (let object of objects) {
let widget = newWidget(undefined, options.group).loadVertices(object.mesh);
widget.meta.file = object.file;
platform.add(widget);
widgets.push(widget);
}
platform.group_done();
API.event.emit("load.url", { url, options, widgets });
}).catch(error => {
API.show.alert(error);
});
}
function platformComputeMaxZ() {
topZ = 0;
forAllWidgets(function(widget) {
topZ = Math.max(topZ, widget.mesh.getBoundingBox().max.z);
});
SPACE.platform.setMaxZ(topZ);
}
function platformGroup() {
grouping = true;
}
// called after all new widgets are loaded to update group positions
function platformGroupDone(skipLayout) {
grouping = false;
Widget.Groups.loadDone();
if (feature.drop_layout && !skipLayout) {
platform.layout();
}
}
function platformAdd(widget, shift, nolayout) {
widget.anno.extruder = widget.anno.extruder || 0;
WIDGETS.push(widget);
SPACE.world.add(widget.mesh);
platform.select(widget, shift);
platform.compute_max_z();
API.event.emit('widget.add', widget);
platformChanged();
auto_save();
if (nolayout) {
return;
}
if (!grouping) {
platformGroupDone();
if (!settings.controller.autoLayout) {
positionNewWidget(widget);
}
}
}
function positionNewWidget(widget) {
if (WIDGETS.length <= 1) return;
let DEG2RAD = Math.PI / 180;
let { bedWidth, bedDepth } = settings.device;
let devOff = bedWidth / 2;
let wbb = widget.getBoundingBox();
let dim = { x: wbb.max.x - wbb.min.x, y: wbb.max.y - wbb.min.y };
let hdim = { x: dim.x / 2, y: dim.y / 2 };
let bounds = BASE.newBounds(), target = BASE.newBounds();
// look for best position for new widget that doesn't collide
outer: for (let rad=10; rad<200; rad += 10) {
inner: for (let d=0; d<360; d += 1) {
let dx = Math.cos(d * DEG2RAD) * rad;
let dy = Math.sin(d * DEG2RAD) * rad;
bounds.set(dx - hdim.x, dx + hdim.x, dy - hdim.y, dy + hdim.y);
for (let w=0, wl=WIDGETS.length; w<wl; w++) {
let wt = WIDGETS[w];
if (wt === widget) {
continue;
}
let tpo = wt.track.pos;
let tbb = wt.getBoundingBox();
let dim = { x: (tbb.max.x - tbb.min.x) / 2, y : (tbb.max.y - tbb.min.y) / 2 };
target.set(tpo.x - dim.x, tpo.x + dim.x, tpo.y - dim.y, tpo.y + dim.y);
if (target.overlaps(bounds, 5)) {
continue inner;
}
}
widget._move(dx, dy, widget.track.pos.z, true);
break outer;
}
}
}
function platformDelete(widget) {
if (!widget) {
return;
}
if (Array.isArray(widget)) {
let mc = widget.slice(), i;
for (i=0; i<mc.length; i++) {
platform.delete(mc[i].widget || mc[i]);
}
return;
}
KIRI.work.clear(widget);
WIDGETS.remove(widget);
Widget.Groups.remove(widget);
SPACE.world.remove(widget.mesh);
selectedMeshes.remove(widget.mesh);
updateSliderMax();
platform.compute_max_z();
if (MODE !== MODES.FDM) {
platform.layout();
}
SPACE.update();
platformUpdateSelected();
if (feature.drop_layout) platform.layout();
API.event.emit('widget.delete', widget);
platformChanged();
auto_save();
}
function platformChanged() {
let fts = $('ft-select');
fts.innerHTML = '';
for (let w of WIDGETS) {
let b = DOC.createElement('button');
fts.appendChild(b);
b.innerText = w.meta.file || 'no name';
let color;
b.onmouseenter = function() {
color = w.getColor();
w.setColor(0x0088ff);
};
b.onmouseleave = function() {
w.setColor(color);
};
b.onclick = function() {
platformSelect(w, true, false);
color = w.getColor();
};
}
}
function platformSelectAll() {
forAllWidgets(function(w) { platform.select(w, true, false) })
}
function platformLayout(event, space = settings.controller.spaceLayout) {
let auto = UI.autoLayout.checked,
proc = settings.process,
dev = settings.device,
isBelt = dev.bedBelt,
oldmode = viewMode,
layout = (viewMode === VIEWS.ARRANGE && auto);
switch (MODE) {
case MODES.SLA:
space = space || (proc.slaSupportLayers && proc.slaSupportDensity ? 2 : 1);
break;
case MODES.CAM:
case MODES.LASER:
space = space || proc.outputTileSpacing || 1;
break;
case MODES.FDM:
space = space || ((proc.sliceSupportExtra || 0) * 2) + 1;
// auto resize device to support a larger object
if (isBelt) {
fitDeviceToWidgets();
}
break;
}
setViewMode(VIEWS.ARRANGE);
hideSlices();
auto_save();
// only auto-layout when in arrange mode
if (oldmode !== VIEWS.ARRANGE) {
API.event.emit('platform.layout');
return SPACE.update();
}
// do not layout when switching back from slice view
if (!auto || (!space && !layout)) {
API.event.emit('platform.layout');
return SPACE.update();
}
let gap = space;
// in CNC mode with >1 widget, force layout with spacing @ 1.5x largest tool diameter
if (MODE === MODES.CAM && WIDGETS.length > 1) {
let spacing = space || 1, CAM = KIRI.driver.CAM;
if (proc.camRoughOn) spacing = Math.max(spacing, CAM.getToolDiameter(settings, proc.camRoughTool));
if (proc.camOutlineOn) spacing = Math.max(spacing, CAM.getToolDiameter(settings, proc.camOutlineTool));
gap = spacing * 1.5;
}
// space parts to account for anchor in belt mode
if (isBelt) {
gap += proc.firstLayerBeltLead || 0;
}
let i, m, sz = isBelt ? {x:1, y:100000} : SPACE.platform.size(),
mp = [sz.x, sz.y],
ms = [mp[0] / 2, mp[1] / 2],
c = Widget.Groups.blocks().sort(MOTO.Sort),
p = new MOTO.Pack(ms[0], ms[1], gap).fit(c);
while (!p.packed) {
ms[0] *= 1.1;
ms[1] *= 1.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.move(p.max.w / 2 - m.fit.x, p.max.h / 2 - m.fit.y, 0, true);
// m.material.visible = true;
}
if (isBelt) {
let bounds = platformUpdateBounds(),
movey = -(dev.bedDepth / 2 + bounds.min.y);
forAllWidgets(widget => {
// only move the root widget in the group
if (widget.id === widget.group.id) {
widget.move(0, movey + 5, 0)
}
});
}
platform.update_origin();
API.event.emit('platform.layout');
SPACE.update();
}
/** ******************************************************************
* Settings Functions
******************************************************************* */
// given a settings region, update values of matching bound UI fields
function updateFieldsFromSettings(setrec, uirec = UI, trace) {
if (!setrec) {
return console.trace("missing scope");
}
for (let key in setrec) {
if (!setrec.hasOwnProperty(key)) {
continue;
}
let val = setrec[key];
if (!uirec.hasOwnProperty(key)) {
continue;
}
let uie = uirec[key], typ = uie ? uie.type : null;
if (typ === 'text') {
if (uie.setv) {
uie.setv(val);
} else {
uie.value = val;
}
} else if (typ === 'checkbox') {
uie.checked = val;
} else if (typ === 'select-one') {
uie.innerHTML = '';
let source = uie.parentNode.getAttribute('source'),
list = uie._source || settings[source] || lists[source],
chosen = null;
if (list) list.forEach(function(el, index) {
let id = el.id || el.name;
let ev = el.value || id;
if (val == id) {
chosen = index;
}
let opt = DOC.createElement('option');
opt.appendChild(DOC.createTextNode(el.name));
opt.setAttribute('value', ev);
uie.appendChild(opt);
});
if (chosen) {
uie.selectedIndex = chosen;
}
} else if (typ === 'textarea') {
if (Array.isArray(val)) {
uie.value = val.join('\n');
} else {
uie.value = '';
}
}
}
}
function isEquals(o1, o2) {
if (o1 == o2) return true;
if (Array.isArray(o1) && Array.isArray(o2)) {
if (o1.length === o2.length) {
for (let i=0; i<o1.length; i++) {
if (o1[i] !== o2[i]) {
return false;
}
}
return true;
}
} else if (typeof(o1) === 'object' && typeof(o2) === 'object') {
let keys = Object.keys(Object.assign({}, o1, o2));
for (let key of keys) {
if (o1[key] !== o2[key]) {
return false;
}
}
return true;
}
return false;
}
/**
* @returns {Object}
*/
function updateSettingsFromFields(setrec, uirec = UI, changes) {
if (!setrec) {
return console.trace("missing scope");
}
let lastChange = UC.lastChange();
// for each key in setrec object
for (let key in setrec) {
if (!setrec.hasOwnProperty(key)) {
// console.log({no_setrec: key});
continue;
}
if (!uirec.hasOwnProperty(key)) {
// console.log({no_uirec: key});
continue;
}
let nval = null, uie = uirec[key];
// skip empty UI values
if (!uie || uie === '') {
// console.log({uie_empty: key});
continue;
}
if (uie.type === 'text') {
nval = uirec[key].convert();
} else if (uie.type === 'checkbox') {
nval = uirec[key].checked;
} else if (uie.type === 'select-one') {
if (uie.selectedIndex >= 0) {
nval = uie.options[uie.selectedIndex].value;
let src = uie.parentNode.getAttribute('source');
if (src === 'tools') {
nval = parseInt(nval);
}
} else {
nval = setrec[key];
}
} else if (uie.type === 'textarea') {
nval = uie.value.trim().split('\n').filter(v => v !== '');
} else {
continue;
}
if (lastChange === uie) {
if (changes) changes[key] = nval;
}
if (!isEquals(setrec[key], nval)) {
setrec[key] = nval;
if (changes) {
changes[key] = nval;
}
}
}
return settings;
}
function updateFields() {
updateFieldsFromSettings(settings.device);
updateFieldsFromSettings(settings.process);
updateFieldsFromSettings(settings.controller);
updateExtruderFields(settings.device);
}
function updateExtruderFields(device) {
if (device.extruders && device.extruders[device.internal]) {
updateFieldsFromSettings(device.extruders[device.internal]);
UI.extruder.firstChild.innerText = `${LANG.dv_gr_ext} [${device.internal+1}/${device.extruders.length}]`;
UI.extPrev.disabled = device.internal === 0;
UI.extPrev.onclick = function() {
device.internal--;
updateExtruderFields(device);
};
UI.extNext.disabled = device.internal === device.extruders.length - 1;
UI.extNext.onclick = function() {
device.internal++;
updateExtruderFields(device);
};
UI.extDel.disabled = UI.extDel.disabled || device.extruders.length < 2;
UI.extDel.onclick = function() {
device.extruders.splice(device.internal,1);
device.internal = Math.min(device.internal, device.extruders.length-1);
updateExtruderFields(device);
};
UI.extAdd.onclick = function() {
let copy = clone(device.extruders[device.internal]);
copy.extSelect = [`T${device.extruders.length}`];
copy.extDeselect = [];
device.extruders.push(copy);
device.internal = device.extruders.length - 1;
updateExtruderFields(device);
};
}
}
function updateSettings(opt = {}) {
let { controller, device, process, mode, sproc, cproc } = settings;
updateSettingsFromFields(controller);
switch (controller.units) {
case 'mm': UC.setUnits(1); break;
case 'in': UC.setUnits(25.4); break;
}
if (opt.controller) {
return;
}
updateSettingsFromFields(device, undefined, undefined, true);
// range-specific values
if (settings.mode === 'FDM' && viewMode === VIEWS.SLICE) {
let changes = {};
let values = process;
let { layer_lo, layer_hi, layer_max } = API.var;
let range = { lo: layer_lo, hi: layer_hi };
let add = false;
if (layer_lo > 0 || layer_hi < layer_max) {
values = Object.clone(process);
add = true;
}
updateSettingsFromFields(values, undefined, changes);
if (range) {
updateRange(range.lo, range.hi, changes, add);
}
} else {
updateSettingsFromFields(process);
}
if (device.extruders && device.extruders[device.internal]) {
updateSettingsFromFields(device.extruders[device.internal]);
}
API.conf.save();
let compare = sproc[mode][cproc[mode]];
let same = true;
if (compare)
for (let [key, val] of Object.entries(compare).filter(v => v[0] !== 'processName')) {
let tval = process[key];
// outputLoopLayers misbehaving and setting null on empty
if (val === '' && tval == null) {
continue;
}
if (Array.isArray(tval) && Array.isArray(val)) {
if (JSON.stringify(tval) == JSON.stringify(val)) {
continue;
}
}
if (tval != val) {
// console.log(key, 'expected', val, 'got', tval);
same = false;
}
}
$('mode-device').innerText = device.deviceName;
$('mode-profile').innerText = `${cproc[mode]}${same ? '' : ' *'}`;
}
function saveSettings() {
const view = SPACE.view.save();
if (view.left || view.up) {
settings.controller.view = view;
}
const mode = settings.mode;
settings.sproc[mode].default = settings.process;
settings.sproc[mode][settings.process.processName] = settings.process;
settings.device.bedBelt = UI.deviceBelt.checked;
settings.device.bedRound = UI.deviceRound.checked;
settings.device.originCenter = UI.deviceOrigin.checked || UI.deviceRound.checked;
settings.device.fwRetract = UI.fwRetract.checked;
SDB.setItem('ws-settings', JSON.stringify(settings));
API.event.emit('settings.saved', settings);
}
function settingsImportZip(data, ask) {
let alert = API.show.alert("Importing Workspace");
JSZip.loadAsync(data).then(zip => {
for (let [key,value] of Object.entries(zip.files)) {
if (key === "workspace.json") {
value.async("string").then(json => {
API.hide.alert(alert);
settingsImport(JSON.parse(json), ask);
});
}
}
});
}
function settingsImport(data, ask) {
if (typeof(data) === 'string') {
try {
data = API.util.b64dec(data);
} catch (e) {
UC.alert('invalid import format');
console.log('data',data);
return;
}
}
if (LOCAL) console.log('import',data);
let isSettings = (data.settings && data.version && data.time);
let isProcess = (data.process && data.version && data.time && data.mode && data.name);
let isDevice = (data.device && data.version && data.time);
let isWork = (data.work);
if (!isSettings && !isDevice && !isProcess) {
UC.alert('invalid settings or device format');
console.log('data',data);
return;
}
function doit() {
if (isDevice) {
if (settings.devices[data.device]) {
UC.confirm(`Replace device ${data.device}?`).then(yes => {
if (yes) {
settings.devices[data.device] = data.code;
API.show.devices();
}
});
} else {
settings.devices[data.device] = data.code;
API.show.devices();
}
}
if (isProcess) {
if (settings.sproc[data.mode][data.name]) {
UC.confirm(`Replace process ${data.name}?`).then(yes => {
if (yes) {
settings.sproc[data.mode][data.name] = data.process;
API.conf.show();
}
});
} else {
settings.sproc[data.mode][data.name] = data.process;
API.conf.show();
}
}
if (isSettings) {
clearWorkspace();
settings = CONF.normalize(data.settings);
SDB.setItem('ws-settings', JSON.stringify(settings));
restoreSettings();
if (LOCAL) console.log('settings',Object.clone(settings));
if (isWork) {
API.platform.clear();
KIRI.codec.decode(data.work).forEach(widget => {
platform.add(widget, 0, true);
});
if (data.view) {
SPACE.view.load(data.view);
}
}
restoreWorkspace(() => {
UI.sync();
}, true);
}
}
if (ask) {
let opt = {};
let prompt = isDevice ?
`Import device "${data.device}"?` : isProcess ?
`Import process "${data.name}"?` :
`Import settings made in Kiri:Moto version ${data.version} on<br>${new Date(data.time)}?`;
if (data.screen) {
opt.pre = [
'<div class="f-col a-center">',
`<img src="${data.screen}" style="width:300px"/>`,
'</div>'
];
}
UC.confirm(prompt,undefined,undefined,opt).then((yes) => {
if (yes) doit();
});
} else {
doit();
}
}
function settingsExport(opts = {}) {
const note = opts.node || undefined;
const shot = opts.work || opts.screen ? SPACE.screenshot() : undefined;
const work = opts.work ? KIRI.codec.encode(WIDGETS,{_json_:true}) : undefined;
const view = opts.work ? SPACE.view.save() : undefined;
const setn = Object.clone(settings);
// stuff in legacy annotations for re-import
for (let w of WIDGETS) {
setn.widget[w.id] = w.anno;
}
const xprt = {
settings: setn,
version: KIRI.version,
screen: shot,
space: SPACE.info,
note: note,
work: work,
view: view,
moto: MOTO.id,
init: SDB.getItem('kiri-init'),
time: Date.now()
};
return opts.clear ? xprt : API.util.b64enc(xprt);
}
function platformLoadFiles(files,group) {
let loaded = files.length;
platform.group();
for (let i=0; i<files.length; i++) {
const file = files[i],
reader = new FileReader(),
lower = files[i].name.toLowerCase(),
israw = lower.indexOf(".raw") > 0 || lower.indexOf('.') < 0,
isstl = lower.indexOf(".stl") > 0,
isobj = lower.indexOf(".obj") > 0,
is3mf = lower.indexOf(".3mf") > 0,
issvg = lower.indexOf(".svg") > 0,
ispng = lower.indexOf(".png") > 0,
isjpg = lower.indexOf(".jpg") > 0,
isgcode = lower.indexOf(".gcode") > 0 || lower.indexOf(".nc") > 0,
isset = lower.indexOf(".b64") > 0 || lower.indexOf(".km") > 0,
iskmz = lower.indexOf(".kmz") > 0;
reader.file = files[i];
reader.onloadend = function (e) {
function load_dec() {
if (--loaded === 0) platform.group_done(isgcode);
}
if (israw) {
platform.add(
newWidget(undefined,group)
.loadVertices(JSON.parse(e.target.result).toFloat32())
);
load_dec();
}
else if (API.feature.on_load && (isstl || isobj || is3mf)) {
API.feature.on_load(e.target.result, file);
load_dec();
}
else if (isstl) {
if (API.feature.on_add_stl) {
API.feature.on_add_stl(e.target.result, file);
} else {
platform.add(
newWidget(undefined,group)
.loadVertices(new LOAD.STL().parse(e.target.result,unitScale()))
.saveToCatalog(e.target.file.name)
);
}
load_dec();
}
else if (isobj) {
let objs = LOAD.OBJ.parse(e.target.result);
let odon = function() {
for (let obj of objs) {
let name = e.target.file.name;
if (obj.name) {
name = obj.name + ' - ' + name;
}
platform.add(
newWidget(undefined,group)
.loadVertices(obj.toFloat32(), true)
.saveToCatalog(name)
);
}
load_dec();
};
if (objs.length > 1 && !group) {
UC.confirm('group objects?').then(ok => {
if (ok) {
group = [];
}
odon();
});
} else {
odon();
}
}
else if (is3mf) {
let odon = function(models) {
let msg = API.show.alert('Adding Objects');
for (let model of models) {
let name = e.target.file.name;
if (model.name) {
name = model.name + ' - ' + name;
}
platform.add(
newWidget(undefined,group)
.loadVertices(model.faces.toFloat32())
.saveToCatalog(name)
);
}
load_dec();
API.hide.alert(msg);
}
let msg = API.show.alert('Decoding 3MF');
LOAD.TMF.parseAsync(e.target.result).then(models => {
API.hide.alert(msg);
if (models.length > 1 && !group) {
UC.confirm(`group ${models.length} objects?`).then(ok => {
if (ok) {
group = [];
}
odon(models);
});
} else {
odon(models);
}
});
}
else if (isgcode) {
loadCode(e.target.result, 'gcode');
load_dec();
}
else if (issvg) {
if (MODE === MODES.LASER) {
loadCode(e.target.result, 'svg');
} else {
let name = e.target.file.name;
let svg = LOAD.SVG.parse(e.target.result);
let ind = 0;
for (let v of svg) {
let num = ind++;
platform.add(
newWidget(undefined, group)
.loadVertices(svg[num].toFloat32())
.saveToCatalog(num ? `${name}-${num}` : name)
);
}
}
load_dec();
}
else if (iskmz) settingsImportZip(e.target.result, true);
else if (isset) settingsImport(e.target.result, true);
else if (ispng) loadImageDialog(e.target.result, e.target.file.name);
else if (isjpg) loadImageConvert(e.target.result, e.target.file.name);
else API.show.alert(`Unsupported file: ${files[i].name}`);
};
if (isstl || ispng || isjpg || iskmz) {
reader.readAsArrayBuffer(reader.file);
} else {
reader.readAsBinaryString(reader.file);
}
}
}
function loadImageConvert(res, name) {
let url = URL.createObjectURL(new Blob([res]));
$('mod-any').innerHTML = `<img id="xsrc" src="${url}"><canvas id="xdst"></canvas>`;
let img = $('xsrc');
let can = $('xdst');
img.onload = () => {
can.width = img.width;
can.height = img.height;
let ctx = can.getContext('2d');
ctx.drawImage(img, 0, 0);
fetch(can.toDataURL()).then(r => r.arrayBuffer()).then(data => {
loadImageDialog(data, name);
});
};
}
function loadFile() {
$('load-file').onchange = function(event) {
console.log(event);
platformLoadFiles(event.target.files);
};
$('load-file').click();
// alert2("drag/drop STL files onto platform to import\nreload page to return to last saved state");
}
function saveWorkspace(quiet) {
API.conf.save();
let newWidgets = [],
oldWidgets = js2o(SDB.getItem('ws-widgets'), []);
forAllWidgets(function(widget) {
if (widget.synth) return;
newWidgets.push(widget.id);
oldWidgets.remove(widget.id);
widget.saveState();
let ann = API.widgets.annotate(widget.id);
ann.file = widget.meta.file;
ann.url = widget.meta.url;
});
SDB.setItem('ws-widgets', o2js(newWidgets));
oldWidgets.forEach(function(wid) {
Widget.deleteFromState(wid);
});
// eliminate dangling saved widgets
ODB.keys(keys => {
keys.forEach(key => {
if (newWidgets.indexOf(key.substring(8)) < 0) {
ODB.remove(key);
}
})
}, "ws-save-" ,"ws-savf");
if (!quiet) {
alert2("workspace saved", 1);
}
}
function restoreSettings(save) {
let newset = ls2o('ws-settings') || settings;
// extract legacy widget annotations into widgets
if (newset.widget) {
for (let id of Object.keys(newset.widget)) {
let anno = newset.widget[id];
let wid = WIDGETS.filter(w => w.id === id)[0];
if (wid && anno) {
wid.anno = anno;
console.log('transfer settings annotations to widget', id);
delete newset.widget[id];
wid.saveState();
} else {
console.log('missing widget for annotations', id);
}
}
}
settings = CONF.normalize(newset);
// override camera from settings
if (settings.controller.view) {
SDB.removeItem('ws-camera');
}
// merge custom filters from localstorage into settings
localFilters.forEach(function(fname) {
let fkey = "gcode-filter-"+fname, ov = ls2o(fkey);
if (ov) settings.devices[fname] = ov;
SDB.removeItem(fkey)
});
SDB.removeItem(localFilterKey);
// save updated settings
if (save) API.conf.save();
return newset;
}
function restoreWorkspace(ondone, skip_widget_load) {
let newset = restoreSettings(false),
camera = newset.controller.view,
toload = ls2o('ws-widgets',[]),
loaded = 0,
position = true;
updateFields();
platform.update_size();
SPACE.view.reset();
if (camera) {
SPACE.view.load(camera);
} else {
SPACE.view.home();
}
if (skip_widget_load) {
if (ondone) {
ondone();
}
return;
}
// remove any widgets from platform
forAllWidgets(function(widget) {
platform.delete(widget);
});
// remove widget keys if they are not going to be restored (TODO: remove in 3.1)
if (settings.widget)
Object.keys(settings.widget).filter(k => toload.indexOf(k) < 0).forEach(k => {
delete settings.widget[k];
});
// load any widget by name that was saved to the workspace
toload.forEach(function(widgetid) {
Widget.loadFromState(widgetid, function(widget) {
if (widget) {
platform.add(widget, 0, position);
let ann = API.widgets.annotate(widgetid);
widget.meta.file = ann.file;
widget.meta.url = ann.url;
}
if (++loaded === toload.length) {
platform.deselect();
if (ondone) {
ondone();
setTimeout(() => {
platform.update_top_z();
SPACE.update();
}, 1);
}
}
}, position);
});
return toload.length > 0;
}
function clearWorkspace() {
// free up worker cache/mem
KIRI.work.clear();
platform.select_all();
platform.delete(selectedMeshes);
}
function modalShowing() {
return UI.modal.style.display === 'flex';
}
function showModal(which) {
let mod = UI.modal,
style = mod.style,
visible = modalShowing(),
info = { pct: 0 };
["help","setup","tools","prefs","saves","files","print","local","any"].forEach(function(name) {
UI[name].style.display = name === which ? 'flex' : '';
});
function ondone() {
API.event.emit('modal.show', which);
}
if (visible) {
return ondone();
}
style.height = '0';
style.display = 'flex';
new TWEEN.Tween(info).
easing(TWEEN.Easing.Quadratic.InOut).
to({ pct: 100 }, 100).
onUpdate(() => { style.height = `${info.pct}%` }).
onComplete(ondone).
start();
}
function hideModal() {
if (!modalShowing()) {
return;
}
let mod = UI.modal, style = mod.style, info={pct:100};
new TWEEN.Tween(info).
easing(TWEEN.Easing.Quadratic.InOut).
to({pct:0}, 100).
onUpdate(() => { style.height = `${info.pct}%` }).
onComplete(() => { style.display = '' }).
start();
}
function showCatalog() {
showModal("files");
}
function getSettings() {
return settings;
}
function putSettings(newset) {
settings = CONF.normalize(newset);
API.conf.save()
API.space.restore(null, true);
}
function editSettings(e) {
let mode = getMode(),
name = e.target.getAttribute("name"),
load = settings.sproc[mode][name],
edit = prompt(`settings for "${name}"`, JSON.stringify(load));
if (edit) {
try {
settings.sproc[mode][name] = JSON.parse(edit);
if (name === settings.process.processName) {
API.conf.load(null, name);
}
API.conf.save();
} catch (e) {
UC.alert('malformed settings object');
}
}
}
function exportSettings(e) {
let mode = getMode(),
name = e.target.getAttribute("name"),
data = API.util.b64enc({
process: settings.sproc[mode][name],
version: KIRI.version,
moto: MOTO.id,
time: Date.now(),
mode,
name
});
UC.prompt("Export Process Filename", name).then(name => {
if (name) {
API.util.download(data, `${name}.km`);
}
});
}
function loadSettings(e, named) {
let mode = getMode(),
name = e ? e.target.getAttribute("load") : named || currentProcessName() || "default",
load = settings.sproc[mode][name];
if (!load) return;
// cloning loaded process into settings requires user to save
// process before switching devices or risk losing any changes
settings.process = clone(load);
// update process name
settings.process.processName = name;
// save named process with the current device
settings.devproc[currentDeviceName()] = name;
// preserve name of last library loaded
if (name !== 'default') {
settings.cproc[mode] = name;
}
// allow mode driver to take any necessary actions
API.event.emit("settings.load", settings);
// update UI fields to reflect current settings
updateFields();
API.conf.update();
if (e) triggerSettingsEvent();
}
function deleteSettings(e) {
let name = e.target.getAttribute("del");
delete settings.sproc[getMode()][name];
updateSettingsList();
API.conf.save();
triggerSettingsEvent();
}
function updateSettingsList() {
let list = [], s = settings, sp = s.sproc[getMode()] || {}, table = UI.settingsList;
table.innerHTML = '';
for (let k in sp) {
if (sp.hasOwnProperty(k)) list.push(k);
}
list.filter(n => n !=='default').sort().forEach(function(sk) {
let row = DOC.createElement('div'),
load = DOC.createElement('button'),
edit = DOC.createElement('button'),
xprt = DOC.createElement('button'),
del = DOC.createElement('button'),
name = sk;
load.setAttribute('load', sk);
load.onclick = (ev) => {
API.conf.load(undefined, sk);
updateSettingsList();
hideModal();
}
load.appendChild(DOC.createTextNode(sk));
if (sk == settings.process.processName) {
load.setAttribute('class', 'selected')
}
UI.settingsName.value = settings.process.processName;
del.setAttribute('del', sk);
del.setAttribute('title', "remove '"+sk+"'");
del.innerHTML = '<i class="far fa-trash-alt"></i>';
del.onclick = deleteSettings;
edit.setAttribute('name', sk);
edit.setAttribute('title', 'edit');
edit.innerHTML = '<i class="far fa-edit"></i>';
edit.onclick = editSettings;
xprt.setAttribute('name', sk);
xprt.setAttribute('title', 'export');
xprt.innerHTML = '<i class="fas fa-download"></i>';
xprt.onclick = exportSettings;
row.setAttribute("class", "flow-row");
row.appendChild(edit);
row.appendChild(load);
row.appendChild(xprt);
row.appendChild(del);
table.appendChild(row);
});
}
function showSettings() {
updateSettingsList();
showModal("saves");
UI.settingsName.focus();
}
function showHelp() {
showHelpFile(`local`,() => {});
}
function showHelpFile(local,then) {
if (!local) {
WIN.open("//docs.grid.space/", "_help");
return;
}
$('kiri-version').innerHTML = `${LANG.version} ${KIRI.version}`;
showModal('help');
API.event.emit('help.show', local);
}
function showLocal() {
showModal('local');
API.probe.local((err,data) => {
let devc = 0;
let bind = [];
let html = ['<table>'];
html.push(`<thead><tr><th>device</th><th>type</th><th>status</th><th></th></tr></thead>`);
html.push(`<tbody>`);
let recs = [];
for (let k in data) {
recs.push(data[k].stat);
}
recs.sort((a,b) => {
return a.device.name < b.device.name ? -1 : 1;
});
for (let r of recs) {
bind.push({uuid: r.device.uuid, host: r.device.addr[0], port: r.device.port});
html.push(`<tr>`);
html.push(`<td>${r.device.name}</td>`);
html.push(`<td>${r.device.mode}</td>`);
html.push(`<td>${r.state}</td>`);
html.push(`<td><button id="${r.device.uuid}">admin</button></td>`);
html.push(`</tr>`);
devc++;
}
html.push(`</tbody>`);
html.push(`</table>`);
if (devc) {
$('mod-local').innerHTML = html.join('');
} else {
$('mod-local').innerHTML = `<br><b>no local devices</b>`;
}
bind.forEach(rec => {
$(rec.uuid).onclick = () => {
window.open(`http://${rec.host}:${rec.port||4080}/`);
};
});
});
}
function setViewMode(mode) {
const oldMode = viewMode;
const isCAM = settings.mode === 'CAM';
viewMode = mode;
platform.deselect();
updateSelectedInfo();
// disable clear in non-arrange modes
$('view-clear').style.display = mode === VIEWS.ARRANGE ? '' : 'none';
switch (mode) {
case VIEWS.ARRANGE:
complete = {};
UI.back.style.display = '';
UI.render.style.display = '';
KIRI.client.clear();
STACKS.clear();
hideSlider();
updateSpeeds();
setVisibleLayer();
setWidgetVisibility(true);
setOpacity(1);
break;
case VIEWS.SLICE:
UI.back.style.display = 'flex';
if (!isCAM) UI.render.classList.remove('lt-enabled');
updateSpeeds();
updateSliderMax();
setWidgetVisibility(true);
break;
case VIEWS.PREVIEW:
UI.back.style.display = 'flex';
if (!isCAM) UI.render.classList.remove('lt-enabled');
setWidgetVisibility(true);
break;
default:
console.log("invalid view mode: "+mode);
return;
}
API.event.emit('view.set', mode);
DOC.activeElement.blur();
}
function getMode() {
return settings.mode;
}
function getModeLower() {
return getMode().toLowerCase();
}
function switchMode(mode) {
setMode(mode, platform.update_size);
}
function setMode(mode, lock, then) {
if (!MODES[mode]) {
console.log("invalid mode: "+mode);
mode = 'FDM';
}
// change mode constants
settings.mode = mode;
MODE = MODES[mode];
DRIVER = KIRI.driver[mode];
// update mode display
$('app-mode-name').innerHTML = mode === 'CAM' ? 'CNC' : mode;
// highlight relevant device mode button
["fdm","sla","cam","laser"].forEach(dev => {
let cl = $(`mode-${dev}`).classList;
if (dev === mode.toLowerCase()) {
cl.add("dev-sel");
} else {
cl.remove("dev-sel");
}
});
// restore cached device profile for this mode
if (settings.cdev[mode]) {
settings.device = clone(settings.cdev[mode]);
API.event.emit('device.set', currentDeviceName());
}
// really belongs in CAM driver (lots of work / abstraction needed)
// updateStockVisibility();
// updates right-hand menu by enabling/disabling fields
setViewMode(VIEWS.ARRANGE);
UC.setMode(MODE);
// sanitize and persist settings
API.conf.load();
API.conf.save();
// other housekeeping
triggerSettingsEvent();
platformUpdateSelected();
updateSelectedBounds(WIDGETS);
updateFields();
// because device dialog, if showing, needs to be updated
if (modalShowing()) {
API.show.devices();
}
API.space.restore(null, true);
API.event.emit("mode.set", mode);
if (then) {
then();
}
}
function currentDeviceName() {
return settings.filter[getMode()];
}
function currentDeviceCode() {
return settings.devices[currentDeviceName()];
}
function currentProcessName() {
return settings.cproc[getMode()];
}
function currentProcessCode() {
return settings.sproc[getMode()][currentProcessName()];
}
function setControlsVisible(show) {
$('mid-lcol').style.display = show ? 'flex' : 'none';
$('mid-rcol').style.display = show ? 'flex' : 'none';
}
function downloadBlob(data, filename) {
let url = WIN.URL.createObjectURL(new Blob([data], {type: "octet/stream"}));
$('mod-any').innerHTML = `<a id="_dexport_" href="${url}" download="${filename}">x</a>`;
$('_dexport_').click();
}
// prevent safari from exiting full screen mode
DOC.onkeydown = function (evt) { if (evt.keyCode == 27) evt.preventDefault() }
// K3DB.onIdle(function() {
// upgrade on db idle. maybe delay init, too
// console.log("k3db idle");
// });
// run optional module functions NOW before kiri-init has run
if (Array.isArray(self.kirimod)) {
kirimod.forEach(function(mod) { mod(kiri.api) });
}
// new module loading
kiri.loader.forEach(mod => { mod(kiri.api)} );
kiri.load = (mod) => { mod(kiri.api) };
// upon restore, seed presets
API.event.emit('preset', API.conf.dbo());
})();