grid-apps-cmms/src/main/mesh.js

535 lines
17 KiB
JavaScript
Raw Normal View History

2021-12-27 10:14:21 -05:00
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
2021-12-26 13:59:02 -05:00
2021-12-26 20:01:56 -05:00
(function() {
let broker = gapp.broker;
2022-01-02 16:30:05 -05:00
let call = broker.send;
2022-01-12 10:52:24 -05:00
let dbindex = [ "admin", "space" ];
2022-01-12 23:02:08 -05:00
let worker = moto.client.fn;
let { Quaternion, Vector3, Mesh, MeshPhongMaterial, PlaneGeometry, DoubleSide } = THREE;
2021-12-26 20:01:56 -05:00
2022-01-12 15:55:09 -05:00
// set below. called once the DOM readyState = complete
// this is the main() entrypoint called after all dependents load
2021-12-26 13:59:02 -05:00
function init() {
2022-01-12 10:52:24 -05:00
let stores = data.open('mesh', { stores: dbindex, version: 4 }).init(),
moto = self.moto,
api = mesh.api,
2021-12-26 13:59:02 -05:00
dark = false,
ortho = false,
zoomrev = true,
2021-12-26 13:59:02 -05:00
zoomspd = 1,
2021-12-26 20:01:56 -05:00
space = moto.Space,
platform = space.platform,
db = mesh.db = {
admin: stores.promise('admin'),
2022-01-12 10:52:24 -05:00
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));
2021-12-26 13:59:02 -05:00
// setup default workspace
2022-01-01 22:39:49 -05:00
space.useDefaultKeys(false);
space.init($('container'), delta => { }, ortho);
2021-12-28 13:02:01 -05:00
space.sky.set({
2022-01-07 22:07:49 -05:00
grid: false,
2021-12-28 13:02:01 -05:00
color: dark ? 0 : 0xffffff
});
platform.set({
volume: false,
round: false,
2022-01-15 22:24:54 -05:00
zOffset: 0,
2022-01-07 21:13:31 -05:00
opacity: 0,
color: 0xdddddd,
2021-12-28 13:02:01 -05:00
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 },
2021-12-28 13:02:01 -05:00
});
2022-01-04 09:48:36 -05:00
platform.onMove(() => {
// save last location and focus
db.admin.put('camera', {
place: space.view.save(),
focus: space.view.getFocus()
});
}, 100);
2021-12-26 20:01:56 -05:00
space.view.setZoom(zoomrev, zoomspd);
2021-12-26 19:43:48 -05:00
// reload stored space when worker is ready
moto.client.on('ready', restore_space);
// start worker
moto.client.start(`/code/mesh_work?${gapp.version}`);
2022-01-02 16:30:05 -05:00
// trigger space event binding
call.space_init({ space, platform });
// trigger ui building
call.ui_build();
}
2022-01-04 09:48:36 -05:00
// restore space layout and view from previous session
function restore_space() {
let { api } = mesh;
2022-01-08 22:06:50 -05:00
let count = 0;
2022-01-04 09:48:36 -05:00
let space = moto.Space;
2022-01-11 14:41:49 -05:00
let mcache = {};
2022-01-04 09:48:36 -05:00
mesh.db.admin.get("camera")
.then(saved => {
if (saved) {
space.view.load(saved.place);
space.view.setFocus(saved.focus);
}
2022-01-04 09:48:36 -05:00
});
mesh.db.admin.get("matrices")
.then(data => {
if (data) {
mcache = data;
}
});
mesh.db.space.iterate({ map: true }).then(cached => {
for (let [id, data] of Object.entries(cached)) {
2022-01-08 22:06:50 -05:00
if (count++ === 0) {
mesh.api.log.emit(`restoring workspace...`);
}
// restore group
if (Array.isArray(data)) {
let models = data
.map(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]));
mesh.api.log.emit(`restored ${models.length} model(s)`);
mesh.api.group.new(models, id).applyMatrix(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
2022-01-18 16:34:42 -05:00
return api.prefs.load().then(() => {
let { map } = api.prefs;
2022-01-13 17:01:35 -05:00
let { space, mode } = map;
api.grid(space.grid);
// restore selected state
2022-01-14 16:45:40 -05:00
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));
api.selection.set([...smodel, ...sgroup]);
2022-01-13 17:01:35 -05:00
// restore edit mode
api.mode.set(mode);
2022-01-18 16:34:42 -05:00
// restore dark mode
set_darkmode(map.space.dark);
});
2022-01-18 16:34:42 -05:00
}).finally(() => {
// hide loading curtain
$d('curtain','none');
});
}
// toggle edit/split temporary mode (present plane on hover)
let temp_mode;
// split functions
let split = {
start() {
let space = moto.Space;
let { api, util } = mesh;
// highlight button
let button = event.target;
button.classList.add('selected');
// create split plane visual
let geo, mat, obj = new Mesh(
geo = new PlaneGeometry(1,1),
mat = new MeshPhongMaterial({
side: DoubleSide,
color: 0x5555aa,
transparent: false,
opacity: 0.5
})
);
space.scene.add(obj);
// hide until first hover
obj.visible = false;
// enable temp mode
let state = split.state = { button, obj, zlist:[] };
let models = state.models = api.selection.models();
let meshes = models.map(m => m.mesh);
// get z list for snapping
Promise.all(models.map(m => m.zlist(3))).then(zs => {
for (let z of zs.flat()) {
state.zlist.addOnce(z);
}
});
// for split and lay flat modes
space.mouse.onHover((int, event, ints) => {
if (!event) {
2022-01-15 15:04:53 -05:00
return meshes;
}
obj.visible = false;
let { button, buttons } = event;
if (buttons) {
return;
}
2022-01-15 15:04:53 -05:00
let { dim, mid } = util.bounds(meshes);
let { x, y, z } = int.point;
// snap to zlist when shift pressed
let { zlist } = state;
mat.color.set(0x5555aa);
obj.visible = true;
if (event.shiftKey && zlist.length) {
for (let v of zlist) {
if (Math.abs(v - y) < 0.1) {
mat.color.set(0xaa5555);
y = v;
break;
}
}
}
2022-01-15 15:04:53 -05:00
// y is z in model space for the purposes of a split
state.plane = { z: y };
obj.scale.set(dim.x + 2, dim.y + 2, 1);
obj.position.set(mid.x, y, -mid.y);
});
temp_mode = split;
},
select() {
2022-01-15 16:55:26 -05:00
let { log } = mesh.api;
let { models, plane, zlist } = split.state;
2022-01-15 16:55:26 -05:00
log.emit(`splitting ${models.length} model(s) at ${plane.z.round(3)}`).pin();
Promise.all(models.map(m => m.split(plane))).then(models => {
mesh.api.selection.set(models);
2022-01-15 16:55:26 -05:00
log.emit('split complete').unpin();
split.end();
});
},
end() {
let space = moto.Space;
let { button, obj } = split.state;
button.classList.remove('selected');
space.scene.remove(obj);
space.mouse.onHover(undefined);
temp_mode = split.state = undefined;
2022-01-15 16:38:50 -05:00
mesh.api.selection.update();
}
}
function edit_split(event) {
if (temp_mode) {
temp_mode.end();
} else {
split.start();
}
}
// add space event bindings
2022-01-02 16:30:05 -05:00
function space_init(data) {
let { space, platform } = data;
let platcolor = 0x00ff00;
2021-12-31 14:12:42 -05:00
let api = mesh.api;
let { selection } = api;
// add file drop handler
space.event.addHandlers(self, [
'drop', (evt) => {
estop(evt);
platform.set({ opacity: 0, color: platcolor });
2022-01-06 17:28:57 -05:00
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 });
2021-12-31 14:12:42 -05:00
},
'keypress', evt => {
2022-01-06 15:10:03 -05:00
if (api.modal.showing) {
return;
}
2022-01-18 16:34:42 -05:00
if (evt.key === '?') {
return api.help();
}
let { shiftKey, metaKey, ctrlKey, code } = evt;
switch (code) {
2022-01-18 16:34:42 -05:00
case 'KeyQ':
return api.settings();
case 'KeyI':
return api.file.import();
case 'KeyX':
return api.file.export();
2022-01-08 21:00:19 -05:00
case 'KeyD':
if (shiftKey) return api.tool.duplicate();
break;
2022-01-08 21:00:19 -05:00
case 'KeyC':
return selection.centerXY().focus();
case 'KeyF':
return selection.floor().focus();
2022-01-11 16:29:54 -05:00
case 'KeyM':
if (shiftKey) return api.tool.mirror();
2022-01-12 23:02:08 -05:00
return api.tool.merge();
case 'KeyA':
2022-01-12 23:02:08 -05:00
return api.tool.analyze();
case 'KeyR':
2022-01-12 23:02:08 -05:00
return api.tool.repair();
2022-01-14 14:14:13 -05:00
case 'KeyE':
2022-01-12 23:02:08 -05:00
return api.tool.clean();
2021-12-31 16:59:38 -05:00
case 'KeyV':
2022-01-08 21:00:19 -05:00
return selection.focus();
2022-01-14 14:14:13 -05:00
case 'KeyN':
return api.normals();
case 'KeyW':
2022-01-08 21:00:19 -05:00
return api.wireframe();
2022-01-07 23:30:19 -05:00
case 'KeyG':
2022-01-08 21:00:19 -05:00
return api.grid();
2022-01-08 22:06:50 -05:00
case 'KeyL':
return api.log.toggle();
2022-01-12 00:28:33 -05:00
case 'KeyS':
if (shiftKey) return call.edit_split();
2022-01-12 00:28:33 -05:00
return selection.visible({toggle:true});
case 'KeyB':
2022-01-08 21:00:19 -05:00
return selection.boundsBox({toggle:true});
case 'KeyH':
2022-01-08 21:00:19 -05:00
return space.view.home();
case 'KeyT':
2022-01-08 21:00:19 -05:00
return space.view.top();
case 'KeyZ':
2022-01-08 21:00:19 -05:00
return space.view.reset();
2021-12-31 16:59:38 -05:00
}
2021-12-31 14:12:42 -05:00
},
'keydown', evt => {
2021-12-31 16:59:38 -05:00
let rv = Math.PI / 16;
2022-01-01 17:28:47 -05:00
let { shiftKey, metaKey, ctrlKey, code } = evt;
2022-01-06 15:10:03 -05:00
if (api.modal.showing) {
if (code === 'Escape') {
api.modal.hide();
}
return;
}
let rot;
2022-01-01 17:28:47 -05:00
switch (code) {
2022-01-01 15:28:59 -05:00
case 'KeyA':
2022-01-01 17:28:47 -05:00
if (metaKey || ctrlKey) {
selection.set(api.group.list());
2022-01-01 15:28:59 -05:00
estop(evt);
}
2022-01-01 17:28:47 -05:00
break;
2022-01-01 15:28:59 -05:00
case 'Escape':
selection.clear();
temp_mode && temp_mode.end();
2022-01-01 15:28:59 -05:00
estop(evt);
break;
case 'Backspace':
case 'Delete':
let mode = api.mode.get();
if (mode !== api.modes.object) {
for (let m of selection.models()) {
m.deleteSelections(mode);
}
} else {
for (let s of selection.list(true)) {
selection.remove(s);
s.showBounds(false);
s.remove();
}
}
2022-01-01 15:28:59 -05:00
estop(evt);
break;
2021-12-31 14:12:42 -05:00
case 'ArrowUp':
rot = selection.rotate(-rv,0,0);
2021-12-31 14:12:42 -05:00
break;
case 'ArrowDown':
rot = selection.rotate(rv,0,0);
2021-12-31 14:12:42 -05:00
break;
case 'ArrowLeft':
2022-01-01 17:28:47 -05:00
if (shiftKey) {
rot = selection.rotate(0,-rv,0);
2022-01-01 17:28:47 -05:00
} else {
rot = selection.rotate(0,0,rv);
2022-01-01 17:28:47 -05:00
}
2021-12-31 14:12:42 -05:00
break;
case 'ArrowRight':
2022-01-01 17:28:47 -05:00
if (shiftKey) {
rot = selection.rotate(0,rv,0);
2022-01-01 17:28:47 -05:00
} else {
rot = selection.rotate(0,0,-rv);
2022-01-01 17:28:47 -05:00
}
2021-12-31 14:12:42 -05:00
break;
}
if (rot && true) {
// todo future pref to auto-floor or not
rot.floor(mesh.group);
}
}
]);
2022-01-12 23:02:08 -05:00
// mouse hover/click handlers. required to enable model drag in space.js
space.mouse.downSelect((int, event) => {
return event && event.shiftKey ? api.objects() : undefined;
});
space.mouse.upSelect((int, event) => {
if (event && event.target.nodeName === "CANVAS") {
let model = int && int.object.model ? int.object.model : undefined;
if (temp_mode) {
return temp_mode.select(model);
}
if (model) {
let group = model.group;
let { ctrlKey, metaKey, shiftKey } = event;
2022-01-13 17:01:35 -05:00
if (metaKey) {
// set focus on intersected face
let { x, y, z } = int.point;
2022-01-12 23:02:08 -05:00
// y,z swap due to world rotation for orbit controls
api.focus({center: { x, y:-z, z:y }});
} else if (ctrlKey) {
2022-01-12 23:02:08 -05:00
// rotate selected face towawrd z "floor"
group.faceDown(int.face.normal);
} else {
let { modes } = api;
2022-01-13 17:01:35 -05:00
switch(api.mode.get()) {
case modes.object:
selection.toggle(shiftKey ? model : model.group);
break;
case modes.face:
case modes.line:
case modes.vertex:
// find faces adjacent to point/line clicked
model.find(int.point, int.face);
break;
}
}
}
} else {
return api.objects().filter(o => o.visible);
}
});
space.mouse.onDrag((delta, offset, up = false) => {
if (delta && delta.event.shiftKey) {
selection.move(delta.x, delta.y, 0);
} else {
return api.objects().length > 0;
}
});
2022-01-02 16:30:05 -05:00
}
2022-01-06 17:28:57 -05:00
function load_files(files) {
2022-01-07 14:39:32 -05:00
mesh.api.log.emit(`loading file...`);
2022-01-06 17:28:57 -05:00
load.File.load([...files])
.then(data => {
call.space_load(data);
})
.catch(error => {
dbug.error(error);
})
.finally(() => {
2022-01-07 14:39:32 -05:00
mesh.api.log.hide();
2022-01-06 17:28:57 -05:00
});
}
// add object loader
2022-01-02 16:30:05 -05:00
function space_load(data) {
mesh.api.group.new(data.flat().map(el => new mesh.model(el)))
.promote()
2021-12-31 16:59:38 -05:00
.focus();
2022-01-02 16:30:05 -05:00
}
2021-12-26 13:59:02 -05:00
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();
}
2022-01-18 16:34:42 -05:00
// listen for changes like dark mode toggle
function set_darkmode(dark) {
2022-01-18 20:58:46 -05:00
let { prefs, selection, model } = mesh.api;
2022-01-18 16:34:42 -05:00
let { sky, platform } = moto.Space;
2022-01-18 20:58:46 -05:00
prefs.map.space.dark = dark;
2022-01-18 16:34:42 -05:00
if (dark) {
2022-01-19 17:48:29 -05:00
mesh.material.wireframe.color.set(0xaaaaaa);
2022-01-19 14:04:04 -05:00
$('app').classList.add('dark');
2022-01-18 16:34:42 -05:00
} else {
mesh.material.wireframe.color.set(0,0,0);
2022-01-19 14:04:04 -05:00
$('app').classList.remove('dark');
2022-01-18 16:34:42 -05:00
}
sky.set({
color: dark ? 0 : 0xffffff,
2022-01-19 21:39:11 -05:00
ambient: { intensity: dark ? 0.5 : 1.0 }
});
2022-01-18 16:34:42 -05:00
platform.set({
2022-01-19 21:39:11 -05:00
light: dark ? 0.06 : 0.06,
2022-01-18 16:34:42 -05:00
grid: dark ? {
colorMajor: 0x666666,
colorMinor: 0x333333,
} : {
colorMajor: 0xcccccc,
colorMinor: 0xeeeeee,
},
});
2022-01-19 20:42:22 -05:00
platform.setSize();
2022-01-18 20:58:46 -05:00
for (let m of model.list()) {
m.normals({ refresh: true });
}
2022-01-18 16:34:42 -05:00
prefs.save();
}
2022-01-02 16:30:05 -05:00
// bind functions to topics
broker.listeners({
2022-01-06 17:28:57 -05:00
load_files,
2022-01-02 16:30:05 -05:00
space_init,
space_load,
object_matrix,
object_destroy,
2022-01-18 16:34:42 -05:00
set_darkmode,
edit_split
2022-01-01 17:28:47 -05:00
});
2021-12-26 20:01:56 -05:00
// remove version cache bust from url
window.history.replaceState({},'','/mesh/');
2021-12-26 20:01:56 -05:00
// setup init() trigger when dom + scripts complete
document.onreadystatechange = function() {
if (document.readyState === 'complete') {
2021-12-26 13:59:02 -05:00
init();
}
}
2021-12-26 20:01:56 -05:00
// finalize modules
gapp.finalize("main.mesh", [
"moto.license", // dep: moto.license
2021-12-29 12:55:45 -05:00
"moto.webui", // dep: moto.webui
2021-12-29 10:37:17 -05:00
"moto.client", // dep: moto.client
2021-12-29 07:54:16 -05:00
"moto.broker", // dep: moto.broker
"moto.space", // dep: moto.space
"data.index", // dep: data.index
"mesh.api", // dep: mesh.api
"mesh.model", // dep: mesh.model
2022-01-06 11:46:52 -05:00
"mesh.build", // dep: mesh.build
"load.file", // dep: load.file
]);
2021-12-26 20:01:56 -05:00
})();