From 7ff172f5f30b1ea18997bdffb52ab773765076fb Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Fri, 9 Aug 2024 21:53:33 -0400 Subject: [PATCH] checkpoint --- src/main/mesh.js | 22 ++--- src/mesh/api.js | 21 ++--- src/mesh/group.js | 55 +++++++----- src/mesh/model.js | 206 +++++++++++++++++++++++++++------------------ src/mesh/object.js | 115 +++++++++++++------------ src/mesh/sketch.js | 2 +- src/mesh/work.js | 12 +-- 7 files changed, 234 insertions(+), 199 deletions(-) diff --git a/src/main/mesh.js b/src/main/mesh.js index 69c98156..26c6e199 100644 --- a/src/main/mesh.js +++ b/src/main/mesh.js @@ -126,12 +126,10 @@ async function restore_space() { 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]?.matrix) - .visible(mcache[r.id]?.visible ?? true)); + .map(r => new mesh.model(r.md, r.id).applyMeta(mcache[r.id])) if (models.length) { log(`restored ${models.length} model(s)`); - mesh.api.group.new(models, id).applyMatrix(mcache[id]?.matrix) + mesh.api.group.new(models, id).applyMeta(mcache[id]) } else { log(`removed empty group ${id}`); db_space.remove(id); @@ -237,15 +235,12 @@ function space_init(data) { case 'KeyA': if (api.mode.is([ api.modes.edge ])) return mesh.edges.add(); return shiftKey && api.tool.analyze(); - // case 'KeyR': - // return shiftKey ? api.tool.rebuild() : api.tool.repair(); case 'KeyE': if (api.mode.is([ api.modes.sketch ])) { estop(evt); return api.sketch.extrude(); } return; - // return api.tool.clean(); case 'KeyV': return shiftKey ? selection.show() : selection.focus(); case 'KeyN': @@ -335,8 +330,7 @@ function space_init(data) { break; } if (rot && floor) { - // todo future pref to auto-floor or not - rot.floor(mesh.group); + rot.floor(); } } ]); @@ -396,7 +390,7 @@ function space_init(data) { } } else if (ctrlKey) { // rotate selected face towawrd z "floor" - group.faceDown(int.face.normal); + group.rotateTowardZ(int.face.normal); selection.update(); } else { const { modes } = api; @@ -610,9 +604,9 @@ function object_visible(data) { } // cache model matrices for page restores -function object_matrix(data) { - let { id, matrix } = data; - update_meta(id, { matrix: matrix.elements }); +function object_meta(data) { + let { id, meta } = data; + update_meta(id, meta); } function object_destroy(id) { @@ -706,7 +700,7 @@ function set_snap_value(snap) { // bind functions to topics broker.listeners({ load_files, - object_matrix, + object_meta, object_destroy, object_visible, space_init, diff --git a/src/mesh/api.js b/src/mesh/api.js index 8da34177..256f29ab 100644 --- a/src/mesh/api.js +++ b/src/mesh/api.js @@ -253,14 +253,14 @@ const selection = { floor() { for (let s of selection.groups()) { - s.floor(...arguments); + s.floor(); } return selection; }, centerXY() { for (let s of [...selection.groups(), ...selection.sketches()]) { - s.centerXY(...arguments); + s.centerXY(); } return selection; }, @@ -1096,17 +1096,10 @@ const tool = { regroup(models) { models = fallback(models, true); - if (models.length === 0) { - return; + if (models.length) { + log(`regrouping ${models.length} model(s)`); + return mesh.api.group.new(models.map(m => m.ungroup())); } - log(`regrouping ${models.length} model(s)`); - let bounds = util.bounds(models); - let { mid } = bounds; - return Promise.all(models.map(m => m.ungroup())).then(() => { - return mesh.api.group.new(models) - .centerModels() - .position(mid.x, mid.y, mid.z);; - }); }, analyze(models, opt = { compound: true }) { @@ -1410,8 +1403,8 @@ const api = exports({ let left, up; if (normal) { let { x, y, z } = normal; - left = new Vector3(x,y,z).angleTo(new Vector3(0,-1,0)); - up = new Vector3(x,y,z).angleTo(new Vector3(0,0,1)); + 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; } // sets "home" views (front, back, home, reset) diff --git a/src/mesh/group.js b/src/mesh/group.js index 2e3887e1..ca5b4b98 100644 --- a/src/mesh/group.js +++ b/src/mesh/group.js @@ -13,13 +13,12 @@ gapp.register("mesh.group", [], (root, exports) => { const { mesh, moto } = root; -const { Quaternion, Group, Vector3 } = THREE; +const { Group, Box3 } = THREE; const { broker } = gapp; const { space } = moto; const call = broker.send; const worker = moto.client.fn; -const lookUp = new Vector3(0,0,-1); mesh.group = class MeshGroup extends mesh.object { @@ -42,6 +41,14 @@ mesh.group = class MeshGroup extends mesh.object { return this.group3; } + get bounds() { + let box3 = new Box3(); + for (let model of this.models) { + box3.union(model.bounds.translate(model.position())); + } + return box3; + } + // @param model {MeshModel} add(model) { model.group = this; @@ -58,7 +65,6 @@ mesh.group = class MeshGroup extends mesh.object { // @param model {MeshModel} // @param free {boolean} prevent cleanup of model so it can be re-used remove(model, opt = { free: true }) { - // console.log('group',this,'remove',model); // remove all models and group if (arguments.length === 0) { for (let m of this.models.slice()) { @@ -87,13 +93,6 @@ mesh.group = class MeshGroup extends mesh.object { return this; } - // rotate model so face normal points toward floor - faceDown(normal) { - let q = new Quaternion().setFromUnitVectors(normal, lookUp); - this.qrotation(q); - this.floor(); - } - promote() { this.centerModels(); let { dim, mid } = this.bounds; @@ -110,34 +109,48 @@ mesh.group = class MeshGroup extends mesh.object { } rotate(x = 0, y = 0, z = 0) { - console.log('group rotate', ...arguments); + this.log('group-rotate', ...arguments); for (let model of this.models) { model.rotate(...arguments); } + return this; + } + + qrotate(quaternion) { + this.log('group-rotate', quaternion.toArray()); + for (let model of this.models) { + model.qrotate(quaternion); + } + return this; } scale(x = 1, y = 1, z = 1) { + this.log('group-scale', ...arguments); for (let m of this.models) { m.scale(x, y, z); } return this; } - // center objects to group bounds - // dependent on first being added to world/scene - centerModels() { - let bounds = this.bounds; + move(x = 0, y = 0, z = 0) { + this.log('group-move', ...arguments); + if (!(x || y || z)) return; for (let model of this.models) { - model.center(bounds); - } - let { center } = mesh.api.prefs.map.space; - if (center === false) { - let { mid } = bounds; - this.move(mid.x, mid.y, mid.z); + model.move(x, y, z); } return this; } + center() { + let b = this.bounds; + return this.move(-b.mid.x, -b.mid.y, -b.mid.z); + } + + centerXY() { + let b = this.bounds; + return this.move(-b.mid.x, -b.mid.y, 0); + } + opacity() { for (let model of this.models) { model.opacity(...arguments); diff --git a/src/mesh/model.js b/src/mesh/model.js index f61fd402..eb0ca767 100644 --- a/src/mesh/model.js +++ b/src/mesh/model.js @@ -13,13 +13,15 @@ gapp.register("mesh.model", [], (root, exports) => { const { MeshPhongMaterial, MeshBasicMaterial, LineBasicMaterial } = THREE; -const { BufferGeometry, BufferAttribute, DoubleSide, Mesh } = 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 = { @@ -122,6 +124,10 @@ mesh.model = class MeshModel extends mesh.object { return this.mesh; } + get bounds() { + return this.geometry.boundingBox.clone(); + } + get matrix() { return this.mesh.matrixWorld.elements; } @@ -166,40 +172,82 @@ mesh.model = class MeshModel extends mesh.object { } } - rotate(x = 0, y = 0, z = 0) { - console.log('model rotate', ...arguments); - worker.model_rotate({ id: this.id, x, y, z }).then(() => { - this.attributes.position.needsUpdate = true; - moto.space.update(); - }); + qrotate(quaternion) { + this.log('model-rotate', quaternion.toArray()); + this.geometry.applyQuaternion(quaternion); + this.updateBounds(); + this.persist(); + moto.space.update(); } - // override and translate mesh - xmove(x = 0, y = 0, z = 0) { - let attr = this.attributes; - let arr = attr.position.array; - for (let i=0, l=arr.length; i { - 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.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; - }); + 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 = {}) { @@ -259,11 +304,9 @@ mesh.model = class MeshModel extends mesh.object { } load(vertices) { - let shared = mesh.util.toSharedF32(vertices); - let local = mesh.util.toLocal32(vertices); - console.log('load', { vertices, local, shared }); + this.log('load'); let geo = new BufferGeometry(); - geo.setAttribute('position', new BufferAttribute(shared, 3)); + geo.setAttribute('position', new BufferAttribute(vertices, 3)); let meh = this.mesh = new Mesh(geo, [ this.mats.normal, this.mats.face @@ -277,39 +320,49 @@ mesh.model = class MeshModel extends mesh.object { // this ref allows clicks to be traced to models and groups meh.model = this; // persist in db so it can be restored on page load - mapp.db.space.put(this.id, { file: this.file, mesh: local }); + this.persist(); // sync data to worker - worker.model_load({ id: this.id, name: this.file, vertices: shared }); + worker.model_load({ id: this.id, name: this.file, vertices }); + // update bounds + this.updateBounds(); } reload(vertices) { - let local = mesh.util.toLocal32(vertices); - let shared = mesh.util.toSharedF32(vertices); - console.trace('reload', { vertices, local, shared }); + this.log('reload'); let was = this.wireframe(false); let geo = this.mesh.geometry; - geo.setAttribute('position', new BufferAttribute(shared, 3)); + geo.setAttribute('position', new BufferAttribute(vertices, 3)); // signal util.box3expand that geometry changed geo._model_invalid = true; geo.computeVertexNormals(); // allows raycasting to work - geo.computeBoundingSphere(); + // geo.computeBoundingSphere(); // persist in db so it can be restored on page load - mapp.db.space.put(this.id, { file: this.file, mesh: local }); + this.persist(); // sync data to worker - worker.model_load({id: this.id, name: this.name, vertices: shared }); + worker.model_load({id: this.id, name: this.name, vertices }); // restore wireframe state this.wireframe(was); // fixup normals this.normals({refresh: true}); // re-gen face index in surface mode mesh.api.mode.check(); + // allows raycasting to work + // update bounds + this.updateBounds(); } rename(file) { this.file = file; + this.persist(); + } + + persist() { // persist in db so it can be restored on page load - mapp.db.space.put(this.id, { file, mesh: this.attributes.position.array }); + mapp.db.space.put(this.id, { + file: this.file, + mesh: this.attributes.position.array + }); } get group() { @@ -323,8 +376,12 @@ mesh.model = class MeshModel extends mesh.object { this._group = gv; } + get geometry() { + return this.mesh.geometry; + } + get attributes() { - return this.mesh.geometry.attributes; + return this.geometry.attributes; } get vertices() { @@ -460,6 +517,10 @@ mesh.model = class MeshModel extends mesh.object { } } + render() { + // TODO + } + // invert normals for entire mesh or selected faces depending on mod invert(mode) { let { modes } = mesh.api; @@ -529,13 +590,7 @@ mesh.model = class MeshModel extends mesh.object { 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); - }); + return this; } // remove model from group and space @@ -553,12 +608,6 @@ mesh.model = class MeshModel extends mesh.object { } } - updateBoundsBox() { - if (this.group) { - mesh.util.defer(this.group.deferUBB); - } - } - clearSelections() { // clear face selections (since they've been deleted); this.sel.faces = []; @@ -597,13 +646,6 @@ mesh.model = class MeshModel extends mesh.object { // 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() { diff --git a/src/mesh/object.js b/src/mesh/object.js index 662f9a03..d75cc7d6 100644 --- a/src/mesh/object.js +++ b/src/mesh/object.js @@ -14,10 +14,11 @@ const { mesh, moto } = root; const { space } = moto; const worker = moto.client.fn; +const lookUp = new Vector3(0,0,-1); // broker updates generated by objects let publish = { - matrix: gapp.broker.bind("object_matrix"), + meta: gapp.broker.bind("object_meta"), destroy: gapp.broker.bind("object_destroy"), visible: gapp.broker.bind("object_visible"), }; @@ -26,8 +27,14 @@ mesh.object = class MeshObject { constructor(id) { this.id = id || mesh.util.uuid(); - this.deferUBB = () => { this.updateBoundsBox() }; worker.object_create({id: this.id, type: this.type}); + this.log('NEW'); + // storage location for meta data like position, scale, rotation + this.meta = {}; + } + + log() { + console.log(this.id, this.type, ...arguments); } get type() { @@ -40,7 +47,7 @@ mesh.object = class MeshObject { } get bounds() { - return mesh.util.bounds(this.object); + throw "bounds() requires implementation"; } // manage lifecycle with worker, mesh app caches, etc @@ -53,16 +60,26 @@ mesh.object = class MeshObject { publish.destroy(this.id); } + // used during state restoration + applyMeta(meta = {}) { + this.log('apply-meta', meta); + this.applyMatrix(meta.matrix); + this.visible(meta.visible ?? true); + return this; + } + + // used during state restoration applyMatrix(elements) { if (elements) { - console.log('matrix', this.type, this.id); + this.log('apply-matrix'); this.object.applyMatrix4(new Matrix4().fromArray(elements)); - return this.matrixChanged(); + return this.metaChanged(); } else { return this; } } + // used during model splitting applyMatrix4(matrix) { return this.applyMatrix(matrix.elements); } @@ -93,73 +110,58 @@ mesh.object = class MeshObject { return this; } - floor(clazz) { - if (!clazz || this instanceof clazz) { - let b = this.bounds; - this.move(0, 0, -b.min.z); - } - return this; + floor() { + let b = this.bounds; + return this.move(0, 0, -b.min.z); } - center(bounds) { - let b = bounds || this.bounds; - this.move(-b.mid.x, -b.mid.y, -b.mid.z); - return this; + center() { + throw "center() requires implementation"; + let b = this.bounds; + return this.move(-b.mid.x, -b.mid.y, -b.mid.z); } - centerXY(bounds) { - let b = bounds || this.bounds; - this.move(-b.mid.x, -b.mid.y, 0); - return this; + centerXY() { + throw "centerXY() requires implementation"; + let b = this.bounds; + return this.move(-b.mid.x, -b.mid.y, 0); } - move(x = 0, y = 0, z = 0) { - let pos = this.position(); - return this.position(pos.x + x, pos.y + y, pos.z + z); + move() { + throw "move() requires implementation"; } scale() { - let scale = this.object.scale; - if (arguments.length === 0) { - return scale; - } - scale.set(...arguments); - this.matrixChanged(); + throw "scale() requires implementation"; + } + + qrotate(quaternion) { + throw "qrotate() requires implementation"; } rotate(x = 0, y = 0, z = 0) { - throw "rotate() requires implementation"; - console.log('object rotate', ...arguments); - if (x) this.object.rotateOnWorldAxis(new Vector3(1,0,0), x); - if (y) this.object.rotateOnWorldAxis(new Vector3(0,1,0), y); - if (z) this.object.rotateOnWorldAxis(new Vector3(0,0,1), z); - this.matrixChanged(); + this.log('object-rotate', ...arguments); + let m = new Matrix4(); + if (x) m.multiply(new Matrix4().makeRotationX(x)); + if (y) m.multiply(new Matrix4().makeRotationY(y)); + if (z) m.multiply(new Matrix4().makeRotationZ(z)); + this.qrotate(new Quaternion().setFromRotationMatrix(m)); return this; } rotation() { - throw "rotate() requires implementation"; - let rotation = this.object.rotation; - if (arguments.length === 0) { - return rotation; - } - return this.qrotation(new Quaternion().setFromEuler(new Euler(...arguments))); + throw "rotation() requires implementation"; } - qrotation(quaternion) { - this.object.setRotationFromQuaternion(quaternion); - this.matrixChanged(); - return this; + // rotate object in the directon of normal (place face on Z plane) + rotateTowardZ(normal) { + let q = new Quaternion().setFromUnitVectors(normal, lookUp); + this.qrotate(q); + this.floor(); } position() { - let pos = this.object.position; - if (arguments.length === 0) { - return pos; - } - pos.set(...arguments); - this.matrixChanged(); - return this; + throw "position() requires implementation"; } showBounds(bool) { @@ -175,6 +177,7 @@ mesh.object = class MeshObject { } updateBoundsBox() { + this.log('update-bounds-box', this._showBounds); let helper = this._boundsBox; let world = space.world; if (helper) { @@ -192,12 +195,12 @@ mesh.object = class MeshObject { } } - matrixChanged() { - if (!this.updateBoundsBox()) { - this.object.updateMatrix(); - } + metaChanged() { + this.object.updateMatrix(); + this.updateBoundsBox(); space.update(); - publish.matrix({ id: this.id, matrix: this.object.matrix }); + this.meta.matrix = this.object.matrix.elements; + publish.meta({ id: this.id, meta: this.meta }); return this; } }; diff --git a/src/mesh/sketch.js b/src/mesh/sketch.js index 19642f98..bdf0fdf9 100644 --- a/src/mesh/sketch.js +++ b/src/mesh/sketch.js @@ -549,7 +549,7 @@ mesh.sketch = class MeshSketch extends mesh.object { const euler = this.group.rotation; const quaternion = new THREE.Quaternion(); quaternion.setFromEuler(euler); - ngrp.qrotation(quaternion); + ngrp.qrotate(quaternion); } } } diff --git a/src/mesh/work.js b/src/mesh/work.js index eef0f3fa..7ad84e85 100644 --- a/src/mesh/work.js +++ b/src/mesh/work.js @@ -44,7 +44,7 @@ function log(msg) { mesh.log = log; function cacheUpdate(id, data) { - Object.assign(cache[id], data); + return Object.assign(cache[id], data); } // translate original mesh vertices into UI world view (PI/2 rotation on X) @@ -98,15 +98,6 @@ function indexFaces(id) { } let model = { - rotate(data) { - let { id, x, y, z } = data; - let rec = cache[id]; - let { geo } = rec; - if (x) geo.applyQuaternion(new Quaternion().setFromAxisAngle(new Vector3(1,0,0), x)); - if (y) geo.applyQuaternion(new Quaternion().setFromAxisAngle(new Vector3(0,1,0), y)); - if (z) geo.applyQuaternion(new Quaternion().setFromAxisAngle(new Vector3(0,0,1), z)); - }, - load(data) { let { vertices, name, id } = data; let geo = new BufferGeometry(); @@ -114,7 +105,6 @@ let model = { cacheUpdate(id, { name, geo, - xmatrix: core_matrix.clone(), trans: undefined, tool: undefined });