add mode key sohrtcuts, face selection re-triangulation

This commit is contained in:
Stewart Allen 2024-08-12 15:54:49 -04:00
commit c960501a87
8 changed files with 95 additions and 22 deletions

View file

@ -436,6 +436,7 @@ function orderClosest(array, fn, from) {
// wrapper for earcut that handles higher order dimensions and finds the // wrapper for earcut that handles higher order dimensions and finds the
// two with the greatest delta to pass to the earcut algorith, then returns // two with the greatest delta to pass to the earcut algorith, then returns
// an unwrapped array in the original dimensions // an unwrapped array in the original dimensions
// at present, only used by load.obj.parse()
function triangulate(array, holes, dims, pos) { function triangulate(array, holes, dims, pos) {
let narray; let narray;
let info; let info;

View file

@ -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) { set(x, y, z) {
this.x = x; this.x = x;
this.y = y; this.y = y;

View file

@ -110,6 +110,10 @@ class Polygon {
return this; return this;
} }
fromVectors(array) {
return this.addVerts(array.map(v => [ ...v ]).flat());
}
toObject() { toObject() {
return { return {
points: this.toArray(), points: this.toArray(),

View file

@ -216,6 +216,18 @@ function space_init(data) {
} }
let { shiftKey, metaKey, ctrlKey, code } = evt; let { shiftKey, metaKey, ctrlKey, code } = evt;
switch (code) { 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': case 'KeyQ':
return api.settings(); return api.settings();
case 'KeyI': case 'KeyI':
@ -261,7 +273,7 @@ function space_init(data) {
case 'KeyH': case 'KeyH':
return shiftKey ? selection.hide() : space.view.home(); return shiftKey ? selection.hide() : space.view.home();
case 'KeyT': case 'KeyT':
return space.view.top(); return shiftKey ? api.tool.triangulate() : space.view.top();
case 'KeyZ': case 'KeyZ':
return space.view.reset(); return space.view.reset();
} }
@ -300,7 +312,7 @@ function space_init(data) {
selection.delete(); selection.delete();
} else { } else {
for (let m of selection.models()) { for (let m of selection.models()) {
m.deleteSelections(mode); m.deleteSelections();
space.refresh() space.refresh()
} }
} }

View file

@ -1065,6 +1065,12 @@ const tool = {
} }
}, },
triangulate() {
for (let m of selection.models()) {
m.triangulateSelections();
}
},
rename(models, type = 'model') { rename(models, type = 'model') {
models = fallback(models, true); models = fallback(models, true);
let model = models[0]; let model = models[0];

View file

@ -353,14 +353,14 @@ function facon(name, sub) {
return h.i({ class: [ sub || "fa", name ] }); return h.i({ class: [ sub || "fa", name ] });
} }
function menu_item(text, fn, short) { function menu_item(text, fn, short, id) {
if (short) { if (short) {
short = Array.isArray(short) ? short : [ short ]; short = Array.isArray(short) ? short : [ short ];
short = div({ class: "short" }, short.map(s => s.startsWith('bi-') ? short = div({ class: "short" }, short.map(s => s.startsWith('bi-') ?
bicon(s) : div(s) bicon(s) : div(s)
)); ));
} }
return div({ onclick: fn }, [ return div({ id, onclick: fn }, [
div({ _: text, class: "grow" }), div({ _: text, class: "grow" }),
short short
].filter(o => o)); ].filter(o => o));
@ -432,14 +432,14 @@ function ui_build() {
div({ class: "menu" }, [ div({ class: "menu" }, [
div('Mode'), div('Mode'),
div({ class: "menu-items" }, [ div({ class: "menu-items" }, [
div({ _: 'Sketch', id: "mode-sketch", onclick: mode.sketch }), menu_item('Sketch', mode.sketch, '1', 'mode-sketch'),
hr(), hr(),
div({ _: 'Object', id: "mode-object", onclick: mode.object }), menu_item('Object', mode.object, '2', 'mode-object'),
div({ _: 'Tool', id: "mode-tool", onclick: mode.tool }), menu_item('Tool', mode.tool, '3', 'mode-tool'),
hr(), hr(),
div({ _: 'Surface', id: "mode-surface", onclick: mode.surface }), menu_item('Surface', mode.surface, '4', 'mode-surface'),
div({ _: 'Face', id: "mode-face", onclick: mode.face }), menu_item('Face', mode.face, '5', 'mode-face'),
div({ _: 'Edge', id: "mode-edge", onclick: mode.edge }), menu_item('Edge', mode.edge, '6', 'mode-edge'),
]) ])
]), ]),
div({ class: "menu sketch-on" }, [ div({ class: "menu sketch-on" }, [
@ -484,6 +484,7 @@ function ui_build() {
div('Faces'), div('Faces'),
div({ class: "menu-items" }, [ div({ class: "menu-items" }, [
menu_item('Flip Normals', tool.invert), menu_item('Flip Normals', tool.invert),
menu_item('Triangulate', tool.triangulate),
menu_item('Analyze', tool.analyze), menu_item('Analyze', tool.analyze),
menu_item('Clean', tool.clean), menu_item('Clean', tool.clean),
]) ])

View file

@ -4,6 +4,8 @@
// dep: add.array // dep: add.array
// dep: add.three // dep: add.three
// dep: geo.polygon
// dep: geo.polygons
// dep: moto.license // dep: moto.license
// dep: moto.client // dep: moto.client
// dep: mesh.object // dep: mesh.object
@ -13,10 +15,11 @@
gapp.register("mesh.model", [], (root, exports) => { gapp.register("mesh.model", [], (root, exports) => {
const { MeshPhongMaterial, MeshBasicMaterial, LineBasicMaterial } = THREE; const { MeshPhongMaterial, MeshBasicMaterial, LineBasicMaterial } = THREE;
const { BufferGeometry, BufferAttribute, DoubleSide, Mesh, Vector3 } = THREE; const { BufferGeometry, BufferAttribute, DoubleSide, Mesh, Vector3, Triangle } = THREE;
const { mesh, moto } = root; const { base, mesh, moto } = root;
const { space } = moto; const { space } = moto;
const { api } = mesh; const { api } = mesh;
const { newPolygon, polygons } = base;
const mapp = mesh; const mapp = mesh;
const worker = moto.client.fn; const worker = moto.client.fn;
@ -598,19 +601,13 @@ mesh.model = class MeshModel extends mesh.object {
} }
} }
clearSelections() { collectFacesByMaterialIndex(index) {
// clear face selections (since they've been deleted);
this.sel.faces = [];
this.updateSelections();
}
deleteSelections(mode) {
let { geometry } = this.mesh; let { geometry } = this.mesh;
let { groups } = geometry; let { groups } = geometry;
let { array } = geometry.attributes.position; let { array } = geometry.attributes.position;
let newtot = 0; let newtot = 0;
// filter to unselected groups // filter to unselected groups
groups = groups.filter(g => g.materialIndex === 0); groups = groups.filter(g => g.materialIndex === index);
for (let group of groups) { for (let group of groups) {
let start = group.start * 3; let start = group.start * 3;
let count = group.count * 3; let count = group.count * 3;
@ -631,11 +628,55 @@ mesh.model = class MeshModel extends mesh.object {
newverts.set(slice, pos); newverts.set(slice, pos);
pos += count; pos += count;
} }
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); this.reload(newverts);
// clear face selections (since they've been deleted); // clear face selections (since they've been deleted);
this.sel.faces = []; this.sel.faces = [];
this.updateSelections(); this.updateSelections();
} }
}
updateSelections() { updateSelections() {
let groups = mesh.util.facesToGroups(this.sel.faces); let groups = mesh.util.facesToGroups(this.sel.faces);

View file

@ -164,7 +164,7 @@ let model = {
let { vertices, name, id } = data; let { vertices, name, id } = data;
let geo = new BufferGeometry(); let geo = new BufferGeometry();
geo.setAttribute('position', new BufferAttribute(vertices, 3)); 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 // merge several model vertices into a single array