diff --git a/src/kiri/core/widget.js b/src/kiri/core/widget.js index 1640ce1c..5206e58f 100644 --- a/src/kiri/core/widget.js +++ b/src/kiri/core/widget.js @@ -21,8 +21,8 @@ let nextId = 0; function newWidget(id,group) { return new Widget(id,group) } +// todo: fix ui-only path thus this unorthodox binding vs importing api function catalog() { return self.kiri_catalog }; - function index() { return self.kiri_catalog.index } class Widget { diff --git a/src/kiri/ui/api.js b/src/kiri/ui/api.js index e882474c..9d705a1a 100644 --- a/src/kiri/ui/api.js +++ b/src/kiri/ui/api.js @@ -1,80 +1,90 @@ /** Copyright Stewart Allen -- All Rights Reserved */ +import '../../ext/base64.js'; + import alerts from './alerts.js'; import settings from './config/manager.js'; import STACKS from './stacks.js'; -import { visuals } from './visuals.js'; +import web from '../../moto/webui.js'; +import { beta, version } from '../../moto/license.js'; import { broker } from '../../moto/broker.js'; import { client as work } from './client.js'; import { consts, COLOR as color, LISTS as lists } from '../core/consts.js'; import { device, devices } from './devices.js'; -import { catalog, dialog, event, group, help, hide, image } from './main.js'; -import { modal, mode, process, show, space, util, view } from './main.js'; -import { local as dataLocal } from '../../data/local.js'; -import { noop, ajax, o2js, js2o } from '../core/utils.js'; import { functions } from './function.js'; +import { group as groupModule } from './groups.js'; +import { help as helpModule } from './help.js'; +import { image as imageModule } from './image.js'; +import { Index } from '../../data/index.js'; +import { LANG } from './lang.js'; +import { local } from './local-storage.js'; +import { local as dataLocal } from '../../data/local.js'; +import { modal } from './modal.js'; +import { mode as modeModule, process as processModule } from './mode.js'; +import { newWidget } from '../core/widget.js'; +import { noop, ajax, o2js, js2o, utils } from '../core/utils.js'; +import { openFiles } from './files.js'; import { platform } from './platform.js'; import { selection } from './select.js'; -import { stats } from './stats.js'; -import { newWidget } from '../core/widget.js'; -import { widgets } from '../core/widgets.js'; -import { updateTool } from '../mode/cam/tools.js'; -import { beta, version } from '../../moto/license.js'; +import { settingsUI } from './config/dialog.js'; +import { showDevices } from './devices.js'; +import { showTools } from '../mode/cam/tools.js'; import { space as SPACE } from '../../moto/space.js'; +import { stats } from './stats.js'; import { types as load } from '../../load/file.js'; - -import { LANG } from './lang.js'; -import { LOCAL, SETUP, SECURE } from './main.js'; import { UI } from './component.js'; +import { updateTool } from '../mode/cam/tools.js'; +import { util as utilModule } from './util.js'; +import { view as viewModule } from './view-state.js'; +import { visuals } from './visuals.js'; +import { widgets } from '../core/widgets.js'; +import { workspace } from './workspace.js'; -import web from '../../moto/webui.js'; +// From main.js - environment setup +let LOC = self.location, + SETUP = utils.parseOpt(LOC.search.substring(1)), + SECURE = utilModule.isSecure(LOC.protocol), + LOCAL = self.debug && !SETUP.remote, + EVENT = broker, + FILES = openFiles(new Index(SETUP.d ? SETUP.d[0] : 'kiri')); -let UC = UI.prefix('kiri').inputAction(settings.conf.update), - und = undefined, - clone = Object.clone, +// todo: fix in widget.js b/c front-end and back-end do not share api +self.kiri_catalog = FILES; +FILES.show = () => modal.show('files'); + +// Broker compatibility patch +EVENT.on = (topic, listener) => { + EVENT.subscribe(topic, listener); + return EVENT; +}; + +let busyVal = 0, 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 = { + undef = undefined; + +// the big kahuna +export const api = { + ajax, + beta, + alerts, + busy: { val() { return busyVal }, inc() { api.event.emit("busy", ++busyVal) }, dec() { api.event.emit("busy", --busyVal) } }, - onkey = (fn) => { - api.feature.on_key2.push(fn); + catalog: FILES, + client: work, + clip(text) { + navigator.clipboard + .writeText(text) + .catch(err => console.error('Clipboard Error:', err)); }, - doit = { - undo: noop, // do.js - redo: noop // do.js - }, - devel = { + clone: Object.clone, + color, + conf: settings.conf, + const: { LANG, LOCAL, SETUP, SECURE, STACKS, ...consts }, + devel: { get enabled() { return settings.ctrl().devel; }, @@ -88,110 +98,128 @@ let UC = UI.prefix('kiri').inputAction(settings.conf.update), api.function.slice(); } }, - local = { - get: (key) => localGet(key), - getItem: (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), - setItem: (key, val) => localSet(key, val), - removeItem: (key) => localRemove(key) - }, - 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; -} - -function localRemove(key) { - let sloc = api.conf.get().local; - return delete sloc[key]; -} - -export const api = { - ajax, - beta, - alerts, - busy, - catalog, - client: work, - clip, - clone, - color, - conf: settings.conf, - const: { LANG, LOCAL, SETUP, SECURE, STACKS, ...consts }, - devel, device, devices, - dialog, - doit, - event, + dialog: { + show: (which) => modal.show(which), + hide: () => modal.hide(), + update_process_list: settingsUI.update_list + }, + doit: { + undo: noop, // do.js + redo: noop // do.js + }, + 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) { alerts.update(clr) }, + import: utilModule.loadFile, + settings: settingsUI.trigger_event + }, electron: navigator.userAgent.includes('Electron'), - feature, + 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: undef, // function to limit devices shown + drop_group: undef, // optional array to group multi drop + drop_layout: true, // layout on new drop + hoverAdds: false, // when true only searches widget additions + on_key: undef, // function override default key handlers + on_key2: [], // allows for multiple key handlers + on_load: undef, // function override file drop loads + on_add_stl: undef, // legacy override stl drop loads + on_mouse_up: undef, // function intercepts mouse up select + on_mouse_down: undef, // 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); + } + }, function: functions, - group, - help, - hide, - image, + group: groupModule, + help: helpModule, + hide: { + alert(rec, recs) { alerts.hide(...arguments) }, + import: noop, + slider: visuals.hide_slider + }, + image: imageModule, js2o, language: LANG, lists, load, local, + LOCAL, modal, - mode, + mode: modeModule, new: { widget: newWidget }, noop, o2js, - onkey, + onkey(fn) { + api.feature.on_key2.push(fn); + }, platform, - process, + process: processModule, sdb: dataLocal, + SECURE, selection, settings, - show, - space, + SETUP, + show: { + alert() { return alerts.show(...arguments) }, + controls() { console.trace('deprecated') }, + devices: showDevices, + import() { api.ui.import.style.display = '' }, + layer: visuals.set_visible_layer, + local() { console.trace('deprecated') }, + progress: utilModule.setProgress, + slices: visuals.show_slices, + tools: showTools + }, + space: workspace, SPACE, stacks: STACKS, stats, tool: { update: updateTool }, - tweak, - uc: UC, - ui: {}, - util, - var: { - layer_lo: 0, - layer_hi: 0, - layer_max: 0 + tweak: { + line_precision(v) { api.work.config({ base: { clipperClean: v } }) }, + gcode_decimals(v) { api.work.config({ base: { gcode_decimals: v } }) } }, + uc: UI.prefix('kiri').inputAction(settings.conf.update), + ui: {}, + util: { + isSecure: utilModule.isSecure, + download: utilModule.download, + 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))) } + }, + // var: { + // layer_lo: 0, + // layer_hi: 0, + // layer_max: 0 + // }, version, - view, + view: { + ...viewModule, + ...visuals, + snapshot: null + }, visuals, web, widgets, diff --git a/src/kiri/ui/config/dialog.js b/src/kiri/ui/config/dialog.js new file mode 100644 index 00000000..042ceffe --- /dev/null +++ b/src/kiri/ui/config/dialog.js @@ -0,0 +1,123 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from '../api.js'; +import { modal } from '../modal.js'; +import { settings } from './manager.js'; +import { version } from '../../../moto/license.js'; + +const DOC = self.document; + +function triggerSettingsEvent() { + api.event.emit('settings', settings.get()); +} + +function editSettings(e) { + let current = settings.get(), + mode = settings.mode(), + 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 = settings.mode(), + 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[settings.mode()][name]; + api.settings.sync.put(); + updateProcessList(); + api.conf.save(); + triggerSettingsEvent(); +} + +function updateProcessList() { + let current = settings.get(); + let list = [], s = current, sp = s.sproc[settings.mode()] || {}, 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(); + modal.hide(); + } + 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); + }); +} + +export const settingsUI = { + edit: editSettings, + export: exportSettings, + delete: deleteSettings, + update_list: updateProcessList, + trigger_event: triggerSettingsEvent +}; diff --git a/src/kiri/ui/file-ops.js b/src/kiri/ui/file-ops.js new file mode 100644 index 00000000..1b0a0f81 --- /dev/null +++ b/src/kiri/ui/file-ops.js @@ -0,0 +1,129 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { api } from './api.js'; +import { base } from '../../geo/base.js'; +import { space } from '../../moto/space.js'; + +const DOC = self.document; +let platformColor; + +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) { + api.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) { + api.platform.add(widget); + api.dialog.hide(); + }); +} + +function updateCatalog(files) { + let table = api.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) { + api.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 = () => { api.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); + } +} + +export const fileOps = { + dragOverHandler, + dragLeave, + dropHandler, + loadCatalogFile, + updateCatalog +}; diff --git a/src/kiri/ui/init.js b/src/kiri/ui/init.js index e392d5f0..dbb7e70e 100644 --- a/src/kiri/ui/init.js +++ b/src/kiri/ui/init.js @@ -1,19 +1,9 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { $, $c } from '../../moto/webui.js'; +import { $ } from '../../moto/webui.js'; import { api } from './api.js'; -import { base } from '../../geo/base.js'; -import { local as sdb } from '../../data/local.js'; -import { slider } from './slider.js'; -import { keyboard } from './keyboard.js'; -import { modal } from './modal.js'; -import { interact } from './interact.js'; -import { space } from '../../moto/space.js'; -import { LOCAL, SETUP } from './main.js'; -import { VIEWS, MODES, SEED } from '../core/consts.js'; import { beta, version } from '../../moto/license.js'; -import { settings as set_ctrl } from './config/manager.js'; -import STACKS from './stacks.js'; +import { fileOps } from './file-ops.js'; import { init as initCAM } from '../mode/cam/init-ui.js'; import { init as initDRAG } from '../mode/drag/init-ui.js'; import { init as initFDM } from '../mode/fdm/init-ui.js'; @@ -21,12 +11,27 @@ import { init as initLaser } from '../mode/laser/init-ui.js'; import { init as initSLA } from '../mode/sla/init-ui.js'; import { init as initWEDM } from '../mode/wedm/init-ui.js'; import { init as initWJET } from '../mode/wjet/init-ui.js'; +import { interact } from './interact.js'; +import { keyboard } from './keyboard.js'; +import { local as sdb } from '../../data/local.js'; import { menu as menuCAM } from '../mode/cam/init-menu.js'; import { menu as menuFDM } from '../mode/fdm/init-menu.js'; import { menu as menuLaser } from '../mode/laser/init-menu.js'; import { menu as menuSLA } from '../mode/sla/init-menu.js'; +import { modal } from './modal.js'; +import { navigation } from './navigation.js'; +import { preferences } from './preferences.js'; +import { selectionTools } from './selection-tools.js'; +import { settings as set_ctrl } from './config/manager.js'; +import { settingsOps } from './settings-ops.js'; +import { slider } from './slider.js'; +import { space } from '../../moto/space.js'; +import { VIEWS, MODES, SEED } from '../core/consts.js'; -let { CAM, SLA, FDM, LASER, DRAG, WJET, WEDM } = MODES, +import STACKS from './stacks.js'; + +let { LOCAL, SETUP } = api, + { CAM, SLA, FDM, LASER, DRAG, WJET, WEDM } = MODES, { client, catalog, platform, selection, stats } = api, LANG = api.language.current, WIN = self.window, @@ -80,135 +85,11 @@ function checkSeed(then) { 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 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 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; - 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.antiAlias = ui.antiAlias.checked; - control.assembly = ui.assembly.checked; - control.autoLayout = ui.autoLayout.checked; - control.autoSave = ui.autoSave.checked; - control.dark = ui.dark.checked; - control.devel = ui.devel.checked; - control.drawer = ui.drawer.checked; - control.exportOcto = ui.exportOcto.checked; - control.exportPreview = ui.exportPreview.checked; - control.exportThumb = ui.exportThumb.checked; - control.freeLayout = ui.freeLayout.checked; - control.healMesh = ui.healMesh.checked; - control.ortho = ui.ortho.checked; - control.manifold = ui.manifold.checked; - control.reverseZoom = ui.reverseZoom.checked; - control.scrolls = ui.scrolls.checked; - control.shiny = ui.shiny.checked; - control.showOrigin = ui.showOrigin.checked; - control.showRulers = ui.showRulers.checked; - control.spaceRandoX = ui.spaceRandoX.checked; - // control.threaded = setThreaded(ui.threaded.checked); - control.webGPU = ui.webGPU.checked; - space.view.setZoom(control.reverseZoom, control.zoomSpeed); - // platform.layout(); - api.conf.save(); - api.platform.update_size(); - updateStats(); - updateDrawer(); - api.event.emit('boolean.update'); - space.view.setProjection(control.ortho ? 'orthographic' : 'perspective'); -} - -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; - } - console.log('updateStats()'); - let { div, fps, rms, rnfo } = ui.stats; - div.style.display = 'flex'; - clearTimeout(statsTimer); - statsTimer = 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(); @@ -239,7 +120,6 @@ function onButtonClick(ev) { api.event.on('click.button', onButtonClick); - function keys(o) { let key, list = []; for (key in o) { if (o.hasOwnProperty(key)) list.push(key) } @@ -252,277 +132,6 @@ function clearSelected(children) { } } -function rotateInputSelection() { - if (selection.meshes().length === 0) { - api.show.alert("select object to rotate"); - return; - } - api.uc.prompt("Enter X,Y,Z degrees of rotation","").then(coord => { - 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 isBelt() { return api.device.isBelt(); } @@ -600,10 +209,10 @@ function init_one() { space.platform.onMove(conf.save); space.platform.setRound(true); space.useDefaultKeys(api.feature.on_key === undefined || api.feature.on_key_defaults); - updateDrawer(); + preferences.updateDrawer(); // api augmentation with local functions - api.device.export = deviceExport; + api.device.export = settingsOps.deviceExport; Object.assign(ui, { tracker: tracker, @@ -726,9 +335,9 @@ function init_one() { 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}), + bedWidth: newInput('X (width)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:settingsOps.updateDeviceSize}), + bedDepth: newInput('Y (depth)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:settingsOps.updateDeviceSize}), + maxHeight: newInput('Z (height)', {title:LANG.dv_bedw_l, convert:toFloat, size:6, units, round:2, action:settingsOps.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 }), @@ -786,39 +395,39 @@ function init_one() { /** 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}), + antiAlias: newBoolean(LANG.op_anta_s, preferences.booleanSave, {title:LANG.op_anta_l}), + reverseZoom: newBoolean(LANG.op_invr_s, preferences.booleanSave, {title:LANG.op_invr_l}), + ortho: newBoolean(LANG.op_orth_s, preferences.booleanSave, {title:LANG.op_orth_l}), + dark: newBoolean(LANG.op_dark_s, preferences.booleanSave, {title:LANG.op_dark_l}), + drawer: newBoolean('slide out', preferences.booleanSave, {title:'slide out settings drawer'}), + scrolls: newBoolean('scrollbars', preferences.booleanSave, {title:'show scrollbars'}), + devel: newBoolean(LANG.op_devl_s, preferences.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"), - manifold: newBoolean(LANG.op_mani_s, booleanSave, {title: LANG.op_mani_l, modes:CAM}, "manifold"), - 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"), + showOrigin: newBoolean(LANG.op_shor_s, preferences.booleanSave, {title:LANG.op_shor_l}), + showRulers: newBoolean(LANG.op_shru_s, preferences.booleanSave, {title:LANG.op_shru_l}), + showSpeeds: newBoolean(LANG.op_sped_s, preferences.speedSave, {title:LANG.op_sped_l}), + shiny: newBoolean(LANG.op_shny_s, preferences.booleanSave, {title:LANG.op_shny_l, modes:FDM}), + lineType: newSelect(LANG.op_line_s, {title: LANG.op_line_l, action: preferences.lineTypeSave, modes:FDM}, "linetype"), + manifold: newBoolean(LANG.op_mani_s, preferences.booleanSave, {title: LANG.op_mani_l, modes:CAM}, "manifold"), + animesh: newSelect(LANG.op_anim_s, {title: LANG.op_anim_l, action: preferences.aniMeshSave, modes:CAM}, "animesh"), + units: newSelect(LANG.op_unit_s, {title: LANG.op_unit_l, action: preferences.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}), + autoSave: newBoolean(LANG.op_save_s, preferences.booleanSave, {title:LANG.op_save_l}), + autoLayout: newBoolean(LANG.op_auto_s, preferences.booleanSave, {title:LANG.op_auto_l}), + freeLayout: newBoolean(LANG.op_free_s, preferences.booleanSave, {title:LANG.op_free_l}), + spaceRandoX: newBoolean(LANG.op_spcx_s, preferences.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}), - exportOcto: newBoolean(`OctoPrint`, booleanSave, {title:LANG.op_exop_l}), - exportThumb: newBoolean(`Thumbnail`, booleanSave, {modes:FDM}), - exportPreview: newBoolean(`Code Preview`, booleanSave), + exportOcto: newBoolean(`OctoPrint`, preferences.booleanSave, {title:LANG.op_exop_l}), + exportThumb: newBoolean(`Thumbnail`, preferences.booleanSave, {modes:FDM}), + exportPreview: newBoolean(`Code Preview`, preferences.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}), - webGPU: newBoolean(LANG.pt_wgpu_s, booleanSave, {title: LANG.pt_wgpu_l, modes:THREED}), + detail: newSelect(LANG.pt_qual_s, {title: LANG.pt_qual_l, action: preferences.detailSave}, "detail"), + healMesh: newBoolean(LANG.pt_heal_s, preferences.booleanSave, {title: LANG.pt_heal_l}), + // threaded: newBoolean(LANG.pt_thrd_s, preferences.booleanSave, {title: LANG.pt_thrd_l, modes:THREED}), + assembly: newBoolean(LANG.pt_assy_s, preferences.booleanSave, {title: LANG.pt_assy_l, modes:THREED}), + webGPU: newBoolean(LANG.pt_wgpu_s, preferences.booleanSave, {title: LANG.pt_wgpu_l, modes:THREED}), prefadd: uc.checkpoint($('prefs-add')), @@ -853,7 +462,7 @@ function init_one() { // override old style settings two-button menu ui.settingsSave.onclick = () => { - settingsSave(undefined, ui.settingsName.value); + settingsOps.settingsSave(undefined, ui.settingsName.value); }; // initialize modal controller @@ -918,8 +527,8 @@ function init_one() { VIEWS, DOC, WIN, - rotateInputSelection, - settingsLoad + rotateInputSelection: selectionTools.rotateInputSelection, + settingsLoad: settingsOps.settingsLoad }); // expose keyboard to API @@ -983,97 +592,13 @@ function init_one() { }; space.event.addHandlers(self, [ - 'dragover', dragOverHandler, - 'dragleave', dragLeave, - 'drop', dropHandler + 'dragover', fileOps.dragOverHandler, + 'dragleave', fileOps.dragLeave, + 'drop', fileOps.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 (xv * xr < 0.1 || yv * yr < 0.1 || zv * zr < 0.1) { - api.alerts.show('invalid scale value'); - 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); - // prevent null scale - if (xv * xr < 0.1 || yv * yr < 0.1 || zv * zr < 0.1) { - api.alerts.show('invalid scale value'); - return; - } - 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; - } - - // on enter but not on blur - space.event.onEnterKey([ - ui.scaleX, selectionScale, - ui.scaleY, selectionScale, - ui.scaleZ, selectionScale, - ui.sizeX, selectionSize, - ui.sizeY, selectionSize, - ui.sizeZ, selectionSize, - ]); - // on enter and blur - space.event.onEnterKey([ - 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); + // Setup selection transformation bindings + selectionTools.setupSelectionBindings(ui); $('lab-axis').onclick = () => { ui.lockX.checked = @@ -1222,7 +747,7 @@ function init_two() { } platform.deselect(); - catalog.addFileListener(updateCatalog); + catalog.addFileListener(fileOps.updateCatalog); space.view.setZoom(control.reverseZoom, control.zoomSpeed); space.platform.setGridZOff(undefined); space.platform.setZOff(0.05); @@ -1246,10 +771,10 @@ function init_two() { // ui.threaded.checked = setThreaded(control.threaded); ui.webGPU.checked = control.webGPU; - setThreaded(true); - lineTypeSave(); - detailSave(); - updateStats(); + preferences.setThreaded(true); + preferences.lineTypeSave(); + preferences.detailSave(); + preferences.updateStats(); // optional set-and-lock mode (hides mode menu) let SETMODE = SETUP.mode ? SETUP.mode[0] : null; @@ -1269,8 +794,8 @@ function init_two() { // default to ARRANGE view mode api.view.set(VIEWS.ARRANGE); - // add ability to override - api.show.controls(api.feature.controls); + // add ability to override (todo: restore?) + // api.show.controls(api.feature.controls); // update everything dependent on the platform size platform.update_size(); @@ -1324,80 +849,8 @@ function init_two() { sdb.gdpr = Date.now(); }; - // bind interface action elements - $('export-support-a').onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') }; - $('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; - $('mesh-split').onclick = selection.isolateBodies; - $('context-duplicate').onclick = selection.duplicate; - $('context-mirror').onclick = selection.mirror; - $('context-layflat').onclick = () => { api.event.emit("tool.mesh.lay-flat") }; - $('context-lefty').onclick = () => { api.event.emit("tool.mesh.lefty") }; - $('context-setfocus').onclick = () => { - api.event.emit( - "tool.camera.focus", - ev => api.space.set_focus(undefined, ev.object.point) - ); - }; - $('context-contents').onclick = () => { api.SPACE.view.fit() }; - $('view-fit').onclick = () => { api.SPACE.view.fit() }; - $('wassup').onmouseover = () => { $('suppopp').classList.remove('hide') }; + // Setup navigation button bindings + navigation.setupNavigation(ui, WIN, LANG); // ui.modal.onclick = api.modal.hide; ui.modalBox.onclick = (ev) => { ev.stopPropagation() }; diff --git a/src/kiri/ui/local-storage.js b/src/kiri/ui/local-storage.js new file mode 100644 index 00000000..9d5d3335 --- /dev/null +++ b/src/kiri/ui/local-storage.js @@ -0,0 +1,50 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +// Lazily access api to avoid circular dependency at module load time +let _api; + +function getAPI() { + if (!_api) { + _api = self.kiri_api; + } + return _api; +} + +function localGet(key) { + const api = getAPI(); + let sloc = api.conf.get().local; + return sloc[key] || api.sdb[key]; +} + +function localSet(key, val) { + const api = getAPI(); + let sloc = api.conf.get().local; + sloc[key] = api.sdb[key] = val; + return val; +} + +function localRemove(key) { + const api = getAPI(); + let sloc = api.conf.get().local; + return delete sloc[key]; +} + +function localGetBoolean(key, def = true) { + let val = localGet(key); + return val === true || val === 'true' || val === def; +} + +export const local = { + get: (key) => localGet(key), + getItem: (key) => localGet(key), + getInt: (key) => parseInt(localGet(key)), + getFloat: (key) => parseFloat(localGet(key)), + getBoolean: localGetBoolean, + toggle: (key, val, def) => { + return localSet(key, val ?? !localGetBoolean(key, def)); + }, + put: (key, val) => localSet(key, val), + set: (key, val) => localSet(key, val), + setItem: (key, val) => localSet(key, val), + removeItem: (key) => localRemove(key) +}; diff --git a/src/kiri/ui/main.js b/src/kiri/ui/main.js deleted file mode 100644 index 88a9ec58..00000000 --- a/src/kiri/ui/main.js +++ /dev/null @@ -1,270 +0,0 @@ -/** Copyright Stewart Allen -- All Rights Reserved */ - -import '../../ext/base64.js'; - -import { modal } from './modal.js'; - -import { $ } from '../../moto/webui.js'; -import { api } from './api.js'; -import { broker } from '../../moto/broker.js'; -import { Index } from '../../data/index.js'; -import { openFiles } from './files.js'; -import { settings } from './config/manager.js'; -import { showDevices } from './devices.js'; -import { noop, utils } from '../core/utils.js'; -import { version } from '../../moto/license.js'; -import { showTools } from '../mode/cam/tools.js'; - -// Import new modules -import { workspace } from './workspace.js'; -import { mode as modeModule, process as processModule } from './mode.js'; -import { help as helpModule } from './help.js'; -import { group as groupModule } from './groups.js'; -import { image as imageModule } from './image.js'; -import { view as viewModule } from './view-state.js'; -import { visuals } from './visuals.js'; - -let { parseOpt } = utils, - 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, - FILES = openFiles(new Index(SETUP.d ? SETUP.d[0] : 'kiri')); - -// allow widget to straddle client / worker FOR NOW -self.kiri_catalog = FILES; - -export const dialog = { - show: (which) => modal.show(which), - hide: () => modal.hide(), - update_process_list: updateProcessList -}; - -// Re-export help from new module -export const help = helpModule; - -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 -}; - -// Re-export group from new module -export const group = groupModule; - -export const hide = { - alert(rec, recs) { api.alerts.hide(...arguments) }, - import: noop, - slider: visuals.hide_slider -}; - -// Re-export image from new module -export const image = imageModule; - -// Re-export modal singleton methods -export { modal }; - -// Re-export mode and process from new module -export const mode = modeModule; -export const process = processModule; - -export const show = { - alert() { return api.alerts.show(...arguments) }, - controls() { console.trace('deprecated') }, - devices: showDevices, - import() { api.ui.import.style.display = '' }, - layer: visuals.set_visible_layer, - local() { console.trace('deprecated') }, - progress: setProgress, - slices: visuals.show_slices, - tools: showTools -}; - -// Re-export space (workspace) from new module -export const space = workspace; - -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))) } -}; - -// Combine view-state and visuals modules -export const view = { - ...viewModule, - ...visuals, - snapshot: null -}; - -// add show() to catalog for API -FILES.show = () => modal.show('files'); - -// patch broker for api backward compatibility -EVENT.on = (topic, listener) => { - EVENT.subscribe(topic, listener); - return EVENT; -}; - -/** ****************************************************************** - * Utility Functions - ******************************************************************* */ - -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 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 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[settings.mode()] || {}, 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(); - modal.hide(); - } - 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 downloadBlob(data, filename) { - let url = WIN.URL.createObjectURL(new Blob([data], {type: "octet/stream"})); - $('mod-any').innerHTML = `x`; - $('_dexport_').click(); -} - -export { FILES as catalog, LOCAL, SETUP, SECURE }; - -// Import frame.js last to avoid circular dependency issues -// import './frame.js'; diff --git a/src/kiri/ui/navigation.js b/src/kiri/ui/navigation.js new file mode 100644 index 00000000..b25b56ea --- /dev/null +++ b/src/kiri/ui/navigation.js @@ -0,0 +1,89 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $ } from '../../moto/webui.js'; +import { api } from './api.js'; +import { space } from '../../moto/space.js'; +import { settingsOps } from './settings-ops.js'; + +const { selection } = api; + +function setupNavigation(ui, WIN, LANG) { + // bind interface action elements + $('export-support-a').onclick = (ev) => { ev.stopPropagation(); api.modal.show('don8') }; + $('mode-device').onclick = api.show.devices; + $('mode-profile').onclick = settingsOps.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(); settingsOps.profileExport() }; + ui.acct.export.title = LANG.acct_xpo; + $('file-new').onclick = (ev) => { ev.stopPropagation(); settingsOps.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 = () => { settingsOps.objectsExport('stl') }; + $('mesh-export-obj').onclick = () => { settingsOps.objectsExport('obj') }; + $('mesh-merge').onclick = selection.merge; + $('mesh-split').onclick = selection.isolateBodies; + $('context-duplicate').onclick = selection.duplicate; + $('context-mirror').onclick = selection.mirror; + $('context-layflat').onclick = () => { api.event.emit("tool.mesh.lay-flat") }; + $('context-lefty').onclick = () => { api.event.emit("tool.mesh.lefty") }; + $('context-setfocus').onclick = () => { + api.event.emit( + "tool.camera.focus", + ev => api.space.set_focus(undefined, ev.object.point) + ); + }; + $('context-contents').onclick = () => { api.SPACE.view.fit() }; + $('view-fit').onclick = () => { api.SPACE.view.fit() }; + $('wassup').onmouseover = () => { $('suppopp').classList.remove('hide') }; +} + +export const navigation = { + setupNavigation +}; diff --git a/src/kiri/ui/preferences.js b/src/kiri/ui/preferences.js new file mode 100644 index 00000000..c3e3cc01 --- /dev/null +++ b/src/kiri/ui/preferences.js @@ -0,0 +1,160 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $c } from '../../moto/webui.js'; +import { api } from './api.js'; +import { base } from '../../geo/base.js'; +import { space } from '../../moto/space.js'; + +let { client, platform } = api; +let ui, statsTimer; + +// Lazy initialization to avoid circular dependencies +function getUI() { + if (!ui) ui = api.ui; + return ui; +} + +function settings() { + return api.conf.get(); +} + +function unitsSave() { + api.conf.update({ controller: true }); + platform.update_size(); +} + +function aniMeshSave() { + api.conf.update({ controller: true }); + api.conf.save(); +} + +function lineTypeSave() { + const ui = getUI(); + const sel = ui.lineType.options[ui.lineType.selectedIndex]; + if (sel) { + settings().controller.lineType = sel.value; + api.conf.save(); + } +} + +function detailSave() { + const ui = getUI(); + 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() { + const ui = getUI(); + settings().controller.showSpeeds = ui.showSpeeds.checked; + api.view.update_speeds(); +} + +function setThreaded(bool) { + if (bool) { + client.pool.start(); + } else { + client.pool.stop(); + } + return bool; +} + +function booleanSave() { + const ui = getUI(); + let control = settings().controller; + 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.antiAlias = ui.antiAlias.checked; + control.assembly = ui.assembly.checked; + control.autoLayout = ui.autoLayout.checked; + control.autoSave = ui.autoSave.checked; + control.dark = ui.dark.checked; + control.devel = ui.devel.checked; + control.drawer = ui.drawer.checked; + control.exportOcto = ui.exportOcto.checked; + control.exportPreview = ui.exportPreview.checked; + control.exportThumb = ui.exportThumb.checked; + control.freeLayout = ui.freeLayout.checked; + control.healMesh = ui.healMesh.checked; + control.ortho = ui.ortho.checked; + control.manifold = ui.manifold.checked; + control.reverseZoom = ui.reverseZoom.checked; + control.scrolls = ui.scrolls.checked; + control.shiny = ui.shiny.checked; + control.showOrigin = ui.showOrigin.checked; + control.showRulers = ui.showRulers.checked; + control.spaceRandoX = ui.spaceRandoX.checked; + // control.threaded = setThreaded(ui.threaded.checked); + control.webGPU = ui.webGPU.checked; + space.view.setZoom(control.reverseZoom, control.zoomSpeed); + // platform.layout(); + api.conf.save(); + api.platform.update_size(); + updateStats(); + updateDrawer(); + api.event.emit('boolean.update'); + space.view.setProjection(control.ortho ? 'orthographic' : 'perspective'); +} + +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; + } + console.log('updateStats()'); + const ui = getUI(); + let { div, fps, rms, rnfo } = ui.stats; + div.style.display = 'flex'; + clearTimeout(statsTimer); + statsTimer = 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); +} + +export const preferences = { + unitsSave, + aniMeshSave, + lineTypeSave, + detailSave, + speedSave, + setThreaded, + booleanSave, + updateDrawer, + updateStats +}; diff --git a/src/kiri/ui/selection-tools.js b/src/kiri/ui/selection-tools.js new file mode 100644 index 00000000..bcb21e1d --- /dev/null +++ b/src/kiri/ui/selection-tools.js @@ -0,0 +1,161 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $ } from '../../moto/webui.js'; +import { api } from './api.js'; +import { space } from '../../moto/space.js'; +import { updateTool } from '../mode/cam/tools.js'; + +const { selection } = api; + +function settings() { + return api.conf.get(); +} + +function boundsSelection() { + // Helper to compute bounds of selected meshes + const THREE = self.THREE; + let bounds = new THREE.Box3(); + selection.for_meshes(mesh => { + let box = mesh.getBoundingBox().clone(); + let pos = mesh.widget.mesh.position; + box.min.add(pos); + box.max.add(pos); + bounds = bounds.union(box); + }); + return bounds; +} + +function rotateInputSelection() { + if (selection.meshes().length === 0) { + api.show.alert("select object to rotate"); + return; + } + api.uc.prompt("Enter X,Y,Z degrees of rotation","").then(coord => { + 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 selectionSize(e, ui) { + 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 (xv * xr < 0.1 || yv * yr < 0.1 || zv * zr < 0.1) { + api.alerts.show('invalid scale value'); + 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, ui) { + 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); + // prevent null scale + if (xv * xr < 0.1 || yv * yr < 0.1 || zv * zr < 0.1) { + api.alerts.show('invalid scale value'); + return; + } + 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; +} + +function setupSelectionBindings(ui) { + // on enter but not on blur + space.event.onEnterKey([ + ui.scaleX, (e) => selectionScale(e, ui), + ui.scaleY, (e) => selectionScale(e, ui), + ui.scaleZ, (e) => selectionScale(e, ui), + ui.sizeX, (e) => selectionSize(e, ui), + ui.sizeY, (e) => selectionSize(e, ui), + ui.sizeZ, (e) => selectionSize(e, ui), + ]); + // on enter and blur + space.event.onEnterKey([ + 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); +} + +export const selectionTools = { + rotateInputSelection, + positionSelection, + setupSelectionBindings +}; diff --git a/src/kiri/ui/settings-ops.js b/src/kiri/ui/settings-ops.js new file mode 100644 index 00000000..e32b1cfd --- /dev/null +++ b/src/kiri/ui/settings-ops.js @@ -0,0 +1,138 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $ } from '../../moto/webui.js'; +import { api } from './api.js'; +import { settings as set_ctrl } from './config/manager.js'; + +function settings() { + return api.conf.get(); +} + +function deviceExport(exp, name) { + name = (name || "device") + .toLowerCase() + .replace(/ /g,'_') + .replace(/\./g,'_'); + api.uc.prompt("Export Device Filename", name).then(name => { + if (name) { + api.util.download(exp, `${name}.km`); + } + }); +} + +function objectsExport(format = "stl") { + // return selection.export(); + api.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(api.selection.export(format), name); + }); +} + +function workspaceNew() { + api.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(' ','_'); + }; + api.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}); + + api.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 { + api.uc.prompt("Save Settings As", cp ? lp || def : def).then(saveAs); + } +} + +function settingsLoad() { + api.conf.show(); +} + +function updateDeviceSize() { + api.conf.update(); + api.platform.update_size(); + api.platform.update_origin(); +} + +async function sync_put() { + await set_ctrl.sync.put(); +} + +export const settingsOps = { + deviceExport, + objectsExport, + workspaceNew, + profileExport, + settingsSave, + settingsLoad, + updateDeviceSize, + sync_put +}; diff --git a/src/kiri/ui/util.js b/src/kiri/ui/util.js new file mode 100644 index 00000000..1d022cc1 --- /dev/null +++ b/src/kiri/ui/util.js @@ -0,0 +1,52 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { $ } from '../../moto/webui.js'; +import { api } from './api.js'; + +const WIN = self.window; + +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 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 downloadBlob(data, filename) { + let url = WIN.URL.createObjectURL(new Blob([data], {type: "octet/stream"})); + $('mod-any').innerHTML = `x`; + $('_dexport_').click(); +} + +export const util = { + isSecure, + download: downloadBlob, + setProgress, + loadFile +};