diff --git a/src/geo/base.js b/src/geo/base.js index 9d80758d..611fe5fb 100644 --- a/src/geo/base.js +++ b/src/geo/base.js @@ -436,6 +436,7 @@ function orderClosest(array, fn, from) { // wrapper for earcut that handles higher order dimensions and finds the // two with the greatest delta to pass to the earcut algorith, then returns // an unwrapped array in the original dimensions +// at present, only used by load.obj.parse() function triangulate(array, holes, dims, pos) { let narray; let info; diff --git a/src/geo/point.js b/src/geo/point.js index 273e646b..aee11c4c 100644 --- a/src/geo/point.js +++ b/src/geo/point.js @@ -37,6 +37,14 @@ class Point { }; } + toArray() { + return [ this.x, this.y, this.z ]; + } + + toVector3() { + return new THREE.Vector3(this.x, this.y, this.z); + } + set(x, y, z) { this.x = x; this.y = y; diff --git a/src/geo/polygon.js b/src/geo/polygon.js index 4f0357a9..73197f9f 100644 --- a/src/geo/polygon.js +++ b/src/geo/polygon.js @@ -110,6 +110,10 @@ class Polygon { return this; } + fromVectors(array) { + return this.addVerts(array.map(v => [ ...v ]).flat()); + } + toObject() { return { points: this.toArray(), diff --git a/src/main/mesh.js b/src/main/mesh.js index 464f4ab2..2f2b14b8 100644 --- a/src/main/mesh.js +++ b/src/main/mesh.js @@ -216,6 +216,18 @@ function space_init(data) { } let { shiftKey, metaKey, ctrlKey, code } = evt; switch (code) { + case 'Digit1': + return api.mode.sketch(); + case 'Digit2': + return api.mode.object(); + case 'Digit3': + return api.mode.tool(); + case 'Digit4': + return api.mode.surface(); + case 'Digit5': + return api.mode.face(); + case 'Digit6': + return api.mode.edge(); case 'KeyQ': return api.settings(); case 'KeyI': @@ -261,7 +273,7 @@ function space_init(data) { case 'KeyH': return shiftKey ? selection.hide() : space.view.home(); case 'KeyT': - return space.view.top(); + return shiftKey ? api.tool.triangulate() : space.view.top(); case 'KeyZ': return space.view.reset(); } @@ -300,7 +312,7 @@ function space_init(data) { selection.delete(); } else { for (let m of selection.models()) { - m.deleteSelections(mode); + m.deleteSelections(); space.refresh() } } diff --git a/src/mesh/api.js b/src/mesh/api.js index 28f9c3e8..e9702a6f 100644 --- a/src/mesh/api.js +++ b/src/mesh/api.js @@ -1065,6 +1065,12 @@ const tool = { } }, + triangulate() { + for (let m of selection.models()) { + m.triangulateSelections(); + } + }, + rename(models, type = 'model') { models = fallback(models, true); let model = models[0]; diff --git a/src/mesh/build.js b/src/mesh/build.js index 7e8d8a70..9182ca6d 100644 --- a/src/mesh/build.js +++ b/src/mesh/build.js @@ -353,14 +353,14 @@ function facon(name, sub) { return h.i({ class: [ sub || "fa", name ] }); } -function menu_item(text, fn, short) { +function menu_item(text, fn, short, id) { if (short) { short = Array.isArray(short) ? short : [ short ]; short = div({ class: "short" }, short.map(s => s.startsWith('bi-') ? bicon(s) : div(s) )); } - return div({ onclick: fn }, [ + return div({ id, onclick: fn }, [ div({ _: text, class: "grow" }), short ].filter(o => o)); @@ -432,14 +432,14 @@ function ui_build() { div({ class: "menu" }, [ div('Mode'), div({ class: "menu-items" }, [ - div({ _: 'Sketch', id: "mode-sketch", onclick: mode.sketch }), + menu_item('Sketch', mode.sketch, '1', 'mode-sketch'), hr(), - div({ _: 'Object', id: "mode-object", onclick: mode.object }), - div({ _: 'Tool', id: "mode-tool", onclick: mode.tool }), + menu_item('Object', mode.object, '2', 'mode-object'), + menu_item('Tool', mode.tool, '3', 'mode-tool'), hr(), - div({ _: 'Surface', id: "mode-surface", onclick: mode.surface }), - div({ _: 'Face', id: "mode-face", onclick: mode.face }), - div({ _: 'Edge', id: "mode-edge", onclick: mode.edge }), + menu_item('Surface', mode.surface, '4', 'mode-surface'), + menu_item('Face', mode.face, '5', 'mode-face'), + menu_item('Edge', mode.edge, '6', 'mode-edge'), ]) ]), div({ class: "menu sketch-on" }, [ @@ -484,6 +484,7 @@ function ui_build() { div('Faces'), div({ class: "menu-items" }, [ menu_item('Flip Normals', tool.invert), + menu_item('Triangulate', tool.triangulate), menu_item('Analyze', tool.analyze), menu_item('Clean', tool.clean), ]) diff --git a/src/mesh/model.js b/src/mesh/model.js index 4daa06b5..2d422389 100644 --- a/src/mesh/model.js +++ b/src/mesh/model.js @@ -4,6 +4,8 @@ // dep: add.array // dep: add.three +// dep: geo.polygon +// dep: geo.polygons // dep: moto.license // dep: moto.client // dep: mesh.object @@ -13,10 +15,11 @@ gapp.register("mesh.model", [], (root, exports) => { const { MeshPhongMaterial, MeshBasicMaterial, LineBasicMaterial } = THREE; -const { BufferGeometry, BufferAttribute, DoubleSide, Mesh, Vector3 } = THREE; -const { mesh, moto } = root; +const { BufferGeometry, BufferAttribute, DoubleSide, Mesh, Vector3, Triangle } = THREE; +const { base, mesh, moto } = root; const { space } = moto; const { api } = mesh; +const { newPolygon, polygons } = base; const mapp = mesh; const worker = moto.client.fn; @@ -598,19 +601,13 @@ mesh.model = class MeshModel extends mesh.object { } } - clearSelections() { - // clear face selections (since they've been deleted); - this.sel.faces = []; - this.updateSelections(); - } - - deleteSelections(mode) { + collectFacesByMaterialIndex(index) { 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); + groups = groups.filter(g => g.materialIndex === index); for (let group of groups) { let start = group.start * 3; let count = group.count * 3; @@ -631,12 +628,56 @@ mesh.model = class MeshModel extends mesh.object { newverts.set(slice, pos); pos += count; } - this.reload(newverts); + return newverts; + } + + clearSelections() { // clear face selections (since they've been deleted); this.sel.faces = []; this.updateSelections(); } + triangulateSelections() { + let selverts = this.collectFacesByMaterialIndex(1); + if (selverts?.length) { + let points = [...selverts].group(3).map(a => new Vector3().fromArray(a)); + let tris = points.group(3).map(a => new Triangle(...a)); + let norms = tris.map(t => t.getNormal(new Vector3())); + let norm = norms.reduce((a,b) => a.add(b)).normalize(); + // compute quaternion and rotate triangles to face Z up + const targetNorm = new THREE.Vector3(0, 0, 1); + const rotato = new THREE.Quaternion().setFromUnitVectors(norm, targetNorm); + points.forEach(p => p.applyQuaternion(rotato)); + // union and earcut the result + let polys = tris.map(t => newPolygon().fromVectors([ t.a, t.b, t.c ])); + let union = polygons.union(polys,0,true); + let ears = union.map(p => p.earcut()).flat(); + // unrotate using the same quaternion + rotato.invert(); + let nupoints = ears.map(p => p.points).flat().map(p => p.toVector3()); + nupoints.forEach(p => p.applyQuaternion(rotato)); + let nuverts = nupoints.map(p => p.toArray()).flat().toFloat32(); + // remove selection and append nuverts + this.deleteSelections(nuverts); + } + } + + deleteSelections(append) { + let newverts = this.collectFacesByMaterialIndex(0); + if (newverts) { + if (append) { + let all = new Float32Array(newverts.length + append.length); + all.set(newverts); + all.set(append, newverts.length); + newverts = all; + } + 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; diff --git a/src/mesh/work.js b/src/mesh/work.js index 00c95b20..b5749a15 100644 --- a/src/mesh/work.js +++ b/src/mesh/work.js @@ -164,7 +164,7 @@ let model = { let { vertices, name, id } = data; let geo = new BufferGeometry(); geo.setAttribute('position', new BufferAttribute(vertices, 3)); - cacheUpdate(id, { name, geo, mkey: '' }); + cacheUpdate(id, { name, geo, mkey: undefined, tool: undefined }); }, // merge several model vertices into a single array