/** Copyright Stewart Allen -- All Rights Reserved */ "use strict"; // dep: add.array // dep: add.three // dep: moto.license // dep: moto.client // dep: mesh.object // use: mesh.api // use: mesh.util // use: mesh.group gapp.register("mesh.model", [], (root, exports) => { const { MeshPhongMaterial, MeshBasicMaterial } = THREE; const { BufferGeometry, BufferAttribute, DoubleSide, Mesh } = THREE; const { mesh, moto } = root; const { space } = moto; const mapp = mesh; const worker = moto.client.fn; /** default materials **/ let materials = mesh.material = { // model unselected normal: new MeshPhongMaterial({ flatShading: true, side: DoubleSide, transparent: true, shininess: 125, specular: 0x202020, color: 0xf0f000, opacity: 1 }), // model selected select: new MeshPhongMaterial({ flatShading: true, side: DoubleSide, transparent: true, shininess: 125, specular: 0x202020, color: 0x00e000, opacity: 1 }), // model selected as tool tool: new MeshPhongMaterial({ flatShading: true, side: DoubleSide, transparent: true, shininess: 125, specular: 0x202020, color: 0xe00000, opacity: 1 }), // face selected (for groups ranges) face: new MeshPhongMaterial({ flatShading: true, side: DoubleSide, transparent: true, shininess: 100, specular: 0x202020, color: 0x0088ee, opacity: 1 }), wireframe: new MeshBasicMaterial({ side: DoubleSide, wireframe: true, color: 0x0, transparent: true, opacity: 0.5 }), }; /** 3D model rendered on plaform **/ mesh.model = class MeshModel extends mesh.object { constructor(data, id) { super(id); let { file, mesh, vertices, indices } = 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(), tool: materials.tool.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'; this.load(mesh || vertices, indices); } get type() { return "model"; } get object() { return this.mesh; } get matrix() { return this.mesh.matrixWorld.elements; } get matrixWorld() { return this.mesh.matrixWorld; } // override and translate mesh move(x = 0, y = 0, z = 0) { let attr = this.attributes; let arr = attr.position.array; for (let i=0, l=arr.length; i { let group = mesh.api.group.new([new mesh.model({ file: `${this.file}`, mesh: data })]).setSelected(); if (opt.mirror) { group.move(0, 0, group.bounds.dim.z); } }); } 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 => { let { lines } = data; if (lines) { let points = []; for (let i=0; i { let { id, matrix } = this; worker.model_split({id, matrix, z}).then(data => { let { o1, o2 } = data; let model; // new model becomes top if (o2.length) this.group.add(model = new mesh.model({ file: `${this.file}`, mesh: o2 }).applyMatrix4(m4)); if (o1.length) { // o1 becomes bottom this.reload(o1); resolve(model); } else { this.remove(); resolve(model); } }); }); } // 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; return worker.model_duplicate({ matrix: this.matrix, id: this.id, opt: {} }).then(data => { this.reload(data); }); } // remove model from group and space 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'; } } updateBoundsBox() { if (this.group) { mesh.util.defer(this.group.deferUBB); } } clearSelections() { // 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 groups = mesh.util.facesToGroups(this.sel.faces); 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; let map = {}; for (let f of list) { map[f] = f; } if (action.toggle) { faces = faces.filter(f => { if (map[f] !== undefined) { map[f] = undefined; return false; } else { return true; } }); faces.appendAll(Object.entries(map).filter(kv => { return kv[1] !== undefined; }).map(kv => { return kv[1]; })); this.sel.faces = faces; } else if (action.clear) { this.sel.faces = faces.filter(f => map[f] === undefined); } else if (action.select) { for (let f of faces) { map[f] = undefined; } faces.appendAll(Object.values(map).filter(f => f !== undefined)); } } 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 }; } } // find adjacent faces to clicked point/line on a face find(int, action, surface) { let { point, face } = int; let { x, y, z } = point; let { a, b, c } = face; let timer = setTimeout(() => { timer = undefined; mesh.api.log.emit("matching surface").pin(); }, 150); // let mark = Date.now(); worker.model_select({ id: this.id, x, y:-z, z:y, a, b, c, matrix: this.matrix, surface }).then(data => { // mesh.api.log.emit(`... data time = ${Date.now() - mark}`); mark = Date.now(); if (timer) { clearTimeout(timer); } let { faces, edges, verts, point } = data; // console.log({data}); // this.toggleSelectedVertices(verts); this.selectFaces(faces, action); // mesh.api.log.emit(`... select time = ${Date.now() - mark}`); mark = Date.now(); this.updateSelections(); if (!timer) { mesh.api.log.emit("surface match complete").unpin(); moto.space.refresh(); } // mesh.api.log.emit(`... paint time = ${Date.now() - mark}`); }); } }; });