more shuffling of ui files
This commit is contained in:
parent
b1d4750ed1
commit
b7f7d87563
39 changed files with 72 additions and 72 deletions
|
|
@ -1,200 +0,0 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import alerts from '../ui/alerts.js';
|
||||
import settings from '../ui/config/manager.js';
|
||||
import STACKS from '../ui/stacks.js';
|
||||
|
||||
import { broker } from '../../moto/broker.js';
|
||||
import { client as work } from '../ui/client.js';
|
||||
import { consts, COLOR as color, LISTS as lists } from './consts.js';
|
||||
import { device, devices } from '../ui/devices.js';
|
||||
import { catalog, dialog, event, group, help, hide, image } from './main.js';
|
||||
import { modal, mode, process, show, space, util, view } from './main.js';
|
||||
import { local as dataLocal } from '../../data/local.js';
|
||||
import { noop, ajax, o2js, js2o } from './utils.js';
|
||||
import { functions } from './function.js';
|
||||
import { platform } from './platform.js';
|
||||
import { selection } from './selection.js';
|
||||
import { stats } from '../ui/stats.js';
|
||||
import { newWidget } from './widget.js';
|
||||
import { widgets } from './widgets.js';
|
||||
import { updateTool } from '../mode/cam/tools.js';
|
||||
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 '../ui/lang.js';
|
||||
import { LOCAL, SETUP, SECURE } from './main.js';
|
||||
import { UI } from '../ui/component.js';
|
||||
|
||||
import web from '../../moto/webui.js';
|
||||
|
||||
let UC = UI.prefix('kiri').inputAction(settings.conf.update),
|
||||
und = undefined,
|
||||
clone = Object.clone,
|
||||
isHover = false,
|
||||
feature = {
|
||||
seed: true, // seed profiles on first use
|
||||
meta: true, // show selected widget metadata
|
||||
frame: true, // receive frame events
|
||||
alert_event: false, // emit alerts as events instead of display
|
||||
controls: true, // show or not side menus
|
||||
device_filter: und, // function to limit devices shown
|
||||
drop_group: und, // optional array to group multi drop
|
||||
drop_layout: true, // layout on new drop
|
||||
hoverAdds: false, // when true only searches widget additions
|
||||
on_key: und, // function override default key handlers
|
||||
on_key2: [], // allows for multiple key handlers
|
||||
on_load: und, // function override file drop loads
|
||||
on_add_stl: und, // legacy override stl drop loads
|
||||
on_mouse_up: und, // function intercepts mouse up select
|
||||
on_mouse_down: und, // function intercepts mouse down
|
||||
work_alerts: true, // allow disabling work progress alerts
|
||||
pmode: consts.PMODES.SPEED, // preview modes
|
||||
// hover: false, // when true fires mouse hover events
|
||||
get hover() {
|
||||
return isHover;
|
||||
},
|
||||
set hover(b) {
|
||||
isHover = b;
|
||||
broker.publish("feature.hover", b);
|
||||
}
|
||||
},
|
||||
busyVal = 0,
|
||||
busy = {
|
||||
val() { return busyVal },
|
||||
inc() { api.event.emit("busy", ++busyVal) },
|
||||
dec() { api.event.emit("busy", --busyVal) }
|
||||
},
|
||||
onkey = (fn) => {
|
||||
api.feature.on_key2.push(fn);
|
||||
},
|
||||
doit = {
|
||||
undo: noop, // do.js
|
||||
redo: noop // do.js
|
||||
},
|
||||
devel = {
|
||||
get enabled() {
|
||||
return settings.ctrl().devel;
|
||||
},
|
||||
xray(layers, raw) {
|
||||
let proc = api.conf.get().process,
|
||||
size = proc.sliceHeight || proc.slaSlice || 1,
|
||||
base = (proc.firstSliceHeight || size);
|
||||
layers = Array.isArray(layers) ? layers : [layers];
|
||||
proc.xray = layers.map(l => raw ? l : base + l * size - size / 2);
|
||||
proc.xrayi = layers.slice();
|
||||
api.function.slice();
|
||||
}
|
||||
},
|
||||
local = {
|
||||
get: (key) => localGet(key),
|
||||
getItem: (key) => localGet(key),
|
||||
getInt: (key) => parseInt(localGet(key)),
|
||||
getFloat: (key) => parseFloat(localGet(key)),
|
||||
getBoolean: (key, def = true) => {
|
||||
let val = localGet(key);
|
||||
return val === true || val === 'true' || val === def;
|
||||
},
|
||||
toggle: (key, val, def) => localSet(key, val ?? !api.local.getBoolean(key, def)),
|
||||
put: (key, val) => localSet(key, val),
|
||||
set: (key, val) => localSet(key, val),
|
||||
setItem: (key, val) => localSet(key, val),
|
||||
removeItem: (key) => localRemove(key)
|
||||
},
|
||||
tweak = {
|
||||
line_precision(v) { api.work.config({ base: { clipperClean: v } }) },
|
||||
gcode_decimals(v) { api.work.config({ base: { gcode_decimals: v } }) }
|
||||
};
|
||||
|
||||
function clip(text) {
|
||||
navigator.clipboard
|
||||
.writeText(text)
|
||||
.catch(err => console.error('Clipboard Error:', err));
|
||||
}
|
||||
|
||||
function localGet(key) {
|
||||
let sloc = api.conf.get().local;
|
||||
return sloc[key] || api.sdb[key];
|
||||
}
|
||||
|
||||
function localSet(key, val) {
|
||||
let sloc = api.conf.get().local;
|
||||
sloc[key] = api.sdb[key] = val;
|
||||
return val;
|
||||
}
|
||||
|
||||
function localRemove(key) {
|
||||
let sloc = api.conf.get().local;
|
||||
return delete sloc[key];
|
||||
}
|
||||
|
||||
export const api = {
|
||||
ajax,
|
||||
beta,
|
||||
alerts,
|
||||
busy,
|
||||
catalog,
|
||||
client: work,
|
||||
clip,
|
||||
clone,
|
||||
color,
|
||||
conf: settings.conf,
|
||||
const: { LANG, LOCAL, SETUP, SECURE, STACKS, ...consts },
|
||||
devel,
|
||||
device,
|
||||
devices,
|
||||
dialog,
|
||||
doit,
|
||||
event,
|
||||
electron: navigator.userAgent.includes('Electron'),
|
||||
feature,
|
||||
function: functions,
|
||||
group,
|
||||
help,
|
||||
hide,
|
||||
image,
|
||||
js2o,
|
||||
language: LANG,
|
||||
lists,
|
||||
load,
|
||||
local,
|
||||
modal,
|
||||
mode,
|
||||
new: {
|
||||
widget: newWidget
|
||||
},
|
||||
noop,
|
||||
o2js,
|
||||
onkey,
|
||||
platform,
|
||||
process,
|
||||
sdb: dataLocal,
|
||||
selection,
|
||||
settings,
|
||||
show,
|
||||
space,
|
||||
SPACE,
|
||||
stacks: STACKS,
|
||||
stats,
|
||||
tool: {
|
||||
update: updateTool
|
||||
},
|
||||
tweak,
|
||||
uc: UC,
|
||||
ui: {},
|
||||
util,
|
||||
var: {
|
||||
layer_lo: 0,
|
||||
layer_hi: 0,
|
||||
layer_max: 0
|
||||
},
|
||||
version,
|
||||
view,
|
||||
web,
|
||||
widgets,
|
||||
work,
|
||||
};
|
||||
|
||||
// allow widget to straddle client / worker FOR NOW
|
||||
self.kiri_api = api;
|
||||
|
|
@ -1,508 +0,0 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { api } from './api.js';
|
||||
import { client } from '../ui/client.js';
|
||||
import { codec } from './codec.js';
|
||||
import { space } from '../../moto/space.js';
|
||||
import { COLOR, PMODES } from './consts.js';
|
||||
import { exportFile } from '../ui/export.js';
|
||||
|
||||
let complete = {};
|
||||
|
||||
function prepareSlices(callback, scale = 1, offset = 0) {
|
||||
const { conf, event, feature, hide, mode, view, platform, show, stacks } = api;
|
||||
|
||||
if (view.is_arrange()) {
|
||||
// in arrange mode, create a screenshot at the start slicing
|
||||
// this can be used later by exports and rendered on some devices
|
||||
let snap = space.screenshot();
|
||||
view.snapshot = snap.substring(snap.indexOf(",") + 1);
|
||||
client.snap(space.screenshot2({ width: 640 }));
|
||||
let bambu = view.bambu = { };
|
||||
space.screenshot3({ width: 512, out(png) { bambu.s512 = png } });
|
||||
space.screenshot3({ width: 128, out(png) { bambu.s128 = png } });
|
||||
}
|
||||
|
||||
if (mode.is_sla() && !callback) {
|
||||
// in SLA mode, slice and preview are the same thing
|
||||
callback = preparePreview;
|
||||
}
|
||||
|
||||
const widgets = api.widgets.all();
|
||||
const settings = conf.get();
|
||||
const { device, process, controller } = settings;
|
||||
const isBelt = device.bedBelt || false;
|
||||
const mark = Date.now();
|
||||
|
||||
// force layout in belt mode when widget exceeds bed length
|
||||
// which has the side-effect (intended) of increasing the bed size
|
||||
if (widgets.length && isBelt) {
|
||||
let doLayout = false;
|
||||
for (let w of widgets) {
|
||||
let bb = w.getBoundingBox();
|
||||
let yspan = bb.max.y - bb.min.y;
|
||||
if (yspan > device.bedDepth) {
|
||||
doLayout = true;
|
||||
}
|
||||
}
|
||||
if (doLayout) {
|
||||
platform.layout();
|
||||
}
|
||||
}
|
||||
|
||||
functions.clear_progress();
|
||||
platform.deselect();
|
||||
hide.slider();
|
||||
view.set_slice();
|
||||
conf.save();
|
||||
|
||||
// allow client.js to generate support widgets
|
||||
event.emit('slice.begin', settings.mode);
|
||||
|
||||
// refresh widget list because 'slice.begin' may generate support widgets
|
||||
const slicing = api.widgets.all()
|
||||
.filter(w => !w.track.ignore && !w.meta.disabled);
|
||||
|
||||
if (slicing.length === 0) {
|
||||
return api.show.alert('nothing to slice');
|
||||
}
|
||||
|
||||
const totalv = slicing.map(w => w.getVertices().count).reduce((a,v) => a + v);
|
||||
const defvert = totalv / slicing.length;
|
||||
const track = {};
|
||||
|
||||
let totalProgress;
|
||||
|
||||
api.widgets.setOpacity(COLOR.slicing_opacity);
|
||||
|
||||
let segtimes = {},
|
||||
segNumber = 0,
|
||||
errored = false,
|
||||
startTime = Date.now(),
|
||||
toSlice = slicing.slice(),
|
||||
camOrLaser = mode.is_cam() || mode.is_laser(),
|
||||
extruders = {},
|
||||
lastMsg;
|
||||
|
||||
for (let widget of toSlice) {
|
||||
widget.belt = null;
|
||||
widget.stats.progress = 0;
|
||||
widget.setColor(COLOR.slicing);
|
||||
let { extruder } = widget.anno;
|
||||
if (extruder >= 0) {
|
||||
extruders[extruder] = extruder;
|
||||
}
|
||||
}
|
||||
|
||||
// in multi-material belt mode, the anchor needs to be extended
|
||||
// to allow room for the purge tower to be built. calculate here
|
||||
extruders = Object.values(extruders);
|
||||
if (isBelt && extruders.length > 1 && process.outputPurgeTower) {
|
||||
process.beltAnchor = Math.max(
|
||||
process.firstLayerBeltLead,
|
||||
Math.sqrt(process.outputPurgeTower) * extruders.length * (1/Math.sqrt(2)));
|
||||
} else if (isBelt) {
|
||||
process.beltAnchor = process.firstLayerBeltLead;
|
||||
}
|
||||
|
||||
stacks.clear(); // clear rendered stacks
|
||||
client.clear(); // clear worker cache
|
||||
client.sync(); // send fresh widget data to worker
|
||||
|
||||
if (isBelt) {
|
||||
// belt op required to rotate meshes for slicing
|
||||
client.rotate(settings);
|
||||
}
|
||||
|
||||
function sliceNext() {
|
||||
if (toSlice.length) {
|
||||
// while more widgets to slice
|
||||
sliceWidget(toSlice.shift())
|
||||
} else {
|
||||
// once all slicing done, run sliceAll() once for all widgets
|
||||
client.sliceAll(settings, sliceDone);
|
||||
}
|
||||
}
|
||||
|
||||
function sliceWidget(widget) {
|
||||
|
||||
function onupdate(update, msg, alert) {
|
||||
if (alert) {
|
||||
api.show.alert(alert);
|
||||
}
|
||||
if (msg && msg !== lastMsg) {
|
||||
let mark = Date.now();
|
||||
if (lastMsg) {
|
||||
let key = slicing.length > 1 ?
|
||||
`${widget.id}_${segNumber++}_${lastMsg}` :
|
||||
`${segNumber++}_${lastMsg}`
|
||||
segtimes[key] = mark - startTime;
|
||||
}
|
||||
lastMsg = msg;
|
||||
startTime = mark;
|
||||
}
|
||||
// on update
|
||||
if (update >= 0) {
|
||||
track[widget.id] = (update || 0) * factor;
|
||||
totalProgress = 0;
|
||||
for (let w of slicing) {
|
||||
totalProgress += (track[w.id] || 0);
|
||||
}
|
||||
show.progress(offset + (totalProgress / slicing.length) * scale, msg);
|
||||
}
|
||||
}
|
||||
|
||||
function ondone(sliced, error) {
|
||||
let mark = Date.now();
|
||||
// update UI info
|
||||
if (sliced) {
|
||||
// update segment time
|
||||
if (lastMsg) {
|
||||
let key = slicing.length > 1 ?
|
||||
`${widget.id}_${segNumber++}_${lastMsg}` :
|
||||
`${segNumber++}_${lastMsg}`
|
||||
segtimes[`${key}`] = mark - startTime;
|
||||
}
|
||||
event.emit('slice', settings.mode);
|
||||
}
|
||||
// handle slicing errors
|
||||
if (error && !errored) {
|
||||
errored = true;
|
||||
view.set_arrange();
|
||||
show.alert(error, 5);
|
||||
show.progress(0);
|
||||
client.restart();
|
||||
event.emit('slice.error', error);
|
||||
setTimeout(view.set_arrange, 1);
|
||||
}
|
||||
if (errored) {
|
||||
// terminate slicing
|
||||
sliceDone();
|
||||
} else {
|
||||
// start next widget slice
|
||||
sliceNext();
|
||||
}
|
||||
}
|
||||
|
||||
// weight each widget progress % by their # vertices
|
||||
let factor = (widget.getVertices().count / defvert);
|
||||
|
||||
widget.settings = settings;
|
||||
widget.clearSlices();
|
||||
|
||||
onupdate(0.0001, "slicing");
|
||||
|
||||
// store slicing visuals
|
||||
widget.stack = api.stacks.create(widget.id, widget.mesh);
|
||||
|
||||
// compensate for zcut (widget moved through floor)
|
||||
widget.stack.obj.view.position.z = widget.track.zcut || 0;
|
||||
|
||||
// in case result of slice is nothing, do not preserve previous
|
||||
widget.slices = []
|
||||
|
||||
// executed from kiri.js
|
||||
client.slice(settings, widget, (reply) => {
|
||||
if (reply.alert) {
|
||||
onupdate(null, null, reply.alert);
|
||||
}
|
||||
if (reply.update) {
|
||||
onupdate(reply.update, reply.updateStatus);
|
||||
}
|
||||
if (reply.send_start) {
|
||||
widget.xfer = {start: reply.send_start};
|
||||
}
|
||||
if (reply.stats) {
|
||||
widget.stats = reply.stats;
|
||||
}
|
||||
if (reply.send_end) {
|
||||
widget.stats.load_time = widget.xfer.start - reply.send_end;
|
||||
}
|
||||
if (reply.slice) {
|
||||
widget.slices.push(codec.decode(reply.slice, {mesh:widget.mesh}));
|
||||
}
|
||||
if (reply.done) {
|
||||
ondone(true);
|
||||
}
|
||||
if (reply.error) {
|
||||
ondone(false, reply.error);
|
||||
}
|
||||
});
|
||||
|
||||
// discard point cache
|
||||
widget.points = undefined;
|
||||
}
|
||||
|
||||
function sliceDone() {
|
||||
let alert = null;
|
||||
if (scale === 1 && feature.work_alerts && slicing.length) {
|
||||
alert = show.alert("Rendering");
|
||||
};
|
||||
for (let widget of slicing) {
|
||||
// on done
|
||||
let key = slicing.length > 1 ?
|
||||
`${widget.id}_${segNumber++}_draw` :
|
||||
`${segNumber++}_draw`
|
||||
segtimes[`${key}`] = widget.render(widget.stack);
|
||||
// rotate stack for belt beds
|
||||
if (widget.belt) {
|
||||
widget.stack.obj.rotate(widget.belt);
|
||||
}
|
||||
if (scale === 1) {
|
||||
// clear wireframe
|
||||
widget.setWireframe(false, COLOR.wireframe, COLOR.wireframe_opacity);
|
||||
widget.setOpacity(camOrLaser ? COLOR.cam_sliced_opacity : COLOR.sliced_opacity);
|
||||
widget.setColor(COLOR.deselected);
|
||||
api.hide.alert(alert);
|
||||
}
|
||||
}
|
||||
view.update_slider_max(true);
|
||||
show.layer(-1, 0);
|
||||
if (scale === 1) {
|
||||
view.update_stack_labels();
|
||||
}
|
||||
// if (!isBelt && controller.lineType === 'line' && !process.xray && !controller.devel) {
|
||||
// $('render-ghost').onclick();
|
||||
// }
|
||||
if (scale === 1) {
|
||||
show.progress(0);
|
||||
}
|
||||
// mark slicing complete for prep/preview
|
||||
complete.slice = true;
|
||||
event.emit('slice.end', settings.mode);
|
||||
// print stats
|
||||
segtimes.total = Date.now() - mark;
|
||||
console.log(segtimes);
|
||||
if (callback && typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
if (isBelt) {
|
||||
// required to render supports properly (in belt mode)
|
||||
// because this updates widget top z which is affected by rotation
|
||||
api.platform.update_top_z();
|
||||
}
|
||||
// refresh / repaint workspace
|
||||
space.refresh();
|
||||
}
|
||||
|
||||
// kick off slicing chain
|
||||
sliceNext();
|
||||
}
|
||||
|
||||
function preparePreview(callback, scale = 1, offset = 0) {
|
||||
const { conf, event, feature, hide, mode, view, platform, show, stacks } = api;
|
||||
const widgets = api.widgets.all();
|
||||
const settings = conf.get();
|
||||
const { device, process, controller } = settings;
|
||||
|
||||
if (complete.preview === feature.pmode) {
|
||||
if (device.extruders && device.extruders.length > 1) {
|
||||
if (++feature.pmode > 2) {
|
||||
feature.pmode = 1;
|
||||
}
|
||||
} else {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!complete.slice) {
|
||||
settings.render = false;
|
||||
prepareSlices(() => { preparePreview(callback, 0.25, 0.75) }, 0.75);
|
||||
return;
|
||||
}
|
||||
|
||||
hide.slider(true);
|
||||
|
||||
const isCam = mode.is_cam(), pMode = settings.mode;
|
||||
const isDark = api.space.is_dark();
|
||||
const isBelt = device.bedBelt || false;
|
||||
|
||||
view.set_preview();
|
||||
conf.save();
|
||||
event.emit('preview.begin', pMode);
|
||||
|
||||
if (isCam) {
|
||||
api.widgets.setOpacity(isDark ? COLOR.cam_preview_opacity_dark : COLOR.cam_preview_opacity);
|
||||
api.widgets.setColor(isDark ? COLOR.cam_preview_dark : COLOR.cam_preview);
|
||||
} else if (offset === 0) {
|
||||
api.widgets.setOpacity(COLOR.preview_opacity);
|
||||
}
|
||||
|
||||
let mark = Date.now(),
|
||||
segNumber = 0,
|
||||
segtimes = {},
|
||||
startTime,
|
||||
lastMsg,
|
||||
output = [];
|
||||
|
||||
// pass preview mode to worker
|
||||
settings.pmode = feature.pmode;
|
||||
settings.render = true;
|
||||
|
||||
client.prepare(settings, (progress, message, layer) => {
|
||||
if (layer) {
|
||||
output.push(codec.decode(layer));
|
||||
}
|
||||
if (message && message !== lastMsg) {
|
||||
const mark = Date.now();
|
||||
if (lastMsg) {
|
||||
segtimes[`${segNumber++}_${lastMsg}`] = mark - startTime;
|
||||
}
|
||||
lastMsg = message;
|
||||
startTime = mark;
|
||||
}
|
||||
show.progress(offset + progress * scale, message);
|
||||
}, (reply, maxSpeed, minSpeed) => {
|
||||
// handle worker errors
|
||||
if (reply && reply.error) {
|
||||
show.alert(reply.error, 5);
|
||||
view.set_arrange();
|
||||
show.progress(0);
|
||||
space.update();
|
||||
event.emit('preview.error', reply.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastMsg) {
|
||||
segtimes[`${segNumber++}_${lastMsg}`] = Date.now() - startTime;
|
||||
}
|
||||
|
||||
show.progress(0);
|
||||
if (!isCam) {
|
||||
api.widgets.setOpacity(0);
|
||||
}
|
||||
|
||||
if (output.length) {
|
||||
let alert = feature.work_alerts ? show.alert("Rendering") : null;
|
||||
startTime = Date.now();
|
||||
stacks.clear();
|
||||
const stack = stacks.create('print', space.world)
|
||||
const view = stack.obj.view;
|
||||
output.forEach(layer => {
|
||||
stack.add(layer);
|
||||
});
|
||||
event.emit('preview.view', view);
|
||||
// rotate stack for belt beds
|
||||
if (isBelt && widgets[0].belt) {
|
||||
let ri = widgets[0].belt;
|
||||
ri.dz = 0;
|
||||
ri.dy = settings.device.bedDepth / 2;
|
||||
stack.obj.rotate(widgets[0].belt);
|
||||
}
|
||||
api.hide.alert(alert);
|
||||
segtimes[`${segNumber}_draw`] = Date.now() - startTime;
|
||||
}
|
||||
|
||||
// print stats
|
||||
segtimes.total = Date.now() - mark;
|
||||
console.log(segtimes);
|
||||
|
||||
event.emit('print', pMode);
|
||||
event.emit('preview.end', pMode);
|
||||
|
||||
space.update();
|
||||
view.update_slider_max(true);
|
||||
show.layer(-1, 0);
|
||||
if (feature.pmode === PMODES.SPEED) {
|
||||
view.update_speeds(maxSpeed, minSpeed);
|
||||
} else {
|
||||
view.update_speeds();
|
||||
}
|
||||
view.update_stack_labels();
|
||||
|
||||
let { controller, process } = settings;
|
||||
// if (!isBelt && controller.lineType === 'line' && !process.xray && !controller.devel) {
|
||||
// $('render-ghost').onclick();
|
||||
// }
|
||||
|
||||
// mark preview complete for export
|
||||
complete.preview = feature.pmode;
|
||||
|
||||
if (typeof(callback) === 'function') {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function prepareAnimation() {
|
||||
if (!window.SharedArrayBuffer) {
|
||||
api.alerts.show("The security context of this");
|
||||
api.alerts.show("window prevents animations.");
|
||||
api.alerts.show("Try a Chromium-based browser");
|
||||
return;
|
||||
}
|
||||
api.event.emit("function.animate", {mode: api.conf.get().mode});
|
||||
}
|
||||
|
||||
function prepareExport() {
|
||||
const settings = api.conf.get();
|
||||
const argsave = arguments;
|
||||
if (!complete.preview) {
|
||||
preparePreview(() => { prepareExport(...argsave) });
|
||||
return;
|
||||
}
|
||||
api.event.emit("function.export", {mode: settings.mode});
|
||||
complete.export = true;
|
||||
exportFile(...argsave);
|
||||
}
|
||||
|
||||
function cancelWorker() {
|
||||
if (client.isBusy()) {
|
||||
client.restart();
|
||||
}
|
||||
}
|
||||
|
||||
function parseCode(code, type) {
|
||||
const { conf, event, show, stacks, widgets, view } = api;
|
||||
const settings = conf.get();
|
||||
|
||||
event.emit("code.load", {code, type});
|
||||
view.set_preview();
|
||||
widgets.setOpacity(0);
|
||||
|
||||
client.parse({code, type, settings}, progress => {
|
||||
show.progress(progress, "parsing");
|
||||
}, (layers, maxSpeed, minSpeed) => {
|
||||
show.progress(0);
|
||||
stacks.clear();
|
||||
const stack = stacks.create('parse', space.world);
|
||||
layers.forEach(layer => stack.add(layer));
|
||||
view.update_slider_max(true);
|
||||
view.update_speeds(maxSpeed, minSpeed);
|
||||
show.slices();
|
||||
view.update_stack_labels();
|
||||
space.update();
|
||||
event.emit("code.loaded", {code, type});
|
||||
});
|
||||
}
|
||||
|
||||
function clear_progress() {
|
||||
complete = {};
|
||||
}
|
||||
|
||||
// extend API (api.function)
|
||||
export const functions = {
|
||||
slice: prepareSlices,
|
||||
print: preparePreview,
|
||||
prepare: preparePreview,
|
||||
animate: prepareAnimation,
|
||||
export: prepareExport,
|
||||
cancel: cancelWorker,
|
||||
parse: parseCode,
|
||||
clear: client.clear,
|
||||
clear_progress,
|
||||
};
|
||||
|
||||
export {
|
||||
prepareSlices as slice,
|
||||
preparePreview as print,
|
||||
preparePreview as prepare,
|
||||
prepareAnimation as animate,
|
||||
prepareExport as export,
|
||||
cancelWorker as cancel,
|
||||
parseCode as parse,
|
||||
clear_progress,
|
||||
};
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,859 +0,0 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import '../ui/frame.js';
|
||||
import '../../ext/base64.js';
|
||||
|
||||
import STACKS from '../ui/stacks.js';
|
||||
import { modal } from '../ui/modal.js';
|
||||
|
||||
import { $ } from '../../moto/webui.js';
|
||||
import { api } from './api.js';
|
||||
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 '../ui/files.js';
|
||||
import { platform } from './platform.js';
|
||||
import { selection } from './selection.js';
|
||||
import { settings } from '../ui/config/manager.js';
|
||||
import { showDevices } from '../ui/devices.js';
|
||||
import { space as SPACE } from '../../moto/space.js';
|
||||
import { stats } from '../ui/stats.js';
|
||||
import { noop, utils } from './utils.js';
|
||||
import { version } from '../../moto/license.js';
|
||||
import { Widget, newWidget } from './widget.js';
|
||||
import { showTools } from '../mode/cam/tools.js';
|
||||
|
||||
let { parseOpt, o2js, js2o, ls2o } = utils,
|
||||
{ COLOR, MODES, VIEWS } = consts,
|
||||
WIN = self.window,
|
||||
DOC = self.document,
|
||||
LOC = self.location,
|
||||
SETUP = parseOpt(LOC.search.substring(1)),
|
||||
SECURE = isSecure(LOC.protocol),
|
||||
LOCAL = self.debug && !SETUP.remote,
|
||||
EVENT = broker,
|
||||
SDB = dataLocal,
|
||||
FILES = openFiles(new Index(SETUP.d ? SETUP.d[0] : 'kiri')),
|
||||
clone = Object.clone,
|
||||
MODE = MODES.FDM,
|
||||
viewMode = VIEWS.ARRANGE,
|
||||
autoSaveTimer = null,
|
||||
inits = parseInt(SDB.getItem('kiri-init') || stats.get('init') || 0) + 1;
|
||||
|
||||
// update version and init count
|
||||
SDB.setItem('kiri-init', inits);
|
||||
stats.set('init', inits);
|
||||
stats.set('kiri', version);
|
||||
|
||||
// allow widget to straddle client / worker FOR NOW
|
||||
self.kiri_catalog = FILES;
|
||||
|
||||
export const dialog = {
|
||||
show: (which) => modal.show(which),
|
||||
hide: () => modal.hide(),
|
||||
update_process_list: updateProcessList
|
||||
};
|
||||
|
||||
export const help = {
|
||||
show: showHelp,
|
||||
file: showHelpFile
|
||||
};
|
||||
|
||||
export const event = {
|
||||
on(t,l) { return EVENT.on(t,l) },
|
||||
emit(t,m,o) { return EVENT.publish(t,m,o) },
|
||||
bind(t,m,o) { return EVENT.bind(t,m,o) },
|
||||
alerts(clr) { api.alerts.update(clr) },
|
||||
import: loadFile,
|
||||
settings: triggerSettingsEvent
|
||||
};
|
||||
|
||||
export const group = {
|
||||
merge: groupMerge,
|
||||
split: groupSplit,
|
||||
};
|
||||
|
||||
export const hide = {
|
||||
alert(rec, recs) { api.alerts.hide(...arguments) },
|
||||
import: noop,
|
||||
slider: hideSlider
|
||||
};
|
||||
|
||||
export const image = {
|
||||
dialog: loadImageDialog,
|
||||
convert: loadImageConvert
|
||||
};
|
||||
|
||||
// Re-export modal singleton methods
|
||||
export { modal };
|
||||
|
||||
export const mode = {
|
||||
get_id() { return MODE },
|
||||
get_lower: getModeLower,
|
||||
get: getMode,
|
||||
set: setMode,
|
||||
switch: switchMode,
|
||||
set_expert: noop,
|
||||
is_fdm() { return MODE === MODES.FDM },
|
||||
is_cam() { return MODE === MODES.CAM },
|
||||
is_sla() { return MODE === MODES.SLA },
|
||||
is_drag() { return MODE === MODES.DRAG },
|
||||
is_wedm() { return MODE === MODES.WEDM },
|
||||
is_wjet() { return MODE === MODES.WJET },
|
||||
is_laser() { return MODE === MODES.LASER },
|
||||
is_2d() { return false ||
|
||||
api.mode.is_drag() ||
|
||||
api.mode.is_wedm() ||
|
||||
api.mode.is_wjet() ||
|
||||
api.mode.is_laser()
|
||||
}
|
||||
};
|
||||
|
||||
export const process = {
|
||||
code: currentProcessCode,
|
||||
get: currentProcessName
|
||||
};
|
||||
|
||||
export const show = {
|
||||
alert() { return api.alerts.show(...arguments) },
|
||||
controls: setControlsVisible,
|
||||
devices: showDevices,
|
||||
import() { api.ui.import.style.display = '' },
|
||||
layer: setVisibleLayer,
|
||||
local: showLocal,
|
||||
progress: setProgress,
|
||||
slices: showSlices,
|
||||
tools: showTools
|
||||
};
|
||||
|
||||
export const space = {
|
||||
reload,
|
||||
auto_save,
|
||||
restore: restoreWorkspace,
|
||||
clear: clearWorkspace,
|
||||
save: saveWorkspace,
|
||||
set_focus: setFocus,
|
||||
update: SPACE.update,
|
||||
is_dark() { return settings.ctrl().dark }
|
||||
};
|
||||
|
||||
export const util = {
|
||||
isSecure,
|
||||
download: downloadBlob,
|
||||
ui2rec() { api.conf.update_from(...arguments) },
|
||||
rec2ui() { api.conf.update_fields(...arguments) },
|
||||
b64enc(obj) { return base64js.fromByteArray(new TextEncoder().encode(JSON.stringify(obj))) },
|
||||
b64dec(obj) { return JSON.parse(new TextDecoder().decode(base64js.toByteArray(obj))) }
|
||||
};
|
||||
|
||||
export const view = {
|
||||
get() { return viewMode },
|
||||
set() { setViewMode(...arguments) },
|
||||
set_arrange() { api.view.set(VIEWS.ARRANGE) },
|
||||
set_slice() { api.view.set(VIEWS.SLICE) },
|
||||
set_preview() { api.view.set(VIEWS.PREVIEW) },
|
||||
set_animate() { api.view.set(VIEWS.ANIMATE) },
|
||||
is_arrange() { return viewMode === VIEWS.ARRANGE },
|
||||
is_slice() { return viewMode === VIEWS.SLICE },
|
||||
is_preview() { return viewMode === VIEWS.PREVIEW },
|
||||
is_animate() { return viewMode === VIEWS.ANIMATE },
|
||||
update_stack_labels: updateStackLabelState,
|
||||
update_slider_max: updateSliderMax,
|
||||
update_slider: updateSlider,
|
||||
update_speeds: updateSpeeds,
|
||||
hide_slices: hideSlices,
|
||||
snapshot: null,
|
||||
edges: setEdges,
|
||||
unit_scale: unitScale,
|
||||
wireframe: setWireframe
|
||||
};
|
||||
|
||||
// add show() to catalog for API
|
||||
FILES.show = () => modal.show('files');
|
||||
|
||||
// patch broker for api backward compatibility
|
||||
EVENT.on = (topic, listener) => {
|
||||
EVENT.subscribe(topic, listener);
|
||||
return EVENT;
|
||||
};
|
||||
|
||||
function updateStackLabelState() {
|
||||
const settings = api.conf.get();
|
||||
// match label checkboxes to preference
|
||||
for (let label of api.stacks.getLabels()) {
|
||||
let check = `${settings.mode}-${api.view.get()}-${label}`;
|
||||
api.stacks.setVisible(label, settings.labels[check] !== false);
|
||||
}
|
||||
}
|
||||
|
||||
function setFocus(sel, point) {
|
||||
if (point) {
|
||||
SPACE.platform.setCenter(point.x, point.z, point.y);
|
||||
SPACE.view.setFocus(new THREE.Vector3(point.x, point.y, point.z));
|
||||
return;
|
||||
}
|
||||
if (sel === undefined) {
|
||||
sel = api.widgets.all();
|
||||
} else if (!Array.isArray) {
|
||||
sel = [ sel ];
|
||||
} else if (sel.length === 0) {
|
||||
sel = api.widgets.all();
|
||||
}
|
||||
let pos = { x:0, y:0, z:0 };
|
||||
for (let widget of sel) {
|
||||
pos.x += widget.track.pos.x;
|
||||
pos.y += widget.track.pos.y;
|
||||
pos.z += widget.track.pos.z;
|
||||
}
|
||||
if (sel.length) {
|
||||
pos.x /= sel.length;
|
||||
pos.y /= sel.length;
|
||||
pos.z /= sel.length;
|
||||
}
|
||||
let cam_index = api.conf.get().process.camStockIndexed || false;
|
||||
let focus_z = cam_index ? 0 : platform.top_z() / 2;
|
||||
SPACE.platform.setCenter(pos.x, -pos.y, focus_z);
|
||||
SPACE.view.setFocus(new THREE.Vector3(pos.x, focus_z, -pos.y));
|
||||
}
|
||||
|
||||
function reload() {
|
||||
api.event.emit('reload');
|
||||
do_reload(100);
|
||||
}
|
||||
|
||||
function do_reload(time) {
|
||||
// allow time for async saves to complete and busy to to to zero
|
||||
setTimeout(() => {
|
||||
if (api.busy.val() === 0) {
|
||||
LOC.reload();
|
||||
} else {
|
||||
console.log(`reload deferred on busy=${api.busy.val()}`);
|
||||
do_reload(250);
|
||||
}
|
||||
}, time || 100);
|
||||
}
|
||||
|
||||
function auto_save() {
|
||||
if (!settings.ctrl().autoSave) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(autoSaveTimer);
|
||||
autoSaveTimer = setTimeout(() => {
|
||||
api.space.save(true);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
/** ******************************************************************
|
||||
* Utility Functions
|
||||
******************************************************************* */
|
||||
|
||||
function unitScale() {
|
||||
return api.mode.is_cam() && settings.ctrl().units === 'in' ? 25.4 : 1;
|
||||
}
|
||||
|
||||
function triggerSettingsEvent() {
|
||||
api.event.emit('settings', settings.get());
|
||||
}
|
||||
|
||||
function isSecure(proto) {
|
||||
return proto.toLowerCase().indexOf("https") === 0;
|
||||
}
|
||||
|
||||
function setProgress(value = 0, msg) {
|
||||
value = (value * 100).round(4);
|
||||
api.ui.progress.width = value+'%';
|
||||
if (self.debug) {
|
||||
// console.log(msg, value.round(2));
|
||||
api.ui.prostatus.style.display = 'flex';
|
||||
if (msg) {
|
||||
api.ui.prostatus.innerHTML = msg;
|
||||
} else {
|
||||
api.ui.prostatus.innerHTML = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function bound(v,min,max) {
|
||||
return Math.max(min,Math.min(max,v));
|
||||
}
|
||||
|
||||
function updateSpeeds(maxSpeed, minSpeed) {
|
||||
const { ui } = api;
|
||||
ui.speeds.style.display =
|
||||
maxSpeed &&
|
||||
settings.mode !== 'SLA' &&
|
||||
settings.mode !== 'LASER' &&
|
||||
viewMode === VIEWS.PREVIEW &&
|
||||
ui.showSpeeds.checked ? 'block' : '';
|
||||
if (maxSpeed) {
|
||||
const colors = [];
|
||||
for (let i = 0; i <= maxSpeed; i += maxSpeed / 20) {
|
||||
colors.push(Math.round(Math.max(i, 1)));
|
||||
}
|
||||
api.client.colors(colors, maxSpeed, speedColors => {
|
||||
const list = [];
|
||||
Object.keys(speedColors).map(v => parseInt(v)).sort((a, b) => b - a).forEach(speed => {
|
||||
const color = speedColors[speed];
|
||||
const hex = color.toString(16).padStart(6, 0);
|
||||
const r = (color >> 16) & 0xff;
|
||||
const g = (color >> 8) & 0xff;
|
||||
const b = (color >> 0) & 0xff;
|
||||
const style = `background-color:#${hex}`;
|
||||
list.push(`<label style="${style}">${speed}</label>`);
|
||||
});
|
||||
ui.speedbar.innerHTML = list.join('');
|
||||
});
|
||||
api.event.emit('preview.speeds', {
|
||||
min: minSpeed,
|
||||
max: maxSpeed
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateSlider() {
|
||||
const { lo, hi, max } = api.slider.getRange();
|
||||
if (max > 0) {
|
||||
api.slider.updatePosition(lo / max, hi / max);
|
||||
}
|
||||
api.conf.update_fields_from_range();
|
||||
}
|
||||
|
||||
function setVisibleLayer(h, l) {
|
||||
const { lo, hi, max } = api.slider.getRange();
|
||||
h = h >= 0 ? h : hi;
|
||||
l = l >= 0 ? l : lo;
|
||||
const newHi = bound(h, 0, max);
|
||||
const newLo = bound(l, 0, newHi);
|
||||
api.slider.setRange(newLo, newHi); // Notify callbacks (STACKS, scene updates)
|
||||
api.slider.showLabels();
|
||||
showSlices();
|
||||
}
|
||||
|
||||
function setWireframe(bool, color, opacity) {
|
||||
api.widgets.each(function(w) { w.setWireframe(bool, color, opacity) });
|
||||
SPACE.update();
|
||||
}
|
||||
|
||||
function setEdges(bool) {
|
||||
if (bool && bool.toggle) {
|
||||
api.local.toggle('model.edges');
|
||||
} else {
|
||||
api.local.set('model.edges', bool);
|
||||
}
|
||||
bool = api.local.getBoolean('model.edges');
|
||||
api.widgets.each(w => w.setEdges(bool));
|
||||
SPACE.update();
|
||||
}
|
||||
|
||||
function updateSliderMax(set) {
|
||||
let max = STACKS.getRange().tallest - 1;
|
||||
api.slider.setMax(max);
|
||||
const { hi } = api.slider.getRange();
|
||||
if (set || max < hi) {
|
||||
api.slider.setRange(max, max);
|
||||
api.slider.showLabels();
|
||||
}
|
||||
}
|
||||
|
||||
function hideSlices() {
|
||||
STACKS.clear();
|
||||
api.widgets.setOpacity(COLOR.model_opacity);
|
||||
api.widgets.each(function(widget) {
|
||||
widget.setWireframe(false);
|
||||
});
|
||||
}
|
||||
|
||||
function setWidgetVisibility(bool) {
|
||||
api.widgets.each(w => {
|
||||
if (bool) {
|
||||
w.show();
|
||||
} else {
|
||||
w.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* hide or show slice-layers and their sub-elements
|
||||
*
|
||||
* @param {number} [layer]
|
||||
*/
|
||||
function showSlices(layer) {
|
||||
if (viewMode === VIEWS.ARRANGE) {
|
||||
return;
|
||||
}
|
||||
|
||||
showSlider();
|
||||
|
||||
const { lo, hi, max } = api.slider.getRange();
|
||||
|
||||
if (typeof(layer) === 'string' || typeof(layer) === 'number') {
|
||||
layer = parseInt(layer);
|
||||
} else {
|
||||
layer = hi;
|
||||
}
|
||||
|
||||
layer = bound(layer, 0, max);
|
||||
const newLo = layer < lo ? layer : lo;
|
||||
api.slider.setRange(newLo, layer);
|
||||
api.slider.showLabels();
|
||||
|
||||
updateSlider();
|
||||
// STACKS.setRange is called by slider callback
|
||||
|
||||
SPACE.update();
|
||||
}
|
||||
|
||||
function showSlider() {
|
||||
api.slider.show();
|
||||
}
|
||||
|
||||
function hideSlider() {
|
||||
api.slider.hide();
|
||||
api.ui.speeds.style.display = 'none';
|
||||
}
|
||||
|
||||
function loadImageDialog(image, name, force) {
|
||||
if (!force && image.byteLength > 2500000) {
|
||||
return api.uc.confirm("Large images may fail to import<br>Consider resizing under 1000 x 1000<br>Proceed with import?").then(ok => {
|
||||
if (ok) {
|
||||
loadImageDialog(image, name, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
const opt = {pre: [
|
||||
"<div class='f-col a-center'>",
|
||||
" <h3>Image Conversion</h3>",
|
||||
" <p class='t-just' style='width:300px;line-height:1.5em'>",
|
||||
" This will create a 3D model from a 2D PNG image. Photos must",
|
||||
" be blurred to be usable. Values from 0=off to 50=high are suggested.",
|
||||
" Higher values incur more processing time.",
|
||||
" </p>",
|
||||
" <div class='f-row t-right'><table>",
|
||||
" <tr><th>blur value</th><td><input id='png-blur' value='0' size='3'></td>",
|
||||
" <th> invert image</th><td><input id='png-inv' type='checkbox'></td></tr>",
|
||||
" <tr><th>base size</th><td><input id='png-base' value='0' size='3'></td>",
|
||||
" <th> invert alpha</th><td><input id='alpha-inv' type='checkbox'></td></tr>",
|
||||
" <tr><th>border size</th><td><input id='png-border' value='0' size='3'></td>",
|
||||
" <th></th><td></td></tr>",
|
||||
" </table></div>",
|
||||
"</div>"
|
||||
]};
|
||||
api.uc.confirm(undefined, {convert:true, cancel:false}, undefined, opt).then((ok) => {
|
||||
if (ok) {
|
||||
loadImage(image, {
|
||||
file: name,
|
||||
blur: parseInt($('png-blur').value) || 0,
|
||||
base: parseInt($('png-base').value) || 0,
|
||||
border: parseInt($('png-border').value) || 0,
|
||||
inv_image: $('png-inv').checked,
|
||||
inv_alpha: $('alpha-inv').checked
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadImage(image, opt = {}) {
|
||||
const info = Object.assign({settings: settings.get(), png:image}, opt);
|
||||
api.client.image2mesh(info, progress => {
|
||||
api.show.progress(progress, "converting");
|
||||
}, vertices => {
|
||||
api.show.progress(0);
|
||||
const widget = newWidget().loadVertices(vertices);
|
||||
widget.meta.file = opt.file;
|
||||
platform.add(widget);
|
||||
});
|
||||
}
|
||||
|
||||
/** ******************************************************************
|
||||
* Selection Functions
|
||||
******************************************************************* */
|
||||
|
||||
function groupMerge() {
|
||||
Widget.Groups.merge(selection.widgets(true));
|
||||
}
|
||||
|
||||
function groupSplit() {
|
||||
Widget.Groups.split(selection.widgets(false));
|
||||
}
|
||||
|
||||
/** ******************************************************************
|
||||
* Settings Functions
|
||||
******************************************************************* */
|
||||
|
||||
// convert any image type to png
|
||||
function loadImageConvert(res, name) {
|
||||
let url = URL.createObjectURL(new Blob([res]));
|
||||
|
||||
$('mod-any').innerHTML = `<img id="xsrc" src="${url}"><canvas id="xdst"></canvas>`;
|
||||
|
||||
let img = $('xsrc');
|
||||
let can = $('xdst');
|
||||
|
||||
img.onload = () => {
|
||||
can.width = img.width;
|
||||
can.height = img.height;
|
||||
let ctx = can.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
fetch(can.toDataURL()).then(r => r.arrayBuffer()).then(data => {
|
||||
loadImageDialog(data, name);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function loadFile(ev) {
|
||||
// use modern Filesystem api when available
|
||||
if (false && window.showOpenFilePicker) {
|
||||
window.showOpenFilePicker().then(files => {
|
||||
return Promise.all(files.map(fh => fh.getFile()))
|
||||
}).then(files => {
|
||||
if (files.length) {
|
||||
api.platform.load_files(files);
|
||||
}
|
||||
}).catch(e => { /* ignore cancel */ });
|
||||
return;
|
||||
}
|
||||
api.ui.load.click();
|
||||
}
|
||||
|
||||
function saveWorkspace(quiet) {
|
||||
api.conf.save();
|
||||
const newWidgets = [];
|
||||
const oldWidgets = js2o(SDB.getItem('ws-widgets'), []);
|
||||
api.widgets.each(function(widget) {
|
||||
if (widget.synth) return;
|
||||
newWidgets.push(widget.id);
|
||||
oldWidgets.remove(widget.id);
|
||||
widget.saveState();
|
||||
let ann = api.widgets.annotate(widget.id);
|
||||
ann.file = widget.meta.file;
|
||||
ann.url = widget.meta.url;
|
||||
});
|
||||
SDB.setItem('ws-widgets', o2js(newWidgets));
|
||||
oldWidgets.forEach(wid => {
|
||||
Widget.deleteFromState(wid);
|
||||
});
|
||||
// eliminate dangling saved widgets
|
||||
FILES.deleteFilter(key => newWidgets.indexOf(key.substring(8)) < 0, "ws-save-", "ws-savf");
|
||||
if (!quiet) {
|
||||
api.show.alert("workspace saved", 1);
|
||||
}
|
||||
}
|
||||
|
||||
function restoreWorkspace(ondone, skip_widget_load) {
|
||||
let newset = api.conf.restore(false),
|
||||
camera = newset.controller.view,
|
||||
toload = ls2o('ws-widgets',[]),
|
||||
loaded = 0,
|
||||
position = true;
|
||||
|
||||
api.conf.update_fields();
|
||||
platform.update_size();
|
||||
|
||||
SPACE.view.reset();
|
||||
if (camera) {
|
||||
SPACE.view.load(camera);
|
||||
} else {
|
||||
SPACE.view.home();
|
||||
}
|
||||
|
||||
if (skip_widget_load) {
|
||||
if (ondone) {
|
||||
ondone();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// remove any widgets from platform
|
||||
api.widgets.each(function(widget) {
|
||||
platform.delete(widget);
|
||||
});
|
||||
|
||||
// load any widget by name that was saved to the workspace
|
||||
toload.forEach(function(widgetid) {
|
||||
Widget.loadFromState(widgetid, function(widget) {
|
||||
if (widget) {
|
||||
platform.add(widget, 0, position, true);
|
||||
let ann = api.widgets.annotate(widgetid);
|
||||
widget.meta.file = ann.file;
|
||||
widget.meta.url = ann.url;
|
||||
}
|
||||
if (++loaded === toload.length) {
|
||||
platform.deselect();
|
||||
if (ondone) {
|
||||
ondone();
|
||||
setTimeout(() => {
|
||||
platform.update_bounds();
|
||||
SPACE.update();
|
||||
}, 1);
|
||||
}
|
||||
}
|
||||
}, position);
|
||||
});
|
||||
|
||||
return toload.length > 0;
|
||||
}
|
||||
|
||||
function clearWorkspace() {
|
||||
// free up worker cache/mem
|
||||
api.client.clear();
|
||||
platform.select_all();
|
||||
platform.delete(selection.meshes());
|
||||
}
|
||||
|
||||
|
||||
function editSettings(e) {
|
||||
let current = settings.get(),
|
||||
mode = getMode(),
|
||||
name = e.target.getAttribute("name"),
|
||||
load = current.sproc[mode][name],
|
||||
loadstr = JSON.stringify(load,null,4).split('\n');
|
||||
api.uc.prompt(`settings for "${name}"`, loadstr).then(edit => {
|
||||
if (edit) {
|
||||
try {
|
||||
current.sproc[mode][name] = JSON.parse(edit);
|
||||
if (name === settings.proc().processName) {
|
||||
api.conf.load(null, name);
|
||||
}
|
||||
api.conf.save();
|
||||
api.settings.sync.put();
|
||||
} catch (e) {
|
||||
console.log({ malformed_settings: e });
|
||||
api.uc.alert('malformed settings object');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function exportSettings(e) {
|
||||
let current = settings.get(),
|
||||
mode = getMode(),
|
||||
name = e.target.getAttribute("name"),
|
||||
data = api.util.b64enc({
|
||||
process: current.sproc[mode][name],
|
||||
version,
|
||||
moto: moto.id,
|
||||
time: Date.now(),
|
||||
mode,
|
||||
name
|
||||
});
|
||||
api.uc.prompt("Export Process Filename", name).then(name => {
|
||||
if (name) {
|
||||
api.util.download(data, `${name}.km`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteSettings(e) {
|
||||
let current = settings.get();
|
||||
let name = e.target.getAttribute("del");
|
||||
delete current.sproc[getMode()][name];
|
||||
api.settings.sync.put();
|
||||
updateProcessList();
|
||||
api.conf.save();
|
||||
triggerSettingsEvent();
|
||||
}
|
||||
|
||||
function updateProcessList() {
|
||||
let current = settings.get();
|
||||
let list = [], s = current, sp = s.sproc[getMode()] || {}, table = api.ui.settingsList;
|
||||
table.innerHTML = '';
|
||||
for (let k in sp) {
|
||||
if (sp.hasOwnProperty(k)) list.push(k);
|
||||
}
|
||||
list.filter(n => n !=='default').sort().forEach(function(sk) {
|
||||
let row = DOC.createElement('div'),
|
||||
load = DOC.createElement('button'),
|
||||
edit = DOC.createElement('button'),
|
||||
xprt = DOC.createElement('button'),
|
||||
del = DOC.createElement('button'),
|
||||
name = sk;
|
||||
|
||||
load.setAttribute('load', sk);
|
||||
load.onclick = (ev) => {
|
||||
api.conf.load(undefined, sk);
|
||||
updateProcessList();
|
||||
modal.hide();
|
||||
}
|
||||
load.appendChild(DOC.createTextNode(sk));
|
||||
if (sk == settings.proc().processName) {
|
||||
load.setAttribute('class', 'selected')
|
||||
}
|
||||
api.ui.settingsName.value = settings.proc().processName;
|
||||
|
||||
del.setAttribute('del', sk);
|
||||
del.setAttribute('title', "remove '"+sk+"'");
|
||||
del.innerHTML = '<i class="far fa-trash-alt"></i>';
|
||||
del.onclick = deleteSettings;
|
||||
|
||||
edit.setAttribute('name', sk);
|
||||
edit.setAttribute('title', 'edit');
|
||||
edit.innerHTML = '<i class="far fa-edit"></i>';
|
||||
edit.onclick = editSettings;
|
||||
|
||||
xprt.setAttribute('name', sk);
|
||||
xprt.setAttribute('title', 'export');
|
||||
xprt.innerHTML = '<i class="fas fa-download"></i>';
|
||||
xprt.onclick = exportSettings;
|
||||
|
||||
row.setAttribute("class", "flow-row");
|
||||
row.appendChild(edit);
|
||||
row.appendChild(load);
|
||||
row.appendChild(xprt);
|
||||
row.appendChild(del);
|
||||
table.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function showHelp() {
|
||||
showHelpFile(`local`,() => {});
|
||||
}
|
||||
|
||||
function showHelpFile(local,then) {
|
||||
if (!local) {
|
||||
WIN.open("//docs.grid.space/", "_help");
|
||||
return;
|
||||
}
|
||||
const LANG = api.language.current;
|
||||
$('kiri-version').innerHTML = `${LANG.version} ${version}`;
|
||||
modal.show('help');
|
||||
api.event.emit('help.show', local);
|
||||
}
|
||||
|
||||
function showLocal() {
|
||||
// deprecated
|
||||
}
|
||||
|
||||
function setViewMode(mode) {
|
||||
const isCAM = settings.mode() === 'CAM';
|
||||
viewMode = mode;
|
||||
platform.deselect();
|
||||
selection.update_info();
|
||||
// clear any bounds forced by, for example, WireEDM previews
|
||||
api.platform.set_bounds();
|
||||
// disable clear in non-arrange modes
|
||||
['view-arrange','act-slice','act-preview','act-animate'].forEach(el => {
|
||||
$(el).classList.remove('selected')
|
||||
});
|
||||
$('render-tools').classList.add('hide');
|
||||
switch (mode) {
|
||||
case VIEWS.ARRANGE:
|
||||
$('view-arrange').classList.add('selected');
|
||||
$('render-tools').classList.remove('hide');
|
||||
api.function.clear_progress();
|
||||
api.client.clear();
|
||||
STACKS.clear();
|
||||
hideSlider();
|
||||
updateSpeeds();
|
||||
setVisibleLayer();
|
||||
setWidgetVisibility(true);
|
||||
api.widgets.setOpacity(1);
|
||||
api.view.edges(api.local.getBoolean('model.edges'));
|
||||
break;
|
||||
case VIEWS.SLICE:
|
||||
$('act-slice').classList.add('selected');
|
||||
updateSpeeds();
|
||||
updateSliderMax();
|
||||
setWidgetVisibility(true);
|
||||
!isCAM && api.view.edges(false);
|
||||
break;
|
||||
case VIEWS.PREVIEW:
|
||||
$('act-preview').classList.add('selected');
|
||||
setWidgetVisibility(true);
|
||||
!isCAM && api.view.edges(false);
|
||||
break;
|
||||
case VIEWS.ANIMATE:
|
||||
$('act-animate').classList.add('selected');
|
||||
!isCAM && api.view.edges(false);
|
||||
break;
|
||||
default:
|
||||
console.log("invalid view mode: "+mode);
|
||||
return;
|
||||
}
|
||||
api.event.emit('view.set', mode);
|
||||
DOC.activeElement.blur();
|
||||
}
|
||||
|
||||
function getMode() {
|
||||
return settings.mode();
|
||||
}
|
||||
|
||||
function getModeLower() {
|
||||
return getMode().toLowerCase();
|
||||
}
|
||||
|
||||
function switchMode(mode) {
|
||||
setMode(mode, null, platform.update_size);
|
||||
}
|
||||
|
||||
function setMode(mode, lock, then) {
|
||||
if (!MODES[mode]) {
|
||||
console.log("invalid mode: "+mode);
|
||||
mode = 'FDM';
|
||||
}
|
||||
const current = settings.get();
|
||||
// change mode constants
|
||||
current.mode = mode;
|
||||
MODE = MODES[mode];
|
||||
// gcode edit area for any non-SLA mode
|
||||
api.uc.setVisible($('gcode-edit'), mode !== 'SLA');
|
||||
// highlight selected mode menu item
|
||||
["FDM","CAM","SLA","LASER","DRAG","WJET","WEDM"].forEach(sm => {
|
||||
const cl = $(`mode-${sm.toLowerCase()}`).classList;
|
||||
if (sm === mode) {
|
||||
cl.add('selected');
|
||||
} else {
|
||||
cl.remove('selected');
|
||||
}
|
||||
});
|
||||
// restore cached device profile for this mode
|
||||
if (current.cdev[mode]) {
|
||||
current.device = clone(current.cdev[mode]);
|
||||
api.event.emit('device.select', api.device.get());
|
||||
}
|
||||
// hide/show
|
||||
api.uc.setVisible($('set-tools'), mode === 'CAM');
|
||||
// updates right-hand menu by enabling/disabling fields
|
||||
setViewMode(VIEWS.ARRANGE);
|
||||
api.uc.setMode(MODE);
|
||||
// sanitize and persist settings
|
||||
api.conf.load();
|
||||
api.conf.save();
|
||||
// other housekeeping
|
||||
triggerSettingsEvent();
|
||||
platform.update_selected();
|
||||
selection.update_bounds(api.widgets.all());
|
||||
api.conf.update_fields();
|
||||
// because device dialog, if showing, needs to be updated
|
||||
if (modal.visible()) {
|
||||
api.show.devices();
|
||||
}
|
||||
api.space.restore(null, true);
|
||||
api.event.emit("mode.set", mode);
|
||||
if (then) {
|
||||
then();
|
||||
}
|
||||
}
|
||||
|
||||
function currentProcessName() {
|
||||
return settings.get().cproc[getMode()];
|
||||
}
|
||||
|
||||
function currentProcessCode() {
|
||||
return settings.get().sproc[getMode()][currentProcessName()];
|
||||
}
|
||||
|
||||
function setControlsVisible(show) {
|
||||
// TODO fix
|
||||
// $('mid-left').style.display = show ? 'flex' : 'none';
|
||||
// $('mid-right').style.display = show ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
function downloadBlob(data, filename) {
|
||||
let url = WIN.URL.createObjectURL(new Blob([data], {type: "octet/stream"}));
|
||||
$('mod-any').innerHTML = `<a id="_dexport_" href="${url}" download="${filename}">x</a>`;
|
||||
$('_dexport_').click();
|
||||
}
|
||||
|
||||
export { FILES as catalog, LOCAL, SETUP, SECURE };
|
||||
|
|
@ -1,950 +0,0 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { $, h } from '../../moto/webui.js';
|
||||
import { api } from './api.js';
|
||||
import { ajax, js2o } from './utils.js';
|
||||
import { base } from '../../geo/base.js';
|
||||
import { load } from '../../load/file.js';
|
||||
import { space } from '../../moto/space.js';
|
||||
import { Widget, newWidget } from './widget.js';
|
||||
import { Packer } from '../ui/pack.js';
|
||||
|
||||
import { COLOR, MODES } from './consts.js';
|
||||
import { THREE } from '../../ext/three.js';
|
||||
|
||||
const V0 = new THREE.Vector3(0,0,0);
|
||||
|
||||
let setbounds = undefined;
|
||||
let grouping = false;
|
||||
let topZ = 0;
|
||||
|
||||
function current() {
|
||||
return api.conf.get();
|
||||
}
|
||||
|
||||
function get_mode() {
|
||||
return api.mode.get_id();
|
||||
}
|
||||
|
||||
function platformUpdateOrigin(update_bounds = true) {
|
||||
if (update_bounds) {
|
||||
platform.update_bounds();
|
||||
}
|
||||
|
||||
const settings = current();
|
||||
const { device, process, controller, stock, bounds } = settings;
|
||||
const { FDM, CAM, SLA, LASER, DRAG, WJET, WEDM } = MODES;
|
||||
const TWOD = [ LASER, DRAG, WJET, WEDM ];
|
||||
const MODE = get_mode();
|
||||
|
||||
let ruler = controller.showRulers;
|
||||
let stockCenter = stock.center || { x: 0, y: 0, z: 0 };
|
||||
let isIndexed = process.camStockIndexed;
|
||||
let isBelt = device.bedBelt;
|
||||
let origin = settings.origin = { x: 0, y: 0, z: 0 };
|
||||
let center = MODE === MODES.FDM ? device.originCenter || device.bedRound :
|
||||
MODE === MODES.SLA ? false :
|
||||
MODE === MODES.CAM ? process.camOriginCenter :
|
||||
device.originCenter || process.ctOriginCenter;
|
||||
|
||||
if (MODE === MODES.CAM && process.camOriginTop) {
|
||||
origin.z = stock.z + 0.01;
|
||||
}
|
||||
|
||||
if (!center) {
|
||||
if (MODE === MODES.CAM) {
|
||||
origin.x = (-stock.x / 2) + stockCenter.x;
|
||||
origin.y = (stock.y / 2) - stockCenter.y;
|
||||
} else {
|
||||
if (TWOD.contains(MODE) && process.ctOriginBounds) {
|
||||
let b = bounds;
|
||||
origin.x = b.min.x,
|
||||
origin.y = -b.min.y
|
||||
} else {
|
||||
origin.x = -device.bedWidth / 2;
|
||||
origin.y = device.bedDepth / 2;
|
||||
}
|
||||
}
|
||||
} else if (MODE === MODES.CAM) {
|
||||
origin.x = stockCenter.x;
|
||||
origin.y = -stockCenter.y;
|
||||
} else if (TWOD.contains(MODE) && process.ctOriginBounds ) {
|
||||
let b = bounds,
|
||||
mx = bounds.min.x,
|
||||
my = bounds.min.y,
|
||||
xd = b.max.x - mx,
|
||||
yd = b.max.y - my;
|
||||
origin.x += (mx + xd / 2);
|
||||
origin.y -= (my + yd / 2);
|
||||
} else if (isBelt) {
|
||||
origin.y = device.bedDepth / 2;
|
||||
}
|
||||
|
||||
// CNC origin offsets
|
||||
if (MODE === MODES.CAM) {
|
||||
origin.x += process.camOriginOffX;
|
||||
origin.y -= process.camOriginOffY;
|
||||
origin.z += process.camOriginOffZ;
|
||||
if (isIndexed) {
|
||||
origin.y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// WireEDM origin offsets
|
||||
if (MODE === MODES.WEDM && process.ctOriginBounds) {
|
||||
origin.x -= process.ctOriginOffX;
|
||||
origin.y += process.ctOriginOffY;
|
||||
}
|
||||
space.platform.setRulers(ruler, ruler, 1 / api.view.unit_scale(), 'X', isBelt ? 'Z' : 'Y');
|
||||
|
||||
let { x, y, z } = origin;
|
||||
let oz = process.camStockIndexed ? z / 2 : z;
|
||||
if (controller.showOrigin && MODE !== MODES.SLA) {
|
||||
space.platform.setOrigin(x, y, oz, true);
|
||||
} else {
|
||||
space.platform.setOrigin(x, y, oz, false);
|
||||
}
|
||||
}
|
||||
|
||||
function platformUpdateSize(updateDark = true) {
|
||||
const { process, device, controller } = current();
|
||||
const { showRulers, units } = controller;
|
||||
|
||||
space.platform.setRound(device.bedRound);
|
||||
space.platform.setSize(
|
||||
parseInt(device.bedWidth || 300),
|
||||
parseInt(device.bedDepth || 300),
|
||||
parseFloat(device.bedHeight || 5),
|
||||
parseFloat(device.maxHeight || 100)
|
||||
);
|
||||
|
||||
const ruler = showRulers,
|
||||
unitMM = units === 'mm',
|
||||
gridMajor = unitMM ? 25 : 25.4,
|
||||
gridMinor = unitMM ? 5 : 25.4 / 10;
|
||||
|
||||
if (updateDark) {
|
||||
if (controller.dark) {
|
||||
space.platform.set({ light: 0.08 });
|
||||
space.platform.setFont({rulerColor:'#888888'});
|
||||
space.platform.setGrid(gridMajor, gridMinor, 0x666666, 0x333333);
|
||||
space.platform.opacity(0.05);
|
||||
space.sky.set({ color: 0, ambient: { intensity: 0.6 } });
|
||||
document.body.classList.add('dark');
|
||||
} else {
|
||||
space.platform.set({ light: 0.08 });
|
||||
space.platform.setFont({rulerColor:'#333333'});
|
||||
space.platform.setGrid(gridMajor, gridMinor, 0x999999, 0xcccccc);
|
||||
space.platform.opacity(0.2);
|
||||
space.sky.set({ color: 0xffffff, ambient: { intensity: 1.1 } });
|
||||
document.body.classList.remove('dark');
|
||||
}
|
||||
space.platform.setSize();
|
||||
}
|
||||
|
||||
space.platform.setRulers(ruler, ruler, 1 / api.view.unit_scale(), 'X', device.bedBelt ? 'Z' : 'Y');
|
||||
platform.update_origin();
|
||||
}
|
||||
|
||||
function platformUpdateMidZ() {
|
||||
topZ = 0;
|
||||
api.widgets.each(widget => {
|
||||
topZ = Math.max(topZ, widget.mesh.getBoundingBox().max.z);
|
||||
});
|
||||
space.platform.setMaxZ(topZ);
|
||||
}
|
||||
|
||||
function platformUpdateTopZ() {
|
||||
const { process, stock } = current();
|
||||
const MODE = get_mode();
|
||||
api.widgets.each(widget => {
|
||||
if (MODE === MODES.CAM) {
|
||||
const bounds = widget.getBoundingBox();
|
||||
const wzmax = bounds.max.z;
|
||||
const zdelta = process.camZOffset || 0;
|
||||
switch (process.camZAnchor) {
|
||||
case 'top':
|
||||
widget.setTopZ(stock.z - zdelta, true);
|
||||
break;
|
||||
case 'middle':
|
||||
if (widget.isIndexed) {
|
||||
widget.setTopZ(zdelta, true);
|
||||
} else {
|
||||
widget.setTopZ(stock.z - (stock.z - wzmax) / 2 + zdelta, true);
|
||||
}
|
||||
break;
|
||||
case 'bottom':
|
||||
widget.setTopZ(wzmax + zdelta, true);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
widget.setTopZ(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function platformUpdateStock() {
|
||||
const settings = current();
|
||||
const { bounds, process, mode } = settings;
|
||||
const { camStockX, camStockY, camStockZ, camStockOffset, camStockIndexed } = process;
|
||||
if (mode === 'CAM') {
|
||||
let stock = settings.stock = {
|
||||
x: camStockX,
|
||||
y: camStockY,
|
||||
z: camStockZ
|
||||
};
|
||||
// drop back to offset mode if any stock dimension is 0
|
||||
if (camStockOffset || (stock.x * stock.y * stock.z === 0)) {
|
||||
stock.x += bounds.max.x - bounds.min.x;
|
||||
stock.y += bounds.max.y - bounds.min.y;
|
||||
stock.z += bounds.max.z - bounds.min.z;
|
||||
}
|
||||
stock.center = {
|
||||
x: (bounds.max.x + bounds.min.x) / 2,
|
||||
y: (bounds.max.y + bounds.min.y) / 2,
|
||||
z: camStockIndexed ? 0 : stock.z / 2
|
||||
};
|
||||
} else {
|
||||
settings.stock = {};
|
||||
}
|
||||
}
|
||||
|
||||
function platformSetBounds(bounds) {
|
||||
setbounds = bounds;
|
||||
return platformUpdateBounds();
|
||||
}
|
||||
|
||||
function platformUpdateBounds() {
|
||||
const bounds = setbounds || new THREE.Box3();
|
||||
if (!setbounds)
|
||||
api.widgets.each(widget => {
|
||||
let wp = widget.track.pos;
|
||||
let wb = widget.getBoundingBox().clone();
|
||||
wb.min.x += wp.x;
|
||||
wb.max.x += wp.x;
|
||||
wb.min.y += wp.y;
|
||||
wb.max.y += wp.y;
|
||||
bounds.union(wb);
|
||||
});
|
||||
if (bounds.isEmpty()) {
|
||||
bounds.set(V0, V0);
|
||||
}
|
||||
current().bounds = bounds;
|
||||
platformUpdateStock();
|
||||
platformUpdateTopZ();
|
||||
platformUpdateMidZ();
|
||||
platformUpdateOrigin(false);
|
||||
return bounds;
|
||||
}
|
||||
|
||||
function platformSelectedCount() {
|
||||
return api.view.is_arrange() ? api.selection.count() : 0;
|
||||
}
|
||||
|
||||
function platformUpdateSelected() {
|
||||
const settings = current();
|
||||
|
||||
const { device } = settings;
|
||||
const { extruders } = device;
|
||||
const { selection, ui } = api;
|
||||
|
||||
if (extruders) {
|
||||
for (let i = 0; i < extruders.length; i++) {
|
||||
let b = $(`sel-ext-${i}`);
|
||||
if (b) b.classList.remove('pop-sel');
|
||||
}
|
||||
selection.for_widgets(w => {
|
||||
w.setColor(COLOR.selected);
|
||||
let ext = api.widgets.annotate(w.id).extruder || 0;
|
||||
let b = $(`sel-ext-${ext}`);
|
||||
if (b) b.classList.add('pop-sel');
|
||||
w.saveState();
|
||||
}, true);
|
||||
} else {
|
||||
selection.for_widgets(w => {
|
||||
w.setColor(COLOR.selected);
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
|
||||
function platformSelect(widget, shift, recurse = true) {
|
||||
const { event, selection, view } = api;
|
||||
|
||||
if (!view.is_arrange()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// apply select to entire group
|
||||
if (recurse && widget && widget.group.length > 1) {
|
||||
for (let w of widget.group) {
|
||||
platformSelect(w, true, false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (selection.contains(widget)) {
|
||||
if (shift) {
|
||||
platform.deselect(widget, recurse)
|
||||
} else if (selection.count() > 1) {
|
||||
platform.deselect(undefined, recurse);
|
||||
platform.select(widget, false, recurse);
|
||||
}
|
||||
} else {
|
||||
// prevent selection in slice view
|
||||
if (!widget.isVisible()) {
|
||||
return;
|
||||
}
|
||||
if (!shift) {
|
||||
platform.deselect(undefined, recurse);
|
||||
}
|
||||
selection.add(widget);
|
||||
event.emit('widget.select', widget);
|
||||
widget.setColor(COLOR.selected);
|
||||
$(`ws-${widget.id}`)?.classList.add('selected');
|
||||
selection.update_info();
|
||||
}
|
||||
|
||||
platform.update_selected();
|
||||
space.update();
|
||||
}
|
||||
|
||||
function platformDeselect(widget, recurse = true) {
|
||||
const { selection, view } = api;
|
||||
|
||||
if (!view.is_arrange()) {
|
||||
// don't de-select and re-color widgets in,
|
||||
// for example, sliced or preview modes
|
||||
return;
|
||||
}
|
||||
|
||||
// apply deselect to entire group
|
||||
if (recurse && widget && widget.group.length > 1) {
|
||||
for (let w of widget.group) {
|
||||
platformDeselect(w, false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!widget) {
|
||||
api.widgets.each(function(widget) {
|
||||
platform.deselect(widget);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (selection.remove(widget)) {
|
||||
api.event.emit('widget.deselect', widget);
|
||||
}
|
||||
|
||||
$(`ws-${widget.id}`)?.classList.remove('selected');
|
||||
widget.setColor(COLOR.deselected);
|
||||
platform.update_selected();
|
||||
selection.update_info();
|
||||
space.update();
|
||||
}
|
||||
|
||||
function platformLoad(url, onload) {
|
||||
if (url.toLowerCase().indexOf(".stl") > 0) {
|
||||
platform.load_stl(url, onload);
|
||||
} else {
|
||||
ajax(url, vertices => {
|
||||
vertices = js2o(vertices).toFloat32();
|
||||
let widget = newWidget().loadVertices(vertices);
|
||||
widget.meta.url = url;
|
||||
platform.add(widget);
|
||||
if (onload) onload(vertices, widget);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function platformLoadSTL(url, onload, formdata, credentials, headers) {
|
||||
new load.STL().load(url, (vertices, filename) => {
|
||||
if (vertices) {
|
||||
let widget = newWidget().loadVertices(vertices);
|
||||
widget.meta.file = filename;
|
||||
platform.add(widget);
|
||||
if (onload) {
|
||||
onload(vertices, widget);
|
||||
}
|
||||
}
|
||||
}, formdata, 1 / api.view.unit_scale(), credentials, headers);
|
||||
}
|
||||
|
||||
function platformLoadURL(url, options = {}) {
|
||||
platform.group();
|
||||
load.URL.load(url, options).then(objects => {
|
||||
let widgets = [];
|
||||
for (let object of objects) {
|
||||
let widget = newWidget(undefined, options.group).loadVertices(object.mesh);
|
||||
widget.meta.file = object.file;
|
||||
platform.add(widget);
|
||||
widgets.push(widget);
|
||||
}
|
||||
platform.group_done();
|
||||
api.event.emit("load.url", {
|
||||
url,
|
||||
options,
|
||||
widgets
|
||||
});
|
||||
}).catch(error => {
|
||||
api.show.alert(error);
|
||||
});
|
||||
}
|
||||
|
||||
function platformGroup() {
|
||||
grouping = true;
|
||||
}
|
||||
|
||||
// called after all new widgets are loaded to update group positions
|
||||
function platformGroupDone(skipLayout) {
|
||||
grouping = false;
|
||||
Widget.Groups.loadDone();
|
||||
if (api.feature.drop_layout && !skipLayout) {
|
||||
platform.layout();
|
||||
}
|
||||
}
|
||||
|
||||
let deferred = [];
|
||||
let deferTimeout;
|
||||
|
||||
function platformAdd(widget, shift, nolayout, defer) {
|
||||
api.widgets.add(widget);
|
||||
space.world.add(widget.mesh);
|
||||
widget.anno = widget.anno || {};
|
||||
widget.anno.extruder = widget.anno.extruder || 0;
|
||||
if (defer) {
|
||||
deferred.push({widget, shift, nolayout});
|
||||
clearTimeout(deferTimeout);
|
||||
deferTimeout = setTimeout(platformAddDeferred, 150);
|
||||
} else {
|
||||
platform.select(widget, shift);
|
||||
platform.update_bounds();
|
||||
api.space.auto_save();
|
||||
platformChanged();
|
||||
api.event.emit('widget.add', widget);
|
||||
if (nolayout) {
|
||||
return;
|
||||
}
|
||||
if (!grouping) {
|
||||
platformGroupDone(nolayout);
|
||||
if (!current().controller.autoLayout) {
|
||||
positionNewWidget(widget);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function platformAddDeferred() {
|
||||
let skiplayout = false;
|
||||
for (let rec of deferred) {
|
||||
let { widget, shift, nolayout } = rec;
|
||||
skiplayout |= nolayout;
|
||||
// platform.select(widget, shift);
|
||||
if (!nolayout && !current().controller.autoLayout) {
|
||||
positionNewWidget(widget);
|
||||
}
|
||||
}
|
||||
if (!grouping) {
|
||||
platformGroupDone(skiplayout);
|
||||
}
|
||||
api.event.emit('widget.add', deferred.map(r => r.widget));
|
||||
platform.update_bounds();
|
||||
api.space.auto_save();
|
||||
platformChanged();
|
||||
deferred = [];
|
||||
}
|
||||
|
||||
function positionNewWidget(widget) {
|
||||
if (api.widgets.count() <= 1) {
|
||||
return;
|
||||
}
|
||||
const settings = current();
|
||||
const { device } = settings;
|
||||
const { bedWidth, bedDepth } = device;
|
||||
const wbb = widget.getBoundingBox();
|
||||
const dim = {
|
||||
x: wbb.max.x - wbb.min.x,
|
||||
y: wbb.max.y - wbb.min.y
|
||||
};
|
||||
const hdim = {
|
||||
x: dim.x / 2,
|
||||
y: dim.y / 2
|
||||
};
|
||||
const bounds = base.newBounds();
|
||||
const target = base.newBounds();
|
||||
const DEG2RAD = Math.PI / 180;
|
||||
const WIDGETS = api.widgets.all();
|
||||
// look for best position for new widget that doesn't collide
|
||||
outer: for (let rad = 10; rad < 200; rad += 10) {
|
||||
inner: for (let d = 0; d < 360; d += 1) {
|
||||
let dx = Math.cos(d * DEG2RAD) * rad;
|
||||
let dy = Math.sin(d * DEG2RAD) * rad;
|
||||
bounds.set(dx - hdim.x, dx + hdim.x, dy - hdim.y, dy + hdim.y);
|
||||
for (let w = 0, wl = WIDGETS.length; w < wl; w++) {
|
||||
let wt = WIDGETS[w];
|
||||
if (wt === widget) {
|
||||
continue;
|
||||
}
|
||||
let tpo = wt.track.pos;
|
||||
let tbb = wt.getBoundingBox();
|
||||
let dim = {
|
||||
x: (tbb.max.x - tbb.min.x) / 2,
|
||||
y: (tbb.max.y - tbb.min.y) / 2
|
||||
};
|
||||
target.set(tpo.x - dim.x, tpo.x + dim.x, tpo.y - dim.y, tpo.y + dim.y);
|
||||
if (target.overlaps(bounds, 5)) {
|
||||
continue inner;
|
||||
}
|
||||
}
|
||||
widget._move(dx, dy, widget.track.pos.z, true);
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function platformDelete(widget, defer) {
|
||||
if (!widget) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(widget)) {
|
||||
const mc = widget.slice();
|
||||
for (let i = 0; i < mc.length; i++) {
|
||||
platform.delete(mc[i].widget || mc[i], true);
|
||||
}
|
||||
platformDeletePost();
|
||||
api.event.emit('widget.delete', widget);
|
||||
return;
|
||||
}
|
||||
api.client.clear(widget);
|
||||
api.widgets.remove(widget);
|
||||
api.selection.remove(widget);
|
||||
Widget.Groups.remove(widget);
|
||||
space.world.remove(widget.mesh);
|
||||
if (!defer) {
|
||||
platformDeletePost();
|
||||
api.event.emit('widget.delete', widget);
|
||||
}
|
||||
}
|
||||
|
||||
function platformDeletePost() {
|
||||
api.view.update_slider_max();
|
||||
platform.update_bounds();
|
||||
if (get_mode() !== MODES.FDM) {
|
||||
platform.layout();
|
||||
}
|
||||
space.update();
|
||||
platform.update_selected();
|
||||
if (api.feature.drop_layout) {
|
||||
platform.layout();
|
||||
}
|
||||
api.space.auto_save();
|
||||
platformChanged();
|
||||
}
|
||||
|
||||
// render list of current widgets
|
||||
function platformChanged() {
|
||||
h.bind($('ws-widgets'), api.widgets.all().map(w => {
|
||||
let color;
|
||||
let file = w.meta.file || 'no name';
|
||||
let fsho = file.length > 25 ? file.slice(0,20) + '..' + file.slice(-2) : file;
|
||||
return [
|
||||
h.div({
|
||||
onmouseenter() {
|
||||
color = w.getColor();
|
||||
w.setColor(0x0088ff, null, false);
|
||||
},
|
||||
onmouseleave() {
|
||||
w.setColor(color, null, false);
|
||||
},
|
||||
},[
|
||||
h.div({ class: "widpop" }, [
|
||||
h.div({ class: "widopt" }, [
|
||||
h.button(
|
||||
{
|
||||
title: "save",
|
||||
onclick() { w.saveToCatalog(null,true) }
|
||||
},
|
||||
[ h.i({ class:"fas fa-save" }) ]
|
||||
),
|
||||
h.button(
|
||||
{
|
||||
title: "rename",
|
||||
onclick() { api.widgets.rename(w) }
|
||||
},
|
||||
[ h.i({ class:"fas fa-pen-to-square" }) ]
|
||||
),
|
||||
h.button(
|
||||
{
|
||||
title: "replace",
|
||||
onclick() { api.widgets.replace(null,w) }
|
||||
},
|
||||
[ h.i({ class:"fas fa-rotate" }) ]
|
||||
),
|
||||
h.button(
|
||||
{
|
||||
id: `w-en-${w.id}`,
|
||||
title: "toggle disabled",
|
||||
class: w.meta.disabled ? "disabled" : "",
|
||||
onclick() {
|
||||
let dis = w.meta.disabled = !w.meta.disabled;
|
||||
let sel = api.selection.contains(w);
|
||||
api.uc.setClass($(`w-en-${w.id}`), "disabled", dis);
|
||||
w.setColor(sel ? COLOR.selected : COLOR.deselected, null, false);
|
||||
}
|
||||
},
|
||||
[ h.i({ class:"fas fa-ban" }) ]
|
||||
),
|
||||
h.button(
|
||||
{
|
||||
title: "delete",
|
||||
onclick() { platformDelete(w) }
|
||||
},
|
||||
[ h.i({ class:"fas fa-trash" }), ]
|
||||
)
|
||||
]),
|
||||
h.button([
|
||||
h.i({ class: "fa-solid fa-caret-left" })
|
||||
])
|
||||
]),
|
||||
h.button({
|
||||
_: fsho,
|
||||
id: `ws-${w.id}`,
|
||||
class: "grow name",
|
||||
title: file,
|
||||
// onmouseenter() {
|
||||
// color = w.getColor();
|
||||
// w.setColor(0x0088ff, null, false);
|
||||
// },
|
||||
// onmouseleave() {
|
||||
// w.setColor(color, null, false);
|
||||
// },
|
||||
onclick() {
|
||||
platformSelect(w, true, false);
|
||||
color = w.getColor();
|
||||
}
|
||||
})
|
||||
])
|
||||
]
|
||||
}));
|
||||
}
|
||||
|
||||
function platformSelectAll() {
|
||||
api.widgets.each(widget => {
|
||||
platform.select(widget, true, false)
|
||||
});
|
||||
}
|
||||
|
||||
function platformLayout() {
|
||||
const MODE = get_mode();
|
||||
const settings = current();
|
||||
const { process, device, controller } = settings;
|
||||
const { ui } = api;
|
||||
|
||||
const auto = ui.autoLayout.checked,
|
||||
isBelt = device.bedBelt,
|
||||
isArrange = api.view.is_arrange(),
|
||||
layout = isArrange && auto;
|
||||
|
||||
let gap = controller.spaceLayout;
|
||||
|
||||
switch (MODE) {
|
||||
case MODES.SLA:
|
||||
gap = gap || (process.slaSupportLayers && process.slaSupportDensity ? 2 : 1);
|
||||
break;
|
||||
case MODES.CAM:
|
||||
case MODES.LASER:
|
||||
gap = gap || process.ctOutTileSpacing || 1;
|
||||
break;
|
||||
case MODES.FDM:
|
||||
gap = gap || ((process.sliceSupportExtra || 0) * 2) + 1;
|
||||
// auto resize device to support a larger object
|
||||
if (isBelt) {
|
||||
fitDeviceToWidgets();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
api.view.set_arrange();
|
||||
api.view.hide_slices();
|
||||
api.space.auto_save();
|
||||
|
||||
if (!isArrange) {
|
||||
// skip auto-layout when not in arrange mode
|
||||
api.event.emit('platform.layout');
|
||||
return space.update();
|
||||
}
|
||||
|
||||
// do not layout when switching back from slice view
|
||||
if (!auto || (!space && !layout)) {
|
||||
api.event.emit('platform.layout');
|
||||
return space.update();
|
||||
}
|
||||
|
||||
// TODO: in CNC mode with >1 widget, force layout min spacing @ largest tool diameter
|
||||
|
||||
// space parts to account for anchor in belt mode
|
||||
if (isBelt) {
|
||||
gap += process.firstLayerBeltLead || 0;
|
||||
const WIDGETS = api.widgets.all();
|
||||
let bounds = platformUpdateBounds(),
|
||||
movey = -(device.bedDepth / 2);
|
||||
for (let widget of WIDGETS) {
|
||||
let ylen = widget.track.box.h;
|
||||
// only move the root widget in the group
|
||||
if (widget.id === widget.group.id) {
|
||||
widget.move(0, movey + ylen/2, 0, true);
|
||||
movey += ylen + gap;
|
||||
}
|
||||
}
|
||||
if (controller.spaceLayout === 0) {
|
||||
let sum = 0;
|
||||
let sorted = WIDGETS.sort((a, b) => a.track.pos.y - b.track.pos.y);
|
||||
sorted.forEach(w => {
|
||||
w.move(0, sum, 0);
|
||||
sum += w.track.box.d + 1 + gap;
|
||||
});
|
||||
}
|
||||
if (controller.spaceRandoX) {
|
||||
for (let w of WIDGETS) {
|
||||
w.move(
|
||||
(0.5 - Math.random()) * (device.bedWidth - w.track.box.w - process.firstLayerBrim),
|
||||
0, 0);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let i, m, sz = space.platform.size(),
|
||||
mp = [sz.x, sz.y],
|
||||
ms = [mp[0] / 2, mp[1] / 2],
|
||||
c = Widget.Groups.blocks().sort(),
|
||||
p = new Packer(ms[0], ms[1], gap).fit(c);
|
||||
|
||||
while (!p.packed) {
|
||||
ms[0] *= 1.1;
|
||||
ms[1] *= 1.1;
|
||||
p = new Packer(ms[0], ms[1], gap).fit(c);
|
||||
}
|
||||
|
||||
for (i = 0; i < c.length; i++) {
|
||||
m = c[i];
|
||||
m.fit.x += m.w / 2;
|
||||
m.fit.y += m.h / 2;
|
||||
m.move(p.max.w / 2 - m.fit.x, p.max.h / 2 - m.fit.y, 0, true);
|
||||
}
|
||||
}
|
||||
|
||||
platform.update_origin();
|
||||
space.update();
|
||||
api.event.emit('platform.layout');
|
||||
}
|
||||
|
||||
function platformLoadWidget(group, vertices, filename) {
|
||||
const widget = newWidget(undefined, group).loadVertices(vertices.toFloat32(), true);
|
||||
widget.meta.file = filename;
|
||||
if (filename) widget.saveToCatalog(filename);
|
||||
platformAdd(widget);
|
||||
return widget;
|
||||
}
|
||||
|
||||
function platformLoadFiles(files, group) {
|
||||
platform.group();
|
||||
let loading = files.length;
|
||||
for (let file of files) {
|
||||
const name = file.name,
|
||||
reader = new FileReader(),
|
||||
lower = name.toLowerCase(),
|
||||
isstl = lower.endsWith(".stl"),
|
||||
isobj = lower.endsWith(".obj"),
|
||||
is3mf = lower.endsWith(".3mf"),
|
||||
issvg = lower.endsWith(".svg"),
|
||||
ispng = lower.endsWith(".png"),
|
||||
isjpg = lower.endsWith(".jpg"),
|
||||
iskmz = lower.endsWith(".kmz"),
|
||||
isini = lower.endsWith(".ini"),
|
||||
isgbr = lower.endsWith(".gbr"),
|
||||
israw = lower.endsWith(".raw") || lower.indexOf('.') < 0,
|
||||
isset = lower.endsWith(".b64") || lower.endsWith(".km"),
|
||||
isgcode = lower.endsWith(".gcode") || lower.endsWith(".nc");
|
||||
reader.file = file;
|
||||
reader.onloadend = function(e) {
|
||||
const data = e.target.result;
|
||||
function load_dec() {
|
||||
if (--loading === 0) {
|
||||
platform.group_done(isgcode);
|
||||
}
|
||||
}
|
||||
if (israw) {
|
||||
platformLoadWidget(group, JSON.parse(data));
|
||||
load_dec();
|
||||
} else if (api.feature.on_load && (isstl || isobj || is3mf)) {
|
||||
api.feature.on_load(data, file);
|
||||
load_dec();
|
||||
} else if (api.feature.on_add_stl && isstl) {
|
||||
api.feature.on_add_stl(data, file);
|
||||
load_dec();
|
||||
} else if (isstl) {
|
||||
const stl = new load.STL().parse(data, api.view.unit_scale());
|
||||
platformLoadWidget(group, stl, name);
|
||||
load_dec();
|
||||
} else if (isobj) {
|
||||
const objs = load.OBJ.parse(data.textDecode('utf-8'));
|
||||
const ondn = function() {
|
||||
for (let obj of objs) {
|
||||
platformLoadWidget(group, obj, obj.name ? `${obj.name}-${name}` : name);
|
||||
}
|
||||
load_dec();
|
||||
};
|
||||
if (objs.length > 1 && !group) {
|
||||
api.uc.confirm('group objects?').then(ok => {
|
||||
group = ok ? [] : group;
|
||||
ondn();
|
||||
});
|
||||
} else {
|
||||
ondn();
|
||||
}
|
||||
} else if (isgbr) {
|
||||
let text = data.textDecode('utf-8');
|
||||
if (api.conf.get().controller.devel) {
|
||||
api.event.emit('cam.parse.gerber', { data: text });
|
||||
} else {
|
||||
api.client.gerber2mesh(text, progress => {
|
||||
api.show.progress(progress, "converting");
|
||||
}, vertices => {
|
||||
api.show.progress(0);
|
||||
let wid = platformLoadWidget(group, vertices, name);
|
||||
if (api.mode.is_cam()) {
|
||||
// attach raw illustration
|
||||
// api.event.emit('cam.parse.gerber', { data: text, mesh: wid.mesh });
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (is3mf) {
|
||||
let odon = function(models) {
|
||||
let msg = api.show.alert('Adding Objects');
|
||||
for (let model of models) {
|
||||
platformLoadWidget(group, model.faces, model.name ? `${model.name}-${name}` : name);
|
||||
}
|
||||
load_dec();
|
||||
api.hide.alert(msg);
|
||||
}
|
||||
let msg = api.show.alert('Decoding 3MF');
|
||||
load.TMF.parseAsync(data).then(models => {
|
||||
api.hide.alert(msg);
|
||||
if (models.length > 1 && !group) {
|
||||
api.uc.confirm(`group ${models.length} objects?`).then(ok => {
|
||||
if (ok) {
|
||||
group = [];
|
||||
}
|
||||
odon(models);
|
||||
});
|
||||
} else {
|
||||
odon(models);
|
||||
}
|
||||
});
|
||||
} else if (isgcode) {
|
||||
api.function.parse(data.textDecode('utf-8'), 'gcode');
|
||||
load_dec();
|
||||
} else if (issvg) {
|
||||
loadSVGDialog(opt => {
|
||||
group = group || [];
|
||||
let svg = load.SVG.parse(data.textDecode('utf-8'), opt);
|
||||
let ind = 0;
|
||||
if (svg.length === 0) {
|
||||
api.show.alert(`SVG contains no polylines`, 10);
|
||||
api.show.alert(`Fonts must be converted to paths`, 10);
|
||||
}
|
||||
for (let v of svg) {
|
||||
platformLoadWidget(group, svg[ind++], ind ? `${name}-${ind}` : name);
|
||||
}
|
||||
load_dec();
|
||||
});
|
||||
}
|
||||
else if (iskmz) api.settings.import_zip(data, true);
|
||||
else if (isset) api.settings.import(data.textDecode('utf-8'), true);
|
||||
else if (ispng) api.image.dialog(data, name);
|
||||
else if (isjpg) api.image.convert(data, name);
|
||||
else if (isini) api.settings.import_prusa(data.textDecode('utf-8'));
|
||||
else api.show.alert(`Unsupported file: ${reader.file.name}`);
|
||||
};
|
||||
reader.readAsArrayBuffer(reader.file);
|
||||
}
|
||||
}
|
||||
|
||||
function loadSVGDialog(doit) {
|
||||
const opt = {pre: [
|
||||
"<div class='f-col a-center'>",
|
||||
" <h3>Import SVG</h3>",
|
||||
" <p class='t-just' style='width:300px;line-height:1.5em'>",
|
||||
" Extrude a 3D model from a 2D SVG.",
|
||||
" Fonts must be converted to paths in an SVG editor.",
|
||||
" </p>",
|
||||
" <div class='f-row t-right'><table>",
|
||||
" <tr><th>z height in mm</th><td><input id='svg-depth' value='5' size='3'></td></tr>",
|
||||
" <tr><th title='higher values results in more points. floating point values like 0.5 are accepted'>arc segments / mm</th><td><input id='svg-arcs' value='1' size='3'></td></tr>",
|
||||
" <tr><th title='minimum number of segments in an arc. helps better represent small arcs'>minimum arc segments</th><td><input id='svg-marc' value='10' size='3'></td></tr>",
|
||||
" <tr><th title='interpret dimensions as pixels using dpi'>pixel dpi (optional)</th><td><input id='svg-dpi' value='0' size='3'></td></tr>",
|
||||
" <tr><th>nest shapes</th><td><input id='svg-nest' value='1' type='checkbox' checked></td></tr>",
|
||||
" </table></div>",
|
||||
"</div>"
|
||||
]};
|
||||
api.uc.confirm(undefined, {convert:true, cancel:false}, undefined, opt).then((ok) => {
|
||||
let depth = Math.max(0.1, parseFloat($('svg-depth').value));
|
||||
let arcs = Math.max(0.01, parseFloat($('svg-arcs').value));
|
||||
let marc = Math.max(1, parseInt($('svg-marc').value));
|
||||
let sdpi = Math.max(0, parseInt($('svg-dpi').value));
|
||||
let soup = $('svg-nest').checked;
|
||||
ok && doit({ soup, resolution: arcs, segmin: marc, depth, dpi: sdpi });
|
||||
});
|
||||
}
|
||||
|
||||
function fitDeviceToWidgets() {
|
||||
let maxy = 0;
|
||||
api.widgets.each(widget => {
|
||||
let wb = widget.mesh.getBoundingBox().clone();
|
||||
maxy = Math.max(maxy, wb.max.y - wb.min.y);
|
||||
});
|
||||
const { device } = current();
|
||||
if (maxy > device.bedDepth) {
|
||||
device.bedDepthSave = device.bedDepth;
|
||||
device.bedDepth = maxy + 10;
|
||||
space.platform.setSize(
|
||||
parseInt(device.bedWidth),
|
||||
parseInt(device.bedDepth),
|
||||
parseFloat(device.bedHeight),
|
||||
parseFloat(device.maxHeight)
|
||||
);
|
||||
space.platform.update();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// extend API (api.platform)
|
||||
export const platform = {
|
||||
fit: fitDeviceToWidgets,
|
||||
add: platformAdd,
|
||||
changed: platformChanged,
|
||||
delete: platformDelete,
|
||||
layout: platformLayout,
|
||||
group: platformGroup,
|
||||
group_done: platformGroupDone,
|
||||
load: platformLoad,
|
||||
load_stl: platformLoadSTL,
|
||||
load_url: platformLoadURL,
|
||||
load_files: platformLoadFiles,
|
||||
load_verts: platformLoadWidget,
|
||||
deselect: platformDeselect,
|
||||
select: platformSelect,
|
||||
select_all: platformSelectAll,
|
||||
selected_count: platformSelectedCount,
|
||||
set_bounds: platformSetBounds,
|
||||
update_origin: platformUpdateOrigin,
|
||||
update_bounds: platformUpdateBounds,
|
||||
update_size: platformUpdateSize,
|
||||
update_top_z: platformUpdateTopZ,
|
||||
update_selected: platformUpdateSelected,
|
||||
update: space.platform.update,
|
||||
set_font: space.platform.setFont,
|
||||
show_axes: space.platform.showAxes,
|
||||
show_volume: space.platform.showVolume,
|
||||
top_z() { return topZ },
|
||||
clear() { api.space.clear(); api.space.save(true) }
|
||||
};
|
||||
|
|
@ -1,328 +0,0 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { api } from './api.js';
|
||||
import { space } from '../../moto/space.js';
|
||||
import { THREE } from '../../ext/three.js';
|
||||
import { tool as MeshTool } from '../../mesh/tool.js';
|
||||
import { encode as objEncode } from '../../load/obj.js';
|
||||
import { encode as stlEncode } from '../../load/stl.js';
|
||||
|
||||
const selectedMeshes = [];
|
||||
|
||||
function for_groups(fn) {
|
||||
let groups = widgets(true).map(w => w.group).uniq();
|
||||
for (let group of groups) {
|
||||
fn(group[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function for_widgets(fn, noauto) {
|
||||
let m = selectedMeshes;
|
||||
let w = api.widgets.all();
|
||||
if (m.length === 0 && w.length) {
|
||||
m = noauto ? [] : w.map(w => w.mesh);
|
||||
}
|
||||
m.slice().forEach(mesh => { fn(mesh.widget) });
|
||||
}
|
||||
|
||||
function for_status(fn) {
|
||||
api.widgets.all().forEach(w => {
|
||||
fn(w, selectedMeshes.contains(w.mesh));
|
||||
});
|
||||
}
|
||||
|
||||
function for_meshes(fn) {
|
||||
selectedMeshes.slice().forEach(mesh => { fn(mesh) });
|
||||
}
|
||||
|
||||
function move(x, y, z, abs) {
|
||||
if (!api.view.is_arrange()) {
|
||||
return;
|
||||
}
|
||||
for_groups(w => w.move(x, y, z, abs));
|
||||
update_bounds();
|
||||
api.platform.update_bounds();
|
||||
api.event.emit('selection.move', {x, y, z, abs});
|
||||
space.update();
|
||||
api.space.auto_save();
|
||||
}
|
||||
|
||||
function update_info() {
|
||||
const ui = api.ui;
|
||||
let bounds = new THREE.Box3(), track;
|
||||
for_meshes(mesh => {
|
||||
let box = mesh.getBoundingBox().clone();
|
||||
track = mesh.widget.track;
|
||||
let {pos} = track
|
||||
box.min.add(pos);
|
||||
box.max.add(pos);
|
||||
bounds = bounds.union(box);
|
||||
});
|
||||
if (bounds.min.x === Infinity) {
|
||||
if (selectedMeshes.length === 0) {
|
||||
ui.sizeX.value = 0;
|
||||
ui.sizeY.value = 0;
|
||||
ui.sizeZ.value = 0;
|
||||
ui.scaleX.value = 1;
|
||||
ui.scaleY.value = 1;
|
||||
ui.scaleZ.value = 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
let dx = bounds.max.x - bounds.min.x,
|
||||
dy = bounds.max.y - bounds.min.y,
|
||||
dz = bounds.max.z - bounds.min.z,
|
||||
scale = api.view.unit_scale();
|
||||
ui.sizeX.value = ui.sizeX.was = (dx / scale).round(2)
|
||||
ui.sizeY.value = ui.sizeY.was = (dy / scale).round(2)
|
||||
ui.sizeZ.value = ui.sizeZ.was = (dz / scale).round(2)
|
||||
ui.scaleX.value = ui.scaleX.was = track.scale.x.round(2);
|
||||
ui.scaleY.value = ui.scaleY.was = track.scale.y.round(2);
|
||||
ui.scaleZ.value = ui.scaleZ.was = track.scale.z.round(2);
|
||||
update_bounds();
|
||||
}
|
||||
|
||||
function update_bounds(widgets) {
|
||||
const settings = api.conf.get();
|
||||
// update bounds on selection for drag limiting
|
||||
const isBelt = settings.device.bedBelt;
|
||||
if (isBelt) {
|
||||
if (api.platform.fit()) {
|
||||
api.platform.update_origin();
|
||||
space.update();
|
||||
}
|
||||
}
|
||||
const dvy = settings.device.bedDepth;
|
||||
const dvx = settings.device.bedWidth;
|
||||
const bounds_sel = new THREE.Box3();
|
||||
if (!widgets) {
|
||||
widgets = selectedMeshes.map(m => m.widget);
|
||||
}
|
||||
for (let widget of widgets) {
|
||||
const wp = widget.track.pos;
|
||||
const bx = widget.track.box;
|
||||
const miny = wp.y - bx.h / 2 + dvy / 2;
|
||||
const maxy = wp.y + bx.h / 2 + dvy / 2;
|
||||
const minx = wp.x - bx.w / 2 + dvx / 2;
|
||||
const maxx = wp.x + bx.w / 2 + dvx / 2;
|
||||
// keep widget in bounds when rotated or scaled
|
||||
const ylo = miny < 0;
|
||||
const yhi = !isBelt && maxy > dvy
|
||||
if (ylo && !yhi) {
|
||||
widget.move(0, -miny, 0);
|
||||
} else if (yhi && !ylo) {
|
||||
widget.move(0, dvy - maxy, 0);
|
||||
}
|
||||
const xlo = minx < 0;
|
||||
const xhi = maxx > dvx;
|
||||
if (xlo && !xhi) {
|
||||
widget.move(-minx, 0, 0);
|
||||
} else if (xhi && !xlo) {
|
||||
widget.move(dvx - maxx, 0, 0);
|
||||
}
|
||||
|
||||
const wb = widget.mesh.getBoundingBox().clone();
|
||||
wb.min.x += wp.x;
|
||||
wb.max.x += wp.x;
|
||||
wb.min.y += wp.y;
|
||||
wb.max.y += wp.y;
|
||||
bounds_sel.union(wb);
|
||||
}
|
||||
settings.bounds_sel = bounds_sel;
|
||||
}
|
||||
|
||||
function duplicate() {
|
||||
if (!api.view.is_arrange()) {
|
||||
return;
|
||||
}
|
||||
for_widgets(widget => {
|
||||
const mesh = widget.mesh;
|
||||
const bb = mesh.getBoundingBox();
|
||||
const ow = widget;
|
||||
const nw = api.widgets.new().loadGeometry(mesh.geometry.clone());
|
||||
nw.meta.file = ow.meta.file;
|
||||
nw.meta.vertices = ow.meta.vertices;
|
||||
nw.anno = ow.annotations();
|
||||
nw.move(bb.max.x - bb.min.x + 1, 0, 0);
|
||||
api.platform.add(nw, true);
|
||||
api.event.emit("widget.duplicate", nw, ow);
|
||||
});
|
||||
}
|
||||
|
||||
function mirror() {
|
||||
if (!api.view.is_arrange()) {
|
||||
return;
|
||||
}
|
||||
for_widgets(widget => {
|
||||
widget.mirror();
|
||||
});
|
||||
space.update();
|
||||
api.space.auto_save();
|
||||
}
|
||||
|
||||
function scale() {
|
||||
if (!api.view.is_arrange()) {
|
||||
return;
|
||||
}
|
||||
let args = [...arguments];
|
||||
for_groups(w => w.scale(...args));
|
||||
selection.update_bounds();
|
||||
api.platform.update_bounds();
|
||||
api.event.emit('selection.scale', args);
|
||||
// skip update if last argument is strictly 'false'
|
||||
if (args.last() === false) {
|
||||
return;
|
||||
}
|
||||
api.space.auto_save();
|
||||
selection.update_info();
|
||||
space.update();
|
||||
}
|
||||
|
||||
function rotate(x, y, z) {
|
||||
if (!api.view.is_arrange()) {
|
||||
return;
|
||||
}
|
||||
for_groups(w => w.rotate(x, y, z));
|
||||
selection.update_bounds();
|
||||
api.platform.update_bounds();
|
||||
api.event.emit('selection.rotate', {x, y, z});
|
||||
api.space.auto_save();
|
||||
selection.update_info();
|
||||
space.update();
|
||||
}
|
||||
|
||||
function merge({ deleteMerged = true }) {
|
||||
let sel = widgets();
|
||||
if (sel.length === 0) {
|
||||
sel = api.widgets.all();
|
||||
}
|
||||
let obj = [];
|
||||
sel.forEach(widget => {
|
||||
let {x, y, z} = widget.track.pos || { x:0, y:0, z:0 };
|
||||
let pos = widget.mesh.geometry.attributes.position;
|
||||
let pvals = pos.array;
|
||||
for (let i=0, il=pos.count; i<il; i += 3) {
|
||||
let pi = i * pos.itemSize;
|
||||
obj.push(pvals[pi++] + x, pvals[pi++] + y, pvals[pi++] + z);
|
||||
obj.push(pvals[pi++] + x, pvals[pi++] + y, pvals[pi++] + z);
|
||||
obj.push(pvals[pi++] + x, pvals[pi++] + y, pvals[pi++] + z);
|
||||
}
|
||||
});
|
||||
let w = api.platform.load_verts([], obj, "merged");
|
||||
if (deleteMerged) api.platform.delete(sel);
|
||||
api.platform.select(w);
|
||||
}
|
||||
|
||||
/**
|
||||
* Isolates the bodies within the selected widgets. If no widgets are selected,
|
||||
* it processes all available widgets. For each widget, it checks if a tool is
|
||||
* associated; if not, it creates a new MeshTool and indexes the vertices.
|
||||
* The function identifies isolated bodies within each widget. If a widget has
|
||||
* a single body, it remains selected. Otherwise, it creates new widgets for
|
||||
* each isolated body and selects them while deleting the original widget.
|
||||
*/
|
||||
function isolateBodies(){
|
||||
let sel = widgets();
|
||||
console.log('isolate', sel);
|
||||
if (sel.length === 0) {
|
||||
sel = api.widgets.all();
|
||||
}
|
||||
for(let widget of sel){
|
||||
if (!widget.tool) {
|
||||
let tool = widget.tool = new MeshTool();
|
||||
tool.index(widget.getGeoVertices({ unroll: true, translate:true }));
|
||||
}
|
||||
let bodies = widget.tool.isolateBodies()
|
||||
if(bodies.length ==1){
|
||||
api.platform.select(widget);
|
||||
}else{
|
||||
// console.log(bodies,Array.from(bodies.entries()))
|
||||
for(let [i,verts] of bodies.entries()){
|
||||
let w = api.platform.load_verts([], verts, `isolate ${i} of ${ widget.anno.file}`);
|
||||
api.platform.select(w)
|
||||
}
|
||||
api.platform.delete(widget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function exportWidgets(format = "stl") {
|
||||
let sel = widgets();
|
||||
if (sel.length === 0) {
|
||||
sel = api.widgets.all();
|
||||
}
|
||||
let facets = 0;
|
||||
let outs = [];
|
||||
sel.forEach(widget => {
|
||||
let mesh = widget.mesh;
|
||||
let geo = mesh.geometry;
|
||||
outs.push({geo, widget});
|
||||
facets += geo.attributes.position.count;
|
||||
});
|
||||
if (format === "obj") {
|
||||
let recs = outs.map(out => {
|
||||
let meta = out.widget.meta;
|
||||
let name = meta.file || 'unnamed';
|
||||
let { position } = out.geo.attributes;
|
||||
return { file: name, varr: position.array };
|
||||
});
|
||||
return objEncode(recs);
|
||||
}
|
||||
let recs = outs.map(out => {
|
||||
let { position } = out.geo.attributes;
|
||||
let pvals = position.array;
|
||||
let varr = pvals;
|
||||
|
||||
// Apply position offset if exporting multiple widgets
|
||||
if (outs.length > 1) {
|
||||
let {x, y, z} = out.widget.track.pos;
|
||||
varr = new Float32Array(pvals.length);
|
||||
for (let i = 0; i < pvals.length; i += 3) {
|
||||
varr[i] = pvals[i] + x;
|
||||
varr[i+1] = pvals[i+1] + y;
|
||||
varr[i+2] = pvals[i+2] + z;
|
||||
}
|
||||
}
|
||||
|
||||
return { file: out.widget.meta.file || 'unnamed', varr };
|
||||
});
|
||||
return stlEncode(recs);
|
||||
}
|
||||
|
||||
function widgets(orall) {
|
||||
let sel = selectedMeshes.slice().map(m => m.widget);
|
||||
return sel.length ? sel : orall ? api.widgets.all() : []
|
||||
}
|
||||
|
||||
function setDisabled(bool) {
|
||||
for_widgets(w => w.meta.disabled = bool);
|
||||
api.platform.update_selected();
|
||||
}
|
||||
|
||||
// extend API (api.selection)
|
||||
export const selection = {
|
||||
move,
|
||||
merge,
|
||||
isolateBodies,
|
||||
scale,
|
||||
rotate,
|
||||
mirror,
|
||||
duplicate,
|
||||
for_groups,
|
||||
for_meshes,
|
||||
for_status,
|
||||
for_widgets,
|
||||
update_bounds,
|
||||
update_info,
|
||||
widgets,
|
||||
export: exportWidgets,
|
||||
add(w) { selectedMeshes.addOnce(w.mesh) },
|
||||
delete() { api.platform.delete(widgets()) },
|
||||
remove(w) { return selectedMeshes.remove(w.mesh) },
|
||||
count() { return selectedMeshes.length },
|
||||
contains(w) { return selectedMeshes.indexOf(w.mesh) >= 0 },
|
||||
enable() { setDisabled(false) },
|
||||
disable() { setDisabled(true) },
|
||||
opacity() { api.widgets.opacity(...arguments) },
|
||||
meshes() { return selectedMeshes.slice() },
|
||||
};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { $ } from '../../moto/webui.js';
|
||||
import { api } from './api.js';
|
||||
import { api } from '../ui/api.js';
|
||||
import { load } from '../../load/file.js';
|
||||
import { Widget, newWidget } from './widget.js';
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue