start ui code re-organization
This commit is contained in:
parent
5dbe906629
commit
a752c18855
28 changed files with 36 additions and 37 deletions
|
|
@ -1,75 +0,0 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- 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 => ['<p>',v[0],'</p>'].join('')).join('');
|
||||
ui.alert.dialog.style.display = 'flex';
|
||||
} else {
|
||||
ui.alert.dialog.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
hide,
|
||||
show,
|
||||
update
|
||||
};
|
||||
|
||||
export default {
|
||||
hide,
|
||||
show,
|
||||
update
|
||||
};
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import alerts from './alerts.js';
|
||||
import alerts from '../ui/alerts.js';
|
||||
import settings from './settings.js'
|
||||
import STACKS from './stacks.js';
|
||||
import STACKS from '../ui/stacks.js';
|
||||
|
||||
import { broker } from '../../moto/broker.js';
|
||||
import { client as work } from './client.js';
|
||||
|
|
@ -15,7 +15,7 @@ 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 './stats.js';
|
||||
import { stats } from '../ui/stats.js';
|
||||
import { newWidget } from './widget.js';
|
||||
import { widgets } from './widgets.js';
|
||||
import { updateTool } from '../mode/cam/tools.js';
|
||||
|
|
@ -23,9 +23,9 @@ import { beta, version } from '../../moto/license.js';
|
|||
import { space as SPACE } from '../../moto/space.js';
|
||||
import { types as load } from '../../load/file.js';
|
||||
|
||||
import { LANG } from './lang.js';
|
||||
import { LANG } from '../ui/lang.js';
|
||||
import { LOCAL, SETUP, SECURE } from './main.js';
|
||||
import { UI } from './ui.js';
|
||||
import { UI } from '../ui/component.js';
|
||||
|
||||
import web from '../../moto/webui.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { Layers } from './layers.js';
|
||||
import { Layers } from '../ui/layers.js';
|
||||
import { newPoint } from '../../geo/point.js';
|
||||
import { Polygon, newPolygon } from '../../geo/polygon.js';
|
||||
import { Slice, newSlice, Top, newTop } from './slice.js';
|
||||
|
|
|
|||
|
|
@ -1,172 +0,0 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { api } from './api.js';
|
||||
import { space } from '../../moto/space.js';
|
||||
|
||||
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() === api.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};
|
||||
});
|
||||
|
|
@ -1,175 +0,0 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- 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<store.listeners.length; i++) {
|
||||
store.listeners[i](store.files);
|
||||
}
|
||||
}
|
||||
|
||||
export const openFiles = function(indexdb) {
|
||||
return new Files(indexdb);
|
||||
};
|
||||
|
||||
export { saveFileList, notifyFileListeners };
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { api } from './api.js';
|
||||
import { load } from '../../load/file.js';
|
||||
import { newWidget } from './widget.js';
|
||||
import { VIEWS } from './consts.js';
|
||||
import { widgets } from './widgets.js';
|
||||
|
||||
// add frame message api listener
|
||||
window.addEventListener('message', msg => {
|
||||
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;
|
||||
case "widgets": send({widgets: widgets.all().map(w => {
|
||||
return {
|
||||
id: w.id,
|
||||
meta: w.meta,
|
||||
track: w.track,
|
||||
}
|
||||
})}); break;
|
||||
default: send({all: settings}); break;
|
||||
|
||||
}
|
||||
|
||||
if (data.set) switch (data.set) {
|
||||
case "":
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
|
@ -4,15 +4,16 @@ import { $, $c } 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 { slider } from '../ui/slider.js';
|
||||
import { keyboard } from '../ui/keyboard.js';
|
||||
import { modal } from '../ui/modal.js';
|
||||
import { interact } from '../ui/interact.js';
|
||||
import { space } from '../../moto/space.js';
|
||||
import { LOCAL, SETUP } from './main.js';
|
||||
import { VIEWS, MODES, SEED } from './consts.js';
|
||||
import { beta, version } from '../../moto/license.js';
|
||||
import { settings as set_ctrl } from './settings.js';
|
||||
import STACKS from '../ui/stacks.js';
|
||||
import { init as initCAM } from '../mode/cam/init-ui.js';
|
||||
import { init as initDRAG } from '../mode/drag/init-ui.js';
|
||||
import { init as initFDM } from '../mode/fdm/init-ui.js';
|
||||
|
|
@ -24,8 +25,6 @@ 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 STACKS from './stacks.js';
|
||||
import './tools.js';
|
||||
|
||||
let { CAM, SLA, FDM, LASER, DRAG, WJET, WEDM } = MODES,
|
||||
{ client, catalog, platform, selection, stats } = api,
|
||||
|
|
|
|||
|
|
@ -1,192 +0,0 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
/**
|
||||
* Interaction Controller (Singleton)
|
||||
* Manages mouse hover, click, and drag interactions with 3D objects
|
||||
*/
|
||||
|
||||
class InteractionControl {
|
||||
#api;
|
||||
#space;
|
||||
#platform;
|
||||
#selection;
|
||||
#ui;
|
||||
#settings;
|
||||
#VIEWS;
|
||||
#initialized = false;
|
||||
|
||||
// Callbacks to replace event emissions
|
||||
#onHover;
|
||||
#onHoverDown;
|
||||
#onHoverUp;
|
||||
#onDragDone;
|
||||
#onSelectionDrag;
|
||||
|
||||
/**
|
||||
* Initialize the interaction controller with dependencies
|
||||
* @param {Object} config
|
||||
* @param {Object} config.api - API object
|
||||
* @param {Object} config.space - Space object (3D scene)
|
||||
* @param {Object} config.platform - Platform object
|
||||
* @param {Object} config.selection - Selection object
|
||||
* @param {Object} config.ui - UI element references
|
||||
* @param {Function} config.settings - Settings getter function
|
||||
* @param {Object} config.VIEWS - View constants
|
||||
* @param {Function} config.onHover - Called when hovering over objects
|
||||
* @param {Function} config.onHoverDown - Called on mouse down over object
|
||||
* @param {Function} config.onHoverUp - Called on mouse up over object
|
||||
* @param {Function} config.onDragDone - Called when drag is complete
|
||||
* @param {Function} config.onSelectionDrag - Called during selection drag
|
||||
*/
|
||||
init({ api, space, platform, selection, ui, settings, VIEWS, onHover, onHoverDown, onHoverUp, onDragDone, onSelectionDrag }) {
|
||||
if (this.#initialized) {
|
||||
throw new Error('Interactions already initialized');
|
||||
}
|
||||
|
||||
this.#api = api;
|
||||
this.#space = space;
|
||||
this.#platform = platform;
|
||||
this.#selection = selection;
|
||||
this.#ui = ui;
|
||||
this.#settings = settings;
|
||||
this.#VIEWS = VIEWS;
|
||||
this.#onHover = onHover;
|
||||
this.#onHoverDown = onHoverDown;
|
||||
this.#onHoverUp = onHoverUp;
|
||||
this.#onDragDone = onDragDone;
|
||||
this.#onSelectionDrag = onSelectionDrag;
|
||||
|
||||
this.#bindHoverHandlers();
|
||||
this.#bindMouseHandlers();
|
||||
this.#initialized = true;
|
||||
}
|
||||
|
||||
#bindHoverHandlers() {
|
||||
// Set up hover handlers
|
||||
this.#api.event.on("feature.hover", enable => {
|
||||
this.#space.mouse.onHover(enable ? this.#mouseOnHover.bind(this) : undefined);
|
||||
this.#space.platform.onHover(enable ? this.#platformOnHover.bind(this) : undefined);
|
||||
});
|
||||
}
|
||||
|
||||
#mouseOnHover(int, event, ints) {
|
||||
if (!this.#api.feature.hover) return;
|
||||
if (!int) return this.#api.feature.hovers || this.#api.widgets.meshes();
|
||||
this.#onHover?.({int, ints, event, point: int.point, type: 'widget'});
|
||||
}
|
||||
|
||||
#platformOnHover(int, event) {
|
||||
if (!this.#api.feature.hover) return;
|
||||
if (int) this.#onHover?.({point: int, event, type: 'platform'});
|
||||
}
|
||||
|
||||
#bindMouseHandlers() {
|
||||
// Mouse down handler
|
||||
this.#space.mouse.downSelect((int, event) => {
|
||||
// Feature hook for custom mouse down handling
|
||||
if (this.#api.feature.on_mouse_down) {
|
||||
if (int) {
|
||||
this.#api.feature.on_mouse_down(int, event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Hover mode handling
|
||||
if (this.#api.feature.hover) {
|
||||
if (int) {
|
||||
this.#onHoverDown?.({int, point: int.point});
|
||||
return;
|
||||
} else {
|
||||
return this.#selection.meshes();
|
||||
}
|
||||
}
|
||||
|
||||
// Lay flat with meta or ctrl clicking a selected face
|
||||
if (int && (event.ctrlKey || event.metaKey || this.#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));
|
||||
this.#selection.rotate(q);
|
||||
}
|
||||
|
||||
if (this.#api.view.get() !== this.#VIEWS.ARRANGE) {
|
||||
// return no selection in modes other than arrange
|
||||
return null;
|
||||
} else {
|
||||
// return selected meshes for further mouse processing
|
||||
return this.#api.feature.hovers || this.#selection.meshes();
|
||||
}
|
||||
});
|
||||
|
||||
// Mouse up handler
|
||||
this.#space.mouse.upSelect((object, event) => {
|
||||
// Feature hook for custom mouse up handling
|
||||
if (this.#api.feature.on_mouse_up) {
|
||||
if (event && object) {
|
||||
return this.#api.feature.on_mouse_up(object, event);
|
||||
} else {
|
||||
return this.#api.widgets.meshes();
|
||||
}
|
||||
}
|
||||
|
||||
// Hover mode handling
|
||||
if (event && this.#api.feature.hover) {
|
||||
this.#onHoverUp?.({ object, event });
|
||||
return;
|
||||
}
|
||||
|
||||
// Regular selection handling
|
||||
if (event && event.target.nodeName === "CANVAS") {
|
||||
if (object && object.object) {
|
||||
if (object.object.widget) {
|
||||
this.#platform.select(object.object.widget, event.shiftKey, false);
|
||||
}
|
||||
} else {
|
||||
this.#platform.deselect();
|
||||
}
|
||||
} else {
|
||||
return this.#api.feature.hovers || this.#api.widgets.meshes();
|
||||
}
|
||||
});
|
||||
|
||||
// Drag handler
|
||||
this.#space.mouse.onDrag((delta, offset, up = false) => {
|
||||
if (this.#api.feature.hover) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (up) {
|
||||
this.#onDragDone?.(offset);
|
||||
}
|
||||
|
||||
if (delta && this.#ui.freeLayout.checked) {
|
||||
let set = this.#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;
|
||||
}
|
||||
|
||||
this.#selection.move(delta.x, delta.y, 0);
|
||||
this.#onSelectionDrag?.(delta);
|
||||
} else {
|
||||
return this.#selection.meshes().length > 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const interact = new InteractionControl();
|
||||
|
|
@ -1,398 +0,0 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
/**
|
||||
* Keyboard Controller (Singleton)
|
||||
* Manages keyboard event handling and shortcuts
|
||||
*/
|
||||
|
||||
class KeyboardControl {
|
||||
#api;
|
||||
#platform;
|
||||
#selection;
|
||||
#slider;
|
||||
#space;
|
||||
#catalog;
|
||||
#sdb;
|
||||
#uc;
|
||||
#setCtrl;
|
||||
#VIEWS;
|
||||
#DOC;
|
||||
#WIN;
|
||||
#rotateInputSelection;
|
||||
#settingsLoad;
|
||||
#initialized = false;
|
||||
|
||||
/**
|
||||
* Initialize the keyboard controller with dependencies
|
||||
* @param {Object} config
|
||||
* @param {Object} config.api - API object
|
||||
* @param {Object} config.platform - Platform object
|
||||
* @param {Object} config.selection - Selection object
|
||||
* @param {Object} config.slider - Slider controller
|
||||
* @param {Object} config.space - Space object
|
||||
* @param {Object} config.catalog - Catalog object
|
||||
* @param {Object} config.sdb - Local storage
|
||||
* @param {Object} config.uc - User confirmation utility
|
||||
* @param {Object} config.setCtrl - Settings controller
|
||||
* @param {Object} config.VIEWS - View constants
|
||||
* @param {Document} config.DOC - Document object
|
||||
* @param {Window} config.WIN - Window object
|
||||
* @param {Function} config.rotateInputSelection - Rotate input callback
|
||||
* @param {Function} config.settingsLoad - Settings load callback
|
||||
*/
|
||||
init({ api, platform, selection, slider, space, catalog, sdb, uc, setCtrl, VIEWS, DOC, WIN, rotateInputSelection, settingsLoad }) {
|
||||
if (this.#initialized) {
|
||||
throw new Error('Keyboard already initialized');
|
||||
}
|
||||
|
||||
this.#api = api;
|
||||
this.#platform = platform;
|
||||
this.#selection = selection;
|
||||
this.#slider = slider;
|
||||
this.#space = space;
|
||||
this.#catalog = catalog;
|
||||
this.#sdb = sdb;
|
||||
this.#uc = uc;
|
||||
this.#setCtrl = setCtrl;
|
||||
this.#VIEWS = VIEWS;
|
||||
this.#DOC = DOC;
|
||||
this.#WIN = WIN;
|
||||
this.#rotateInputSelection = rotateInputSelection;
|
||||
this.#settingsLoad = settingsLoad;
|
||||
|
||||
this.#bindEvents();
|
||||
this.#initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an input element has focus
|
||||
* @returns {boolean}
|
||||
*/
|
||||
inputHasFocus() {
|
||||
let active = this.#DOC.activeElement;
|
||||
return active && (active.nodeName === "INPUT" || active.nodeName === "TEXTAREA");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get character code at position 0
|
||||
* @param {string} c - Character
|
||||
* @returns {number}
|
||||
*/
|
||||
#cca(c) {
|
||||
return c.charCodeAt(0);
|
||||
}
|
||||
|
||||
#bindEvents() {
|
||||
this.#space.event.addHandlers(self, [
|
||||
'keyup', this.#handleKeyUp.bind(this),
|
||||
'keydown', this.#handleKeyDown.bind(this),
|
||||
'keypress', this.#handleKeyPress.bind(this)
|
||||
]);
|
||||
}
|
||||
|
||||
#handleKeyUp(evt) {
|
||||
// Allow feature hooks to intercept
|
||||
if (this.#api.feature.on_key) {
|
||||
if (this.#api.feature.on_key({up:evt})) return;
|
||||
}
|
||||
for (let handler of this.#api.feature.on_key2) {
|
||||
if (handler({up:evt})) return;
|
||||
}
|
||||
|
||||
switch (evt.keyCode) {
|
||||
// escape
|
||||
case 27:
|
||||
// blur text input focus
|
||||
this.#DOC.activeElement.blur();
|
||||
// dismiss modals
|
||||
this.#api.modal.hide();
|
||||
// deselect widgets
|
||||
this.#platform.deselect();
|
||||
// hide all dialogs
|
||||
this.#api.dialog.hide();
|
||||
// cancel slicing
|
||||
this.#api.function.cancel();
|
||||
// trigger escape handlers (used by FDM mode for support editing)
|
||||
this.#onEscape();
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#handleKeyDown(evt) {
|
||||
if (this.#api.modal.visible()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allow feature hooks to intercept
|
||||
if (this.#api.feature.on_key) {
|
||||
if (this.#api.feature.on_key({down:evt})) return;
|
||||
}
|
||||
for (let handler of this.#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 (this.inputHasFocus()) return false;
|
||||
this.#platform.delete(this.#selection.meshes());
|
||||
evt.preventDefault();
|
||||
break;
|
||||
case 37: // left arrow
|
||||
if (this.inputHasFocus()) return false;
|
||||
if (deg) this.#selection.rotate(0, 0, -deg);
|
||||
if (move > 0) this.#selection.move(-move, 0, 0);
|
||||
evt.preventDefault();
|
||||
break;
|
||||
case 39: // right arrow
|
||||
if (this.inputHasFocus()) return false;
|
||||
if (deg) this.#selection.rotate(0, 0, deg);
|
||||
if (move > 0) this.#selection.move(move, 0, 0);
|
||||
evt.preventDefault();
|
||||
break;
|
||||
case 38: // up arrow
|
||||
if (this.inputHasFocus()) return false;
|
||||
if (evt.metaKey) {
|
||||
const { hi } = this.#slider.getRange();
|
||||
return this.#api.show.layer(hi + 1);
|
||||
}
|
||||
if (deg) this.#selection.rotate(deg, 0, 0);
|
||||
if (move > 0) this.#selection.move(0, move, 0);
|
||||
evt.preventDefault();
|
||||
break;
|
||||
case 40: // down arrow
|
||||
if (this.inputHasFocus()) return false;
|
||||
if (evt.metaKey) {
|
||||
const { hi } = this.#slider.getRange();
|
||||
return this.#api.show.layer(hi - 1);
|
||||
}
|
||||
if (deg) this.#selection.rotate(-deg, 0, 0);
|
||||
if (move > 0) this.#selection.move(0, -move, 0);
|
||||
evt.preventDefault();
|
||||
break;
|
||||
case 65: // 'a' for select all
|
||||
if (evt.metaKey || evt.ctrlKey) {
|
||||
if (this.inputHasFocus()) return false;
|
||||
evt.preventDefault();
|
||||
this.#platform.deselect();
|
||||
this.#platform.select_all();
|
||||
}
|
||||
break;
|
||||
case 83: // 's' for save workspace
|
||||
if (evt.ctrlKey) {
|
||||
evt.preventDefault();
|
||||
this.#api.conf.save();
|
||||
console.log("settings saved");
|
||||
} else
|
||||
if (evt.metaKey) {
|
||||
evt.preventDefault();
|
||||
this.#api.space.save();
|
||||
this.#setCtrl.sync.put();
|
||||
}
|
||||
break;
|
||||
case 76: // 'l' for restore workspace
|
||||
if (evt.metaKey) {
|
||||
evt.preventDefault();
|
||||
this.#api.space.restore();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#handleKeyPress(evt) {
|
||||
let handled = true;
|
||||
if (this.#api.modal.visible() || this.inputHasFocus()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allow feature hooks to intercept
|
||||
if (this.#api.feature.on_key) {
|
||||
if (this.#api.feature.on_key({key:evt})) return;
|
||||
}
|
||||
for (let handler of this.#api.feature.on_key2) {
|
||||
if (handler({key:evt})) return;
|
||||
}
|
||||
|
||||
// Handle Ctrl key combinations
|
||||
if (evt.ctrlKey) {
|
||||
switch (evt.key) {
|
||||
case 'g': return this.#api.group.merge();
|
||||
case 'u': return this.#api.group.split();
|
||||
}
|
||||
}
|
||||
|
||||
switch (evt.charCode) {
|
||||
case this.#cca('`'): this.#api.show.slices(0); break;
|
||||
case this.#cca('0'): {
|
||||
const { max } = this.#slider.getRange();
|
||||
this.#api.show.slices(max);
|
||||
break;
|
||||
}
|
||||
case this.#cca('1'): {
|
||||
const { max } = this.#slider.getRange();
|
||||
this.#api.show.slices(max/10);
|
||||
break;
|
||||
}
|
||||
case this.#cca('2'): {
|
||||
const { max } = this.#slider.getRange();
|
||||
this.#api.show.slices(max*2/10);
|
||||
break;
|
||||
}
|
||||
case this.#cca('3'): {
|
||||
const { max } = this.#slider.getRange();
|
||||
this.#api.show.slices(max*3/10);
|
||||
break;
|
||||
}
|
||||
case this.#cca('4'): {
|
||||
const { max } = this.#slider.getRange();
|
||||
this.#api.show.slices(max*4/10);
|
||||
break;
|
||||
}
|
||||
case this.#cca('5'): {
|
||||
const { max } = this.#slider.getRange();
|
||||
this.#api.show.slices(max*5/10);
|
||||
break;
|
||||
}
|
||||
case this.#cca('6'): {
|
||||
const { max } = this.#slider.getRange();
|
||||
this.#api.show.slices(max*6/10);
|
||||
break;
|
||||
}
|
||||
case this.#cca('7'): {
|
||||
const { max } = this.#slider.getRange();
|
||||
this.#api.show.slices(max*7/10);
|
||||
break;
|
||||
}
|
||||
case this.#cca('8'): {
|
||||
const { max } = this.#slider.getRange();
|
||||
this.#api.show.slices(max*8/10);
|
||||
break;
|
||||
}
|
||||
case this.#cca('9'): {
|
||||
const { max } = this.#slider.getRange();
|
||||
this.#api.show.slices(max*9/10);
|
||||
break;
|
||||
}
|
||||
case this.#cca('?'):
|
||||
this.#api.help.show();
|
||||
break;
|
||||
case this.#cca('Z'): // reset stored state
|
||||
this.#uc.confirm('clear all settings and preferences?').then(yes => {
|
||||
if (yes) {
|
||||
this.#sdb.clear();
|
||||
this.#WIN.location.reload();
|
||||
}
|
||||
});
|
||||
break;
|
||||
case this.#cca('C'): // refresh catalog
|
||||
this.#catalog.refresh();
|
||||
break;
|
||||
case this.#cca('i'): // file import
|
||||
this.#api.event.import();
|
||||
break;
|
||||
case this.#cca('S'): // slice
|
||||
case this.#cca('s'): // slice
|
||||
this.#api.function.slice();
|
||||
break;
|
||||
case this.#cca('P'): // prepare
|
||||
case this.#cca('p'): // prepare
|
||||
if (this.#api.mode.get() !== 'SLA') {
|
||||
// hidden in SLA mode
|
||||
this.#api.function.prepare();
|
||||
}
|
||||
break;
|
||||
case this.#cca('X'): // export
|
||||
case this.#cca('x'): // export
|
||||
this.#api.function.export();
|
||||
break;
|
||||
case this.#cca('g'): // CAM animate
|
||||
this.#api.function.animate();
|
||||
break;
|
||||
case this.#cca('O'): // manual rotation
|
||||
this.#rotateInputSelection();
|
||||
break;
|
||||
case this.#cca('r'): // recent files
|
||||
this.#api.modal.show('files');
|
||||
break;
|
||||
case this.#cca('q'): // preferences
|
||||
this.#api.modal.show('prefs');
|
||||
break;
|
||||
case this.#cca('l'): // device
|
||||
this.#settingsLoad();
|
||||
break;
|
||||
case this.#cca('e'): // device
|
||||
this.#api.show.devices();
|
||||
break;
|
||||
case this.#cca('o'): // tools
|
||||
this.#api.show.tools();
|
||||
break;
|
||||
case this.#cca('c'): // local devices
|
||||
this.#api.show.local();
|
||||
break;
|
||||
case this.#cca('v'): // toggle single slice view mode
|
||||
if (this.#api.view.get() === this.#VIEWS.ARRANGE) {
|
||||
this.#api.space.set_focus(this.#selection.widgets());
|
||||
}
|
||||
const { lo, hi } = this.#slider.getRange();
|
||||
if (hi === lo) {
|
||||
this.#slider.setRange(0, hi);
|
||||
} else {
|
||||
this.#slider.setRange(hi, hi);
|
||||
}
|
||||
this.#api.show.slices();
|
||||
break;
|
||||
case this.#cca('d'): // duplicate object
|
||||
this.#selection.duplicate();
|
||||
break;
|
||||
case this.#cca('m'): // mirror object
|
||||
this.#selection.mirror();
|
||||
break;
|
||||
case this.#cca('a'):
|
||||
if (this.#api.view.get() === this.#VIEWS.ARRANGE) {
|
||||
// auto arrange items on platform
|
||||
this.#platform.layout();
|
||||
if (!this.#api.conf.get().controller.spaceRandoX) {
|
||||
this.#api.space.set_focus(this.#selection.widgets());
|
||||
}
|
||||
} else {
|
||||
// go to arrange view
|
||||
this.#api.view.set(this.#VIEWS.ARRANGE);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
this.#onUnhandledKey(evt);
|
||||
handled = false;
|
||||
break;
|
||||
}
|
||||
if (handled) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when escape key is pressed
|
||||
* Replaces api.event.emit('key.esc')
|
||||
*/
|
||||
#onEscape() {
|
||||
// Notify listeners (currently used by FDM mode for support editing)
|
||||
this.#api.event.emit("key.esc");
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an unhandled key is pressed
|
||||
* Replaces api.event.emit('keypress', evt)
|
||||
*/
|
||||
#onUnhandledKey(evt) {
|
||||
// Notify listeners for custom key handling
|
||||
this.#api.event.emit('keypress', evt);
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const keyboard = new KeyboardControl();
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- 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<keys.length; i++)
|
||||
{
|
||||
key = keys[i]
|
||||
if (!(map = LANG[key])) {
|
||||
continue;
|
||||
}
|
||||
lset = key;
|
||||
let missing = [];
|
||||
let invalid = [];
|
||||
// default to EN values when missing
|
||||
Object.keys(LANG.en).forEach(key => {
|
||||
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 };
|
||||
|
|
@ -1,268 +0,0 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { newPolygon, Polygon } 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<lines.length-1; i += 2) {
|
||||
polys.push(new Polygon()
|
||||
.append(lines[i])
|
||||
.append(lines[i+1])
|
||||
.setOpen());
|
||||
}
|
||||
return this.addPolys(polys, options);
|
||||
}
|
||||
for (let i=0; i<lines.length-1; i += 2) {
|
||||
this.addLine(lines[i], lines[i+1]);
|
||||
this.stats.line++;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
// an open or closed polygon
|
||||
addPoly(poly, options) {
|
||||
return this.addPolys([ poly ], options);
|
||||
}
|
||||
|
||||
// a polygon rendered as a webgl line
|
||||
addPolys(polys, options) {
|
||||
if (!polys) {
|
||||
return this;
|
||||
}
|
||||
if (!Array.isArray(polys)) {
|
||||
throw "polys must be an array";
|
||||
}
|
||||
if (polys.length === 0) {
|
||||
return this;
|
||||
}
|
||||
if (options && options.clean) {
|
||||
polys = polys.map(p => 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, 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import './frame.js';
|
||||
import '../ui/frame.js';
|
||||
import '../../ext/base64.js';
|
||||
|
||||
import STACKS from './stacks.js';
|
||||
import { modal } from './modal.js';
|
||||
import STACKS from '../ui/stacks.js';
|
||||
import { modal } from '../ui/modal.js';
|
||||
|
||||
import { $ } from '../../moto/webui.js';
|
||||
import { api } from './api.js';
|
||||
|
|
@ -12,13 +12,13 @@ import { broker } from '../../moto/broker.js';
|
|||
import { consts } from './consts.js';
|
||||
import { Index } from '../../data/index.js';
|
||||
import { local as dataLocal } from '../../data/local.js';
|
||||
import { openFiles } from './files.js';
|
||||
import { openFiles } from '../ui/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 './stats.js';
|
||||
import { stats } from '../ui/stats.js';
|
||||
import { noop, utils } from './utils.js';
|
||||
import { version } from '../../moto/license.js';
|
||||
import { Widget, newWidget } from './widget.js';
|
||||
|
|
|
|||
|
|
@ -1,100 +0,0 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
/**
|
||||
* Modal Manager (Singleton)
|
||||
* Manages modal dialog display and animations
|
||||
*/
|
||||
|
||||
class ModalControl {
|
||||
#ui;
|
||||
#onShow;
|
||||
#onHide;
|
||||
#initialized = false;
|
||||
|
||||
/**
|
||||
* Initialize the modal controller with dependencies
|
||||
* @param {Object} config
|
||||
* @param {Object} config.ui - UI element references
|
||||
* @param {HTMLElement} config.ui.modal - Modal container element
|
||||
* @param {Object} config.ui.modals - Object containing all modal elements by name
|
||||
* @param {Function} config.onShow - Optional callback when modal is shown
|
||||
* @param {Function} config.onHide - Optional callback when modal is hidden
|
||||
*/
|
||||
init({ ui, onShow, onHide }) {
|
||||
if (this.#initialized) {
|
||||
throw new Error('Modal already initialized');
|
||||
}
|
||||
|
||||
this.#ui = ui;
|
||||
this.#onShow = onShow;
|
||||
this.#onHide = onHide;
|
||||
this.#initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if modal is currently visible
|
||||
* @returns {boolean}
|
||||
*/
|
||||
visible() {
|
||||
return this.#ui.modal.style.display === 'flex';
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a specific modal with animation
|
||||
* @param {string} which - Name of the modal to show
|
||||
*/
|
||||
show(which) {
|
||||
let mod = this.#ui.modal,
|
||||
style = mod.style,
|
||||
visible = this.visible(),
|
||||
info = { pct: 0 };
|
||||
|
||||
// hide all modals before showing another
|
||||
Object.keys(this.#ui.modals).forEach(name => {
|
||||
this.#ui.modals[name].style.display = name === which ? 'flex' : '';
|
||||
});
|
||||
|
||||
const ondone = () => {
|
||||
this.#onShow?.(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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the modal with animation
|
||||
*/
|
||||
hide() {
|
||||
if (!this.visible()) {
|
||||
return;
|
||||
}
|
||||
let mod = this.#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 = '';
|
||||
this.#onHide?.();
|
||||
}).
|
||||
start();
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const modal = new ModalControl();
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import { base } from '../../geo/base.js';
|
|||
import { load } from '../../load/file.js';
|
||||
import { space } from '../../moto/space.js';
|
||||
import { Widget, newWidget } from './widget.js';
|
||||
import { Packer } from './pack.js';
|
||||
import { Packer } from '../ui/pack.js';
|
||||
|
||||
import { COLOR, MODES } from './consts.js';
|
||||
import { THREE } from '../../ext/three.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { Layers } from './layers.js';
|
||||
import { Layers } from '../ui/layers.js';
|
||||
import { newPoint } from '../../geo/point.js';
|
||||
import { newPolygon } from '../../geo/polygon.js';
|
||||
import { newSlopeFromAngle } from '../../geo/slope.js';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { Layers } from './layers.js';
|
||||
import { Layers } from '../ui/layers.js';
|
||||
import { newPoint } from '../../geo/point.js';
|
||||
import { polygons as POLY } from '../../geo/polygons.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,305 +0,0 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
/**
|
||||
* Layer Slider Controller (Singleton)
|
||||
* Manages layer range selection and visualization for slice/preview views
|
||||
*/
|
||||
|
||||
class SliderControl {
|
||||
#layerLo = 0;
|
||||
#layerHi = 0;
|
||||
#layerMax = 0;
|
||||
#ui;
|
||||
#tracker;
|
||||
#mobile;
|
||||
#sliderBar;
|
||||
#sliderBar2;
|
||||
#drag = {};
|
||||
|
||||
// Callbacks to replace event emissions
|
||||
#onLayerChange;
|
||||
#onStackUpdate;
|
||||
#onSceneUpdate;
|
||||
|
||||
#initialized = false;
|
||||
|
||||
/**
|
||||
* Initialize the slider with configuration
|
||||
* @param {Object} config
|
||||
* @param {Object} config.ui - UI element references
|
||||
* @param {HTMLElement} config.tracker - Drag tracker element
|
||||
* @param {boolean} config.mobile - Mobile device flag
|
||||
* @param {Function} config.onLayerChange - Called when layer range changes
|
||||
* @param {Function} config.onStackUpdate - Called to update stack visibility
|
||||
* @param {Function} config.onSceneUpdate - Called to refresh scene
|
||||
*/
|
||||
init({ ui, tracker, mobile, onLayerChange, onStackUpdate, onSceneUpdate }) {
|
||||
if (this.#initialized) {
|
||||
throw new Error('Slider already initialized');
|
||||
}
|
||||
|
||||
this.#ui = ui;
|
||||
this.#tracker = tracker;
|
||||
this.#mobile = mobile;
|
||||
this.#sliderBar = mobile ? 80 : 30;
|
||||
this.#sliderBar2 = this.#sliderBar * 2;
|
||||
this.#onLayerChange = onLayerChange;
|
||||
this.#onStackUpdate = onStackUpdate;
|
||||
this.#onSceneUpdate = onSceneUpdate;
|
||||
|
||||
this.#setupUI();
|
||||
this.#bindEvents();
|
||||
this.#initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current layer range
|
||||
* @returns {{lo: number, hi: number, max: number}}
|
||||
*/
|
||||
getRange() {
|
||||
return {
|
||||
lo: this.#layerLo,
|
||||
hi: this.#layerHi,
|
||||
max: this.#layerMax
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set layer range (bounds are enforced)
|
||||
* @param {number} lo - Lower layer
|
||||
* @param {number} hi - Upper layer
|
||||
* @param {boolean} notify - Whether to trigger callbacks (default: true)
|
||||
*/
|
||||
setRange(lo, hi, notify = true) {
|
||||
this.#layerLo = Math.max(0, Math.min(hi, lo));
|
||||
this.#layerHi = Math.max(this.#layerLo, Math.min(this.#layerMax, hi));
|
||||
this.#updateVisuals();
|
||||
this.showLabels();
|
||||
if (notify) {
|
||||
this.#notifyChange();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set maximum layer count
|
||||
* @param {number} max - Maximum layer value
|
||||
*/
|
||||
setMax(max) {
|
||||
this.#layerMax = max;
|
||||
this.#ui.sliderMax.innerText = max;
|
||||
if (max < this.#layerHi) {
|
||||
this.#layerHi = max;
|
||||
this.showLabels();
|
||||
this.#updateVisuals();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update slider visual position from normalized values
|
||||
* @param {number} startPercent - Start position (0-1)
|
||||
* @param {number} endPercent - End position (0-1)
|
||||
*/
|
||||
updatePosition(startPercent, endPercent) {
|
||||
const width = this.#ui.sliderRange.clientWidth;
|
||||
const maxval = width - this.#sliderBar2;
|
||||
const start = Math.max(0, Math.min(1, startPercent));
|
||||
const end = Math.max(start, Math.min(1, endPercent));
|
||||
|
||||
const lowval = start * maxval;
|
||||
const midval = ((end - start) * maxval) + this.#sliderBar;
|
||||
const hival = maxval - end * maxval;
|
||||
|
||||
this.#ui.sliderHold.style.marginLeft = `${lowval}px`;
|
||||
this.#ui.sliderMid.style.width = `${midval}px`;
|
||||
this.#ui.sliderHold.style.marginRight = `${hival}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show layer number labels
|
||||
*/
|
||||
showLabels() {
|
||||
const digits = this.#layerMax.toString().length;
|
||||
this.#ui.sliderZero.style.width = `${digits}em`;
|
||||
this.#ui.sliderMax.style.width = `${digits}em`;
|
||||
this.#ui.sliderZero.innerText = this.#layerLo;
|
||||
this.#ui.sliderMax.innerText = this.#layerHi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show slider UI
|
||||
*/
|
||||
show() {
|
||||
this.#ui.layers.style.display = 'flex';
|
||||
this.#ui.slider.style.display = 'flex';
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide slider UI
|
||||
*/
|
||||
hide() {
|
||||
this.#ui.layers.style.display = 'none';
|
||||
this.#ui.slider.style.display = 'none';
|
||||
}
|
||||
|
||||
#setupUI() {
|
||||
if (this.#mobile) {
|
||||
this.#ui.slider.classList.add('slider-mobile');
|
||||
this.#ui.sliderLo.classList.add('slider-mobile');
|
||||
this.#ui.sliderHi.classList.add('slider-mobile');
|
||||
}
|
||||
}
|
||||
|
||||
#bindEvents() {
|
||||
// Min/Max buttons
|
||||
this.#ui.sliderMin.onclick = () => {
|
||||
this.#layerLo = 0;
|
||||
this.#layerHi = 0;
|
||||
this.showLabels();
|
||||
this.#updateVisuals();
|
||||
this.#notifyChange();
|
||||
};
|
||||
|
||||
this.#ui.sliderMax.onclick = () => {
|
||||
this.#layerLo = this.#layerMax;
|
||||
this.#layerHi = this.#layerMax;
|
||||
this.showLabels();
|
||||
this.#updateVisuals();
|
||||
this.#notifyChange();
|
||||
};
|
||||
|
||||
// Mouse hover for labels
|
||||
this.#ui.slider.onmouseover = () => {
|
||||
this.showLabels();
|
||||
};
|
||||
|
||||
this.#ui.slider.onmouseleave = (ev) => {
|
||||
if (!ev.buttons) {
|
||||
this.hideLabels();
|
||||
}
|
||||
};
|
||||
|
||||
// Drag handlers for the three slider sections
|
||||
this.#bindDrag(this.#ui.sliderLo, this.#handleLoDrag.bind(this));
|
||||
this.#bindDrag(this.#ui.sliderMid, this.#handleMidDrag.bind(this));
|
||||
this.#bindDrag(this.#ui.sliderHi, this.#handleHiDrag.bind(this));
|
||||
}
|
||||
|
||||
#bindDrag(el, deltaHandler) {
|
||||
const slider = this.#ui.sliderRange;
|
||||
|
||||
el.ontouchstart = el.onmousedown = (ev) => {
|
||||
this.#tracker.style.display = 'block';
|
||||
ev.stopPropagation();
|
||||
|
||||
let obj = (ev.touches ? ev.touches[0] : ev);
|
||||
this.#drag.width = slider.clientWidth;
|
||||
this.#drag.maxval = this.#drag.width - this.#sliderBar2;
|
||||
this.#drag.start = obj.screenX;
|
||||
this.#drag.loat = this.#drag.low = this.#pxToInt(this.#ui.sliderHold.style.marginLeft);
|
||||
this.#drag.mdat = this.#drag.mid = this.#ui.sliderMid.clientWidth;
|
||||
this.#drag.hiat = this.#pxToInt(this.#ui.sliderHold.style.marginRight);
|
||||
this.#drag.mdmax = this.#drag.width - this.#sliderBar - this.#drag.loat;
|
||||
this.#drag.himax = this.#drag.width - this.#sliderBar - this.#drag.mdat;
|
||||
|
||||
const cancelDrag = this.#tracker.ontouchend = this.#tracker.onmouseup = (ev) => {
|
||||
if (ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
slider.onmousemove = undefined;
|
||||
this.#tracker.style.display = 'none';
|
||||
};
|
||||
|
||||
el.ontouchend = cancelDrag;
|
||||
el.ontouchmove = this.#tracker.ontouchmove = this.#tracker.onmousemove = (ev) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
if (ev.buttons === 0) {
|
||||
return cancelDrag();
|
||||
}
|
||||
if (deltaHandler) {
|
||||
let obj = (ev.touches ? ev.touches[0] : ev);
|
||||
deltaHandler(obj.screenX - this.#drag.start);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
#handleLoDrag(delta) {
|
||||
let midval = this.#drag.mdat - delta;
|
||||
let lowval = this.#drag.loat + delta;
|
||||
|
||||
if (midval < this.#sliderBar || lowval < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#ui.sliderHold.style.marginLeft = `${lowval}px`;
|
||||
this.#ui.sliderMid.style.width = `${midval}px`;
|
||||
this.#drag.low = lowval;
|
||||
this.#drag.mid = midval;
|
||||
this.#updateFromDrag();
|
||||
}
|
||||
|
||||
#handleMidDrag(delta) {
|
||||
let loval = this.#drag.loat + delta;
|
||||
let hival = this.#drag.hiat - delta;
|
||||
|
||||
if (loval < 0 || hival < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#ui.sliderHold.style.marginLeft = `${loval}px`;
|
||||
this.#ui.sliderHold.style.marginRight = `${hival}px`;
|
||||
this.#drag.low = loval;
|
||||
this.#updateFromDrag();
|
||||
}
|
||||
|
||||
#handleHiDrag(delta) {
|
||||
let midval = this.#drag.mdat + delta;
|
||||
let hival = this.#drag.hiat - delta;
|
||||
|
||||
if (midval < this.#sliderBar || midval > this.#drag.mdmax || hival < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#ui.sliderMid.style.width = `${midval}px`;
|
||||
this.#ui.sliderHold.style.marginRight = `${hival}px`;
|
||||
this.#drag.mid = midval;
|
||||
this.#updateFromDrag();
|
||||
}
|
||||
|
||||
#updateFromDrag() {
|
||||
const start = this.#drag.low / this.#drag.maxval;
|
||||
const end = (this.#drag.low + this.#drag.mid - this.#sliderBar) / this.#drag.maxval;
|
||||
|
||||
this.#layerLo = Math.round(start * this.#layerMax);
|
||||
this.#layerHi = Math.round(end * this.#layerMax);
|
||||
|
||||
this.#notifyChange();
|
||||
}
|
||||
|
||||
#updateVisuals() {
|
||||
if (this.#layerMax === 0) return;
|
||||
const start = this.#layerLo / this.#layerMax;
|
||||
const end = this.#layerHi / this.#layerMax;
|
||||
this.updatePosition(start, end);
|
||||
}
|
||||
|
||||
#notifyChange() {
|
||||
// Notify callbacks of layer change
|
||||
this.#onLayerChange?.(this.#layerHi, this.#layerLo);
|
||||
this.#onStackUpdate?.(this.#layerLo, this.#layerHi);
|
||||
this.#onSceneUpdate?.();
|
||||
}
|
||||
|
||||
#pxToInt(txt) {
|
||||
return txt ? parseInt(txt.substring(0, txt.length - 2)) : 0;
|
||||
}
|
||||
|
||||
hideLabels() {
|
||||
// Labels hide themselves on mouse leave, but can be called explicitly
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const slider = new SliderControl();
|
||||
|
|
@ -1,295 +0,0 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- 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<len; i++) {
|
||||
s[i].visible = 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<len; i += 3) {
|
||||
vertices.push(new THREE.Vector3(points[i-3], points[i-2], points[i-1]));
|
||||
vertices.push(new THREE.Vector3(points[i+0], points[i+1], points[i+2]));
|
||||
}
|
||||
if (!poly.open) {
|
||||
vertices.push(new THREE.Vector3(points[0], points[1], points[2]));
|
||||
vertices.push(new THREE.Vector3(points[len-3], points[len-2], points[len-1]));
|
||||
}
|
||||
}
|
||||
|
||||
const { polys, lines, faces, cface, paths, cpath } = layer;
|
||||
const { color, off, norms, rotation, position } = layer;
|
||||
const { fat, order, opacity } = color;
|
||||
const meshes = [];
|
||||
const defstate = !off;
|
||||
const mats = [];
|
||||
mats.state = defstate;
|
||||
|
||||
if (polys.length || lines.length) {
|
||||
const vert = []; // vertexes
|
||||
const mat = []; // materials
|
||||
const grp = []; // material groups
|
||||
const geo = fat ? new THREE.LineSegmentsGeometry() : new THREE.BufferGeometry();
|
||||
// map all the poly and line colors for re-use
|
||||
const cmap = {}
|
||||
let cidx = 0;
|
||||
let last = undefined;
|
||||
for (let i=0, il=polys.length; i<il; i++) {
|
||||
const vl = vert.length;
|
||||
const poly = polys[i];
|
||||
addPoly(vert, poly);
|
||||
const pc = poly.color !== undefined ? { line: poly.color, opacity } : color;
|
||||
const pk = pc.line;
|
||||
const cc = cmap[pk] = cmap[pk] || {
|
||||
idx: cidx++,
|
||||
mat: createLineMaterial(pc, mat)
|
||||
};
|
||||
if (last !== pk) {
|
||||
if (grp.length) {
|
||||
// rewrite counts for last group
|
||||
const prev = grp[grp.length - 1]
|
||||
prev[1] = vl;
|
||||
}
|
||||
grp.push([vl, Infinity, cc.idx]);
|
||||
last = pk;
|
||||
}
|
||||
}
|
||||
// for now, line segments inherit the last color
|
||||
for (let i=0, il=lines.length; i<il; i += 3) {
|
||||
vert.push(new THREE.Vector3(lines[i], lines[i+1], lines[i+2]));
|
||||
}
|
||||
// ensure at least one group using the default color settings
|
||||
if (grp.length === 0) {
|
||||
grp.push([0, Infinity, 0]);
|
||||
mat.push(createLineMaterial(color));
|
||||
}
|
||||
mat.forEach(m => m.visible = defstate);
|
||||
for (let i=0; i<grp.length; i++) {
|
||||
const g = grp[i];
|
||||
geo.addGroup(g[0], g[1], g[2]);
|
||||
}
|
||||
if (fat) {
|
||||
// LineSegmentsGeometry does not support setFromPoints()
|
||||
geo.setPositions(vert.map(v => 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
|
||||
});
|
||||
}
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- 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
|
||||
};
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- 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 };
|
||||
|
|
@ -1,190 +0,0 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { api } from './api.js';
|
||||
import { THREE } from '../../ext/three.js';
|
||||
import { space } from '../../moto/space.js';
|
||||
|
||||
const { Vector3, Quaternion } = THREE;
|
||||
|
||||
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<pi.length; i++) {
|
||||
if (pi[i] * pi[i+1]) {
|
||||
np.push(pp[pi[i]*3+0]);
|
||||
np.push(pp[pi[i]*3+1]);
|
||||
np.push(pp[pi[i]*3+2]);
|
||||
np.push(pp[pi[i+1]*3+0]);
|
||||
np.push(pp[pi[i+1]*3+1]);
|
||||
np.push(pp[pi[i+1]*3+2]);
|
||||
}
|
||||
}
|
||||
|
||||
let wgeo = new THREE.BufferGeometry();
|
||||
wgeo.setAttribute('position', new THREE.BufferAttribute(np.toFloat32(), 3));
|
||||
|
||||
let wmat = new THREE.LineBasicMaterial({ color: 0x6699cc });
|
||||
let wmesh = new THREE.LineSegments(wgeo, wmat);
|
||||
pmesh.add(wmesh);
|
||||
|
||||
let alert;
|
||||
let lastobj;
|
||||
let lastface;
|
||||
let enabled = false;
|
||||
let opName = 'lay flat';
|
||||
let onDone = onLayFlatSelect;
|
||||
|
||||
function onLayFlatSelect() {
|
||||
let q = new Quaternion().setFromUnitVectors(lastface.normal, new Vector3(0,0,-1));
|
||||
api.selection.rotate(q);
|
||||
// endIt();
|
||||
}
|
||||
|
||||
function onLeftySelect() {
|
||||
// Project the face normal onto the XY plane (ignore Z component)
|
||||
let projectedNormal = new Vector3(lastface.normal.x, lastface.normal.y, 0).normalize();
|
||||
|
||||
// Target direction on XY plane (negative Y axis)
|
||||
let targetDir = new Vector3(-1, 0, 0);
|
||||
|
||||
// Calculate angle between projected normal and target
|
||||
let angle = Math.atan2(
|
||||
projectedNormal.x * targetDir.y - projectedNormal.y * targetDir.x, // cross product Z component
|
||||
projectedNormal.x * targetDir.x + projectedNormal.y * targetDir.y // dot product
|
||||
);
|
||||
|
||||
// Create quaternion for Z-axis rotation only
|
||||
let q = new Quaternion().setFromAxisAngle(ZAXIS, angle);
|
||||
api.selection.rotate(q);
|
||||
}
|
||||
|
||||
function onFaceUpSelect() {
|
||||
api.event.emit('tool.mesh.face-normal', lastface.normal);
|
||||
// endIt();
|
||||
}
|
||||
|
||||
function startIt() {
|
||||
if (enabled) {
|
||||
endIt();
|
||||
return;
|
||||
}
|
||||
if (api.feature.hover) {
|
||||
console.log(`${opName} cannot pre-empt hover`);
|
||||
return;
|
||||
}
|
||||
api.feature.hover = true;
|
||||
enabled = true;
|
||||
alert = api.show.alert(`[ESC] to end ${opName}`, 600000);
|
||||
}
|
||||
|
||||
function endIt() {
|
||||
if (enabled) {
|
||||
api.hide.alert(alert);
|
||||
api.feature.hover = false;
|
||||
enabled = false;
|
||||
alert = undefined;
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
if (lastobj) {
|
||||
lastobj.remove(pmesh);
|
||||
lastobj = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
api.event.on('key.esc', endIt);
|
||||
|
||||
api.event.on('tool.camera.focus', (fn) => {
|
||||
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();
|
||||
});
|
||||
|
||||
api.event.on('tool.mesh.lefty', () => {
|
||||
opName = 'align y axis';
|
||||
onDone = onLeftySelect;
|
||||
startIt();
|
||||
});
|
||||
|
||||
function scale() {
|
||||
let cam = 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 };
|
||||
|
|
@ -1,879 +0,0 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- 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,
|
||||
toFloatArray,
|
||||
toDegsFloat,
|
||||
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 = [
|
||||
`<div class="confirm f-col a-stretch" style="padding:5px !important">`
|
||||
];
|
||||
if (message) {
|
||||
html.push(`<label style="user-select:text">${message}</label>`);
|
||||
}
|
||||
if (opt.pre) {
|
||||
html = opt.pre.appendAll(html);
|
||||
}
|
||||
let iid;
|
||||
if (Array.isArray(input)) {
|
||||
iid = `confirm-input-${rnd}`;
|
||||
html.append(`<div><textarea rows="15" cols="40" class="grow" type="text" spellcheck="false" id="${iid}"></textarea></div>`);
|
||||
} else if (input !== undefined) {
|
||||
iid = `confirm-input-${rnd}`;
|
||||
html.append(`<div><input class="grow" type="text" spellcheck="false" id="${iid}"/></div>`);
|
||||
}
|
||||
html.append(`<div class="f-row j-end">`);
|
||||
Object.entries(btns).forEach((row,i) => {
|
||||
html.append(`<button id="confirm-${i}-${rnd}">${row[0]}</button>`);
|
||||
});
|
||||
html.append(`</div></div>`);
|
||||
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 = '<i class="fa-solid fa-caret-down"></i>';
|
||||
row.classList.add('hidden');
|
||||
} else {
|
||||
arr.innerHTML = '<i class="fa-solid fa-caret-up"></i>';
|
||||
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);
|
||||
this.value = nv;
|
||||
if (this.setv) {
|
||||
return this.setv(nv * units);
|
||||
}
|
||||
return nv;
|
||||
}
|
||||
|
||||
function toFloatArray() {
|
||||
let nv = this.value !== '' ? this.value.split(',').map(v => parseFloat(v)) : null;
|
||||
console.log({ toFloatArray: nv });
|
||||
if (this.bound) nv = nv.map(v => this.bound(v));
|
||||
this.array = nv;
|
||||
return nv;
|
||||
}
|
||||
|
||||
function toDegsFloat(){
|
||||
let nv = this.value !== '' ? parseFloat(this.value) : null;
|
||||
if (nv !== null && this.bound) nv = this.bound(nv);
|
||||
nv = (nv+360)% 360; // bound the val to 0-359.99
|
||||
this.value = nv;
|
||||
if (this.setv) {
|
||||
return this.setv(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);
|
||||
if (opt.content) div.innerHTML = opt.content;
|
||||
if (opt.group !== false) {
|
||||
lastGroup?.push(div);
|
||||
div._group = groupName;
|
||||
} else {
|
||||
lastGroup = undefined;
|
||||
groupName = undefined;
|
||||
}
|
||||
return div;
|
||||
}
|
||||
|
||||
function newExpand(label, opt = {}) {
|
||||
let div = DOC.createElement('details');
|
||||
div.setAttribute('class', opt.class || 'f-col');
|
||||
if (opt.open) div.setAttribute('open', true);
|
||||
addModeControls(div, opt);
|
||||
|
||||
let summary = DOC.createElement('summary');
|
||||
summary.setAttribute('class', opt.class || 'var-row');
|
||||
summary.innerHTML = `<label>${label}</label>`;
|
||||
|
||||
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,
|
||||
action = opt.action || bindTo || inputAction,
|
||||
ip = height > 1 ? DOC.createElement('textarea') : DOC.createElement('input');
|
||||
|
||||
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.id) ip.setAttribute("id", opt.id);
|
||||
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;
|
||||
}
|
||||
// console.log({ value }, input.real, input.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.id) ip.setAttribute("id", options.id);
|
||||
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();
|
||||
}
|
||||
if (options.post) {
|
||||
options.post();
|
||||
}
|
||||
};
|
||||
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;
|
||||
}
|
||||
Loading…
Reference in a new issue