721 lines
25 KiB
JavaScript
721 lines
25 KiB
JavaScript
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
|
|
|
"use strict";
|
|
|
|
// dep: moto.license
|
|
// dep: moto.webui
|
|
// dep: moto.client
|
|
// dep: moto.broker
|
|
// dep: moto.space
|
|
// dep: data.index
|
|
// dep: mesh.api
|
|
// dep: mesh.split
|
|
// dep: mesh.edges
|
|
// dep: mesh.model
|
|
// dep: mesh.build
|
|
// dep: load.file
|
|
// use: geo.polygons
|
|
gapp.main("main.mesh", [], (root) => {
|
|
|
|
const { Quaternion } = THREE;
|
|
const { broker } = gapp;
|
|
const { mesh, moto } = root;
|
|
const { space } = moto;
|
|
|
|
const version = mesh.version = '1.4.9';
|
|
const call = broker.send;
|
|
const dbindex = [ "admin", "space" ];
|
|
|
|
function log() {
|
|
return mesh.api.log.emit(...arguments);
|
|
}
|
|
|
|
// set below. called once the DOM readyState = complete
|
|
// this is the main() entrypoint called after all dependents load
|
|
function init() {
|
|
let stores = data.open('mesh', { stores: dbindex, version: 4 }).init(),
|
|
dark = false,
|
|
ortho = false,
|
|
zoomrev = true,
|
|
zoomspd = 1,
|
|
platform = space.platform,
|
|
db = mesh.db = {
|
|
admin: stores.promise('admin'),
|
|
space: stores.promise('space')
|
|
};
|
|
|
|
// mark init time and use count
|
|
db.admin.put("init", Date.now());
|
|
db.admin.get("uses").then(v => db.admin.put("uses", (v||0) + 1));
|
|
|
|
// setup default workspace
|
|
space.setAntiAlias(true);
|
|
space.useDefaultKeys(false);
|
|
space.init($('container'), delta => { }, ortho);
|
|
space.sky.set({
|
|
grid: false,
|
|
color: dark ? 0 : 0xffffff
|
|
});
|
|
platform.set({
|
|
volume: false,
|
|
round: false,
|
|
zOffset: 0,
|
|
opacity: 0,
|
|
color: 0xdddddd,
|
|
zoom: { reverse: true, speed: 1 },
|
|
size: { width: 2000, depth: 2000, height: 0.05, maxz: 2000 },
|
|
grid: { major: 25, minor: 5, zOffset: 0,
|
|
colorMajor: 0xcccccc, colorMinor: 0xeeeeee,
|
|
colorX: 0xff7777, colorY: 0x7777ff },
|
|
});
|
|
platform.onMove(() => {
|
|
// save last location and focus
|
|
db.admin.put('camera', {
|
|
place: space.view.save(),
|
|
focus: space.view.getFocus()
|
|
});
|
|
}, 100);
|
|
space.view.setZoom(zoomrev, zoomspd);
|
|
|
|
// reload stored space when worker is ready
|
|
moto.client.on('ready', restore_space);
|
|
|
|
// start worker
|
|
moto.client.start(`/code/mesh_work?${gapp.version}`);
|
|
|
|
// trigger space event binding
|
|
call.space_init({ space, platform });
|
|
|
|
// trigger ui building
|
|
call.ui_build();
|
|
}
|
|
|
|
// restore space layout and view from previous session
|
|
async function restore_space() {
|
|
const { api } = mesh;
|
|
const space = moto.space;
|
|
const db_admin = mesh.db.admin;
|
|
const db_space = mesh.db.space;
|
|
// let mcache = {};
|
|
await db_admin.get("camera")
|
|
.then(saved => {
|
|
if (saved) {
|
|
space.view.load(saved.place);
|
|
space.view.setFocus(saved.focus);
|
|
}
|
|
});
|
|
const mcache = await db_admin.get("matrices") || {};
|
|
let count = 0;
|
|
await db_space.iterate({ map: true }).then(cached => {
|
|
const keys = [];
|
|
const claimed = [];
|
|
for (let [id, data] of Object.entries(cached)) {
|
|
// console.log({ id, data });
|
|
keys.push(id);
|
|
if (count++ === 0) {
|
|
log(`restoring workspace`);
|
|
}
|
|
// restore object based on type
|
|
// group arrays load models they contain
|
|
// sketches are loaded by type since they're not grouped
|
|
if (Array.isArray(data)) {
|
|
claimed.push(id);
|
|
let models = data
|
|
.map(id => {
|
|
claimed.push(id);
|
|
return { id, md: cached[id] }
|
|
})
|
|
.filter(r => r.md) // filter cache misses
|
|
.map(r => new mesh.model(r.md, r.id).applyMatrix(mcache[r.id]));
|
|
if (models.length) {
|
|
log(`restored ${models.length} model(s)`);
|
|
mesh.api.group.new(models, id).applyMatrix(mcache[id]);
|
|
} else {
|
|
log(`removed empty group ${id}`);
|
|
db_space.remove(id);
|
|
}
|
|
} else if (data.type === 'sketch') {
|
|
claimed.push(id);
|
|
mesh.api.add.sketch({ id, ...data });
|
|
}
|
|
}
|
|
for (let id of claimed) {
|
|
keys.remove(id);
|
|
}
|
|
if (keys.length) {
|
|
log(`removing ${keys.length} unclaimed meshes`);
|
|
}
|
|
// clear out meshes left in the space db along with their matrices
|
|
for (let id of keys) {
|
|
db_space.remove(id);
|
|
delete mcache[id];
|
|
}
|
|
// restore global cache only after objects are restored
|
|
// otherwise their setup will corrupt the cache for other restores
|
|
matrixCache = mcache;
|
|
store_matrices();
|
|
}).then(() => {
|
|
// restore preferences after models are restored
|
|
return api.prefs.load().then(() => {
|
|
let { map } = api.prefs;
|
|
let { space, mode } = map;
|
|
api.grid(space.grid);
|
|
// restore selected state
|
|
let selist = space.select || [];
|
|
let smodel = api.model.list().filter(m => selist.contains(m.id));
|
|
let sgroup = api.group.list().filter(m => selist.contains(m.id));
|
|
let tolist = space.tools || [];
|
|
let tmodel = api.model.list().filter(m => tolist.contains(m.id));
|
|
let tgroup = api.group.list().filter(m => tolist.contains(m.id));
|
|
let sklist = api.sketch.list().filter(s => selist.contains(s.id));
|
|
api.selection.set([...smodel, ...sgroup, ...sklist], [...tmodel, ...tgroup]);
|
|
// restore edit mode
|
|
api.mode[mode]();
|
|
// restore dark mode
|
|
set_darkmode(map.space.dark);
|
|
});
|
|
}).finally(() => {
|
|
// hide loading curtain
|
|
$d('curtain','none');
|
|
if (api.prefs.map.info.welcome !== false) {
|
|
api.welcome(version);
|
|
}
|
|
});
|
|
}
|
|
|
|
// add space event bindings
|
|
function space_init(data) {
|
|
let platcolor = 0x00ff00;
|
|
let { space, platform } = data;
|
|
let { api } = mesh;
|
|
let { selection } = api;
|
|
|
|
// add file drop handler
|
|
space.event.addHandlers(self, [
|
|
'drop', (evt) => {
|
|
estop(evt);
|
|
platform.set({ opacity: 0, color: platcolor });
|
|
call.load_files([...evt.dataTransfer.files]);
|
|
},
|
|
'dragover', evt => {
|
|
estop(evt);
|
|
evt.dataTransfer.dropEffect = 'copy';
|
|
let color = platform.setColor(0x00ff00);
|
|
if (color !== 0x00ff00) platcolor = color;
|
|
platform.set({ opacity: 0.1 });
|
|
},
|
|
'dragleave', evt => {
|
|
platform.set({ opacity: 0, color: platcolor });
|
|
},
|
|
'keypress', evt => {
|
|
if (api.modal.showing) {
|
|
return;
|
|
}
|
|
if (evt.key === '?') {
|
|
return api.welcome(version);
|
|
}
|
|
let { shiftKey, metaKey, ctrlKey, code } = evt;
|
|
switch (code) {
|
|
case 'KeyQ':
|
|
return api.settings();
|
|
case 'KeyI':
|
|
return shiftKey ? api.tool.invert() : api.file.import();
|
|
case 'KeyX':
|
|
return api.file.export();
|
|
case 'KeyD':
|
|
return shiftKey && api.tool.duplicate();
|
|
case 'KeyC':
|
|
return selection.centerXY().focus();
|
|
case 'KeyF':
|
|
return shiftKey ? selection.focus() : selection.floor().focus();
|
|
case 'KeyM':
|
|
return shiftKey ? api.tool.merge() : api.tool.mirror();
|
|
case 'KeyU':
|
|
return shiftKey && api.tool.union();
|
|
case 'KeyA':
|
|
if (api.mode.is([ api.modes.edge ])) return mesh.edges.add();
|
|
return shiftKey && api.tool.analyze();
|
|
// case 'KeyR':
|
|
// return shiftKey ? api.tool.rebuild() : api.tool.repair();
|
|
case 'KeyE':
|
|
if (api.mode.is([ api.modes.sketch ])) {
|
|
estop(evt);
|
|
return api.sketch.extrude();
|
|
}
|
|
return;
|
|
// return api.tool.clean();
|
|
case 'KeyV':
|
|
return shiftKey ? selection.show() : selection.focus();
|
|
case 'KeyN':
|
|
return shiftKey ? estop(evt, api.tool.rename()) : api.normals();
|
|
case 'KeyW':
|
|
return api.wireframe();
|
|
case 'KeyG':
|
|
return shiftKey ?
|
|
(api.mode.is([ api.modes.sketch ]) ? api.sketch.arrange.group() : api.tool.regroup()) :
|
|
api.grid();
|
|
case 'KeyL':
|
|
return api.log.toggle({ spinner: false });
|
|
case 'KeyS':
|
|
if (!api.mode.is([ api.modes.object ])) return;
|
|
return shiftKey ? selection.visible({toggle:true}) : mesh.split.start();
|
|
case 'KeyB':
|
|
return selection.boundsBox({toggle:true});
|
|
case 'KeyH':
|
|
return shiftKey ? selection.hide() : space.view.home();
|
|
case 'KeyT':
|
|
return space.view.top();
|
|
case 'KeyZ':
|
|
return space.view.reset();
|
|
}
|
|
},
|
|
'keydown', evt => {
|
|
let { shiftKey, metaKey, ctrlKey, code } = evt;
|
|
let rv = (Math.PI / 12);
|
|
if (api.modal.showing) {
|
|
if (code === 'Escape') {
|
|
api.modal.cancel();
|
|
}
|
|
return;
|
|
}
|
|
let rot, floor = api.prefs.map.space.floor !== false;
|
|
switch (code) {
|
|
case 'KeyA':
|
|
if (metaKey || ctrlKey) {
|
|
if (!selection.sketch()?.selection.all()) {
|
|
api.mode.object();
|
|
selection.set(api.group.list());
|
|
}
|
|
estop(evt);
|
|
}
|
|
break;
|
|
case 'Escape':
|
|
if (selection.clear()) {
|
|
mesh.edges.clear();
|
|
mesh.split.end();
|
|
}
|
|
estop(evt);
|
|
break;
|
|
case 'Backspace':
|
|
case 'Delete':
|
|
let mode = api.mode.get();
|
|
if ([api.modes.object, api.modes.tool, api.modes.sketch].contains(mode)) {
|
|
selection.delete();
|
|
} else {
|
|
for (let m of selection.models()) {
|
|
m.deleteSelections(mode);
|
|
space.refresh()
|
|
}
|
|
}
|
|
estop(evt);
|
|
break;
|
|
case 'ArrowUp':
|
|
rot = selection.rotate(-rv,0,0);
|
|
break;
|
|
case 'ArrowDown':
|
|
rot = selection.rotate(rv,0,0);
|
|
break;
|
|
case 'ArrowLeft':
|
|
if (shiftKey) {
|
|
rot = selection.rotate(0,-rv,0);
|
|
} else {
|
|
floor = false;
|
|
rot = selection.rotate(0,0,rv);
|
|
}
|
|
break;
|
|
case 'ArrowRight':
|
|
if (shiftKey) {
|
|
rot = selection.rotate(0,rv,0);
|
|
} else {
|
|
floor = false;
|
|
rot = selection.rotate(0,0,-rv);
|
|
}
|
|
break;
|
|
}
|
|
if (rot && floor) {
|
|
// todo future pref to auto-floor or not
|
|
rot.floor(mesh.group);
|
|
}
|
|
}
|
|
]);
|
|
|
|
// mouse hover/click handlers. required to enable model drag in space.js
|
|
// called two ways:
|
|
// without args to return a list of selectable targets
|
|
// with args when a selection is made
|
|
space.mouse.downSelect((int, event, ints) => {
|
|
let obj = int?.object;
|
|
if (obj) api.selection.drag({ start: int.object });
|
|
if (obj) obj.sketch_item?.sketch.drag({ start: int.object });
|
|
if (event?.shiftKey) {
|
|
return api.objects();
|
|
} else {
|
|
return undefined;
|
|
}
|
|
});
|
|
|
|
// called two ways:
|
|
// without args to return a list of selectable targets
|
|
// with args when a selection is made
|
|
space.mouse.upSelect((int, event, ints) => {
|
|
if (ints?.length > 1) {
|
|
// ensure sketch items are returned before sketch plane / handles
|
|
let els = ints.filter(i => i.object.sketch_item);
|
|
int = els.length ? els[0] : int;
|
|
}
|
|
if (event?.target?.nodeName === "CANVAS") {
|
|
// a selection was made
|
|
const { model, sketch, sketch_item } = int?.object || {};
|
|
const { altKey, ctrlKey, metaKey, shiftKey } = event;
|
|
if (mesh.split.active()) {
|
|
return mesh.split.select(model);
|
|
}
|
|
if (model) {
|
|
const group = model.group;
|
|
if (metaKey) {
|
|
// set focus on intersected face
|
|
const { x, y, z } = int.point;
|
|
const q = new Quaternion().setFromRotationMatrix(group.object.matrix);
|
|
// rotate normal using group's matrix
|
|
const normal = shiftKey ? int.face.normal.applyQuaternion(q) : undefined;
|
|
// y,z swap due to world rotation for orbit controls
|
|
api.focus({center: { x, y:-z, z:y }, normal});
|
|
let one = api.sketch.selected.one;
|
|
if (one && confirm('attach sketch to face?')) {
|
|
one.center = {
|
|
x, y: -z, z: y
|
|
};
|
|
one.normal = {
|
|
x: normal.x,
|
|
y: normal.y,
|
|
z: normal.z
|
|
};
|
|
one.render();
|
|
}
|
|
} else if (ctrlKey) {
|
|
// rotate selected face towawrd z "floor"
|
|
group.faceDown(int.face.normal);
|
|
selection.update();
|
|
} else {
|
|
const { modes } = api;
|
|
const { surface } = api.prefs.map;
|
|
const opt = { radians: 0, radius: surface.radius };
|
|
const mode = api.mode.get();
|
|
switch(mode) {
|
|
case modes.sketch:
|
|
api.mode.object();
|
|
case modes.object:
|
|
case modes.tool:
|
|
selection.toggle(shiftKey ? model : model.group, mode === modes.tool);
|
|
break;
|
|
case modes.surface:
|
|
opt.radians = surface.radians;
|
|
case modes.face:
|
|
// find faces adjacent to point/line clicked
|
|
model.find(int,
|
|
altKey ? { toggle: true } :
|
|
shiftKey ? { clear: true } : { select: true },
|
|
opt);
|
|
break;
|
|
case modes.edge:
|
|
mesh.edges.select();
|
|
break;
|
|
}
|
|
}
|
|
} else if (sketch) {
|
|
if (metaKey) {
|
|
// set focus on intersected face
|
|
const { x, y, z } = int.point;
|
|
// rotate normal using group's matrix
|
|
const normal = shiftKey ? int.face.normal.clone(): undefined;
|
|
// ransform the normal to align with sketch group
|
|
normal?.transformDirection(int.object.sketch.group.matrix);
|
|
// y,z swap due to world rotation for orbit controls
|
|
api.focus({ center: { x, y:-z, z:y }, normal });
|
|
} else {
|
|
selection.toggle(sketch);
|
|
}
|
|
} else if (sketch_item) {
|
|
sketch_item.toggle();
|
|
}
|
|
} else {
|
|
// return objects upSelect can choose from
|
|
let visible = api.objects().filter(o => o.visible);
|
|
let sketches = api.selection.sketches();
|
|
if (sketches.length) {
|
|
// when sketch selected, append sketch items, too
|
|
visible.appendAll(sketches[0].object.children);
|
|
};
|
|
return visible;
|
|
}
|
|
});
|
|
|
|
space.mouse.onDrag((delta, offset, up = false) => {
|
|
let { mode, modes } = api;
|
|
if (delta) {
|
|
if (delta.event?.shiftKey && selection.count()) {
|
|
selection.drag({ delta, offset });
|
|
}
|
|
} else if (up) {
|
|
if (selection.count()) {
|
|
selection.drag({ end: true });
|
|
}
|
|
} else if (mode.is([ modes.object, modes.tool, modes.sketch ])) {
|
|
// return true if there are draggable elements
|
|
return api.objects().length > 0 || api.selection.sketches().length;
|
|
}
|
|
});
|
|
}
|
|
|
|
function load_files(files) {
|
|
log(`loading file...`);
|
|
let api = mesh.api;
|
|
let has_image = false;
|
|
let has_svg = false;
|
|
let has_gbr = false;
|
|
let sketch = api.sketch.selected.one;
|
|
for (let file of files) {
|
|
has_image = has_image || file.type === 'image/png';
|
|
has_svg = has_svg || file.name.toLowerCase().endsWith(".svg") > 0;
|
|
has_gbr = has_gbr || file.name.toLowerCase().endsWith(".gbr") > 0;
|
|
}
|
|
if (sketch && has_gbr) {
|
|
load.File.load([...files], { flat: true }).then(layers => {
|
|
for (let layer of layers.flat()) {
|
|
let { circs, closed, open, rects } = layer;
|
|
open = open.map(poly => {
|
|
const diam = poly.tool?.shape?.diameter;
|
|
return diam ? poly.offset_open(diam / 2, 'round') : null;
|
|
}).filter(p => p).flat();
|
|
for (let set of [ closed, open, circs, rects ]) {
|
|
let group = mesh.util.uuid();
|
|
for (let poly of set) {
|
|
sketch.add.polygon({ poly, group });
|
|
}
|
|
}
|
|
}
|
|
});
|
|
} else
|
|
if (sketch && has_svg) {
|
|
load.File.load([...files], { flat: true })
|
|
.then(polys => polys.forEach(set => {
|
|
let group = mesh.util.uuid();
|
|
set.forEach(poly => sketch.add.polygon({ poly, group }))
|
|
}))
|
|
.catch(error => dbug.error(error))
|
|
.finally(() => mesh.api.log.hide());
|
|
} else
|
|
if (has_svg) {
|
|
api.modal.dialog({
|
|
title: `svg import`,
|
|
body: [ h.div({ class: "image-import" }, [
|
|
h.div([
|
|
h.label("extrude"),
|
|
h.input({ id: "extrude_height", value: 5, size: 4 })
|
|
]),
|
|
h.div([
|
|
h.label("repair"),
|
|
h.input({ id: "svg_repair", type: "checkbox", checked: true })
|
|
]),
|
|
h.div([
|
|
h.button({ _: "import", onclick() {
|
|
let { svg_repair, extrude_height } = api.modal.bound;
|
|
load_files_opt(files, {
|
|
soup: svg_repair.checked,
|
|
depth: extrude_height
|
|
});
|
|
api.modal.hide();
|
|
} }),
|
|
])
|
|
]) ]
|
|
});
|
|
} else
|
|
if (has_image) {
|
|
api.modal.dialog({
|
|
title: `image import`,
|
|
body: [ h.div({ class: "image-import" }, [
|
|
h.div([
|
|
h.label("invert pixels"),
|
|
h.input({ id: "inv_image", type: "checkbox" })
|
|
]),
|
|
h.div([
|
|
h.label("invert alpha"),
|
|
h.input({ id: "inv_alpha", type: "checkbox" })
|
|
]),
|
|
h.div([
|
|
h.label("border size"),
|
|
h.input({ id: "img_border", value: 0, size: 4 })
|
|
]),
|
|
h.div([
|
|
h.label("blur pixels"),
|
|
h.input({ id: "img_blur", value: 0, size: 4 })
|
|
]),
|
|
h.div([
|
|
h.label("base pixels"),
|
|
h.input({ id: "img_base", value: 0, size: 4 })
|
|
]),
|
|
h.div([
|
|
h.button({ _: "import", onclick() {
|
|
let { inv_image, inv_alpha, img_border, img_blur, img_base } = api.modal.bound;
|
|
load_files_opt(files, {
|
|
inv_image: inv_image.checked,
|
|
inv_alpha: inv_alpha.checked,
|
|
border: parseInt(img_border.value || 0),
|
|
blur: parseInt(img_blur.value || 0),
|
|
base: parseInt(img_base.value || 0),
|
|
});
|
|
api.modal.hide();
|
|
} }),
|
|
])
|
|
]) ]
|
|
});
|
|
} else {
|
|
load_files_opt(files);
|
|
}
|
|
}
|
|
|
|
function load_files_opt(files, opt) {
|
|
return load.File.load([...files], opt)
|
|
.then(data => call.space_load(data))
|
|
.catch(error => log(error).pin({}) && dbug.error(error))
|
|
// .finally(() => mesh.api.log.hide());
|
|
}
|
|
|
|
// add object loader
|
|
function space_load(data) {
|
|
if (data && data.length && (data = data.flat()).length)
|
|
mesh.api.group.new(data.map(el => new mesh.model(el)))
|
|
.promote()
|
|
.focus();
|
|
}
|
|
|
|
let matrixCache = {};
|
|
|
|
// todo deferred with util
|
|
function store_matrices() {
|
|
mesh.db.admin.put("matrices", matrixCache);
|
|
}
|
|
|
|
// cache model matrices for page restores
|
|
function object_matrix(data) {
|
|
let { id, matrix } = data;
|
|
matrixCache[id] = matrix.elements;
|
|
store_matrices();
|
|
}
|
|
|
|
function object_destroy(id) {
|
|
delete matrixCache[id];
|
|
store_matrices();
|
|
}
|
|
|
|
// listen for changes like dark mode toggle
|
|
function set_darkmode(dark) {
|
|
let { prefs, model } = mesh.api;
|
|
let { sky, platform } = moto.space;
|
|
prefs.map.space.dark = dark;
|
|
if (dark) {
|
|
mesh.material.wireframe.color.set(0xaaaaaa);
|
|
mesh.material.wireline.color.set(0xaaaaaa);
|
|
$('app').classList.add('dark');
|
|
} else {
|
|
mesh.material.wireframe.color.set(0,0,0);
|
|
mesh.material.wireline.color.set(0,0,0);
|
|
$('app').classList.remove('dark');
|
|
}
|
|
sky.set({
|
|
color: dark ? 0 : 0xffffff,
|
|
ambient: { intensity: dark ? 0.55 : 1.1 }
|
|
});
|
|
platform.set({
|
|
light: dark ? 0.08 : 0.08,
|
|
grid: dark ? {
|
|
colorMajor: 0x666666,
|
|
colorMinor: 0x333333,
|
|
} : {
|
|
colorMajor: 0xcccccc,
|
|
colorMinor: 0xeeeeee,
|
|
},
|
|
});
|
|
mesh.api.updateFog();
|
|
platform.setSize();
|
|
for (let m of model.list()) {
|
|
m.normals({ refresh: true });
|
|
}
|
|
prefs.save();
|
|
}
|
|
|
|
function set_normals_length(length) {
|
|
let { prefs } = mesh.api;
|
|
prefs.map.normals.length = length || 1;
|
|
prefs.save();
|
|
}
|
|
|
|
function set_normals_color(color) {
|
|
let { prefs } = mesh.api;
|
|
let { map } = prefs;
|
|
if (map.space.dark) {
|
|
map.normals.color_dark = color || 0;
|
|
} else {
|
|
map.normals.color_lite = color || 0;
|
|
}
|
|
prefs.save();
|
|
}
|
|
|
|
function set_surface_radians(radians) {
|
|
let { prefs } = mesh.api;
|
|
prefs.map.surface.radians = parseFloat(radians || 0.1);
|
|
prefs.save();
|
|
}
|
|
|
|
function set_surface_radius(radius) {
|
|
let { prefs } = mesh.api;
|
|
prefs.map.surface.radius = parseFloat(radius || 0.2);
|
|
prefs.save();
|
|
}
|
|
|
|
function set_wireframe_opacity(opacity) {
|
|
let { prefs } = mesh.api;
|
|
prefs.map.wireframe.opacity = parseFloat(opacity || 0.15);
|
|
prefs.save();
|
|
}
|
|
|
|
function set_wireframe_fog(fogx) {
|
|
let { prefs } = mesh.api;
|
|
prefs.map.wireframe.fog = parseFloat(fogx || 3);
|
|
prefs.save();
|
|
}
|
|
|
|
function set_snap_value(snap) {
|
|
let { prefs } = mesh.api;
|
|
prefs.map.space.snap = parseFloat(snap || 1);
|
|
prefs.save();
|
|
}
|
|
|
|
// bind functions to topics
|
|
broker.listeners({
|
|
load_files,
|
|
object_matrix,
|
|
object_destroy,
|
|
space_init,
|
|
space_load,
|
|
set_darkmode,
|
|
set_normals_color,
|
|
set_normals_length,
|
|
set_surface_radians,
|
|
set_surface_radius,
|
|
set_wireframe_opacity,
|
|
set_wireframe_fog,
|
|
set_snap_value
|
|
});
|
|
|
|
// remove version cache bust from url
|
|
window.history.replaceState({},'','/mesh/');
|
|
|
|
// setup init() trigger when dom + scripts complete
|
|
document.onreadystatechange = function() {
|
|
if (document.readyState === 'complete') {
|
|
init();
|
|
}
|
|
}
|
|
|
|
});
|