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

603 lines
18 KiB
JavaScript
Raw Normal View History

/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
"use strict";
(function() {
gapp.register("mesh.model", [
"add.array", // dep: add.array
"add.three", // dep: add.three
"moto.license", // dep: moto.license
"mesh.object", // dep: mesh.object
2021-12-30 22:59:46 -05:00
"mesh.group", // dep: mesh.group
2022-01-11 20:02:29 -05:00
"mesh.util", // dep: mesh.util
"mesh.api", // dep: mesh.api
]);
let mesh = self.mesh = self.mesh || {};
if (mesh.model) return;
let mapp = mesh;
let space = moto.Space;
let worker = moto.client.fn;
2022-01-12 11:46:10 -05:00
let { MeshPhongMaterial, MeshBasicMaterial } = THREE;
let { BufferGeometry, BufferAttribute, DoubleSide, Mesh } = THREE;
/** default materials **/
let materials = mesh.material = {
// model unselected
normal: new MeshPhongMaterial({
2022-01-12 11:46:10 -05:00
side: DoubleSide,
2021-12-29 17:20:17 -05:00
transparent: true,
2022-01-19 21:39:11 -05:00
shininess: 125,
specular: 0x202020,
2022-01-15 15:04:53 -05:00
color: 0xf0f000,
2021-12-29 17:20:17 -05:00
opacity: 1
}),
// model selected
select: new MeshPhongMaterial({
side: DoubleSide,
transparent: true,
2022-01-19 21:39:11 -05:00
shininess: 125,
specular: 0x202020,
2022-01-15 15:04:53 -05:00
color: 0x00e000,
opacity: 1
}),
// face selected (for groups ranges)
face: new MeshPhongMaterial({
2022-01-12 11:46:10 -05:00
side: DoubleSide,
transparent: true,
shininess: 100,
specular: 0x202020,
2022-01-14 16:45:40 -05:00
color: 0x0088ee,
opacity: 1
}),
2022-01-12 11:46:10 -05:00
wireframe: new MeshBasicMaterial({
side: DoubleSide,
wireframe: true,
2022-01-14 14:14:13 -05:00
color: 0x0,
transparent: true,
opacity: 0.5
}),
2021-12-29 17:20:17 -05:00
};
/** 3D model rendered on plaform **/
mesh.model = class MeshModel extends mesh.object {
constructor(data, id) {
super(id);
2022-01-05 09:29:27 -05:00
let { file, mesh, vertices, indices, normals } = data;
if (!mesh) {
dbug.error(`'${file}' missing mesh data`);
return;
}
// remove file name extensions
let text = file || '';
let dot = text.lastIndexOf('.');
if (dot > 0) file = text.substring(0, dot);
// create local materials
this.mats = {
normal: materials.normal.clone(),
select: materials.select.clone(),
face: materials.face.clone()
};
// information about selected faces, lines, and vertices
// vertex compound keys are sorted least index to greatest
this.sel = {
faces: [], // first index into vertices
lines: {}, // key = vertex-vertex, val = [ faces ]
verts: {}, // key = x-y-z, val = { faces:[], sphere }
};
this.file = file || 'unnamed';
2022-01-05 09:29:27 -05:00
this.load(mesh || vertices, indices, normals);
2021-12-29 17:20:17 -05:00
}
get type() {
return "model";
}
get object() {
return this.mesh;
2021-12-30 22:59:46 -05:00
}
2022-01-08 21:00:19 -05:00
get matrix() {
return this.mesh.matrixWorld.elements;
}
// override and translate mesh
move(x = 0, y = 0, z = 0) {
let arr = this.attributes.position.array;
for (let i=0, l=arr.length; i<l; ) {
arr[i] = arr[i++] + x;
arr[i] = arr[i++] + y;
arr[i] = arr[i++] + z;
}
this.reload(arr);
return this;
}
// override and translate mesh
scale(x = 1, y = 1, z = 1) {
let arr = this.attributes.position.array;
for (let i=0, l=arr.length; i<l; ) {
arr[i] = arr[i++] *= x;
arr[i] = arr[i++] *= y;
arr[i] = arr[i++] *= z;
}
this.reload(arr);
return this;
}
mirror() {
return this.duplicate({ mirror: true });
}
2022-01-11 14:41:49 -05:00
// return new group containing just this model in world coordinates
duplicate(opt = {}) {
2022-01-11 14:41:49 -05:00
worker.model_duplicate({
2022-01-08 21:00:19 -05:00
matrix: this.matrix,
id: this.id,
opt
2021-12-31 22:11:20 -05:00
}).then(data => {
let group = mesh.api.group.new([new mesh.model({
file: `${this.file}`,
2022-01-07 14:39:32 -05:00
mesh: data
})]).setSelected();
if (opt.mirror) {
group.move(0, 0, group.bounds.dim.z);
}
2021-12-31 22:11:20 -05:00
});
}
rebuild(opt = {}) {
if (this.lines) {
space.scene.remove(this.lines);
this.lines = undefined;
}
if (opt.clean) {
return;
}
worker.model_rebuild({
matrix: this.matrix,
id: this.id
}).then(data => {
2022-01-26 23:13:50 -05:00
let points = [];
2022-01-27 11:34:37 -05:00
for (let i=0; i<data.length;) {
points.push(new THREE.Vector3(data[i++], data[i++], data[i++]));
2022-01-26 23:13:50 -05:00
}
let material = new THREE.LineBasicMaterial( { color: 0xdddddd } );
let geometry = new THREE.BufferGeometry().setFromPoints(points);
let lines = this.lines = new THREE.LineSegments(geometry, material);
space.scene.add(lines);
});
}
2022-01-05 09:29:27 -05:00
load(vertices, indices, normals) {
2022-01-12 11:46:10 -05:00
let geo = new BufferGeometry();
geo.setAttribute('position', new BufferAttribute(vertices, 3));
if (indices) geo.setIndex(new BufferAttribute(indices, 1));
2022-01-05 09:29:27 -05:00
if (!normals) geo.computeVertexNormals();
let meh = this.mesh = new Mesh(geo, [
this.mats.normal,
this.mats.face
]);
geo.addGroup(0, Infinity, 0);
2021-12-29 17:20:17 -05:00
meh.receiveShadow = true;
meh.castShadow = true;
meh.renderOrder = 1;
2022-01-06 09:38:04 -05:00
// sets fallback opacity for wireframe toggle
this.opacity(1);
// this ref allows clicks to be traced to models and groups
meh.model = this;
2022-01-13 21:17:35 -05:00
// persist in db so it can be restored on page load
mapp.db.space.put(this.id, { file: this.file, mesh: vertices });
// sync data to worker
worker.model_load({id: this.id, name: this.file, vertices, indices});
}
2022-01-05 09:29:27 -05:00
reload(vertices, indices, normals) {
2022-01-11 14:41:49 -05:00
let was = this.wireframe(false);
let geo = this.mesh.geometry;
2022-01-12 11:46:10 -05:00
geo.setAttribute('position', new BufferAttribute(vertices, 3));
geo.setAttribute('normal', undefined);
// signal util.box3expand that geometry changed
geo._model_invalid = true;
2022-01-12 11:46:10 -05:00
if (indices) geo.setIndex(new BufferAttribute(indices, 1));
2022-01-05 09:29:27 -05:00
if (!normals) geo.computeVertexNormals();
2022-01-20 21:53:06 -05:00
// allows raycasting to work
geo.computeBoundingSphere();
2022-01-13 21:17:35 -05:00
// persist in db so it can be restored on page load
mapp.db.space.put(this.id, { file: this.file, mesh: vertices });
// sync data to worker
worker.model_load({id: this.id, name: this.name, vertices, indices});
2022-01-11 14:41:49 -05:00
// restore wireframe state
this.wireframe(was);
2022-01-15 22:24:54 -05:00
// fixup normals
this.normals({refresh: true});
}
2022-01-04 01:11:13 -05:00
2022-01-20 17:40:10 -05:00
rename(file) {
this.file = file;
// persist in db so it can be restored on page load
mapp.db.space.put(this.id, { file, mesh: this.attributes.position.array });
}
get group() {
return this._group;
}
set group(gv) {
2021-12-30 22:59:46 -05:00
if (gv && this._group && this._group !== gv) {
throw "models can only belong to one group";
}
this._group = gv;
}
2021-12-30 22:59:46 -05:00
get attributes() {
return this.mesh.geometry.attributes;
}
get vertices() {
return this.attributes.position.count;
}
get faces() {
return this.vertices / 3;
}
// get, set, or toggle visibility of model and wireframe
2022-01-06 09:38:04 -05:00
visible(bool) {
if (bool === undefined) {
return this.mesh.visible;
}
if (bool.toggle) {
return this.visible(!this.mesh.visible);
}
this.mesh.visible = bool;
}
// get, set, or toggle selection of model (coloring)
select(bool) {
if (bool === undefined) {
return this.mesh.material[0] === this.mats.normal;
}
if (bool.toggle) {
return this.select(!this.select());
}
this.mesh.material[0] = bool ? this.mats.select : this.mats.normal;
return bool;
}
// return selected state
selected() {
return this.select();
2022-01-06 09:38:04 -05:00
}
opacity(ov, opt = {}) {
2021-12-30 23:23:45 -05:00
let mat = this.mesh.material;
if (ov === undefined) {
return mat[0].opacity;
2021-12-30 23:23:45 -05:00
}
2022-01-06 09:38:04 -05:00
if (ov.restore) {
ov = this._op;
} else if (ov.temp !== undefined) {
ov = ov.temp;
} else {
this._op = ov;
}
for (let m of Object.values(this.mats)) {
if (ov <= 0.0) {
m.transparent = false;
m.opacity = 1;
m.visible = false;
} else {
m.transparent = true;
m.opacity = ov;
m.visible = true;
}
2021-12-30 23:23:45 -05:00
}
space.update();
2021-12-30 23:23:45 -05:00
}
wireframe(bool, opt = {}) {
2022-01-14 14:14:13 -05:00
let was = this._wire ? true : false;
2021-12-30 23:28:36 -05:00
if (bool === undefined) {
2022-01-14 14:14:13 -05:00
return was ? {
2021-12-30 23:28:36 -05:00
enabled: true,
2022-01-06 09:38:04 -05:00
opacity: this.opacity(),
2022-01-14 14:14:13 -05:00
color: was ? this._wire.material.color : undefined,
2021-12-30 23:28:36 -05:00
} : {
enabled: false
};
}
if (bool.toggle) {
2022-01-14 14:14:13 -05:00
bool = !was;
}
2022-01-14 14:14:13 -05:00
// no change
if (was === bool) {
return was;
}
2022-01-14 14:14:13 -05:00
if (was) {
2021-12-30 23:23:45 -05:00
this.mesh.remove(this._wire);
this._wire = undefined;
2022-01-06 09:38:04 -05:00
this.opacity({restore: true});
2021-12-30 23:23:45 -05:00
}
if (bool) {
2022-01-12 11:46:10 -05:00
this._wire = new Mesh(this.mesh.geometry.shallowClone(), materials.wireframe);
2021-12-30 23:23:45 -05:00
this.mesh.add(this._wire);
this.opacity({temp: opt.opacity || 0.15});
2021-12-30 23:23:45 -05:00
}
space.update();
2022-01-11 14:41:49 -05:00
return was;
2021-12-30 23:23:45 -05:00
}
2022-01-14 14:14:13 -05:00
normals(bool) {
let was = this._norm ? true : false;
if (bool === undefined) {
return was;
}
if (bool.toggle) {
bool = !was;
}
2022-01-15 22:24:54 -05:00
if (bool.refresh && !was) {
return;
}
2022-01-14 14:14:13 -05:00
// no change
if (was === bool) {
return was;
}
if (was) {
this.mesh.remove(this._norm);
this._norm = undefined;
}
if (bool) {
this.mesh.add(this._norm = mesh.util.faceNormals(this.mesh, { size: 0.5 }));
}
}
2022-01-14 14:30:07 -05:00
// invert normals for entire mesh or selected faces depending on mod
invert(mode) {
let { modes } = mesh.api;
2022-01-14 16:45:40 -05:00
let geo = this.mesh.geometry;
let pos = geo.attributes.position;
let arr = pos.array;
function swap(i) {
let v1x = arr[i ];
let v1y = arr[i+1];
let v1z = arr[i+2];
arr[i ] = arr[i+3];
arr[i+1] = arr[i+4];
arr[i+2] = arr[i+5];
arr[i+3] = v1x;
arr[i+4] = v1y;
arr[i+5] = v1z;
}
2022-01-14 14:30:07 -05:00
switch (mode) {
case modes.object:
2022-01-14 16:45:40 -05:00
for (let i=0, l=arr.length; i<l; i += 9) {
swap(i);
}
2022-01-14 14:30:07 -05:00
break;
case modes.face:
2022-01-14 16:45:40 -05:00
for (let face of this.sel.faces) {
swap(face * 9);
}
2022-01-14 14:30:07 -05:00
break;
}
2022-01-14 16:45:40 -05:00
this.reload(arr);
if (this._norm) this._norm.update();
2022-01-14 14:30:07 -05:00
}
2022-01-15 15:04:53 -05:00
// split model along given plane
split(plane) {
// extract axes from plane and split when present (only z for now)
let { z } = plane;
2022-01-15 16:38:50 -05:00
let m4 = this.mesh.matrix;
2022-01-15 15:04:53 -05:00
return new Promise((resolve,reject) => {
let { id, matrix } = this;
worker.model_split({id, matrix, z}).then(data => {
let { o1, o2 } = data;
let model;
2022-01-15 15:04:53 -05:00
// new model becomes top
2022-01-15 16:38:50 -05:00
if (o2.length)
this.group.add(model = new mesh.model({
2022-01-15 16:55:26 -05:00
file: `${this.file}`,
2022-01-15 16:38:50 -05:00
mesh: o2
2022-01-20 00:08:06 -05:00
}).applyMatrix4(m4));
2022-01-15 16:38:50 -05:00
if (o1.length) {
2022-01-20 00:08:06 -05:00
// o1 becomes bottom
2022-01-15 16:38:50 -05:00
this.reload(o1);
2022-01-20 00:08:06 -05:00
resolve(model);
2022-01-15 16:38:50 -05:00
} else {
this.remove();
resolve(model);
2022-01-15 16:38:50 -05:00
}
});
});
}
zlist(round = 2) {
return new Promise((resolve,reject) => {
let { id, matrix } = this;
worker.model_zlist({id, matrix, round}).then(data => {
resolve(data);
2022-01-15 15:04:53 -05:00
});
});
}
2022-01-20 19:21:26 -05:00
// release from a group but remain in memory and storage
// so it can be re-assigned to another group
ungroup() {
this.group.remove(this, { free: false });
this.group = undefined;
2022-01-20 21:53:06 -05:00
return worker.model_duplicate({
matrix: this.matrix,
id: this.id,
opt: {}
}).then(data => {
this.reload(data);
});
2022-01-20 19:21:26 -05:00
}
2022-01-14 14:30:07 -05:00
// remove model from group and space
2021-12-30 22:59:46 -05:00
remove() {
if (arguments.length === 0) {
// direct call requires pass through group
this.group.remove(this);
this.group = undefined;
this.removed = 'pending';
} else {
// manage lifecycle with worker, mesh app caches, etc
this.destroy();
// tag removed for debugging
this.removed = 'complete';
}
2021-12-30 22:59:46 -05:00
}
updateBoundsBox() {
2022-01-13 12:59:18 -05:00
if (this.group) {
mesh.util.defer(this.group.deferUBB);
}
}
clearSelections() {
2022-01-13 21:17:35 -05:00
// clear face selections (since they've been deleted);
this.sel.faces = [];
this.updateSelections();
this.rebuild({ clean: true });
}
deleteSelections(mode) {
let { geometry } = this.mesh;
let { groups } = geometry;
let { array } = geometry.attributes.position;
let newtot = 0;
// filter to unselected groups
groups = groups.filter(g => g.materialIndex === 0);
for (let group of groups) {
let start = group.start * 3;
let count = group.count * 3;
newtot += group.count < Infinity ? count : array.length - start;
}
// nothing to do if new length is the same
if (newtot === array.length) {
return;
}
let newverts = new Float32Array(newtot);
let pos = 0;
// copy back unselected faces
for (let group of groups) {
let start = group.start * 3;
let count = group.count * 3;
if (count === Infinity) count = array.length - start;
let slice = array.slice(start, start + count);
newverts.set(slice, pos);
pos += count;
}
this.reload(newverts);
// clear face selections (since they've been deleted);
this.sel.faces = [];
this.updateSelections();
// let { modes } = mesh.api;
// switch (mode) {
// case modes.face:
// console.log('delete selected faces');
// break;
// }
}
updateSelections() {
let faces = this.sel.faces;
2022-01-13 12:59:18 -05:00
let groups = [];
if (faces && faces.length) {
faces = faces.sort((a,b) => a - b).slice();
let first = faces.shift();
if (first > 0) {
groups.push({ start: 0, count: first, mat: 0 });
}
let range = { start: first, count: 1, mat: 1 };
groups.push(range);
for (let face of faces) {
if (face === range.start + range.count) {
range.count++;
} else {
groups.push(range = { start: range.start + range.count, count: face - range.start - range.count });
groups.push(range = { start: face, count: 1, mat: 1 });
}
}
groups.push({ start: range.start + range.count, count: Infinity });
} else {
groups.push({ start: 0, count: Infinity });
}
let geo = this.mesh.geometry;
geo.clearGroups();
for (let group of groups) {
geo.addGroup(group.start*3, group.count*3, group.mat || 0);
}
}
selectFaces(list = [], action = {}) {
let faces = this.sel.faces;
for (let t of list) {
if (action.toggle) {
faces.remove(t) || faces.addOnce(t);
} else if (action.clear) {
faces.remove(t);
} else if (action.select) {
faces.addOnce(t);
}
2022-01-13 12:59:18 -05:00
}
}
toggleSelectedVertices(vert) {
if (Array.isArray(vert)) {
for (let e of vert) {
this.toggleSelectedVertices(e);
}
return;
}
let sel = this.sel;
let key = [x,y,z].map(v => v.round(5)).join('-');
let rec = sel.verts[key];
if (rec) {
delete sel.verts[key];
space.scene.remove(rec.sphere);
} else {
let geometry = new THREE.SphereGeometry( 0.5, 16, 16 );
let material = new MeshPhongMaterial( { color: 0x777777, transparent: true, opacity: 0.25 } );
let sphere = new Mesh( geometry, material );
sphere.position.set(x, y, z);
space.scene.add(sphere);
sel.verts[key] = {
sphere,
faces,
verts
};
}
}
2022-01-12 23:02:08 -05:00
// find adjacent faces to clicked point/line on a face
find(int, action) {
let { point, face } = int;
2022-01-12 23:02:08 -05:00
let { x, y, z } = point;
let { a, b, c } = face;
worker.model_select({
id: this.id, x, y:-z, z:y, a, b, c, matrix: this.matrix
2022-01-12 23:02:08 -05:00
}).then(data => {
let { faces, edges, verts, point } = data;
2022-01-13 21:17:35 -05:00
// console.log({data});
// this.toggleSelectedVertices(verts);
this.selectFaces(faces, action);
this.updateSelections();
2022-01-12 23:02:08 -05:00
});
}
};
})();