/** 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, LineBasicMaterial } = THREE; const { BufferGeometry, BufferAttribute, DoubleSide, Mesh} = THREE; const { Quaternion, Box3, Vector3 } = THREE; const { mesh, moto } = root; const { space } = moto; const { api } = mesh; const mapp = mesh; const worker = moto.client.fn; const zero = new Vector3(0,0,0); /** 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 }), wireline: new LineBasicMaterial({ side: DoubleSide, 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 } = 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); } get type() { return "model"; } get object() { return this.mesh; } get bounds() { return this.geometry.boundingBox.clone(); } get matrix() { return this.mesh.matrixWorld.elements; } get matrixWorld() { return this.mesh.matrixWorld; } drag(opt = {}) { let { start, delta, offset, end } = opt; let { snap, snapon } = api.prefs.map.space; if (start) { let mid = this.bounds.mid; this._save = { pos: Object.assign({}, mid), start: Object.assign({}, mid), } } else if (end) { delete this._save; } else if (offset) { let { pos, start } = this._save; let target = { x: start.x + offset.x, y: start.y + offset.y, z: start.z + offset.z }; if (snapon && snap) { target.x = Math.round(target.x / snap) * snap; target.y = Math.round(target.y / snap) * snap; } delta = { x: target.x - pos.x, y: target.y - pos.y, z: target.z - pos.z }; pos.x += delta.x; pos.y += delta.y; pos.z += delta.z; } if (delta) { this.move(delta.x, delta.y, delta.z); } } qrotate(quaternion) { this.log('model-rotate', quaternion.toArray()); this.geometry.applyQuaternion(quaternion); this.updateBounds(); this.persist(); moto.space.update(); } scale(x = 1, y = 1, z = 1) { this.log('model-scale', ...arguments, this.bounds); if (x === 1 && y === 1 && z === 1) return; this.geometry.scale(x, y, z); this.updateBounds(); this.persist(); moto.space.update(); } translate(x = 0, y = 0, z = 0) { this.log('model-translate', ...arguments); if (!(x || y || z)) return; this.geometry.translate(x, y, z); this.updateBounds(); this.persist(); } move(x = 0, y = 0, z = 0) { this.log('model-move', ...arguments); if (!(x || y || z)) return; let pos = this.position(); return this.position(pos.x + x, pos.y + y, pos.z + z); } position() { let pos = this.object.position; if (arguments.length === 0) { return pos; } pos.set(...arguments); this.metaChanged(); return this; } // preserves world location while updating mesh for rotation and scaling // moves mesh center to 0,0,0 via translation then // moves mesh object to former bounds center reCenter() { this.log('model-center'); let pos = this.position(); let { mid } = this.bounds; let moveTo = mid.clone().add(pos); this.translate(-mid.x, -mid.y, -mid.z); this.position(moveTo.x, moveTo.y, moveTo.z); return this; } // preserves world location while updating mesh for rotation and scaling // moves mesh center to x,y,z via translation then // moves mesh object to former bounds center centerTo(pos) { } updateBounds() { if (this._wire) this._wire.geometry.attributes.position.needsUpdate = true; this.attributes.position.needsUpdate = true; this.geometry.computeBoundingBox(); this.geometry.computeBoundingSphere(); this.log('update-bounds', this.geometry.boundingBox); this.updateBoundsBox(); moto.space.update(); } updateBoundsBox() { this.group?.updateBoundsBox(); } mirror() { return this.duplicate({ mirror: true }); } // return a model containing just this model // translated into world coordinates (rebuilt from rotation matrix) // defaults to returning a new model in a new group // options to mirror, re-use a group, or update model in-place duplicate(opt = { select: true }) { let pos = this.position(); let data = this.attributes.position.clone().array; if (opt.append) data = [...data, ...opt.append].toFloat32(); if (opt.x) return this.reload(data); let model = new mesh.model({ file: `${this.file}`, mesh: data }); let group = opt.group || mesh.api.group.new(); group.add(model); model.position(pos.x, pos.y, pos.z); model.wireframe(this.wireframe()); if (opt.select) { api.selection.add(model); } if (opt.mirror) { group.move(0, 0, group.bounds.dim.z); } else if (opt.shift) { group.move(group.bounds.dim.x, 0, 0); } return model; } 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() { if (this.group) { this.group.remove(this, { free: false }); this.group = undefined; } return this; } // 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'; } } 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(); } 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}`); }); } }; });