From c9667476ee80690311ab2e8d2c220a546f47e3a3 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 4 Jul 2025 22:18:48 -0400 Subject: [PATCH] add missing src2/kiri --- src2/kiri/alerts.js | 75 ++ src2/kiri/api.js | 172 ++++ src2/kiri/boxes.js | 44 + src2/kiri/client.js | 344 +++++++ src2/kiri/codec.js | 472 +++++++++ src2/kiri/conf.js | 984 ++++++++++++++++++ src2/kiri/consts.js | 156 +++ src2/kiri/conv.js | 32 + src2/kiri/devices.js | 381 +++++++ src2/kiri/do.js | 180 ++++ src2/kiri/export.js | 734 ++++++++++++++ src2/kiri/files.js | 175 ++++ src2/kiri/frame.js | 129 +++ src2/kiri/function.js | 502 ++++++++++ src2/kiri/init.js | 2155 ++++++++++++++++++++++++++++++++++++++++ src2/kiri/lang-en.js | 932 +++++++++++++++++ src2/kiri/lang.js | 78 ++ src2/kiri/layers.js | 263 +++++ src2/kiri/main.js | 956 ++++++++++++++++++ src2/kiri/pack.js | 141 +++ src2/kiri/platform.js | 942 ++++++++++++++++++ src2/kiri/print.js | 680 +++++++++++++ src2/kiri/render.js | 547 ++++++++++ src2/kiri/selection.js | 315 ++++++ src2/kiri/settings.js | 805 +++++++++++++++ src2/kiri/slice.js | 254 +++++ src2/kiri/stack.js | 295 ++++++ src2/kiri/stacks.js | 181 ++++ src2/kiri/stats.js | 53 + src2/kiri/tools.js | 166 ++++ src2/kiri/ui.js | 846 ++++++++++++++++ src2/kiri/utils.js | 117 +++ src2/kiri/widget.js | 964 ++++++++++++++++++ src2/kiri/widgets.js | 117 +++ 34 files changed, 15187 insertions(+) create mode 100644 src2/kiri/alerts.js create mode 100644 src2/kiri/api.js create mode 100644 src2/kiri/boxes.js create mode 100644 src2/kiri/client.js create mode 100644 src2/kiri/codec.js create mode 100644 src2/kiri/conf.js create mode 100644 src2/kiri/consts.js create mode 100644 src2/kiri/conv.js create mode 100644 src2/kiri/devices.js create mode 100644 src2/kiri/do.js create mode 100644 src2/kiri/export.js create mode 100644 src2/kiri/files.js create mode 100644 src2/kiri/frame.js create mode 100644 src2/kiri/function.js create mode 100644 src2/kiri/init.js create mode 100644 src2/kiri/lang-en.js create mode 100644 src2/kiri/lang.js create mode 100644 src2/kiri/layers.js create mode 100644 src2/kiri/main.js create mode 100644 src2/kiri/pack.js create mode 100644 src2/kiri/platform.js create mode 100644 src2/kiri/print.js create mode 100644 src2/kiri/render.js create mode 100644 src2/kiri/selection.js create mode 100644 src2/kiri/settings.js create mode 100644 src2/kiri/slice.js create mode 100644 src2/kiri/stack.js create mode 100644 src2/kiri/stacks.js create mode 100644 src2/kiri/stats.js create mode 100644 src2/kiri/tools.js create mode 100644 src2/kiri/ui.js create mode 100644 src2/kiri/utils.js create mode 100644 src2/kiri/widget.js create mode 100644 src2/kiri/widgets.js diff --git a/src2/kiri/alerts.js b/src2/kiri/alerts.js new file mode 100644 index 00000000..5cf3c46d --- /dev/null +++ b/src2/kiri/alerts.js @@ -0,0 +1,75 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from './api.js'; + +let alerts = []; + +function show(message, time) { + if (message === undefined || message === null) { + return update(true); + } + let rec = [message, Date.now(), time, true]; + if (api.feature.alert_event) { + api.event.emit('alert', rec); + } else { + alerts.push(rec); + setTimeout( update, time+0.02); + update(); + } + return rec; +} + +function hide(rec, recs) { + if (Array.isArray(recs)) { + for (let r of recs) { + hide(r); + } + return; + } + if (api.feature.alert_event) { + api.event.emit('alert.cancel', rec); + return; + } + if (Array.isArray(rec)) { + rec[3] = false; + update(); + } +} + +function update(clear) { + if (clear) { + alerts = []; + } + const now = Date.now(); + const { ui } = api; + // 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 => ['

',v[0],'

'].join('')).join(''); + ui.alert.dialog.style.display = 'flex'; + } else { + ui.alert.dialog.style.display = 'none'; + } +} + +export { + hide, + show, + update +}; + +export default { + hide, + show, + update +}; diff --git a/src2/kiri/api.js b/src2/kiri/api.js new file mode 100644 index 00000000..aa995035 --- /dev/null +++ b/src2/kiri/api.js @@ -0,0 +1,172 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import alerts from './alerts.js'; +import settings from './settings.js' + +import { broker } from '../moto/broker.js'; +import { client as work } from './client.js'; +import { consts, COLOR as color, LISTS as lists, MODES, VIEWS } from './consts.js'; +import { device, devices } from './devices.js'; +import { catalog, dialog, event, group, help, hide, image } from './main.js'; +import { modal, mode, probe, process, show, space, util, view } from './main.js'; +import { local as dataLocal } from '../data/local.js'; +import { noop, ajax, o2js, js2o } from './utils.js'; +import { functions } from './function.js'; +import { platform } from './platform.js'; +import { selection } from './selection.js'; +import { stats } from '../kiri/stats.js'; +import { widgets } from './widgets.js'; + +import { LANG } from './lang.js'; +import { LOCAL, SETUP, SECURE } from './main.js'; +import { UI } from './ui.js'; +import STACKS from './stacks.js'; + +let UC = UI.prefix('kiri').inputAction(settings.conf.update), + und = undefined, + clone = Object.clone, + isHover = false, + feature = { + seed: true, // seed profiles on first use + meta: true, // show selected widget metadata + frame: true, // receive frame events + alert_event: false, // emit alerts as events instead of display + controls: true, // show or not side menus + device_filter: und, // function to limit devices shown + drop_group: und, // optional array to group multi drop + drop_layout: true, // layout on new drop + hoverAdds: false, // when true only searches widget additions + on_key: und, // function override default key handlers + on_key2: [], // allows for multiple key handlers + on_load: und, // function override file drop loads + on_add_stl: und, // legacy override stl drop loads + on_mouse_up: und, // function intercepts mouse up select + on_mouse_down: und, // function intercepts mouse down + work_alerts: true, // allow disabling work progress alerts + pmode: consts.PMODES.SPEED, // preview modes + // hover: false, // when true fires mouse hover events + get hover() { + return isHover; + }, + set hover(b) { + isHover = b; + broker.publish("feature.hover", b); + } + }, + busyVal = 0, + busy = { + val() { return busy }, + inc() { api.event.emit("busy", ++busyVal) }, + dec() { api.event.emit("busy", --busyVal) } + }, + onkey = (fn) => { + api.feature.on_key2.push(fn); + }, + doit = { + undo: noop, // do.js + redo: noop // do.js + }, + devel = { + xray(layers, raw) { + let proc = api.conf.get().process, + size = proc.sliceHeight || proc.slaSlice || 1, + base = (proc.firstSliceHeight || size); + layers = Array.isArray(layers) ? layers : [ layers ]; + proc.xray = layers.map(l => raw ? l : base + l * size - size / 2); + proc.xrayi = layers.slice(); + api.function.slice(); + } + }, + local = { + get: (key) => localGet(key), + getInt: (key) => parseInt(localGet(key)), + getFloat: (key) => parseFloat(localGet(key)), + getBoolean: (key, def = true) => { + let val = localGet(key); + return val === true || val === 'true' || val === def; + }, + toggle: (key, val, def) => localSet(key, val ?? !api.local.getBoolean(key, def)), + put: (key, val) => localSet(key, val), + set: (key, val) => localSet(key, val), + }, + tweak = { + line_precision(v) { api.work.config({base:{clipperClean: v}}) }, + gcode_decimals(v) { api.work.config({base:{gcode_decimals: v}}) } + }; + +function clip(text) { + navigator.clipboard + .writeText(text) + .catch(err => console.error('Clipboard Error:', err)); +} + +function localGet(key) { + let sloc = api.conf.get().local; + return sloc[key] || api.sdb[key]; +} + +function localSet(key, val) { + let sloc = api.conf.get().local; + sloc[key] = api.sdb[key] = val; + return val; +} + +export const api = { + ajax, // via utils + alerts, // alerts.js + busy, + catalog, + client: work, // alias for "work" + clip, // <-- + clone, // <-- + color, + conf: settings.conf, + const: { LANG, LOCAL, MODES, SETUP, SECURE, STACKS, VIEWS }, + devel, // <-- + device, + devices, + dialog, + doit, // <-- + event, + feature, // <-- + function: functions, + group, + help, + hide, + image, + js2o, // via utils + language: LANG, + lists, // <-- + local, // <-- + modal, + mode, + noop, + o2js, // via utils + onkey, // <-- + platform, + probe, + process, + sdb: dataLocal, + selection, + settings, + show, + space, + stacks: STACKS, + stats, + tool: {}, // kiri-mode/cam/tools.js + tweak, // <-- + uc: UC, + ui: {}, // init.js + util, + var: { + layer_lo: 0, + layer_hi: 0, + layer_max: 0 + }, + view, + widgets, + work, // message worker +}; + +// allow widget to straddle client / worker FOR NOW +self.kiri_api = api; diff --git a/src2/kiri/boxes.js b/src2/kiri/boxes.js new file mode 100644 index 00000000..9d5784d6 --- /dev/null +++ b/src2/kiri/boxes.js @@ -0,0 +1,44 @@ +const { BoxGeometry, Matrix4, MeshPhongMaterial, Quaternion, Vector3 } = THREE; +const boxes = []; + +export function delbox(name) { + const old = boxes[name]; + if (old) { + old.groupTo.remove(old); + } +} + +export function addbox(point, color, name, dim = {x:1,y:1,z:1,rz:0}, opt = {}) { + delbox(name); + + const box = boxes[name] = new Mesh( + new BoxGeometry(dim.x, dim.y, dim.z), + new MeshPhongMaterial({ + transparent: true, + opacity: opt.opacity || 0.5, + color + }) + ); + + box.position.x = point.x; + box.position.y = point.y; + box.position.z = point.z; + + lastBox = { point, dim }; + + const group = opt.group || space.scene + group.add(box); + box.groupTo = group; + + if (dim.rz) { + opt.rotate = new Quaternion().setFromAxisAngle(new Vector3(0,0,1), dim.rz); + } + if (opt.rotate) { + opt.matrix = new Matrix4().makeRotationFromQuaternion(opt.rotate); + } + if (opt.matrix) { + box.geometry.applyMatrix4(opt.matrix); + } + + return box; +} diff --git a/src2/kiri/client.js b/src2/kiri/client.js new file mode 100644 index 00000000..c8bf6032 --- /dev/null +++ b/src2/kiri/client.js @@ -0,0 +1,344 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from './api.js'; +import { noop } from './utils.js'; +import { version } from '../moto/license.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; + +/** + * @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, + + pool: { + start() { + minions = true; + send("pool_start", {}, 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(`/v2/lib/kiri-run/worker.js`, { type: 'module' }); + worker.onerror = (error) => { + console.log({ worker_error: error }); + error.preventDefault(); + }; + worker.onmessageerror = (error) => { + console.log({ worker_message_error: error }); + }; + console.log({ worker }); + return worker; + } + }, + + isBusy() { + let current = 0; + + for (let rec of Object.values(running)) { + if (rec.fn) current++; + } + return current > 0; + }, + + 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 = function(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 kiri.Widget.Groups.forid(reply.group)) { + widget.belt = reply.belt; + } + } else if (callback) { + callback(); + } + }); + }, + + // called once for each widget + slice(settings, widget, callback) { + console.log('CSLICE', { 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 = kiri.newPrint().parseSVG(code, offset); + send("parse_svg", output, reply => { + if (reply.parsed) { + done(kiri.codec.decode(reply.parsed)); + } + }); + return; + } + send("parse", args, reply => { + if (reply.progress) { + progress(reply.progress); + } + if (reply.parsed) { + done(kiri.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 }); + }); + } +}; + +// start worker +client.restart(); + +self.kiri_client = client; + +export default client; diff --git a/src2/kiri/codec.js b/src2/kiri/codec.js new file mode 100644 index 00000000..13af5c84 --- /dev/null +++ b/src2/kiri/codec.js @@ -0,0 +1,472 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { base } from '../geo/base.js'; +import { Layers } from './layers.js'; +import { Polygon } from '../geo/polygon.js'; +import { Slice, newSlice, Top, newTop } from './slice.js'; +import { Widget, newWidget } from './widget.js'; + +const decoders = {}; +const freeMem = true; + +const TYPE = { + WIDGET: 100, + SLICE: 200, + TOP: 300, + POLY: 400, + LAYERS: 500, +}; + +function toCodable(object) { + switch (typeof object) { + case 'function': + return undefined; + // case 'boolean': + case 'string': + case 'number': + return object; + case 'object': + if (Array.isArray(object)) { + return object.map(v => toCodable(v)); + } + break; + } + let o = {}; + for (let [key, val] of Object.entries(object)) { + switch (typeof val) { + case 'function': + break; + case 'object': + if (Array.isArray(val)) { + val = val.map(v => toCodable(v)); + } else { + val = toCodable(val); + } + default: + o[key] = val; + break; + } + } + return o; +} + +function allocFloat32Array(arg, zeros) { + if (arg === undefined) { + return undefined; + } + if (arg.length === 0) { + return []; + } + let f32; + if (arg.byteLength) { + // already a float array + f32 = arg; + } else if (Array.isArray(arg)) { + // create float array from array + f32 = new Float32Array(arg); + } else { + // usually a number (size) for array + f32 = new Float32Array(arg); + } + if (zeros && f32.byteLength > 0) { + zeros.push(f32.buffer); + } + return f32; +} + +function encode(o, state) { + state = state || {}; + if (o === null) return null; + if (o === undefined) return undefined; + if (Array.isArray(o)) { + let arr = new Array(o.length), i=0; + while (i < o.length) arr[i] = encode(o[i++], state); + return arr; + } + switch (typeof(o)) { + case 'boolean': + case 'string': + case 'number': + return o; + case 'object': + if (o.encode) return o.encode(state); + return genericObjectEncode(o, state); + } + return null; +} + +function decode(o, state) { + state = state || {}; + if (o === null) return null; + if (o === undefined) return undefined; + if (Array.isArray(o)) { + for (let i=0; i < o.length; i++) o[i] = decode(o[i],state); + return o; + } + switch (typeof(o)) { + case 'boolean': + case 'string': + case 'number': + return o; + case 'object': + if (o.type && decoders[o.type]) return decoders[o.type](o,state); + return genODecode(o, state); + } + return null; +} + +function registerDecoder(type, handler) { + decoders[type] = handler; +} + +function genericObjectEncode(o, state) { + if (o instanceof Float32Array) return o; + let out = {}; + for (let k in o) { + if (o.hasOwnProperty(k)) out[k] = encode(o[k], state); + } + return out; +} + +function genODecode(o, state) { + if (o instanceof Float32Array) return o; + let out = {}; + for (let k in o) { + if (o.hasOwnProperty(k)) out[k] = decode(o[k], state); + } + return out; +} + +function encodePointArray(points, state, z) { + if (!points) { + return null; + } + + const length = points.length; + + if (length === 0) { + return points; + } + + const zeros = state ? state.zeros : undefined; + const array = codec.allocFloat32Array(length * 3, zeros); + + for (let i=0, pos=0, point; i { + return { + open: poly.open, + color: poly.color, + points: codec.encodePointArray(poly.points, state.zeros) + } + }); +} + +Layers.prototype.encode = function(state) { + let zeros = state.zeros; + let enc = { + type: TYPE.LAYERS, + layers: Object.keys(this.layers), + data: Object.values(this.layers).map(layer => { + const paths = layer.paths; + return { + // defaults + off: layer.off, + color: layer.color, + // 1D lines and polylines + polys: encodeLayerPolys(layer.polys, state), + lines: encodePointArray(layer.lines, state), + // 2D lines and flat areas + faces: codec.allocFloat32Array(layer.faces, zeros), + norms: codec.allocFloat32Array(layer.norms, zeros), + cface: layer.cface || codec.undef, + // 3D lines + paths: paths ? { + z: paths.z, + index: paths.index, + faces: codec.allocFloat32Array(paths.faces, zeros), + norms: codec.allocFloat32Array(paths.norms, zeros) + } : undefined, + cpath: layer.cpath || codec.undef, + rotation: layer.rotation, + position: layer.position + } + }) + }; + // aggressively free memory + if (freeMem) this.init(); + return enc; +}; + +registerDecoder(TYPE.LAYERS, function(v, state) { + const render = new Layers(); + const { layers } = v; + + for (let i=0; i Infinity in material counts (JSON stringify sucks) + if (d.cface) { + d.cface.forEach(rec => { + if (rec.count === null) { + rec.count = Infinity; + } + }); + } + if (d.cpath) { + d.cpath.forEach(rec => { + if (rec.count === null) { + rec.count = Infinity; + } + }); + } + } + + return render; +}); + +export const codec = { + encode, + decode, + registerDecoder, + allocFloat32Array, + encodePointArray, + decodePointArray, + encodePointArray2D, + decodePointArray2D, + toCodable +}; + +export { + encode, + decode, + registerDecoder, + allocFloat32Array, + encodePointArray, + decodePointArray, + encodePointArray2D, + decodePointArray2D, + toCodable +}; + +self.kiri_codec = codec; diff --git a/src2/kiri/conf.js b/src2/kiri/conf.js new file mode 100644 index 00000000..40c9accf --- /dev/null +++ b/src2/kiri/conf.js @@ -0,0 +1,984 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +const { clone } = Object; +const CVER = 410; + +function genID() { + while (true) { + let k = Math.round(Math.random() * 9999999999).toString(36); + if (k.length >= 4 && k.length <= 8) return k; + } +} + +// add fields to o(bject) from d(efault) that are missing +// remove fields from o(bject) that don't exist in d(efault) +function fill_cull_once(obj, def, debug) { + if (!obj) return; + // handle renaming + for (let k in obj) { + if (obj.hasOwnProperty(k)) { + let nam = renamed[k] || k; + if (nam !== k) { + // handle field renames + obj[nam] = obj[k]; + if (debug) console.log({rename: k, to: nam}); + delete obj[k]; + } + } + } + // fill missing + for (let k in def) { + if (def.hasOwnProperty(k)) { + let okv = obj[k]; + if ((okv === undefined || okv === null)) { + // handle fill + if (debug) console.log({fill: k, val: def[k]}); + if (typeof def[k] === 'object') { + obj[k] = clone(def[k]); + } else { + obj[k] = def[k]; + } + } + } + } + // remove invalid + for (let k in obj) { + if (!def.hasOwnProperty(k)) { + if (debug) console.log({cull: k}); + delete obj[k]; + } + } +} + +function fill_cull_many(map, def) { + forValues(map, (obj) => { fill_cull_once(obj, def) }); +} + +function objectMap(o, fn) { + for (let [key, val] of Object.entries(o)) { + o[key] = fn(val) || val; + } +} + +function valueOf(val, dv) { + if (typeof(val) === 'string' && Array.isArray(dv)) { + val = [val]; + } + return typeof(val) !== 'undefined' ? val : dv; +} + +function forValues(o, fn) { + Object.values(o).forEach(v => fn(v)); +} + +// convert default filter (from server) into device structure +function device_from_code(code,mode) { + // presence of internal field indicates already converted + if (code.internal >= 0) return code; + + // if (self.navigator) console.log({mode, convert: code}); + let cmd = code.cmd || {}, + set = code.settings || {}, + ext = code.extruders; + + // currently causes unecessary fills and culls because + // it's not mode and device type sensitive + let device = { + noclone: valueOf(code.no_clone, false), + mode: mode || code.mode || '', + internal: 0, + imageScale: valueOf(set.image_scale, 0.75), + imageAnchor: valueOf(set.image_anchor, 0), + bedHeight: valueOf(set.bed_height, 2.5), + bedWidth: valueOf(set.bed_width, 300), + bedDepth: valueOf(set.bed_depth, 175), + bedRound: valueOf(set.bed_circle, false), + bedBelt: valueOf(set.bed_belt, false), + resolutionX: valueOf(set.resolution_x, 1600), + resolutionY: valueOf(set.resolution_y, 900), + deviceZMax: valueOf(set.z_move_max, 0), + gcodeTime: valueOf(set.time_factor, 1), + maxHeight: valueOf(set.build_height, 150), + originCenter: valueOf(set.origin_center, false), + extrudeAbs: valueOf(set.extrude_abs, false), + spindleMax: valueOf(set.spindle_max, 0), + gcodeFan: valueOf(cmd.fan_power || code.fan_power, []), + gcodeFeature: valueOf(cmd.feature || code.feature, []), + gcodeTrack: valueOf(cmd.progress || code.progress, []), + gcodeLayer: valueOf(cmd.layer || code.layer, []), + gcodePre: valueOf(code.pre, []), + gcodePost: valueOf(code.post, []), + // post processor script of which only one exists + // for XYZ.daVinci.Mini.w triggered in kiri.export + // in the fdm driver to turn gcode into base64 + gcodeProc: valueOf(code.proc, ''), + gcodeDwell: valueOf(code.dwell, []), + gcodeSpindle: valueOf(code.spindle || cmd.spindle, []), + gcodeChange: valueOf(code['tool-change'], []), + gcodeFExt: valueOf(code['file-ext'], 'gcode'), + gcodeSpace: valueOf(code['token-space'], true), + gcodeStrip: valueOf(code['strip-comments'], false), + gcodeLaserOn: valueOf(code['laser-on'], []), + gcodeLaserOff: valueOf(code['laser-off'], []), + extruders: [] + }; + + if (ext) { + // synthesize extruders from new style settings + ext.forEach(rec => { + let e = clone(conf.defaults.fdm.d.extruders[0]); + if (rec.nozzle) e.extNozzle = rec.nozzle; + if (rec.filament) e.extFilament = rec.filament; + if (rec.offset_x) e.extOffsetX = rec.offset_x; + if (rec.offset_y) e.extOffsetY = rec.offset_y; + device.extruders.push(e); + }); + } else { + // synthesize extruders from old style settings + device.extruders = [ clone(conf.defaults.fdm.d.extruders[0]) ]; + device.extruders[0].extNozzle = valueOf(set.nozzle_size, 0.4); + device.extruders[0].extFilament = valueOf(set.filament_diameter, 1.75); + } + + return device; +} + +// ensure settings structure is up-to-date +function normalize(settings) { + let defaults = conf.defaults, + template = conf.template, + mode = settings.mode.toLowerCase(), + default_dev = defaults[mode].d, + default_pro = defaults[mode].p; + + // fixup old/new detail settings + let detail = settings.controller.detail; + settings.controller.detail = { + "best": "100", + "good": "75", + "fair": "50", + "poor": "25" + }[detail] || detail; + + fill_cull_once(settings, template); + fill_cull_once(settings.device, default_dev); + fill_cull_once(settings.process, default_pro); + fill_cull_once(settings.cdev, template.cdev); + fill_cull_once(settings.cproc, template.cproc); + fill_cull_once(settings.sproc, template.sproc); + fill_cull_once(settings.defaults, template.defaults); + fill_cull_once(settings.cdev.FDM, defaults.fdm.d); + fill_cull_once(settings.cdev.SLA, defaults.sla.d); + fill_cull_once(settings.cdev.CAM, defaults.cam.d); + fill_cull_once(settings.cdev.LASER, defaults.laser.d); + fill_cull_many(settings.sproc.FDM, defaults.fdm.p); + fill_cull_many(settings.sproc.SLA, defaults.sla.p); + fill_cull_many(settings.sproc.CAM, defaults.cam.p); + fill_cull_many(settings.sproc.LASER, defaults.laser.p); + fill_cull_once(settings.controller, template.controller); + + return settings; +} + +// auto field renaming on import +const renamed = { + roughingTool: "camRoughTool", + roughingSpindle: "camRoughSpindle", + roughingDown: "camRoughDown", + roughingOver: "camRoughOver", + roughingSpeed: "camRoughSpeed", + roughingPlunge: "camRoughPlunge", + roughingStock: "camRoughStock", + roughingPocket: "camRoughVoid", + roughingOn: "camRoughOn", + finishingTool: "camOutlineTool", + finishingSpindle: "camOutlineSpindle", + finishingDown: "camOutlineDown", + finishingOver: "camContourOver", + finishingSpeed: "camOutlineSpeed", + finishingPlunge: "camOutlinePlunge", + finishingOn: "camOutlineOn", + finishingXOn: "camContourXOn", + finishingYOn: "camContourYOn", + drillTool: "camDrillTool", + drillSpindle: "camDrillSpindle", + drillDownSpeed: "camDrillDownSpeed", + drillDown: "camDrillDown", + drillDwell: "camDrillDwell", + drillLift: "camDrillLift", + drillingOn: "camDrillingOn", + camWideCutout: "camOutlineWide", + outputClockwise: "camConventional" +}; + +export const conf = { + // --------------- helper functions + normalize, + device_from_code, + fill_cull_once, + // --------------- device and process defaults + defaults: { + fdm:{ + // device defaults FDM:Device + d:{ + new: true, + mode: "", + deviceName: "", + imageURL: "", + internal: 0, + bedWidth: 300, + bedDepth: 175, + bedHeight: 2.5, + bedRound: false, + bedBelt: false, + fwRetract: false, + filamentSource: "direct", + originCenter: false, + deviceZMax: 0, + maxHeight: 150, + gcodeTime: 1, + gcodeChange: ["T{tool}"], + gcodePre: [], + gcodePost: [], + gcodeProc: "", + gcodeFan: [], + gcodeFeature: [], + gcodeTrack: [], + gcodeLayer: [], + gcodeFExt: "", + extruders:[{ + extFilament: 1.75, + extNozzle: 0.4, + extOffsetX: 0, + extOffsetY: 0 + }], + profiles: [], + // other stored config info + extras: {} + }, + // process defaults FDM:Process + p:{ + processName: "default", + sliceAngle: 45, + sliceHeight: 0.25, + sliceShells: 3, + sliceShellOrder: "in-out", + sliceLayerStart: "last", + sliceLayerStartX: 0, + sliceLayerStartY: 0, + sliceLineWidth: 0, + sliceFillGrow: 0, + sliceFillAngle: 45, + sliceFillWidth: 1, + sliceFillOverlap: 0.35, + sliceFillSparse: 0.25, + sliceFillRepeat: 1, + sliceFillRate: 0, + sliceSolidRate: 0, + sliceFillType: "hex", + sliceSupportDensity: 0.1, + sliceSupportOffset: 1.0, + sliceSupportGap: 1, + sliceSupportSize: 5, + sliceSupportArea: 0.1, + sliceSupportSpan: 5, + sliceSupportGrow: 0, + sliceSupportExtra: 0, + sliceSupportAngle: 50, + sliceSupportNozzle: 0, + sliceSupportEnable: false, + sliceSupportOutline: true, + sliceZInterleave: false, + sliceSolidMinArea: 1, + sliceBottomLayers: 3, + sliceTopLayers: 3, + firstSliceHeight: 0.25, + firstLayerRate: 30, + firstLayerFillRate: 35, + firstLayerPrintMult: 1.0, + firstLayerLineMult: 1.0, + firstLayerYOffset: 0, + firstLayerNozzleTemp: 0, + firstLayerBedTemp: 0, + firstLayerBrim: 0, + firstLayerBrimIn: 0, + firstLayerBrimTrig: 0, + firstLayerBrimGap: 0, + firstLayerBeltLead: 3, + firstLayerBeltBump: 0, + firstLayerFanSpeed: 0, + firstLayerFlatten: 0, + outputRaft: false, + outputRaftSpacing: 0.2, + outputDraftShield: false, + outputTemp: 200, + outputBedTemp: 60, + outputFeedrate: 50, + outputFinishrate: 50, + outputSeekrate: 80, + outputShellMult: 1.25, + outputFillMult: 1.25, + outputSparseMult: 1.25, + outputFanLayer: 1, + outputFanSpeed: 255, + outputRetractDist: 1.5, + outputRetractSpeed: 40, + outputRetractWipe: 0, + outputRetractDwell: 20, + outputBrimCount: 2, + outputBrimOffset: 2, + outputShortPoly: 100.0, + outputMinSpeed: 10.0, + outputCoastDist: 0, + outputPurgeTower: 0, + outputBeltFirst: false, + outputAvoidGaps: true, + outputAlternating: false, + outputLayerRetract: false, + outputLoops: 0, + outputInvertX: false, + outputInvertY: false, + sliceDetectThin: "off", + sliceMinHeight: 0, + sliceAdaptive: false, + zHopDistance: 0.2, + arcTolerance: 0, + antiBacklash: 0, + ranges: [] + } + }, + sla:{ + // device defaults SLA:Device + d:{ + new: true, + mode: "", + deviceName: "", + imageURL: "", + noclone: false, + internal: 0, + bedWidth: 150, + bedDepth: 150, + bedHeight: 1.5, + maxHeight: 150, + resolutionX: 1600, + resolutionY: 900 + }, + // process defaults SLA:Process + p:{ + processName: "default", + slaSlice: 0.05, + slaShell: 0.00, + slaOpenTop: false, + slaOpenBase: false, + slaAntiAlias: 1, + slaLayerOff: 0.1, + slaLayerOn: 7, + slaPeelDist: 6, + slaPeelLiftRate: 1.5, + slaPeelDropRate: 3, + slaBaseLayers: 5, + slaBaseOff: 0.1, + slaBaseOn: 30, + slaBasePeelDist: 6, + slaBasePeelLiftRate: 1.5, + slaFillDensity: 0, + slaFillLine: 0.5, + slaFirstOffset: 0, + slaSupportLayers: 10, + slaSupportDensity: 0.5, + slaSupportSize: 0.6, + slaSupportPoints: 4, + slaSupportGap: 10, + slaSupportEnable: false + } + }, + cam:{ + // device defaults CAM:Device + d:{ + new: true, + mode: "", + deviceName: "", + imageURL: "", + internal: 0, + bedWidth: 300, + bedDepth: 175, + bedHeight: 2.5, + maxHeight: 300, + useLaser: false, + originCenter: false, + spindleMax: 0, + gcodePre: [], + gcodePost: [], + gcodeFExt: "", + gcodeSpace: true, + gcodeStrip: true, + gcodeDwell: ["G4 P{time}"], + gcodeChange: ["M6 T{tool}"], + gcodeSpindle: ["M3 S{speed}"] + }, + // process defaults CAM:Process + p:{ + processName: "default", + camLevelTool: 1000, + camLevelSpindle: 1000, + camLevelOver: 0.75, + camLevelSpeed: 1000, + camLevelDown: 0, + camLevelOver: 0.5, + camLevelStock: true, + camRoughTool: 1000, + camRoughSpindle: 1000, + camRoughDown: 2, + camRoughOver: 0.4, + camRoughSpeed: 1000, + camRoughPlunge: 250, + camRoughStock: 0, + camRoughStockZ: 0, + camRoughAll: true, + camRoughVoid: false, + camRoughFlat: true, + camRoughTop: true, + camRoughIn: true, + camRoughOn: true, + camRoughOmitVoid: false, + camOutlineTool: 1000, + camOutlineSpindle: 1000, + camOutlineTop: true, + camOutlineDown: 3, + camOutlineOver: 0.4, + camOutlineOverCount: 1, + camOutlineSpeed: 800, + camOutlinePlunge: 250, + camOutlineWide: false, + camOutlineDogbone: false, + camOutlineOmitThru: false, + camOutlineOmitVoid: false, + camOutlineOut: true, + camOutlineIn: false, + camOutlineOn: true, + camContourTool: 1000, + camContourSpindle: 1000, + camContourOver: 0.5, + camContourSpeed: 1000, + camContourAngle: 85, + camContourLeave: 0, + camContourReduce: 2, + camContourBottom: false, + camContourCurves: false, + camContourIn: false, + camContourXOn: true, + camContourYOn: true, + camLatheTool: 1000, + camLatheSpindle: 1000, + camLatheOver: 0.1, + camLatheAngle: 1, + camLatheSpeed: 500, + camLatheLinear: true, + camTolerance: 0, + camTraceTool: 1000, + camTraceSpindle: 1000, + camTraceType: "follow", + camTraceOver: 0.5, + camTraceDown: 0, + camTraceThru: 0, + camTraceSpeed: 250, + camTracePlunge: 200, + camTraceOffOver: 0, + camTraceDogbone: false, + camTraceMerge: false, + camTraceLines: false, + camTraceZTop: 0, + camTraceZBottom: 0, + camPocketSpindle: 1000, + camPocketTool: 1000, + camPocketOver: 0.25, + camPocketDown: 1, + camPocketSpeed: 250, + camPocketPlunge: 200, + camPocketExpand: 0, + camPocketSmooth: 0, + camPocketRefine: 20, + camPocketFollow: 5, + camPocketContour: false, + camPocketEngrave: false, + camPocketOutline: false, + camPocketZTop: 0, + camPocketZBottom: 0, + camDrillTool: 1006, + camDrillSpindle: 1000, + camDrillDownSpeed: 250, + camDrillDown: 5, + camDrillDwell: 250, + camDrillLift: 2, + camDrillMark: false, + camDrillFromStockTop: false, + camDrillThru: 5, + camDrillPrecision: 1, + camDrillingOn: false, + camRegisterSpeed: 1000, + camRegisterThru: 5, + camFlipAxis: "X", + camFlipOther: "", + camLaserEnable: ["M321"], + camLaserDisable: ["M322"], + camLaserOn: ["M3"], + camLaserOff: ["M5"], + camLaserSpeed: 100, + camLaserPower: 1, + camLaserAdaptive: false, + camLaserAdaptMod: false, + camLaserFlatten: false, + camLaserFlatZ: 0, + camLaserPowerMin: 0, + camLaserPowerMax: 1, + camLaserZMin: 0, + camLaserZMax: 0, + camTabsWidth: 5, + camTabsHeight: 5, + camTabsDepth: 5, + camTabsMidline: false, + camDepthFirst: false, + camEaseDown: false, + camEaseAngle: 10, + camOriginTop: true, + camZAnchor: "middle", + camZOffset: 0, + camZTop: 0, + camZBottom: 0, + camZClearance: 1, + camZThru: 0, + camFastFeed: 6000, + camFastFeedZ: 300, + camTolerance: 0, + camFlatness: 0.001, + camContourBridge: 0, + camStockX: 5, + camStockY: 5, + camStockZ: 5, + camStockOffset: true, + camStockClipTo: false, + camStockIndexed: false, + camStockIndexGrid: true, + camIndexAxis: 0, + camIndexAbs: true, + camConventional: false, // outputClockwise + camOriginCenter: false, + camOriginOffX: 0, + camOriginOffY: 0, + camOriginOffZ: 0, + outputInvertX: false, + outputInvertY: false, + camExpertFast: false, + camTrueShadow: false, + camArcTolerance: 0.006, + camArcResolution: 5, + camForceZMax: false, + camFirstZMax: false, + camToolInit: true, + camFullEngage: 0.8, + ops: [], // current ops + op2: [] // flip ops + } + }, + laser: { + // device defaults Laser:Device + d:{ + new: true, + mode: "", + deviceName: "", + imageURL: "", + internal: 0, + bedWidth: 300, + bedDepth: 200, + bedHeight: 2.5, + maxHeight: 100, + laserMaxPower: 255, + gcodePre: [], + gcodePost: [], + gcodeFExt: "", + gcodeSpace: true, + gcodeLaserOn: ["M106 S{power}"], + gcodeLaserOff: ["M107"] + }, + // process defaults Laser:Process + p:{ + processName: "default", + ctSliceKerf: 0.1, + ctSliceHeight: 1, + ctSliceHeightMin: 0, + ctSliceSingle: true, + ctOutTileSpacing: 1, + ctOutPower: 100, + ctOutSpeed: 1000, + ctOutGroup: true, + ctOutZColor: false, + ctOutLayer: false, + ctOutMark: false, + ctOutStack: false, + ctOutMerged: false, + ctOriginCenter: true, + ctOriginBounds: false, + outputInvertX: false, + outputInvertY: false, + ctOutKnifeDepth: 1, + ctOutKnifePasses: 1, + ctOutKnifeTip: 2, + ctOutInches: false, + ctOutShaper: false + } + }, + drag: { + // device defaults Drag:Device + d:{ + new: true, + mode: "", + deviceName: "", + imageURL: "", + internal: 0, + bedWidth: 300, + bedDepth: 200, + bedHeight: 2.5, + maxHeight: 100, + gcodePre: [], + gcodePost: [], + gcodeFExt: "", + gcodeSpace: true, + gcodeKnifeDn: [";; Z down"], + gcodeKnifeUp: [";; Z up"] + }, + // process defaults Drag:Process + p:{ + processName: "default", + ctSliceKerf: 0.1, + ctSliceHeight: 1, + ctSliceHeightMin: 0, + ctSliceSingle: true, + ctOutTileSpacing: 1, + ctOutPower: 100, + ctOutSpeed: 1000, + ctOutGroup: true, + ctOutZColor: false, + ctOutLayer: false, + ctOutMark: false, + ctOutMerged: false, + ctOriginCenter: true, + ctOriginBounds: false, + outputInvertX: false, + outputInvertY: false, + ctOutKnifeDepth: 1, + ctOutKnifePasses: 1, + ctOutKnifeTip: 2, + ctOutStack: true, + }, + }, + wjet: { + // device defaults WaterJet:Device + d:{ + new: true, + mode: "", + deviceName: "", + imageURL: "", + internal: 0, + bedWidth: 300, + bedDepth: 200, + bedHeight: 2.5, + maxHeight: 100, + gcodePre: [], + gcodePost: [], + gcodeFExt: "", + gcodeSpace: true, + gcodeWaterOn: ["M106 S{power}"], + gcodeWaterOff: ["M107"] + }, + // process defaults WaterJet:Process + p:{ + processName: "default", + ctSliceKerf: 0.1, + ctSliceHeight: 1, + ctSliceHeightMin: 0, + ctSliceSingle: true, + ctOutTileSpacing: 1, + ctOutPower: 100, + ctOutSpeed: 1000, + ctOutGroup: true, + ctOutZColor: false, + ctOutLayer: false, + ctOutMark: false, + ctOutMerged: false, + ctOriginCenter: true, + ctOriginBounds: false, + outputInvertX: false, + outputInvertY: false, + ctOutKnifeDepth: 1, + ctOutKnifePasses: 1, + ctOutKnifeTip: 2, + ctOutStack: false, + }, + }, + wedm: { + // device defaults WireEDM:Device + d:{ + new: true, + mode: "", + deviceName: "", + imageURL: "", + internal: 0, + bedWidth: 300, + bedDepth: 200, + bedHeight: 2.5, + maxHeight: 100, + gcodePre: [], + gcodePost: [], + gcodeFExt: "", + gcodeSpace: true + }, + // process defaults WireEDM:Process + p:{ + processName: "default", + ctSliceKerf: 0.1, + ctSliceHeight: 1, + ctSliceHeightMin: 0, + ctSliceSingle: true, + ctOmitInner: false, + ctOutTileSpacing: 1, + ctOutPower: 100, + ctOutSpeed: 1000, + ctOutGroup: true, + ctOutZColor: false, + ctOutLayer: false, + ctOutMark: false, + ctOutMerged: false, + ctOriginCenter: false, + ctOriginBounds: true, + ctOriginOffX: 0, + ctOriginOffY: 0, + outputInvertX: false, + outputInvertY: false, + ctOutStack: false, + } + } + }, + // --------------- settings template + template: { + bounds: {}, + // CAM only + origin: {}, + stock: {}, + tools:[ + { + id: 1000, + number: 1, + type: "endmill", + name: "end 1/4", + metric: false, + shaft_diam: 0.25, + shaft_len: 1, + flute_diam: 0.25, + flute_len: 2, + taper_tip: 0, + }, + { + id: 1001, + number: 2, + type: "endmill", + name: "end 1/8", + metric: false, + shaft_diam: 0.125, + shaft_len: 1, + flute_diam: 0.125, + flute_len: 1.5, + taper_tip: 0, + }, + { + id: 1002, + number: 3, + type: "endmill", + name: "end 1/16", + metric: false, + shaft_diam: 0.0625, + shaft_len: 1, + flute_diam: 0.0625, + flute_len: 1.5, + taper_tip: 0, + }, + { + id: 1003, + number: 4, + type: "tapermill", + name: "vee 1/8", + metric: true, + shaft_diam: 0.125, + shaft_len: 1, + flute_diam: 0.125, + flute_len: 1.5, + taper_angle: 5.3, + taper_tip: 0, + }, + { + id: 1004, + number: 5, + type: "ballmill", + name: "ball 1/8", + metric: false, + shaft_diam: 0.125, + shaft_len: 1, + flute_diam: 0.125, + flute_len: 1.5, + taper_tip: 0, + }, + { + id: 1005, + number: 6, + type: "drill", + name: "drill 1/8", + metric: false, + shaft_diam: 0.125, + shaft_len: 1, + flute_diam: 0.125, + flute_len: 1.5, + taper_tip: 0, + }, + { + id: 1006, + number: 7, + type: "drill", + name: "drill 1/4", + metric: false, + shaft_diam: 0.25, + shaft_len: 1, + flute_diam: 0.25, + flute_len: 2, + taper_tip: 0, + } + ], + // currently selected device (current mode) + device:{}, + // currently selected process (current mode) + process:{}, + // current process (name of last used) by mode + cproc:{ + FDM: "default", + SLA: "default", + CAM: "default", + DRAG: "default", + WJET: "default", + WEDM: "default", + LASER: "default", + }, + // stored process (copy of last used) by mode + sproc:{ + FDM: {}, + SLA: {}, + CAM: {}, + DRAG: {}, + WJET: {}, + WEDM: {}, + LASER: {}, + }, + // current device (name of last used) by mode + filter:{ + FDM: "Any.Generic.Marlin", + SLA: "Anycubic.Photon", + CAM: "Any.Generic.Grbl", + DRAG: "Any.Generic.DragKnife", + WJET: "HydroBLADE", + WEDM: "RackRobo.Betta.Wire.V1", + LASER: "Any.Generic.Laser", + }, + // current (last used) device by mode + cdev: { + FDM: null, + SLA: null, + CAM: null, + LASER: null, + DRAG: null, + }, + // custom devices by name (all modes) + devices:{}, + // map of device name to last process setting name + devproc: {}, + // application ui and control preferences (Q menu) + controller:{ + view: null, + dark: false, + shiny: false, + drawer: false, + scrolls: true, + zoomSpeed: 1.0, + lineType: "path", + autoSave: true, + antiAlias: true, + reverseZoom: true, + showOrigin: false, + showRulers: true, + showSpeeds: true, + freeLayout: true, + autoLayout: true, + spaceRandoX: false, + spaceLayout: 1, + edgeangle: 20, + units: "mm", + exportOcto: false, + exportGhost: false, + exportLocal: false, + exportThumb: false, + exportPreview: false, + detail: "50", + animesh: "800", + healMesh: false, + threaded: true, + assembly: false, + ortho: false, + devel: false + }, + // default hidden ui groups + hidden: { + "fdm-base": true, + "fdm-cool": true, + "fdm-fill": true, + "fdm-supp": true, + "fdm-xprt": true + }, + // label state preferences (slice/preview toggles) + labels: { + 'CAM-3-arrows': false, + 'FDM-3-engage': false, + 'FDM-3-retract': false, + 'FDM-3-arrows': false, + 'FDM-3-move': false + }, + // for passing temporary slice hints (topo currently) + synth: {}, + // widget extra info for slicing (extruder mapping) + widget: {}, + // legacy localStorage settings (like octo print) + local: { + 'model.edges': false, + 'cam.anim.trans': true, + 'cam.anim.model': false, + 'cam.anim.stock': false, + 'cam.anim.speed': 3 + }, + mode: 'FDM', + id: genID(), + ver: CVER + } +}; + +const settings = conf.template; + +// seed defaults. will get culled on save +settings.sproc.FDM.default = clone(settings.process); +settings.sproc.SLA.default = clone(settings.process); +settings.sproc.CAM.default = clone(settings.process); +settings.sproc.LASER.default = clone(settings.process); +settings.sproc.DRAG.default = clone(settings.process); +settings.sproc.WJET.default = clone(settings.process); +settings.sproc.WEDM.default = clone(settings.process); +settings.cdev.FDM = clone(settings.device); +settings.cdev.SLA = clone(settings.device); +settings.cdev.CAM = clone(settings.device); +settings.cdev.LASER = clone(settings.device); +settings.cdev.DRAG = clone(settings.device); +settings.cdev.WJET = clone(settings.device); +settings.cdev.WEDM = clone(settings.device); diff --git a/src2/kiri/consts.js b/src2/kiri/consts.js new file mode 100644 index 00000000..750f4296 --- /dev/null +++ b/src2/kiri/consts.js @@ -0,0 +1,156 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +const COLOR = { + wireframe: 0x444444, + wireframe_opacity: 0.25, + selected: [ 0xbbff00, 0xbbee00, 0xbbdd00, 0xbb9900 ], + deselected: [ 0xffff00, 0xffdd00, 0xffbb00, 0xff9900 ], + model_opacity: 1.0, + preview_opacity: 0.0, + slicing: 0xffaaaa, + slicing_opacity: 0.5, + sliced_opacity: 0.0, + cam_preview: 0x888888, + cam_preview_opacity_dark: 0.2, + cam_preview_opacity: 0.1, + cam_sliced_opacity: 0.2 +}; + +const LISTS = { + shell: [ + { name: "in-out" }, + { name: "out-in" }, + { name: "alternate" } + ], + start: [ + { name: "last" }, + { name: "center" }, + { name: "origin" }, + { name: "random" }, + ], + infill: [ + { name: "none" }, + { 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" } + ], + animesh: [ + { name: "100" }, + { name: "200" }, + { name: "300" }, + { name: "400" }, + { name: "500" }, + { name: "600" }, + { name: "700" }, + { name: "800" }, + { name: "900" }, + { name: "1000" }, + { name: "1500" }, + { name: "2000" }, + { name: "2500" }, + { name: "3000" }, + { name: "4000" }, + ], + select: [ + { name: "loops" }, + { name: "lines" }, + // { name: "surface" } + ], + trace: [ + { name: "follow" }, + { name: "clear" } + ], + traceoff: [ + { name: "none" }, + { name: "inside" }, + { name: "outside" } + ], + zanchor: [ + { name: "top" }, + { name: "middle" }, + { name: "bottom" } + ], + xyaxis: [ + { name: "X" }, + { name: "Y" } + ], + regaxis: [ + { name: "X" }, + { name: "Y" }, + { name: "-" } + ], + regpoints: [ + { name: "2" }, + { name: "3" } + ], + thin: [ + { name: "off" }, + { name: "type 1" }, + { name: "type 2" }, + { name: "type 3" } + ] +}; + +// primary device mode +const MODES = { + FDM: 1, // fused deposition modeling (also FFF) + LASER: 2, // laser cutters (base for all 2d device types) + CAM: 3, // 3 axis milling/machining + SLA: 4, // cured resin printers + DRAG: 5, // drag knife (a variation on laser / 2d) + WJET: 6, // waterjet (a varation on laser / 2d) + WEDM: 7 // wire-edm (a variation on laser / 2d) +}; + +// view mode within device mode +const VIEWS = { + ARRANGE: 1, + SLICE: 2, + PREVIEW: 3, + ANIMATE: 4 +}; + +// preview modes +const PMODES = { + SPEED: 1, + TOOLS: 2 +}; + +const SEED = 'kiri-seed'; + +export let consts = { + PMODES, + COLOR, + LISTS, + MODES, + VIEWS, + SEED, + beltfact: Math.cos(Math.PI / 4) +}; + +export { COLOR, LISTS, MODES, VIEWS, PMODES, SEED }; diff --git a/src2/kiri/conv.js b/src2/kiri/conv.js new file mode 100644 index 00000000..6408bd82 --- /dev/null +++ b/src2/kiri/conv.js @@ -0,0 +1,32 @@ +// device legacy format converter + +const arg = process.argv.slice(2); +const mode = arg.shift(); +const umode = mode.toUpperCase(); +const lmode = mode.toLowerCase(); + +const fs = require('fs'); +const self = {}; + +eval( fs.readFileSync("src/main/gapp.js").toString() ); +const gapp = self.gapp; + +eval( fs.readFileSync("src/add/array.js").toString() ); +eval( fs.readFileSync("src/data/local.js").toString() ); +eval( fs.readFileSync("src/kiri/conf.js").toString() ); + +gapp.main(undefined, undefined, root => { + let kiri = root.kiri; + let conf = kiri.conf; + + for (let file of arg) { + let code = JSON.parse(fs.readFileSync(file).toString()); + let devi = conf.device_from_code(code, umode); + conf.fill_cull_once(devi, conf.defaults[lmode].d); + delete devi.new; + delete devi.internal; + delete devi.deviceName; + // console.log(JSON.stringify(code, undefined, 4)); + console.log(JSON.stringify([code, "-----", devi], undefined, 2)); + } +}); diff --git a/src2/kiri/devices.js b/src2/kiri/devices.js new file mode 100644 index 00000000..3fd42e1d --- /dev/null +++ b/src2/kiri/devices.js @@ -0,0 +1,381 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $, h } from '../moto/webui.js'; +import { api } from './api.js'; +import { conf } from './conf.js'; +import { space } from '../moto/space.js'; +import { devices as devlist } from '../pack/devices.js'; + +export const device = { + clone: cloneDevice, + code: currentDeviceCode, + get: currentDeviceName, + set: selectDevice, + isBelt +}; + +export const devices = { + show: showDevices, + select: selectDevice, + refresh: updateDeviceList, + update_laser_state: updateLaserState +}; + +function isBelt() { + return api.conf.get().device.bedBelt; +} + +function currentDeviceName() { + return api.conf.get().filter[api.mode.get()]; +} + +function currentDeviceCode() { + return api.conf.get().devices[currentDeviceName()]; +} + +function getModeDevices() { + return Object.keys(devlist[ api.mode.get_lower() ]).sort(); +} + +export function showDevices() { + api.settings.sync.get().then(_showDevices); +} + +function _showDevices() { + updateDeviceList(); + api.modal.show('setup'); +} + +function updateDeviceList() { + renderDevices(getModeDevices()); +} + +function updateDeviceName(newname) { + let selected = api.device.get(), + devs = api.conf.get().devices; + if (newname !== selected) { + devs[newname] = devs[selected]; + delete devs[selected]; + selectDevice(newname); + updateDeviceList(); + } +} + +function putLocalDevice(devicename, obj) { + api.conf.get().devices[devicename] = obj; + api.conf.save(); +} + +function removeLocalDevice(devicename) { + delete api.conf.get().devices[devicename]; + api.conf.save(); + api.settings.sync.put(); +} + +function isLocalDevice(devicename) { + return api.conf.get().devices[devicename] ? true : false; +} + +function getSelectedDevice() { + return api.device.get(); +} + +function selectDevice(devicename) { + if (isLocalDevice(devicename)) { + setDeviceCode(api.conf.get().devices[devicename], devicename); + } else { + let code = devices[api.mode.get_lower()][devicename]; + if (code) { + setDeviceCode(code, devicename); + } + } +} + +// only for local filters +function cloneDevice() { + let name = `${getSelectedDevice().replace(/\./g,' ')}`; + let code = api.clone(api.conf.get().device); + code.mode = api.mode.get(); + if (name.toLowerCase().indexOf('my ') >= 0) { + name = `${name} copy`; + } else { + name = `My ${name}`; + } + putLocalDevice(name, code); + setDeviceCode(code, name); + api.settings.sync.put(); +} + +function updateLaserState() { + const dev = api.conf.get().device; + $('laser-on').style.display = dev.useLaser ? 'flex' : 'none'; + $('laser-off').style.display = dev.useLaser ? 'flex' : 'none'; +} + +function setDeviceCode(code, devicename) { + api.event.emit('device.select', devicename); + try { + if (typeof(code) === 'string') code = js2o(code) || {}; + + let mode = api.mode.get(), + lmode = mode.toLowerCase(), + current = api.conf.get(), + local = isLocalDevice(devicename), + dev = current.device = conf.device_from_code(code,mode), + dproc = current.devproc[devicename], // last process name for this device + newdev = dproc === undefined, // first time device is selected + predev = current.filter[mode], // previous device selection + chgdev = predev !== devicename; // device is changing + + // fill missing device fields + conf.fill_cull_once(dev, conf.defaults[lmode].d); + + // first time device use, add any print profiles and set to default if present + if (code.profiles) { + for (let profile of code.profiles) { + let profname = profile.processName; + // if no saved profile by that name for this mode... + if (!current.sproc[mode][profname]) { + console.log('adding profile', profname, 'to', mode); + current.sproc[mode][profname] = profile; + } + // if it's a new device, seed the new profile name as last profile + if (newdev && !current.devproc[devicename]) { + console.log('setting default profile for new device', devicename, 'to', profname); + current.devproc[devicename] = dproc = profname; + } + } + } + + dev.new = false; + dev.deviceName = devicename; + + let { platform, ui, uc } = api; + + ui.deviceBelt.checked = dev.bedBelt; + ui.deviceRound.checked = dev.bedRound; + ui.deviceOrigin.checked = dev.ctOriginCenter || dev.originCenter || dev.bedRound; + ui.fwRetract.checked = dev.fwRetract; + + // add extruder selection buttons + if (dev.extruders) { + let ext = api.lists.extruders = []; + dev.internal = 0; + for (let i=0; i { + if (newname) { + updateDeviceName(newname); + api.conf.save(); + api.settings.sync.put(); + showDevices(); + } else { + showDevices(); + } + }); + }; + ui.deviceExport.onclick = function(event) { + const record = { + version: kiri.version, + device: selected, + process: api.process.code(), + profiles: event.altKey ? api.settings.prof() : undefined, + code: devs[selected], + time: Date.now() + }; + let exp = api.util.b64enc(record); + api.device.export(exp, selected, { event, record }); + }; + + let dedup = {}; + let list_cdev = []; + let list_mdev = []; + devices.forEach(function(device, index) { + // prevent device from appearing twice + // such as local name = standard device name + if (dedup[device]) { + return; + } + dedup[device] = device; + let loc = isLocalDevice(device); + if (dfilter && dfilter(device) === false) { + return; + } + if (loc) { + list_mdev.push(h.option(device)); + } else { + list_cdev.push(h.option(device)); + } + }); + + let dev_list = $('dev-list'); + h.bind(dev_list, [ + h.option({ _: '-- My Devices --', disabled: true }), + ...list_mdev, + h.option({ _: '-- Stock Devices --', disabled: true }), + ...list_cdev + ]); + let dev_opts = [...dev_list.options].map(o => o.innerText); + dev_list.selectedIndex = dev_opts.indexOf(selected); + dev_list.onchange = ev => { + const seldev = dev_list.options[dev_list.selectedIndex]; + selectDevice(seldev.innerText); + api.platform.layout(); + } + selectDevice(selected); +} + diff --git a/src2/kiri/do.js b/src2/kiri/do.js new file mode 100644 index 00000000..862adf2b --- /dev/null +++ b/src2/kiri/do.js @@ -0,0 +1,180 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ +import { kiri } from './kiri.js'; +import { moto } from '../moto.js'; +import { api } from './api.js'; +import { space } from '../moto/space.js'; + + +const { kiri, moto } = root; +const { api } = kiri; +const { space } = moto; +const { event } = api; + +let stack = []; +let stpos = 0; +let moved = { x: 0, y: 0 }; +let msgid; + +event.on("init-done", () => { + $('undo').onclick = undo; + $('redo').onclick = redo; +}); + +let undo = api.doit.undo = function() { + if (stpos) { + action(stack[--stpos].undo); + } else { + message('nothing to undo'); + } +}; + +let redo = api.doit.redo = function() { + if (stpos < stack.length) { + action(stack[stpos++].redo); + } else { + message('nothing to redo'); + } +}; + +let clear = api.doit.clear = function() { + stack = []; + stpos = 0; + moved = {x:0, y:0}; + updateButtons(); +}; + +function updateButtons() { + let isArrange = api.view.get() === kiri.consts.VIEWS.ARRANGE; + $('doit').style.display = isArrange && stack.length ? 'flex' : 'none'; + $('undo').disabled = stpos === 0; + $('redo').disabled = stpos == stack.length; +} + +function message(txt) { + if (msgid) { + api.hide.alert(msgid); + } + msgid = api.show.alert(txt); +} + +function action(rec) { + switch (rec.type) { + case 'move': + for (let w of rec.widgets) { + w._move(rec.dist.x, rec.dist.y, 0); + } + break; + case 'rotate': + for (let w of rec.widgets) { + w.rotate(rec.x, rec.y, rec.z); + } + break; + case 'scale': + for (let w of rec.widgets) { + w._scale(rec.x, rec.y, rec.z); + } + api.selection.update_info(); + break; + } + space.update(); + updateButtons(); +} + +function pushActions(ur) { + stack.length = stpos++; + stack.push(ur); + updateButtons(); +} + +event.on([ + 'platform.layout', + 'widget.add', + 'widget.delete' +], (widget) => { + clear(); +}); + +event.on('view.set', mode => { + updateButtons(); +}); + +// selection event (store position) +event.on('selection.rotate', (rec) => { + let type = "rotate"; + let widgets = api.selection.widgets(true); + if (!widgets.length) { + return; + } + let {x, y, z} = rec; + if (!(x || y || z)) { + return; + } + pushActions({ + redo: { + type, + widgets, + x, y, z + }, + undo: { + type, + widgets, + x: -x, y: -y, z: -z + } + }) +}); + +event.on('selection.scale', (rec) => { + let [x, y, z] = rec; + // ignore scale close to 1 + if (Math.abs(1 - (x * y * z)) < 0.0001) { + return; + } + let widgets = api.selection.widgets(true); + if (!widgets.length) { + return; + } + let type = "scale"; + pushActions({ + redo: { + type, + widgets, + x, y, z + }, + undo: { + type, + widgets, + x: 1/x, y: 1/y, z: 1/z + } + }); +}); + +// selection moved (accumulator) +event.on('selection.drag', (delta) => { + moved.x += delta.x; + moved.y += delta.y; +}); + +// move complete (store updated position) +event.on('mouse.drag.done', () => { + let widgets = api.selection.widgets(true); + if (!widgets.length) { + return; + } + let type = "move"; + pushActions({ + redo: { + type, + widgets, + dist: moved + }, + undo: { + type, + widgets, + dist: {x: -moved.x, y: -moved.y} + } + }); + moved = {x:0, y:0}; +}); + + +// No direct exports; extends api.doit diff --git a/src2/kiri/export.js b/src2/kiri/export.js new file mode 100644 index 00000000..75cbf6cd --- /dev/null +++ b/src2/kiri/export.js @@ -0,0 +1,734 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $ } from '../moto/webui.js'; +import { api } from './api.js'; +import { client } from './client.js'; +import { local } from '../data/local.js'; +import { util } from '../geo/base.js'; +import { MODES } from './consts.js'; +import { driver as laser_driver } from '../kiri-mode/laser/driver.js'; +import { client as sla_client } from '../kiri-mode/sla/client.js'; + +let printSeq = parseInt(local['kiri-print-seq'] || local['print-seq'] || "0") + 1; + +function localGet(key) { + return api.local.get(key); +} + +function localSet(key, val) { + return api.local.set(key, val); +} + +export function exportFile(options) { + let mode = api.mode.get(); + let names = api.widgets.all().map(w => w.meta ? w.meta.file : undefined) + .filter(v => v) + .map(v => v.replace(/ /g,'-')) + .map(v => v.substring(0,20)) + .map(v => v.split('.')[0]); + api.event.emit('export', mode); + switch (mode) { + case 'WEDM': + case 'WJET': + case 'DRAG': + case 'LASER': return callExportLaser(options, names); + case 'FDM': return callExport(options, mode, names); + case 'CAM': return callExport(options, mode, names); + case 'SLA': return callExportSLA(options, names); + } +} + +function callExport(callback, mode, names) { + let alert = api.feature.work_alerts ? api.show.alert("Exporting") : null; + let gcode = []; + let section = []; + let sections = { }; + client.export(api.conf.get(), (line) => { + if (typeof line !== 'string') { + if (line.section) { + sections[line.section] = section = []; + } + return; + } + gcode.push(line); + section.push(line); + }, (output, error) => { + api.hide.alert(alert); + if (error) { + api.show.alert(error, 5); + } else if (callback) { + callback(gcode.join('\r\n'), output); + } else { + exportGCodeDialog(gcode, mode === 'CAM' ? sections : undefined, output, names); + } + }); +} + +function callExportLaser(options, names) { + client.export(api.conf.get(), (line) => { + // engine export uses lines + // console.log({unexpected_line: line}); + }, (output, error) => { + // UI export uses output + if (error) { + api.show.alert(error, 5); + } else { + exportLaserDialog(output, names); + } + }); +} + +function callExportSLA(options, names) { + client.export(api.conf.get(), (line) => { + api.show.progress(line.progress, "exporting"); + }, (output, error) => { + api.show.progress(0); + if (error) { + api.show.alert(error, 5); + } else { + sla_client.printDownload(output, api, names); + } + }); +} + +function exportLaserDialog(data, names) { + localSet('kiri-print-seq', printSeq++); + + const fileroot = names[0] || "laser"; + const filename = `${fileroot}-${(printSeq.toString().padStart(3,"0"))}`; + const settings = api.conf.get(); + const driver = laser_driver; + + function download_svg() { + api.util.download( + driver.exportSVG(settings, data), + $('print-filename-laser').value + ".svg" + ); + } + + function download_dxf() { + api.util.download( + driver.exportDXF(settings, data), + $('print-filename-laser').value + ".dxf" + ); + } + + function download_gcode() { + api.util.download( + driver.exportGCode(settings, data), + $('print-filename-laser').value + ".gcode" + ); + } + + api.modal.show('xlaser'); + + let segments = 0; + data.forEach(layer => { segments += layer.length }); + + $('print-filename-laser').value = filename; + $('print-lines').value = util.comma(segments); + $('print-svg').onclick = download_svg; + $('print-dxf').onclick = download_dxf; + $('print-lg').onclick = download_gcode; +} + +function bindField(field, varname) { + $(field).onblur = function() { + console.log('save', field, 'to', varname); + localSet(varname, $(field).value.trim()); + }; +} + +function exportGCodeDialog(gcode, sections, info, names) { + localSet('kiri-print-seq', printSeq++); + + let settings = api.conf.get(), + MODE = api.mode.get_id(), + name = names[0] || (MODE === MODES.CAM ? "cnc" : "print"), + pre = `${name}-${(printSeq.toString().padStart(3,"0"))}`, + filename = pre,// + (new Date().getTime().toString(36)), + fileext = settings.device.gcodeFExt || "gcode", + codeproc = settings.device.gcodeProc, + octo_host, + octo_apik, + grid_host, + grid_apik, + grid_target, + grid_targets = {}, + grid_local, + grid_uuid; + + // join gcode array into a string + gcode = gcode.join('\r\n') + '\r\n'; + + // run gcode post processor function (when supplied and valid) + if (codeproc && self[codeproc]) { + gcode = self[codeproc](gcode); + } + + function getBlob() { + return new Blob( + [gcode], + {type:"application/octet-stream"}); + } + + function sendto_octoprint() { + if (!(octo_host && octo_apik)) return; + + let form = new FormData(), + ajax = new XMLHttpRequest(), + host = octo_host.value.toLowerCase(), + apik = octo_apik.value; + + if (host.indexOf("http") !== 0) { + api.show.alert("host missing protocol (http:// or https://)"); + return; + } + if (api.const.SECURE && !api.util.isSecure(host)) { + api.show.alert("host must begin with 'https' on a secure site"); + return; + } + + localSet('octo-host', host.trim()); + localSet('octo-apik', apik.trim()); + + filename = $('print-filename').value; + form.append("file", getBlob(), filename+"."+fileext); + ajax.onreadystatechange = function() { + if (ajax.readyState === 4) { + let status = ajax.status; + api.stats.add(`ua_${api.mode.get_lower()}_print_octo_${status}`); + if (status >= 200 && status < 300) { + api.modal.hide(); + } else { + api.show.alert("octoprint error\nstatus: "+status+"\nmessage: "+ajax.responseText); + } + api.show.progress(0); + } + api.show.progress(0); + }; + ajax.upload.addEventListener('progress', function(evt) { + api.show.progress(evt.loaded/evt.total, "sending"); + }); + ajax.open("POST", host+"/api/files/local"); + if (apik) { + ajax.setRequestHeader("X-Api-Key", apik); + } + ajax.send(form); + } + + function gridhost_tracker(host,key) { + ajax(host+"/api/check?key="+key, function(data) { + data = js2o(data); + console.log(data); + if (!(data.done || data.error)) { + setTimeout(function() { gridhost_tracker(host,key) }, 1000); + } + }); + } + + function gridlocal_probe(ev, devs) { + if (ev && ev.code !== 'Enter') return; + + if (!devs && api.probe.local(gridlocal_probe)) return; + + grid_local = devs; + + let gdev = localGet('grid-local'); + let gloc = $('grid-local'); + let html = []; + for (let uuid in devs) { + gdev = gdev || uuid; + let dev = devs[uuid]; + let sel = uuid === gdev ? ' selected' : ''; + html.push(``); + } + + if (html.length) { + gloc.innerHTML = html.join('\n'); + gloc.onchange = (ev) => { + localSet('grid-local', gloc.options[gloc.selectedIndex].value); + }; + $('send-to-gridhead').style.display = 'flex'; + $('send-to-gridspool').style.display = 'flex'; + } else { + $('send-to-gridhead').style.display = ''; + $('send-to-gridspool').style.display = ''; + } + } + + function sendto_gridlocal() { + let uuid = $('grid-local').value; + let dev = grid_local[uuid]; + if (dev) { + let file = $('print-filename').value; + fetch( + `${api.probe.live}/api/grid_send?uuid=${uuid}&file=${encodeURIComponent(file + "." + fileext)}`, + {method: "POST", body: gcode} + ) + .then(t => t.text()) + .then(t => { + api.stats.add(`ua_${api.mode.get_lower()}_print_local_ok`); + console.log({grid_spool_said: t}); + }) + .catch(e => { + api.stats.add(`ua_${api.mode.get_lower()}_print_local_err`); + console.log({grid_local_spool_error: e}); + }) + .finally(() => { + api.modal.hide(); + }); + } + } + + function admin_gridlocal() { + let dev = grid_local[$('grid-local').value]; + if (dev && dev.stat && dev.stat.device) { + let dsd = dev.stat.device; + window.open(`http://${dsd.addr[0]}:${dsd.port || 4080}`, "_grid_admin"); + } + } + + function gridhost_probe(ev, set_host) { + if (ev && ev.code !== 'Enter') return; + if (!(grid_host && grid_apik)) return; + + if (set_host) grid_host.value = set_host; + + let xhtr = new XMLHttpRequest(), + host = grid_host.value, + apik = grid_apik.value, + target = grid_target.value; + + if (!apik) $('gpapik').style.display = 'none'; + + if (!host && api.probe.grid(gridhost_probe)) return; + + if (!host) return; + + xhtr.onreadystatechange = function() { + if (xhtr.readyState === 4) { + if (xhtr.status >= 200 && xhtr.status < 300) { + localSet('grid-host', host); + localSet('grid-apik', apik); + let res = JSON.parse(xhtr.responseText); + let sel = false; + let match = false; + let first = null; + let html = []; + grid_targets = {}; + for (let key in res) { + first = first || key; + if (!localGet('grid-target')) { + localSet('grid-target', key); + sel = true; + } else { + sel = localGet('grid-target') === key; + } + match = match || sel; + grid_targets[html.length] = key; + html.push( + "" + ); + } + if (!match) { + localSet('grid-target', first); + } + grid_target.innerHTML = html.join('\n'); + } else if (xhtr.status === 401) { + $('gpapik').style.display = ''; + } else { + local.removeItem('grid-host'); + local.removeItem('grid-apik'); + console.log("invalid grid:host url"); + } + } + }; + + xhtr.open("GET", host + "/api/active?key=" + apik); + xhtr.send(); + } + + function sendto_gridhost() { + if (!(grid_host && grid_apik)) return; + + let xhtr = new XMLHttpRequest(), + host = grid_host.value, + apik = grid_apik.value, + target = localGet('grid-target') || ''; + + if (target === '') { + api.show.alert('invalid or missing target'); + return; + } + if (host.indexOf("http") !== 0) { + api.show.alert("host missing protocol (http:// or https://)"); + return; + } + if (host.indexOf("://") < 0) { + api.show.alert("host:port malformed"); + return; + } + if (api.const.SECURE && !api.util.isSecure(host)) { + api.show.alert("host must begin with 'https' on a secure site"); + return; + } + + localSet('grid-host', host.trim()); + localSet('grid-apik', apik.trim()); + + xhtr.onreadystatechange = function() { + if (xhtr.readyState === 4) { + let status = xhtr.status; + api.stats.add(`ua_${api.mode.get_lower()}_print_grid_${status}`); + if (status >= 200 && status < 300) { + let json = js2o(xhtr.responseText); + gridhost_tracker(host,json.key); + api.ajax(host+"/api/wait?key="+json.key, function(data) { + data = js2o(data); + console.log(data); + api.show.alert("print to "+target+": "+data.status, 600); + }); + } else { + api.show.alert("grid:host error\nstatus: "+status+"\nmessage: "+xhtr.responseText, 10000); + } + api.show.progress(0); + } + }; + xhtr.upload.addEventListener('progress', function(evt) { + api.show.progress(evt.loaded/evt.total, "sending"); + }); + filename = $('print-filename').value; + xhtr.open("POST", + host + "/api/print?" + + "filename=" + filename + + "&target=" + target + + "&key=" + apik + + "&time=" + Math.round(info.time) + + "&length=" + Math.round(info.distance) + + "&image=" + filename + ); + xhtr.setRequestHeader("Content-Type", "text/plain"); + let snapshot = api.view.snapshot; + xhtr.send(snapshot ? [gcode,snapshot].join("\0") : gcode); + api.modal.hide(); + } + + function download() { + filename = $('print-filename').value; + api.util.download(gcode, filename + "." + fileext); + // saveAs(getBlob(), filename + "." + fileext); + } + + function pad(v) { + v = v.toString(); + return v.length < 2 ? '0' + v : v; + } + + function calcWeight() { + try { + let density = $('print-density'); + info.weight = ( + (Math.PI * util.sqr( + info.settings.device.extruders[0].extFilament / 2 + )) * + info.distance * + (parseFloat(density.value) || 1.25) / + 1000 + ).round(2); + $('print-weight').value = info.weight.toFixed(2); + density.onkeyup = (ev) => { + if (ev.key === 'Enter') calcWeight(); + }; + } catch (e) { } + } + + function calcTime() { + let floor = Math.floor, + time = floor(info.time), + hours = floor(time / 3600), + newtime = time - hours * 3600, + mins = floor(newtime / 60), + secs = newtime - mins * 60; + + $('output-time').value = [pad(hours),pad(mins),pad(secs)].join(':'); + } + + api.modal.show('xany'); + let set = api.conf.get(); + let fdm = MODE === MODES.FDM; + let octo = set.controller.exportOcto && MODE !== MODES.CAM; + let ghost = set.controller.exportGhost; + let local = set.controller.exportLocal; + let preview = set.controller.exportPreview; + $('code-preview-head').style.display = preview ? '' : 'none'; + $('code-preview').style.display = preview ? '' : 'none'; + $('print-download').onclick = download; + $('print-filament').style.display = fdm ? '' : 'none'; + $('print-filename').value = filename; + $('print-filesize').value = util.comma(info.bytes); + $('print-filament').value = Math.round(info.distance); + if (set.controller.devel) { + $('code-preview-textarea').style.height = "30em"; + } + calcTime(); + if (fdm) { + calcWeight(); + } + + // persist fields when changed + bindField('octo-host', 'octo-host'); + bindField('octo-apik', 'octo-apik'); + bindField('grid-host', 'grid-host'); + bindField('grid-apik', 'grid-apik'); + + // in cam mode, show zip file option + let downloadZip = $('print-zip'); + downloadZip.style.display = sections ? 'flex' : 'none'; + downloadZip.onclick = function() { + let files = []; + for (let [ name, data ] of Object.entries(sections)) { + if (name.indexOf('op-') === 0) { + let head = sections.header || []; + let foot = sections.footer || []; + files.push({ + name: `${name}.${fileext}`, + data: [ ...head, ...data, ...foot ].join('\r\n') + }) + } + } + client.zip(files, progress => { + api.show.progress(progress.percent/100, "generating zip files"); + }, output => { + api.show.progress(0); + api.util.download(output, `${$('print-filename').value}.zip`); + }) + }; + + // in fdm mode, show 3mf file option + let nozzle0 = set.device?.extruders?.[0]?.extNozzle || 0.4; + let download3MF = $('print-3mf'); + download3MF.style.display = fdm ? 'flex' : 'none'; + download3MF.onclick = function() { + gen3mf(zip => api.util.download(zip, `${$('print-filename').value}.3mf`)); + }; + + // present bambu print options when selected device is bambu + if (api.bambu) { + api.bambu.prep_export(gen3mf, gcode, info, settings); + } + + // let wids = api.widgets.all(); + // let bnds = settings.bounds; + // console.log({ wids, bnds }); + + function gen3mf(then, ptype = 'unknown', ams = [0]) { + let now = new Date(); + let ymd = [ + now.getFullYear(), + (now.getMonth() + 1).toString().padStart(2,0), + now.getDate().toString().padStart(2,0), + ].join('-'); + let files = [{ + name: `[Content_Types].xml`, + data: [ + '', + '', + ' ', + ' ', + ' ', + '' + ].join('\n') + },{ + name: `_rels/.rels`, + data: [ + '', + '', + ' ', + '' + ].join('\n') + },{ + name: `3D/3dmodel.model`, + data: [ + '', + '', + ' Kiri:Moto', + ' ', + ` ${ymd}`, + ' ', + ' ', + ' ', + ' ', + ` ${ymd}`, + ' ', + ' ', + ' ', + ' ', + ' ', + '' + ].join('\n') + },{ + name: `Metadata/_rels/model_settings.config.rels`, + data: [ + '', + '', + ' ', + '' + ].join('\n') + // },{ + // name: `Metadata/plate_1.json`, + // data: JSON.stringify({ + // "bbox_all": [ 100, 100, 200, 200 ], + // "bbox_objects": (info.labels || []).map(label => { + // return { + // area: 600, + // bbox: [ 100, 100, 200, 200 ], + // id: label, + // layer_height: 0.2, + // name: "Object" + // } + // }), + // "bed_type": "textured_plate", + // "filament_colors": ["#FFFFFF"], + // "filament_ids": ams, + // "first_extruder": ams[0], + // "is_seq_print": false, + // "nozzle_diameter": 0.6, + // "version": 2 + // }) + },{ + name: `Metadata/model_settings.config`, + data: [ + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '' + ].join('\n') + },{ + name: `Metadata/slice_info.config`, + data: [ + '', + '', + '
', + ' ', + ' ', + '
', + ' ', + ' ', + // setting this value allows for the printer to error out if + // the gcode / 3mf was intended for a different target type. + // leaving it blank bypasses the check + // ` `, + ` `, + ' ', + ` `, + ` `, + ' ', + ' ', + ' ', + // (info.labels || []).map(label => + // ` `), + // ' ', + // ' ', + ' ', + '' + ].filter(v => v).flat().join('\n') + },{ + name: `Metadata/project_settings.config`, + data: JSON.stringify({},undefined,4) + },{ + name: `Metadata/plate_1.gcode`, + data: gcode + },{ + name: `Metadata/plate_1.gcode.md5`, + data: ext.md5.hash(gcode) + },{ + name: `Metadata/plate_1_small.png`, + data: api.view.bambu.s128.png + },{ + name: `Metadata/plate_no_light_1.png`, + data: api.view.bambu.s512.png + },{ + name: `Metadata/plate_1.png`, + data: api.view.bambu.s512.png + },{ + name: `Metadata/pick_1.png`, + data: api.view.bambu.s512.png + },{ + name: `Metadata/top_1.png`, + data: api.view.bambu.s512.png + }]; + client.zip(files, progress => { + api.show.progress(progress.percent/100, "generating 3mf"); + }, output => { + api.show.progress(0); + then(output); + }) + }; + + // octoprint setup + $('send-to-octohead').style.display = octo ? '' : 'none'; + $('send-to-octoprint').style.display = octo ? '' : 'none'; + if (octo) try { + $('print-octoprint').onclick = sendto_octoprint; + octo_host = $('octo-host'); + octo_apik = $('octo-apik'); + if (MODE === MODES.CAM) { + $('send-to-octohead').style.display = 'none'; + $('send-to-octoprint').style.display = 'none'; + } else { + $('send-to-octohead').style.display = ''; + $('send-to-octoprint').style.display = ''; + } + // hide octoprint when hard-coded in the url + if (api.const.OCTO) { + $('ophost').style.display = 'none'; + $('opapik').style.display = 'none'; + $('oph1nt').style.display = 'none'; + $('send-to-gridhost').style.display = 'none'; + } + octo_host.value = localGet('octo-host') || ''; + octo_apik.value = localGet('octo-apik') || ''; + } catch (e) { console.log(e) } + + // grid:host setup + $('send-to-hosthead').style.display = ghost ? '' : 'none'; + $('send-to-gridhost').style.display = ghost ? '' : 'none'; + if (ghost) try { + $('print-gridhost').onclick = sendto_gridhost; + $('grid-host').onkeyup = gridhost_probe; + $('grid-apik').onkeyup = gridhost_probe; + grid_host = $('grid-host'); + grid_apik = $('grid-apik'); + grid_target = $('grid-target'); + grid_target.onchange = function(ev) { + localSet('grid-target', grid_targets[grid_target.selectedIndex]); + }; + grid_host.value = localGet('grid-host') || ''; + grid_apik.value = localGet('grid-apik') || ''; + gridhost_probe(); + } catch (e) { console.log(e) } + + // grid:local setup + if (local) try { + gridlocal_probe(); + $('print-gridlocal').onclick = sendto_gridlocal; + $('admin-gridlocal').onclick = admin_gridlocal; + } catch (e) { console.log(e) } + + // preview of the generated GCODE (first 64k max) + if (preview && gcode) $('code-preview-textarea').value = gcode.substring(0,65535); +} diff --git a/src2/kiri/files.js b/src2/kiri/files.js new file mode 100644 index 00000000..0067915e --- /dev/null +++ b/src2/kiri/files.js @@ -0,0 +1,175 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +class Files { + constructor(indexdb) { + let store = this; + this.db = indexdb; + this.files = {}; + this.listeners = []; + this.deferredHandler = null; + this.concurrent = 0; + this.refresh(); + } + + get index() { + return this.db; + } + + refresh() { + let store = this; + store.db.get('files', function(files) { + if (files) { + store.files = files; + notifyFileListeners(store); + } + }); + }; + + wipe() { + let key, files = this.files; + for (key in files) { + if (files.hasOwnProperty(key)) this.deleteFile(key); + } + }; + + fileList() { + return this.files; + }; + + addFileListener(listener) { + if (!this.listeners.contains(listener)) { + this.listeners.push(listener); + listener(this.files); + } + }; + + removeFileListener(listener) { + this.listeners.remove(listener); + }; + + setDeferredHandler(handler) { + this.deferredHandler = handler; + }; + + putDeferred(name, mark) { + // triggers refresh callback + this.files[name] = { + deferred: mark + }; + saveFileList(this); + }; + + /** + * @param {String} name + * @param {Float32Array} vertices + * @param {Function} [callback] + */ + putFile(name, vertices, callback) { + console.log({ putFile: name, vertices }); + + let mark = Date.now(); + let store = this; + let pdb = store.db.promise(); + store.concurrent++; + + function ondone(error) { + if (callback) callback(error); + if (--store.concurrent === 0) { + notifyFileListeners(store); + } + } + + pdb.put(`file-${name}`, vertices) + .then(() => { + store.files[name] = { + vertices: vertices.length / 3, + updated: new Date().getTime() + }; + return pdb.put('files', store.files); + }) + .then(ondone) + .catch(ondone); + }; + + rename(name, newname, callback) { + if (!this.files[name]) return callback({error: 'no such file'}); + if (!newname || newname == name) return callback({error: 'invalid new name'}); + let done = 0; + let error = []; + let store = this; + function complete(ok, err) { + if (err) error.push(err); + if (++done === 1) { + store.files[newname] = store.files[name]; + delete store.files[name]; + saveFileList(store); + store.db.remove(`file-${name}`); + callback(error.length ? {error} : {}); + } + } + store.db.get(`file-${name}`, (vertices) => { + if (!vertices) return complete(false, 'no raw file'); + store.db.put(`file-${newname}`, vertices, complete); + }); + }; + + /** + * @param {String} name + * @param {Function} callback + */ + getFile(name, callback) { + let store = this, + rec = store.files[name]; + if (rec && rec.deferred) { + if (store.deferredHandler) return store.deferredHandler(rec.deferred, name, callback); + return callback(); + } + this.db.get('file-'+name, function(vertices) { + callback(vertices); + }); + }; + + /** + * @param {String} name + * @param {Function} callback + */ + deleteFile(name, callback) { + let store = this; + if (store.files[name]) { + delete store.files[name]; + store.db.remove('file-'+name, function(ok) { + saveFileList(store); + if (callback) callback(ok); + }); + return; + } + if (callback) callback(false); + }; + + deleteFilter(fn, from, to) { + this.db.keys(keys => { + for (let key of keys) { + if (fn(key)) { + this.db.remove(key); + } + } + }, from, to); + } +} + +function saveFileList(store) { + store.db.put('files', store.files); + notifyFileListeners(store); +} + +function notifyFileListeners(store) { + for (let i=0; i -- All Rights Reserved */ +import { load } from '../load.js'; +import { kiri } from './kiri.js'; +import { moto } from '../moto.js'; +import { api } from './api.js'; +import { newWidget } from './widget.js'; +import { conf, event, feature, platform, settings, show } from './api.js'; +import { VIEWS } from './consts.js'; + + +// add frame message api listener +window.addEventListener('message', msg => { + const { load, kiri, moto } = self; + const { api, newWidget } = kiri; + const { conf, event, feature, platform, settings, show } = api; + + if (!feature.frame) return; + + const { origin, source, target, data } = msg; + + if (source.window === target.window) return; + + const 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) { + const cb = data.callback ? (output) => { + send({event:`${data.function}.done`, data: output}); + } : undefined; + api.function[data.function.toLowerCase()](cb); + } + + if (data.event) { + event.on(data.event, (evd) => { + send({event: data.event, data: evd}); + }); + } + + if (data.emit) { + event.emit(data.emit, data.message) + } + + if (data.get) switch (data.get) { + case "mode": send({mode: settings.mode()}); break; + case "device": send({device: settings.dev()}); break; + case "process": send({process: settings.proc()}); break; + default: send({all: settings}); break; + + } + + if (data.features) { + Object.assign(feature, data.features); + api.show.controls(api.feature.controls); + } + + if (data.device) { + Object.assign(settings.dev(), data.device); + conf.save(); + } + + if (data.process){ + Object.assign(settings.proc(), data.process); + conf.save(); + } + + if (data.controller){ + let ctrl = settings.ctrl(); + Object.assign(ctrl, data.controller); + api.event.emit("set.threaded", ctrl.threaded); + conf.save(); + } + + if (data.parse) { + let bin = data.parse; + let widget; + switch ((data.type || 'stl').toLowerCase()) { + case 'stl': + if (!bin.buffer) bin = new Float32Array(bin).buffer; + new load.STL().parse(bin, vertices => { + platform.add(widget = newWidget().loadVertices(vertices)); + send({event: "parsed", data: [ widget.id ]}); + }); + break; + case 'obj': + // todo + break; + case '3mf': + // todo + break; + case 'svg': + let wid = []; + for (let svg of load.SVG.parse(bin)) { + if (!(svg && svg.length)) continue; + platform.add(widget = newWidget().loadVertices(svg.toFloat32())); + wid.push(widget.id); + } + send({event: "parsed", data: wid}); + break; + } + } + + if (data.load) { + platform.load(data.load, (verts, widget) => { + send({event: "loaded", data: [ widget.id ]}); + }) + }; + + if (data.clear) { + platform.clear(); + } + + if (data.alert) { + show.alert(data.alert, data.time); + } + + if (data.progress >= 0) { + show.progress(data.progress, data.message); + } +}); + + +// No direct exports; sets up message listener diff --git a/src2/kiri/function.js b/src2/kiri/function.js new file mode 100644 index 00000000..556121fc --- /dev/null +++ b/src2/kiri/function.js @@ -0,0 +1,502 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from './api.js'; +import { client } from './client.js'; +import { codec } from './codec.js'; +import { space } from '../moto/space.js'; +import { COLOR, PMODES } from './consts.js'; +import { exportFile } from './export.js'; + +let complete = {}; + +function prepareSlices(callback, scale = 1, offset = 0) { + const { conf, event, feature, hide, mode, view, platform, show, stacks } = api; + + if (view.is_arrange()) { + // in arrange mode, create a screenshot at the start slicing + // this can be used later by exports and rendered on some devices + let snap = space.screenshot(); + view.snapshot = snap.substring(snap.indexOf(",") + 1); + client.snap(space.screenshot2({ width: 640 })); + let bambu = view.bambu = { }; + space.screenshot3({ width: 512, out(png) { bambu.s512 = png } }); + space.screenshot3({ width: 128, out(png) { bambu.s128 = png } }); + } + + if (mode.is_sla() && !callback) { + // in SLA mode, slice and preview are the same thing + callback = preparePreview; + } + + const widgets = api.widgets.all(); + const settings = conf.get(); + const { device, process, controller } = settings; + const isBelt = device.bedBelt || false; + const mark = Date.now(); + + // force layout in belt mode when widget exceeds bed length + // which has the side-effect (intended) of increasing the bed size + if (widgets.length && isBelt) { + let doLayout = false; + for (let w of widgets) { + let bb = w.getBoundingBox(); + let yspan = bb.max.y - bb.min.y; + if (yspan > device.bedDepth) { + doLayout = true; + } + } + if (doLayout) { + platform.layout(); + } + } + + functions.clear_progress(); + platform.deselect(); + hide.slider(); + view.set_slice(); + conf.save(); + + // allow client.js to generate support widgets + event.emit('slice.begin', settings.mode); + + // refresh widget list because 'slice.begin' may generate support widgets + const slicing = api.widgets.all() + .filter(w => !w.track.ignore && !w.meta.disabled); + + if (slicing.length === 0) { + return api.show.alert('nothing to slice'); + } + + const totalv = slicing.map(w => w.getVertices().count).reduce((a,v) => a + v); + const defvert = totalv / slicing.length; + const track = {}; + + let totalProgress; + + api.widgets.opacity(COLOR.slicing_opacity); + + let segtimes = {}, + segNumber = 0, + errored = false, + startTime = Date.now(), + toSlice = slicing.slice(), + camOrLaser = mode.is_cam() || mode.is_laser(), + extruders = {}, + lastMsg; + + for (let widget of toSlice) { + widget.belt = null; + widget.stats.progress = 0; + widget.setColor(COLOR.slicing); + let { extruder } = widget.anno; + if (extruder >= 0) { + extruders[extruder] = 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 if (isBelt) { + process.beltAnchor = process.firstLayerBeltLead; + } + + stacks.clear(); // clear rendered stacks + client.clear(); // clear worker cache + client.sync(); // send fresh widget data to worker + + if (isBelt) { + // belt op required to rotate meshes for slicing + client.rotate(settings); + } + + function sliceNext() { + if (toSlice.length) { + // while more widgets to slice + sliceWidget(toSlice.shift()) + } else { + // once all slicing done, run sliceAll() once for all widgets + client.sliceAll(settings, sliceDone); + } + } + + function sliceWidget(widget) { + + function onupdate(update, msg, alert) { + if (alert) { + api.show.alert(alert); + } + if (msg && msg !== lastMsg) { + let mark = Date.now(); + if (lastMsg) { + segtimes[`${widget.id}_${segNumber++}_${lastMsg}`] = mark - startTime; + } + lastMsg = msg; + startTime = mark; + } + // on update + if (update >= 0) { + track[widget.id] = (update || 0) * factor; + totalProgress = 0; + for (let w of slicing) { + totalProgress += (track[w.id] || 0); + } + show.progress(offset + (totalProgress / slicing.length) * scale, msg); + } + } + + function ondone(sliced, error) { + let mark = Date.now(); + // update UI info + if (sliced) { + // update segment time + if (lastMsg) { + segtimes[`${widget.id}_${segNumber++}_${lastMsg}`] = mark - startTime; + } + event.emit('slice', settings.mode); + } + // handle slicing errors + if (error && !errored) { + errored = true; + view.set_arrange(); + show.alert(error, 5); + show.progress(0); + client.restart(); + event.emit('slice.error', error); + setTimeout(view.set_arrange, 1); + } + if (errored) { + // terminate slicing + sliceDone(); + } else { + // start next widget slice + sliceNext(); + } + } + + // weight each widget progress % by their # vertices + let factor = (widget.getVertices().count / defvert); + + widget.settings = settings; + widget.clearSlices(); + + onupdate(0.0001, "slicing"); + + // store slicing visuals + widget.stack = api.stacks.create(widget.id, widget.mesh); + + // compensate for zcut (widget moved through floor) + widget.stack.obj.view.position.z = widget.track.zcut || 0; + + // in case result of slice is nothing, do not preserve previous + widget.slices = [] + + // executed from kiri.js + client.slice(settings, widget, (reply) => { + if (reply.alert) { + onupdate(null, null, reply.alert); + } + if (reply.update) { + onupdate(reply.update, reply.updateStatus); + } + if (reply.send_start) { + widget.xfer = {start: reply.send_start}; + } + if (reply.stats) { + widget.stats = reply.stats; + } + if (reply.send_end) { + widget.stats.load_time = widget.xfer.start - reply.send_end; + } + if (reply.slice) { + widget.slices.push(codec.decode(reply.slice, {mesh:widget.mesh})); + } + if (reply.done) { + ondone(true); + } + if (reply.error) { + ondone(false, reply.error); + } + }); + + // discard point cache + widget.points = undefined; + } + + function sliceDone() { + let alert = null; + if (scale === 1 && feature.work_alerts && slicing.length) { + alert = show.alert("Rendering"); + }; + for (let widget of slicing) { + // on done + segtimes[`${widget.id}_${segNumber++}_draw`] = widget.render(widget.stack); + // rotate stack for belt beds + if (widget.belt) { + widget.stack.obj.rotate(widget.belt); + } + 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); + } + } + view.update_slider_max(true); + show.layer(-1, 0); + if (scale === 1) { + view.update_stack_labels(); + } + // if (!isBelt && controller.lineType === 'line' && !process.xray && !controller.devel) { + // $('render-ghost').onclick(); + // } + if (scale === 1) { + show.progress(0); + } + // mark slicing complete for prep/preview + complete.slice = true; + event.emit('slice.end', settings.mode); + // print stats + segtimes.total = Date.now() - mark; + console.log(segtimes); + if (callback && typeof callback === 'function') { + callback(); + } + if (isBelt) { + // required to render supports properly (in belt mode) + // because this updates widget top z which is affected by rotation + api.platform.update_top_z(); + } + // refresh / repaint workspace + space.refresh(); + } + + // kick off slicing chain + sliceNext(); +} + +function preparePreview(callback, scale = 1, offset = 0) { + const { conf, event, feature, hide, mode, view, platform, show, stacks } = api; + const widgets = api.widgets.all(); + const settings = conf.get(); + const { device, process, controller } = settings; + + if (complete.preview === feature.pmode) { + if (device.extruders && 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; + } + + hide.slider(true); + + const isCam = mode.is_cam(), pMode = settings.mode; + const isDark = api.space.is_dark(); + const isBelt = device.bedBelt || false; + + view.set_preview(); + conf.save(); + event.emit('preview.begin', pMode); + + if (isCam) { + api.widgets.opacity(isDark ? COLOR.cam_preview_opacity_dark : COLOR.cam_preview_opacity); + api.widgets.each(widget => { + widget.setColor(COLOR.cam_preview); + }); + } else if (offset === 0) { + api.widgets.opacity(COLOR.preview_opacity); + } + + let mark = Date.now(), + segNumber = 0, + segtimes = {}, + startTime, + lastMsg, + output = []; + + // pass preview mode to worker + settings.pmode = feature.pmode; + settings.render = true; + + client.prepare(settings, (progress, message, layer) => { + if (layer) { + output.push(codec.decode(layer)); + } + if (message && message !== lastMsg) { + const mark = Date.now(); + if (lastMsg) { + segtimes[`${segNumber++}_${lastMsg}`] = mark - startTime; + } + lastMsg = message; + startTime = mark; + } + show.progress(offset + progress * scale, message); + }, (reply, maxSpeed, minSpeed) => { + // handle worker errors + if (reply && reply.error) { + show.alert(reply.error, 5); + view.set_arrange(); + show.progress(0); + space.update(); + event.emit('preview.error', reply.error); + return; + } + + if (lastMsg) { + segtimes[`${segNumber++}_${lastMsg}`] = Date.now() - startTime; + } + + show.progress(0); + if (!isCam) { + api.widgets.opacity(0); + } + + if (output.length) { + let alert = feature.work_alerts ? show.alert("Rendering") : null; + startTime = Date.now(); + stacks.clear(); + const stack = stacks.create('print', space.world) + const view = stack.obj.view; + output.forEach(layer => { + stack.add(layer); + }); + event.emit('preview.view', view); + // rotate stack for belt beds + if (isBelt && widgets[0].belt) { + let ri = widgets[0].belt; + ri.dz = 0; + ri.dy = settings.device.bedDepth / 2; + stack.obj.rotate(widgets[0].belt); + } + api.hide.alert(alert); + segtimes[`${segNumber}_draw`] = Date.now() - startTime; + } + + // print stats + segtimes.total = Date.now() - mark; + console.log(segtimes); + + event.emit('print', pMode); + event.emit('preview.end', pMode); + + space.update(); + view.update_slider_max(true); + show.layer(-1, 0); + if (feature.pmode === PMODES.SPEED) { + view.update_speeds(maxSpeed, minSpeed); + } else { + view.update_speeds(); + } + view.update_stack_labels(); + + let { controller, process } = settings; + // if (!isBelt && controller.lineType === 'line' && !process.xray && !controller.devel) { + // $('render-ghost').onclick(); + // } + + // mark preview complete for export + complete.preview = feature.pmode; + + if (typeof(callback) === 'function') { + callback(); + } + }); +} + +function prepareAnimation() { + if (!window.SharedArrayBuffer) { + api.alerts.show("The security context of this"); + api.alerts.show("window prevents animations."); + api.alerts.show("Try a Chromium-based browser"); + return; + } + api.event.emit("function.animate", {mode: api.conf.get().mode}); +} + +function prepareExport() { + const settings = api.conf.get(); + const argsave = arguments; + if (!complete.preview) { + preparePreview(() => { prepareExport(...argsave) }); + return; + } + api.event.emit("function.export", {mode: settings.mode}); + complete.export = true; + exportFile(...argsave); +} + +function cancelWorker() { + if (client.isBusy()) { + client.restart(); + } +} + +function parseCode(code, type) { + const { conf, event, show, widgets, view } = api; + const { stacks } = kiri; + const settings = conf.get(); + + event.emit("code.load", {code, type}); + view.set_preview(); + widgets.opacity(0); + + client.parse({code, type, settings}, progress => { + show.progress(progress, "parsing"); + }, (layers, maxSpeed, minSpeed) => { + show.progress(0); + stacks.clear(); + const stack = stacks.create('parse', space.world); + layers.forEach(layer => stack.add(layer)); + view.update_slider_max(true); + view.update_speeds(maxSpeed, minSpeed); + show.slices(); + view.update_stack_labels(); + space.update(); + event.emit("code.loaded", {code, type}); + }); +} + +function clear_progress() { + complete = {}; +} + +// extend API (api.function) +export const functions = { + slice: prepareSlices, + print: preparePreview, + prepare: preparePreview, + animate: prepareAnimation, + export: prepareExport, + cancel: cancelWorker, + parse: parseCode, + clear: client.clear, + clear_progress, +}; + +export { + prepareSlices as slice, + preparePreview as print, + preparePreview as prepare, + prepareAnimation as animate, + prepareExport as export, + cancelWorker as cancel, + parseCode as parse, + clear_progress, +}; diff --git a/src2/kiri/init.js b/src2/kiri/init.js new file mode 100644 index 00000000..be0772d2 --- /dev/null +++ b/src2/kiri/init.js @@ -0,0 +1,2155 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $, $c } from '../moto/webui.js'; +import { api } from '../kiri/api.js'; +import { base } from '../geo/base.js'; +import { local as sdb } from '../data/local.js'; +import { space } from '../moto/space.js'; +import { LOCAL, SETUP } from './main.js'; +import { VIEWS, MODES, SEED } from '../kiri/consts.js'; +import { init as initCAM } from '../kiri-mode/cam/client.js'; +import { init as initFDM } from '../kiri-mode/fdm/client.js'; +import { beta, version } from '../moto/license.js'; +import { settings as set_ctrl } from './settings.js'; + +let { CAM, SLA, FDM, LASER, DRAG, WJET, WEDM } = MODES, + { client, catalog, platform, selection } = api, + LANG = api.language.current, + WIN = self.window, + DOC = self.document, + STARTMODE = SETUP.sm && SETUP.sm.length === 1 ? SETUP.sm[0] : null, + TWOD = [ LASER, DRAG, WJET, WEDM ], + TWONED = [ LASER, DRAG, WJET ], + THREED = [ FDM, CAM, SLA ], + GCODE = [ FDM, CAM, ...TWOD ], + CAM_LZR = [ CAM, ...TWOD ], + FDM_LZN = [ FDM, ...TWONED ], + NO_WEDM = [ FDM, CAM, SLA, LASER, DRAG, WJET ], + FDM_CAM = [ FDM, CAM ], + proto = location.protocol, + platformColor, + separator = true, + hideable = true, + inline = true, + driven = true, + trigger = true, + units = true, + ui = api.ui, + uc = api.uc; + +function settings() { + return api.conf.get(); +} + +function updateTool(ev) { + api.tool.update(ev); +} + +function checkSeed(then) { + // skip sample object load in onshape (or any script postload) + if (!sdb[SEED]) { + sdb[SEED] = new Date().getTime(); + if (!SETUP.s && api.feature.seed) { + if (SETUP.debug) { + return then(); + } + platform.load_stl("/obj/cube.stl", function(vert) { + catalog.putFile("sample cube.stl", vert); + platform.update_bounds(); + space.view.home(); + setTimeout(() => { api.space.save(true) },500); + then(); + api.help.show(); + }); + return true; + } + } + return false; +} + +function unitsSave() { + api.conf.update({ controller: true }); + platform.update_size(); +} + +function aniMeshSave() { + api.conf.update({ controller: true }); + api.conf.save(); +} + +function lineTypeSave() { + const sel = ui.lineType.options[ui.lineType.selectedIndex]; + if (sel) { + settings().controller.lineType = sel.value; + api.conf.save(); + } +} + +function thinWallSave() { + let opt = ui.sliceDetectThin; + let level = opt.options[opt.selectedIndex]; + if (level) { + settings().process.sliceDetectThin = level.value; + api.conf.save(); + } +} + +function detailSave() { + let level = ui.detail.options[ui.detail.selectedIndex]; + if (level) { + level = level.value; + let rez = base.config.clipperClean; + switch (level) { + case '100': rez = 50; break; + case '75': rez = base.config.clipperClean; break; + case '50': rez = 500; break; + case '25': rez = 1000; break; + } + client.config({ + base: { clipperClean: rez } + }); + settings().controller.detail = level; + api.conf.save(); + } +} + +function speedSave() { + settings().controller.showSpeeds = ui.showSpeeds.checked; + api.view.update_speeds(); +} + +function zAnchorSave() { + api.conf.update(); + api.platform.update_top_z(); +} + +function setThreaded(bool) { + if (bool) { + client.pool.start(); + } else { + client.pool.stop(); + } + return bool; +} + +// upon restore, seed presets +api.event.emit('preset', api.conf.dbo()); + +api.event.on("set.threaded", bool => setThreaded(bool)); + +function booleanSave() { + let control = settings().controller; + let doAlert = ui.ortho.checked !== control.ortho; + if (control.assembly != ui.assembly.checked) { + client.wasm(ui.assembly.checked); + } + if (control.antiAlias != ui.antiAlias.checked) { + api.show.alert('Page Reload Required to Change Aliasing'); + } + control.shiny = ui.shiny.checked; + control.drawer = ui.drawer.checked; + control.scrolls = ui.scrolls.checked; + control.showOrigin = ui.showOrigin.checked; + control.showRulers = ui.showRulers.checked; + control.autoLayout = ui.autoLayout.checked; + control.freeLayout = ui.freeLayout.checked; + control.spaceRandoX = ui.spaceRandoX.checked; + control.autoSave = ui.autoSave.checked; + control.antiAlias = ui.antiAlias.checked; + control.reverseZoom = ui.reverseZoom.checked; + control.dark = ui.dark.checked; + control.exportOcto = ui.exportOcto.checked; + control.exportGhost = ui.exportGhost.checked; + control.exportLocal = ui.exportLocal.checked; + control.exportThumb = ui.exportThumb.checked; + control.exportPreview = ui.exportPreview.checked; + control.healMesh = ui.healMesh.checked; + control.threaded = setThreaded(ui.threaded.checked); + control.assembly = ui.assembly.checked; + control.ortho = ui.ortho.checked; + control.devel = ui.devel.checked; + space.view.setZoom(control.reverseZoom, control.zoomSpeed); + // platform.layout(); + api.conf.save(); + api.platform.update_size(); + updateStats(); + updateDrawer(); + api.event.emit('boolean.update'); + if (doAlert) { + api.show.alert("change requires page refresh"); + } +} + +function updateDrawer() { + const { drawer, scrolls } = settings().controller; + $c('app', drawer ? 'slideshow' : '', drawer ? '' : 'slideshow'); + $c('app', scrolls ? '' : 'hide-scroll', scrolls ? 'hide-scroll' : ''); +} + +function updateStats() { + if (self.debug !== true) { + return; + } + let { div, fps, rms, rnfo } = ui.stats; + div.style.display = 'flex'; + setInterval(() => { + const nrms = space.view.getRMS().toFixed(1); + const nfps = space.view.getFPS().toFixed(1); + const rend = space.renderInfo(); + const { memory, render } = rend; + if (nfps !== fps.innerText) { + fps.innerText = nfps; + } + if (nrms !== rms.innerText) { + rms.innerText = nrms; + } + if (rnfo.offsetParent !== null) { + rnfo.innerHTML = Object.entries({ ...memory, ...render, render_ms: nrms, frames_sec: nfps }).map(row => { + return `
${row[0]}
` + }).join(''); + } + }, 100); +} + +function onBooleanClick(el) { + // copy some ui elements to target settings + let settings = api.conf.get(); + settings.device.bedBelt = ui.deviceBelt.checked && api.mode.is_fdm(); + settings.device.bedRound = ui.deviceRound.checked && api.mode.is_fdm(); + settings.device.originCenter = ui.deviceOrigin.checked || ui.deviceRound.checked; + settings.device.fwRetract = ui.fwRetract.checked; + // refresh vars and other ui elements + uc.refresh(); + if (el === ui.camStockIndexed) { + api.view.set_arrange(); + } + api.conf.update(); + DOC.activeElement.blur(); + api.event.emit("boolean.click"); + api.devices.update_laser_state(); +} + +function onButtonClick(ev) { + let target = ev.target; + while (target && target.tagName !== 'BUTTON') { + target = target.parentNode; + } + api.event.emit("button.click", target); +} + +function inputHasFocus() { + let active = DOC.activeElement; + return active && (active.nodeName === "INPUT" || active.nodeName === "TEXTAREA"); +} + +function cca(c) { + return c.charCodeAt(0); +} + +function keyUpHandler(evt) { + if (api.feature.on_key) { + if (api.feature.on_key({up:evt})) return; + } + for (let handler of api.feature.on_key2) { + if (handler({up:evt})) return; + } + switch (evt.keyCode) { + // escape + case 27: + // blur text input focus + DOC.activeElement.blur(); + // dismiss modals + api.modal.hide(); + // deselect widgets + platform.deselect(); + // hide all dialogs + api.dialog.hide(); + // cancel slicing + api.function.cancel(); + // and send an event (used by FDM client) + api.event.emit("key.esc"); + break; + } + return false; +} + +function keyDownHandler(evt) { + if (api.modal.visible()) { + return false; + } + if (api.feature.on_key) { + if (api.feature.on_key({down:evt})) return; + } + for (let handler of api.feature.on_key2) { + if (handler({down:evt})) return; + } + let move = evt.altKey ? 5 : 0, + deg = move ? 0 : -Math.PI / (evt.shiftKey ? 36 : 2); + switch (evt.keyCode) { + case 8: // apple: delete/backspace + case 46: // others: delete + if (inputHasFocus()) return false; + platform.delete(selection.meshes()); + evt.preventDefault(); + break; + case 37: // left arrow + if (inputHasFocus()) return false; + if (deg) selection.rotate(0, 0, -deg); + if (move > 0) selection.move(-move, 0, 0); + evt.preventDefault(); + break; + case 39: // right arrow + if (inputHasFocus()) return false; + if (deg) selection.rotate(0, 0, deg); + if (move > 0) selection.move(move, 0, 0); + evt.preventDefault(); + break; + case 38: // up arrow + if (inputHasFocus()) return false; + if (evt.metaKey) return api.show.layer(api.var.layer_at+1); + if (deg) selection.rotate(deg, 0, 0); + if (move > 0) selection.move(0, move, 0); + evt.preventDefault(); + break; + case 40: // down arrow + if (inputHasFocus()) return false; + if (evt.metaKey) return api.show.layer(api.var.layer_at-1); + if (deg) selection.rotate(-deg, 0, 0); + if (move > 0) selection.move(0, -move, 0); + evt.preventDefault(); + break; + case 65: // 'a' for select all + if (evt.metaKey || evt.ctrlKey) { + if (inputHasFocus()) return false; + evt.preventDefault(); + platform.deselect(); + platform.select_all(); + } + break; + case 83: // 's' for save workspace + if (evt.ctrlKey) { + evt.preventDefault(); + api.conf.save(); + console.log("settings saved"); + } else + if (evt.metaKey) { + evt.preventDefault(); + api.space.save(); + set_ctrl.sync.put(); + } + break; + case 76: // 'l' for restore workspace + if (evt.metaKey) { + evt.preventDefault(); + api.space.restore(); + } + break; + } +} + +function keyHandler(evt) { + let handled = true; + if (api.modal.visible() || inputHasFocus()) { + return false; + } + if (api.feature.on_key) { + if (api.feature.on_key({key:evt})) return; + } + for (let handler of api.feature.on_key2) { + if (handler({key:evt})) return; + } + if (evt.ctrlKey) { + switch (evt.key) { + case 'g': return api.group.merge(); + case 'u': return api.group.split(); + } + } + switch (evt.charCode) { + case cca('`'): api.show.slices(0); break; + case cca('0'): api.show.slices(api.var.layer_max); break; + case cca('1'): api.show.slices(api.var.layer_max/10); break; + case cca('2'): api.show.slices(api.var.layer_max*2/10); break; + case cca('3'): api.show.slices(api.var.layer_max*3/10); break; + case cca('4'): api.show.slices(api.var.layer_max*4/10); break; + case cca('5'): api.show.slices(api.var.layer_max*5/10); break; + case cca('6'): api.show.slices(api.var.layer_max*6/10); break; + case cca('7'): api.show.slices(api.var.layer_max*7/10); break; + case cca('8'): api.show.slices(api.var.layer_max*8/10); break; + case cca('9'): api.show.slices(api.var.layer_max*9/10); break; + case cca('?'): + api.help.show(); + break; + case cca('Z'): // reset stored state + uc.confirm('clear all settings and preferences?').then(yes => { + if (yes) { + sdb.clear(); + WIN.location.reload(); + } + }); + break; + case cca('C'): // refresh catalog + catalog.refresh(); + break; + case cca('i'): // file import + api.event.import(); + break; + case cca('S'): // slice + case cca('s'): // slice + api.function.slice(); + break; + case cca('P'): // prepare + case cca('p'): // prepare + if (api.mode.get() !== 'SLA') { + // hidden in SLA mode + api.function.print(); + } + break; + case cca('X'): // export + case cca('x'): // export + api.function.export(); + break; + case cca('g'): // CAM animate + api.function.animate(); + break; + case cca('O'): // manual rotation + rotateInputSelection(); + break; + case cca('r'): // recent files + api.modal.show('files'); + break; + case cca('q'): // preferences + api.modal.show('prefs'); + break; + case cca('l'): // device + settingsLoad(); + break; + case cca('e'): // device + api.show.devices(); + break; + case cca('o'): // tools + api.show.tools(); + break; + case cca('c'): // local devices + api.show.local(); + break; + case cca('v'): // toggle single slice view mode + if (api.view.get() === VIEWS.ARRANGE) { + api.space.set_focus(selection.widgets()); + } + if (api.var.layer_hi == api.var.layer_lo) { + api.var.layer_lo = 0; + } else { + api.var.layer_lo = api.var.layer_hi; + } + api.show.slices(); + break; + case cca('d'): // duplicate object + duplicateSelection(); + break; + case cca('m'): // mirror object + mirrorSelection(); + break; + case cca('a'): + if (api.view.get() === VIEWS.ARRANGE) { + // auto arrange items on platform + platform.layout(); + if (!api.conf.get().controller.spaceRandoX) { + api.space.set_focus(selection.widgets()); + } + } else { + // go to arrange view + api.view.set(VIEWS.ARRANGE); + } + break; + default: + api.event.emit('keypress', evt); + handled = false; + break; + } + if (handled) { + evt.preventDefault(); + evt.stopPropagation(); + } + return false; +} + +function duplicateSelection() { + selection.duplicate(); +} + +function mirrorSelection() { + selection.mirror(); +} + +function keys(o) { + let key, list = []; + for (key in o) { if (o.hasOwnProperty(key)) list.push(key) } + return list.sort(); +} + +function clearSelected(children) { + for (let i=0; i { + coord = (coord || '').split(','); + let prod = Math.PI / 180, + x = parseFloat(coord[0] || 0.0) * prod, + y = parseFloat(coord[1] || 0.0) * prod, + z = parseFloat(coord[2] || 0.0) * prod; + selection.rotate(x, y, z); + }); +} + +function positionSelection() { + if (selection.meshes().length === 0) { + api.show.alert("select object to position"); + return; + } + let current = settings(), + { device, process} = current, + center = process.ctOriginCenter || process.camOriginCenter || device.bedRound || device.originCenter, + bounds = boundsSelection(); + + api.uc.prompt("Enter X,Y coordinates for selection","").then(coord => { + coord = (coord || '').split(','); + let x = parseFloat(coord[0] || 0.0), + y = parseFloat(coord[1] || 0.0), + z = parseFloat(coord[2] || 0.0); + + if (!center) { + x = x - device.bedWidth/2 + (bounds.max.x - bounds.min.x)/2; + y = y - device.bedDepth/2 + (bounds.max.y - bounds.min.y)/2 + } + + selection.move(x, y, z, true); + }); +} + +function deviceExport(exp, name) { + name = (name || "device") + .toLowerCase() + .replace(/ /g,'_') + .replace(/\./g,'_'); + uc.prompt("Export Device Filename", name).then(name => { + if (name) { + api.util.download(exp, `${name}.km`); + } + }); +} + +function objectsExport(format = "stl") { + // return selection.export(); + uc.confirm("Export Filename", {ok:true, cancel: false}, `selected.${format}`).then(name => { + if (!name) return; + if (name.toLowerCase().indexOf(`.${format}`) < 0) { + name = `${name}.${format}`; + } + api.util.download(selection.export(format), name); + }); +} + +function workspaceNew() { + uc.confirm("Clear Workspace?", {ok:true, cancel: false}).then(value => { + if (value === true) { + let proc = set_ctrl.proc(); + proc.ops && (proc.ops.length = 0); + proc.op2 && (proc.op2.length = 0); + api.platform.clear(); + } + }); +} + +function profileExport() { + const opt = {pre: [ + "
", + "

Workspace Export

", + " ", + " ", + "  Exclude meshes", + "
" + ]}; + let suggestion = "workspace"; + let file = api.widgets.all()[0]?.meta.file || ''; + if (file) { + suggestion = `${suggestion}_${file.split('.')[0]}`.replaceAll(' ','_'); + }; + uc.confirm("Filename", {ok:true, cancel: false}, suggestion, opt).then(name => { + if (!name) return; + + let work = !$('excwork').checked; + let json = api.conf.export({work, clear:true}); + + client.zip([ + {name:"workspace.json", data:JSON.stringify(json)} + ], progress => { + api.show.progress(progress.percent/100, "compressing workspace"); + }, output => { + api.show.progress(0); + api.util.download(output, `${name}.kmz`); + }); + }); +} + +function settingsSave(ev, name) { + if (ev) { + ev.stopPropagation(); + ev.preventDefault(); + } + + api.dialog.hide(); + let mode = api.mode.get(), + s = settings(), + def = "default", + cp = s.process, + pl = s.sproc[mode], + lp = s.cproc[mode], + saveAs = (name) => { + if (!name) { + return; + } + let np = pl[name] = {}; + cp.processName = name; + pl[name] = Object.clone(cp); + for (let k in cp) { + if (!cp.hasOwnProperty(k)) continue; + np[k] = cp[k]; + } + s.cproc[mode] = name; + s.devproc[s.device.deviceName] = name; + api.conf.save(); + api.conf.update(); + api.event.settings(); + sync_put(); + }; + + if (name) { + saveAs(name); + } else { + uc.prompt("Save Settings As", cp ? lp || def : def).then(saveAs); + } +} + +function settingsLoad() { + api.conf.show(); +} + +function updateDeviceSize() { + api.conf.update(); + platform.update_size(); + platform.update_origin(); +} + +async function sync_put() { + await set_ctrl.sync.put(); +} + +function dragOverHandler(evt) { + evt.stopPropagation(); + evt.preventDefault(); + + // prevent drop actions when a dialog is open + if (api.modal.visible()) { + return; + } + + evt.dataTransfer.dropEffect = 'copy'; + let oldcolor = space.platform.setColor(0x00ff00); + if (oldcolor !== 0x00ff00) platformColor = oldcolor; +} + +function dragLeave() { + space.platform.setColor(platformColor); +} + +function dropHandler(evt) { + evt.stopPropagation(); + evt.preventDefault(); + + // prevent drop actions when a dialog is open + if (api.modal.visible()) { + return; + } + + space.platform.setColor(platformColor); + + let files = evt.dataTransfer.files; + + switch (api.feature.drop_group) { + case true: + return api.platform.load_files(files, []); + case false: + return api.platform.load_files(files, undefined); + } + + if (files.length === 1) { + api.platform.load_files(files); + } else if (files.length > 1) { + uc.confirm(`group ${files.length} files?`).then(yes => { + api.platform.load_files(files, yes ? [] : undefined); + }); + } +} + +function loadCatalogFile(e) { + api.widgets.load(e.target.getAttribute('load'), function(widget) { + platform.add(widget); + api.dialog.hide(); + }); +} + +function updateCatalog(files) { + let table = ui.catalogList, + list = []; + table.innerHTML = ''; + for (let name in files) { + list.push({n:name, ln:name.toLowerCase(), v:files[name].vertices, t:files[name].updated}); + } + list.sort(function(a,b) { + return a.ln < b.ln ? -1 : 1; + }); + for (let i=0; i { + api.uc.prompt(`rename file`, short).then(newname => { + if (newname && newname !== short) { + catalog.rename(name, `${newname}${ext}`, then => { + api.modal.show('files'); + }); + } + }); + }; + + load.setAttribute('load', name); + load.setAttribute('title', `file: ${name}\nvertices: ${file.v}\ndate: ${date}`); + load.onclick = loadCatalogFile; + load.appendChild(DOC.createTextNode(short)); + + del.setAttribute('del', name); + del.setAttribute('title', "remove '"+name+"'"); + del.onclick = () => { catalog.deleteFile(name) }; + del.innerHTML = ''; + + size.setAttribute("disabled", true); + size.setAttribute("class", "label"); + size.appendChild(DOC.createTextNode(base.util.comma(file.v))); + + row.setAttribute("class", "f-row a-center"); + row.appendChild(renm); + row.appendChild(load); + row.appendChild(size); + row.appendChild(del); + table.appendChild(row); + } +} + +function isMultiHead() { + let dev = api.conf.get().device; + return isNotBelt() && dev.extruders && dev.extruders.length > 1; +} + +function isBelt() { + return api.device.isBelt(); +} + +function isNotBelt() { + return !isBelt(); +} + +// MAIN INITIALIZATION FUNCTION +function init_one() { + let { event, conf, view, show } = api, + { bound, toInt, toFloat } = uc, + { newBlank, newButton, newBoolean, newGroup, newInput } = uc, + { newSelect, newLabel, newValue, newRow, newGCode, newDiv } = uc; + + event.emit('init.one'); + + // ensure we have settings from last session + conf.restore(); + + let container = $('container'), + gcode = $('dev-gcode'), + tracker = $('tracker'), + controller = settings().controller; + + WIN.addEventListener("resize", () => { + event.emit('resize'); + }); + + event.on('resize', () => { + if (WIN.innerHeight < 800) { + ui.modalBox.classList.add('mh85'); + } else { + ui.modalBox.classList.remove('mh85'); + } + view.update_slider(); + }); + + space.sky.showGrid(false); + space.sky.setColor(controller.dark ? 0 : 0xffffff); + space.setAntiAlias(controller.antiAlias); + space.init(container, function (delta) { + let vars = api.var; + if (vars.layer_max === 0 || !delta) return; + if (controller.reverseZoom) delta = -delta; + let same = vars.layer_hi === vars.layer_lo; + let track = vars.layer_lo > 0; + if (delta > 0) { + vars.layer_hi = Math.max(same ? 0 : vars.layer_lo, vars.layer_hi - 1); + if (track) { + vars.layer_lo = Math.max(0, vars.layer_lo - 1); + } + } else if (delta < 0) { + vars.layer_hi = Math.min(vars.layer_max, vars.layer_hi + 1); + if (track) { + vars.layer_lo = Math.min(vars.layer_hi, vars.layer_lo + 1); + } + } + if (same) { + vars.layer_lo = vars.layer_hi; + } + view.update_slider(); + show.slices(); + }, controller.ortho); + space.platform.onMove(conf.save); + space.platform.setRound(true); + space.useDefaultKeys(api.feature.on_key === undefined || api.feature.on_key_defaults); + updateDrawer(); + + // api augmentation with local functions + api.device.export = deviceExport; + let anim = ui.anim = {}; + + Object.assign(ui, { + tracker: tracker, + container: container, + + alert: { + dialog: $('alert-area'), + text: $('alert-text') + }, + func: { + slice: $('act-slice'), + preview: $('act-preview'), + animate: $('act-animate'), + export: $('act-export') + }, + label: { + slice: $('label-slice'), + preview: $('label-preview'), + animate: $('label-animate'), + export: $('label-export'), + }, + acct: { + help: $('app-help'), + don8: $('app-don8'), + mesh: $('app-mesh'), + export: $('app-export') + }, + dev: { + header: $('dev-header'), + search: $('dev-search'), + filter: $('dev-filter') + }, + mesh: { + name: $('mesh-name'), + points: $('mesh-points'), + faces: $('mesh-faces'), + }, + + stats: { + fps: $('fps'), + rms: $('rms'), + div: $('stats'), + rnfo: $('rnfo'), + }, + + load: $('load-file'), + speeds: $('speeds'), + speedbar: $('speedbar'), + context: $('context-menu'), + + ltsetup: $('lt-setup'), + ltfile: $('lt-file'), + ltview: $('lt-view'), + ltact: $('lt-start'), + edit: $('lt-tools'), + nozzle: $('menu-nozzle'), + + modal: $('modal'), + modalBox: $('modal-box'), + modals: { + help: $('mod-help'), + setup: $('mod-setup'), + prefs: $('mod-prefs'), + files: $('mod-files'), + saves: $('mod-saves'), + tools: $('mod-tools'), + xany: $('mod-x-any'), + xsla: $('mod-x-sla'), + xlaser: $('mod-x-laser'), + local: $('mod-local'), + don8: $('mod-don8'), + any: $('mod-any'), + }, + + catalogBody: $('catalogBody'), + catalogList: $('catalogList'), + + devices: $('devices'), + deviceAdd: $('device-add'), + deviceDelete: $('device-del'), + deviceRename: $('device-ren'), + deviceExport: $('device-exp'), + deviceSave: $('device-save'), + + toolsSave: $('tools-save'), + toolsClose: $('tools-close'), + toolsImport: $('tools-import'), + toolsExport: $('tools-export'), + toolSelect: $('tool-select'), + toolAdd: $('tool-add'), + toolCopy: $('tool-dup'), + toolDelete: $('tool-del'), + toolType: $('tool-type'), + toolName: $('tool-name'), + toolNum: $('tool-num'), + toolFluteDiam: $('tool-fdiam'), + toolFluteLen: $('tool-flen'), + toolShaftDiam: $('tool-sdiam'), + toolShaftLen: $('tool-slen'), + toolTaperAngle: $('tool-tangle'), + toolTaperTip: $('tool-ttip'), + toolMetric: $('tool-metric'), + + setMenu: $('set-menu'), + settings: $('settings'), + settingsBody: $('settingsBody'), + settingsList: $('settingsList'), + + slider: $('slider'), + sliderMax: $('slider-max'), + sliderMin: $('slider-zero'), + sliderLo: $('slider-lo'), + sliderMid: $('slider-mid'), + sliderHi: $('slider-hi'), + sliderHold: $('slider-hold'), + sliderRange: $('slider-center'), + + loading: $('progress').style, + progress: $('progbar').style, + prostatus: $('progtxt'), + + selection: $('selection'), + sizeX: $('size_x'), + sizeY: $('size_y'), + sizeZ: $('size_z'), + scaleX: $('scale_x'), + scaleY: $('scale_y'), + scaleZ: $('scale_z'), + lockX: $('lock_x'), + lockY: $('lock_y'), + lockZ: $('lock_z'), + stock: $('stock'), + stockWidth: $('stock-width'), + stockDepth: $('stock-width'), + stockHeight: $('stock-width'), + + /** CAM Animation Bar */ + + _____: { + _____: newDiv({ addto: $('layer-animate'), set:true }), + row: newRow([ + anim.replay = newButton(null,"anim.replay",{icon:'',title:"restart"}), + anim.play = newButton(null,"anim.play",{icon:'',title:"play"}), + anim.pause = newButton(null,"anim.pause",{icon:'',title:"pause"}), + anim.step = newButton(null,"anim.step",{icon:'',title:"single step"}), + anim.speed = newButton(null,"anim.fast",{icon:'',title:"toggle speed"}), + anim.labspd = newValue(3, {class:"center padleft"}), + anim.labx = newLabel("X", {class:"padleft"}), + anim.valx = newValue(7, {class:"center"}), + anim.laby = newLabel("Y", {class:"padleft"}), + anim.valy = newValue(7, {class:"center"}), + anim.labz = newLabel("Z", {class:"padleft"}), + anim.valz = newValue(6, {class:"center"}), + anim.laba = newLabel("A", {class:"padleft hide"}), + anim.vala = newValue(6, {class:"center hide"}), + anim.labpro = newLabel("%", {class:"padleft"}), + anim.progress = newValue(5, {class:"center"}), + anim.trans = newButton(null,"anim.trans",{icon:'',title:"transparency",class:"padleft"}), + anim.model = newButton(null,"anim.model",{icon:'',title:"show model"}), + anim.shade = newButton(null,"anim.stock",{icon:'',title:"stock box"}), + ]) + }, + + /** Device Browser / Editor */ + + _____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true }), + device: newGroup(LANG.dv_gr_dev, null, {group:"ddev", inline, class:"noshow"}), + + _____: newGroup("workspace", null, {group:"dext", inline}), + bedWidth: newInput('X (width)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:updateDeviceSize}), + bedDepth: newInput('Y (depth)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:updateDeviceSize}), + maxHeight: newInput('Z (height)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:updateDeviceSize}), + resolutionX: newInput(LANG.dv_rezx_s, {title:LANG.dv_rezx_l, convert:toInt, size:6, modes:SLA}), + resolutionY: newInput(LANG.dv_rezy_s, {title:LANG.dv_rezy_l, convert:toInt, size:6, modes:SLA}), + _____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true, modes:NO_WEDM }), + _____: newGroup("firmware", null, {group:"dext", inline, modes:NO_WEDM}), + fwRetract: newBoolean(LANG.dv_retr_s, onBooleanClick, {title:LANG.dv_retr_l, modes:FDM}), + deviceOrigin: newBoolean(LANG.dv_orgc_s, onBooleanClick, {title:LANG.dv_orgc_l, modes:FDM_LZN, show:() => !ui.deviceRound.checked}), + deviceRound: newBoolean(LANG.dv_bedc_s, onBooleanClick, {title:LANG.dv_bedc_l, modes:FDM, trigger, show:isNotBelt}), + deviceBelt: newBoolean(LANG.dv_belt_s, onBooleanClick, {title:LANG.dv_belt_l, modes:FDM, trigger, show:() => !ui.deviceRound.checked}), + separator: newBlank({class:"pop-sep", modes:FDM, driven}), + spindleMax: newInput(LANG.dv_spmx_s, {title:LANG.dv_spmx_l, convert:toInt, size:5, modes:CAM, trigger}), + deviceZMax: newInput(LANG.dv_zmax_s, {title:LANG.dv_zmax_l, convert:toInt, size:5, modes:FDM}), + gcodeTime: newInput(LANG.dv_time_s, {title:LANG.dv_time_l, convert:toFloat, size:5, modes:FDM}), + _____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true, modes:FDM }), + extruder: newGroup(LANG.dv_gr_ext, null, {group:"dext", inline}), + extFilament: newInput(LANG.dv_fila_s, {title:LANG.dv_fila_l, convert:toFloat, modes:FDM}), + extNozzle: newInput(LANG.dv_nozl_s, {title:LANG.dv_nozl_l, convert:toFloat, modes:FDM}), + extOffsetX: newInput(LANG.dv_exox_s, {title:LANG.dv_exox_l, convert:toFloat, modes:FDM}), + extOffsetY: newInput(LANG.dv_exoy_s, {title:LANG.dv_exoy_l, convert:toFloat, modes:FDM}), + extPad: newBlank({class:"grow", modes:FDM}), + separator: newBlank({class:"pop-sep", modes:FDM, driven}), + extActions: newRow([ + ui.extPrev = newButton(undefined, undefined, {icon:''}), + ui.extAdd = newButton(undefined, undefined, {icon:''}), + ui.extDel = newButton(undefined, undefined, {icon:''}), + ui.extNext = newButton(undefined, undefined, {icon:''}) + ], {class:"dev-buttons ext-buttons var-row", modes:FDM}), + _____: newDiv({ class: "f-col t-body t-inset", addto: $('dev-config'), set:true, modes:CAM_LZR }), + _____: newGroup(LANG.dv_gr_out, null, {group:"dgco", inline}), + gcodeStrip: newBoolean(LANG.dv_strc_s, onBooleanClick, {title:LANG.dv_strc_l, modes:CAM}), + gcodeSpace: newBoolean(LANG.dv_tksp_s, onBooleanClick, {title:LANG.dv_tksp_l, modes:CAM_LZR}), + laserMaxPower: newInput(LANG.ou_maxp_s, {title:LANG.ou_maxp_l, modes:LASER, size:7, text:true}), + useLaser: newBoolean(LANG.dv_lazr_s, onBooleanClick, {title:LANG.dv_lazr_l, modes:CAM}), + gcodeFExt: newInput(LANG.dv_fext_s, {title:LANG.dv_fext_l, modes:CAM_LZR, size:7, text:true}), + gcodeEd: newGroup(LANG.dv_gr_gco, $('dg'), {group:"dgcp", inline, modes:GCODE}), + gcodeMacros: newRow([ + (ui.gcodePre = newGCode(LANG.dv_head_s, {title:LANG.dv_head_l, modes:GCODE, area:gcode})).button, + (ui.gcodePost = newGCode(LANG.dv_foot_s, {title:LANG.dv_foot_l, modes:GCODE, area:gcode})).button, + (ui.gcodeLayer = newGCode(LANG.dv_layr_s, {title:LANG.dv_layr_l, modes:FDM, area:gcode})).button, + (ui.gcodeTrack = newGCode(LANG.dv_prog_s, {title:LANG.dv_prog_l, modes:FDM, area:gcode})).button, + (ui.gcodeFan = newGCode(LANG.dv_fanp_s, {title:LANG.dv_fanp_l, modes:FDM, area:gcode})).button, + (ui.gcodeFeature = newGCode(LANG.dv_feat_s, {title:LANG.dv_feat_l, modes:FDM, area:gcode})).button, + (ui.gcodeLaserOn = newGCode(LANG.dv_lzon_s, {title:LANG.dv_lzon_l, modes:LASER, area:gcode})).button, + (ui.gcodeLaserOff = newGCode(LANG.dv_lzof_s, {title:LANG.dv_lzof_l, modes:LASER, area:gcode})).button, + (ui.gcodeWaterOn = newGCode(LANG.dv_waon_s, {title:LANG.dv_waon_l, modes:WJET, area:gcode})).button, + (ui.gcodeWaterOff = newGCode(LANG.dv_waof_s, {title:LANG.dv_waof_l, modes:WJET, area:gcode})).button, + (ui.gcodeKnifeDn = newGCode(LANG.dv_dkon_s, {title:LANG.dv_dkon_l, modes:DRAG, area:gcode})).button, + (ui.gcodeKnifeUp = newGCode(LANG.dv_dkof_s, {title:LANG.dv_dkof_l, modes:DRAG, area:gcode})).button, + (ui.gcodeChange = newGCode(LANG.dv_tool_s, {title:LANG.dv_tool_l, modes:FDM_CAM, area:gcode})).button, + (ui.gcodeDwell = newGCode(LANG.dv_dwll_s, {title:LANG.dv_dwll_l, modes:CAM, area:gcode})).button, + (ui.gcodeSpindle = newGCode(LANG.dv_sspd_s, {title:LANG.dv_sspd_l, modes:CAM, area:gcode, show:() => ui.spindleMax.value > 0})).button + ], {class:"ext-buttons f-row gcode-macros"}), + + /** Preferences Menu */ + + _____: newGroup(LANG.op_menu, $('prefs-gen1'), {inline}), + antiAlias: newBoolean(LANG.op_anta_s, booleanSave, {title:LANG.op_anta_l}), + reverseZoom: newBoolean(LANG.op_invr_s, booleanSave, {title:LANG.op_invr_l}), + ortho: newBoolean(LANG.op_orth_s, booleanSave, {title:LANG.op_orth_l}), + dark: newBoolean(LANG.op_dark_s, booleanSave, {title:LANG.op_dark_l}), + drawer: newBoolean('slide out', booleanSave, {title:'slide out settings drawer'}), + scrolls: newBoolean('scrollbars', booleanSave, {title:'show scrollbars'}), + devel: newBoolean(LANG.op_devl_s, booleanSave, {title:LANG.op_devl_l}), + _____: newGroup(LANG.op_disp, $('prefs-gen2'), {inline}), + showOrigin: newBoolean(LANG.op_shor_s, booleanSave, {title:LANG.op_shor_l}), + showRulers: newBoolean(LANG.op_shru_s, booleanSave, {title:LANG.op_shru_l}), + showSpeeds: newBoolean(LANG.op_sped_s, speedSave, {title:LANG.op_sped_l}), + shiny: newBoolean(LANG.op_shny_s, booleanSave, {title:LANG.op_shny_l, modes:FDM}), + lineType: newSelect(LANG.op_line_s, {title: LANG.op_line_l, action: lineTypeSave, modes:FDM}, "linetype"), + animesh: newSelect(LANG.op_anim_s, {title: LANG.op_anim_l, action: aniMeshSave, modes:CAM}, "animesh"), + units: newSelect(LANG.op_unit_s, {title: LANG.op_unit_l, action: unitsSave, modes:CAM}, "units"), + edgeangle: newInput(LANG.op_spoa_s, {title:LANG.op_spoa_l, convert:toFloat, size:3}), + _____: newGroup(LANG.lo_menu, $('prefs-lay'), {inline}), + autoSave: newBoolean(LANG.op_save_s, booleanSave, {title:LANG.op_save_l}), + autoLayout: newBoolean(LANG.op_auto_s, booleanSave, {title:LANG.op_auto_l}), + freeLayout: newBoolean(LANG.op_free_s, booleanSave, {title:LANG.op_free_l}), + spaceRandoX: newBoolean(LANG.op_spcx_s, booleanSave, {title:LANG.op_spcx_l, show:isBelt}), + spaceLayout: newInput(LANG.op_spcr_s, {title:LANG.op_spcr_l, convert:toFloat, size:3, units}), + _____: newGroup(LANG.xp_menu, $('prefs-xpo'), {inline: true}), + exportLocal: newBoolean(`Grid:Local`, booleanSave, {title:LANG.op_exgl_l}), + exportGhost: newBoolean(`Grid:Host`, booleanSave, {title:LANG.op_exgh_l}), + exportOcto: newBoolean(`OctoPrint`, booleanSave, {title:LANG.op_exop_l}), + exportThumb: newBoolean(`Thumbnail`, booleanSave, {modes:FDM}), + exportPreview: newBoolean(`Code Preview`, booleanSave), + _____: newGroup(LANG.pt_menu, $('prefs-prt'), {inline}), + detail: newSelect(LANG.pt_qual_s, {title: LANG.pt_qual_l, action: detailSave}, "detail"), + healMesh: newBoolean(LANG.pt_heal_s, booleanSave, {title: LANG.pt_heal_l}), + threaded: newBoolean(LANG.pt_thrd_s, booleanSave, {title: LANG.pt_thrd_l, modes:THREED}), + assembly: newBoolean(LANG.pt_assy_s, booleanSave, {title: LANG.pt_assy_l, modes:THREED}), + + prefadd: uc.checkpoint($('prefs-add')), + + /** FDM Settings */ + + _____: newGroup(LANG.sl_menu, $('fdm-layers'), { modes:FDM, driven, hideable, separator, group:"fdm-layers" }), + sliceHeight: newInput(LANG.sl_lahi_s, { title:LANG.sl_lahi_l, convert:toFloat }), + sliceMinHeight: newInput(LANG.ad_minl_s, { title:LANG.ad_minl_l, convert:toFloat, bound:bound(0,3.0), show:() => ui.sliceAdaptive.checked }), + sliceTopLayers: newInput(LANG.sl_ltop_s, { title:LANG.sl_ltop_l, convert:toInt }), + sliceBottomLayers: newInput(LANG.sl_lbot_s, { title:LANG.sl_lbot_l, convert:toInt }), + separator: newBlank({ class:"set-sep", driven }), + sliceAdaptive: newBoolean(LANG.ad_adap_s, onBooleanClick, { title: LANG.ad_adap_l }), + _____: newGroup(LANG.sw_menu, $('fdm-walls'), { modes:FDM, driven, hideable, separator, group:"fdm-walls" }), + sliceShells: newInput(LANG.sl_shel_s, { title:LANG.sl_shel_l, convert:toFloat }), + sliceLineWidth: newInput(LANG.sl_line_s, { title:LANG.sl_line_l, convert:toFloat, bound:bound(0,5) }), + separator: newBlank({ class:"set-sep", driven }), + sliceShellOrder: newSelect(LANG.sl_ordr_s, { title:LANG.sl_ordr_l}, "shell"), + sliceDetectThin: newSelect(LANG.ad_thin_s, { title: LANG.ad_thin_l, action: thinWallSave }, "thin"), + outputAlternating: newBoolean(LANG.ad_altr_s, onBooleanClick, {title:LANG.ad_altr_l}), + sliceZInterleave: newBoolean(LANG.ad_zint_s, onBooleanClick, {title:LANG.ad_zint_l, show:zIntShow}), + _____: newGroup(LANG.fi_menu, $('fdm-fill'), { modes:FDM, driven, hideable, separator, group:"fdm-fill" }), + sliceFillType: newSelect(LANG.fi_type, {trigger}, "infill"), + sliceFillSparse: newInput(LANG.fi_pcnt_s, {title:LANG.fi_pcnt_l, convert:toFloat, bound:bound(0.0,1.0), show:hasInfill}), + sliceFillRepeat: newInput(LANG.fi_rept_s, {title:LANG.fi_rept_l, convert:toInt, bound:bound(1,10), show:fillIsLinear}), + sliceFillOverlap: newInput(LANG.fi_over_s, {title:LANG.fi_over_l, convert:toFloat, bound:bound(0.0,2.0), show:hasInfill}), + separator: newBlank({ class:"set-sep", driven }), + sliceFillRate: newInput(LANG.ou_feed_s, {title:LANG.ou_feed_l, convert:toInt, bound:bound(0,500)}), + sliceSolidRate: newInput(LANG.ou_fini_s, {title:LANG.ou_fini_l, convert:toInt, bound:bound(0,500)}), + separator: newBlank({ class:"set-sep", driven }), + sliceFillGrow: newInput(LANG.fi_grow_s, {title:LANG.fi_grow_l, convert:toFloat}), + sliceFillAngle: newInput(LANG.fi_angl_s, {title:LANG.fi_angl_l, convert:toFloat}), + _____: newGroup(LANG.fh_menu, $('fdm-heat'), { modes:FDM, driven, hideable, separator, group:"fdm-heat" }), + outputTemp: newInput(LANG.ou_nozl_s, {title:LANG.ou_nozl_l, convert:toInt}), + outputBedTemp: newInput(LANG.ou_bedd_s, {title:LANG.ou_bedd_l, convert:toInt}), + _____: newGroup(LANG.fc_menu, $('fdm-cool'), { modes:FDM, driven, hideable, separator, group:"fdm-cool" }), + outputFanLayer: newInput(LANG.ou_fanl_s, { title:LANG.ou_fanl_l, convert:toInt, bound:bound(0,255) }), + outputFanSpeed: newInput(LANG.ou_fans_s, {title:LANG.ou_fans_l, convert:toInt, bound:bound(0,255)}), + _____: newGroup(LANG.sp_menu, $('fdm-support'), { modes:FDM, driven, hideable, separator, group:"fdm-supp" }), + sliceSupportNozzle: newSelect(LANG.sp_nozl_s, {title:LANG.sp_nozl_l, show:isMultiHead}, "extruders"), + sliceSupportDensity: newInput(LANG.sp_dens_s, {title:LANG.sp_dens_l, convert:toFloat, bound:bound(0.0,1.0)}), + sliceSupportSize: newInput(LANG.sp_size_s, {title:LANG.sp_size_l, convert:toFloat, bound:bound(1.0,200.0)}), + sliceSupportOffset: newInput(LANG.sp_offs_s, {title:LANG.sp_offs_l, convert:toFloat, bound:bound(0.0,200.0)}), + sliceSupportGap: newInput(LANG.sp_gaps_s, {title:LANG.sp_gaps_l, convert:toInt, bound:bound(0,5)}), + sliceSupportArea: newInput(LANG.sp_area_s, {title:LANG.sp_area_l, convert:toFloat, bound:bound(0.0,200.0)}), + sliceSupportExtra: newInput(LANG.sp_xpnd_s, {title:LANG.sp_xpnd_l, convert:toFloat, bound:bound(0.0,10.0)}), + sliceSupportGrow: newInput(LANG.sp_grow_s, {title:LANG.sp_grow_l, convert:toFloat, bound:bound(0.0,10.0)}), + sliceSupportAngle: newInput(LANG.sp_angl_s, {title:LANG.sp_angl_l, convert:toFloat, bound:bound(0.0,90.0)}), + sliceSupportSpan: newInput(LANG.sp_span_s, {title:LANG.sp_span_l, convert:toFloat, bound:bound(0.0,200.0), show:() => ui.sliceSupportEnable.checked }), + separator: newBlank({ class:"set-sep", driven }), + sliceSupportEnable: newBoolean(LANG.sp_auto_s, onBooleanClick, {title:LANG.sp_auto_l, show:isNotBelt}), + sliceSupportOutline: newBoolean(LANG.sp_outl_s, onBooleanClick, {title:LANG.sp_outl_l}), + separator: newBlank({ class:"set-sep", driven }), + sliceSupportGen: newRow([ + ui.ssaGen = newButton(LANG.sp_detect, onButtonClick, {class: "f-col grow a-center"}) + ], { modes: FDM, class: "ext-buttons f-row grow" }), + separator: newBlank({ class:"set-sep", driven }), + sliceSupportManual: newRow([ + (ui.ssmAdd = newButton(undefined, onButtonClick, {icon:''})), + (ui.ssmDun = newButton(undefined, onButtonClick, {icon:''})), + (ui.ssmClr = newButton(undefined, onButtonClick, {icon:''})) + ], {class:"ext-buttons f-row"}), + _____: newGroup(LANG.fl_menu, $('fdm-base'), { modes:FDM, driven, hideable, separator, group:"fdm-base" }), + firstSliceHeight: newInput(LANG.fl_lahi_s, {title:LANG.fl_lahi_l, convert:toFloat, show:isNotBelt}), + firstLayerNozzleTemp:newInput(LANG.fl_nozl_s, {title:LANG.fl_nozl_l, convert:toInt, show:isNotBelt}), + firstLayerBedTemp: newInput(LANG.fl_bedd_s, {title:LANG.fl_bedd_l, convert:toInt, show:isNotBelt}), + separator: newBlank({ class:"set-sep", driven }), + firstLayerFanSpeed: newInput(LANG.ou_fans_s, {title:LANG.ou_fans_l, convert:toInt, bound:bound(0,255), show:isBelt}), + firstLayerYOffset: newInput(LANG.fl_zoff_s, {title:LANG.fl_zoff_l, convert:toFloat, show:isBelt}), + firstLayerFlatten: newInput(LANG.fl_flat_s, {title:LANG.fl_flat_l, convert:toFloat, show:isBelt}), + firstLayerRate: newInput(LANG.fl_rate_s, {title:LANG.fl_rate_l, convert:toFloat}), + firstLayerFillRate: newInput(LANG.fl_frat_s, {title:LANG.fl_frat_l, convert:toFloat, show:isNotBelt}), + separator: newBlank({ class:"set-sep", driven, show:isNotBelt }), + firstLayerLineMult: newInput(LANG.fl_sfac_s, {title:LANG.fl_sfac_l, convert:toFloat, bound:bound(0.5,2), show:isNotBelt}), + firstLayerPrintMult: newInput(LANG.fl_mult_s, {title:LANG.fl_mult_l, convert:toFloat}), + separator: newBlank({ class:"set-sep", driven, show:isBelt }), + firstLayerBrim: newInput(LANG.fl_brim_s, {title:LANG.fl_brim_l, convert:toInt, show:isBelt}), + firstLayerBrimIn: newInput(LANG.fl_brin_s, {title:LANG.fl_brin_l, convert:toInt, show:isBelt}), + firstLayerBrimTrig: newInput(LANG.fl_brmn_s, {title:LANG.fl_brmn_l, convert:toInt, show:isBelt}), + firstLayerBrimGap: newInput(LANG.fl_brgp_s, {title:LANG.fl_brgp_l, convert:toFloat, show:isBelt}), + separator: newBlank({ class:"set-sep", driven, show:isBelt }), + firstLayerBeltLead: newInput(LANG.fl_bled_s, {title:LANG.fl_bled_l, convert:toFloat, show:isBelt}), + firstLayerBeltBump: newInput(LANG.fl_blmp_s, {title:LANG.fl_blmp_l, convert:toFloat, bound:bound(0, 10), show:isBelt}), + separator: newBlank({ class:"set-sep", driven, show:isNotBelt }), + outputBrimCount: newInput(LANG.fl_skrt_s, {title:LANG.fl_skrt_l, convert:toInt, show:isNotBelt}), + outputBrimOffset: newInput(LANG.fl_skro_s, {title:LANG.fl_skro_l, convert:toFloat, show:isNotBelt}), + outputRaftSpacing: newInput(LANG.fr_spac_s, {title:LANG.fr_spac_l, convert:toFloat, bound:bound(0.0,3.0), show: () => ui.outputRaft.checked && isNotBelt() }), + separator: newBlank({ class:"set-sep", driven, show:isNotBelt }), + outputRaft: newBoolean(LANG.fr_nabl_s, onBooleanClick, {title:LANG.fr_nabl_l, trigger, show:() => isNotBelt()}), + outputDraftShield: newBoolean(LANG.fr_draf_s, onBooleanClick, {title:LANG.fr_draf_l, trigger, show:() => isNotBelt()}), + _____: newGroup(LANG.ou_menu, $('fdm-output'), { modes:FDM, driven, hideable, separator, group:"fdm-out" }), + outputFeedrate: newInput(LANG.ou_feed_s, {title:LANG.ou_feed_l, convert:toInt}), + outputFinishrate: newInput(LANG.ou_fini_s, {title:LANG.ou_fini_l, convert:toInt}), + outputSeekrate: newInput(LANG.ou_move_s, {title:LANG.ou_move_l, convert:toInt}), + separator: newBlank({ class:"set-sep", driven }), + outputShellMult: newInput(LANG.ou_shml_s, {title:LANG.ou_exml_l, convert:toFloat, bound:bound(0.0,2.0)}), + outputFillMult: newInput(LANG.ou_flml_s, {title:LANG.ou_exml_l, convert:toFloat, bound:bound(0.0,2.0)}), + outputSparseMult: newInput(LANG.ou_spml_s, {title:LANG.ou_exml_l, convert:toFloat, bound:bound(0.0,2.0)}), + separator: newBlank({ class:"set-sep", driven }), + outputRetractDist: newInput(LANG.ad_rdst_s, {title:LANG.ad_rdst_l, convert:toFloat}), + outputRetractSpeed: newInput(LANG.ad_rrat_s, {title:LANG.ad_rrat_l, convert:toInt}), + outputRetractWipe: newInput(LANG.ad_wpln_s, {title:LANG.ad_wpln_l, convert:toFloat, bound:bound(0.0,10)}), + separator: newBlank({ class:"set-sep", driven }), + sliceLayerStart: newSelect(LANG.sl_strt_s, {title:LANG.sl_strt_l}, "start"), + outputLayerRetract: newBoolean(LANG.ad_lret_s, onBooleanClick, {title:LANG.ad_lret_l}), + outputAvoidGaps: newBoolean(LANG.ad_agap_s, onBooleanClick, {title:LANG.ad_agap_l}), + separator: newBlank({ class:"set-sep", driven, show:isBelt }), + outputBeltFirst: newBoolean(LANG.ad_lbir_s, onBooleanClick, {title:LANG.ad_lbir_l, show:isBelt}), + _____: newGroup(LANG.ad_menu, $('fdm-expert'), { modes:FDM, driven, hideable, separator, group:"fdm-xprt" }), + sliceAngle: newInput(LANG.sl_angl_s, {title:LANG.sl_angl_l, convert:toFloat, show:isBelt}), + outputRetractDwell: newInput(LANG.ad_rdwl_s, {title:LANG.ad_rdwl_l, convert:toInt}), + sliceSolidMinArea: newInput(LANG.ad_msol_s, {title:LANG.ad_msol_l, convert:toFloat}), + outputMinSpeed: newInput(LANG.ad_mins_s, {title:LANG.ad_mins_l, convert:toFloat, bound:bound(1,200)}), + outputShortPoly: newInput(LANG.ad_spol_s, {title:LANG.ad_spol_l, convert:toFloat, bound:bound(0,10000)}), + outputCoastDist: newInput(LANG.ad_scst_s, {title:LANG.ad_scst_l, convert:toFloat, bound:bound(0.0,10)}), + zHopDistance: newInput(LANG.ad_zhop_s, {title:LANG.ad_zhop_l, convert:toFloat, bound:bound(0,3.0)}), + arcTolerance: newInput(LANG.ad_arct_s, {title:LANG.ad_arct_l, convert:toFloat, bound:bound(0,1.0), show:() => { return isNotBelt() }}), + antiBacklash: newInput(LANG.ad_abkl_s, {title:LANG.ad_abkl_l, convert:toInt, bound:bound(0,3)}), + outputLoops: newInput(LANG.ag_loop_s, {title:LANG.ag_loop_l, convert:toInt, bound:bound(-1,1000), show:isBelt}), + outputPurgeTower: newInput(LANG.ad_purg_s, {title:LANG.ad_purg_l, convert:toInt, bound:bound(0,1000)}), + + fdmRanges: $('fdm-ranges'), + + /** CAM Settings */ + + _____: newGroup(LANG.ct_menu, $('cam-tabs'), { modes:CAM, marker:true, driven, separator }), + camTabsWidth: newInput(LANG.ct_wdth_s, {title:LANG.ct_wdth_l, convert:toFloat, bound:bound(0.005,100), units}), + camTabsHeight: newInput(LANG.ct_hght_s, {title:LANG.ct_hght_l, convert:toFloat, bound:bound(0.005,100), units}), + camTabsDepth: newInput(LANG.ct_dpth_s, {title:LANG.ct_dpth_l, convert:toFloat, bound:bound(0.005,100), units}), + separator: newBlank({ class:"set-sep", driven }), + camTabsMidline: newBoolean(LANG.ct_midl_s, onBooleanClick, {title:LANG.ct_midl_l}), + separator: newBlank({ class:"set-sep", driven }), + camTabsManual: newRow([ + (ui.tabAdd = newButton(undefined, onButtonClick, {icon:''})), + (ui.tabDun = newButton(undefined, onButtonClick, {icon:''})), + (ui.tabClr = newButton(undefined, onButtonClick, {icon:''})) + ], {class:"ext-buttons f-row"}), + _____: newGroup(LANG.cs_menu, $('cam-stock'), { modes:CAM, driven, separator }), + camStockX: newInput(LANG.cs_wdth_s, {title:LANG.cs_wdth_l, convert:toFloat, bound:bound(0,9999), units}), + camStockY: newInput(LANG.cs_dpth_s, {title:LANG.cs_dpth_l, convert:toFloat, bound:bound(0,9999), units}), + camStockZ: newInput(LANG.cs_hght_s, {title:LANG.cs_hght_l, convert:toFloat, bound:bound(0,9999), units}), + separator: newBlank({ class:"set-sep", driven }), + camStockOffset: newBoolean(LANG.cs_offs_s, onBooleanClick, {title:LANG.cs_offs_l}), + camStockClipTo: newBoolean(LANG.cs_clip_s, onBooleanClick, {title:LANG.cs_clip_l}), + camStockIndexed: newBoolean(LANG.cs_indx_s, onBooleanClick, {title:LANG.cs_indx_l}), + camStockIndexGrid: newBoolean(LANG.cs_ishg_s, onBooleanClick, {title:LANG.cs_ishg_l, show:() => ui.camStockIndexed.checked}), + _____: newGroup(LANG.cc_menu, $('cam-limits'), { modes:CAM, driven, separator }), + camZAnchor: newSelect(LANG.ou_zanc_s, {title: LANG.ou_zanc_l, action:zAnchorSave, show:() => !ui.camStockIndexed.checked}, "zanchor"), + camZOffset: newInput(LANG.ou_ztof_s, {title:LANG.ou_ztof_l, convert:toFloat, units}), + camZTop: newInput(LANG.ou_ztop_s, {title:LANG.ou_ztop_l, convert:toFloat, units, trigger}), + camZBottom: newInput(LANG.ou_zbot_s, {title:LANG.ou_zbot_l, convert:toFloat, units, trigger}), + camZThru: newInput(LANG.ou_ztru_s, {title:LANG.ou_ztru_l, convert:toFloat, bound:bound(0.0,100), units }), + camZClearance: newInput(LANG.ou_zclr_s, {title:LANG.ou_zclr_l, convert:toFloat, bound:bound(0.01,100), units }), + camFastFeedZ: newInput(LANG.cc_rzpd_s, {title:LANG.cc_rzpd_l, convert:toFloat, units}), + camFastFeed: newInput(LANG.cc_rapd_s, {title:LANG.cc_rapd_l, convert:toFloat, units}), + _____: newGroup(LANG.ou_menu, $('cam-output'), { modes:CAM, driven, separator, group:"cam-output" }), + camConventional: newBoolean(LANG.ou_conv_s, onBooleanClick, {title:LANG.ou_conv_l}), + camEaseDown: newBoolean(LANG.cr_ease_s, onBooleanClick, {title:LANG.cr_ease_l}), + camDepthFirst: newBoolean(LANG.ou_depf_s, onBooleanClick, {title:LANG.ou_depf_l}), + camToolInit: newBoolean(LANG.ou_toin_s, onBooleanClick, {title:LANG.ou_toin_l}), + separator: newBlank({ class:"set-sep", driven }), + camFirstZMax: newBoolean(LANG.ou_z1st_s, onBooleanClick, {title:LANG.ou_z1st_l}), + camForceZMax: newBoolean(LANG.ou_forz_s, onBooleanClick, {title:LANG.ou_forz_l}), + separator: newBlank({ class:"set-sep", driven }), + camEaseAngle: newInput(LANG.ou_eang_s, {title:LANG.ou_eang_l, convert:toFloat, bound:bound(0.1,85), show:() => ui.camEaseDown.checked}), + camFullEngage: newInput(LANG.ou_feng_s, {title:LANG.ou_feng_l, convert:toFloat, bound:bound(0.1,1.0)}), + _____: newGroup(LANG.co_menu, $('cam-origin'), { modes:CAM, driven, separator }), + camOriginTop: newBoolean(LANG.or_topp_s, onBooleanClick, {title:LANG.or_topp_l}), + camOriginCenter: newBoolean(LANG.or_cntr_s, onBooleanClick, {title:LANG.or_cntr_l}), + separator: newBlank({ class:"set-sep", driven }), + camOriginOffX: newInput(LANG.co_offx_s, {title:LANG.co_offx_l, convert:toFloat, units}), + camOriginOffY: newInput(LANG.co_offy_s, {title:LANG.co_offy_l, convert:toFloat, units}), + camOriginOffZ: newInput(LANG.co_offz_s, {title:LANG.co_offz_l, convert:toFloat, units}), + _____: newGroup(LANG.op_xprt_s, $('cam-expert'), { group:"cam_expert", modes:CAM, marker: false, driven, separator }), + camExpertFast: newBoolean(LANG.cx_fast_s, onBooleanClick, {title:LANG.cx_fast_l, show: () => !ui.camTrueShadow.checked }), + camTrueShadow: newBoolean(LANG.cx_true_s, onBooleanClick, {title:LANG.cx_true_l, show: () => !ui.camExpertFast.checked }), + separator: newBlank({ class:"set-sep", driven }), + camArcTolerance: newInput(LANG.cx_arct_s, {title:LANG.cx_arct_l, convert:toFloat, units, bound:bound(0,100)}), + camArcResolution: newInput(LANG.cx_arcr_s, {title:LANG.cx_arcr_l, convert:toFloat, bound:bound(0,180), show:() => ui.camArcTolerance.value > 0}), + + /** LASER/DRAG/WJET/WEDM cut tool Settings */ + + _____: newGroup(LANG.sl_menu, $('lzr-slice'), { modes:TWOD, driven, separator }), + ctSliceKerf: newInput(LANG.ls_offs_s, {title:LANG.ls_offs_l, convert:toFloat}), + ctSliceHeight: newInput(LANG.ls_lahi_s, {title:LANG.ls_lahi_l, convert:toFloat, trigger}), + ctSliceHeightMin: newInput(LANG.ls_lahm_s, {title:LANG.ls_lahm_l, convert:toFloat, show:() => ui.ctSliceHeight.value == 0 && !ui.ctSliceSingle.checked }), + separator: newBlank({ class:"set-sep", driven }), + ctSliceSingle: newBoolean(LANG.ls_sngl_s, onBooleanClick, {title:LANG.ls_sngl_l}), + ctOmitInner: newBoolean(LANG.we_omit_s, onBooleanClick, {title:LANG.we_omit_l, modes:WEDM}), + _____: newGroup('surfaces', $('lzr-surface'), { modes:[-1], driven, separator }), + ctSurfaces: newRow([ + (ui.faceAdd = newButton(undefined, onButtonClick, {icon:''})), + (ui.faceDun = newButton(undefined, onButtonClick, {icon:''})), + (ui.faceClr = newButton(undefined, onButtonClick, {icon:''})) + ], {class:"ext-buttons f-row", modes:WEDM}), + _____: newGroup(LANG.dk_menu, $('lzr-knife'), { modes:DRAG, marker:true, driven, separator }), + ctOutKnifeDepth: newInput(LANG.dk_dpth_s, { title:LANG.dk_dpth_l, convert:toFloat, bound:bound(0.0,5.0) }), + ctOutKnifePasses: newInput(LANG.dk_pass_s, { title:LANG.dk_pass_l, convert:toInt, bound:bound(0,5) }), + ctOutKnifeTip: newInput(LANG.dk_offs_s, { title:LANG.dk_offs_l, convert:toFloat, bound:bound(0.0,10.0) }), + _____: newGroup(LANG.lo_menu, $('lzr-layout'), { modes:TWOD, driven, separator }), + ctOutTileSpacing: newInput(LANG.ou_spac_s, { title:LANG.ou_spac_l, convert:toInt }), + ctOutMerged: newBoolean(LANG.ou_mrgd_s, onBooleanClick, {title:LANG.ou_mrgd_l, modes:TWONED, show:() => !ui.ctOutStack.checked }), + ctOutGroup: newBoolean(LANG.ou_grpd_s, onBooleanClick, {title:LANG.ou_grpd_l, show:() => !(ui.ctOutMark.checked || ui.ctOutStack.checked) }), + _____: newGroup(LANG.ou_menu, $('lzr-output'), { modes:TWOD, driven, separator, group:"lzr-output" }), + ctOutPower: newInput(LANG.ou_powr_s, {title:LANG.ou_powr_l, convert:toInt, bound:bound(1,100), modes:TWONED }), + ctOutSpeed: newInput(LANG.ou_sped_s, {title:LANG.ou_sped_l, convert:toInt }), + ctAdaptive: newBoolean('adaptive speed', onBooleanClick, {modes:WEDM, title:'controller determines best cutting speed based on material feedback at runtime'}), + separator: newBlank({ class:"set-sep", driven }), + ctOriginBounds: newBoolean(LANG.or_bnds_s, onBooleanClick, { title:LANG.or_bnds_l, show:() => !ui.ctOriginCenter.checked }), + ctOriginCenter: newBoolean(LANG.or_cntr_s, onBooleanClick, { title:LANG.or_cntr_l, show:() => !ui.ctOriginBounds.checked }), + separator: newBlank({ class:"set-sep", driven, modes:WEDM, show:() => ui.ctOriginBounds.checked }), + ctOriginOffX: newInput(LANG.or_offx_s, { title:LANG.or_offx_l, convert:toFloat, modes:WEDM, show:() => ui.ctOriginBounds.checked }), + ctOriginOffY: newInput(LANG.or_offy_s, { title:LANG.or_offy_l, convert:toFloat, modes:WEDM, show:() => ui.ctOriginBounds.checked }), + separator: newBlank({ class:"set-sep", driven, modes:TWONED }), + ctOutZColor: newBoolean(LANG.ou_layo_s, onBooleanClick, { title:LANG.ou_layo_l, modes:TWONED, show:() => !ui.ctOutMerged.checked }), + ctOutLayer: newBoolean(LANG.ou_layr_s, onBooleanClick, { title:LANG.ou_layr_l, modes:TWONED, show:() => !ui.ctOutStack.checked }), + ctOutMark: newBoolean(LANG.ou_lays_s, onBooleanClick, { title:LANG.ou_lays_l, modes:TWONED, show:() => !ui.ctOutStack.checked }), + separator: newBlank({ class:"set-sep", driven, modes:LASER }), + ctOutInches: newBoolean(LANG.ou_inch_s, onBooleanClick, { title:LANG.ou_inch_l, modes:LASER }), + ctOutStack: newBoolean(LANG.ou_stak_s, onBooleanClick, { title:LANG.ou_stak_l, modes:LASER }), + ctOutShaper: newBoolean(LANG.ou_shap_s, onBooleanClick, { title:LANG.ou_shap_l, modes:LASER, show:() => ui.ctOutStack.checked }), + + /** SLA SETTINGS */ + + slaProc: newGroup(LANG.sa_menu, $('sla-slice'), { modes:SLA, group:"sla-slice", driven, separator }), + slaSlice: newInput(LANG.sa_lahe_s, {title:LANG.sa_lahe_l, convert:toFloat}), + slaShell: newInput(LANG.sa_shel_s, {title:LANG.sa_shel_l, convert:toFloat}), + slaOpenTop: newBoolean(LANG.sa_otop_s, onBooleanClick, {title:LANG.sa_otop_l}), + slaOpenBase: newBoolean(LANG.sa_obas_s, onBooleanClick, {title:LANG.sa_obas_l}), + slaLayers: newGroup(LANG.sa_layr_m, $('sla-layers'), { modes:SLA, group:"sla-layers", driven, separator }), + slaLayerOn: newInput(LANG.sa_lton_s, {title:LANG.sa_lton_l, convert:toFloat}), + slaLayerOff: newInput(LANG.sa_ltof_s, {title:LANG.sa_ltof_l, convert:toFloat}), + slaPeelDist: newInput(LANG.sa_pldi_s, {title:LANG.sa_pldi_l, convert:toFloat}), + slaPeelLiftRate: newInput(LANG.sa_pllr_s, {title:LANG.sa_pllr_l, convert:toFloat}), + slaPeelDropRate: newInput(LANG.sa_pldr_s, {title:LANG.sa_pldr_l, convert:toFloat}), + slaBase: newGroup(LANG.sa_base_m, $('sla-base'), { modes:SLA, group:"sla-base", driven, separator }), + slaBaseLayers: newInput(LANG.sa_balc_s, {title:LANG.sa_balc_l, convert:toInt}), + slaBaseOn: newInput(LANG.sa_lton_s, {title:LANG.sa_bltn_l, convert:toFloat}), + slaBaseOff: newInput(LANG.sa_ltof_s, {title:LANG.sa_bltf_l, convert:toFloat}), + slaBasePeelDist: newInput(LANG.sa_pldi_s, {title:LANG.sa_pldi_l, convert:toFloat}), + slaBasePeelLiftRate: newInput(LANG.sa_pllr_s, {title:LANG.sa_pllr_l, convert:toFloat}), + slaFill: newGroup(LANG.sa_infl_m, $('sla-fill'), { modes:SLA, group:"sla-infill", driven, separator }), + slaFillDensity: newInput(LANG.sa_ifdn_s, {title:LANG.sa_ifdn_l, convert:toFloat, bound:bound(0,1)}), + slaFillLine: newInput(LANG.sa_iflw_s, {title:LANG.sa_iflw_l, convert:toFloat, bound:bound(0,5)}), + slaSupport: newGroup(LANG.sa_supp_m, $('sla-support'), { modes:SLA, group:"sla-support", driven, separator }), + slaSupportLayers: newInput(LANG.sa_slyr_s, {title:LANG.sa_slyr_l, convert:toInt, bound:bound(5,100)}), + slaSupportGap: newInput(LANG.sa_slgp_s, {title:LANG.sa_slgp_l, convert:toInt, bound:bound(3,30)}), + slaSupportDensity: newInput(LANG.sa_sldn_s, {title:LANG.sa_sldn_l, convert:toFloat, bound:bound(0.01,0.9)}), + slaSupportSize: newInput(LANG.sa_slsz_s, {title:LANG.sa_slsz_l, convert:toFloat, bound:bound(0.1,1)}), + slaSupportPoints: newInput(LANG.sa_slpt_s, {title:LANG.sa_slpt_l, convert:toInt, bound:bound(3,10)}), + slaSupportEnable: newBoolean(LANG.enable, onBooleanClick, {title:LANG.sl_slen_l}), + slaOutput: newGroup(LANG.sa_outp_m, $('sla-output'), { modes:SLA, driven, separator, group:"sla-output" }), + slaFirstOffset: newInput(LANG.sa_opzo_s, {title:LANG.sa_opzo_l, convert:toFloat, bound:bound(0,1)}), + slaAntiAlias: newSelect(LANG.sa_opaa_s, {title:LANG.sa_opaa_l}, "antialias"), + + layers: uc.setGroup($("layers")), + + settingsName: $('settingsName'), + settingsSave: $('settingsSave'), + }); + + // run client mode initializations right after UI is bound + initFDM(); + initCAM(); + + // override old style settings two-button menu + ui.settingsSave.onclick = () => { + settingsSave(undefined, ui.settingsName.value); + }; + + function optSelected(sel) { + let opt = sel.options[sel.selectedIndex]; + return opt ? opt.value : undefined; + } + + function hasInfill() { + return optSelected(ui.sliceFillType) !== 'none' + } + + function fillIsLinear() { + return hasInfill() && optSelected(ui.sliceFillType) === 'linear'; + } + + function spindleShow() { + return settings().device.spindleMax > 0; + } + + function zIntShow() { + return settings().controller.devel; + } + + // slider setup + const mobile = space.info.mob; + const slbar = mobile ? 80 : 30; + const slbar2 = slbar * 2; + const slider = ui.sliderRange; + const drag = { }; + + if (mobile) { + ui.slider.classList.add('slider-mobile'); + ui.sliderLo.classList.add('slider-mobile'); + ui.sliderHi.classList.add('slider-mobile'); + // add css style for mobile devices + DOC.body.classList.add('mobile'); + } + + function pxToInt(txt) { + return txt ? parseInt(txt.substring(0,txt.length-2)) : 0; + } + + function sliderUpdate() { + let start = drag.low / drag.maxval; + let end = (drag.low + drag.mid - slbar) / drag.maxval; + api.event.emit('slider.pos', { start, end }); + api.var.layer_lo = Math.round(start * api.var.layer_max); + api.var.layer_hi = Math.round(end * api.var.layer_max); + api.show.layer(); + space.scene.active(); + } + + function dragit(el, delta) { + el.ontouchstart = el.onmousedown = (ev) => { + // el.classList.add('sli-drag-el'); + tracker.style.display = 'block'; + ev.stopPropagation(); + let obj = (ev.touches ? ev.touches[0] : ev); + drag.width = slider.clientWidth; + drag.maxval = drag.width - slbar2; + drag.start = obj.screenX; + drag.loat = drag.low = pxToInt(ui.sliderHold.style.marginLeft); + drag.mdat = drag.mid = ui.sliderMid.clientWidth; + drag.hiat = pxToInt(ui.sliderHold.style.marginRight); + drag.mdmax = drag.width - slbar - drag.loat; + drag.himax = drag.width - slbar - drag.mdat; + let cancel_drag = tracker.ontouchend = tracker.onmouseup = (ev) => { + // el.classList.remove('sli-drag-el'); + if (ev) { + ev.stopPropagation(); + ev.preventDefault(); + } + slider.onmousemove = undefined; + tracker.style.display = 'none'; + }; + el.ontouchend = cancel_drag; + el.ontouchmove = tracker.ontouchmove = tracker.onmousemove = (ev) => { + ev.stopPropagation(); + ev.preventDefault(); + if (ev.buttons === 0) { + return cancel_drag(); + } + if (delta) { + let obj = (ev.touches ? ev.touches[0] : ev); + delta(obj.screenX - drag.start); + } + }; + }; + } + + dragit(ui.sliderLo, (delta) => { + let midval = drag.mdat - delta; + let lowval = drag.loat + delta; + if (midval < slbar || lowval < 0) { + return; + } + ui.sliderHold.style.marginLeft = `${lowval}px`; + ui.sliderMid.style.width = `${midval}px`; + drag.low = lowval; + drag.mid = midval; + sliderUpdate(); + }); + dragit(ui.sliderMid, (delta) => { + let loval = drag.loat + delta; + let hival = drag.hiat - delta; + if (loval < 0 || hival < 0) return; + ui.sliderHold.style.marginLeft = `${loval}px`; + ui.sliderHold.style.marginRight = `${hival}px`; + drag.low = loval; + sliderUpdate(); + }); + dragit(ui.sliderHi, (delta) => { + let midval = drag.mdat + delta; + let hival = drag.hiat - delta; + if (midval < slbar || midval > drag.mdmax || hival < 0) return; + ui.sliderMid.style.width = `${midval}px`; + ui.sliderHold.style.marginRight = `${hival}px`; + drag.mid = midval; + sliderUpdate(); + }); + + ui.load.onchange = function(event) { + api.platform.load_files(event.target.files); + ui.load.value = ''; // reset so you can re-import the same filee + }; + + ui.sliderMin.onclick = () => { + api.show.layer(0,0); + } + + ui.sliderMax.onclick = () => { + api.show.layer(api.var.layer_max,0); + } + + ui.slider.onmouseover = (ev) => { + api.event.emit('slider.label'); + }; + + ui.slider.onmouseleave = (ev) => { + if (!ev.buttons) api.event.emit('slider.unlabel'); + }; + + api.event.on('slider.unlabel', (values) => { + }); + + api.event.on('slider.label', (values) => { + let digits = api.var.layer_max.toString().length; + $('slider-zero').style.width = `${digits}em`; + $('slider-max').style.width = `${digits}em`; + $('slider-zero').innerText = api.var.layer_lo; + $('slider-max').innerText = api.var.layer_hi; + }); + + api.event.on('slider.set', (values) => { + let width = slider.clientWidth; + let maxval = width - slbar2; + let start = Math.max(0, Math.min(1, values.start)); + let end = Math.max(start, Math.min(1, values.end)); + let lowval = start * maxval; + let midval = ((end - start) * maxval) + slbar; + let hival = maxval - end * maxval; + ui.sliderHold.style.marginLeft = `${lowval}px`; + ui.sliderMid.style.width = `${midval}px`; + ui.sliderHold.style.marginRight = `${hival}px`; + }); + + // store layer preferences + api.event.on('stack.show', label => { + let mode = api.mode.get(); + let view = api.view.get(); + api.conf.get().labels[`${mode}-${view}-${label}`] = true; + }); + + api.event.on('stack.hide', label => { + let mode = api.mode.get(); + let view = api.view.get(); + api.conf.get().labels[`${mode}-${view}-${label}`] = false; + }); + + // bind language choices + $('lset-en').onclick = function() { + sdb.setItem('kiri-lang', 'en-us'); + api.space.reload(); + }; + $('lset-da').onclick = function() { + sdb.setItem('kiri-lang', 'da-dk'); + api.space.reload(); + }; + $('lset-de').onclick = function() { + sdb.setItem('kiri-lang', 'de-de'); + api.space.reload(); + }; + $('lset-fr').onclick = function() { + sdb.setItem('kiri-lang', 'fr-fr'); + api.space.reload(); + }; + $('lset-pl').onclick = function() { + sdb.setItem('kiri-lang', 'pl-pl'); + api.space.reload(); + }; + $('lset-pt').onclick = function() { + sdb.setItem('kiri-lang', 'pt-pt'); + api.space.reload(); + }; + $('lset-es').onclick = function() { + sdb.setItem('kiri-lang', 'es-es'); + api.space.reload(); + }; + $('lset-zh').onclick = function() { + sdb.setItem('kiri-lang', 'zh'); + api.space.reload(); + }; + + space.event.addHandlers(self, [ + 'keyup', keyUpHandler, + 'keydown', keyDownHandler, + 'keypress', keyHandler, + 'dragover', dragOverHandler, + 'dragleave', dragLeave, + 'drop', dropHandler + ]); + + function selectionSize(e) { + let dv = parseFloat(e.target.value || 1), + pv = parseFloat(e.target.was || 1), + ra = dv / pv, + xv = parseFloat(ui.sizeX.was ?? ui.scaleX.value) || 1, + yv = parseFloat(ui.sizeY.was ?? ui.scaleY.value) || 1, + zv = parseFloat(ui.sizeZ.was ?? ui.scaleZ.value) || 1, + ta = e.target, + xc = ui.lockX.checked, + yc = ui.lockY.checked, + zc = ui.lockZ.checked, + xt = ta === ui.sizeX, + yt = ta === ui.sizeY, + zt = ta === ui.sizeZ, + tl = (xt && xc) || (yt && yc) || (zt && zc), + xr = ((tl && xc) || (!tl && xt) ? ra : 1), + yr = ((tl && yc) || (!tl && yt) ? ra : 1), + zr = ((tl && zc) || (!tl && zt) ? ra : 1); + // prevent null scale + if (xr * yr * zr === 0) { + return; + } + selection.scale(xr,yr,zr); + ui.sizeX.was = ui.sizeX.value = xv * xr; + ui.sizeY.was = ui.sizeY.value = yv * yr; + ui.sizeZ.was = ui.sizeZ.value = zv * zr; + } + + function selectionScale(e) { + let dv = parseFloat(e.target.value || 1), + pv = parseFloat(e.target.was || 1), + ra = dv / pv, + xv = parseFloat(ui.scaleX.was ?? ui.scaleX.value) || 1, + yv = parseFloat(ui.scaleY.was ?? ui.scaleY.value) || 1, + zv = parseFloat(ui.scaleZ.was ?? ui.scaleY.value) || 1, + ta = e.target, + xc = ui.lockX.checked, + yc = ui.lockY.checked, + zc = ui.lockZ.checked, + xt = ta === ui.scaleX, + yt = ta === ui.scaleY, + zt = ta === ui.scaleZ, + tl = (xt && xc) || (yt && yc) || (zt && zc), + xr = ((tl && xc) || (!tl && xt) ? ra : 1), + yr = ((tl && yc) || (!tl && yt) ? ra : 1), + zr = ((tl && zc) || (!tl && zt) ? ra : 1); + + selection.scale(xr,yr,zr); + ui.scaleX.was = ui.scaleX.value = xv * xr; + ui.scaleY.was = ui.scaleY.value = yv * yr; + ui.scaleZ.was = ui.scaleZ.value = zv * zr; + } + + function selectionRotate(e) { + let val = parseFloat(e.target.value) || 0; + e.target.value = val; + } + + space.event.onEnterKey([ + ui.scaleX, selectionScale, + ui.scaleY, selectionScale, + ui.scaleZ, selectionScale, + ui.sizeX, selectionSize, + ui.sizeY, selectionSize, + ui.sizeZ, selectionSize, + ui.toolName, updateTool, + ui.toolNum, updateTool, + ui.toolFluteDiam, updateTool, + ui.toolFluteLen, updateTool, + ui.toolShaftDiam, updateTool, + ui.toolShaftLen, updateTool, + ui.toolTaperTip, updateTool, + ui.toolTaperAngle, updateTool, + $('rot_x'), selectionRotate, + $('rot_y'), selectionRotate, + $('rot_z'), selectionRotate + ], true); + + $('lab-axis').onclick = () => { + ui.lockX.checked = + ui.lockY.checked = + ui.lockZ.checked = !( + ui.lockX.checked || + ui.lockY.checked || + ui.lockZ.checked + ); + }; + + $('scale-reset').onclick = $('lab-scale').onclick = () => { + selection.scale(1 / ui.scaleX.was, 1 / ui.scaleY.was, 1 / ui.scaleZ.was); + ui.scaleX.value = ui.scaleY.value = ui.scaleZ.value = + ui.scaleX.was = ui.scaleY.was = ui.scaleZ.was = 1; + }; + + $('app-xpnd').onclick = () => { + try { + DOC.body.requestFullscreen(); + } catch (e) { + event.emit('resize'); + moto.space.event.onResize(); + } + }; + + if (!DOC.body.requestFullscreen) { + $('app-xpnd').style.display = 'none'; + } + + uc.onBlur([ + ui.toolName, + ui.toolNum, + ui.toolFluteDiam, + ui.toolFluteLen, + ui.toolShaftDiam, + ui.toolShaftLen, + ui.toolTaperTip, + ], updateTool); + + ui.toolMetric.onclick = updateTool; + ui.toolType.onchange = updateTool; + // default show gcode pre + ui.gcodePre.button.click(); + + function mksvg(src) { + let svg = DOC.createElement('svg'); + svg.innerHTML = src; + return svg; + } + + function mklbl(src) { + let lbl = DOC.createElement('label'); + lbl.innerText = src; + return lbl; + } + + api.platform.update_size(); + + function mouseOnHover(int, event, ints) { + if (!api.feature.hover) return; + if (!int) return api.feature.hovers || api.widgets.meshes(); + api.event.emit('mouse.hover', {int, ints, event, point: int.point, type: 'widget'}); + } + + function platformOnHover(int, event) { + if (!api.feature.hover) return; + if (int) api.event.emit('mouse.hover', {point: int, event, type: 'platform'}); + } + + api.event.on("feature.hover", enable => { + space.mouse.onHover(enable ? mouseOnHover : undefined); + space.platform.onHover(enable ? platformOnHover : undefined); + }); + + // block standard browser context menu + DOC.oncontextmenu = (event) => { + let et = event.target; + if (et.tagName === 'CANVAS' || et.id === 'context-menu' || et.classList.contains('draggable')) { + event.preventDefault(); + event.stopPropagation(); + return false; + } + }; + + space.mouse.downSelect((int, event) => { + if (api.feature.on_mouse_down) { + if (int) { + api.feature.on_mouse_down(int, event); + return; + } + } + if (api.feature.hover) { + if (int) { + return api.event.emit('mouse.hover.down', {int, point: int.point}); + } else { + return selection.meshes(); + } + } + // lay flat with meta or ctrl clicking a selected face + if (int && (event.ctrlKey || event.metaKey || api.feature.on_face_select)) { + let q = new THREE.Quaternion(); + // find intersecting point, look "up" on Z and rotate to face that + q.setFromUnitVectors(int.face.normal, new THREE.Vector3(0,0,-1)); + selection.rotate(q); + } + if (api.view.get() !== VIEWS.ARRANGE) { + // return no selection in modes other than arrange + return null; + } else { + // return selected meshes for further mouse processing + return api.feature.hovers || selection.meshes(); + } + }); + + space.mouse.upSelect((object, event) => { + if (api.feature.on_mouse_up) { + if (event && object) { + return api.feature.on_mouse_up(object, event); + } else { + return api.widgets.meshes(); + } + } + if (event && api.feature.hover) { + api.event.emit('mouse.hover.up', { object, event }); + return; + } + if (event && event.target.nodeName === "CANVAS") { + if (object && object.object) { + if (object.object.widget) { + platform.select(object.object.widget, event.shiftKey, false); + } + } else { + platform.deselect(); + } + } else { + return api.feature.hovers || api.widgets.meshes(); + } + }); + + space.mouse.onDrag(function(delta, offset, up = false) { + if (api.feature.hover) { + return; + } + if (up) { + api.event.emit('mouse.drag.done', offset); + } + if (delta && ui.freeLayout.checked) { + let set = settings(); + let dev = set.device; + let bound = set.bounds_sel; + let width = dev.bedWidth/2; + let depth = dev.bedDepth/2; + let isout = ( + bound.min.x <= -width || + bound.min.y <= -depth || + bound.max.x >= width || + bound.max.y >= depth + ); + if (!isout) { + if (bound.min.x + delta.x <= -width) return; + if (bound.min.y + delta.y <= -depth) return; + if (bound.max.x + delta.x >= width) return; + if (bound.max.y + delta.y >= depth) return; + } + selection.move(delta.x, delta.y, 0); + api.event.emit('selection.drag', delta); + } else { + return selection.meshes().length > 0; + } + }); + + api.space.restore(init_two) || checkSeed(init_two) || init_two(); +}; + +// SECOND STAGE INIT AFTER UI RESTORED +function init_two() { + api.event.emit('init.two'); + + // load script extensions + if (SETUP.s) SETUP.s.forEach(function(lib) { + let scr = DOC.createElement('script'); + scr.setAttribute('defer',true); + scr.setAttribute('src',`/code/${lib}.js?${kiri.version}`); + DOC.body.appendChild(scr); + stats.add('load_'+lib); + api.event.emit('load.lib', lib); + }); + + // load CSS extensions + if (SETUP.ss) SETUP.ss.forEach(function(style) { + style = style.charAt(0) === '/' ? style : `/kiri/style-${style}`; + let ss = DOC.createElement('link'); + ss.setAttribute("type", "text/css"); + ss.setAttribute("rel", "stylesheet"); + ss.setAttribute("href", `${style}.css?${kiri.version}`); + DOC.body.appendChild(ss); + }); + + // override stored settings + if (SETUP.v) SETUP.v.forEach(function(kv) { + kv = kv.split('='); + sdb.setItem(kv[0],kv[1]); + }); + + // import octoprint settings + if (SETUP.ophost) { + let ohost = api.const.OCTO = { + host: SETUP.ophost[0], + apik: SETUP.opkey ? SETUP.opkey[0] : '' + }; + sdb['octo-host'] = ohost.host; + sdb['octo-apik'] = ohost.apik; + console.log({octoprint:ohost}); + } + + // load workspace from url + if (SETUP.wrk) { + set_ctrl.import_url(`${proto}//${SETUP.wrk[0]}`, false); + } + + // load an object from url + if (SETUP.load) { + console.log({load:SETUP}); + api.platform.load_url(`${proto}//${SETUP.load[0]}`); + } + + // bind this to UI so main can call it on settings import + ui.sync = function() { + const current = settings(); + const control = current.controller; + + if (!control.devel) { + // TODO: hide thin type 3 during development + kiri.consts.LISTS.thin.length = 3; + } + + platform.deselect(); + catalog.addFileListener(updateCatalog); + space.view.setZoom(control.reverseZoom, control.zoomSpeed); + space.platform.setGridZOff(undefined); + space.platform.setZOff(0.05); + + // restore UI state from settings + ui.showOrigin.checked = control.showOrigin; + ui.showRulers.checked = control.showRulers; + ui.showSpeeds.checked = control.showSpeeds; + ui.freeLayout.checked = control.freeLayout; + ui.autoLayout.checked = control.autoLayout; + ui.spaceRandoX.checked = control.spaceRandoX; + ui.antiAlias.checked = control.antiAlias; + ui.reverseZoom.checked = control.reverseZoom; + ui.autoSave.checked = control.autoSave; + ui.healMesh.checked = control.healMesh; + ui.threaded.checked = setThreaded(control.threaded); + ui.assembly.checked = control.assembly; + ui.ortho.checked = control.ortho; + ui.devel.checked = control.devel; + lineTypeSave(); + detailSave(); + updateStats(); + + // optional set-and-lock mode (hides mode menu) + let SETMODE = SETUP.mode ? SETUP.mode[0] : null; + + // optional set-and-lock device (hides device menu) + let DEVNAME = SETUP.dev ? SETUP.dev[0] : null; + + // setup default mode and enable mode locking, if set + api.mode.set(SETMODE || STARTMODE || current.mode, SETMODE); + + // fill device list + api.devices.refresh(); + + // update ui fields from settings + api.conf.update_fields(); + + // default to ARRANGE view mode + api.view.set(VIEWS.ARRANGE); + + // add ability to override + api.show.controls(api.feature.controls); + + // update everything dependent on the platform size + platform.update_size(); + + // load wasm if indicated + client.wasm(control.assembly === true); + }; + + ui.sync(); + + // clear alerts as they build up + setInterval(api.event.alerts, 1000); + + // add hide-alerts-on-alert-click + ui.alert.dialog.onclick = function() { + api.event.alerts(true); + }; + + // enable modal hiding + $('mod-x').onclick = api.modal.hide; + + if (!SETUP.s) console.log(`kiri | init main | ${version}`); + + // send init-done event + api.event.emit('init-done', stats); + + // show gdpr if it's never been seen and we're not iframed + const isLocal = LOCAL || WIN.location.host.split(':')[0] === 'localhost'; + if (!sdb.gdpr && WIN.self === WIN.top && !SETUP.debug && !isLocal) { + $('gdpr').style.display = 'flex'; + } + + // warn of degraded functionality when SharedArrayBuffers are missing + if (api.feature.work_alerts && !window.SharedArrayBuffer) { + api.alerts.show("The security context of this", 10); + api.alerts.show("Window blocks important functionality.", 10); + api.alerts.show("Try a Chromium-base Browser instead", 10); + } + + // add keyboard focus handler (must use for iframes) + WIN.addEventListener('load', function () { + WIN.focus(); + DOC.body.addEventListener('click', function() { + WIN.focus(); + },false); + }); + + // dismiss gdpr alert + $('gotit').onclick = () => { + $('gdpr').style.display = 'none'; + sdb.gdpr = Date.now(); + }; + + // bind interface action elements + $('mode-device').onclick = api.show.devices; + $('mode-profile').onclick = settingsLoad; + $('mode-fdm').onclick = () => api.mode.set('FDM'); + $('mode-cam').onclick = () => api.mode.set('CAM'); + $('mode-sla').onclick = () => api.mode.set('SLA'); + $('mode-laser').onclick = () => api.mode.set('LASER'); + $('mode-drag').onclick = () => api.mode.set('DRAG'); + $('mode-wjet').onclick = () => api.mode.set('WJET'); + $('mode-wedm').onclick = () => api.mode.set('WEDM'); + $('set-device').onclick = (ev) => { ev.stopPropagation(); api.show.devices() }; + $('set-profs').onclick = (ev) => { ev.stopPropagation(); api.conf.show() }; + $('set-tools').onclick = (ev) => { ev.stopPropagation(); api.show.tools() }; + $('set-prefs').onclick = (ev) => { ev.stopPropagation(); api.modal.show('prefs') }; + ui.acct.help.onclick = (ev) => { ev.stopPropagation(); api.help.show() }; + ui.acct.don8.onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') }; + ui.acct.mesh.onclick = (ev) => { ev.stopPropagation(); WIN.location = "/mesh" }; + ui.acct.export.onclick = (ev) => { ev.stopPropagation(); profileExport() }; + ui.acct.export.title = LANG.acct_xpo; + $('file-new').onclick = (ev) => { ev.stopPropagation(); workspaceNew() }; + $('file-recent').onclick = () => { api.modal.show('files') }; + $('file-import').onclick = (ev) => { api.event.import(ev); }; + ui.func.slice.onclick = (ev) => { ev.stopPropagation(); api.function.slice() }; + ui.func.preview.onclick = (ev) => { ev.stopPropagation(); api.function.print() }; + ui.func.animate.onclick = (ev) => { ev.stopPropagation(); api.function.animate() }; + ui.func.export.onclick = (ev) => { ev.stopPropagation(); api.function.export() }; + $('view-arrange').onclick = api.platform.layout; + $('view-top').onclick = space.view.top; + $('view-home').onclick = space.view.home; + $('view-front').onclick = space.view.front; + $('view-back').onclick = space.view.back; + $('view-left').onclick = space.view.left; + $('view-right').onclick = space.view.right; + $('unrotate').onclick = () => { + api.widgets.for(w => w.unrotate()); + selection.update_info(); + }; + // attach button handlers to support targets + for (let btn of ["don8pt","don8gh","don8pp"]) { + $(btn).onclick = (ev) => { + window.open(ev.target.children[0].href); + } + } + // rotation buttons + let d = (Math.PI / 180); + $('rot_x_lt').onclick = () => { selection.rotate(-d * $('rot_x').value,0,0) }; + $('rot_x_gt').onclick = () => { selection.rotate( d * $('rot_x').value,0,0) }; + $('rot_y_lt').onclick = () => { selection.rotate(0,-d * $('rot_y').value,0) }; + $('rot_y_gt').onclick = () => { selection.rotate(0, d * $('rot_y').value,0) }; + $('rot_z_lt').onclick = () => { selection.rotate(0,0, d * $('rot_z').value) }; + $('rot_z_gt').onclick = () => { selection.rotate(0,0,-d * $('rot_z').value) }; + // rendering options + $('render-edges').onclick = () => { api.view.edges({ toggle: true }); api.conf.save() }; + $('render-ghost').onclick = () => { api.view.wireframe(false, 0, api.view.is_arrange() ? 0.4 : 0.25); }; + $('render-wire').onclick = () => { api.view.wireframe(true, 0, api.space.is_dark() ? 0.25 : 0.5); }; + $('render-solid').onclick = () => { api.view.wireframe(false, 0, 1); }; + $('mesh-export-stl').onclick = () => { objectsExport('stl') }; + $('mesh-export-obj').onclick = () => { objectsExport('obj') }; + $('mesh-merge').onclick = selection.merge; + $('context-duplicate').onclick = duplicateSelection; + $('context-mirror').onclick = mirrorSelection; + $('context-layflat').onclick = () => { api.event.emit("tool.mesh.lay-flat") }; + $('context-setfocus').onclick = () => { + api.event.emit( + "tool.camera.focus", + ev => api.space.set_focus(undefined, ev.object.point) + ); + }; + + // ui.modal.onclick = api.modal.hide; + ui.modalBox.onclick = (ev) => { ev.stopPropagation() }; + + // add app name hover info + $('app-info').innerText = version; + + // show topline separator when iframed + try { if (WIN.self !== WIN.top) $('top-sep').style.display = 'flex' } catch (e) { console.log(e) } + + // warn users they are running a beta release + if (beta && beta > 0 && sdb.kiri_beta != kiri.beta) { + api.show.alert("CAUTION"); + api.show.alert("this is a development release"); + api.show.alert("and may not function properly"); + sdb.kiri_beta = beta; + } + + // lift curtain + $('curtain').style.display = 'none'; +} + +// update static html elements with language overrides +function lang_rewrite() { + // lk attribute causes inner text to be replaced with lang value + for (let el of [...DOC.querySelectorAll("[lk]")]) { + let key = el.getAttribute('lk'); + let val = LANG[key]; + if (val) { + el.innerText = val; + } else { + console.log({missing_ln: key}); + } + } + // lkt attribute causes a title attribute to be set from lang value + for (let el of [...DOC.querySelectorAll("[lkt]")]) { + let key = el.getAttribute('lkt'); + let val = LANG[key]; + if (val) { + el.setAttribute("title", val); + } else { + console.log({missing_ln: key}); + } + } +} + +// init lang must happen before all other init functions +function init_lang() { + // if a language needs to load, the script is injected and loaded + // first. once this loads, or doesn't, the initialization begins + let lang = SETUP.ln ? SETUP.ln[0] : sdb.getItem('kiri-lang') || api.language.get(); + api.event.emit('init.lang', lang); + // inject language script if not english + if (lang && lang !== 'en' && lang !== 'en-us') { + let map = api.language.map(lang); + let scr = DOC.createElement('script'); + // scr.setAttribute('defer',true); + scr.setAttribute('src',`/kiri/lang/${map}.js?${version}`); + (DOC.body || DOC.head).appendChild(scr); + stats.set('ll',lang); + scr.onload = function() { + api.language.set(map); + lang_rewrite(); + init_one(); + }; + scr.onerror = function(err) { + console.log({language_load_error: err, lang}) + api.language.set(); + lang_rewrite(); + init_one(); + } + } else { + // set to browser default which will be overridden + // by any future script loads (above) + api.language.set(); + lang_rewrite(); + init_one(); + } +} + +export function run() { + init_lang(); +} diff --git a/src2/kiri/lang-en.js b/src2/kiri/lang-en.js new file mode 100644 index 00000000..85179cdd --- /dev/null +++ b/src2/kiri/lang-en.js @@ -0,0 +1,932 @@ +// English language map. Other localization maps will +// defer to English map for any missing key/value pairs +self.lang['en'] = +self.lang['en-us'] = { + // common keys and menus + animate: "animate", // CAM animate button + arrange: "arrange", // layout workspace objects + axis: "axis", // left object scale pop menu + back: "back", + clear: "clear", // clear workspace (remove all objects) + copy: "copy", + clone: "clone", // create duplicate + delete: "delete", + detail: "detail", + done: "done", + edit: "edit", + enable: "enable", + disable: "disable", + donate: "donate", + export: "export", + files: "files", + filter: "filter", + front: "front", + fullscreen: "fullscreen", + help: "help", + ghost: "ghost", // left render pop menu (wireframe) + hide: "hide", // left render pop menu (invisible) + home: "home", + import: "import", + language: "language", + left: "left", + machines: "machines", // device or machine + mesh: "mesh", // mesh editing + metric: "metric", + name: "name", + new: "new", // file new + off: "off", // turn something off (like a lightbulb) + on: "on", // turn something on (like a lightbulb) + profs: "profiles", // right menu "profiles" + prefs: "preferences", // right menu "preferences" + preview: "preview", + recent: "recent", + rename: "rename", + render: "render", // left render pop menu + reset: "reset", + right: "right", + rotate: "rotate", // left rotate pop menu + save: "save", + scale: "scale", // left object scale pop menu + select: "select", + setup: "setup", + settings: "settings", + size: "size", + slice: "slice", + solid: "solid", // view type pop menu + start: "start", + tool: "tool", + tools: "tools", // CAM tool menu button + top: "top", + type: "type", // CAM tool type + version: "version", + view: "view", // left view pop menu + wire: "wire", // left render pop menu + volume: "volume", // device build area + + acct_xpo: ["make a backup of your device","and device profiles with the","option to include workspace","objects and positions"], + + // RIGHT-CLICK CONTEXT MENU + rc_clws: "clear workspace", + rc_xpws: "export workspace", + rc_lafl: "lay flat", + rc_mirr: "mirror", + rc_dupl: "duplicate", + rc_focs: "set focus", + rc_heal: "heal meshes", + rc_swap: "replace vertices", + rc_xobj: "export OBJ", + rc_xstl: "export STL", + sb_info: ["print speed","in mm/s"], + rc_merg: "merge object meshes", + + // DEVICE MENU and related dialogs + dm_sldt: "select a device type", + dm_stdd: "standard devices", + dm_mydd: "my devices", + dm_seld: "selected device", + dm_rcnt: "recent files", + dm_savs: "settings", + dm_appp: "Application Preferences", + + // CAM Tool Dialog + td_tyem: "end", // end mill + td_tybm: "ball", // ball mill + td_tytm: "taper", // taper mill + td_tydr: "drill", // drill + td_tonm: "tool #", + td_shft: "shaft", // endmill shaft specs + td_flut: "flute", // endmill flute specs + td_tapr: "taper", // endmill taper specs + + // DEVICE dialog groups + dv_gr_dev: "device", + dv_gr_ext: "extruder", + dv_gr_out: "output", + dv_gr_gco: "gcode macros", + + // DEVICE dialog (_s = label, _l = hover help) + dv_name_s: "name", + dv_name_l: "device name", + dv_fila_s: "filament", + dv_fila_l: "diameter in millimeters", + dv_nozl_s: "nozzle", + dv_nozl_l: "diameter in millimeters", + dv_bedw_s: "width", + dv_bedw_l: "workspace units", + dv_bedd_s: "depth", + dv_bedd_l: "workspace units", + dv_bedh_s: "height", + dv_bedh_l: ["max build height","in workspace units"], + dv_rezx_s: "resolution X", + dv_rezx_l: ["x resolution"], + dv_rezy_s: "resolution Y", + dv_rezy_l: ["y resolution"], + dv_zmax_s: "Z axis speed limit", + dv_zmax_l: ["maximum Z axis speed","in millimeters / second","0 to disable"], + dv_time_s: "output time factor", + dv_time_l: ["modifier to print time estimates","in the gcode export dialog.","raise or lower value based on your","predicted vs observed print times"], + dv_spmx_s: "max spindle", + dv_spmx_l: ["max spindle rpm speed","0 to disable"], + dv_xtab_s: "absolute positioning", + dv_xtab_l: "extrusion moves absolute", + dv_orgc_s: "origin center", + dv_orgc_l: "bed origin center", + // dv_orgt_s: "origin top", + // dv_orgt_l: "part z origin top", + dv_bedc_s: "circular bed", + dv_bedc_l: "device bed is circular", + dv_belt_s: "belt bed", + dv_belt_l: "continuous printing bed", + dv_fsrc_s: "filament source", + dv_fsrc_l: ["how filament is fed\ninto the extruder"], + dv_retr_s: "firmware retract", + dv_retr_l: ["device fimware supports G10/G11"], + dv_fanp_s: "cooling", + dv_fanp_l: "set cooling fan power", + dv_feat_s: "feature", + dv_feat_l: "inject code for specified features", + dv_prog_s: "progress", + dv_prog_l: "output on each % progress", + dv_layr_s: "layer", + dv_layr_l: ["output at each","layer change"], + dv_tksp_s: "token spacer", + dv_tksp_l: ["add a space between","gcode axis parameters","G0X0Y0X0","vs","G0 X0 Y0 Z0"], + dv_strc_s: "strip comments", + dv_strc_l: ["strip gcode comments","comments begin with ;"], + dv_fext_s: "file extension", + dv_fext_l: "file name extension", + dv_lazr_s: "enable laser", + dv_lazr_l: ["enable laser operations"], + + dv_dwll_s: "dwell", + dv_dwll_l: "gcode dwell script", + dv_tool_s: "tool", + dv_tool_l: "tool change script", + dv_sspd_s: "spindle", + dv_sspd_l: "set spindle speed", + dv_paus_s: "pause", + dv_paus_l: "gcode pause script", + dv_head_s: "header", + dv_head_l: "gcode header script", + dv_foot_s: "footer", + dv_foot_l: "gcode footer script", + dv_pext_s: "exterior", + dv_pext_l: ["gcode script run before","printing exterior lines"], + dv_pint_s: "interior", + dv_pint_l: ["gcode script run before","printing interior lines"], + dv_lzon_s: "laser on", + dv_lzon_l: "gcode laser on script", + dv_lzof_s: "laser off", + dv_lzof_l: "gcode laser off script", + + dv_waon_s: "water on", + dv_waon_l: "gcode water pump on script", + dv_waof_s: "water off", + dv_waof_l: "gcode water pump off script", + + dv_dkon_s: "knife down", + dv_dkon_l: "gcode knife down script", + dv_dkof_s: "knife up", + dv_dkof_l: "gcode knife up script", + + dv_exts_s: "select", + dv_exts_l: "gcode run to enable this extruder", + dv_dext_s: "deselect", + dv_dext_l: "gcode run before enabling another extruder", + dv_extd_s: "deselect", + dv_extd_l: "gcode to deselect this extruder", + dv_exox_s: "offset x", + dv_exox_l: "nozzle offset x", + dv_exoy_s: "offset y", + dv_exoy_l: "nozzle offset y", + + dv_paid_s: "printer id", + dv_paid_l: ["unique printer identifier","may be made up"], + dv_paps_s: "ping spacing", + dv_paps_l: ["minimum filament consumption","between pings in millimeters","recommended 500 - 1000","0 = disable pings"], + dv_pafe_s: "feed length", + dv_pafe_l: ["feeder tube length in millimeters","short=570, medium=800, long=1100"], + dv_papl_s: "push length", + dv_papl_l: ["distance between extruder gear","and nozzle tip in millimeters"], + dv_paof_s: "offset length", + dv_paof_l: ["length to add or remove from","the first segment in millimeters","usually 0"], + dv_pahe_s: "heating time", + dv_pahe_l: ["offset from default"], + dv_paco_s: "cooling time", + dv_paco_l: ["offset from default"], + dv_pacm_s: "compression time", + dv_pacm_l: ["offset from default"], + + // MODE + mo_menu: "mode", + mo_fdmp: "FDM Print", + mo_slap: "SLA Print", + mo_lazr: "Laser Cut", + mo_cncm: "CNC Mill", + + // SETUP + su_menu: "setup", + su_devi: "Devices", + su_tool: "Tools", + su_locl: "Local", + su_xprt: "Export", + su_help: "Help", + + // LOAD + fe_menu: "file", + fn_recn: "Recent", + fn_impo: "Import", + + // FUNCTION + fn_menu: "action", + fn_arra: "Arrange", + fn_slic: "Slice", + fn_prev: "Preview", + fn_expo: "Export", + + // VIEW + vu_menu: "view", + vu_home: "Home", + vu_rset: "Reset", + vu_sptp: "Top", + vu_spfr: "Front", + vu_splt: "Left", + vu_sprt: "Right", + + // WORKSPACE + ws_menu: "view", + ws_save: "Save", + ws_cler: "Clear", + + // OPTIONS + op_menu: "interface", + op_disp: "display", + op_xprt_s: "expert", + op_xprt_l: "show more settings options", + op_decl_s: "show decals", + op_decl_l: "show device decals and logos", + op_shny_s: "detail render", + op_shny_l: "choose a slice render method that is slower and uses specular highlighting. this makes it easier to see detail in some models at the expense of render speed", + op_devl_s: "developer", + op_devl_l: ["show developer options","enable developer debugging"], + op_hopo_s: "hover pop", + op_hopo_l: ["enable menu hover","to activate"], + op_dark_s: "dark mode", + op_dark_l: "dark mode interface", + op_comp_s: "compact ui", + op_comp_l: ["compact user interface","better for small screens","and tablets"], + op_shor_s: "show origin", + op_shor_l: "show device or process origin", + op_shru_s: "show rulers", + op_shru_l: ["show axes rulers","on major gridlines"], + op_sped_s: "show speeds", + op_sped_l: ["show speed to color bar","in preview mode"], + op_auto_s: "auto layout", + op_auto_l: ["automatically layout platform","when new items added"], + op_free_s: "free layout", + op_free_l: ["permit dragable layout","no effect in laser mode"], + op_spcr_s: "spacing", + op_spcr_l: ["spacing between objects","during auto layout","in workspace units","in belt mode, use 0","to auto-space by height"], + op_spoa_s: "edge angle", + op_spoa_l: ["surface angle difference triggering an edge trace"], + op_spcx_s: "random X", + op_spcx_l: ["in belt mode, randomly","layout the part on X","to even belt wear"], + op_orth_s: "orthographic", + op_orth_l: ["orthographic display","requires page refresh"], + op_anta_s: "anti-alias", + op_anta_l: ["smooth display. better but slower","requires a page refresh or reload"], + op_invr_s: "invert zoom", + op_invr_l: ["invert mouse wheel","scroll zoom"], + op_save_s: "auto save", + op_save_l: ["preserve objects in workspace","between application reloads"], + op_line_s: "line type", + op_line_l: ["line style for path rendering","impacts 3d performance","path: 3d best","flat: 2d good","line = 1d fast"], + op_unit_s: "units", + op_unit_l: ["workspace units affects","speeds and distances"], + op_anim_s: "animate", + op_anim_l: ["animation mesh density","higher is more dense","takes more memory","and is slower"], + op_exgl_l: ["show Grid:Local devices in export dialog","includes Grid:Bots and OctoPrint plugins"], + op_exgh_l: ["legacy Grid:Host local network support"], + op_exop_l: ["legacy OctoPrint local network support","Grid:Space OctoPrint plugin recommended"], + + lo_menu: "layout", + + pt_menu: "slicer", + pt_deci_s: "decimate", + pt_deci_l: ["enable or disable point decimation","during port import. for faster slicing","and lower memory usage"], + pt_qual_s: "detail", + pt_qual_l: ["level of detail to retain during slicing operations. lower is faster and uses less memory. large scale models often do no benefit from higher detail. instead it my only result in memory and processing problems. reduce this value if you are getting out of memory errors."], + pt_heal_s: "heal mesh", + pt_heal_l: ["attempt to heal non-manifold meshes during slicing. may increase slicing time. enables a warning when non-manifold geometries are detected."], + pt_thrd_s: "threaded", + pt_thrd_l: ["use parallel processing when browser supports nested workers. may increase memory pressure on larger models. disable if you are getting out of memory errors"], + pt_assy_s: "assembly", + pt_assy_l: ["use web assembly when supported by browser. if you experience artifacts or memory errors, try disabling this."], + + xp_menu: "exports", + + // SETTINGS + se_menu: "profile", + se_load: "load", + se_save: "save", + + // FDM LAYERS + sl_menu: "paths", + sl_angl_s: "slice angle", + sl_angl_l: ["slice angle bias in degrees"], + sl_lahi_s: "layer height", + sl_lahi_l: ["height of each slice","layer in millimeters"], + ad_minl_s: "layer minimum", + ad_minl_l: ["adaptive min layer height","in millimeters","must be non-zero"], + ad_adap_s: "adaptive", + ad_adap_l: ["use adaptive layer heights","with 'layer height' as max","and 'layer min' as the min"], + sl_ltop_s: "top layers", + sl_ltop_l: ["number of solid layers at the top of the print or on flat areas facing up"], + sl_lsld_s: "solid layers", + sl_lsld_l: ["solid fill areas computed","from bridges and flat areas","detected betwen sliced layers"], + sl_lbot_s: "base layers", + sl_lbot_l: ["number of solid layers at bottom of the print or under bridge spans"], + + // FDM WALLS + sw_menu: "shells", + sl_shel_s: "shell count", + sl_shel_l: ["number of perimeter","walls to generate"], + ad_thin_s: "thin walls", + ad_thin_l: ["detect and fill gaps","between shell walls"], + + // FDM SHELLS + sl_line_s: "shell width", + sl_line_l: ["extrusion width in millimeters","0 = nozzle diameter"], + sl_ordr_s: "shell order", + sl_ordr_l: ["output shell order","inside to outside","or outside to inside","affects surface quality"], + sl_strt_s: "layer start", + sl_strt_l: ["layer starting point","last = last layer end","center = part center","origin = device origin"], + + // FDM FILL + fi_menu: "sparse fill", + fi_type: "fill type", + fi_pcnt_s: "fill amount", + fi_pcnt_l: ["fill density values","0.0 - 1.0"], + fi_rept_s: "fill repeat", + fi_rept_l: ["number of times to repeat","unchanged fill layers"], + fi_angl_s: "solid angle", + fi_angl_l: ["starting angle in degrees","90 degrees added to ","each following layer","applies only to solid layers"], + fi_grow_s: "solid expand", + fi_grow_l: ["expand projected solids","can help with solid areas","over sparse infill areas","units in millimeters"], + fi_over_s: "fill overlap", + fi_over_l: ["overlap with shell and other fill","for improved extrusion bonding","as fraction of nozzle diameter","0.0 - 2.0"], + // fi_rate_s: "print speed", + fi_rate_l: ["extrusion speed for infill","set to 0 to use default","output print speeds"], + + // FDM HEATING + fh_menu: "heating", + + // FDM COOLING + fc_menu: "cooling", + + // FDM FIRST LAYER + fl_menu: "base", + fl_lahi_s: "layer height", + fl_lahi_l: ["height of each slice","in millimeters","should be >= slice height"], + fl_rate_s: "shell speed", + fl_rate_l: ["shell printing max speed","in millimeters / second"], + fl_frat_s: "fill speed", + fl_frat_l: ["fill printing max speed","in millimeters / second"], + fl_mult_s: "flow factor", + fl_mult_l: ["extrusion multiplier","0.0 - 2.0"], + fl_sfac_s: "width factor", + fl_sfac_l: ["multiplier on nozzle size","changes line spacing"], + fl_skrt_s: "skirt count", + fl_skrt_l: ["number of first-layer offset","brims to generate"], + fl_skro_s: "skirt offset", + fl_skro_l: ["skirt offset from part","in millimeters","when rafts are used","this is the raft offset"], + fl_nozl_s: "nozzle temp", + fl_nozl_l: ["degrees in celsius","output setting used","when this is zero"], + fl_bedd_s: "bed temp", + fl_bedd_l: ["degrees in celsius","output setting used","when this is zero"], + fr_spac_s: "raft gap", + fr_spac_l: ["additional layer spacing","between 1st layer and raft","in millimeters"], + fr_nabl_s: "raft enable", + fr_nabl_l: ["create a raft under the","model for better adhesion","uses skirt offset and","disables skirt output"], + fr_draf_s: "draft shield", + fr_draf_l: ["extend skirt to height of part","which acts as a shield against","drafts and traps hot air"], + + // FDM BELT ONLY + fl_zoff_s: "belt offset", + fl_zoff_l: ["height offset from belt","of lowest extrusion","in millimeters"], + fl_flat_s: "flatten", + fl_flat_l: ["flatten to the belt any point","height less than this value","in millimeters"], + fl_brim_s: "brim sides", + fl_brim_l: ["add brim to part perimeter","size is width in millimeters","0 to disable"], + fl_brin_s: "brim interior", + fl_brin_l: ["add brim to part interior","when interior voids exist","size is width in millimeters","0 to disable"], + fl_brmn_s: "brim trigger", + fl_brmn_l: ["add brims only when segment","facing belt is shorter than this","value in millimeters","0 = Infinity"], + fl_brco_s: "brim comb", + fl_brco_l: ["add brim gaps","every n lines","0 to disable"], + fl_brgp_s: "brim gap", + fl_brgp_l: ["spacing between brim and part","in millimeters"], + fl_bled_s: "part anchor", + fl_bled_l: ["belt part anchor","at start of print","in millimeters"], + fl_blmp_s: "anchor bump", + fl_blmp_l: ["add a bump to the start of","an anchor to help it peel","off at the end of the belt","in millimeters"], + + // FDM SUPPORT + sp_menu: "support", + sp_detect: "detect", + sp_dens_s: "density", + sp_dens_l: ["fraction 0.0 - 1.0","recommended 0.15","0 to disable"], + sp_size_s: "pillar size", + sp_size_l: ["pillar width in millimeters","smaller values exponentially","increase detection times"], + sp_offs_s: "part offset", + sp_offs_l: ["offset from part","in millimeters"], + sp_gaps_s: "gap layers", + sp_gaps_l: ["number of layers","offset from part"], + sp_span_s: "max span", + sp_span_l: ["unsupported span that causes","a new support to be generated","in millimeters"], + sp_angl_s: "max angle", + sp_angl_l: ["max overhang angle before","supporting pillar is generated","normal values 45-60","belt values 0-10"], + sp_area_s: "min area", + sp_area_l: ["minimum area for","a support column","in millimeters","smaller values will","slow detection"], + sp_xpnd_s: "expand", + sp_xpnd_l: ["expand support area","beyond part boundary","in millimeters"], + sp_grow_s: "grow", + sp_grow_l: ["grow support column to fill small gaps","in millimeters"], + sp_nozl_s: "extruder", + sp_nozl_l: ["in multi-extruder systems","the extruder to use for","support material"], + sp_outl_s: "enclosed", + sp_outl_l: ["enclose support pattern","inside a perimeter shell"], + sp_auto_s: "automatic", + sp_auto_l: ["enable generated supports","using at-slice-time geometry","supports will only appear","after slicing completes"], + + // CNC COMMON terms + cc_tool: "tool", + cc_offs_s: "offset", + cc_offs_l: ["offset tool center","from chosen path"], + cc_spnd_s: "spindle rpm", + cc_spnd_l: ["spindle speed in","revolutions / minute"], + cc_sovr_s: "step over", + cc_sovr_l: ["a fraction of tool diameter between 0 and 1. values greater than 0.49 may result in uncut areas or undesirable artifacts."], + cc_sovc_s: "wide steps", + cc_sovc_l: ["number of wide cutout steps"], + cc_sang_s: "step angle", + cc_sang_l: ["rotation in degrees between lathe axial points"], + cc_sdwn_s: "step down", + cc_sdwn_l: ["step down depth","for each pass","in workspace units","0 to disable"], + cc_thru_s: "cut thru", + cc_thru_l: ["additional cut depth below trace bottom"], + cc_offd_s: "offset", + cc_offd_l: ["overrides default offset distance (which is the tool radius) when set to any value but zero"], + cc_feed_s: "feed rate", + cc_feed_l: ["max cutting speed in","workspace units / minute"], + cc_plng_s: "plunge rate", + cc_plng_l: ["max z axis speed in","workspace units / minute"], + cc_sngl_s: "select lines only", + cc_sngl_l: ["select only single edges","instead of connected polylines"], + cc_sele_s: "select", + cc_sele_l: ["select lines for follow","select loops for clear","select surface for pocket"], + + // CNC COMMON + cc_menu: "limits", + cc_rapd_s: "xy feed", + cc_rapd_l: ["max xy moves feedrate","in workspace units / minute"], + cc_rzpd_s: "z feed", + cc_rzpd_l: ["max z moves feedrate","in workspace units / minute"], + + // CNC LEVELING + cc_loff_s: "z offset", + cc_loff_l: ["distance from stock face","in workspace units"], + cc_lxyo_s: "xy offset", + cc_lxyo_l: ["xy offset from part boundary as a fraction of the tool diameter (0 - 1.0)"], + cc_lsto_s: "stock", + cc_lsto_l: ["use stock area instead of part area"], + + // CNC ROUGHING + cr_menu: "rough", + cr_lsto_s: "leave stock", + cr_lsto_l: ["horizontal offset from vertical faces","stock to leave for finishing pass","in workspace units"], + cr_lstz_s: "leave stock z", + cr_lstz_l: ["vertical offset for roughing passes in workspace units. best for roughing curved surfaces."], + cr_ease_s: "ease down", + cr_ease_l: ["plunge cuts will","spiral down or ease","along a linear path"], + cr_clst_s: "clear stock", + cr_clst_l: ["in indexed mode, clear entire stock area, not just to part periemter"], + cr_clrt_s: "clear top", + cr_clrt_l: ["run a clearing pass over","the bounding area of the part","at z = 0"], + cr_clrp_s: "clear voids", + cr_clrp_l: ["mill out through pockets","instead of just the outline"], + cr_clrf_s: "clear faces", + cr_clrf_l: ["interpolate step down to","clear any detected flat areas"], + cr_olin_s: "inside only", + cr_olin_l: ["limit cutting to","inside part boundaries"], + + // CNC OUTLINE + co_menu: "outline", + co_merg_s: "merge overlap", + co_merg_l: ["merge overlapping lines to prevent overcutting into adjacent solids"], + co_dogb_s: "dogbones", + co_dogb_l: ["insert dogbone cuts","into inside corners"], + co_dogr_s: "reverse bones", + co_dogr_l: ["reverse dogbone direction"], + co_clrt_s: "clear top", + co_clrt_l: ["cut starting at the top of stock","when stock is enabled"], + co_wide_s: "wide cutout", + co_wide_l: ["widen outside cutout paths","for deep cuts in hard material"], + co_olin_s: "inside only", + co_olin_l: ["limit cutting to","inside part boundaries"], + co_olot_s: "outside only", + co_olot_l: ["limit cutting to","exterior part boundaries","which can be thought of","as the shadow outline"], + co_omit_s: "omit through", + co_omit_l: "eliminate thru holes", + co_omvd_s: "omit pocket", + co_omvd_l: "eliminate interior pockets", + co_olen_s: "enable", + co_olen_l: "enabled outline cutting", + + // CNC CONTOUR + cn_menu: "contour", + cf_angl_s: "max angle", + cf_angl_l: ["changes in z that result in angles greater than this are considered vertical and omitted from curves"], + cf_leav_s: "leave stock", + cf_leav_l: ["amount of stock to leave above part surface"], + cf_botm_s: "z bottom", + cf_botm_l: ["obey z bottom limit"], + cf_curv_s: "curves only", + cf_curv_l: ["limit linear cleanup","to curved surfaces"], + cf_olin_s: "inside only", + cf_olin_l: ["limit cutting to","inside part boundaries"], + cf_linx_s: "enable y pass", + cf_linx_l: "linear y-axis finishing", + cf_liny_s: "enable x pass", + cf_liny_l: "linear x-axis finishing", + + // CNC TRACE + cu_menu: "trace", + cu_type_s: "type", + cu_type_l: ["clear = clear a closed-line pocket","follow = tool tip follows line","with offset option when closed"], + + // CNC POCKET + cp_xpnd_s: "expand", + cp_xpnd_l: ["grow (+) or shrink (-) selected areas","contour uses entire selected area","non-contour defaults to tool radius","in workspace units"], + cp_smoo_s: "smooth", + cp_smoo_l: ["clean up jagged edges caused by angled walls. can speedup and improve pocketing for detailed geometries. recommend starting with values around 1 if slicing takes too long or the contours look jagged"], + cp_foll_s: "selection", + cp_foll_l: ["curvature selection limit in degrees. values closer to 0 will restrict selections to flatter surfaces"], + cp_refi_s: "refine", + cp_refi_l: ["number of refining passes to perform on contoured polylines. meant to address Z delta sawtoothing caused by faced geometries whereas smoothing handles XY. values > 10 work well for gently contoured geometries with significant Z movement."], + cp_cont_s: "contour", + cp_cont_l: ["ignore interior voids and features"], + cp_engr_s: "engrave", + cp_engr_l: ["sets up a single pass around the perimeter of the selected area. also useful for 3D laser marking"], + cp_outl_s: "outline only", + cp_outl_l: ["ignore interior voids and features"], + + // CNC DRILLING + cd_menu: "drill", + cd_axis: "axis", + cd_points: "points", + // cd_plpr_s: "plunge per", + // cd_plpr_l: ["max plunge between","dwell periods","in workspace units","0 to disable"], + cd_dwll_s: "dwell time", + cd_dwll_l: ["dwell time","between plunges in","in milliseconds"], + cd_lift_s: "drill lift", + cd_lift_l: ["lift between plunges","after dwell period","in workspace units","0 to disable"], + cd_regi_s: "register", + cd_regi_l: ["drill registration holes","for double-sided parts","independent of enable","drilling but uses same","tool and settings"], + cd_thru_s: "z extend", + cd_thru_l: ["extend downward cut through stock","in workspace units"], + cd_dtru_s: "drill through", + cd_dtru_l: ["additional drill depth below part bottom","overrides global z thru","0 to disable"], + cd_mark_s: "marking", + cd_mark_l: ["only mark holes, do not drill out","the step down setting is used","to determine how deep to mark"], + cd_ftop_s: "from stock top", + cd_ftop_l: ["drill from stock top","instead of part surface"], + cd_prcn_s: "precision", + cd_prcn_l: ["distance between slices","to search for holes","in workspace units","lower is slower and","uses more memory","0 = only flats"], + + cd_seli_l: ["individually select holes","to drill"], + cd_sela_s: "Select Matching Diameter", + cd_sela_l: ["select all holes that", "match selected tool diameter"], + + // CNC FLIP + cf_menu: "flip", + cf_nvrt_s: "z bottom", + cf_nvrt_l: "invert z bottom setting to follow stock", + + // CNC INDEX AXIS + ci_degr_s: "degrees", + ci_degr_l: "degrees to rotate the A (indexed) axis", + ci_abso_s: "absolute", + ci_abso_l: "degree rotation is absolute", + ci_face_s: "face", + ci_face_l: "select face to rotate facing up", + ci_line_s: "linear", + ci_line_l: "make linear passes along X then rotate Y", + + // CNC LASER On/Off Operations (Carvera) + cl_powr_s: "power", + cl_powr_l: "laser power from 0 to 1.0", + cl_adap_s: "adaptive", + cl_adap_l: "use adaptive power based on Z height", + cl_adrp_s: "repeating", + cl_adrp_l: "apply a moduluse of Z height using the given band to make repeating patterns", + cl_flat_s: "flatten", + cl_flat_l: "flatten output to a given Z height", + cl_flaz_s: "flat height", + cl_flaz_l: ["flatten output to this Z height", "in workspace units"], + cl_minp_s: "z min power", + cl_minp_l: ["power setting at the lowest part of the Z band. may be higher or lower than the other end of the Z band."], + cl_maxp_s: "z max power", + cl_maxp_l: ["power setting at the highest part of the Z band. may be higher or lower than the other end of the Z band."], + cl_minz_s: "min z height", + cl_minz_l: ["lowest Z band position"], + cl_maxz_s: "max z height", + cl_maxz_l: ["highest Z band position. when set to 0, uses the highest Z point of the workspace"], + + // CNC CUTOUT TABS + ct_menu: "tabs", + ct_angl_s: "angle", + ct_angl_l: ["starting angle for tab spacing","in degrees (0-360)"], + ct_numb_s: "count", + ct_numb_l: ["number of tabs to use","will be spaced evenly","around the part"], + ct_wdth_s: "width", + ct_wdth_l: "width in workspace units", + ct_hght_s: "height", + ct_hght_l: "height in workspace units", + ct_dpth_s: "depth", + ct_dpth_l: ["distance in workspace units","that the tab projects from","the part surface"], + ct_midl_s: "midline", + ct_midl_l: ["use midline of tab","instead of z bottom","for double-sided work"], + ct_nabl_s: "auto", + ct_nabl_l: ["auto generate radial tabs","projected from part center","using count and angle offset"], + + // OUTPUT + ou_menu: "output", + + // LASER SLICING + ls_offs_s: "offset / kerf", + ls_offs_l: ["distance between the center of the cutting","path and the edge of remaining material","in millimeters"], + ls_lahi_s: "slice height", + ls_lahi_l: ["layer height","in millimeters","0 = auto/detect"], + ls_lahm_s: "slice minimum", + ls_lahm_l: ["layer height minimum","will merge auto slices","under this thickness","in millimeters"], + ls_sngl_s: "single slice", + ls_sngl_l: ["perform only one slice","at specified layer height"], + + // LASER KNIFE + dk_menu: "drag knife", + dk_dpth_s: "cut depth", + dk_dpth_l: ["final cut depth","in millimeters"], + dk_pass_s: "cut passes", + dk_pass_l: ["number of passes","down to cut depth"], + dk_offs_s: "tip offset", + dk_offs_l: ["distance from blade tip","to center of tool","in millimeters"], + + // WIRE EDM + we_omit_s: "omit inner", + we_omit_l: "omit inner polygons", + + // LASER OUTPUT + ou_spac_s: "spacing", + ou_spac_l: ["distance between layer output","in millimeters"], + ou_scal_s: "scaling", + ou_scal_l: "multiplier (0.1 to 100)", + ou_powr_s: "power", + ou_powr_l: ["0 - 100","represents %"], + ou_sped_s: "speed", + ou_sped_l: "millimeters / second", + ou_mrgd_s: "merged", + ou_mrgd_l: ["merge all layers using","color coding to denote","stacking depth"], + ou_grpd_s: "grouped", + ou_grpd_l: ["retain each layer as","a unified grouping","instead of separated","polygons"], + ou_layr_s: "layer order", + ou_layr_l: ["output layer order","from top right to","bottom left"], + ou_layo_s: "layer color", + ou_layo_l: ["output layer colors","for each z index","overridden by merged"], + ou_lays_s: "layer marking", + ou_lays_l: ["mark layers for stacking. the layer above will be output in a different color which the laser can mark with a lower power. turns on layer grouping."], + ou_drkn_s: "drag knife", + ou_drkn_l: ["enable drag knife","output in gcode","cut radii are added","to corners with","cut down passes"], + ou_stak_s: "3d stack", + ou_stak_l: ["output is a 3D stack of 2D paths rather than packing layers flat in 2D"], + ou_maxp_s: "max power", + ou_maxp_l: ["max power value in gcode. power % from settings will scale from 0 to this value"], + ou_inch_s: "imperial", + ou_inch_l: ["output values in inches"], + ou_shap_s: "shaper tool", + ou_shap_l: ["annotate SVG with Shaper Tool custom attributes"], + + // OUTPUT FDM + ou_nozl_s: "nozzle temp", + ou_nozl_l: "degrees in celsius", + ou_bedd_s: "bed temp", + ou_bedd_l: "degrees in celsius", + ou_feed_s: "print speed", + ou_feed_l: ["max printing speed","set to 0 to use default","millimeters / second"], + ou_fini_s: "finish speed", + ou_fini_l: ["outermost shell speed","bridge/flat solid layers","set to 0 to use default","millimeters / second"], + ou_move_s: "move speed", + ou_move_l: ["non-print move speed","millimeters / second","0 = enable G0 moves"], + ou_shml_s: "shell factor", + ou_flml_s: "solid factor", + ou_spml_s: "infill factor", + ou_exml_l: ["extrusion multiplier","0.0 - 2.0"], + ou_fanl_s: "fan on layer", + ou_fanl_l: ["layer # to turn on fan","layers start at 0"], + ou_fans_s: "fan speed", + ou_fans_l: "0 - 255", + + // OUTPUT CAM + ou_toll_s: "precision", + ou_toll_l: ["surface precision","in workspace units","lower is slower and","uses more memory","0 = automatic based","on animate preference"], + ou_flat_s: "flatness", + ou_flat_l: ["delta z below which two adjacent points are considered co-linear. 0 = compute using tolerance"], + ou_redu_s: "reduction", + ou_redu_l: ["number of axis steps to skip for each unit of precision. higher precision may require reduction to produce reasonable file sizes. start at 0 and test in increments of 1."], + ou_brdg_s: "bridging", + ou_brdg_l: ["flat distance between curved","areas to bridge with a cut"], + ou_zanc_s: "z anchor", + ou_zanc_l: ["controls the position of the part","when stock Z exceeds part Z"], + ou_ztof_s: "z offset", + ou_ztof_l: ["offset z anchor","in workspace units"], + ou_ztop_s: "z top", + ou_ztop_l: ["offset from stock bottom","to set start of cutting depth","in workspace units","* drill/contour ignore this *"], + ou_zbot_s: "z bottom", + ou_zbot_l: ["offset from stock bottom","to limit cutting depth","in workspace units"], + ou_zclr_s: "z clearance", + ou_zclr_l: ["safe travel offset","from top of part","in workspace units"], + ou_ztru_s: "z thru", + ou_ztru_l: ["extend cutout pass down","in workspace units"], + ou_conv_s: "conventional", + ou_conv_l: ["milling direction","uncheck for 'climb'"], + ou_depf_s: "depth first", + ou_depf_l: ["optimize pocket cuts","with depth priority"], + ou_toin_s: "tool init", + ou_toin_l: "add tool change with first operation. when disabled, the gcode assumes the first tool is loaded and offset probed", + ou_forz_s: "force z max", + ou_forz_l: "all moves between pockets will go to z max (above stock). this is usually required if you are not performing a roughing operation before pocketing operations.", + ou_z1st_s: "first z max", + ou_z1st_l: "first move in the job will be to Z max (travel distance above stock)", + ou_feng_s: "engage factor", + ou_feng_l: "speed scale (0.1-1) when endmill is fully engaged on a new cut. stepped over cuts when the endmill is less than fully engaged run at a factor of 1 relative to output speed. typically this relates to roughing or any operation with step over.", + ou_eang_s: "ease angle", + ou_eang_l: "descent angle for ease down in degrees", + + // CAM STOCK + cs_menu: "stock", + cs_wdth_s: "width", + cs_wdth_l: ["width (x) in workspace units","0 defaults to part size"], + cs_dpth_s: "depth", + cs_dpth_l: ["depth (y) in workspace units","0 defaults to part size"], + cs_hght_s: "height", + cs_hght_l: ["height (z) in workspace units","0 defaults to part size"], + cs_offs_s: "offset", + cs_offs_l: ["use width, depth, height","as offsets from max","part size on platform"], + cs_clip_s: "clip to", + cs_clip_l: ["rough and outline","clip cutting paths","to defined stock"], + cs_ishg_s: "show grid", + cs_ishg_l: ["show platform grid in indexed mode"], + cs_indx_s: "indexed", + cs_indx_l: ["stock is mounted to a rotatary indexer"], + cs_offe_s: "enable", + cs_offe_l: "enable milling stock", + + // ORIGIN (CAM & LASER) + or_bnds_s: "origin bounds", + or_bnds_l: ["origin is relative to","boundary of all objects"], + or_cntr_s: "origin center", + or_cntr_l: "origin is referenced from the center", + or_topp_s: "origin top", + or_topp_l: "origin is references from the top of objects", + or_offx_s: "origin offset X", + or_offx_l: "origin offset X", + or_offy_s: "origin offset Y", + or_offy_l: "origin offset Y", + + // CAM ORIGIN + co_menu: "origin", + co_offx_s: "offset x", + co_offx_l: ["offset origin along x axis","in workspace units"], + co_offy_s: "offset y", + co_offt_l: ["offset origin along y axis","in workspace units"], + co_offz_s: "offset z", + co_offz_l: ["offset origin along z axis","in workspace units"], + + // FDM ADVANCED + ad_menu: "expert", + ad_rdst_s: "retract dist", + ad_rdst_l: ["amount to retract filament","for long moves. in millimeters"], + ad_rrat_s: "retract rate", + ad_rrat_l: ["speed of filament","retraction in mm/s"], + ad_rdwl_s: "engage dwell", + ad_wpln_s: "retract wipe", + ad_wpln_l: ["non-printing move","after retraction","in millimeters"], + ad_rdwl_l: ["time between re-engaging","filament and movement","in milliseconds"], + ad_scst_s: "shell coast", + ad_scst_l: ["non-printing end","of perimeter shells","in millimeters"], + ad_msol_s: "min solid", + ad_msol_l: ["minimum area (mm^2)","required to keep solid","must be > 0.1"], + ad_mins_s: "min speed", + ad_mins_l: ["minimum speed","for short segments"], + ad_spol_s: "short path", + ad_spol_l: ["polygons shorter than this","will have their print speed","scaled down to min speed","in millimeters"], + ad_arct_s: "arc tolerance", + ad_arct_l: ["convert faceted lines to arcs","center point drift tolerance","when matching arc points","consider values around 0.15","in millimeters","0 to disable"], + ad_zhop_s: "z hop dist", + ad_zhop_l: ["amount to raise z","on retraction moves","in millimeters","0 to disable"], + ad_abkl_s: "anti-backlash", + ad_abkl_l: ["for better flat surface finish","use micro-movements to cancel","backlash in solid layer output","in millimeters","0 to disable","if your firmware has M425","put that in the gcode header","and leave this as 0"], + ad_lbir_s: "belt first", + ad_lbir_l: ["always print shells touching the belt before any other shells. this is no longer a recommended setting based on extensive testing."], + ad_altr_s: "alternating", + ad_altr_l: ["alternate shell winding order","clockwise / counter-clockwise","may reduce warping in thin areas","and help with belt edge adhesion"], + ad_zint_s: "interleave z", + ad_zint_l: ["interleave Z heights with odd number shells"], + ad_lret_s: "layer retract", + ad_lret_l: ["force filament retraction","between layers"], + ad_agap_s: "avoid gaps", + ad_agap_l: ["route around gaps rather","than perform a retract","when possible"], + ad_play_s: "polish layers", + ad_play_l: ["polish up to specified","# of layers at a time"], + ad_pspd_s: "polish speed", + ad_pspd_l: ["polishing speed","in millimeters / second"], + ad_purg_s: "purge tower", + ad_purg_l: ["purge tower area in mm^2","for multi extruder setups","0 = disabled"], + + // CAM EXPERT + cx_fast_s: "skip shadow", + cx_fast_l: ["disable overhang detection","can be faster and use less","memory with complex models","but fails with overhangs","try enabling if slicing","hangs during shadowing"], + cx_true_s: "true shadow", + cx_true_l: ["computationally correct shadow","will be slower but produce","better cuts for complex parts"], + cx_arct_s: "arc tolerance", + cx_arct_l: ["convert cicrular paths to arcs;","center point drift tolerance","when matching arc points","consider values around 0.15","in mm, or 0.006 inches;","High values may cause","unexpected behavior","0 to disable"], + cx_arcr_s: "arc resolution", + cx_arcr_l: ["change in line direction","aceptible in an arc","consider values around 5","High values may cause","unexpected behavior",], + + // FDM GCODE + ag_menu: "gcode", + ag_nozl_s: "nozzle", + ag_nozl_l: "select output nozzle or head", + ag_peel_s: "peel guard", + ag_peel_l: ["starting at this belt z position","periodically roll the print off and","back on to the belt to unstick it","and prevent rolling deflection"], + // ag_paws_s: "pause layers", + // ag_paws_l: ["comma-separated list of layers","to inject pause commands before"], + // ag_loop_s: "loop layers", + // ag_loop_l: ["layer ranges to repeat in the format","first-last-count,first-last-count,...","omitted count = 1"], + ag_loop_s: "loop count", + ag_loop_l: ["number of times to repeat","the selected range or the","entire print if no range"], + + // SLA MENU + sa_menu: "slicing", + sa_lahe_s: "layer height", + sa_lahe_l: ["layer height","in millimeters"], + sa_shel_s: "hollow shell", + sa_shel_l: ["shell thickness in mm","use multiple of layer height","use 0 for solid (disabled)"], + sa_otop_s: "open top", + sa_otop_l: ["if shell is enabled","results in an open top"], + sa_obas_s: "open base", + sa_obas_l: ["if shell is enabled","results in an open base","disabled with supports"], + + sa_layr_m: "layers", + sa_lton_s: "light on time", + sa_lton_l: ["layer light on","time in seconds"], + sa_ltof_s: "light off time", + sa_ltof_l: ["layer light off","time in seconds"], + sa_pldi_s: "peel distance", + sa_pldi_l: ["peel distance","in millimeters"], + sa_pllr_s: "peel lift rate", + sa_pllr_l: ["peel lift speed","in mm/sec"], + sa_pldr_s: "peel drop rate", + sa_pldr_l: ["peel drop speed","in mm/sec"], + + sa_base_m: "base", + sa_balc_s: "layer count", + sa_balc_l: ["number of","base layers"], + sa_bltn_l: ["base layer light on","time in seconds"], + sa_bltf_l: ["base layer light off","time in seconds"], + + sa_infl_m: "infill", + sa_ifdn_s: "density", + sa_ifdn_l: ["percent infill","requires shell","0 = disabled","valid 0.0 - 1.0"], + sa_iflw_s: "line width", + sa_iflw_l: ["hatch line width","in millimeters"], + + sa_supp_m: "support", + sa_slyr_s: "base layers", + sa_slyr_l: ["base support layers","value range 0-10"], + sa_slgp_s: "gap layers", + sa_slgp_l: ["number of layers between","raft and bottom of object"], + sa_sldn_s: "density", + sa_sldn_l: ["used to compute the","number of support pillars","0.0-1.0 (0 = disable)"], + sa_slsz_s: "size", + sa_slsz_l: ["max size of a","support pillar","in millimeters"], + sa_slpt_s: "points", + sa_slpt_l: ["number of points in","each support pillar","in millimeters"], + sl_slen_l: "enable supports", + + sa_outp_m: "output", + sa_opzo_s: "z offset", + sa_opzo_l: ["z layer offset","almost always 0.0","0.0-1.0 in millimeters"], + sa_opaa_s: "anti alias", + sa_opaa_l: ["enable anti-aliasing","produces larger files","can blur details"] +}; diff --git a/src2/kiri/lang.js b/src2/kiri/lang.js new file mode 100644 index 00000000..187f590b --- /dev/null +++ b/src2/kiri/lang.js @@ -0,0 +1,78 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +const LANG = self.lang = { current: {} }; +const KDFL = 'en-us'; + +let lset = navigator.language.toLocaleLowerCase(); + +LANG.map = function(key) { + if (!key) { + return KDFL; + } + let tok = key.split('-'); + switch (tok[0]) { + case 'da': return 'da-dk'; + case 'de': return 'de-de'; + case 'en': return 'en-us'; + case 'es': return 'es-es'; + case 'fr': return 'fr-fr'; + case 'pl': return 'pl-pl'; + case 'pt': return 'pt-pt'; + case 'zh': return 'zh'; + } + return KDFL; +}; + +LANG.get = function() { + return lset; +}; + +LANG.set = function() { + let map, key, keys = [...arguments]; + // provide default if none given + if (keys.length === 0) { + keys = [lset, lset.split('-')[0], 'en']; + } + for (let i=0; i { + if (!map[key]) { + map[key] = LANG.en[key]; + missing.push(key); + } + }); + // update current map from chosen map + Object.keys(map).forEach(key => { + if (LANG.en[key]) { + let val = map[key]; + // turn arrays into newline separated strings + if (Array.isArray(val)) { + val = val.join('\n'); + } + LANG.current[key] = val; + } else { + invalid.push(key); + } + }); + if (missing.length) { + console.log(`language map "${key}" missing keys [${missing.length}]: ${missing.join(', ')}`); + } + if (invalid.length) { + console.log(`language map "${key}" invalid keys [${invalid.length}]: ${invalid.join(', ')}`); + } + return key; + } + return undefined; +} + +export default { LANG }; + +export { LANG }; diff --git a/src2/kiri/layers.js b/src2/kiri/layers.js new file mode 100644 index 00000000..2e427727 --- /dev/null +++ b/src2/kiri/layers.js @@ -0,0 +1,263 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { newPolygon } from '../geo/polygon.js'; +import { polygons as POLY } from '../geo/polygons.js'; + +export class Layers { + constructor() { + this.init(); + } + + init() { + this.layers = {}; + this.profiles = {}; + this.stats = { + contour: 0, + flat_line: 0, + flat_poly: 0, + line_poly: 0, + line: 0 + }; + } + + getLayer(layer) { + return this.layers[layer]; + } + + // in radians + setRotation(x = 0, y = 0, z = 0) { + this.rotation = { x, y, z }; + return this; + } + + setPosition(x = 0, y = 0, z = 0) { + this.position = { x, y, z }; + return this; + } + + setLayer(layer, colors, off) { + let layers = this.layers; + if (typeof(colors) === 'number') { + colors = { + line: colors, + face: colors, + opacity: 1 + }; + } + this.current = layers[layer] = layers[layer] || { + rotation: this.rotation, + position: this.position, + off: off === true, + lines: [], // basic line segments + polys: [], // colors are an attribute on polygons + faces: [], // triangles for areas and flats + norms: undefined, // flats vertex normals + cface: undefined, // flats face color indices + paths: undefined, // 3d extrusion / tube paths (used to be an array, now merged - FIX) + cpath: undefined, // path colors indices + color: colors || { + fat: 0, // render polys & lines with thickness + line: 0, + face: 0, + opacity: 1 + }, + }; + return this; + } + + // add a line segment (two points) + addLine(p1, p2) { + this.current.lines.push(p1, p2); + return this; + } + + // add an array of line segments + addLines(lines, options) { + if (options) { + // the open option encodes lines as open polygons + options.open = true; + const polys = []; + for (let i=0; i p.clean(true)); + } + if (options && options.flat) { + return this.addFlats(polys, options); + } + if (options && !options.thin) { + return this.addPaths(polys, options); + } + polys = flat(polys); + if (options) { + for (let p of polys) { + if (options.z !== undefined) { + p.setZ(options.z); + } + switch (typeof(options.color)) { + case 'number': p.color = options.color; break; + case 'object': p.color = options.color.line; break; + } + } + } + this.current.polys.appendAll(polys); + this.stats.line_poly += polys.length; + return this; + } + + // add an enclosed 3D polygon earcut into faces + // used for FDM solids, bridges, flats debug, CAM hole generation & SLA slice visualization + addAreas(polys, options) { + const faces = this.current.faces; + polys = Array.isArray(polys) ? polys : [ polys ]; + for (let poly of polys) { + for (let ep of poly.earcut()) { + for (let p of ep.points) { + faces.push(p.x, p.y, p.z); + } + } + } + if (options && options.outline) { + this.addPolys(polys.clone(true)); + } + } + + // add a 2D polyline path (usually FDM extrusion paths) + addFlats(polys, options) { + const opts = options || {}; + const offset = opts.offset || 1; + polys = flat(polys); + if (!polys.length) { + return; + } + const cur = this.current, faces = cur.faces; + const norms = []; + for (let poly of polys) { + const faceidx = faces.length / 3; + const path = poly.toPath2D(offset * 0.95); + const { left, right, normals } = path; + for (let p of path.faces) { + faces.push(p.x, p.y, p.z); + } + norms.push(normals); + if (opts.outline) { + let p1 = newPolygon().addPoints(left).setOpenValue(poly.open); + let p2 = newPolygon().addPoints(right).setOpenValue(poly.open); + if (poly.z) { + p1.setZ(poly.z); + p2.setZ(poly.z); + } + this.addPolys(p1); + this.addPolys(p2); + } + const color = opts.color ? + (typeof(opts.color) === 'number' ? { line: opts.color, face: opts.color } : opts.color) : + cur.color; + if (!cur.cface) { + cur.cface = [ Object.assign({ start: faceidx, count: Infinity }, color) ]; + } else { + // rewrite last color count if color or opacity have changed + const pc = cur.cface[cur.cface.length - 1]; + if (pc.face !== color.face || pc.opacity !== color.opacity) { + pc.count = faceidx; + cur.cface.push(Object.assign({ start: faceidx, count: Infinity }, color)); + } + } + } + if (cur.norms) { + cur.norms.appendAll(norms.flat()); + } else { + cur.norms = norms.flat(); + } + return this; + } + + // add 3D polyline path (FDM extrusion paths) + addPaths(polys, options) { + const opts = options || {}; + const height = opts.height || 1; + const offset = opts.offset || 1; + + polys = flat(polys); + if (!polys.length) { + return; + } + + for (let poly of polys) { + if (poly.length < (poly.open ? 2 : 3)) { + continue; + } + const z = opts.z || poly.getZ(); + const { faces, normals } = poly.toPath3D(offset, height, 0); + const cur = this.current; + const one = cur.paths; + // happens with very short lines being omitted + if (!(faces && normals)) { + continue; + } + if (one) { + // for some reason, incremental merge is faster than all at the end + const add = one.faces.length / 3; + const feces = new Float32Array(one.faces.length + faces.length); + const indln = one.faces.length / 3; + feces.set(one.faces); + feces.set(faces, one.faces.length); + one.faces = feces; + // allow changing colors + if (opts.color) { + if (!cur.cpath) { + cur.cpath = [ Object.assign({ start: 0, count: indln - 1 }, cur.color) ]; + } + // rewrite last color count if color or opacity have changed + const pc = cur.cpath[cur.cpath.length - 1]; + if (pc.face !== opts.color.face || pc.opacity !== opts.color.opacity) { + pc.count = indln; + cur.cpath.push(Object.assign({ start: indln, count: Infinity }, opts.color)); + } + } + if (normals) { + one.norms.appendAll(normals); + } + } else { + cur.paths = { faces, z, norms: normals }; + if (opts.color) { + cur.cpath = [ Object.assign({ start: 0, count: Infinity }, opts.color) ]; + } + } + this.stats.contour++; + } + + return this; + } +} + +function flat(polys) { + if (Array.isArray(polys)) { + return POLY.flatten(polys.clone(true), [], true); + } else { + return POLY.flatten([polys.clone(true)], [], true); + } +} diff --git a/src2/kiri/main.js b/src2/kiri/main.js new file mode 100644 index 00000000..1e4d2bca --- /dev/null +++ b/src2/kiri/main.js @@ -0,0 +1,956 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import lang from './lang.js'; +import STACKS from './stacks.js'; + +import { $ } from '../moto/webui.js'; +import { api } from '../kiri/api.js'; +import { broker } from '../moto/broker.js'; +import { consts } from '../kiri/consts.js'; +import { Index } from '../data/index.js'; +import { local as dataLocal } from '../data/local.js'; +import { openFiles } from './files.js'; +import { platform } from './platform.js'; +import { selection } from './selection.js'; +import { settings } from './settings.js'; +import { showDevices } from './devices.js'; +import { space as SPACE } from '../moto/space.js'; +import { stats } from '../kiri/stats.js'; +import { noop, utils } from '../kiri/utils.js'; +import { version } from '../moto/license.js'; +import { Widget } from './widget.js'; + +let { parseOpt, o2js, js2o, ls2o } = utils, + { COLOR, MODES, VIEWS } = consts, + LANG = lang.current, + WIN = self.window, + DOC = self.document, + LOC = self.location, + SETUP = parseOpt(LOC.search.substring(1)), + SECURE = isSecure(LOC.protocol), + LOCAL = self.debug && !SETUP.remote, + EVENT = broker, + SDB = dataLocal, + FILES = openFiles(new Index(SETUP.d ? SETUP.d[0] : 'kiri')), + clone = Object.clone, + MODE = MODES.FDM, + viewMode = VIEWS.ARRANGE, + autoSaveTimer = null; + +// allow widget to straddle client / worker FOR NOW +self.kiri_catalog = FILES; + +// API completion + +export const dialog = { + show: showModal, + hide: hideModal, + update_process_list: updateProcessList +}; + +export const help = { + show: showHelp, + file: showHelpFile +}; + +export const event = { + on(t,l) { return EVENT.on(t,l) }, + emit(t,m,o) { return EVENT.publish(t,m,o) }, + bind(t,m,o) { return EVENT.bind(t,m,o) }, + alerts(clr) { api.alerts.update(clr) }, + import: loadFile, + settings: triggerSettingsEvent +}; + +export const group = { + merge: groupMerge, + split: groupSplit, +}; + +export const hide = { + alert(rec, recs) { api.alerts.hide(...arguments) }, + import: noop, + slider: hideSlider +}; + +export const image = { + dialog: loadImageDialog, + convert: loadImageConvert +}; + +export const modal = { + show: showModal, + hide: hideModal, + visible: modalShowing +}; + +export const mode = { + get_id() { return MODE }, + get_lower: getModeLower, + get: getMode, + set: setMode, + switch: switchMode, + set_expert: noop, + is_fdm() { return MODE === MODES.FDM }, + is_cam() { return MODE === MODES.CAM }, + is_sla() { return MODE === MODES.SLA }, + is_drag() { return MODE === MODES.DRAG }, + is_wedm() { return MODE === MODES.WEDM }, + is_wjet() { return MODE === MODES.WJET }, + is_laser() { return MODE === MODES.LASER }, + is_2d() { return false || + api.mode.is_drag() || + api.mode.is_wedm() || + api.mode.is_wjet() || + api.mode.is_laser() + } +}; + +export const probe = { + live: "https://live.grid.space", + grid: noop, + local: noop +}; + +export const process = { + code: currentProcessCode, + get: currentProcessName +}; + +export const show = { + alert() { return api.alerts.show(...arguments) }, + devices: showDevices, + progress: setProgress, + controls: setControlsVisible, + slices: showSlices, + layer: setVisibleLayer, + local: showLocal, + import: function() { api.ui.import.style.display = '' } +}; + +export const space = { + reload, + auto_save, + restore: restoreWorkspace, + clear: clearWorkspace, + save: saveWorkspace, + set_focus: setFocus, + update: SPACE.update, + is_dark() { return settings.ctrl().dark } +}; + +export const util = { + isSecure, + download: downloadBlob, + ui2rec() { api.conf.update_from(...arguments) }, + rec2ui() { api.conf.update_fields(...arguments) }, + b64enc(obj) { return base64js.fromByteArray(new TextEncoder().encode(JSON.stringify(obj))) }, + b64dec(obj) { return JSON.parse(new TextDecoder().decode(base64js.toByteArray(obj))) } +}; + +export const view = { + get() { return viewMode }, + set() { setViewMode(...arguments) }, + set_arrange() { api.view.set(VIEWS.ARRANGE) }, + set_slice() { api.view.set(VIEWS.SLICE) }, + set_preview() { api.view.set(VIEWS.PREVIEW) }, + set_animate() { api.view.set(VIEWS.ANIMATE) }, + is_arrange() { return viewMode === VIEWS.ARRANGE }, + is_slice() { return viewMode === VIEWS.SLICE }, + is_preview() { return viewMode === VIEWS.PREVIEW }, + is_animate() { return viewMode === VIEWS.ANIMATE }, + update_stack_labels: updateStackLabelState, + update_slider_max: updateSliderMax, + update_slider: updateSlider, + update_speeds: updateSpeeds, + hide_slices: hideSlices, + snapshot: null, + edges: setEdges, + unit_scale: unitScale, + wireframe: setWireframe +}; + +// add show() to catalog for API +FILES.show = showCatalog; + +// patch broker for api backward compatibility +EVENT.on = (topic, listener) => { + EVENT.subscribe(topic, listener); + return EVENT; +}; + +function updateStackLabelState() { + const settings = api.conf.get(); + // match label checkboxes to preference + for (let label of api.stacks.getLabels()) { + let check = `${settings.mode}-${api.view.get()}-${label}`; + api.stacks.setVisible(label, settings.labels[check] !== false); + } +} + +function setFocus(sel, point) { + if (point) { + SPACE.platform.setCenter(point.x, point.z, point.y); + SPACE.view.setFocus(new THREE.Vector3(point.x, point.y, point.z)); + return; + } + if (sel === undefined) { + sel = api.widgets.all(); + } else if (!Array.isArray) { + sel = [ sel ]; + } else if (sel.length === 0) { + sel = api.widgets.all(); + } + let pos = { x:0, y:0, z:0 }; + for (let widget of sel) { + pos.x += widget.track.pos.x; + pos.y += widget.track.pos.y; + pos.z += widget.track.pos.z; + } + if (sel.length) { + pos.x /= sel.length; + pos.y /= sel.length; + pos.z /= sel.length; + } + let cam_index = api.conf.get().process.camStockIndexed || false; + let focus_z = cam_index ? 0 : platform.top_z() / 2; + SPACE.platform.setCenter(pos.x, -pos.y, focus_z); + SPACE.view.setFocus(new THREE.Vector3(pos.x, focus_z, -pos.y)); +} + +function reload() { + api.event.emit('reload'); + do_reload(100); +} + +function do_reload(time) { + // allow time for async saves to complete and busy to to to zero + setTimeout(() => { + if (busy === 0) { + LOC.reload(); + } else { + console.log(`reload deferred on busy=${busy}`); + do_reload(250); + } + }, time || 100); +} + +function auto_save() { + if (!settings.ctrl().autoSave) { + return; + } + clearTimeout(autoSaveTimer); + autoSaveTimer = setTimeout(() => { + api.space.save(true); + }, 1000); +} + +// update version and init count +let inits = parseInt(SDB.getItem('kiri-init') || stats.get('init') || 0) + 1; +SDB.setItem('kiri-init', inits); +stats.set('init', inits); +stats.set('kiri', version); + +/** ****************************************************************** + * Utility Functions + ******************************************************************* */ + +function unitScale() { + return api.mode.is_cam() && settings.ctrl().units === 'in' ? 25.4 : 1; +} + +function triggerSettingsEvent() { + api.event.emit('settings', settings.get()); +} + +function isSecure(proto) { + return proto.toLowerCase().indexOf("https") === 0; +} + +function setProgress(value = 0, msg) { + value = (value * 100).round(4); + api.ui.progress.width = value+'%'; + if (self.debug) { + // console.log(msg, value.round(2)); + api.ui.prostatus.style.display = 'flex'; + if (msg) { + api.ui.prostatus.innerHTML = msg; + } else { + api.ui.prostatus.innerHTML = ''; + } + } +} + +function bound(v,min,max) { + return Math.max(min,Math.min(max,v)); +} + +function updateSpeeds(maxSpeed, minSpeed) { + const { ui } = api; + ui.speeds.style.display = + maxSpeed && + settings.mode !== 'SLA' && + settings.mode !== 'LASER' && + viewMode === VIEWS.PREVIEW && + ui.showSpeeds.checked ? 'block' : ''; + if (maxSpeed) { + const colors = []; + for (let i = 0; i <= maxSpeed; i += maxSpeed / 20) { + colors.push(Math.round(Math.max(i, 1))); + } + api.client.colors(colors, maxSpeed, speedColors => { + const list = []; + Object.keys(speedColors).map(v => parseInt(v)).sort((a, b) => b - a).forEach(speed => { + const color = speedColors[speed]; + const hex = color.toString(16).padStart(6, 0); + const r = (color >> 16) & 0xff; + const g = (color >> 8) & 0xff; + const b = (color >> 0) & 0xff; + const style = `background-color:#${hex}`; + list.push(``); + }); + ui.speedbar.innerHTML = list.join(''); + }); + api.event.emit('preview.speeds', { + min: minSpeed, + max: maxSpeed + }); + } +} + +function updateSlider() { + api.event.emit("slider.set", { + start: (api.var.layer_lo / api.var.layer_max), + end: (api.var.layer_hi / api.var.layer_max) + }); + api.conf.update_fields_from_range(); +} + +function setVisibleLayer(h, l) { + h = h >= 0 ? h : api.var.layer_hi; + l = l >= 0 ? l : api.var.layer_lo; + api.var.layer_hi = bound(h, 0, api.var.layer_max); + api.var.layer_lo = bound(l, 0, h); + api.event.emit("slider.label"); + updateSlider(); + showSlices(); +} + +function setWireframe(bool, color, opacity) { + api.widgets.each(function(w) { w.setWireframe(bool, color, opacity) }); + SPACE.update(); +} + +function setEdges(bool) { + if (bool && bool.toggle) { + api.local.toggle('model.edges'); + } else { + api.local.set('model.edges', bool); + } + bool = api.local.getBoolean('model.edges'); + api.widgets.each(w => w.setEdges(bool)); + SPACE.update(); +} + +function updateSliderMax(set) { + let max = STACKS.getRange().tallest - 1; + api.var.layer_max = api.ui.sliderMax.innerText = max; + if (set || max < api.var.layer_hi) { + api.var.layer_hi = api.var.layer_max; + api.event.emit("slider.label"); + updateSlider(); + } +} + +function hideSlices() { + STACKS.clear(); + api.widgets.opacity(COLOR.model_opacity); + api.widgets.each(function(widget) { + widget.setWireframe(false); + }); +} + +function setWidgetVisibility(bool) { + api.widgets.each(w => { + if (bool) { + w.show(); + } else { + w.hide(); + } + }); +} + +/** + * hide or show slice-layers and their sub-elements + * + * @param {number} [layer] + */ +function showSlices(layer) { + if (viewMode === VIEWS.ARRANGE) { + return; + } + + showSlider(); + + if (typeof(layer) === 'string' || typeof(layer) === 'number') { + layer = parseInt(layer); + } else { + layer = api.var.layer_hi; + } + + layer = bound(layer, 0, api.var.layer_max); + if (layer < api.var.layer_lo) api.var.layer_lo = layer; + api.var.layer_hi = layer; + api.event.emit("slider.label"); + + updateSlider(); + STACKS.setRange(api.var.layer_lo, api.var.layer_hi); + + SPACE.update(); +} + +function showSlider() { + api.ui.layers.style.display = 'flex'; + api.ui.slider.style.display = 'flex'; +} + +function hideSlider() { + api.ui.layers.style.display = 'none'; + api.ui.slider.style.display = 'none'; + api.ui.speeds.style.display = 'none'; +} + +function loadImageDialog(image, name, force) { + if (!force && image.byteLength > 2500000) { + return api.uc.confirm("Large images may fail to import
Consider resizing under 1000 x 1000
Proceed with import?").then(ok => { + if (ok) { + loadImageDialog(image, name, true); + } + }); + } + const opt = {pre: [ + "
", + "

Image Conversion

", + "

", + " This will create a 3D model from a 2D PNG image. Photos must", + " be blurred to be usable. Values from 0=off to 50=high are suggested.", + " Higher values incur more processing time.", + "

", + "
", + " ", + " ", + " ", + " ", + " ", + " ", + "
blur value invert image
base size invert alpha
border size
", + "
" + ]}; + api.uc.confirm(undefined, {convert:true, cancel:false}, undefined, opt).then((ok) => { + if (ok) { + loadImage(image, { + file: name, + blur: parseInt($('png-blur').value) || 0, + base: parseInt($('png-base').value) || 0, + border: parseInt($('png-border').value) || 0, + inv_image: $('png-inv').checked, + inv_alpha: $('alpha-inv').checked + }); + } + }); +} + +function loadImage(image, opt = {}) { + const info = Object.assign({settings: settings.get(), png:image}, opt); + api.client.image2mesh(info, progress => { + api.show.progress(progress, "converting"); + }, vertices => { + api.show.progress(0); + const widget = newWidget().loadVertices(vertices); + widget.meta.file = opt.file; + platform.add(widget); + }); +} + +/** ****************************************************************** + * Selection Functions + ******************************************************************* */ + +function groupMerge() { + Widget.Groups.merge(selection.widgets(true)); +} + +function groupSplit() { + Widget.Groups.split(selection.widgets(false)); +} + +/** ****************************************************************** + * Settings Functions + ******************************************************************* */ + +// convert any image type to png +function loadImageConvert(res, name) { + let url = URL.createObjectURL(new Blob([res])); + + $('mod-any').innerHTML = ``; + + let img = $('xsrc'); + let can = $('xdst'); + + img.onload = () => { + can.width = img.width; + can.height = img.height; + let ctx = can.getContext('2d'); + ctx.drawImage(img, 0, 0); + fetch(can.toDataURL()).then(r => r.arrayBuffer()).then(data => { + loadImageDialog(data, name); + }); + }; +} + +function loadFile(ev) { + // use modern Filesystem api when available + if (false && window.showOpenFilePicker) { + window.showOpenFilePicker().then(files => { + return Promise.all(files.map(fh => fh.getFile())) + }).then(files => { + if (files.length) { + api.platform.load_files(files); + } + }).catch(e => { /* ignore cancel */ }); + return; + } + api.ui.load.click(); +} + +function saveWorkspace(quiet) { + api.conf.save(); + const newWidgets = []; + const oldWidgets = js2o(SDB.getItem('ws-widgets'), []); + api.widgets.each(function(widget) { + if (widget.synth) return; + newWidgets.push(widget.id); + oldWidgets.remove(widget.id); + widget.saveState(); + let ann = api.widgets.annotate(widget.id); + ann.file = widget.meta.file; + ann.url = widget.meta.url; + }); + SDB.setItem('ws-widgets', o2js(newWidgets)); + oldWidgets.forEach(wid => { + Widget.deleteFromState(wid); + }); + // eliminate dangling saved widgets + FILES.deleteFilter(key => newWidgets.indexOf(key.substring(8)) < 0, "ws-save-", "ws-savf"); + if (!quiet) { + api.show.alert("workspace saved", 1); + } +} + +function restoreWorkspace(ondone, skip_widget_load) { + let newset = api.conf.restore(false), + camera = newset.controller.view, + toload = ls2o('ws-widgets',[]), + loaded = 0, + position = true; + + api.conf.update_fields(); + platform.update_size(); + + SPACE.view.reset(); + if (camera) { + SPACE.view.load(camera); + } else { + SPACE.view.home(); + } + + if (skip_widget_load) { + if (ondone) { + ondone(); + } + return; + } + + // remove any widgets from platform + api.widgets.each(function(widget) { + platform.delete(widget); + }); + + // load any widget by name that was saved to the workspace + toload.forEach(function(widgetid) { + Widget.loadFromState(widgetid, function(widget) { + if (widget) { + platform.add(widget, 0, position, true); + let ann = api.widgets.annotate(widgetid); + widget.meta.file = ann.file; + widget.meta.url = ann.url; + } + if (++loaded === toload.length) { + platform.deselect(); + if (ondone) { + ondone(); + setTimeout(() => { + platform.update_bounds(); + SPACE.update(); + }, 1); + } + } + }, position); + }); + + return toload.length > 0; +} + +function clearWorkspace() { + // free up worker cache/mem + api.client.clear(); + platform.select_all(); + platform.delete(selection.meshes()); +} + +function modalShowing() { + return api.ui.modal.style.display === 'flex'; +} + +function showModal(which) { + let mod = api.ui.modal, + style = mod.style, + visible = modalShowing(), + info = { pct: 0 }; + + // hide all modals befroe showing another + Object.keys(api.ui.modals).forEach(name => { + api.ui.modals[name].style.display = name === which ? 'flex' : ''; + }); + + function ondone() { + api.event.emit('modal.show', which); + } + + if (visible) { + return ondone(); + } + + style.height = '0'; + style.display = 'flex'; + + new TWEEN.Tween(info). + easing(TWEEN.Easing.Quadratic.InOut). + to({ pct: 100 }, 100). + onUpdate(() => { style.height = `${info.pct}%` }). + onComplete(ondone). + start(); +} + +function hideModal() { + if (!modalShowing()) { + return; + } + let mod = api.ui.modal, style = mod.style, info={pct:100}; + new TWEEN.Tween(info). + easing(TWEEN.Easing.Quadratic.InOut). + to({pct:0}, 100). + onUpdate(() => { style.height = `${info.pct}%` }). + onComplete(() => { + style.display = ''; + api.event.emit('modal.hide'); + }). + start(); +} + +function showCatalog() { + showModal("files"); +} + +function editSettings(e) { + let current = settings.get(), + mode = getMode(), + name = e.target.getAttribute("name"), + load = current.sproc[mode][name], + loadstr = JSON.stringify(load,null,4).split('\n'); + api.uc.prompt(`settings for "${name}"`, loadstr).then(edit => { + if (edit) { + try { + current.sproc[mode][name] = JSON.parse(edit); + if (name === settings.proc().processName) { + api.conf.load(null, name); + } + api.conf.save(); + api.settings.sync.put(); + } catch (e) { + console.log({ malformed_settings: e }); + api.uc.alert('malformed settings object'); + } + } + }); +} + +function exportSettings(e) { + let current = settings.get(), + mode = getMode(), + name = e.target.getAttribute("name"), + data = api.util.b64enc({ + process: current.sproc[mode][name], + version, + moto: moto.id, + time: Date.now(), + mode, + name + }); + api.uc.prompt("Export Process Filename", name).then(name => { + if (name) { + api.util.download(data, `${name}.km`); + } + }); +} + +function deleteSettings(e) { + let current = settings.get(); + let name = e.target.getAttribute("del"); + delete current.sproc[getMode()][name]; + api.settings.sync.put(); + updateProcessList(); + api.conf.save(); + triggerSettingsEvent(); +} + +function updateProcessList() { + let current = settings.get(); + let list = [], s = current, sp = s.sproc[getMode()] || {}, table = api.ui.settingsList; + table.innerHTML = ''; + for (let k in sp) { + if (sp.hasOwnProperty(k)) list.push(k); + } + list.filter(n => n !=='default').sort().forEach(function(sk) { + let row = DOC.createElement('div'), + load = DOC.createElement('button'), + edit = DOC.createElement('button'), + xprt = DOC.createElement('button'), + del = DOC.createElement('button'), + name = sk; + + load.setAttribute('load', sk); + load.onclick = (ev) => { + api.conf.load(undefined, sk); + updateProcessList(); + hideModal(); + } + load.appendChild(DOC.createTextNode(sk)); + if (sk == settings.proc().processName) { + load.setAttribute('class', 'selected') + } + api.ui.settingsName.value = settings.proc().processName; + + del.setAttribute('del', sk); + del.setAttribute('title', "remove '"+sk+"'"); + del.innerHTML = ''; + del.onclick = deleteSettings; + + edit.setAttribute('name', sk); + edit.setAttribute('title', 'edit'); + edit.innerHTML = ''; + edit.onclick = editSettings; + + xprt.setAttribute('name', sk); + xprt.setAttribute('title', 'export'); + xprt.innerHTML = ''; + xprt.onclick = exportSettings; + + row.setAttribute("class", "flow-row"); + row.appendChild(edit); + row.appendChild(load); + row.appendChild(xprt); + row.appendChild(del); + table.appendChild(row); + }); +} + +function showHelp() { + showHelpFile(`local`,() => {}); +} + +function showHelpFile(local,then) { + if (!local) { + WIN.open("//docs.grid.space/", "_help"); + return; + } + $('kiri-version').innerHTML = `${LANG.version} ${version}`; + showModal('help'); + api.event.emit('help.show', local); +} + +function showLocal() { + showModal('local'); + api.probe.local((err,data) => { + let devc = 0; + let bind = []; + let html = ['']; + html.push(``); + html.push(``); + let recs = []; + for (let k in data) { + recs.push(data[k].stat); + } + recs.sort((a,b) => { + return a.device.name < b.device.name ? -1 : 1; + }); + for (let r of recs) { + bind.push({uuid: r.device.uuid, host: r.device.addr[0], port: r.device.port}); + html.push(``); + html.push(``); + html.push(``); + html.push(``); + html.push(``); + html.push(``); + devc++; + } + html.push(``); + html.push(`
devicetypestatus
${r.device.name}${r.device.mode}${r.state}
`); + if (devc) { + $('mod-local').innerHTML = html.join(''); + } else { + $('mod-local').innerHTML = `
no local devices`; + } + bind.forEach(rec => { + $(rec.uuid).onclick = () => { + window.open(`http://${rec.host}:${rec.port||4080}/`); + }; + }); + }); +} + +function setViewMode(mode) { + const isCAM = settings.mode() === 'CAM'; + viewMode = mode; + platform.deselect(); + selection.update_info(); + // clear any bounds forced by, for example, WireEDM previews + api.platform.set_bounds(); + // disable clear in non-arrange modes + ['view-arrange','act-slice','act-preview','act-animate'].forEach(el => { + $(el).classList.remove('selected') + }); + $('render-tools').classList.add('hide'); + switch (mode) { + case VIEWS.ARRANGE: + $('view-arrange').classList.add('selected'); + $('render-tools').classList.remove('hide'); + api.function.clear_progress(); + api.client.clear(); + STACKS.clear(); + hideSlider(); + updateSpeeds(); + setVisibleLayer(); + setWidgetVisibility(true); + api.widgets.opacity(1); + api.view.edges(api.local.getBoolean('model.edges')); + break; + case VIEWS.SLICE: + $('act-slice').classList.add('selected'); + updateSpeeds(); + updateSliderMax(); + setWidgetVisibility(true); + !isCAM && api.view.edges(false); + break; + case VIEWS.PREVIEW: + $('act-preview').classList.add('selected'); + setWidgetVisibility(true); + !isCAM && api.view.edges(false); + break; + case VIEWS.ANIMATE: + $('act-animate').classList.add('selected'); + !isCAM && api.view.edges(false); + break; + default: + console.log("invalid view mode: "+mode); + return; + } + api.event.emit('view.set', mode); + DOC.activeElement.blur(); +} + +function getMode() { + return settings.mode(); +} + +function getModeLower() { + return getMode().toLowerCase(); +} + +function switchMode(mode) { + setMode(mode, null, platform.update_size); +} + +function setMode(mode, lock, then) { + if (!MODES[mode]) { + console.log("invalid mode: "+mode); + mode = 'FDM'; + } + const current = settings.get(); + // change mode constants + current.mode = mode; + MODE = MODES[mode]; + // gcode edit area for any non-SLA mode + api.uc.setVisible($('gcode-edit'), mode !== 'SLA'); + // highlight selected mode menu item + ["FDM","CAM","SLA","LASER","DRAG","WJET","WEDM"].forEach(sm => { + const cl = $(`mode-${sm.toLowerCase()}`).classList; + if (sm === mode) { + cl.add('selected'); + } else { + cl.remove('selected'); + } + }); + // restore cached device profile for this mode + if (current.cdev[mode]) { + current.device = clone(current.cdev[mode]); + api.event.emit('device.select', api.device.get()); + } + // hide/show + api.uc.setVisible($('set-tools'), mode === 'CAM'); + // updates right-hand menu by enabling/disabling fields + setViewMode(VIEWS.ARRANGE); + api.uc.setMode(MODE); + // sanitize and persist settings + api.conf.load(); + api.conf.save(); + // other housekeeping + triggerSettingsEvent(); + platform.update_selected(); + selection.update_bounds(api.widgets.all()); + api.conf.update_fields(); + // because device dialog, if showing, needs to be updated + if (modalShowing()) { + api.show.devices(); + } + api.space.restore(null, true); + api.event.emit("mode.set", mode); + if (then) { + then(); + } +} + +function currentProcessName() { + return settings.get().cproc[getMode()]; +} + +function currentProcessCode() { + return settings.get().sproc[getMode()][currentProcessName()]; +} + +function setControlsVisible(show) { + // TODO fix + // $('mid-left').style.display = show ? 'flex' : 'none'; + // $('mid-right').style.display = show ? 'flex' : 'none'; +} + +function downloadBlob(data, filename) { + let url = WIN.URL.createObjectURL(new Blob([data], {type: "octet/stream"})); + $('mod-any').innerHTML = `x`; + $('_dexport_').click(); +} + +// prevent safari from exiting full screen mode +DOC.onkeydown = function (evt) { if (evt.keyCode == 27) evt.preventDefault() } + +export { FILES as catalog, LOCAL, SETUP, SECURE }; diff --git a/src2/kiri/pack.js b/src2/kiri/pack.js new file mode 100644 index 00000000..2d35e626 --- /dev/null +++ b/src2/kiri/pack.js @@ -0,0 +1,141 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +export class Packer { + constructor(w, h, spacing, opt = {}) { + this.w = w; + this.h = h; + this.spacing = spacing; + this.opt = opt; + this.init(); + } + + init() { + const { w, h, spacing, opt } = this; + this.root = { x: 0, y: 0, w: w, h: h }; + this.max = { w: 0, h: 0 }; + this.packed = false; + this.spacing = typeof(spacing) === 'number' ? spacing : 1; + this.opt = opt; + this.pad = this.spacing / 2; + } + + get size() { + return { w: this.w, h: this. h }; + } + + resize(w, h) { + this.w = w; + this.h = h; + return this; + } + + rescale(wm, hm) { + this.w *= wm; + this.h *= hm; + return this; + } + + pack(blocks, onretry) { + for (;;) { + let simple = this.opt.simple; + this.fit(blocks, simple); + if (!this.packed && onretry && onretry(this)) { + this.init(); + continue; + } + return this; + } + } + + // array of blocks/tiles with {w,h} properties + fit(blocks, simple) { + if (simple) { + return this.#fit_simple(blocks); + } else { + return this.#fit_split(blocks); + } + } + + // array of blocks/tiles with {w,h} properties + #fit_simple(blocks) { + let x = 0, y = 0, mh = 0, maxx = 0, maxy = 0; + let spacing = this.spacing; + for (let block of blocks) { + block.fit = { x, y }; + this.max.w = Math.max(this.max.w, x + block.w); + this.max.h = Math.max(this.max.h, y + block.h); + maxx = Math.max(maxx, x); + maxy = Math.max(maxy, y); + mh = Math.max(mh, block.h); + x += block.w; + if (x > this.root.w) { + x = 0; + y = y + mh + spacing; + mh = 0; + } else { + x += spacing; + } + if (y > this.root.h) { + return this; + } + } + if (this.opt.invx) { + for (let block of blocks) { + block.fit.x = maxx - block.fit.x; + } + } + if (this.opt.invy) { + for (let block of blocks) { + block.fit.y = maxy - block.fit.y; + } + } + this.packed = true; + return this; + } + + // array of blocks/tiles with {w,h} properties + #fit_split(blocks) { + let node, w, h; + for (let block of blocks) { + w = block.w; + h = block.h; + if (node = this.#findNode(this.root, w, h)) { + block.fit = this.#splitNode(node, w, h); + } else { + return this; + } + } + this.packed = true; + return this; + } + + #findNode(root, w, h) { + if (root.used) { + return this.#findNode(root.right, w, h) || this.#findNode(root.down, w, h); + } else if (w <= root.w && h <= root.h) { + return root; + } else { + return null; + } + } + + #splitNode(node, w, h) { + let spacing = this.spacing; + node.used = true; + node.down = { + x: node.x, + y: node.y + h + spacing, + w: node.w, + h: node.h - h + }; + node.right = { + x: node.x + w + spacing, + y: node.y, + w: node.w - w, + h: h + }; + this.max.w = Math.max(this.max.w, node.x + w); + this.max.h = Math.max(this.max.h, node.y + h); + return node; + } +} diff --git a/src2/kiri/platform.js b/src2/kiri/platform.js new file mode 100644 index 00000000..a29bb0bc --- /dev/null +++ b/src2/kiri/platform.js @@ -0,0 +1,942 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $, h } from '../moto/webui.js'; +import { api } from './api.js'; +import { ajax, js2o } from './utils.js'; +import { base } from '../geo/base.js'; +import { load } from '../load/file.js'; +import { space } from '../moto/space.js'; +import { Widget } from './widget.js'; + +import { COLOR, MODES } from './consts.js'; +import { THREE } from '../ext/three.js'; + +const V0 = new THREE.Vector3(0,0,0); + +let setbounds = undefined; +let grouping = false; +let topZ = 0; + +function current() { + return api.conf.get(); +} + +function get_mode() { + return api.mode.get_id(); +} + +function platformUpdateOrigin(update_bounds = true) { + if (update_bounds) { + platform.update_bounds(); + } + + const settings = current(); + const { device, process, controller, stock, bounds } = settings; + const { FDM, CAM, SLA, LASER, DRAG, WJET, WEDM } = MODES; + const TWOD = [ LASER, DRAG, WJET, WEDM ]; + const MODE = get_mode(); + + let ruler = controller.showRulers; + let stockCenter = stock.center || { x: 0, y: 0, z: 0 }; + let isIndexed = process.camStockIndexed; + let isBelt = device.bedBelt; + let origin = settings.origin = { x: 0, y: 0, z: 0 }; + let center = MODE === MODES.FDM ? device.originCenter || device.bedRound : + MODE === MODES.SLA ? false : + MODE === MODES.CAM ? process.camOriginCenter : + device.originCenter || process.ctOriginCenter; + + if (MODE === MODES.CAM && process.camOriginTop) { + origin.z = stock.z + 0.01; + } + + if (!center) { + if (MODE === MODES.CAM) { + origin.x = (-stock.x / 2) + stockCenter.x; + origin.y = (stock.y / 2) - stockCenter.y; + } else { + if (TWOD.contains(MODE) && process.ctOriginBounds) { + let b = bounds; + origin.x = b.min.x, + origin.y = -b.min.y + } else { + origin.x = -device.bedWidth / 2; + origin.y = device.bedDepth / 2; + } + } + } else if (MODE === MODES.CAM) { + origin.x = stockCenter.x; + origin.y = -stockCenter.y; + } else if (TWOD.contains(MODE) && process.ctOriginBounds ) { + let b = bounds, + mx = bounds.min.x, + my = bounds.min.y, + xd = b.max.x - mx, + yd = b.max.y - my; + origin.x += (mx + xd / 2); + origin.y -= (my + yd / 2); + } else if (isBelt) { + origin.y = device.bedDepth / 2; + } + + // CNC origin offsets + if (MODE === MODES.CAM) { + origin.x += process.camOriginOffX; + origin.y -= process.camOriginOffY; + origin.z += process.camOriginOffZ; + if (isIndexed) { + origin.y = 0; + } + } + + // WireEDM origin offsets + if (MODE === MODES.WEDM && process.ctOriginBounds) { + origin.x -= process.ctOriginOffX; + origin.y += process.ctOriginOffY; + } + space.platform.setRulers(ruler, ruler, 1 / api.view.unit_scale(), 'X', isBelt ? 'Z' : 'Y'); + + let { x, y, z } = origin; + let oz = process.camStockIndexed ? z / 2 : z; + if (controller.showOrigin && MODE !== MODES.SLA) { + space.platform.setOrigin(x, y, oz, true); + } else { + space.platform.setOrigin(x, y, oz, false); + } +} + +function platformUpdateSize(updateDark = true) { + const { process, device, controller } = current(); + const { showRulers, units } = controller; + + space.platform.setRound(device.bedRound); + space.platform.setSize( + parseInt(device.bedWidth || 300), + parseInt(device.bedDepth || 300), + parseFloat(device.bedHeight || 5), + parseFloat(device.maxHeight || 100) + ); + + const ruler = showRulers, + unitMM = units === 'mm', + gridMajor = unitMM ? 25 : 25.4, + gridMinor = unitMM ? 5 : 25.4 / 10; + + if (updateDark) { + if (controller.dark) { + space.platform.set({ light: 0.08 }); + space.platform.setFont({rulerColor:'#888888'}); + space.platform.setGrid(gridMajor, gridMinor, 0x666666, 0x333333); + space.platform.opacity(0.05); + space.sky.set({ color: 0, ambient: { intensity: 0.6 } }); + document.body.classList.add('dark'); + } else { + space.platform.set({ light: 0.08 }); + space.platform.setFont({rulerColor:'#333333'}); + space.platform.setGrid(gridMajor, gridMinor, 0x999999, 0xcccccc); + space.platform.opacity(0.2); + space.sky.set({ color: 0xffffff, ambient: { intensity: 1.1 } }); + document.body.classList.remove('dark'); + } + space.platform.setSize(); + } + + space.platform.setRulers(ruler, ruler, 1 / api.view.unit_scale(), 'X', device.bedBelt ? 'Z' : 'Y'); + platform.update_origin(); +} + +function platformUpdateMidZ() { + topZ = 0; + api.widgets.each(widget => { + topZ = Math.max(topZ, widget.mesh.getBoundingBox().max.z); + }); + space.platform.setMaxZ(topZ); +} + +function platformUpdateTopZ() { + const { process, stock } = current(); + const MODE = get_mode(); + api.widgets.each(widget => { + if (MODE === MODES.CAM) { + const bounds = widget.getBoundingBox(); + const wzmax = bounds.max.z; + const zdelta = process.camZOffset || 0; + switch (process.camZAnchor) { + case 'top': + widget.setTopZ(stock.z - zdelta); + break; + case 'middle': + if (widget.isIndexed) { + widget.setTopZ(zdelta); + } else { + widget.setTopZ(stock.z - (stock.z - wzmax) / 2 + zdelta); + } + break; + case 'bottom': + widget.setTopZ(wzmax + zdelta); + break; + } + } else { + widget.setTopZ(0); + } + }); +} + +function platformUpdateStock() { + const settings = current(); + const { bounds, process, mode } = settings; + const { camStockX, camStockY, camStockZ, camStockOffset, camStockIndexed } = process; + if (mode === 'CAM') { + let stock = settings.stock = { + x: camStockX, + y: camStockY, + z: camStockZ + }; + // drop back to offset mode if any stock dimension is 0 + if (camStockOffset || (stock.x * stock.y * stock.z === 0)) { + stock.x += bounds.max.x - bounds.min.x; + stock.y += bounds.max.y - bounds.min.y; + stock.z += bounds.max.z - bounds.min.z; + } + stock.center = { + x: (bounds.max.x + bounds.min.x) / 2, + y: (bounds.max.y + bounds.min.y) / 2, + z: camStockIndexed ? 0 : stock.z / 2 + }; + } else { + settings.stock = {}; + } +} + +function platformSetBounds(bounds) { + setbounds = bounds; + return platformUpdateBounds(); +} + +function platformUpdateBounds() { + const bounds = setbounds || new THREE.Box3(); + if (!setbounds) + api.widgets.each(widget => { + let wp = widget.track.pos; + let wb = widget.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); + }); + if (bounds.isEmpty()) { + bounds.set(V0, V0); + } + current().bounds = bounds; + platformUpdateStock(); + platformUpdateTopZ(); + platformUpdateMidZ(); + platformUpdateOrigin(false); + return bounds; +} + +function platformSelectedCount() { + return api.view.is_arrange() ? api.selection.count() : 0; +} + +function platformUpdateSelected() { + const settings = current(); + + const { device } = settings; + const { extruders } = device; + const { selection, ui } = api; + + if (extruders) { + for (let i = 0; i < extruders.length; i++) { + let b = $(`sel-ext-${i}`); + if (b) b.classList.remove('pop-sel'); + } + selection.for_widgets(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); + } else { + selection.for_widgets(w => { + w.setColor(COLOR.selected); + }, true); + } +} + +function platformSelect(widget, shift, recurse = true) { + const { event, selection, view } = api; + + if (!view.is_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; + } + + if (selection.contains(widget)) { + if (shift) { + platform.deselect(widget, recurse) + } else if (selection.count() > 1) { + platform.deselect(undefined, recurse); + platform.select(widget, false, recurse); + } + } else { + // prevent selection in slice view + if (!widget.isVisible()) { + return; + } + if (!shift) { + platform.deselect(undefined, recurse); + } + selection.add(widget); + event.emit('widget.select', widget); + widget.setColor(COLOR.selected); + $(`ws-${widget.id}`)?.classList.add('selected'); + selection.update_info(); + } + + platform.update_selected(); + space.update(); +} + +function platformDeselect(widget, recurse = true) { + const { selection, view } = api; + + if (!view.is_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) { + api.widgets.each(function(widget) { + platform.deselect(widget); + }); + return; + } + + if (selection.remove(widget)) { + api.event.emit('widget.deselect', widget); + } + + $(`ws-${widget.id}`)?.classList.remove('selected'); + widget.setColor(COLOR.deselected); + platform.update_selected(); + selection.update_info(); + space.update(); +} + +function platformLoad(url, onload) { + if (url.toLowerCase().indexOf(".stl") > 0) { + platform.load_stl(url, onload); + } else { + ajax(url, 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, credentials, headers) { + new load.STL().load(url, (vertices, filename) => { + if (vertices) { + let widget = newWidget().loadVertices(vertices); + widget.meta.file = filename; + platform.add(widget); + if (onload) { + onload(vertices, widget); + } + } + }, formdata, 1 / api.view.unit_scale(), credentials, headers); +} + +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 platformGroup() { + grouping = true; +} + +// called after all new widgets are loaded to update group positions +function platformGroupDone(skipLayout) { + grouping = false; + Widget.Groups.loadDone(); + if (api.feature.drop_layout && !skipLayout) { + platform.layout(); + } +} + +let deferred = []; +let deferTimeout; + +function platformAdd(widget, shift, nolayout, defer) { + api.widgets.add(widget); + space.world.add(widget.mesh); + widget.anno = widget.anno || {}; + widget.anno.extruder = widget.anno.extruder || 0; + if (defer) { + deferred.push({widget, shift, nolayout}); + clearTimeout(deferTimeout); + deferTimeout = setTimeout(platformAddDeferred, 150); + } else { + platform.select(widget, shift); + platform.update_bounds(); + api.space.auto_save(); + platformChanged(); + api.event.emit('widget.add', widget); + if (nolayout) { + return; + } + if (!grouping) { + platformGroupDone(nolayout); + if (!current().controller.autoLayout) { + positionNewWidget(widget); + } + } + } +} + +function platformAddDeferred() { + let skiplayout = false; + for (let rec of deferred) { + let { widget, shift, nolayout } = rec; + skiplayout |= nolayout; + // platform.select(widget, shift); + if (!nolayout && !current().controller.autoLayout) { + positionNewWidget(widget); + } + } + if (!grouping) { + platformGroupDone(skiplayout); + } + api.event.emit('widget.add', deferred.map(r => r.widget)); + platform.update_bounds(); + api.space.auto_save(); + platformChanged(); + deferred = []; +} + +function positionNewWidget(widget) { + if (api.widgets.count() <= 1) { + return; + } + const settings = current(); + const { device } = settings; + const { bedWidth, bedDepth } = device; + const wbb = widget.getBoundingBox(); + const dim = { + x: wbb.max.x - wbb.min.x, + y: wbb.max.y - wbb.min.y + }; + const hdim = { + x: dim.x / 2, + y: dim.y / 2 + }; + const bounds = base.newBounds(); + const target = base.newBounds(); + const DEG2RAD = Math.PI / 180; + const WIDGETS = api.widgets.all(); + // 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, defer) { + if (!widget) { + return; + } + if (Array.isArray(widget)) { + const mc = widget.slice(); + for (let i = 0; i < mc.length; i++) { + platform.delete(mc[i].widget || mc[i], true); + } + platformDeletePost(); + api.event.emit('widget.delete', widget); + return; + } + api.client.clear(widget); + api.widgets.remove(widget); + api.selection.remove(widget); + Widget.Groups.remove(widget); + space.world.remove(widget.mesh); + if (!defer) { + platformDeletePost(); + api.event.emit('widget.delete', widget); + } +} + +function platformDeletePost() { + api.view.update_slider_max(); + platform.update_bounds(); + if (get_mode() !== MODES.FDM) { + platform.layout(); + } + space.update(); + platform.update_selected(); + if (api.feature.drop_layout) { + platform.layout(); + } + api.space.auto_save(); + platformChanged(); +} + +// render list of current widgets +function platformChanged() { + h.bind($('ws-widgets'), api.widgets.all().map(w => { + let color; + let file = w.meta.file || 'no name'; + let fsho = file.length > 25 ? file.slice(0,20) + '..' + file.slice(-2) : file; + return [ + h.div({ + onmouseenter() { + color = w.getColor(); + w.setColor(0x0088ff, null, false); + }, + onmouseleave() { + w.setColor(color, null, false); + }, + },[ + h.div({ class: "widpop" }, [ + h.div({ class: "widopt" }, [ + h.button( + { + title: "rename", + onclick() { api.widgets.rename(w) + }}, + [ h.i({ class:"fas fa-pen-to-square" }) ] + ), + h.button( + { + title: "replace", + onclick() { api.widgets.replace(null,w) } + }, + [ h.i({ class:"fas fa-rotate" }) ] + ), + h.button( + { + id: `w-en-${w.id}`, + title: "toggle disabled", + class: w.meta.disabled ? "disabled" : "", + onclick() { + let dis = w.meta.disabled = !w.meta.disabled; + let sel = api.selection.contains(w); + api.uc.setClass($(`w-en-${w.id}`), "disabled", dis); + w.setColor(sel ? COLOR.selected : COLOR.deselected, null, false); + } + }, + [ h.i({ class:"fas fa-ban" }) ] + ), + h.button( + { + title: "delete", + onclick() { platformDelete(w) } + }, + [ h.i({ class:"fas fa-trash" }), ] + ) + ]), + h.button([ + h.i({ class: "fa-solid fa-caret-left" }) + ]) + ]), + h.button({ + _: fsho, + id: `ws-${w.id}`, + class: "grow name", + title: file, + // onmouseenter() { + // color = w.getColor(); + // w.setColor(0x0088ff, null, false); + // }, + // onmouseleave() { + // w.setColor(color, null, false); + // }, + onclick() { + platformSelect(w, true, false); + color = w.getColor(); + } + }) + ]) + ] + })); +} + +function platformSelectAll() { + api.widgets.each(widget => { + platform.select(widget, true, false) + }); +} + +function platformLayout() { + const MODE = get_mode(); + const settings = current(); + const { process, device, controller } = settings; + const { ui } = api; + + const auto = ui.autoLayout.checked, + isBelt = device.bedBelt, + isArrange = api.view.is_arrange(), + layout = isArrange && auto; + + let gap = controller.spaceLayout; + + switch (MODE) { + case MODES.SLA: + gap = gap || (process.slaSupportLayers && process.slaSupportDensity ? 2 : 1); + break; + case MODES.CAM: + case MODES.LASER: + gap = gap || process.ctOutTileSpacing || 1; + break; + case MODES.FDM: + gap = gap || ((process.sliceSupportExtra || 0) * 2) + 1; + // auto resize device to support a larger object + if (isBelt) { + fitDeviceToWidgets(); + } + break; + } + + api.view.set_arrange(); + api.view.hide_slices(); + api.space.auto_save(); + + if (!isArrange) { + // skip auto-layout when not in arrange mode + 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(); + } + + // TODO: in CNC mode with >1 widget, force layout min spacing @ largest tool diameter + + // space parts to account for anchor in belt mode + if (isBelt) { + gap += process.firstLayerBeltLead || 0; + const WIDGETS = api.widgets.all(); + let bounds = platformUpdateBounds(), + movey = -(device.bedDepth / 2); + for (let widget of WIDGETS) { + let ylen = widget.track.box.h; + // only move the root widget in the group + if (widget.id === widget.group.id) { + widget.move(0, movey + ylen/2, 0, true); + movey += ylen + gap; + } + } + if (controller.spaceLayout === 0) { + let sum = 0; + let sorted = WIDGETS.sort((a, b) => a.track.pos.y - b.track.pos.y); + sorted.forEach(w => { + w.move(0, sum, 0); + sum += w.track.box.d + 1 + gap; + }); + } + if (controller.spaceRandoX) { + for (let w of WIDGETS) { + w.move( + (0.5 - Math.random()) * (device.bedWidth - w.track.box.w - process.firstLayerBrim), + 0, 0); + } + } + } else { + let i, m, sz = space.platform.size(), + mp = [sz.x, sz.y], + ms = [mp[0] / 2, mp[1] / 2], + c = Widget.Groups.blocks().sort(), + p = new kiri.Pack(ms[0], ms[1], gap).fit(c); + + while (!p.packed) { + ms[0] *= 1.1; + ms[1] *= 1.1; + p = new kiri.Pack(ms[0], ms[1], gap).fit(c); + } + + for (i = 0; i < c.length; i++) { + m = c[i]; + m.fit.x += m.w / 2; + m.fit.y += m.h / 2; + m.move(p.max.w / 2 - m.fit.x, p.max.h / 2 - m.fit.y, 0, true); + } + } + + platform.update_origin(); + space.update(); + api.event.emit('platform.layout'); +} + +function platformLoadWidget(group, vertices, filename) { + const widget = newWidget(undefined, group).loadVertices(vertices.toFloat32(), true); + widget.meta.file = filename; + if (filename) widget.saveToCatalog(filename); + platformAdd(widget); + return widget; +} + +function platformLoadFiles(files, group) { + platform.group(); + let loading = files.length; + for (let file of files) { + const name = file.name, + reader = new FileReader(), + lower = name.toLowerCase(), + isstl = lower.endsWith(".stl"), + isobj = lower.endsWith(".obj"), + is3mf = lower.endsWith(".3mf"), + issvg = lower.endsWith(".svg"), + ispng = lower.endsWith(".png"), + isjpg = lower.endsWith(".jpg"), + iskmz = lower.endsWith(".kmz"), + isini = lower.endsWith(".ini"), + isgbr = lower.endsWith(".gbr"), + israw = lower.endsWith(".raw") || lower.indexOf('.') < 0, + isset = lower.endsWith(".b64") || lower.endsWith(".km"), + isgcode = lower.endsWith(".gcode") || lower.endsWith(".nc"); + reader.file = file; + reader.onloadend = function(e) { + const data = e.target.result; + function load_dec() { + if (--loading === 0) { + platform.group_done(isgcode); + } + } + if (israw) { + platformLoadWidget(group, JSON.parse(data)); + load_dec(); + } else if (api.feature.on_load && (isstl || isobj || is3mf)) { + api.feature.on_load(data, file); + load_dec(); + } else if (api.feature.on_add_stl && isstl) { + api.feature.on_add_stl(data, file); + load_dec(); + } else if (isstl) { + const stl = new load.STL().parse(data, api.view.unit_scale()); + platformLoadWidget(group, stl, name); + load_dec(); + } else if (isobj) { + const objs = load.OBJ.parse(data.textDecode('utf-8')); + const ondn = function() { + for (let obj of objs) { + platformLoadWidget(group, obj, obj.name ? `${obj.name}-${name}` : name); + } + load_dec(); + }; + if (objs.length > 1 && !group) { + api.uc.confirm('group objects?').then(ok => { + group = ok ? [] : group; + ondn(); + }); + } else { + ondn(); + } + } else if (isgbr) { + let text = data.textDecode('utf-8'); + if (api.conf.get().controller.devel) { + api.event.emit('cam.parse.gerber', { data: text }); + } else { + api.client.gerber2mesh(text, progress => { + api.show.progress(progress, "converting"); + }, vertices => { + api.show.progress(0); + let wid = platformLoadWidget(group, vertices, name); + if (api.mode.is_cam()) { + // attach raw illustration + // api.event.emit('cam.parse.gerber', { data: text, mesh: wid.mesh }); + } + }); + } + } else if (is3mf) { + let odon = function(models) { + let msg = api.show.alert('Adding Objects'); + for (let model of models) { + platformLoadWidget(group, model.faces, model.name ? `${model.name}-${name}` : name); + } + load_dec(); + api.hide.alert(msg); + } + let msg = api.show.alert('Decoding 3MF'); + load.TMF.parseAsync(data).then(models => { + api.hide.alert(msg); + if (models.length > 1 && !group) { + api.uc.confirm(`group ${models.length} objects?`).then(ok => { + if (ok) { + group = []; + } + odon(models); + }); + } else { + odon(models); + } + }); + } else if (isgcode) { + api.function.parse(data.textDecode('utf-8'), 'gcode'); + load_dec(); + } else if (issvg) { + loadSVGDialog(opt => { + group = group || []; + let svg = load.SVG.parse(data.textDecode('utf-8'), opt); + let ind = 0; + if (svg.length === 0) { + api.show.alert(`SVG contains no polylines`, 10); + api.show.alert(`Fonts must be converted to paths`, 10); + } + for (let v of svg) { + platformLoadWidget(group, svg[ind++], ind ? `${name}-${ind}` : name); + } + load_dec(); + }); + } + else if (iskmz) api.settings.import_zip(data, true); + else if (isset) api.settings.import(data.textDecode('utf-8'), true); + else if (ispng) api.image.dialog(data, name); + else if (isjpg) api.image.convert(data, name); + else if (isini) api.settings.import_prusa(data.textDecode('utf-8')); + else api.show.alert(`Unsupported file: ${reader.file.name}`); + }; + reader.readAsArrayBuffer(reader.file); + } +} + +function loadSVGDialog(doit) { + const opt = {pre: [ + "
", + "

Import SVG

", + "

", + " Extrude a 3D model from a 2D SVG.", + " Fonts must be converted to paths in an SVG editor.", + "

", + "
", + " ", + " ", + " ", + " ", + " ", + "
z height in mm
arc segments / mm
minimum arc segments
pixel dpi (optional)
nest shapes
", + "
" + ]}; + api.uc.confirm(undefined, {convert:true, cancel:false}, undefined, opt).then((ok) => { + let depth = Math.max(0.1, parseFloat($('svg-depth').value)); + let arcs = Math.max(0.01, parseFloat($('svg-arcs').value)); + let marc = Math.max(1, parseInt($('svg-marc').value)); + let sdpi = Math.max(0, parseInt($('svg-dpi').value)); + let soup = $('svg-nest').checked; + ok && doit({ soup, resolution: arcs, segmin: marc, depth, dpi: sdpi }); + }); +} + +function fitDeviceToWidgets() { + let maxy = 0; + api.widgets.each(widget => { + let wb = widget.mesh.getBoundingBox().clone(); + maxy = Math.max(maxy, wb.max.y - wb.min.y); + }); + const { device } = current(); + if (maxy > device.bedDepth) { + device.bedDepthSave = device.bedDepth; + device.bedDepth = maxy + 10; + space.platform.setSize( + parseInt(device.bedWidth), + parseInt(device.bedDepth), + parseFloat(device.bedHeight), + parseFloat(device.maxHeight) + ); + space.platform.update(); + return true; + } +} + +// extend API (api.platform) +export const platform = { + fit: fitDeviceToWidgets, + add: platformAdd, + changed: platformChanged, + delete: platformDelete, + layout: platformLayout, + group: platformGroup, + group_done: platformGroupDone, + load: platformLoad, + load_stl: platformLoadSTL, + load_url: platformLoadURL, + load_files: platformLoadFiles, + load_verts: platformLoadWidget, + deselect: platformDeselect, + select: platformSelect, + select_all: platformSelectAll, + selected_count: platformSelectedCount, + set_bounds: platformSetBounds, + update_origin: platformUpdateOrigin, + update_bounds: platformUpdateBounds, + update_size: platformUpdateSize, + update_top_z: platformUpdateTopZ, + update_selected: platformUpdateSelected, + update: space.platform.update, + set_font: space.platform.setFont, + show_axes: space.platform.showAxes, + show_volume: space.platform.showVolume, + top_z() { return topZ }, + clear() { api.space.clear(); api.space.save(true) } +}; diff --git a/src2/kiri/print.js b/src2/kiri/print.js new file mode 100644 index 00000000..8a39bde5 --- /dev/null +++ b/src2/kiri/print.js @@ -0,0 +1,680 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { arcToPath } from '../geo/paths.js'; +import { consts } from './consts.js'; +import { newPoint } from '../geo/point.js'; +import { util } from '../geo/base.js'; + +const { numOrDefault } = util; +const { beltfact } = consts; + +const XAXIS = new THREE.Vector3(1,0,0); +const DEG2RAD = Math.PI / 180; + +class Print { + constructor(settings, widgets, id) { + this.id = id || new Date().getTime().toString(36); + this.settings = settings; + this.widgets = widgets; + this.lastPoint = null; + this.lastPoly = null; + this.lastEmit = null; + this.lastOut = null; + this.lastPos = null; + this.tools = {}; + // set to 1 to enable flow rate analysis (console) + this.debugE = settings ? (settings.controller.devel ? 1 : 0) : 0; + } + + setType(type) { + this.nextType = type; + } + + // allows for gcode object id annotations enabling + // discrete object cancellation during print (bambu) + setWidget(widget) { + this.widget = widget; + } + + /** + * addOutput - add a new point to the output gcode array + * @param {any[]} array - the output gcode array + * @param {Point} point - the new point + * @param {number} emit - the extrusion value + * @param {number} speed - the feed rate + * @param {string} tool - the tool id + * @param {"lerp"|string} opts.type - the output type + * @param {Point} opts.center - the center of the arc + * @param {Point[]} opts.arcPoints - point based approximation of arc used for rendering + * @param {unknown} opts.retract - the retraction value used for FDM + * @return {Output} - the new output object + */ + addOutput(array, point, emit, speed, tool, opts) { + const { type, retract, center, arcPoints} = opts ?? {}; + let { lastPoint, lastEmit, lastOut } = this; + let arc = emit == 2 || emit == 3; + // drop duplicates (usually intruced by FDM bisections) + if (lastPoint && point && !arc && type !== 'lerp') { + // nested due to uglify confusing browser + const { x, y, z } = lastPoint; + if (point.x == x && point.y == y && point.z == z && lastEmit == emit) { + return lastOut; + } + } + // if (emit && emit < 1) console.log(emit); + this.lastPoint = point; + this.lastEmit = emit; + this.lastOut = lastOut = new Output(point, emit, speed, tool, { + type: type ?? this.nextType, + center, + arcPoints, + }); + if (tool !== undefined) { + this.tools[tool] = true; + } + lastOut.retract = retract; + lastOut.widget = this.widget; + array.push(lastOut); + // console.log("addOutput Called", structuredClone({lastOut,array})) + this.nextType = undefined; + return lastOut; + } + + addPrintPoints(input, output, startPoint, tool) { + if (this.startPoint && input.length > 0) { + this.lastPoint = this.startPoint; + // TODO: revisit seek to origin as the first move + // addOutput(output, startPoint, 0, undefined, tool); + } + output.appendAll(input); + } + + /** + * Prints a polygon to a given output array, possibly with a given extrude factor, + * and starting from a given point. The last point is returned. + * used for FDM and laser + * @param {Polygon} poly - the polygon to print + * @param {Point} startPoint - the point to start printing from + * @param {Array} output - the array to print to + * @param {Object} [options] - optional parameters + * @param {boolean} [options.ccw] - set the polygon to be counter-clockwise + * @param {number} [options.extrude] - extrude factor for the polygon + * @param {number} [options.rate] - print speed in mm/s + * @param {number} [options.coast] - distance to coast at the end of the polygon + * @param {number} [options.simple] - if true, use the first point of the polygon + * @param {number} [options.open] - if true, don't close the polygon + * @param {number} [options.tool] - the tool to use + * @param {function} [options.onfirst] - called with the first point of the polygon + * @param {function} [options.onfirstout] - called with the first output point + * @returns {Point} the last point of the polygon + */ + polyPrintPath(poly, startPoint, output, options = {}) { + if (options.ccw) { + poly.setCounterClockwise(); + } else { + poly.setClockwise(); + } + + const scope = this; + const { settings } = scope; + const { process } = settings; + + let shortDist = process.outputShortDistance, + shellMult = numOrDefault(options.extrude, process.outputShellMult), + printSpeed = options.rate || process.outputFeedrate, + moveSpeed = process.outputSeekrate, + minSpeed = process.outputMinSpeed, + coastDist = options.coast || 0, + closest = options.simple ? poly.first() : poly.findClosestPointTo(startPoint), + perimeter = poly.perimeter(), + close = !options.open, + tool = options.tool, + last = startPoint, + first = true; + + // if short, use calculated print speed based on sliding scale + if (perimeter < process.outputShortPoly) { + printSpeed = minSpeed + (printSpeed - minSpeed) * (perimeter / process.outputShortPoly); + } + + poly.forEachPoint((point, pos, points, count) => { + if (first) { + if (options.onfirst) { + options.onfirst(point); + } + // move to first output point on poly + let out = scope.addOutput(output, point, 0, moveSpeed, tool); + if (options.onfirstout) { + options.onfirstout(out); + } + first = false; + } else { + let seglen = last.distTo2D(point); + if (coastDist && shellMult && perimeter - seglen <= coastDist) { + let delta = perimeter - coastDist; + let offset = seglen - delta; + let offPoint = last.offsetPointFrom(point, offset) + scope.addOutput(output, offPoint, shellMult, printSpeed, tool); + shellMult = 0; + } + perimeter -= seglen; + scope.addOutput(output, point, shellMult, printSpeed, tool); + } + last = point; + }, close, closest.index); + + this.lastPoly = poly; + + return output.last().point; + } + + constReplace(str, consts, start, pad, short) { + let cs = str.indexOf("{", start || 0), + ce = str.indexOf("}", cs), + tok, nutok, nustr; + if (cs >=0 && ce > cs) { + tok = str.substring(cs+1,ce); + let eva = []; + for (let [k,v] of Object.entries(consts)) { + switch (typeof v) { + case 'object': + eva.push(`let ${k} = ${JSON.stringify(v)};`); + break; + case 'number': + case 'boolean': + eva.push(`let ${k} = ${v};`); + break; + default: + if (v === undefined) v = ''; + eva.push(`let ${k} = "${v.replace(/\"/g,"\\\"")}";`); + break; + } + } + eva.push(`function range(a,b) { return (a + (layer / layers) * (b-a)).round(4) }`); + eva.push(`try {( ${tok} )} catch (e) {console.log(e);0}`); + let scr = eva.join(''); + let evl = eval(`{ ${scr} }`); + nutok = evl; + if (pad === 666) { + return evl; + } + if (pad) { + nutok = nutok.toString(); + let oldln = ce-cs+1; + let tokln = nutok.length; + if (tokln < oldln) { + short = (short || 1) + (oldln - tokln); + } + } + nustr = str.replace("{"+tok+"}",nutok); + return this.constReplace(nustr, consts, ce+1+(nustr.length-str.length), pad, short); + } else { + // insert compensating spaces for accumulated replace string shortages + if (short) { + let si = str.indexOf(';'); + if (si > 0) { + str = str.replace(';', ';'.padStart(short,' ')); + } + } + return str; + } + } + + parseSVG(code, offset) { + let scope = this, + svg = new DOMParser().parseFromString(code, 'text/xml'), + lines = [...svg.getElementsByTagName('polyline')], + output = scope.output = [], + bounds = scope.bounds = { + max: { x:-Infinity, y:-Infinity, z:-Infinity}, + min: { x:Infinity, y:Infinity, z:Infinity} + }; + lines.forEach(line => { + let seq = []; + let points = [...line.points]; + points.forEach(point => { + if (offset) { + point.x += offset.x; + point.y += offset.y; + } + if (point.x) bounds.min.x = Math.min(bounds.min.x, point.x); + if (point.x) bounds.max.x = Math.max(bounds.max.x, point.x); + if (point.y) bounds.min.y = Math.min(bounds.min.y, point.y); + if (point.y) bounds.max.y = Math.max(bounds.max.y, point.y); + if (point.z) bounds.min.z = Math.min(bounds.min.z, point.z); + if (point.z) bounds.max.z = Math.max(bounds.max.z, point.z); + const { x, y, z } = point; // SVGPoint is not serializable + scope.addOutput(seq, { x, y, z }, seq.length > 0); + }); + output.push(seq); + }); + scope.imported = code; + scope.lines = lines.length; + scope.bytes = code.length; + return scope.output; + }; + + parseGCode(gcode, offset, progress, done, opts = {}) { + const fdm = opts.fdm; + const cam = opts.cam; + const belt = opts.belt; + const lines = gcode + .toUpperCase() + .replaceAll("X", " X") + .replaceAll("Y", " Y") + .replaceAll("Z", " Z") + .replaceAll("A", " A") + .replaceAll("E", " E") + .replaceAll("F", " F") + .replaceAll("G", " G") + .replaceAll("I", " I") + .replaceAll("J", " J") + .replaceAll(" ", " ") + .split("\n"); + + const scope = this, + // morph = false, + morph = true, + bounds = scope.bounds = { + max: { x:-Infinity, y:-Infinity, z:-Infinity}, + min: { x:Infinity, y:Infinity, z:Infinity} + }, + pos = { + X: 0.0, + Y: 0.0, + Z: 0.0, + A: 0.0, + F: 0.0, + E: 0.0 + }, + off = { + x: offset ? offset.x || 0 : 0, + y: offset ? offset.y || 0 : 0, + z: offset ? offset.z || 0 : 0 + }, + xoff = { + x: 0, + y: 0, + z: 0 + }; + + let dz = 0, + abs = true, + absE = true, + defh = 0, + height = 0, + factor = 1, + tool = 0, + time = 0, + minf = Infinity, + maxf = 0, + seq = [], + autolayer = true, + newlayer = false, + hasmoved = false, + lastG = 'G1'; + + const output = scope.output = [ seq ]; + const beltaxis = { X: "X", Y: "Z", Z: "Y", E: "E", F: "F" }; + + function LOG() { + console.log(...[...arguments].map(o => Object.clone(o))); + } + + /** + * @function processLine + * @description parses a line of g-code into individual axis movements + * @param {string[]} line - the line of g-code as an array of strings, + * each representing a single axis movement + * @param {Object} axes - an object to store the axis values + * @returns {Object} an object containing the current and previous points + */ + function processLine(line, axes) { + const prevPoint = newPoint( + factor * pos.X , + factor * pos.Y , + factor * pos.Z + dz + ) + .add(xoff) + .add(off); + + // apply origin offset + // for (let layer of output) { + // for (let rec of layer) { + // let point = rec.point; + // point.x += off.X; + // point.y += off.Y; + // point.z += off.Z; + // } + // } + + const point = prevPoint.clone() + + line.forEach(tok => { + let axis = tok.charAt(0).toUpperCase(); + if (morph && belt) { + axis = beltaxis[axis]; + } + // console.log("position updated",structuredClone(pos)) + + let val = parseFloat(tok.substring(1)); + axes[axis] = val; + // if( axis == 'I' || axis == "J") return + if (abs) { + pos[axis] = val; + + if (axis == "X") point.x = factor * pos.X + xoff.x + off.x + else if (axis == "Y") point.y = factor * pos.Y + xoff.y + off.y + else if (axis == "Z") point.z = factor * pos.Z + xoff.z + off.z + dz + + + } else { + mov[axis] = val; + pos[axis] += val; + } + // console.log("position updated",structuredClone(pos)) + }); + + let center; + if(axes.I !== undefined && axes.J !== undefined) { + center = newPoint( + factor* axes.I+ xoff.x, + factor* axes.J+ xoff.y, + 0, + ); + }else if(axes.R !== undefined) { + center = newPoint( + factor* Math.cos(axes.R * DEG2RAD), + factor* Math.sin(axes.R * DEG2RAD), + 0, + ); + } + if(center){ + center = center.add(prevPoint); + center.setZ((prevPoint.z+point.z)/2+dz); + } + + return { + center, + point, + prevPoint + }; + } + + function outputPoint(point,lastP,emit,{center,arcPoints,retract}) { + + // non-move in a new plane means burp out + // the old sequence and start a new one + + if (newlayer || (autolayer && seq.z != point.z)) { + newlayer = false; + let dz = point.z - seq.z; + let nh = dz > 0 ? dz : defh; + seq = []; + seq.height = height = nh; + if (fdm) dz = -height / 2; + output.push(seq); + } + + if (!hasmoved) { + seq.height = seq.z = pos.Z; + hasmoved = true; + } + + // debug extrusion rate + const lastPos = scope.lastPos; + if (scope.debugE && fdm && lastPos && pos.E) { + // extruder move + let dE = (absE ? pos.E - scope.lastPosE : pos.E); + // distance moved in XY + let dV = point.distTo2D(lastP); + // debug print time + time += (dV * pos.F) / 1000; + // filament per mm + let dR = (dE / dV); + if (dV > 2 && dE > 0.001) { + let lab = (absE ? 'aA' : 'rR')[scope.debugE++ % 2]; + console.log(lab, height.toFixed(2), dV.toFixed(2), dE.toFixed(3), dR.toFixed(4), pos.F.toFixed(0)); + } + } + // add point to current sequence + scope.addOutput(seq, point, emit, pos.F, tool,{retract,arcPoints}); + scope.lastPos = Object.assign({}, pos); + scope.lastPosE = pos.E; + } + + /** + * Handles G2 and G3 arcs, which are circular arcs. + * @param {boolean} g2 - Whether this is a G2 or G3 arc. G2 is a clockwise arc, G3 is a counter-clockwise arc. + * @param {string[]} line - The line of the g-code file that contains the G2 or G3 command. + * @param {number} index - The line number of the g-code file that contains the G2 or G3 command. + */ + function G2G3(g2, line, index) { + const axes = {}; + const {point, prevPoint, center} = processLine(line,axes); + + // console.log(structuredClone({point,prevPoint,center})); + + let arcPoints = arcToPath( prevPoint, point, 64,{ clockwise:g2,center}) ?? [] + let emit = g2 ? 2 : 3; + + // console.log("clone point",structuredClone({point,prevPoint,center,arcPoints,emit})); + // console.log("pointer point",{point,prevPoint,center,arcPoints,emit}); + + outputPoint(point,prevPoint,emit,{center,arcPoints}); + // scope.addOutput(seq, point, emit, pos.F, tool,{center,arcPoints}); + } + + function G0G1(g0, line) { + const mov = {}; + const axes = {}; + + lastG = g0 ? 'G0' : 'G1'; + const {point, prevPoint} = processLine(line,axes); + + if (morph && belt) { + point.y -= point.z * beltfact; + point.z *= beltfact; + } + + if (pos.A) { + let ip = new THREE.Vector3(pos.X, pos.Y, pos.Z) + .applyAxisAngle(XAXIS, -pos.A * DEG2RAD); + point.x = ip.x; + point.y = ip.y; + point.z = ip.z; + } + + const retract = (fdm && pos.E < 0) || undefined; + const moving = g0 || (fdm && (pos.E <= 0 || !(axes.X || axes.Y || axes.Z))); + + if (!moving && point.x) bounds.min.x = Math.min(bounds.min.x, point.x); + if (!moving && point.x) bounds.max.x = Math.max(bounds.max.x, point.x); + if (!moving && point.y) bounds.min.y = Math.min(bounds.min.y, point.y); + if (!moving && point.y) bounds.max.y = Math.max(bounds.max.y, point.y); + if (!moving && point.z) bounds.min.z = Math.min(bounds.min.z, point.z); + if (!moving && point.z) bounds.max.z = Math.max(bounds.max.z, point.z); + + // update max speed + if (pos.F) minf = Math.min(minf, pos.F); + maxf = Math.max(maxf, pos.F); + + // always add moves to the current sequence + if (moving) { + // console.log("move",structuredClone(point)) + scope.addOutput(seq, point, 0, pos.F, tool,{retract}) + scope.lastPos = Object.assign({}, pos); + return; + } + if (seq.z === undefined) { + seq.z = point.z; + } + if (fdm && height === 0) { + seq.height = defh = height = pos.Z; + } + + outputPoint(point,prevPoint,1,{retract}) + } + + const linemod = cam ? Math.ceil(lines.length / 2500) : 0; + + lines.forEach((line, idx) => { + if (linemod && idx % linemod === 0) { + newlayer = true; + autolayer = false; + } + if (line.indexOf(';LAYER:') === 0) { + newlayer = true; + autolayer = false; + } + if (line.indexOf('- LAYER ') > 0) { + seq.height = defh; + const hd = line.replace('(','').replace(')','').split(' '); + defh = parseFloat(hd[4]); + if (fdm) dz = -defh / 2; + newlayer = true; + autolayer = false; + } + // if (["X","Y","Z"].indexOf(line.charAt(0)) >= 0) { + // line = `G0${line}`; + // } + line = line.trim().split(";")[0].split(" ").filter(v => v); + if (!line.length) return; + const c0 = line[0].charAt(0); + let cmd = ["X","Y","Z"].indexOf(c0) >= 0 ? lastG : line.shift(); + if (!cmd) return; + if (cmd.charAt(0) === 'T') { + let ext = scope.settings.device.extruders; + let pos = parseInt(cmd.charAt(1)); + if (ext && ext[pos]) { + xoff.X = -ext[pos].extOffsetX; + xoff.Y = -ext[pos].extOffsetY; + } + } + + pos.E = 0.0; + switch (cmd) { + case 'M82': + absE = true; + break; + case 'M83': + absE = false; + break; + case 'G20': + factor = 25.4; + break; + case 'G21': + factor = 1; + break; + case 'G90': + // absolute positioning + abs = true; + break; + case 'G91': + // relative positioning + abs = false; + break; + case 'G92': + line.forEach(tok => { + pos[tok.charAt(0)] = parseFloat(tok.substring(1)); + }); + break; + case 'G10': + if (seq && seq.length) { + seq.last().retract = true; + } + break; + case 'G11': + break; + case 'G0': + G0G1(1, line); + break; + case 'G1': + G0G1(0, line); + break; + case 'G2': + // turn arc into a series of points + G2G3(true, line, idx) + break; + case 'G3': + // turn arc into a series of points + G2G3(false, line, idx); + break; + case 'M6': + tool = parseInt(line[0].substring(1)); + break; + } + }); + scope.imported = gcode; + scope.lines = lines.length; + scope.bytes = gcode.length; + scope.minSpeed = Math.floor(minf / 60); + scope.maxSpeed = Math.floor(maxf / 60); + scope.belt = belt; + + if (scope.debugE) { + console.log({ bounds, print_time: time.round(2) }); + } + + console.log(scope.output) + done({ output: scope.output }); + } +} + +class Output { + /** + * Construct a new output element. + * + * in cam, emit is the G code number (G0, G1, G2, G3) + * + * @param {Point} point point to emit, with x, y, and z properties + * @param {number} emit emit (feed for printers, power for lasers, cut for cam) + * @param {number} speed speed in mm/min + * @param {number} tool tool id + * @param {Object} options options object + * @param {string} [options.type] type of point + * @param {Point} [options.center] the center of the arc + * @param {Point[]} [options.arcPoints] point based approximation of arc + */ + constructor(point, emit, speed, tool, options) { + + const { type, center, arcPoints } = (options ?? {}); + //speed, tool, type, center, arcPoints + this.point = point; + this.emit = Number(emit); //convert bools into 0/1 + this.speed = speed; + this.tool = tool; + this.type = type; + this.center = center; + this.arcPoints = arcPoints; + // this.where = new Error().stack.split("\n"); + } + + clone(z) { + let o = new Output( + this.point.clone(), + this.emit, + this.speed, + this.tool, + this.type + ); + if (z !== undefined) { + o.point.setZ(z); + } + return o; + } + + set_retract() { + this.retract = true; + return this; + } +} + +function newPrint(settings, widgets, id) { + return new Print(settings, widgets, id); +}; + +export { + Print, + newPrint +}; diff --git a/src2/kiri/render.js b/src2/kiri/render.js new file mode 100644 index 00000000..39070d66 --- /dev/null +++ b/src2/kiri/render.js @@ -0,0 +1,547 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { base } from '../geo/base.js'; +import { Layers } from './layers.js'; +import { newPoint } from '../geo/point.js'; +import { newPolygon } from '../geo/polygon.js'; +import { newSlopeFromAngle } from '../geo/slope.js'; + +const hsV = 0.9; +const XAXIS = new THREE.Vector3(1,0,0); +const DEG2RAD = Math.PI / 180; + +function worker() { + return self.kiri_worker.current; +} + +function is_cam() { + return worker().print.settings.mode === 'CAM'; +} + +function is_dark() { + return worker().print.settings.controller.dark ? true : false; +}; + +function rate_to_color(rate, max) { + if (is_cam()) { + return is_dark() ? + darkColorFunctionCAM(rate/max, 1, hsV) : + lightColorFunctionCAM(rate/max, 1, hsV); + } else { + return is_dark() ? + darkColorFunction(rate/max, 1, hsV) : + lightColorFunction(rate/max, 1, hsV); + } +}; + +/** + * Generate visual representation of a gcode program. + * @param {Output[][]} levels - Array of arrays containing `Output` objects. + * @param {function} update - Called with a completion percentage and the rendered layer. + * @param {{tools: Object, flat: Boolean, thin: Boolean, speed: Boolean, lineWidth: Number, toolMode: Boolean, z: Number, action: String, other: String}} [opts] - Optional parameters. + * @returns {Promise.kiri.Layers[]} - Array of layers. + */ +async function path(levels, update, opts = {}) { + levels = levels.filter(level => level.length); + if (levels.length === 0) { + worker().print.maxSpeed = 0; + return []; + } + + const dark = is_dark(); + const tools = opts.tools || {}; + const flat = opts.flat; + const thin = opts.thin && !flat; + const ckspeed = opts.speed !== false; + const headColor = 0x888888; + const moveColor = opts.move >= 0 ? opts.move : (dark ? 0x666666 : 0xaaaaaa); + const printColor = opts.print >= 0 ? opts.print : 0x777700; + const arrowAll = true; + const arrowSize = arrowAll ? 0.2 : 0.4; + const layers = []; + const toolMode = opts.toolMode; + const lineWidth = opts.lineWidth; + + const moveOpt = { + face: moveColor, + line: flat ? 1 : moveColor, + opacity: flat ? 0.5 : 1 + }; + const printOpt = { + // fat: thin ? 1.5 : 0, + face: printColor, + line: flat ? 1 : printColor, + opacity: flat ? 0.5 : 1 + }; + + let minspd = Infinity; + let maxspd = opts.maxspeed || 0; + let maxtool = []; + + for (let level of levels) { + for (let o of level) { + if (o.speed) { + minspd = Math.min(minspd, o.speed); + maxspd = Math.max(maxspd, o.speed); + } + if (toolMode && o.tool !== undefined) { + if (maxtool.indexOf(o.tool) < 0) { + maxtool.push(o.tool); + } + } + } + } + + // const maxspd = levels.map(level => { + // return level.map(o => o.speed || 0).reduce((a, v) => Math.max(a,v)); + // }).reduce((a, v) => Math.max(a, v)) + 1; + + // for reporting + let print = worker().print; + print.minSpeed = minspd; + print.maxSpeed = maxspd; + print.thinColor = thin; + print.flatColor = flat; + + let lastTool = null; + let lastEnd = null; + let lastOut = null; + let lastOutPoint = null; + let current = null; + let currentLaser = null; + let retracted = false; + let retractz = 0; + + function color(point) { + if (toolMode) { + return rate_to_color(maxtool.indexOf(point.tool), maxtool.length); + } else { + return rate_to_color(point.speed, maxspd); + } + } + + levels.forEach((level, index) => { + const prints = {}; + const moves = []; + const heads = []; + const changes = []; + const retracts = []; + const engages = []; + const lasers = []; + const sparks = []; + const output = new Layers(); + layers.push(output); + + const pushPrint = (toolid, poly) => { + toolid = toolid || 0; + const array = prints[toolid] = prints[toolid] || []; + const tool = tools[toolid] || {}; + array.width = (lineWidth || tool.extNozzle || 1) / 2; + array.push(poly); + emits++; + }; + + let height = level.height / 2; + let width = 1; + let emits = 0; + + level.forEach(out => { + // skip cam gcode records + if (out.gcode) { + return; + } + if (retracted && out.emit) { + retracted = false; + engages.push(lastOutPoint); + } + let outPoint = out.point; + // rotate 4th axis indexed points + // todo -- add spark lines + if (outPoint && outPoint.a !== undefined) { + let { point } = out; + // calculate rotated point + let p2 = new THREE.Vector3(point.x, point.y, point.z) + .applyAxisAngle(XAXIS, point.a * DEG2RAD); + // reconstruct point point for display without A axis + outPoint = newPoint(p2.x, p2.y, p2.z); + // let sp = newPoint(outPoint.x * 1.1, outPoint.y * 1.1, outPoint.z * 1.1); + // sparks.push(outPoint, sp); + } + if (out.tool !== lastTool) { + lastTool = out.tool; + changes.push(outPoint); + } + if (out.retract) { + retracts.push(outPoint); + retracted = true; + retractz++; + } + if (!outPoint) { + // in cam mode, these are drilling or dwell ops + return; + } + + if (lastOut) { // if last outpoint is defined + + if (arrowAll || lastOut.emit !== out.emit) { + heads.push({p1: lastOutPoint, p2: outPoint}); + } + const op = outPoint, lp = lastOutPoint; + // const moved = Math.max( + // Math.abs(op.x - lp.x), + // Math.abs(op.y - lp.y), + // Math.abs(op.z - lp.z)); + // if (moved < 0.0001) return; + if( is_cam() && (out.emit == 2 || out.emit == 3 )) { // cam arc emit + // checks if a new poly should be started + if (!lastOut.emit || (ckspeed && out.speed !== lastOut.speed) || lastEnd) { + current = newPolygon().setOpen(); + current.push(lastOutPoint); + current.color = color(out); + pushPrint(out.tool, current); + } + + out.arcPoints.forEach(p => { + current.push(p); + }) + + current.push(outPoint); + + } else if (out.emit) { + // explicity G1 in CAM mode + // just a non-move in other modes + + // checks if a new poly should be started + if (!lastOut.emit || (ckspeed && out.speed !== lastOut.speed) || lastEnd) { + current = newPolygon().setOpen(); + current.push(lastOutPoint); + current.color = color(out); + pushPrint(out.tool, current); + } + current.push(outPoint); + } else { + if (lastOut.emit || lastEnd) { + current = newPolygon().setOpen(); + current.push(lastOutPoint); + moves.push(current); + } + current.push(outPoint); + } + if (out.type === 'laser') { + current.isLaser = true; + if (out.emit) { + if (!lastOut.emit || out.emit !== lastOut.emit) { + // off to on or different power + currentLaser = newPolygon().setOpen(); + currentLaser.push(lastOutPoint); + currentLaser.color = rate_to_color(out.emit, 1); + lasers.push(currentLaser); + } + currentLaser.push(outPoint); + } + } + lastEnd = null; + } else { + current = newPolygon().setOpen(); + current.push(outPoint); + if (out.emit) { + current.color = color(out); + pushPrint(out.tool, current); + } else { + moves.push(current); + } + } + lastOut = out; + lastOutPoint = outPoint; + }); + + if (!lastOut) { + // laser levels alone + return; + } + + lastEnd = lastOut; + + // all moves with an emit at the very end (common in contouring) + if (lastOut.emit && !emits) { + pushPrint(lastOut.tool, current) + } + + if (lasers.length) { + for (let poly of lasers) + output + .setLayer('power', { line: 0x000055, face: 0x0000ff, opacity: 0.5 }, true) + .addPolys([ poly ], { thin: true, z: opts.z, color: poly.color }); + } + + if (changes.length) { + output + .setLayer('tool', { line: 0x000055, face: 0x0000ff, opacity: 0.5 }, true) + .addAreas(changes.map(point => { + return newPolygon().centerCircle(point, 0.2, 4).setZ(point.z + 0.03); + }), { outline: true }); + } + + if (retracts.length) { + output + .setLayer('retract', { line: 0x550000, face: 0xff0000, opacity: 0.5 }, true) + .addAreas(retracts.map(point => { + return newPolygon().centerCircle(point, 0.2, 5).setZ(point.z + 0.01); + }), { outline: true }); + } + + if (engages.length) { + output + .setLayer('engage', { line: 0x005500, face: 0x00ff00, opacity: 0.5 }, true) + .addAreas(engages.map(point => { + return newPolygon().centerCircle(point, 0.2, 7).setZ(point.z + 0.02); + }), { outline: true }); + } + + if (thin && sparks.length) { + let line = dark ? 0xffffff : 0x112233; + output + .setLayer('sparks', { face: headColor, line, opacity: 0.75 }, true) + .addLines(sparks, { thin: true }); + } + + if (thin && heads.length) { + let line = dark ? 0xffffff : 0x112233; + output + .setLayer('arrows', { face: headColor, line, opacity: 0.75 }, true) + .addAreas(heads.map(points => { + const {p1, p2} = points; + const slope = p2.slopeTo(p1); + const s1 = newSlopeFromAngle(slope.angle + 20); + const s2 = newSlopeFromAngle(slope.angle - 20); + const p3 = points.p2.projectOnSlope(s1, arrowSize); + const p4 = points.p2.projectOnSlope(s2, arrowSize); + return newPolygon().addPoints([p2,p3,p4]).setZ(p2.z + 0.01); + }), { thin: true, outline: true }); + } + + // always add moves + output + .setLayer(opts.other || 'move', moveOpt, opts.moves !== true) + .addPolys(moves, { thin: true, z: opts.z }); + + // force level when present + let pz = level.z ? level.z - height : opts.z; + for (let array of Object.values(prints)) { + for (let poly of array) { + if (flat && poly.appearsClosed()) { + poly.setClosed(); + poly.points.pop(); + } + let action = poly.isLaser ? 'speed' : opts.action || 'print'; + output + .setLayer(action, printOpt) + .addPolys([ poly ], + thin ? { thin, z: opts.z, color: poly.color } : + flat ? { + flat, z: pz, color: poly.color, + outline: true, offset: array.width, open: poly.open } : + { + offset: array.width, height, z: pz, + color: { face: poly.color, line: poly.color } + }) + } + } + + update(index / levels.length, output); + }); + + return layers; +} + +const colorFunctions = { + simple: hsv2rgb.bind({ seg: 3, fn: color4 }), + light: hsv2rgb.bind({ seg: 4, fn: color4light }), + light2: hsv2rgb.bind({ seg: 4, fn: color4light2 }), + light_cam: hsv2rgb.bind({ seg: 2, fn: color4light }), + dark: hsv2rgb.bind({ seg: 4, fn: color4dark }), + dark_cam: hsv2rgb.bind({ seg: 2, fn: color4dark }) +}; + +let lightColorFunction = colorFunctions.light2; +let darkColorFunction = colorFunctions.dark; +let lightColorFunctionCAM = colorFunctions.light_cam; +let darkColorFunctionCAM = colorFunctions.dark_cam; + +// hsv values all = 0 to 1 +function hsv2rgb(h, s, v) { + const div = this.seg; + const ss = 1 / div; + const seg = Math.floor(h / ss); + const rem = h - (seg * ss); + const inc = (rem / ss); + const dec = (1 - inc); + const rgb = {r: 0, g: 0, b: 0}; + this.fn(rgb, inc, seg); + rgb.r = ((rgb.r * 255 * v) & 0xff) << 16; + rgb.g = ((rgb.g * 255 * v) & 0xff) << 8; + rgb.b = ((rgb.b * 255 * v) & 0xff); + return rgb.r | rgb.g | rgb.b; +} + +function color5(rgb, inc, seg) { + const dec = 1 - inc; + switch (seg) { + case 0: + rgb.r = 1; + rgb.g = inc; + rgb.b = 0; + break; + case 1: + rgb.r = dec; + rgb.g = 1; + rgb.b = 0; + break; + case 2: + rgb.r = 0; + rgb.g = dec; + rgb.b = inc; + break; + case 3: + rgb.r = inc; + rgb.g = 0; + rgb.b = 1; + break; + case 4: + rgb.r = dec; + rgb.g = 0; + rgb.b = dec; + break; + } +} + +function color4light(rgb, inc, seg) { + const dec = 1 - inc; + switch (seg) { + case 0: + rgb.r = 0.8; + rgb.g = inc * 0.75; + rgb.b = 0; + break; + case 1: + rgb.r = dec * 0.8; + rgb.g = 0.75; + rgb.b = 0; + break; + case 2: + rgb.r = 0; + rgb.g = dec * 0.75; + rgb.b = inc; + break; + case 3: + rgb.r = inc * 0.75; + rgb.g = 0; + rgb.b = 1; + break; + case 4: + rgb.r = 0.85; + rgb.g = 0; + rgb.b = 1; + break; + } +} + +function color4light2(rgb, inc, seg) { + const dec = 1 - inc; + switch (seg) { + case 0: + rgb.r = 0.9; + rgb.g = inc * 0.9; + rgb.b = 0; + break; + case 1: + rgb.r = dec * 0.9; + rgb.g = 0.8; + rgb.b = 0; + break; + case 2: + rgb.r = 0; + rgb.g = dec * 0.8; + rgb.b = inc; + break; + case 3: + rgb.r = inc * 0.9; + rgb.g = 0; + rgb.b = 1; + break; + case 4: + rgb.r = 0.9; + rgb.g = 0; + rgb.b = 1; + break; + } +} + +function color4dark(rgb, inc, seg) { + const dec = 1 - inc; + switch (seg) { + case 0: + rgb.r = 1; + rgb.g = inc; + rgb.b = 0; + break; + case 1: + rgb.r = dec; + rgb.g = 1; + rgb.b = 0; + break; + case 2: + rgb.r = 0; + rgb.g = dec * 0.85 + 0.25; + rgb.b = inc; + break; + case 3: + rgb.r = inc * 0.85; + rgb.g = 0.25; + rgb.b = 1; + break; + case 4: + rgb.r = 0.85; + rgb.g = 0.25; + rgb.b = 1; + break; + } +} + +function color4(rgb, inc, seg) { + const dec = 1 - inc; + switch (seg) { + case 0: + rgb.r = 0; + rgb.g = inc; + rgb.b = 1; + break; + case 1: + rgb.r = inc; + rgb.g = 1; + rgb.b = 0; + break; + case 2: + rgb.r = 1; + rgb.g = dec; + rgb.b = 0; + break; + case 3: + rgb.r = dec; + rgb.g = 0; + rgb.b = 0; + break; + } +} + +export const render = { + path, + is_dark, + rate_to_color +}; + +export { + path, + is_dark, + rate_to_color +}; diff --git a/src2/kiri/selection.js b/src2/kiri/selection.js new file mode 100644 index 00000000..f19c350d --- /dev/null +++ b/src2/kiri/selection.js @@ -0,0 +1,315 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from './api.js'; +import { space } from '../moto/space.js'; +import { THREE } from '../ext/three.js'; + +const selectedMeshes = []; + +function for_groups(fn) { + let groups = widgets(true).map(w => w.group).uniq(); + for (let group of groups) { + fn(group[0]); + } +} + +function for_widgets(fn, noauto) { + let m = selectedMeshes; + let w = api.widgets.all(); + if (m.length === 0 && w.length) { + m = noauto ? [] : w.map(w => w.mesh); + } + m.slice().forEach(mesh => { fn(mesh.widget) }); +} + +function for_status(fn) { + api.widgets.all().forEach(w => { + fn(w, selectedMeshes.contains(w.mesh)); + }); +} + +function for_meshes(fn) { + selectedMeshes.slice().forEach(mesh => { fn(mesh) }); +} + +function move(x, y, z, abs) { + if (!api.view.is_arrange()) { + return; + } + for_groups(w => w.move(x, y, z, abs)); + update_bounds(); + api.platform.update_bounds(); + api.event.emit('selection.move', {x, y, z, abs}); + space.update(); + api.space.auto_save(); +} + +function update_info() { + const ui = api.ui; + let bounds = new THREE.Box3(), track; + for_meshes(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 = api.view.unit_scale(); + 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); + update_bounds(); +} + +function update_bounds(widgets) { + const settings = api.conf.get(); + // update bounds on selection for drag limiting + const isBelt = settings.device.bedBelt; + if (isBelt) { + if (api.platform.fit()) { + api.platform.update_origin(); + space.update(); + } + } + const dvy = settings.device.bedDepth; + const dvx = settings.device.bedWidth; + const bounds_sel = new THREE.Box3(); + if (!widgets) { + widgets = selectedMeshes.map(m => m.widget); + } + for (let widget of widgets) { + const wp = widget.track.pos; + const bx = widget.track.box; + const miny = wp.y - bx.h / 2 + dvy / 2; + const maxy = wp.y + bx.h / 2 + dvy / 2; + const minx = wp.x - bx.w / 2 + dvx / 2; + const maxx = wp.x + bx.w / 2 + dvx / 2; + // keep widget in bounds when rotated or scaled + const ylo = miny < 0; + const yhi = !isBelt && maxy > dvy + if (ylo && !yhi) { + widget.move(0, -miny, 0); + } else if (yhi && !ylo) { + widget.move(0, dvy - maxy, 0); + } + const xlo = minx < 0; + const xhi = maxx > dvx; + if (xlo && !xhi) { + widget.move(-minx, 0, 0); + } else if (xhi && !xlo) { + widget.move(dvx - maxx, 0, 0); + } + + const 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 duplicate() { + if (!api.view.is_arrange()) { + return; + } + for_widgets(widget => { + const mesh = widget.mesh; + const bb = mesh.getBoundingBox(); + const ow = widget; + const nw = api.widgets.new().loadGeometry(mesh.geometry.clone()); + nw.meta.file = ow.meta.file; + nw.meta.vertices = ow.meta.vertices; + nw.anno = ow.annotations(); + nw.move(bb.max.x - bb.min.x + 1, 0, 0); + api.platform.add(nw, true); + api.event.emit("widget.duplicate", nw, ow); + }); +} + +function mirror() { + if (!api.view.is_arrange()) { + return; + } + for_widgets(widget => { + widget.mirror(); + }); + space.update(); + api.space.auto_save(); +} + +function scale() { + if (!api.view.is_arrange()) { + return; + } + let args = [...arguments]; + for_groups(w => w.scale(...args)); + selection.update_bounds(); + api.platform.update_bounds(); + api.event.emit('selection.scale', args); + // skip update if last argument is strictly 'false' + if (args.last() === false) { + return; + } + api.space.auto_save(); + selection.update_info(); + space.update(); +} + +function rotate(x, y, z) { + if (!api.view.is_arrange()) { + return; + } + for_groups(w => w.rotate(x, y, z)); + selection.update_bounds(); + api.platform.update_bounds(); + api.event.emit('selection.rotate', {x, y, z}); + api.space.auto_save(); + selection.update_info(); + space.update(); +} + +function merge() { + let sel = widgets(); + if (sel.length === 0) { + sel = api.widgets.all(); + } + let obj = []; + sel.forEach(widget => { + let {x, y, z} = widget.track.pos || { x:0, y:0, z:0 }; + let pos = widget.mesh.geometry.attributes.position; + let pvals = pos.array; + for (let i=0, il=pos.count; i { + 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 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; +} + +function widgets(orall) { + let sel = selectedMeshes.slice().map(m => m.widget); + return sel.length ? sel : orall ? api.widgets.all() : [] +} + +function setDisabled(bool) { + for_widgets(w => w.meta.disabled = bool); + api.platform.update_selected(); +} + +// extend API (api.selection) +export const selection = { + move, + merge, + scale, + rotate, + mirror, + duplicate, + for_groups, + for_meshes, + for_status, + for_widgets, + update_bounds, + update_info, + widgets, + export: exportWidgets, + add(w) { selectedMeshes.addOnce(w.mesh) }, + delete() { api.platform.delete(widgets()) }, + remove(w) { return selectedMeshes.remove(w.mesh) }, + count() { return selectedMeshes.length }, + contains(w) { return selectedMeshes.indexOf(w.mesh) >= 0 }, + enable() { setDisabled(false) }, + disable() { setDisabled(true) }, + opacity() { api.widgets.opacity(...arguments) }, + meshes() { return selectedMeshes.slice() }, +}; diff --git a/src2/kiri/settings.js b/src2/kiri/settings.js new file mode 100644 index 00000000..57553739 --- /dev/null +++ b/src2/kiri/settings.js @@ -0,0 +1,805 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $, h } from '../moto/webui.js'; +import { api } from './api.js'; +import { conf } from './conf.js'; +import { consts } from './consts.js'; +import { space } from '../moto/space.js'; +import { local } from '../data/local.js'; +import { utils } from './utils.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; + 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; + } + // 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, trace) { + if (!setrec) { + updateFieldsFromSettings(settings.device); + updateFieldsFromSettings(settings.process); + updateFieldsFromSettings(settings.controller); + updateExtruderFields(settings.device); + return; + } + 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] || 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 = ''; + } + } + } +} + +function updateExtruderFields(device) { + const { ui } = api; + const { LANG } = api.const; + if (device.extruders && device.extruders[device.internal]) { + updateFieldsFromSettings(device.extruders[device.internal]); + ui.extruder.innerHTML = + `${LANG.dv_gr_ext}${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); + 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) { + 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 ? 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: 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.version && data.time); + let isProcess = (data.process && data.version && data.time && data.mode && data.name); + let isDevice = (data.device && data.version && data.time); + let isTools = (data.tools && data.version && 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; + } + } + kiri.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
${new Date(data.time)}?`; + if (data.screen) { + opt.pre = [ + '
', + ``, + '
' + ]; + } + 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"); + 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); +} + +// extend API (api.conf) +const apiConf = { + dbo: () => { return ls2o('ws-settings') }, + 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, + restore: restoreSettings, + export: settingsExport, + import: settingsImport, +}; + +// extend API (api.settings) +const apiSet = { + get: getSettings, + import: settingsImport, + import_zip: settingsImportZip, + import_url: settingsImportUrl, + import_prusa: settingsPrusaConvert, + dev() { return settings.device }, + proc() { return settings.process }, + ctrl() { return settings.controller }, + mode() { return settings.mode }, + prof() { return settings.sproc[settings.mode] }, + sync: { + async get() {}, + async put() {}, + status: false + } +}; + +export default { + conf: apiConf, + set: apiSet, + settings: apiSet +}; + +export { + apiConf as conf, + apiSet as set, + apiSet as settings +}; diff --git a/src2/kiri/slice.js b/src2/kiri/slice.js new file mode 100644 index 00000000..dce114bf --- /dev/null +++ b/src2/kiri/slice.js @@ -0,0 +1,254 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { base } from '../geo/base.js'; +import { Layers } from './layers.js'; +import { polygons as POLY } from '../geo/polygons.js'; + +let tracker; + +function setSliceTracker(nv) { + return tracker = nv; +} + +/** + * Object encapsulates a z-slice from an object. This code is shared by the + * client and the worker thread. As such, the view layers are ignored in the + * worker code paths. + */ +class Slice { + + constructor (z, view) { + this.z = z; // z-index + this.index = 0; // slice index + this.lines = null; // slice raw + this.groups = null; // grouped lines + this.up = null; // slice above (linked list) + this.down = null; // slice below (linked list) + this.tops = []; // array of Top objects + this.view = view; // for rendering this slice + this.finger = null; // cached fingerprint + this.layers = null; // will replace most of the layer output data + } + + /** + * return Layers object for this slice. creates it if necessary. + */ + output() { + if (this.layers) return this.layers; + let layers = this.layers = new Layers(); + if (tracker) { + layers.setRotation(-tracker.rotation || 0); + } + return layers; + }; + + /** + * returns a cloned slice the option of a deep clone on the top polys + */ + clone(deep) { + const from = this, slice = newSlice(from.z, from.view); + from.tops?.forEach(function(top) { + slice.addTop(top.poly.clone(deep)); + }); + return slice; + }; + + topPolys() { + return this.tops.map(top => top.poly); + }; + + topSimples() { + return this.tops.map(top => top.simple); + }; + + // FDM top intersect optimization + topPolysFlat() { + if (this.topFlatPolys) { + return this.topFlatPolys; + } + return this.topFlatPolys = POLY.flatten(this.topPolys().clone(true), [], true); + }; + + // FDM retract path routing using first shell + topRouteFlat() { + if (this.topFlatRoutes) { + return this.topFlatRoutes; + } + let topShells0 = this.tops.map(top => top.shells[0]).filter(p => p); + return this.topFlatRoutes = POLY.flatten(topShells0.clone(true), [], true); + } + + // CAM only + topPolyInners() { + return this.tops.map(top => top.poly.inner).flat().filter(poly => poly); + }; + + // FDM / SLA only + topInners() { + return this.tops.map(top => top.last).flat().filter(poly => poly); + }; + + // FDM / SLA only + topFillOff() { + return this.tops.map(top => top.fill_off).flat().filter(poly => poly); + }; + + // FDM only + topFill() { + return this.tops.map(top => top.fill_lines).flat().filter(poly => poly); + }; + + // FDM only + topShells() { + return this.tops.map(top => top.shells).flat().filter(poly => poly); + }; + + /** + * produces a fingerprint for a slice that should be the same for + * layers that are identical. this happens in parts with unchanging + * vertical wall regions. this allows us to eliminate expensive diffs + * and infill computation when we detect the layers are the same. + */ + fingerprint() { + if (this.finger) { + return this.finger; + } + return this.finger = POLY.fingerprint(this.topPolys()); + }; + + /** + * returns true if the layers' fingerprints are the same + */ + fingerprintSame(slice) { + return slice ? POLY.fingerprintCompare(this.fingerprint(), slice.fingerprint()) : false; + }; + + addTops(polys) { + polys.forEach(p => { + this.addTop(p); + }); + return this; + } + + /** + * @param {Object | Polygon} data + * @returns Slice.Top + */ + addTop(data) { + if (data.length) { + // standard legacy polygon + let top = new Top(data); + this.tops.push(top); + top.simple = data; + return top; + } else { + // create top object from object bundle passed back by slicePost() + let top = new Top(data.poly); + top.thin_fill = data.thin_fill ? data.thin_fill.map(p => base.newPoint(p.x,p.y,p.z)) : undefined; + top.fill_lines = data.fill_lines; + top.fill_sparse = data.fill_sparse; + top.fill_off = data.fill_off; + top.last = data.last; + top.gaps = data.gaps; + top.shells = data.shells; + top.simple = data.simple; + this.tops.push(top); + return top; + } + }; + + findClosestPointTo(target) { + let min, find; + + if (this.tops && this.tops.length) { + this.tops.forEach(function(top) { + find = top.poly.findClosestPointTo(target); + if (!min || find.distance < min.distance) { + min = find; + } + }); + } else if (this.supports) { + this.supports.forEach(function(poly) { + find = poly.findClosestPointTo(target); + if (!min || find.distance < min.distance) { + min = find; + } + }); + } + + return min; + }; + + setFields(fields = {}) { + Object.assign(this, fields); + return this; + } + + // xray(dash = 3) { + // // console.log('xray', this); + // this.output().setLayer(`xp`, 0x888800).addPolys(this.topPolys()); + // this.lines.forEach((line, i) => { + // const group = i % dash; + // const color = [ 0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 0xff00ff ][group]; + // this.output().setLayer(`xl-${group}`, color).addLine(line.p1, line.p2); + // }); + // } +} + +/** + * Represents a top-level (outer) polygon in a slice. Slices may contain + * multiple tops each with nested structures. Top objects contain cached + * and computed objects for quick access for rendering and dependent computations. + */ +class Top { + + constructor(polygon) { + this.poly = polygon; // outline poly + } + + clone(deep) { + let top = new Top(this.poly.clone(deep)); + return top; + } + + /** + * return innermost traces under a given top. for FDM, this represents + * the outline shell that the fill touches. used by Print, Laser + */ + innerShells() { + let shells = this.shells, + array = []; + if (shells) shells.forEach(function(p) { + if (p.inner) array.appendAll(p.inner); + }); + return array; + } + + /** + * Returns shell polygons of a given depth + * used by Print (FDM) + * + * @param {Polygon[]} out array to populate + * @returns {Polygon[]} array of top polygons + */ + shellsAtDepth(depth) { + return this.shells ? this.shells.filter(poly => poly.depth === depth) : []; + } + +} + +function newTop(poly) { + return new Top(poly); +} + +function newSlice(z, view) { + return new Slice(z, view); +} + +export { + Top, + Slice, + newTop, + newSlice, + setSliceTracker +}; diff --git a/src2/kiri/stack.js b/src2/kiri/stack.js new file mode 100644 index 00000000..74a47d5b --- /dev/null +++ b/src2/kiri/stack.js @@ -0,0 +1,295 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +/* + * converts `layers.js` output data structures into three.js meshes for display + */ +export class Stack { + constructor(view, freeMem, shiny) { + this._view = view; + this.view = view.newGroup(); + this.slices = []; + this.meshes = []; + this.freeMem = freeMem; + newMat = shiny ? createPhongMaterial : createLambertMaterial; + } + + size() { + return this.slices.length; + } + + hide() { + this.view.visible = false; + } + + show() { + this.view.visible = true; + } + + rotate(set) { + this.view.rotation.x = -set.angle * (Math.PI/180); + this.view.position.y = -set.dy; + this.view.position.z = -set.dz; + } + + destroy() { + this._view.remove(this.view); + THREE.dispose(this.view); + this.view = this.slices = this.meshes = null; + } + + setFreeMem(bool) { + this.freeMem = bool; + } + + setVisible(newMin, newMax) { + this.show(); + for (let i=0, s=this.slices, len=s.length; i= newMin && i <= newMax; + if (s[i].visible) this.lastVis = s[i]; + } + } + + setLastFraction(frac = 1) { + if (this.lastVis) + for (let mesh of this.lastVis.children) { + let geo = mesh.geometry; + let pos = geo.attributes.position; + let len = frac === 1 ? Infinity : Math.round(pos.count * frac); + geo.setDrawRange(0, len); + } + } + + addLayers(layers) { + // each slice gets a group so the slice can be toggled efficiently + let group = this.view.newGroup(); + this.slices.push(group); + let map = this.renderLayers(layers, group); + // by default release memory after layers are rendered + if (this.freeMem) { + layers.init(); + } + // return map of layers to materials (so they can be toggled on/off) + return map; + } + + renderLayers(layers, group) { + const map = {} + for (const [label, layer] of Object.entries(layers.layers)) { + map[label] = this.renderLayer(layer, group); + } + return map; + } + + renderLayer(layer, group) { + function addPoly(vertices, poly) { + // unroll native polys into points + if (poly.id) { + poly = { + open: poly.open, + points: poly.points.map(p => [p.x, p.y, p.z]).flat().toFloat32() + }; + } + const points = poly.points, len = points.length; + for (let i=3; i m.visible = defstate); + for (let i=0; i v.toArray()).flat()); + } else { + geo.setFromPoints(vert); + } + // LineSegments2 does not support multiple materials + const segs = fat ? + new THREE.LineSegments2(geo, mat[0]) : + new THREE.LineSegments(geo, mat); + if (rotation) segs.rotation.set(rotation.x, rotation.y, rotation.z); + if (position) segs.position.set(position.x, position.y, position.z); + if (order !== undefined) segs.renderOrder = order; + meshes.push(segs); + group.add(segs); + mats.appendAll(mat); + } + if (faces.length) { + const mat = []; + if (cface) { + cface.forEach(c => { mat.push(newMat(c, true)) }); + } else { + mat.push(newMat(color, true)); + } + mat.forEach(m => m.visible = defstate); + const geo = new THREE.BufferGeometry(); + if (faces.toFloat32) { + geo.setAttribute('position', new THREE.BufferAttribute(faces.toFloat32(), 3)); + } else { + geo.setAttribute('position', new THREE.BufferAttribute(faces, 3)); + } + if (norms) { + geo.setAttribute('normal', new THREE.BufferAttribute(norms, 3)); + } else { + geo.computeVertexNormals(); + } + if (cface) { + cface.forEach((c, i) => geo.addGroup(c.start, c.count, i)); + } else { + geo.addGroup(0, Infinity, 0); + } + const mesh = new THREE.Mesh(geo, mat); + if (rotation) mesh.rotation.set(rotation.x, rotation.y, rotation.z); + if (position) mesh.position.set(position.x, position.y, position.z); + meshes.push(mesh); + group.add(mesh); + mats.appendAll(mat); + } + if (paths) { + const mat = []; + if (cpath) { + cpath.forEach(c => { mat.push(newMat(c)) }); + } else { + mat.push(newMat(color)); + } + mat.forEach(m => m.visible = defstate); + const { index, faces, norms, z } = paths; + const geo = new THREE.BufferGeometry(); + geo.setAttribute('position', new THREE.BufferAttribute(faces, 3)); + if (index && index.length) { + geo.setIndex(index); + } + if (norms) { + geo.setAttribute('normal', new THREE.BufferAttribute(norms, 3)); + } else { + geo.computeVertexNormals(); + } + if (cpath) { + cpath.forEach((c, i) => geo.addGroup(c.start, c.count, i)); + } else { + geo.addGroup(0, Infinity, 0); + } + const mesh = new THREE.Mesh(geo, mat); + mesh.position.z = z; + if (rotation) mesh.rotation.set(rotation.x, rotation.y, rotation.z); + if (position) mesh.position.set(position.x, position.y, position.z); + meshes.push(mesh); + group.add(mesh); + mats.appendAll(mat); + } + + this.new_meshes = meshes; + this.meshes.appendAll(meshes); + mats.forEach(mat => mat.visible = defstate); + return mats; + } +} + +let shininess = 15, + specular = 0x444444, + emissive = 0x101010, + metalness = 0, + roughness = 0.3, + newMat = createPhongMaterial; + +function createLineMaterial(color, array) { + const opacity = color.lopacity || color.opacity || 1; + const mat = color.fat ? new THREE.LineMaterial({ + // transparent: true, + // opacity: opacity, + color: color.line, + linewidth: color.fat, + alphaToCoverage: false + }) : new THREE.LineBasicMaterial({ + transparent: true, + opacity: opacity, + color: color.line + }); + if (array) { + array.push(mat); + } + return mat; +} + +function createStandardMaterial(color, flat) { + return new THREE.MeshMatcapMaterial({ + transparent: color.opacity != 1, + opacity: color.opacity || 1, + color: color.face, + side: flat ? THREE.DoubleSide : THREE.FrontSide + }); +} + +function createPhongMaterial(color, flat) { + return new THREE.MeshPhongMaterial({ + shininess, + specular, + transparent: color.opacity != 1, + opacity: color.opacity || 1, + color: color.face, + side: flat ? THREE.DoubleSide : THREE.FrontSide + }); +} + +function createLambertMaterial(color, flat) { + return new THREE.MeshLambertMaterial({ + transparent: color.opacity != 1, + opacity: color.opacity || 1, + color: color.face, + side: flat ? THREE.DoubleSide : THREE.FrontSide + }); +} diff --git a/src2/kiri/stacks.js b/src2/kiri/stacks.js new file mode 100644 index 00000000..562a0e32 --- /dev/null +++ b/src2/kiri/stacks.js @@ -0,0 +1,181 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $ } from '../moto/webui.js'; +import { api } from './api.js'; +import { Stack } from './stack.js'; + +let freeMem = true, + stacks = {}, + tallest = 0, + min = 0, + max = 0, + labels, API, UC, UI, DYN; + +function init() { + labels = $("layers"); + API = api, + UC = API.uc, + UI = API.ui; +} + +function setFreeMem(bool) { + Object.values(stacks).forEach(stack => stack.obj.setFreeMem(bool)); + freeMem = bool; + return this; +} + +function clear() { + if (!API) { + init(); + } + // remove stacks from their views + for (const [stack, data] of Object.entries(stacks)) { + data.clear(); + } + min = max = tallest = 0; + stacks = {}; + DYN = UI.dyn = {}; + + // clear labels + UC.setGroup(labels); + labels.innerHTML = ''; +} + +function rotate(set) { + Object.values(stacks).forEach(stack => { + stack.obj.rotate(set); + }); +} + +function getStack(name) { + return stacks[name]; +} + +function getStacks() { + return Object.entries(stacks); +} + +function getLabels() { + return Object.keys(DYN); +} + +function setVisible(label, bool) { + let ctrl = DYN[label]; + if (ctrl) { + ctrl.toggle.checked = bool; + ctrl.group.forEach(mat => { + mat.visible = bool; + }); + } +} + +function create(name, view) { + if (stacks[name]) { + return stacks[name]; + } + const stack = stacks[name] = { + layers: [ ], + obj: new Stack(view, freeMem, api.conf.get().controller.shiny), + add: function(layers) { + let lmap = layers.layers; + let map = stack.obj.addLayers(layers); + for (const [label, mats] of Object.entries(map)) { + if (!DYN[label]) { + let check = DYN[label] = { + group: [], + toggle: UC.newBoolean(label, (abc) => { + ctrl.group.forEach(mat => { + mat.visible = ctrl.toggle.checked; + }); + if (ctrl.toggle.checked) { + api.event.emit("stack.show", label); + } else { + api.event.emit("stack.hide", label) + } + }) + }; + let color = lmap[label].color.check; + if (color !== undefined) { + let div = document.createElement('div'); + div.setAttribute('class', 'color'); + div.style.backgroundColor = `#${color.toString(16).padStart(6,0)}`; + check.toggle.parentNode.appendChild(div); + check.toggle.classList.add("color"); + } + } + const ctrl = DYN[label]; + ctrl.group.appendAll(mats); + ctrl.toggle.checked = mats.state; + } + tallest = Math.max(tallest, stack.obj.size()); + }, + remove: function() { + view.remove(stack.obj.view); + THREE.dispose(stack.obj.view); + }, + clear: function() { + view.remove(stack.obj.view); + THREE.dispose(stack.obj.view); + }, + button: function(label, action) { + UC.newRow([ UC.newButton(label, action) ]); + } + }; + return stack; +} + +function remove(name) { + const stack = stacks[name]; + if (stack) { + stack.remove(); + delete stacks[name]; + } +} + +function getRange() { + return {min, max, tallest}; +} + +function setRange(newMin, newMax) { + Object.values(stacks).forEach(stack => { + stack.obj.setVisible(newMin,newMax); + }); + min = newMin; + max = newMax; +} + +function setFraction(frac) { + Object.values(stacks).forEach(stack => { + stack.obj.setLastFraction(frac); + }); +} + +export default { + clear, + create, + rotate, + remove, + getStack, + getStacks, + getRange, + setRange, + getLabels, + setVisible, + setFraction, + setFreeMem +}; + +export { + clear, + create, + rotate, + remove, + getStack, + getStacks, + getRange, + setRange, + getLabels, + setVisible, + setFraction, + setFreeMem +}; diff --git a/src2/kiri/stats.js b/src2/kiri/stats.js new file mode 100644 index 00000000..cfe201d3 --- /dev/null +++ b/src2/kiri/stats.js @@ -0,0 +1,53 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { local } from '../data/local.js'; +import { broker } from '../moto/broker.js'; +import { js2o, o2js } from './utils.js'; + +class Stats { + constructor(db) { + this.db = db || local; + 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]; + } + } + } + + save(quiet) { + this.db['stats'] = o2js(this.obj); + if (!quiet) { + broker.publish('stats', this.obj); + } + return this; + } + + get(k) { + return this.obj[k]; + } + + set(k,v,quiet) { + this.obj[k] = v; + this.save(quiet); + return this; + } + + add(k,v,quiet) { + this.obj[k] = (this.obj[k] || 0) + (v || 1); + this.save(quiet); + return this; + } + + del(k, quiet) { + delete this.obj[k]; + this.save(quiet); + return this; + } +} + +export const stats = new Stats(); + +export { stats as Stats }; diff --git a/src2/kiri/tools.js b/src2/kiri/tools.js new file mode 100644 index 00000000..25f28c41 --- /dev/null +++ b/src2/kiri/tools.js @@ -0,0 +1,166 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from './api.js'; +import { THREE } from '../ext/three.js'; + +const { Vector3, Quaternion } = THREE; +const { kiri, moto } = root; + +const XAXIS = new THREE.Vector3(1,0,0); +const ZAXIS = new THREE.Vector3(0,0,1); + +// circle +let pgeo = new THREE.CircleGeometry(8, 30); +let pmat = new THREE.MeshBasicMaterial({color: 0xddeeff, opacity: 0.6, transparent: true}); +let pmesh = new THREE.Mesh(pgeo, pmat); + +// circle outline +let pi = pgeo.index.array; +let pp = pgeo.attributes.position.array; +let np = []; +for (let i=0; i { + opName = 'camera focus'; + onDone = fn; + startIt(); +}); + +api.event.on('tool.mesh.face-up', () => { + opName = 'face select'; + onDone = onFaceUpSelect; + startIt(); +}); + +api.event.on('tool.mesh.lay-flat', () => { + opName = 'lay flat'; + onDone = onLayFlatSelect; + startIt(); +}); + +function scale() { + let cam = moto.space.internals().camera; + let dist = cam.position.distanceTo(pmesh.position); + let scale = opName === 'camera focus' ? + Math.min(0.25, 100 / dist) : dist / 100; + pmesh.scale.set(scale,scale,scale); +} + +api.event.on("space.view.zoom", scale); + +api.event.on('mouse.hover', (ev) => { + if (!enabled) { + return; + } + cleanup(); + let { int, ints, event, point, type } = ev; + if (type === 'widget') { + lastface = int.face; + let obj = lastobj = int.object; + let norm = int.face.normal; + let widget = obj.widget; + let track = widget.track; + let opos = track.pos; + obj.add(pmesh); + pmesh.position.x = point.x - opos.x + norm.x * 0.1; + if (track.indexed) { + let rad = track.indexRad; + let py = point.y * 1.001; + let pz = point.z * 1.001; + let v = new THREE.Vector3(point.x, py, pz).applyAxisAngle(XAXIS, rad); + pmesh.position.y = -v.z; + pmesh.position.z = v.y; + } else { + const zcomp = api.mode.get() === 'CAM' ? track.tzoff : 0; + pmesh.position.y = -point.z - opos.y + norm.y * 0.1; + pmesh.position.z = point.y + zcomp + norm.z * 0.1; + } + let q = new Quaternion().setFromUnitVectors(ZAXIS, + new Vector3(norm.x,norm.y,norm.z) + ); + pmesh.setRotationFromQuaternion(q); + scale(); + } +}); + +api.event.on('mouse.hover.up', (ev) => { + if (!enabled) { + return; + } + let { object } = ev; + if (!object) { + return; + } + onDone(ev); + endIt(); +}); + + +export { onLayFlatSelect, onFaceUpSelect, startIt, endIt, cleanup, scale, onDone }; diff --git a/src2/kiri/ui.js b/src2/kiri/ui.js new file mode 100644 index 00000000..1166ec98 --- /dev/null +++ b/src2/kiri/ui.js @@ -0,0 +1,846 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $ } from '../moto/webui.js'; +import { api } from './api.js'; + +let DOC = self.document, + inputAction = null, + lastAddTo = null, + lastGroup = null, + lastDiv = null, + addTo = null, + bindTo = null, + groups = {}, + groupSticky = false, + groupName = undefined, + heads = {}, // hideable group heads (clickable label) + hidden = {}, // hidden groups (by name) + hasModes = [], + setters = [], + lastMode = null, + lastExpert = true, + prefix = "tab", + units = 1, + lastChange = null, + lastBtn = null, + lastTxt = null, + lastPop = null; + +export const UI = { + prefix: function(pre) { prefix = pre; return UI }, + inputAction: function(fn) { inputAction = fn; return UI }, + lastChange: function() { return lastChange }, + checkpoint, + restore, + refresh, + setHidden, + setMode, + bound, + toInt, + toFloat, + isSticky, + setSticky, + newBoolean, + newButton, + newBlank, + newDiv, + newElement, + newExpand, + endExpand, + newGCode, + newGroup, + newLabel, + newInput, + newValue, + newRange, + newRow, + newSelect, + newText, + setGroup, + addUnits, + setUnits, + confirm, + prompt, + alert, + onBlur, + setEnabled(el, bool) { + if (bool) { + el.removeAttribute('disabled'); + } else { + el.setAttribute('disabled',''); + } + }, + setVisible(el, bool) { + UI.setClass(el, 'hide', !bool); + }, + setClass(el, clazz, bool) { + if (bool) { + el.classList.add(clazz); + } else { + el.classList.remove(clazz); + } + } +}; + +function setHidden(map) { + hidden = map; + refresh(); + for (let ctrl of Object.values(heads)) { + ctrl.update(); + } +} + +function onBlur(obj, fn) { + if (Array.isArray(obj)) { + for (let o of obj) onBlur(o, fn); + return; + } + obj.addEventListener('blur', fn); +} + +function alert(message) { + return confirm(message, {ok:true}); +} + +function prompt(message, value) { + return confirm(message, {ok:true, cancel:undefined}, value); +} + +function confirm(message, buttons, input, opt = {}) { + return new Promise((resolve, reject) => { + let { feature } = api; + let onkey_save = feature.on_key; + feature.on_key = key => { + // console.log({ eat_key: key }); + return true; + }; + let dialog = $('dialog'); + let btns = buttons || { + "yes": true, + "no": false + }; + let rnd = Date.now().toString(36); + let html = [ + `
` + ]; + if (message) { + html.push(``); + } + if (opt.pre) { + html = opt.pre.appendAll(html); + } + let iid; + if (Array.isArray(input)) { + iid = `confirm-input-${rnd}`; + html.append(`
`); + } else if (input !== undefined) { + iid = `confirm-input-${rnd}`; + html.append(`
`); + } + html.append(`
`); + Object.entries(btns).forEach((row,i) => { + html.append(``); + }); + html.append(`
`); + if (opt.post) { + html.appendAll(opt.post); + } + dialog.innerHTML = html.join(''); + function done(value) { + dialog.close(); + feature.on_key = onkey_save; + if (value !== undefined) { + setTimeout(() => { resolve(value) }, 150); + } + } + if (iid) { + let array = Array.isArray(input); + iid = $(iid); + iid.value = array ? input.join('\n') : input; + if (!array) + iid.onkeypress = (ev) => { + if (ev.key === 'Enter' || ev.charCode === 13) { + done(ev.target.value); + } + }; + } + Object.entries(btns).forEach((row,i) => { + $(`confirm-${i}-${rnd}`).onclick = (ev) => { + let value = iid && row[1] ? iid.value : row[1]; + ev.preventDefault(); + ev.stopPropagation(); + done(value); + } + }); + setTimeout(() => { + dialog.showModal(); + if (iid) { + iid.focus(); + iid.selectionStart = 0; + iid.selectionEnd = iid.value.length; + } + }, 150); + }); +} + +function refresh() { + setMode(lastMode); + setters.forEach(input => { + if (input.setv) { + input.setv(input.real); + } + }); +} + +function setMode(mode) { + lastMode = mode; + hasModes.forEach(div => div.setMode(mode)); +} + +function checkpoint(at) { + return { addTo: at || addTo, lastDiv: at || lastDiv, lastGroup, groupName }; +} + +function restore(opt = {}) { + addTo = opt.addTo || addTo; + bindTo = opt.bindTo || null; + lastDiv = opt.lastDiv || lastDiv; + lastGroup = opt.lastGroup || lastGroup; + groupName = opt.groupName || groupName; +} + +// at present only used by the layers popup menu +function setGroup(div) { + addTo = lastDiv = div; + groupName = undefined; + lastGroup = []; + return div; +} + +function newElement(type, opt = {}) { + let el = DOC.createElement(type); + if (opt.id) { + el.setAttribute("id", opt.id); + } + if (opt.class) { + for (let cl of opt.class.split(' ')) { + el.classList.add(cl); + } + } + if (opt.attr) { + for (let [key, val] of Object.entries(opt.attr)) { + el.setAttribute(key, val); + } + } + return el; +} + +function newGroup(label, div, opt = {}) { + lastDiv = div = (div || lastDiv); + + let group = opt.group || label, + row = DOC.createElement('div'), + dbkey = `beta-${prefix}-show-${group}`, + link; + + if (opt.class) { + opt.class.split(' ').forEach(ce => { + row.classList.add(ce); + }); + } else { + row.setAttribute("class", "set-header"); + } + + if (div && opt.driven) { + addModeControls(div, opt); + } + + addTo = lastDiv; + + div.appendChild(row); + if (label) { + link = DOC.createElement('a'); + link.appendChild(DOC.createTextNode(label)); + row.appendChild(link); + } + + addModeControls(row, opt); + lastGroup = groups[group] = []; + lastGroup.key = dbkey; + groupName = group; + + if (opt.hideable) { + let pad = DOC.createElement('i'); + let arr = DOC.createElement('span'); + pad.setAttribute('class','grow'); + row.appendChild(pad); + row.appendChild(arr); + const ctrl = heads[group] = { + row, + arr, + update() { + if (!hidden[group]) { + arr.innerHTML = ''; + row.classList.add('hidden'); + } else { + arr.innerHTML = ''; + row.classList.remove('hidden'); + } + } + }; + row.onclick = () => { + hidden[group] = !hidden[group]; + refresh(); + ctrl.update(); + }; + } + + return row; +} + +function addCollapsableElement(parent, options = {}) { + let row = newDiv(options); + if (parent) parent.appendChild(row); + if (lastGroup) lastGroup.push(row); + return row; +} + +function bound(low,high) { + return function(v) { + if (isNaN(v)) return low; + return v < low ? low : v > high ? high : v; + }; +} + +function toInt() { + let nv = this.value !== '' ? parseInt(this.value) : null; + if (isNaN(nv)) nv = 0; + if (nv !== null && this.bound) nv = this.bound(nv); + this.value = nv; + if (this.setv) { + return this.real = Math.round(nv * units); + }; + return nv; +} + +function toFloat() { + let nv = this.value !== '' ? parseFloat(this.value) : null; + if (nv !== null && this.bound) nv = this.bound(nv); + if (this.setv) { + return this.setv(nv * units); + } else { + this.value = nv; + } + return nv; +} + +function raw() { + return this.value !== '' ? this.value : null; +} + +function setUnits(v) { + if (v !== units) { + units = v; + refresh(); + } +} + +function newLabel(text, opt = {}) { + let label = DOC.createElement('label'); + label.appendChild(DOC.createTextNode(text)); + label.setAttribute("class", "noselect"); + if (opt.class) opt.class.split(' ').forEach(cl => { + label.classList.add(cl); + }) + return label; +} + +function newValue(size = 6, opt = {}) { + let value = DOC.createElement('input'); + value.setAttribute("size", size); + value.setAttribute("readonly", ''); + value.setAttribute("class", "nooutline noselect"); + if (opt.class) opt.class.split(' ').forEach(cl => { + value.classList.add(cl); + }) + return value; +} + +function addId(el, opt = {}) { + if (opt.id) { + el.setAttribute("id", opt.id); + } +} + +function safecall(fn) { + try { + return fn(); + } catch (e) { + // console.log({ safecall_error: e }); + return false; + } +} + +function addModeControls(el, opt = {}) { + el.__opt = opt; + el.showMe = function() { + if (opt.trace) console.log({ showMe: el }); + el.classList.remove('hide'); + }; + el.hideMe = function() { + if (opt.trace) console.log({ hideMe: el }); + el.classList.add('hide'); + }; + el.setVisible = function(show) { + if (opt.trace) console.log({ setVisible: show }); + if (show) el.showMe(); + else el.hideMe(); + }; + el.setMode = function(mode) { + let hidn = hidden[el._group] === true; + let xprt = opt.expert === undefined || (opt.expert === lastExpert); + let show = opt.show ? safecall(opt.show) : true; + let disp = opt.visible ? opt.visible() : true; + let hmod = el.hasMode(mode); + if (opt.trace) console.log({ setMode: mode, xprt, show, disp, hmod, modes:el.modes }); + if (opt.manual) return; + el.setVisible(!hidn && hmod && show && xprt && disp); + } + el.hasMode = function(mode) { + return (el.modes.length === 0) || + (el.modes.contains && el.modes.contains(mode)) || + (el.modes === mode); + } + el.modes = opt.modes || []; + hasModes.push(el); +} + +function newDiv(opt = {}) { + let div = DOC.createElement(opt.tag || 'div'); + addModeControls(div, opt); + (opt.addto || addTo).appendChild(div); + if (opt.addto) lastDiv = addTo = div; + if (opt.class) div.setAttribute('class', opt.class); + lastGroup?.push(div); + div._group = groupName; + return div; +} + +function newExpand(label, opt = {}, opteach = {}) { + let div = DOC.createElement('details'); + div.setAttribute('class', opt.class || 'f-col'); + addModeControls(div, opt); + + let summary = DOC.createElement('summary'); + summary.setAttribute('class', opt.class || 'var-row'); + summary.innerHTML = ``; + + div.appendChild( summary ); + div.collapse = () => { + div.removeAttribute('open'); + }; + + lastAddTo = addTo; + addTo.appendChild(div); + addTo = div; + + return div; +} + +function endExpand() { + addTo = lastAddTo; + return addTo; +} + +function isSticky() { + return groupSticky; +} + +function setSticky(bool) { + groupSticky = bool; +} + +function newGCode(label, options) { + let opt = options || {}, + btn = DOC.createElement("button"), + txt = DOC.createElement("textarea"), + area = opt.area; + + txt.setAttribute("wrap", "off"); + txt.setAttribute("spellcheck", "false"); + txt.setAttribute("style", "resize: none"); + txt.onblur = bindTo || inputAction; + txt.button = btn; + + btn.setAttribute("class", "basis-50"); + btn.appendChild(DOC.createTextNode(label)); + btn.setAttribute("title", opt.title || undefined); + btn.onclick = function(ev) { + ev.stopPropagation(); + if (ev.target === txt) { + // drop clicks on TextArea + ev.target.focus(); + } else { + let fc = area.firstChild; + if (fc) area.removeChild(fc); + area.appendChild(txt); + txt.scrollTop = 0; + txt.scrollLeft = 0; + txt.selectionEnd = 0; + let rows = txt.value.split('\n'); + let cols = 0; + rows.forEach(row => { + cols = Math.max(cols, row.length); + }); + let showing = btn === lastBtn; + if (lastTxt) { + lastTxt.classList.remove('txt-sel'); + } + if (lastBtn) { + lastBtn.classList.remove('btn-sel'); + } + if (!showing) { + btn.classList.add('btn-sel'); + lastTxt = btn; + lastBtn = btn; + txt.focus(); + } else { + inputAction(); + } + } + }; + + addModeControls(btn, opt); + + return txt; +} + +function newText(label, options) { + let opt = options || {}, + inline = opt.row, + row = inline ? lastDiv : newDiv(options), + btn = DOC.createElement("button"), + pop = DOC.createElement("div"), + txt = DOC.createElement("textarea"), + area = opt.area; + + txt.setAttribute("wrap", "off"); + txt.setAttribute("spellcheck", "false"); + txt.setAttribute("style", "resize: none"); + txt.onblur = inputAction; + + btn.appendChild(DOC.createTextNode(inline ? label : "edit")); + + btn.onclick = function(ev) { + ev.stopPropagation(); + if (ev.target === txt) { + // drop clicks on TextArea + ev.target.focus(); + } else { + let fc = area.firstChild; + if (fc) area.removeChild(fc); + area.appendChild(txt); + // first time, button click / show + btn.parentNode.onclick = btn.onclick; + txt.scrollTop = 0; + txt.scrollLeft = 0; + txt.selectionEnd = 0; + let rows = txt.value.split('\n'); + let cols = 0; + rows.forEach(row => { + cols = Math.max(cols, row.length); + }); + + let showing = pop === lastPop; + if (lastBtn) { + lastBtn.classList.remove('btn-sel'); + } + if (lastTxt) { + lastTxt.classList.remove('txt-sel'); + } + if (!showing) { + row.classList.add('txt-sel'); + pop.style.display = "flex"; + lastPop = pop; + lastTxt = row; + txt.focus(); + } else { + inputAction(); + } + } + }; + addModeControls(btn, opt); + addId(btn, opt); + + if (!inline) { + row.appendChild(newLabel(label)); + row.setAttribute("class", "var-row"); + } + row.appendChild(btn); + if (opt.title) row.setAttribute("title", options.title); + if (row.setVisible) { + btn.setVisible = row.setVisible; + } + + return txt; +} + +function newInput(label, opt = {}) { + let row = newDiv(opt), + hide = opt.hide, + size = opt.size || 5, + height = opt.height || 0, + ip = height > 1 ? DOC.createElement('textarea') : DOC.createElement('input'), + action = opt.action || bindTo || inputAction; + + row.appendChild(newLabel(label)); + row.appendChild(ip); + row.setAttribute("class", opt.class || "var-row"); + if (height > 1) { + ip.setAttribute("cols", size); + ip.setAttribute("rows", height); + ip.setAttribute("wrap", "off"); + } else { + if (Number.isInteger(size)) { + ip.setAttribute("size", size); + } else { + ip.setAttribute("style", `width:${size}`); + } + } + ip.setAttribute("type", "text"); + ip.setAttribute("spellcheck", "false"); + row.style.display = hide ? 'none' : ''; + if (opt.disabled) ip.setAttribute("disabled", "true"); + if (opt.title) row.setAttribute("title", opt.title); + if (opt.convert) ip.convert = opt.convert.bind(ip); + if (opt.bound) ip.bound = opt.bound; + if (opt.action) action = opt.action; + ip.addEventListener('focus', function(event) { + setSticky(true); + }); + if (action) { + ip.addEventListener('keydown', function(event) { + let key = event.key; + if ( + opt.text || + (key >= '0' && key <= '9') || + key === '.' || + key === '-' || + key === 'Backspace' || + key === 'Delete' || + key === 'ArrowLeft' || + key === 'ArrowRight' || + key === 'Tab' || + event.metaKey || + event.ctrlKey || + (key === ',' && options.comma) + ) { + return; + } + event.preventDefault(); + event.stopPropagation(); + }); + ip.addEventListener('keyup', function(event) { + if (event.keyCode === 13) { + ip.blur(); + } + }); + ip.addEventListener('blur', function(event) { + setSticky(false); + lastChange = ip; + action(event); + if (opt.trigger) { + refresh(opt.trigger === 1 || opt.trigger === true); + } + }); + if (opt.units) { + addUnits(ip, opt.round || 3); + } + } + if (!ip.convert) ip.convert = raw.bind(ip); + ip.setVisible = row.setVisible; + + return ip; +} + +function addUnits(input, round) { + setters.push(input); + input.setv = function(value) { + if (typeof(value) === 'number') { + input.real = value; + input.value = (value / units).round(round); + } else { + input.value = value; + } + return input.real; + }; + return input; +} + +function newRange(label, options) { + let row = newDiv(options), + ip = DOC.createElement('input'), + hide = options && options.hide, + action = bindTo || inputAction; + + if (label) row.appendChild(newLabel(label)); + row.appendChild(ip); + row.setAttribute("class", "var-row"); + ip.setAttribute("type", "range"); + ip.setAttribute("min", (options && options.min ? options.min : 0)); + ip.setAttribute("max", (options && options.max ? options.max : 100)); + ip.setAttribute("value", 0); + row.style.display = hide ? 'none' : ''; + if (options) { + if (options.title) { + ip.setAttribute("title", options.title); + row.setAttribute("title", options.title); + } + if (options.action) action = options.action; + } + ip.setVisible = row.setVisible; + + return ip; +} + +function newSelect(label, options = {}, source) { + let row = newDiv(options), + ip = DOC.createElement('select'), + hide = options && options.hide, + action = bindTo || inputAction; + + row.appendChild(newLabel(label)); + row.appendChild(ip); + if (Array.isArray(source)) { + ip._source = source; + } else { + row.setAttribute("source", source || "tools"); + } + row.setAttribute("class", "var-row"); + row.style.display = hide ? 'none' : ''; + if (options.convert) ip.convert = options.convert.bind(ip); + if (options.disabled) ip.setAttribute("disabled", "true"); + if (options.title) row.setAttribute("title", options.title); + if (options.action) action = options.action; + ip.setVisible = row.setVisible; + ip.onchange = function(ev) { + lastChange = ip; + action(); + if (options.trigger) { + refresh(); + } + }; + ip.onclick = (ev) => { + groupSticky = true; + }; + // because firefox + ip.onmouseenter = (ev) => { + groupSticky = true; + }; + + return ip; +} + +function newBoolean(label, action = bindTo, opt = {}) { + let row = newDiv(opt), + ip = DOC.createElement('input'), + hide = opt.hide; + + if (label) { + row.appendChild(newLabel(label)); + } + row.appendChild(ip); + row.setAttribute("class", "var-row"); + row.style.display = hide ? 'none' : ''; + ip.setAttribute("type", "checkbox"); + ip.checked = false; + if (opt.disabled) { + ip.setAttribute("disabled", "true"); + } + if (opt.title) { + ip.setAttribute("title", opt.title); + row.setAttribute("title", opt.title); + } + if (action) { + ip.onclick = function(ev) { + action(ip); + if (opt.trigger) { + refresh(); + } + }; + } + ip.setVisible = row.setVisible; + + return ip; +} + +function newBlank(options) { + let opt = options || {}, + row = newDiv(opt), + hide = opt.hide; + + row.isBlank = true; + row.style.display = hide ? 'none' : ''; + + if (!opt.driven) { + row.setAttribute("class", "var-row"); + } + + if (opt.class) { + opt.class.split(' ').forEach(ce => { + row.classList.add(ce); + }); + } + + return row; +} + +// unlike other elements, does not auto-add to a row +function newButton(label, action, opt = {}) { + let b = DOC.createElement('button'); + + b.onclick = function() { + switch (typeof action) { + case "string": + api.event.emit(action); + break; + case "function": + action(...arguments); + break; + } + }; + + if (opt.class) { + opt.class.split(' ').forEach(ce => { + b.classList.add(ce); + }); + } + if (opt.icon) { + let d = DOC.createElement('div'); + d.innerHTML = opt.icon; + b.appendChild(d); + } + if (opt.title) { + b.setAttribute('title', opt.title); + } + if (label) { + b.appendChild(DOC.createTextNode(label)); + } + + addModeControls(b, opt); + addId(b, opt); + + return b; +} + +function newRow(children, options) { + let row = addCollapsableElement((options && options.noadd) ? null : addTo); + if (children) children.forEach(function (c) { row.appendChild(c) }); + addModeControls(row, options); + if (options && options.class) { + options.class.split(' ').forEach(ce => { + row.classList.add(ce); + }); + } + return row; +} diff --git a/src2/kiri/utils.js b/src2/kiri/utils.js new file mode 100644 index 00000000..78957a18 --- /dev/null +++ b/src2/kiri/utils.js @@ -0,0 +1,117 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { local as dataLocal } from '../data/local.js'; + +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 moto.ajax.new(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) { + // defer ref b/c it may run in a worker + return js2o(dataLocal.getItem(key),def); +} + +// split 24 bit color into [ r, g, b ] +function rgb(c) { + return [ (c >> 16) & 0xff, (c >> 8) & 0xff, c & 0xff ]; +} + +// return 24 bit color from [ r, g, b ] array +function a2c(a) { + return (a[0] << 16) | (a[1] << 8) | a[0]; +} + +// average two color arrays with weighting @ return 24 bit color +function avgc(c1, c2, w = 3) { + let r1 = rgb(c1); + let r2 = rgb(c2); + let d = (w + 2); + return ((r1[0] * w + r2[0]) / d) << 16 + | ((r1[1] * w + r2[1]) / d) << 8 + | ((r1[2] * w + r2[2]) / d); +} + +function areEqual(o1, o2) { + if (o1 == o2) return true; + if (Array.isArray(o1) && Array.isArray(o2)) { + if (o1.length === o2.length) { + for (let i=0; i -- All Rights Reserved */ + +import { base } from '../geo/base.js'; +import { verticesToPoints } from '../geo/points.js'; + +const { inRange, time } = base.util; +const solid_opacity = 1.0; +const groups = []; + +const sharedArrayClass = self.SharedArrayBuffer || undefined; +const hasSharedArrays = sharedArrayClass ? true : false; + +let nextId = 0; + +function newWidget(id,group) { return new Widget(id,group) } + +function index() { return self.kiri_catalog.index } + +class Widget { + constructor(id, group) { + this.api = self.kiri_api; + + this.id = id || Date.now().toString(36)+(nextId++); + this.grouped = group ? true : false; + this.group = group || []; + this.group.push(this); + if (!this.group.id) { + this.group.id = this.id; + } + if (groups.indexOf(this.group) < 0) { + groups.push(this.group); + } + // rotation stack (for undo) + this.roto = []; + // added meshes (supports, tabs, etc) + this.adds = []; + // persisted client annotations (cam tabs, fdm supports) + this.anno = {}; + // THREE Mesh and points + this.mesh = null; + this.points = null; + // todo resolve use of this vs. mesh.bounds + this.bounds = null; + // wireframe + this.wire = null; + this.slices = null; + this.settings = null; // used?? + this.modified = true; + this.boundingBoxNeedsUpdate = true + this.track = { + // box size for packer + box: { + w: 0, + h: 0, + d: 0 + }, + scale: { + x: 1.0, + y: 1.0, + z: 1.0 + }, + rot: { + x: 0, + y: 0, + z: 0 + }, + pos: { + x: 0, + y: 0, + z: 0 + }, + top: 0, // z top + mirror: false, + indexed: false + }, + // used to cache shadow geo + this.cache = {}; + this.stats = { + slice_time: 0, + load_time: 0, + progress: 0 + }; + this.meta = { + url: null, + file: null, + saved: false + }; + // if this is a synthesized support widget + this.support = false; + } + + saveToCatalog(filename) { + if (this.grouped) { + return this; + } + const widget = this; + const mark = time(); + const vertices = widget.getGeoVertices({ unroll: true }).slice(); + widget.meta.file = filename; + widget.meta.save = mark; + catalog().putFile(filename, vertices, () => { + console.log("saved mesh ["+(vertices.length/3)+"] time ["+(time()-mark)+"]"); + }); + return this; + } + + saveState(ondone) { + if (!ondone) { + clearTimeout(this._save_timer); + this._save_timer = setTimeout(() => { + this._save_timer = undefined; + this.saveState(() => {}); + }, 1500); + return; + } + const widget = this; + index().put('ws-save-'+this.id, { + geo: widget.getGeoVertices({ unroll: false }).slice(), + track: widget.track, + group: this.group.id, + meta: this.meta, + anno: this.annotations() + }, result => { + widget.meta.saved = time(); + if (ondone) ondone(); + }); + } + + annotations() { + let o = Object.clone(this.anno); + if (o.support) { + // clear out THREE.Box temps + for (let s of o.support) { + delete s.box; + } + } + return o; + } + + #newBuffer(length) { + if (hasSharedArrays) { + return new SharedArrayBuffer(length); + } else { + return new ArrayBuffer(length); + } + } + + /** + * + * @param {Float32Array} vertices + * @returns {Widget} + */ + loadVertices(data, options = { index: false }) { + // console.log({ loadVertices: this.id, worker: this.inWorker, data }); + let vertices, + autoscale = false; + if (ArrayBuffer.isView(data) || typeof(data) != 'object') { + vertices = data; + } else { + vertices = data.vertices; + throw "deprecated vertex data format"; + } + if (vertices.buffer) { + if (hasSharedArrays && vertices.buffer instanceof sharedArrayClass) { + // console.log('converting to shared vertices'); + let newvert = new Float32Array(this.#newBuffer(vertices.buffer.byteLength)); + newvert.set(vertices); + vertices = newvert; + } + } + switch (typeof(autoscale)) { + case 'boolean': + autoscale = options; + break; + case 'object': + autoscale = options.autoscale; + break; + } + if (!vertices) { + console.log('missing vertices', {data, options}); + return; + } + if (autoscale === true) { + // onshape exports obj in meters by default :/ + let maxv = 0; + for (let i=0; i= 0 && to >= 0) ? [ + new THREE.Plane(new THREE.Vector3(0, 1, 0), -from), + new THREE.Plane(new THREE.Vector3(0, -1, 0), to), + ] : null; + moto.space.refresh(); + } + + isVisible() { + return this.getMaterial().visible; + } + + selectFaces(faces) { + let groups = mesh.util.facesToGroups(faces || []); + let geo = this.mesh.geometry; + geo.clearGroups(); + for (let group of groups) { + geo.addGroup(group.start*3, group.count*3, group.mat || 0); + } + } + + getVisualState() { + return { + edges: this.outline ? true : false, + wires: this.wire ? true : false, + opacity: this.getMaterial().opacity + }; + } + + setVisualState({ edges, wires, opacity}) { + this.cache.vizstate = this.getVisualState(); + this.setEdges(edges ?? false); + this.setWireframe(wires ?? false); + this.setOpacity(opacity ?? 1); + } + + restoreVisualState() { + if (this.cache.vizstate) { + this.setVisualState(this.cache.vizstate); + } + } + + /** + * @param {number} value + */ + setOpacity(value) { + const mesh = this.mesh; + const mat = this.getMaterial(); + if (value <= 0.0) { + mat.transparent = solid_opacity < 1.0; + mat.opacity = solid_opacity; + mat.visible = false; + } else if (inRange(value, 0.0, solid_opacity)) { + mat.transparent = value < 1.0; + mat.opacity = value; + mat.visible = true; + } + } + + toggleVisibility(bool) { + const mat = this.getMaterial(); + mat.visible = bool ?? !mat.visible; + } + + /** + * center geometry bottom (on platform) at 0,0,0 + */ + center(init) { + let bb = init ? this.mesh.getBoundingBox(true) : this.groupBounds(), + bm = bb.min.clone(), + bM = bb.max.clone(), + bd = bM.sub(bm).multiplyScalar(0.5), + dx = bm.x + bd.x, + dy = bm.y + bd.y, + dz = bm.z; + if (this.track.indexed) { + dz += bd.z; + } + this.track.center = { dx, dy, dz }; + // move mesh for each widget in group + if (!init) { + this.group.forEach(w => { + w.moveMesh(dx,dy,dz); + }); + } + return this; + } + + /** + * called by center() and Group.center() + * todo use new prototype.moveMesh() + */ + moveMesh(x, y, z) { + // if (!(x || y || z)) { + // return; + // } + let gap = this.mesh.geometry.attributes.position, + pa = gap.array; + // center point array on 0,0,0 + if (x || y || z) { + for (let i=0; i < pa.length; i += 3) { + pa[i ] -= x; + pa[i + 1] -= y; + pa[i + 2] -= z; + } + gap.needsUpdate = true; + } + let bb = this.groupBounds(); + // critical to layout and grouping + this.track.box = { + w: (bb.max.x - bb.min.x), + h: (bb.max.y - bb.min.y), + d: (bb.max.z - bb.min.z) + }; + // for use with the packer + // invalidate cached points + if (x || y || z) { + this.points = null; + this.setModified(); + } + } + + get isIndexed() { + return this.track.indexed ? true : false; + } + + setIndexed(z) { + if (z !== this.track.indexed) { + this.track.indexed = z; + this.center(false); + this._updateMeshPosition(); + } + } + + setAxisIndex(deg) { + let rad = deg * (Math.PI / 180); + if (rad !== this.track.indexRad) { + this.track.indexRad = rad; + this.setModified(); + this._updateMeshPosition(); + } + } + + /** + * moves top of widget to given Z + * + * @param {number} z position + */ + setTopZ(z) { + let mesh = this.mesh, + track = this.track, + mbb = mesh.getBoundingBox(), + mbz = mbb.max.z, + idx = this.isIndexed; + if ((idx && z != undefined) || (!idx && z)) { + track.top = z; + } else { + track.top = mbz; + } + // difference between top of stock/bounds and top of widget (cam mode) + track.tzoff = mbz - z; + this._updateMeshPosition(); + } + + move(x, y, z, abs) { + this.group.forEach(w => { + w._move(x, y, z, abs); + }); + // allow for use in engine / cli + if ((x || y || z) && this.api && this.api.event) { + this.api.event.emit('widget.move', {widget: this, pos: {x, y, z}, abs}); + } + } + + _move(x, y, z, abs) { + let mesh = this.mesh, + mat = this.getMaterial(), + pos = this.track.pos; + // do not allow moves in pure slice view + if (!mat.visible) return; + if (abs) { + pos.x = (x || 0); + pos.y = (y || 0); + pos.z = (z || 0); + } else { + pos.x += (x || 0); + pos.y += (y || 0); + pos.z += (z || 0); + } + if (x || y || z) { + this.setModified(); + this._updateMeshPosition(); + } + } + + _updateMeshPosition() { + let mesh = this.mesh, + track = this.track, + tzoff = track.tzoff, + top = track.top, + tz = tzoff !== top ? -tzoff : 0, + { x, y, z } = track.pos; + if (track.indexed) { + let rad = track.indexRad; + let ln = track.box.d / 2; + let dx = Math.sin(rad) * ln; + let dy = ln - Math.cos(rad) * ln; + this.mesh.rotation.x = -rad; + z = top; + } else { + this.mesh.rotation.x = 0; + z += tz; + } + mesh.position.set(x, y, z); + } + + scale(x, y, z) { + this.group.forEach(w => { + w._scale(x, y, z); + }); + this.center(false); + if (this.api && this.api.event) { + this.api.event.emit('widget.scale', {widget: this, x, y, z}); + } + } + + _scale(x, y, z) { + let mesh = this.mesh, + scale = this.track.scale; + this.bounds = null; + this.setWireframe(false); + this.clearSlices(); + mesh.geometry.applyMatrix4(new THREE.Matrix4().makeScale(x, y, z)); + if (this.outline) { + this.setEdges(true); + } + scale.x *= (x || 1.0); + scale.y *= (y || 1.0); + scale.z *= (z || 1.0); + this.setModified(); + } + + rotate(x, y, z, temp, center = true) { + this.group.forEach(w => { + w._rotate(x, y, z, temp); + }); + if (center) { + this.center(false); + } + if (this.outline) { + this.setEdges(true); + } + if ((x || y || z) && this.api && this.api.event) { + this.api.event.emit('widget.rotate', {widget: this, x, y, z}); + } + } + + _rotate(x, y, z, temp) { + if (!temp) { + this.bounds = null; + this.setWireframe(false); + this.clearSlices(); + } + let m4 = new THREE.Matrix4(); + let euler = typeof(x) === 'number'; + if (euler) { + m4 = m4.makeRotationFromEuler(new THREE.Euler(x || 0, y || 0, z || 0)); + } else { + m4 = m4.makeRotationFromQuaternion(x); + } + this.roto.push(m4); + this.mesh.geometry.applyMatrix4(m4); + if (!temp && euler) { + let rot = this.track.rot; + rot.x += (x || 0); + rot.y += (y || 0); + rot.z += (z || 0); + } + this.setModified(); + } + + // undo all accumulated rotations + unrotate() { + this.roto.reverse().forEach(m => { + this.mesh.geometry.applyMatrix4(m.clone().invert()); + }); + this.roto = []; + this.center(); + this.setModified(); + } + + mirror() { + this.group.forEach(w => { + w._mirror(); + }); + this.center(); + if (this.api && this.api.event) { + this.api.event.emit('widget.mirror', {widget: this}); + } + } + + _mirror() { + this.clearSlices(); + this.setWireframe(false); + let geo = this.mesh.geometry, ot = this.track; + let pos = geo.attributes.position; + let arr = pos.array; + let count = pos.count; + // invert x + for (let i=0; i { + if (slice.layers) { + stack.add(slice.layers); + } + }); + return Date.now() - mark; + } + + setEdges(set) { + if (!(this.api && this.api.conf)) { + // missing api features in engine mode + return; + } + let mesh = this.mesh; + if (set && set.toggle) { + set = this.outline ? false : true; + } + if (this.outline) { + mesh.remove(this.outline); + this.outline = null; + + } + if (set) { + let dark = this.api.space.is_dark(); + let angle = this.api.conf.get().controller.edgeangle || 20; + let edges = new THREE.EdgesGeometry(mesh.geometry, angle); + let material = new THREE.LineBasicMaterial({ color: 0 }); + this.outline = new THREE.LineSegments(edges, material); + this.outline.renderOrder = -20; + mesh.add(this.outline); + } + } + + setWireframe(set, color, opacity) { + if (!(this.api && this.api.conf)) { + // missing api features in engine mode + return; + } + let mesh = this.mesh, + widget = this; + if (this.wire) { + this.setOpacity(solid_opacity); + mesh.remove(this.wire); + this.wire = null; + } + if (set) { + let dark = this.api.space.is_dark(); + let mat = new THREE.MeshBasicMaterial({ + wireframe: true, + color: dark ? 0xaaaaaa : 0, + opacity: 0.5, + transparent: true + }) + let wire = widget.wire = new THREE.Mesh(mesh.geometry.shallowClone(), mat); + mesh.add(wire); + } + if (this.api.view.is_arrange()) { + this.setColor(this.color); + } else { + this.setColor(0x888888,undefined,false); + } + if (opacity !== undefined) { + widget.setOpacity(opacity); + } + } + + show() { + this.mesh.visible = true; + } + + hide() { + this.mesh.visible = false; + } +} + +// Widget Grouping API +const Group = Widget.Groups = { + list() { + return groups.slice() + }, + + merge(widgets) { + let grps = widgets.map(w => w.group).uniq(); + if (grps.length > 1) { + let root = grps.shift(); + let rpos = root[0].track.pos; + for (let grp of grps) { + for (let w of grp) { + let wpos = w.track.pos; + w.group = root; + w.moveMesh(rpos.x - wpos.x, rpos.y - wpos.y, rpos.z - wpos.z); + w._move(rpos.x, rpos.y, rpos.z, true); + root.push(w); + } + groups.splice(groups.indexOf(grp),1); + } + } + }, + + split(widgets) { + for (let group of widgets.map(w => w.group).uniq()) { + groups.splice(groups.indexOf(group),1); + for (let widget of group) { + let nugroup = Group.forid(widget.id); + nugroup.push(widget); + widget.group = nugroup; + } + } + }, + + forid(id) { + for (let i=0; i { + let pos = group.indexOf(widget); + if (pos >= 0) { + group.splice(pos,1); + } + if (group.length === 0) { + pos = groups.indexOf(group); + groups.splice(pos,1); + } + }); + }, + + blocks() { + return groups.map(group => { + return { + w: group[0].track.box.w, + h: group[0].track.box.h, + move: (x,y,z,abs) => { + group.forEach(widget => { + widget.getMaterial().visible = true; + widget._move(x, y, z, abs); + }); + } + }; + }); + }, + + loadDone() { + groups.forEach(group => { + if (!group.centered) { + group[0].center(); + group.centered = true; + } + }); + }, + + bounds(group) { + let bounds = null; + group.forEach(widget => { + let wb = widget.mesh.getBoundingBox(true); + if (bounds) { + bounds = bounds.union(wb); + } else { + bounds = wb; + } + }); + return bounds; + }, + + clear() { + groups.length = 0; + } +}; + +Widget.loadFromCatalog = function(filename, ondone) { + catalog().getFile(filename, function(data) { + let widget = newWidget().loadVertices(data); + widget.meta.file = filename; + ondone(widget); + }); +}; + +Widget.loadFromState = function(id, ondone, move) { + index().get('ws-save-'+id, function(data) { + if (data) { + let vertices = data.geo || data, + track = data.track || undefined, + group = data.group || id, + anno = data.anno || undefined, + widget = newWidget(id, Group.forid(group)), + meta = data.meta || widget.meta, + ptr = widget.loadVertices(vertices); + widget.meta = meta; + widget.anno = anno || widget.anno; + // restore widget position if specified + if (move && track && track.pos) { + widget.track = track; + widget.move(track.pos.x, track.pos.y, track.pos.z, true); + } + ondone(ptr); + } else { + ondone(null); + } + }); +}; + +Widget.deleteFromState = function(id,ondone) { + index().remove('ws-save-'+id, ondone); +}; + +export { Widget, newWidget }; diff --git a/src2/kiri/widgets.js b/src2/kiri/widgets.js new file mode 100644 index 00000000..f01d48cc --- /dev/null +++ b/src2/kiri/widgets.js @@ -0,0 +1,117 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from './api.js'; +import { load } from '../load/file.js'; +import { Widget, newWidget } from './widget.js'; + +let WIDGETS = []; + +function map() { + let map = {}; + for (let widget of WIDGETS) { + map[widget.id] = widget; + } + return map; +} + +function annotate(id) { + let w = WIDGETS.filter(w => w.id === id)[0]; + if (!w) { + console.trace(`annotate missing widget ${id}`); + return {}; + } + return (w.anno = w.anno || {}); +} + +/** TODO really should be on widget, not widgets */ +function rename(sel) { + let widgets = sel ? [ sel ] : api.selection.widgets(true); + if (widgets.length !== 1) { + return; + } + let widget = widgets[0]; + kiri.ui.prompt("new widget name", widget.meta.file || "no name").then(newname => { + if (newname) { + widget.meta.file = newname; + api.platform.changed(); + api.space.save(true); + } + }); +} + +function replace(vertices, sel) { + let widgets = sel ? [sel] : 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); + } + api.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 meshes() { + let out = []; + widgets.each(widget => { + if (!api.feature.hoverAdds) { + out.push(widget.mesh); + } + out.appendAll(widget.adds.filter(m=>m.visible)); + }); + return out; +} + +function opacity(value) { + api.widgets.each(w => w.setOpacity(value)); + api.space.update(); +} + +function setIndexed(value) { + api.widgets.each(w => w.setIndexed(value)); +} + +function setAxisIndex(value) { + api.widgets.each(w => w.setAxisIndex(value)); +} + +// extend API (api.widgets) +export const widgets = { + load: Widget.loadFromCatalog, + new: newWidget, + map, + meshes, + rename, + replace, + opacity, + annotate, + setIndexed, + setAxisIndex, + all() { return WIDGETS.slice() }, + add(w) { return WIDGETS.push(w) }, + remove(w) { return WIDGETS.remove(w) }, + filter(fn) { return WIDGETS = WIDGETS.filter(fn) }, + count() { return WIDGETS.length }, + each(fn) { WIDGETS.slice().forEach(widget => fn(widget)) }, + for(fn) { widgets.each(fn) }, + forid(id) { return WIDGETS.filter(w => w.id === id)[0] } +};