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

650 lines
17 KiB
JavaScript
Raw Normal View History

/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
(function() {
// dep: ext.three
// dep: ext.three-bgu
2022-01-01 22:39:49 -05:00
gapp.register("mesh.api", [
"moto.license", // dep: moto.license
"moto.client", // dep: moto.client
"moto.broker", // dep: moto.broker
"moto.space", // dep: moto.space
2022-01-10 23:02:34 -05:00
"data.index", // dep: data.index
"mesh.tool", // dep: mesh.tool
2022-01-11 20:02:29 -05:00
"mesh.util", // dep: mesh.util
"add.array", // dep: add.array
]);
let mesh = self.mesh = self.mesh || {};
if (mesh.api) return;
2022-01-12 11:46:10 -05:00
let { Matrix4, Vector3 } = THREE;
let space = moto.Space;
2022-01-09 14:43:20 -05:00
let worker = moto.client.fn;
2022-01-11 20:02:29 -05:00
let util = mesh.util;
2021-12-30 22:59:46 -05:00
let groups = [];
2021-12-31 14:12:42 -05:00
let selected = [];
let toggle = {
wire: false,
bound: false
};
2021-12-31 14:12:42 -05:00
let selection = {
// @returns {MeshObject[]} or all groups if not strict and no selection
list(strict = false) {
return selected.length || strict ? selected.slice() : groups.slice();
2021-12-31 14:12:42 -05:00
},
2022-01-12 23:02:08 -05:00
// return selected groups + groups from selected models
groups(strict) {
let all = selection.list(strict);
let grp = all.filter(s => s instanceof mesh.group);
let mdl = all.filter(s => s instanceof mesh.model);
for (let m of mdl) {
grp.addOnce(m.group);
}
return grp;
},
2022-01-12 23:02:08 -05:00
// return selected models + models from selected groups
models(strict) {
let all = selection.list(strict);
let grp = all.filter(s => s instanceof mesh.group);
let mdl = all.filter(s => s instanceof mesh.model);
for (let g of grp) {
for (let m of g.models) {
mdl.addOnce(m);
}
}
return mdl;
},
2022-01-02 16:30:05 -05:00
contains(object) {
return selected.contains(object);
},
// @param group {MeshObject[]}
2021-12-31 14:12:42 -05:00
set(objects) {
selected = objects;
util.defer(selection.update);
2021-12-31 14:12:42 -05:00
},
// @param group {MeshObject}
2021-12-31 14:12:42 -05:00
add(object) {
// pendantic code necessary to minimize re-entrant api calls
if (object.models) {
// if group, remove discrete selected members
for (let m of object.models) {
if (selected.contains(m)) {
selection.remove(m);
}
}
} else {
// if model, remove selcted group
if (selected.contains(object.group)) {
selection.remove(object.group);
}
}
2021-12-31 14:12:42 -05:00
selected.addOnce(object);
util.defer(selection.update);
2021-12-31 14:12:42 -05:00
},
// @param group {MeshObject}
2021-12-31 14:12:42 -05:00
remove(object) {
selected.remove(object);
util.defer(selection.update);
2021-12-31 14:12:42 -05:00
},
// @param group {MeshObject}
toggle(object) {
if (selected.contains(object)) {
selection.remove(object);
} else {
selection.add(object);
}
},
2021-12-31 14:12:42 -05:00
clear() {
for (let s of selection.list()) {
selection.remove(s);
}
2022-01-13 21:17:35 -05:00
for (let m of api.model.list()) {
m.clearSelections();
2022-01-13 21:17:35 -05:00
}
},
2022-01-12 00:28:33 -05:00
visible() {
for (let m of selection.models()) {
m.visible(...arguments);
}
util.defer(selection.update);
},
// update selection and wireframe for all objects
update() {
for (let group of groups) {
group.select(false);
2022-01-14 14:14:13 -05:00
group.wireframe(prefs.map.space.wire || false);
group.normals(prefs.map.space.norm || false);
}
2022-01-02 16:30:05 -05:00
// prevent selection of model and its group
let mgsel = selected.filter(s => s instanceof mesh.model).map(m => m.group);
selected = selected.filter(sel => !mgsel.contains(sel)).filter(v => v);
// highlight selected
for (let object of selected) {
object.select(true);
}
// update saved selection id list
prefs.save( prefs.map.space.select = selected.map(s => s.id) );
// force repaint in case of idle
space.update();
return selection;
2021-12-31 14:12:42 -05:00
},
move(dx = 0, dy = 0, dz = 0) {
for (let s of selection.groups()) {
2021-12-31 14:12:42 -05:00
s.move(dx, dy, dz);
2021-12-30 22:59:46 -05:00
}
2021-12-31 14:12:42 -05:00
return selection;
2021-12-30 22:59:46 -05:00
},
2021-12-31 14:12:42 -05:00
rotate(dx = 0, dy = 0, dz = 0) {
for (let s of selection.groups()) {
2021-12-31 16:12:37 -05:00
s.rotate(dx, dy, dz);
2021-12-31 14:12:42 -05:00
}
return selection;
},
2022-01-01 10:44:06 -05:00
qrotate(q) {
for (let s of selection.groups()) {
2022-01-01 10:44:06 -05:00
s.qrotate(q);
}
return selection;
},
2021-12-31 14:12:42 -05:00
scale(dx = 0, dy = 0, dz = 0) {
for (let s of selection.groups()) {
2021-12-31 14:12:42 -05:00
let { x, y, z } = s.scale();
2022-01-10 18:10:57 -05:00
s.scale(x * dx, y * dy, z * dz);
2021-12-31 14:12:42 -05:00
}
return selection;
},
floor() {
for (let s of selection.groups()) {
2021-12-31 14:12:42 -05:00
s.floor(...arguments);
}
return selection;
},
centerXY() {
for (let s of selection.groups()) {
2021-12-31 16:59:38 -05:00
s.centerXY(...arguments);
2021-12-31 14:12:42 -05:00
}
return selection;
},
boundsBox() {
for (let m of selection.groups()) {
m.showBounds(...arguments);
}
return selection;
},
2021-12-31 14:12:42 -05:00
home() {
return selection.centerXY().floor();
2021-12-31 16:59:38 -05:00
},
focus() {
2022-01-07 14:39:32 -05:00
api.focus(selection.list());
2021-12-31 16:59:38 -05:00
},
bounds() {
return util.bounds(selected.map(s => s.object));
2021-12-31 14:12:42 -05:00
}
};
let group = {
// @returns {MeshGroup[]}
list() {
return groups.slice();
},
// @param group {MeshModel[]}
2022-01-11 21:04:24 -05:00
new(models, id, name) {
return group.add(new mesh.group(models, id, name));
2021-12-31 14:12:42 -05:00
},
// @param group {MeshGroup}
2022-01-01 23:48:36 -05:00
add(group) {
2021-12-31 14:12:42 -05:00
groups.addOnce(group);
space.world.add(group.object);
api.selection.update();
2021-12-31 14:12:42 -05:00
space.update();
return group;
},
// @param group {MeshGroup}
2022-01-01 23:48:36 -05:00
remove(group) {
2021-12-31 14:12:42 -05:00
groups.remove(group);
space.world.remove(group.object);
2021-12-31 14:12:42 -05:00
space.update();
}
};
2022-01-01 22:39:49 -05:00
let model = {
// @returns {MeshModel[]}
list() {
return groups.map(g => g.models).flat();
}
};
2022-01-06 20:22:50 -05:00
let file = {
import() {
// binding created in mesh.build
$('import').click();
},
export() {
2022-01-08 21:00:19 -05:00
let recs = selection.models().map(m => { return {
id: m.id, matrix: m.matrix, file: m.file
} });
2022-01-08 22:06:50 -05:00
if (recs.length === 0) {
return api.log.emit(`no models to export`);
}
function doit(ext = 'obj') {
2022-01-09 14:43:20 -05:00
api.log.emit(`exporting ${recs.length} model(s)`);
let file = api.modal.bound.filename.value || "export.obj";
if (file.toLowerCase().indexOf(`.${ext}`) < 0) {
file = `${file}.${ext}`;
2022-01-08 22:06:50 -05:00
}
2022-01-09 14:43:20 -05:00
worker.file_export({
recs, format: ext
2022-01-09 14:43:20 -05:00
}).then(data => {
if (data.length) {
util.download(data, file);
}
}).finally( api.modal.hide );
}
api.modal.dialog({
title: `export ${recs.length} model(s)`,
body: [ h.div({ class: "export" }, [
h.div([
h.div("filename"),
h.input({ id: "filename", value: "mesh_export" })
]),
h.div([
h.button({ _: "download OBJ", onclick() { doit('obj') } }),
h.button({ _: "download STL", onclick() { doit('stl') } })
2022-01-09 14:43:20 -05:00
])
]) ]
2022-01-08 21:00:19 -05:00
});
2022-01-06 20:22:50 -05:00
},
};
2022-01-12 23:02:08 -05:00
// return selection if models are invalid. can happen when called from
// even bound to a button which delivers a PointEvent argument
function fallback(models, strict) {
return Array.isArray(models) ? models : selection.models(strict);
2022-01-12 23:02:08 -05:00
}
2022-01-06 20:22:50 -05:00
let tool = {
2022-01-11 16:29:54 -05:00
merge(models) {
2022-01-12 23:02:08 -05:00
models = fallback(models);
if (models.length <= 1) {
return api.log.emit('nothing to merge');
}
2022-01-11 16:29:54 -05:00
api.log.emit(`merging ${models.length} models`).pin();
worker.model_merge(models.map(m => {
return { id: m.id, matrix: m.matrix }
}))
.then(data => {
let group = api.group.new([new mesh.model({
file: `merged`,
mesh: data
})]).promote();
2022-01-11 16:29:54 -05:00
api.selection.set([group]);
api.log.emit('merge complete').unpin();
});
},
2022-01-30 21:56:53 -05:00
union(models) {
models = fallback(models);
api.log.emit(`union ${models.length} model(s)`).pin();
worker.model_union(models.map(m => {
return { id: m.id, matrix: m.matrix }
}))
.then(data => {
let group = api.group.new([new mesh.model({
file: `unioned`,
mesh: data
})]).promote();
api.selection.set([group]);
api.log.emit('union complete').unpin();
});
},
2022-01-13 17:01:35 -05:00
duplicate() {
for (let m of selection.models()) {
m.duplicate();
}
},
mirror() {
for (let m of selection.models()) {
m.mirror();
}
},
2022-01-14 14:30:07 -05:00
invert() {
2022-01-14 16:45:40 -05:00
for (let m of selection.models()) {
2022-01-14 14:30:07 -05:00
m.invert(api.mode.get());
}
},
2022-01-20 17:40:10 -05:00
rename(models) {
models = fallback(models, true);
let model = models[0];
if (!model) {
return;
}
if (models.length > 1) {
return api.log.emit('rename requires a single selection');
}
let onclick = onkeydown = (ev) => {
if (ev.code && ev.code !== 'Enter') {
return;
}
model.rename( $('tempedit').value.trim() );
selection.update();
api.modal.hide();
};
let { tempedit } = api.modal.show(`rename model`, h.div({ class: "rename"}, [
h.input({ id: "tempedit", value: model.file, onkeydown }),
h.button({ _: 'ok', onclick })
]));
tempedit.setSelectionRange(0,1000);
tempedit.focus();
},
2022-01-20 19:21:26 -05:00
regroup(models) {
models = fallback(models, true);
if (models.length === 0) {
return;
}
api.log.emit(`regrouping ${models.length} model(s)`);
let bounds = util.bounds(models);
2022-01-20 21:53:06 -05:00
let { mid } = bounds;
Promise.all(models.map(m => m.ungroup())).then(() => {
mesh.api.group.new(models)
.centerModels()
.position(mid.x, mid.y, mid.z)
.setSelected();
2022-01-20 21:53:06 -05:00
});
2022-01-20 19:21:26 -05:00
},
analyze(models, opt = { compound: true }) {
2022-01-12 23:02:08 -05:00
models = fallback(models);
2022-01-11 14:41:49 -05:00
api.log.emit('analyzing mesh(es)...').pin();
2022-01-11 21:04:24 -05:00
let promises = [];
let newmdl = [];
2022-01-12 11:46:10 -05:00
let mcore = new Matrix4().makeRotationX(Math.PI / 2);
for (let m of models) {
2022-01-12 09:32:13 -05:00
// todo - translate vertices with source model's matrix
let p = worker.model_analyze({ id: m.id, opt }).then(data => {
2022-01-11 21:04:24 -05:00
let { areas, edges } = data.mapped;
let nm = areas.map(area => new mesh.model({
file: (area.length/3).toString(),
mesh: area.toFloat32()
2022-01-13 21:17:35 -05:00
})).map( nm => nm.applyMatrix4(mcore.clone().multiply(m.mesh.matrixWorld)) );
2022-01-11 21:04:24 -05:00
newmdl.appendAll(nm);
2022-01-11 14:41:49 -05:00
});
2022-01-11 16:29:54 -05:00
promises.push(p);
2022-01-11 14:41:49 -05:00
}
2022-01-11 16:29:54 -05:00
Promise.all(promises).then(() => {
if (newmdl.length) {
mesh.api.group.new(newmdl, undefined, "patch").setSelected();
}
2022-01-11 16:29:54 -05:00
api.log.emit('analysis complete').unpin();
});
2022-01-06 20:22:50 -05:00
},
2022-01-11 22:42:43 -05:00
heal(models, opt = {}) {
2022-01-12 23:02:08 -05:00
models = fallback(models);
2022-01-11 21:04:24 -05:00
let promises = [];
for (let m of models) {
2022-01-11 22:42:43 -05:00
let p = worker.model_heal({
id: m.id, opt
}).then(data => {
2022-01-06 23:51:35 -05:00
if (data) {
m.reload(
data.vertices,
data.indices,
data.normals
);
}
});
2022-01-11 16:29:54 -05:00
promises.push(p);
2022-01-06 20:22:50 -05:00
}
2022-01-11 22:42:43 -05:00
return new Promise((resolve, reject) => {
Promise.all(promises).then(() => {
resolve();
});
});
},
repair(models) {
2022-01-12 23:02:08 -05:00
models = fallback(models);
2022-01-11 22:42:43 -05:00
api.log.emit('repairing mesh(es)...').pin();
tool.heal(models, { merge: true }).then(() => {
api.log.emit('repair commplete').unpin();
2022-01-12 10:52:24 -05:00
api.selection.update();
2022-01-11 22:42:43 -05:00
});
},
clean(models) {
2022-01-12 23:02:08 -05:00
models = fallback(models);
2022-01-11 22:42:43 -05:00
tool.heal(models, { merge: false }).then(() => {
api.log.emit('cleaning complete').unpin();
2022-01-12 10:52:24 -05:00
api.selection.update();
2022-01-11 16:29:54 -05:00
});
},
rebuild(models) {
Promise.all(fallback(models).map(m => m.rebuild()));
2022-01-06 20:22:50 -05:00
}
};
2022-01-13 17:01:35 -05:00
let modes = { object: "object", face: "face", line: "line", vertex: "vertex" };
// edit mode
let mode = {
set(mode) {
prefs.save(prefs.map.mode = mode);
for (let key of Object.values(modes)) {
$(`mode-${key}`).classList.remove('selected');
}
$(`mode-${mode}`).classList.add('selected');
},
get() {
return prefs.map.mode;
},
object() {
mode.set(modes.object);
},
face() {
mode.set(modes.face);
},
line() {
mode.set(modes.line);
},
vertex() {
mode.set(modes.vertex);
},
};
2022-01-18 16:34:42 -05:00
// cache pref signature so we know when it changes
let prefsig;
let prefsignew;
2022-01-10 23:02:34 -05:00
// persisted preference map
let prefs = {
map: {
2022-01-20 17:46:39 -05:00
info: {
group: "show",
span: "show"
},
2022-01-13 17:01:35 -05:00
mode: modes.object,
edit: {
scale_group_X: true,
scale_group_Y: true,
scale_group_Z: true
},
space: {
center: true,
floor: true,
wire: false,
grid: true,
dark: false,
select: []
},
},
2022-01-10 23:02:34 -05:00
2022-01-18 16:34:42 -05:00
// look for changes in pref signature
changed() {
prefsignew = JSON.stringify(prefs.map);
return prefsignew !== prefsig;
2022-01-10 23:02:34 -05:00
},
// persist to data store
save() {
2022-01-18 16:34:42 -05:00
if (prefs.changed()) {
mesh.db.admin.put("prefs", prefs.map);
prefsig = prefsignew;
}
2022-01-10 23:02:34 -05:00
},
// reload from data store
load() {
return mesh.db.admin.get("prefs").then(data => {
// handle/ignore incompatible older prefs
if (data) Object.assign(prefs.map, data);
2022-01-10 23:02:34 -05:00
});
}
};
2022-01-08 21:00:19 -05:00
// api is augmented in mesh.build (log, modal, download)
2021-12-31 14:12:42 -05:00
let api = mesh.api = {
2022-01-09 10:13:09 -05:00
help() {
window.open("https://docs.grid.space/projects/mesh-tool");
},
2022-01-01 23:48:36 -05:00
clear() {
2021-12-31 14:12:42 -05:00
for (let group of group.list()) {
group.remove(group);
}
},
// @param object {THREE.Object3D | THREE.Object3D[] | Point}
2022-01-01 23:48:36 -05:00
focus(object) {
let { center } = object.center ? object : util.bounds(object);
2021-12-31 17:20:41 -05:00
// when no valid objects supplied, set origin
2021-12-31 16:59:38 -05:00
if (isNaN(center.x * center.y * center.z)) {
2021-12-31 17:20:41 -05:00
center = { x: 0, y: 0, z: 0 };
2021-12-31 16:59:38 -05:00
}
let { normal } = object;
let left, up;
if (normal) {
let { x, y, z } = normal;
left = new Vector3(x,y,0).angleTo(new Vector3(0,-1,0));
up = new Vector3(0,y,z).angleTo(new Vector3(0,0,1));
if (x < 0) left = -left;
}
2021-12-31 17:20:41 -05:00
// sets "home" views (front, back, home, reset)
space.platform.setCenter(center.x, -center.y, center.z);
2021-12-31 17:20:41 -05:00
// sets camera focus
space.view.panTo(center.x, center.z, -center.y, left, up);
// space.view.setFocus(new Vector3(
// center.x, center.z, -center.y
// ));
2021-12-31 16:59:38 -05:00
},
2022-01-07 21:32:53 -05:00
grid(state = {toggle:true}) {
let { platform } = space;
let { map, save } = prefs;
2022-01-07 21:32:53 -05:00
if (state.toggle) {
platform.showGrid(!platform.isGridVisible());
2022-01-07 21:32:53 -05:00
} else {
platform.showGrid(state);
2022-01-07 21:32:53 -05:00
}
map.space.grid = platform.isGridVisible();
save();
2022-01-07 21:32:53 -05:00
},
wireframe(state = {toggle:true}, opt = { }) {
let wire = prefs.map.space.wire;
if (state.toggle) {
wire = !wire;
} else {
wire = state;
}
2022-01-14 14:14:13 -05:00
for (let m of model.list()) {
m.wireframe(wire, opt);
2022-01-06 09:38:04 -05:00
}
prefs.save( prefs.map.space.wire = wire );
2022-01-06 09:38:04 -05:00
},
2022-01-14 14:14:13 -05:00
normals(state = {toggle:true}) {
let norm = prefs.map.space.norm;
if (state.toggle) {
norm = !norm;
} else {
norm = state;
}
for (let m of model.list()) {
m.normals(norm);
}
prefs.save( prefs.map.space.norm = norm );
},
2022-01-13 17:01:35 -05:00
mode,
modes,
2021-12-31 14:12:42 -05:00
selection,
group,
2022-01-01 22:39:49 -05:00
model,
2022-01-06 20:22:50 -05:00
file,
tool,
2022-01-06 17:28:57 -05:00
2022-01-10 23:02:34 -05:00
prefs,
2022-01-01 23:48:36 -05:00
objects() {
// return model objects suitable for finding ray intersections
return group.list().map(o => o.models).flat().map(o => o.mesh);
}
};
2022-01-01 22:39:49 -05:00
let broker = gapp.broker;
2022-01-01 23:48:36 -05:00
// publish messages when api functions are called
broker.wrapObject(selection, 'selection');
broker.wrapObject(model, 'model');
broker.wrapObject(group, 'group');
2022-01-01 22:39:49 -05:00
2022-01-10 23:02:34 -05:00
// optimize db writes by merging updates
2022-01-18 16:34:42 -05:00
prefs.save = util.deferWrap(prefs.save, 100);
2022-01-10 23:02:34 -05:00
})();