move client, conf to ui
This commit is contained in:
parent
a0091b8abb
commit
b1d4750ed1
12 changed files with 28 additions and 28 deletions
|
|
@ -1,11 +1,11 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import alerts from '../ui/alerts.js';
|
||||
import settings from './settings.js'
|
||||
import settings from '../ui/config/manager.js';
|
||||
import STACKS from '../ui/stacks.js';
|
||||
|
||||
import { broker } from '../../moto/broker.js';
|
||||
import { client as work } from './client.js';
|
||||
import { client as work } from '../ui/client.js';
|
||||
import { consts, COLOR as color, LISTS as lists } from './consts.js';
|
||||
import { device, devices } from '../ui/devices.js';
|
||||
import { catalog, dialog, event, group, help, hide, image } from './main.js';
|
||||
|
|
|
|||
|
|
@ -1,358 +0,0 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { api } from './api.js';
|
||||
import { noop } from './utils.js';
|
||||
import { codec } from './codec.js';
|
||||
import { Widget } from './widget.js';
|
||||
import { newPrint } from './print.js';
|
||||
|
||||
// this code runs in kiri's main loop
|
||||
let debug = self.debug === true,
|
||||
time = Date.now,
|
||||
seqid = 1,
|
||||
syncd = {},
|
||||
running = {},
|
||||
worker = null,
|
||||
minions = false,
|
||||
restarting = false,
|
||||
workpath,
|
||||
poolpath;
|
||||
|
||||
/**
|
||||
* @param {Function} fn name of function in kiri.worker
|
||||
* @param {Object} data to send to server
|
||||
* @param {Function} onreply function to call on reply messages
|
||||
* @param {Object[]} zerocopy array of objects to pass using zerocopy
|
||||
*/
|
||||
function send(fn, data, onreply, zerocopy) {
|
||||
let seq = seqid++;
|
||||
|
||||
if (onreply) {
|
||||
// track it only when we expect and can handle a reply
|
||||
running[seq] = { fn:onreply };
|
||||
}
|
||||
let msg = {
|
||||
seq: seq,
|
||||
task: fn,
|
||||
time: time(),
|
||||
data: data
|
||||
};
|
||||
// console.log('client send', msg);
|
||||
// if (!fn) { console.trace({empty_fn: data}) };
|
||||
try {
|
||||
worker.postMessage(msg, zerocopy);
|
||||
} catch (error) {
|
||||
console.trace('work send error', {data, error});
|
||||
}
|
||||
}
|
||||
|
||||
// code is running in the browser / client context
|
||||
export const client = {
|
||||
send: send,
|
||||
|
||||
setWorkPath(path) {
|
||||
workpath = path;
|
||||
return client;
|
||||
},
|
||||
|
||||
setPoolPath(path) {
|
||||
poolpath = path;
|
||||
return client;
|
||||
},
|
||||
|
||||
pool: {
|
||||
start() {
|
||||
minions = true;
|
||||
send("pool_start", { url: poolpath }, noop);
|
||||
},
|
||||
|
||||
stop() {
|
||||
minions = false;
|
||||
send("pool_stop", {}, noop);
|
||||
}
|
||||
},
|
||||
|
||||
newWorker() {
|
||||
if (self.createWorker) {
|
||||
return self.createWorker();
|
||||
} else if (self.kiri_work) {
|
||||
const work = atob(self.kiri_work);
|
||||
const blob = new Blob([ work ], { type: 'application/javascript' });
|
||||
return new Worker(URL.createObjectURL(blob));
|
||||
} else {
|
||||
let worker = new Worker(workpath || api.const.PATHS.work, { type: 'module' });
|
||||
worker.onerror = (error) => {
|
||||
console.log({ WORKER_ERROR: error });
|
||||
error.preventDefault();
|
||||
};
|
||||
worker.onmessageerror = (error) => {
|
||||
console.log({ WORKER_MESSAGE_ERROR: error });
|
||||
error.preventDefault();
|
||||
};
|
||||
return worker;
|
||||
}
|
||||
},
|
||||
|
||||
isBusy() {
|
||||
let current = 0;
|
||||
|
||||
for (let rec of Object.values(running)) {
|
||||
if (rec.fn) current++;
|
||||
}
|
||||
return current > 0;
|
||||
},
|
||||
|
||||
start() {
|
||||
client.restart();
|
||||
},
|
||||
|
||||
restart() {
|
||||
// prevent re-entry from cancel callback
|
||||
if (restarting) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (worker) {
|
||||
worker.terminate();
|
||||
}
|
||||
|
||||
restarting = true;
|
||||
|
||||
for (let key in running) {
|
||||
let rec = running[key];
|
||||
if (rec.fn) {
|
||||
rec.fn({error: "cancelled operation"});
|
||||
}
|
||||
}
|
||||
|
||||
syncd = {};
|
||||
running = {};
|
||||
worker = client.newWorker();
|
||||
|
||||
client.onmessage = worker.onmessage = (e) => {
|
||||
let now = time(),
|
||||
reply = e.data,
|
||||
record = running[reply.seq],
|
||||
onreply = record ? record.fn : undefined;
|
||||
|
||||
// console.log('client recv', e)
|
||||
if (reply.done) {
|
||||
delete running[reply.seq];
|
||||
}
|
||||
|
||||
// calculate and replace recv time
|
||||
reply.time_recv = now - reply.time_recv;
|
||||
|
||||
if (onreply) {
|
||||
onreply(reply.data, reply);
|
||||
} else {
|
||||
console.log({unexpected_reply: reply});
|
||||
}
|
||||
};
|
||||
|
||||
// restart minions if they were running
|
||||
if (minions) {
|
||||
client.pool.start();
|
||||
}
|
||||
|
||||
restarting = false;
|
||||
},
|
||||
|
||||
config(obj) {
|
||||
send("config", obj, noop);
|
||||
},
|
||||
|
||||
clear() {
|
||||
syncd = {};
|
||||
send("clear", {}, noop);
|
||||
},
|
||||
|
||||
snap(data) {
|
||||
send("snap", data, noop);
|
||||
},
|
||||
|
||||
png(data, ondone) {
|
||||
send("png", data, ondone);
|
||||
},
|
||||
|
||||
// widget sync
|
||||
sync(widgets) {
|
||||
if (!widgets) {
|
||||
widgets = api.widgets.all();
|
||||
}
|
||||
// sync any widget that has changed
|
||||
for (let widget of widgets.filter(w => w.modified || !syncd[w.id])) {
|
||||
syncd[widget.id] = true;
|
||||
let vertices = widget.getGeoVertices();
|
||||
send("sync", {
|
||||
id: widget.id,
|
||||
meta: widget.meta,
|
||||
group: widget.group.id,
|
||||
track: widget.track,
|
||||
vertices,
|
||||
position: widget.mesh.position,
|
||||
}, done => {
|
||||
widget.modified = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// for belt / angled printing
|
||||
rotate(settings, callback) {
|
||||
send("rotate", { settings }, reply => {
|
||||
if (reply.group) {
|
||||
// collect post-rotation data for slice/preview renders
|
||||
for (let widget of Widget.Groups.forid(reply.group)) {
|
||||
widget.belt = reply.belt;
|
||||
}
|
||||
} else if (callback) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// called once for each widget
|
||||
slice(settings, widget, callback) {
|
||||
send("slice", {
|
||||
id: widget.id,
|
||||
anno: widget.annotations(),
|
||||
settings: settings
|
||||
}, reply => {
|
||||
callback(reply);
|
||||
if (reply.done || reply.error) {
|
||||
// in belt mode, slicing modifies a widget and requires re-sync
|
||||
if (settings.device.bedBelt) {
|
||||
widget.modified = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// called once after each widget is sliced
|
||||
// performs merged actions that consider all widget slice data
|
||||
sliceAll(settings, callback) {
|
||||
send("sliceAll", { settings }, callback);
|
||||
},
|
||||
|
||||
// turn widget slices into unified path output
|
||||
prepare(settings, update, done) {
|
||||
send("prepare", { settings }, reply => {
|
||||
if (reply.progress) {
|
||||
update(reply.progress, reply.message, reply.layer);
|
||||
}
|
||||
if (reply.done) {
|
||||
done(reply.output, reply.maxSpeed, reply.minSpeed);
|
||||
}
|
||||
if (reply.error) {
|
||||
done(reply);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// convert path output to gcode or other target file output
|
||||
export(settings, online, ondone) {
|
||||
send("export", { settings }, reply => {
|
||||
if (reply.line) {
|
||||
online(reply.line);
|
||||
}
|
||||
if (reply.done) {
|
||||
ondone(reply.output);
|
||||
}
|
||||
if (reply.error) {
|
||||
ondone(null, reply.error);
|
||||
}
|
||||
if (reply.debug) {
|
||||
api.event.emit("export.debug", reply.debug);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
colors(colors, max, done) {
|
||||
send("colors", { colors, max }, reply => {
|
||||
done(reply);
|
||||
});
|
||||
},
|
||||
|
||||
parse(args, progress, done) {
|
||||
// have to do this client side because DOMParser is not in workers
|
||||
if (args.type === 'svg') {
|
||||
const { settings, code, type } = args;
|
||||
const origin = settings.origin;
|
||||
const offset = {
|
||||
x: origin.x,
|
||||
y: -origin.y,
|
||||
z: origin.z
|
||||
};
|
||||
const output = newPrint().parseSVG(code, offset);
|
||||
send("parse_svg", output, reply => {
|
||||
if (reply.parsed) {
|
||||
done(codec.decode(reply.parsed));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
send("parse", args, reply => {
|
||||
if (reply.progress) {
|
||||
progress(reply.progress);
|
||||
}
|
||||
if (reply.parsed) {
|
||||
done(codec.decode(reply.parsed), reply.maxSpeed, reply.minSpeed);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
image2mesh(data, progress, output) {
|
||||
send("image2mesh", data, reply => {
|
||||
if (reply.progress) {
|
||||
progress(reply.progress);
|
||||
}
|
||||
if (reply.vertices) {
|
||||
output(reply.vertices);
|
||||
}
|
||||
}, [ data.png ]);
|
||||
},
|
||||
|
||||
gerber2mesh(data, progress, output) {
|
||||
send('gerber2mesh', data, reply => {
|
||||
if (reply.progress) {
|
||||
progress(reply.progress);
|
||||
}
|
||||
if (reply.vertices) {
|
||||
output(reply.vertices);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
zip(files, progress, output) {
|
||||
send("zip", {files}, reply => {
|
||||
if (reply.percent !== undefined) {
|
||||
progress(reply);
|
||||
} else {
|
||||
output(reply);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
wasm(enable) {
|
||||
send("wasm", {enable}, reply => {
|
||||
// console.log({wasm_worker_said: reply});
|
||||
});
|
||||
},
|
||||
|
||||
putCache(key, data) {
|
||||
console.log({ putCache: key, data });
|
||||
send("putCache", {key, data}, reply => {
|
||||
// console.log({ putCache_reply: reply });
|
||||
});
|
||||
},
|
||||
|
||||
clearCache() {
|
||||
send("clearCache", {}, reply => {
|
||||
// console.log({ clearCache_reply: reply });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
self.kiri_client = client;
|
||||
|
||||
export default client;
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,7 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { api } from './api.js';
|
||||
import { client } from './client.js';
|
||||
import { client } from '../ui/client.js';
|
||||
import { codec } from './codec.js';
|
||||
import { space } from '../../moto/space.js';
|
||||
import { COLOR, PMODES } from './consts.js';
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { space } from '../../moto/space.js';
|
|||
import { LOCAL, SETUP } from './main.js';
|
||||
import { VIEWS, MODES, SEED } from './consts.js';
|
||||
import { beta, version } from '../../moto/license.js';
|
||||
import { settings as set_ctrl } from './settings.js';
|
||||
import { settings as set_ctrl } from '../ui/config/manager.js';
|
||||
import STACKS from '../ui/stacks.js';
|
||||
import { init as initCAM } from '../mode/cam/init-ui.js';
|
||||
import { init as initDRAG } from '../mode/drag/init-ui.js';
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { local as dataLocal } from '../../data/local.js';
|
|||
import { openFiles } from '../ui/files.js';
|
||||
import { platform } from './platform.js';
|
||||
import { selection } from './selection.js';
|
||||
import { settings } from './settings.js';
|
||||
import { settings } from '../ui/config/manager.js';
|
||||
import { showDevices } from '../ui/devices.js';
|
||||
import { space as SPACE } from '../../moto/space.js';
|
||||
import { stats } from '../ui/stats.js';
|
||||
|
|
|
|||
|
|
@ -1,816 +0,0 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { $, h } from '../../moto/webui.js';
|
||||
import { api } from './api.js';
|
||||
import { conf } from './conf.js';
|
||||
import { codec } from './codec.js';
|
||||
import { consts } from './consts.js';
|
||||
import { space } from '../../moto/space.js';
|
||||
import { local } from '../../data/local.js';
|
||||
import { utils } from './utils.js';
|
||||
import { version } from '../../moto/license.js';
|
||||
import { JSZip } from '../../ext/jszip-esm.js';
|
||||
|
||||
const { areEqual, ls2o, js2o } = utils;
|
||||
const { COLOR } = consts;
|
||||
const { clone } = Object;
|
||||
|
||||
const localFilterKey ='kiri-gcode-filters';
|
||||
const localFilters = js2o(local.getItem(localFilterKey)) || [];
|
||||
|
||||
let settings = clone(conf.template);
|
||||
|
||||
function normalize(set) {
|
||||
return conf.normalize(set);
|
||||
}
|
||||
|
||||
function getSettings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
function putSettings(newset) {
|
||||
settings = normalize(newset);
|
||||
saveSettings();
|
||||
api.space.restore(null, true);
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
const { ui } = api;
|
||||
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;
|
||||
// moved to init.onBooleanClick() -- delete later after confirming no side-effects
|
||||
// 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;
|
||||
local.setItem('ws-settings', JSON.stringify(settings));
|
||||
api.event.emit('settings.saved', settings);
|
||||
}
|
||||
|
||||
function loadSettings(evt, named) {
|
||||
let mode = settings.mode,
|
||||
name = evt ? evt.target.getAttribute("load") : named || api.process.get() || "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[api.device.get()] = 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
|
||||
api.conf.update_fields();
|
||||
api.conf.update();
|
||||
|
||||
// publish event when loading from ui event
|
||||
if (evt) api.event.settings();
|
||||
|
||||
// update hidden map from settings
|
||||
api.uc.setHidden(settings.hidden);
|
||||
}
|
||||
|
||||
function showSettings() {
|
||||
api.settings.sync.get().then(() => {
|
||||
api.dialog.update_process_list();
|
||||
api.modal.show("saves");
|
||||
api.ui.settingsName.focus();
|
||||
});
|
||||
}
|
||||
|
||||
function updateSettings(opt = {}) {
|
||||
const { controller, device, process, mode, sproc, cproc } = settings;
|
||||
const { uc } = api;
|
||||
const changes = {};
|
||||
|
||||
updateSettingsFromFields(controller, undefined, changes);
|
||||
|
||||
switch (controller.units) {
|
||||
case 'mm': uc.setUnits(1); break;
|
||||
case 'in': uc.setUnits(25.4); break;
|
||||
}
|
||||
|
||||
// limit update to controller fields (ui prefs dialog)
|
||||
if (opt.controller) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateSettingsFromFields(device, undefined, changes);
|
||||
|
||||
// range-specific values
|
||||
if (settings.mode === 'FDM' && api.view.is_slice()) {
|
||||
let changes = {};
|
||||
let values = process;
|
||||
const { lo: layer_lo, hi: layer_hi, max: layer_max } = api.slider.getRange();
|
||||
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;
|
||||
}
|
||||
// collect changes to pass to range updates
|
||||
updateSettingsFromFields(values, undefined, changes);
|
||||
if (range) {
|
||||
updateRange(range.lo, range.hi, changes, add);
|
||||
}
|
||||
} else {
|
||||
updateSettingsFromFields(process, undefined, changes);
|
||||
}
|
||||
|
||||
if (device.extruders && device.extruders[device.internal]) {
|
||||
updateSettingsFromFields(device.extruders[device.internal]);
|
||||
}
|
||||
|
||||
// invalidate slice, preview, export on settings changes
|
||||
if (Object.keys(changes).length) {
|
||||
api.function.clear_progress();
|
||||
}
|
||||
|
||||
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 updateSettingsFromFields(setrec, uirec = api.ui, changes) {
|
||||
if (!setrec) {
|
||||
return console.trace("missing scope");
|
||||
}
|
||||
let lastChange = api.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' || src == 'extruders') {
|
||||
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 (!areEqual(setrec[key], nval)) {
|
||||
setrec[key] = nval;
|
||||
if (changes) {
|
||||
changes[key] = nval;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
// given a settings region, update values of matching bound UI fields
|
||||
function updateFieldsFromSettings(setrec, uirec = api.ui, opt = {}) {
|
||||
if (!setrec) {
|
||||
updateFieldsFromSettings(settings.device);
|
||||
updateFieldsFromSettings(settings.process);
|
||||
updateFieldsFromSettings(settings.controller);
|
||||
updateExtruderFields(settings.device);
|
||||
return;
|
||||
}
|
||||
if (opt.trace) {
|
||||
console.log({ setrec, uirec });
|
||||
}
|
||||
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 (opt.trace) {
|
||||
console.log({ key, uie, typ, val });
|
||||
}
|
||||
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] || api.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 = document.createElement('option');
|
||||
opt.appendChild(document.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 = '';
|
||||
}
|
||||
} else if (opt.trace) {
|
||||
console.log('skipped', { key, val, typ });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateExtruderFields(device) {
|
||||
const { ui } = api;
|
||||
const { current: LANG } = api.const.LANG;
|
||||
if (device.extruders && device.extruders[device.internal]) {
|
||||
updateFieldsFromSettings(device.extruders[device.internal]);
|
||||
ui.extruder.innerHTML =
|
||||
`${LANG.dv_gr_ext}<label class='grow'></label>${device.internal+1} of ${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]);
|
||||
device.extruders.push(copy);
|
||||
device.internal = device.extruders.length - 1;
|
||||
updateExtruderFields(device);
|
||||
};
|
||||
api.uc.setClass($('tool-nozzle'), 'hide', false);
|
||||
h.bind($('ft-nozzle'), device.extruders.map((d,i) => h.div([
|
||||
h.button({
|
||||
_: `extruder ${i}`,
|
||||
onclick() {
|
||||
api.selection.for_widgets(w => {
|
||||
w.anno.extruder = i;
|
||||
w.setColor(
|
||||
api.selection.contains(w) ?
|
||||
COLOR.selected : COLOR.deselected,
|
||||
undefined,
|
||||
false);
|
||||
});
|
||||
}
|
||||
}),
|
||||
h.div({
|
||||
class: "splat",
|
||||
style: `background-color: #${COLOR.deselected[i].toString(16)}`
|
||||
})
|
||||
])));
|
||||
} else {
|
||||
api.uc.setClass($('tool-nozzle'), 'hide', true);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
api.conf.update_fields(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 && areEqual(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);
|
||||
|
||||
api.conf.update_fields(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() {
|
||||
if (true) return;
|
||||
if (settings.mode !== 'FDM' || !api.view.is_slice() || !settings.process.ranges) {
|
||||
let okeys = Object.keys(overrides);
|
||||
if (okeys.length) {
|
||||
api.conf.update_fields(overrides);
|
||||
overrides = {};
|
||||
}
|
||||
return;
|
||||
}
|
||||
let match = 0;
|
||||
let values = {};
|
||||
let restores = Object.clone(overrides);
|
||||
const { lo: layer_lo, hi: layer_hi } = api.slider.getRange();
|
||||
for (let range of getOverlappingRanges(layer_lo, 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) {
|
||||
api.conf.update_fields(values);
|
||||
}
|
||||
let rkeys = Object.keys(restores);
|
||||
if (rkeys.length) {
|
||||
api.conf.update_fields(restores);
|
||||
for (let key of rkeys) {
|
||||
delete overrides[key];
|
||||
}
|
||||
}
|
||||
api.uc.refresh();
|
||||
}
|
||||
|
||||
function restoreSettings(save) {
|
||||
const widgets = api.widgets.all();
|
||||
const 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);
|
||||
wid.saveState();
|
||||
} else {
|
||||
console.log('missing widget for annotations', id);
|
||||
}
|
||||
delete newset.widget[id];
|
||||
}
|
||||
}
|
||||
settings = normalize(newset);
|
||||
// override camera from settings
|
||||
if (settings.controller.view) {
|
||||
local.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;
|
||||
local.removeItem(fkey)
|
||||
});
|
||||
local.removeItem(localFilterKey);
|
||||
// save updated settings
|
||||
if (save) api.conf.save();
|
||||
|
||||
return newset;
|
||||
}
|
||||
|
||||
function settingsExport(opts = {}) {
|
||||
const widgets = api.widgets.all();
|
||||
const note = opts.node || undefined;
|
||||
const shot = opts.work || opts.screen ? space.screenshot() : undefined;
|
||||
const work = opts.work ? codec.encode(widgets,{_json_:true}) : undefined;
|
||||
const 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: version,
|
||||
screen: shot,
|
||||
space: space.info,
|
||||
note: note,
|
||||
work: work,
|
||||
view: view,
|
||||
moto: moto.id,
|
||||
init: local.getItem('kiri-init'),
|
||||
time: Date.now()
|
||||
};
|
||||
return opts.clear ? xprt : api.util.b64enc(xprt);
|
||||
}
|
||||
|
||||
function settingsImport(data, ask) {
|
||||
const { uc, ui } = api;
|
||||
if (typeof(data) === 'string') {
|
||||
try {
|
||||
data = data.charAt(0) === '{' ? JSON.parse(data) : api.util.b64dec(data);
|
||||
} catch (e) {
|
||||
uc.alert('invalid import format');
|
||||
console.log('data',data,{type: typeof data},e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (api.const.LOCAL) console.log('import', data);
|
||||
let isSettings = (data.settings && data.time);
|
||||
let isProcess = (data.process && data.time && data.mode && data.name);
|
||||
let isDevice = (data.device && data.time);
|
||||
let isTools = (data.tools && data.time)
|
||||
let isWork = (data.work);
|
||||
|
||||
if (!isSettings && !isDevice && !isProcess && !isTools) {
|
||||
uc.alert('invalid settings or device format');
|
||||
console.log('data',data);
|
||||
return;
|
||||
}
|
||||
function doit() {
|
||||
function devset() {
|
||||
settings.devices[data.device] = data.code;
|
||||
settings.devproc[data.device] = data.name;
|
||||
api.show.devices();
|
||||
}
|
||||
function procset() {
|
||||
settings.sproc[data.mode][data.name] = data.process;
|
||||
if (!isDevice) {
|
||||
api.conf.show();
|
||||
}
|
||||
}
|
||||
if (isDevice) {
|
||||
if (data.process && data.process.processName && data.code.mode) {
|
||||
data.mode = data.code.mode;
|
||||
data.name = data.process.processName;
|
||||
isProcess = true;
|
||||
}
|
||||
if (settings.devices[data.device]) {
|
||||
uc.confirm(`Replace device ${data.device}?`).then(yes => {
|
||||
if (yes) devset()
|
||||
});
|
||||
} else {
|
||||
devset();
|
||||
}
|
||||
}
|
||||
if (isProcess) {
|
||||
console.log({ data });
|
||||
if (data.name === 'default') data.name = data.device || data.name;
|
||||
if (settings.sproc[data.mode][data.name]) {
|
||||
uc.confirm(`Replace process ${data.name}?`).then(yes => {
|
||||
if (yes) procset();
|
||||
});
|
||||
} else {
|
||||
procset();
|
||||
}
|
||||
}
|
||||
if (isSettings) {
|
||||
api.space.clear();
|
||||
settings = normalize(data.settings);
|
||||
local.setItem('ws-settings', JSON.stringify(settings));
|
||||
if (api.const.LOCAL) console.log('settings', Object.clone(settings));
|
||||
if (isWork) {
|
||||
api.platform.clear();
|
||||
// really old workspaces encoded the types as strings
|
||||
for (let work of data.work) {
|
||||
if (work.type === "widget") {
|
||||
work.type = 100;
|
||||
}
|
||||
}
|
||||
codec.decode(data.work).forEach(widget => {
|
||||
api.platform.add(widget, 0, true, true);
|
||||
});
|
||||
if (data.view) {
|
||||
space.view.load(data.view);
|
||||
}
|
||||
}
|
||||
restoreSettings();
|
||||
api.space.restore(() => { ui.sync() }, true);
|
||||
}
|
||||
if (isTools && Array.isArray(data.tools)) {
|
||||
const settool = settings.tools;
|
||||
for (let tool of data.tools) {
|
||||
if (settool.filter(t => t.id === tool.id).length === 0) {
|
||||
settool.push(tool);
|
||||
}
|
||||
}
|
||||
// settings.tools = data.tools;
|
||||
api.show.tools();
|
||||
}
|
||||
}
|
||||
if (ask) {
|
||||
let opt = {};
|
||||
let prompt = isDevice ?
|
||||
`Import device "${data.device}"?` : isProcess ?
|
||||
`Import process "${data.name}"?` : isTools ?
|
||||
`Import tool definitions?`:
|
||||
`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 settingsImportUrl(url, ask) {
|
||||
const kmz = url.endsWith(".kmz");
|
||||
fetch(url).then(r => kmz ? r.arrayBuffer() : r.text()).then(a => {
|
||||
kmz ? settingsImportZip(a, ask) : settingsImport(a, ask);
|
||||
}).catch(error => {
|
||||
console.log({workspace_url: url, error: error.message || error});
|
||||
api.show.alert('workspace load failed');
|
||||
});
|
||||
}
|
||||
|
||||
function settingsImportZip(data, ask) {
|
||||
let alert = api.show.alert("Importing Workspace");
|
||||
new 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// import and convert prusa ini file
|
||||
function settingsPrusaConvert(data) {
|
||||
const { uc } = api;
|
||||
const map = {};
|
||||
const vsub = {
|
||||
'[first_layer_bed_temperature]': '{bed_temp}',
|
||||
'[first_layer_temperature]': '{temp}',
|
||||
'[layer_z]': '{z}'
|
||||
};
|
||||
try {
|
||||
data.split('\n')
|
||||
.filter(l => l.charAt(0) !== '#')
|
||||
.map(l => l.split('=').map(v => v.trim()))
|
||||
.map(l => {
|
||||
// convert gcode string into a string array
|
||||
if (l[0].indexOf('_gcode') > 0) {
|
||||
l[1] = l[1].replaceAll('\\n','\n').split('\n')
|
||||
.map(line => {
|
||||
for (let [k,v] of Object.entries(vsub)) {
|
||||
line = line.replace(k,v);
|
||||
}
|
||||
return line;
|
||||
});
|
||||
}
|
||||
return l;
|
||||
})
|
||||
.forEach(l => {
|
||||
map[l[0]] = l[1];
|
||||
});
|
||||
} catch (e) {
|
||||
return uc.alert('invalid file');
|
||||
}
|
||||
// device setup
|
||||
let device = Object.clone(conf.defaults.fdm.d);
|
||||
let dname = device.deviceName = map.printer_model;
|
||||
if (dname) {
|
||||
device.mode = "FDM";
|
||||
device.extruders[0].extNozzle = parseFloat(map.nozzle_diameter);
|
||||
device.gcodePre = map.start_gcode;
|
||||
device.gcodePost = map.end_gcode;
|
||||
device.gcodeLayer = map.layer_gcode || [];
|
||||
device.maxHeight = parseInt(map.max_print_height || device.maxHeight);
|
||||
if (map.bed_shape) {
|
||||
let shape = map.bed_shape.split(',').map(l => l.split('x'));
|
||||
device.bedWidth = parseInt(shape[2][0]);
|
||||
device.bedDepth = parseInt(shape[2][1]);
|
||||
}
|
||||
}
|
||||
// profile setup
|
||||
let process = Object.clone(conf.defaults.fdm.p);
|
||||
let pname = process.processName = map.print_settings_id;
|
||||
if (pname) {
|
||||
process.sliceShells = parseInt(map.perimeters);
|
||||
process.sliceHeight = parseFloat(map.layer_height);
|
||||
process.outputFeedrate = parseInt(map.perimeter_speed);
|
||||
process.outputSeekrate = parseInt(map.travel_speed);
|
||||
process.outputTemp = parseInt(map.temperature);
|
||||
process.outputBedTemp = parseInt(map.bed_temperature);
|
||||
process.sliceTopLayers = parseInt(map.top_solid_layers);
|
||||
process.sliceBottomLayers = parseInt(map.bottom_solid_layers);
|
||||
process.firstSliceHeight = parseFloat(map.first_layer_height);
|
||||
process.firstLayerNozzleTemp = parseInt(map.first_layer_temperature);
|
||||
process.firstLayerRate = (
|
||||
(parseFloat(map.first_layer_speed) / 100) * process.outputFeedrate);
|
||||
process.firstLayerBedTemp = parseInt(map.first_layer_bed_temperature);
|
||||
process.outputRetractDist = parseFloat(map.retract_length);
|
||||
process.outputRetractSpeed = parseFloat(map.retract_speed);
|
||||
}
|
||||
uc.confirm(`Import "${dname}"?`).then(yes => {
|
||||
if (yes) {
|
||||
// create device, associated profile, set as current and show dialog
|
||||
settings.devices[dname] = device;
|
||||
settings.devproc[dname] = pname;
|
||||
settings.process = settings.sproc.FDM[pname] = process;
|
||||
settings.filter.FDM = dname;
|
||||
settings.cproc.FDM = pname;
|
||||
api.show.devices();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// function setThreaded(bool) {
|
||||
// bool = bool ? true : false;
|
||||
// settings.controller.threaded = bool;
|
||||
// api.event.emit("set.threaded", bool);
|
||||
// }
|
||||
|
||||
function setEnableWASM(bool) {
|
||||
bool = bool ? true : false;
|
||||
settings.controller.assembly = bool;
|
||||
api.event.emit("set.assembly", bool);
|
||||
}
|
||||
|
||||
// merged api for backward compatibility
|
||||
const apiSet = {
|
||||
dbo() { return ls2o('ws-settings') },
|
||||
dev() { return settings.device },
|
||||
proc() { return settings.process },
|
||||
ctrl() { return settings.controller },
|
||||
mode() { return settings.mode },
|
||||
prof() { return settings.sproc[settings.mode] },
|
||||
get: getSettings,
|
||||
put: putSettings,
|
||||
load: loadSettings,
|
||||
save: saveSettings,
|
||||
show: showSettings,
|
||||
update: updateSettings,
|
||||
update_from: updateSettingsFromFields,
|
||||
update_fields: updateFieldsFromSettings,
|
||||
update_fields_from_range: updateFieldsFromRange,
|
||||
set_enable_wasm: setEnableWASM,
|
||||
// set_threaded: setThreaded,
|
||||
set_threaded() { console.trace('set_threaded deprecated' )},
|
||||
restore: restoreSettings,
|
||||
export: settingsExport,
|
||||
import: settingsImport,
|
||||
import_zip: settingsImportZip,
|
||||
import_url: settingsImportUrl,
|
||||
import_prusa: settingsPrusaConvert,
|
||||
sync: {
|
||||
async get() {},
|
||||
async put() {},
|
||||
status: false
|
||||
}
|
||||
};
|
||||
|
||||
// for backward compatibility
|
||||
apiSet.conf = apiSet;
|
||||
apiSet.set = apiSet;
|
||||
apiSet.settings = apiSet;
|
||||
|
||||
export default apiSet;
|
||||
|
||||
// for backward compatibility
|
||||
export {
|
||||
apiSet as conf,
|
||||
apiSet as set,
|
||||
apiSet as settings
|
||||
};
|
||||
Loading…
Reference in a new issue