add surface selection and angle preference

This commit is contained in:
Stewart Allen 2022-03-31 15:24:11 -04:00
commit a9939a4b51
7 changed files with 202 additions and 58 deletions

View file

@ -33,7 +33,7 @@
## Mesh:Tool
* `C` add preferences for normal length and color
* `-` add preferences for face selection and matching
* `C` add preferences for face selection and surface matching
* `P` add svg and image import conversion (created shared load. libs)
* `C` replace triangulation algorithm that was causing some union failures
* `C` add version chooser

View file

@ -105,7 +105,7 @@ function restore_space() {
mesh.db.space.iterate({ map: true }).then(cached => {
for (let [id, data] of Object.entries(cached)) {
if (count++ === 0) {
mesh.api.log.emit(`restoring workspace...`);
mesh.api.log.emit(`restoring workspace`);
}
// restore group
if (Array.isArray(data)) {
@ -359,6 +359,8 @@ function space_init(data) {
if (mode !== api.modes.object) {
for (let m of selection.models()) {
m.deleteSelections(mode);
// re-gen face index
api.mode.check();
}
} else {
for (let s of selection.list(true)) {
@ -427,17 +429,21 @@ function space_init(data) {
selection.update();
} else {
let { modes } = api;
let radians = 0;
switch(api.mode.get()) {
case modes.object:
selection.toggle(shiftKey ? model : model.group);
break;
case modes.surface:
radians = api.prefs.map.surface.radians;
case modes.face:
case modes.line:
case modes.vertex:
// find faces adjacent to point/line clicked
model.find(int,
altKey ? { toggle: true } :
shiftKey ? { clear: true } : { select: true });
shiftKey ? { clear: true } : { select: true },
radians);
break;
}
}
@ -546,6 +552,12 @@ function set_normals_color(color) {
prefs.save();
}
function set_surface_radians(radians) {
let { prefs } = mesh.api;
prefs.map.surface.radians = radians || 0.1;
prefs.save();
}
// bind functions to topics
broker.listeners({
edit_split,
@ -557,6 +569,7 @@ broker.listeners({
set_darkmode,
set_normals_color,
set_normals_length,
set_surface_radians
});
// remove version cache bust from url

View file

@ -378,7 +378,7 @@ const tool = {
analyze(models, opt = { compound: true }) {
models = fallback(models);
api.log.emit('analyzing mesh(es)...').pin();
api.log.emit('analyzing mesh(es)').pin();
let promises = [];
let mcore = new Matrix4().makeRotationX(Math.PI / 2);
for (let m of models) {
@ -400,6 +400,21 @@ const tool = {
});
},
mapFaces(models) {
models = fallback(models);
api.log.emit('mapping faces').pin();
let promises = [];
for (let m of models) {
let p = worker.model_mapFaces({ id: m.id }).then(data => {
// console.log({map_info: data});
});
promises.push(p);
}
Promise.all(promises).then(() => {
api.log.emit('mapping complete').unpin();
});
},
heal(models, opt = {}) {
models = fallback(models);
let promises = [];
@ -426,7 +441,7 @@ const tool = {
repair(models) {
models = fallback(models);
api.log.emit('repairing mesh(es)...').pin();
api.log.emit('repairing mesh(es)').pin();
tool.heal(models, { merge: true }).then(() => {
api.log.emit('repair commplete').unpin();
api.selection.update();
@ -462,12 +477,19 @@ const mode = {
$(`mode-${key}`).classList.remove('selected');
}
$(`mode-${mode}`).classList.add('selected');
api.mode.check();
},
get() {
return prefs.map.mode;
},
check() {
if (prefs.map.mode === modes.surface) {
tool.mapFaces();
}
},
object() {
mode.set(modes.object);
},
@ -518,6 +540,9 @@ const prefs = {
length: 0.5,
color_lite: 0xff0000,
color_dark: 0x00ffff
},
surface: {
radians: 0.1
}
},

View file

@ -214,7 +214,7 @@ api.welcome = function(version = "unknown") {
api.settings = function() {
const { util } = mesh;
const { prefs } = api;
const { normals, space } = prefs.map;
const { surface, normals, space } = prefs.map;
const { dark } = space;
const col1 = h.div([
@ -249,7 +249,18 @@ api.settings = function() {
}),
]);
modal.show('settings', h.div({ class: "settings" }, [ col1, col2 ] ));
const col3 = h.div([
h.label(''),
h.label('surface'),
h.label('radians'),
h.input({ type: "text", size: 5, value: surface.radians,
onchange: ev => call.set_surface_radians(ev.target.value)
}),
h.label(' '),
h.label(' ')
]);
modal.show('settings', h.div({ class: "settings" }, [ col1, col2, col3 ] ));
}
// create html elements

View file

@ -586,12 +586,12 @@ mesh.model = class MeshModel extends mesh.object {
}
// find adjacent faces to clicked point/line on a face
find(int, action) {
find(int, action, radians = 0) {
let { point, face } = int;
let { x, y, z } = point;
let { a, b, c } = face;
worker.model_select({
id: this.id, x, y:-z, z:y, a, b, c, matrix: this.matrix
id: this.id, x, y:-z, z:y, a, b, c, matrix: this.matrix, radians
}).then(data => {
let { faces, edges, verts, point } = data;
// console.log({data});

View file

@ -16,19 +16,27 @@ const { geom } = mesh;
* to base.util.triagulate
*/
mesh.tool = class MeshTool {
constructor(params = {}, opt = { clean: true }) {
constructor(params = {}) {
this.precision = Math.pow(10, params.precision || 6);
if (params.vertices) {
this.setVertices(params.vertices, opt);
}
checkVertices(vertices, mod) {
if (!vertices) {
throw "missing vertices";
}
if (mod && vertices.length % mod !== 0) {
throw "invalid vertices";
}
return vertices;
}
/**
* @param {number[]} vertices
* @param {number[]} vertices non-indexed
*/
setVertices(vertices, opt = {}) {
this.checkVertices(vertices);
generateFaces(vertices, opt = {}) {
this.checkVertices(vertices, 3);
let doClean = opt.clean !== false;
let doDedup = opt.dedup !== false;
let faces = [];
let fcac = {}; // seen face hash
let fnew = []; // accumulate vertex triplets
@ -65,13 +73,17 @@ mesh.tool = class MeshTool {
cull++;
continue;
}
let key = fnew.slice().sort().join('-');
// drop duplicate faces (sort handles reverse order)
if (!fcac[key]) {
faces.appendAll(fnew);
fcac[key] = key;
if (doDedup) {
let key = fnew.slice().sort().join('-');
// drop duplicate faces (sort handles reverse order)
if (!fcac[key]) {
faces.appendAll(fnew);
fcac[key] = key;
} else {
dups++;
}
} else {
dups++;
faces.appendAll(fnew);
}
fnew = [];
}
@ -79,29 +91,18 @@ mesh.tool = class MeshTool {
faces.push(vpos);
}
}
vertices = nuvt;
this.stats = {cull, dups, faces:faces.length/3};
this.stats = {cull, dups, faces: faces.length/3};
// unique vertex array
this.vertices = vertices;
this.uvert = nuvt;
// unique face array. elements are offset into vertices
this.faces = faces;
return this;
}
checkVertices(v) {
const vertices = v || this.vertices;
if (!vertices) {
throw "missing vertices";
}
if (vertices.length % 3 !== 0) {
throw "invalid vertices";
}
return vertices;
}
generateFaceNormals() {
const vertices = this.checkVertices(this.vertices);
generateFaceMap(vertices) {
this.checkVertices(vertices, 3);
const normals = this.normals = [];
const sides = this.sides = [];
const _va = new THREE.Vector3();
const _vb = new THREE.Vector3();
const _vc = new THREE.Vector3();
@ -109,19 +110,80 @@ mesh.tool = class MeshTool {
_va.set(vertices[i++], vertices[i++], vertices[i++]);
_vb.set(vertices[i++], vertices[i++], vertices[i++]);
_vc.set(vertices[i++], vertices[i++], vertices[i++]);
let vn = THREE.computeFaceNormal(_va, _vb, _vc);
normals.push(vn.x, vn.y, vn.z);
const vn = THREE.computeFaceNormal(_va, _vb, _vc);
normals.push([vn.x, vn.y, vn.z]);
}
return normals;
const prec = this.precision;
const vround = vertices.map(v => (v * prec) | 0);
const sidekeyid = {};
const side2face = this.side2face = [];
for (let i=0, side=0, face=0, l=vround.length; i<l; ) {
const v1 = [ vround[i++], vround[i++], vround[i++] ].join(',');
const v2 = [ vround[i++], vround[i++], vround[i++] ].join(',');
const v3 = [ vround[i++], vround[i++], vround[i++] ].join(',');
const s1 = ( v1 < v2 ? [ v1, v2] : [ v2, v1 ] ).join('=');
const s2 = ( v2 < v3 ? [ v2, v3] : [ v3, v2 ] ).join('=');
const s3 = ( v3 < v1 ? [ v3, v1] : [ v1, v3 ] ).join('=');
const smap = [ s1, s2, s3 ].map(key => {
let id = sidekeyid[key];
if (id >= 0) {
return id;
}
id = sidekeyid[key] = side++;
side2face.push([]);
return id;
});
for (let sfa of smap.map(id => side2face[id])) {
sfa.push(face);
}
sides[face] = smap;
face++;
}
return this;
}
getAdjacentFaces(face) {
if (!this.sides) {
throw "missing sides";
}
if (!this.side2face) {
throw "missing side2face";
}
const sides = this.sides[face];
if (!sides) {
console.log(`no adjacent faces to ${face}`);
return [];
}
return sides.map(side => this.side2face[side].filter(v => v != face)).flat();
}
findConnectedSurface(faces, radians) {
const check = faces.slice();
while (check.length) {
const face = check.shift();
const norm = this.normals[face];
const fadj = this.getAdjacentFaces(face).filter(f => !faces.contains(f));
for (let f of fadj) {
const fn = this.normals[f]
.map((v,i) => Math.abs(norm[i] - v))
.reduce((a,v) => a + v);
if (fn <= radians) {
faces.push(f);
check.push(f)
}
}
}
return faces;
}
/**
* finds edge lines which are line segments on a single face.
* construct ordered line maps with array of connected edges.
* connect lines into polys. earcut polys into new faces.
* requires generateFaces() be run first
*/
patch(opt = { merge: true }) {
let vertices = this.checkVertices(this.vertices);
let vertices = this.checkVertices(this.uvert);
let faces = this.faces;
let hash = {}; // key to line map
let vmap = {}; // vertext to line map
@ -450,7 +512,7 @@ mesh.tool = class MeshTool {
// add vertex data from given index into array
appendVertex(index, array = []) {
let vertx = this.vertices;
let vertx = this.uvert;
array.push(vertx[index++]);
array.push(vertx[index++]);
array.push(vertx[index]);

View file

@ -28,6 +28,9 @@ const { client, worker } = moto;
const { util } = mesh;
const cache = {};
// add scoped access to cache
mesh.work = { cache };
// compensation for space/world/platform rotation
const core_matrix = new Matrix4().makeRotationX(Math.PI / 2);
@ -57,15 +60,33 @@ function translate_encode(id, matrix) {
}
function analyze(id, opt = {}) {
log(`${id} | analyzing...`);
let geo = cache[id].geo;
let tool = new mesh.tool({
vertices: geo.attributes.position.array,
debug: false
});
log(`${id} | patching...`);
tool.patch(opt);
dbug.log(tool);
let rec = cache[id];
let { geo, tool } = rec;
if (!tool) {
tool = rec.tool = new mesh.tool();
}
if (tool.faces) {
log(`${id} | analysis cached`);
} else {
log(`${id} | analyzing...`);
tool.generateFaces(geo.attributes.position.array);
log(`${id} | patching...`);
tool.patch(opt);
}
return tool;
}
function mapFaces(id, opt = {}) {
let rec = cache[id];
let { geo, tool } = rec;
if (!tool) {
tool = rec.tool = new mesh.tool();
}
const vertices = geo.attributes.position.array;
if (!tool.normals) {
log(`${id} | generating face map`);
tool.generateFaceMap(vertices);
}
return tool;
}
@ -74,7 +95,7 @@ let model = {
let { vertices, name, id } = data;
let geo = new BufferGeometry();
geo.setAttribute('position', new BufferAttribute(vertices, 3));
cacheUpdate(id, { name, geo, xmatrix: core_matrix.clone(), trans: undefined });
cacheUpdate(id, { name, geo, xmatrix: core_matrix.clone(), trans: undefined, tool: undefined });
},
// return new vertices in world coordinates
@ -253,20 +274,27 @@ let model = {
};
},
mapFaces(data) {
let { id, opt } = data;
let tool = mapFaces(id, opt);
return { mapped: true };
},
// given model and point, locate matching vertices, lines, and faces
select(data) {
let { id, x, y, z, a, b, c, matrix } = data;
let { id, x, y, z, a, b, c, matrix, radians } = data;
// translate point into mesh matrix space
let v3 = new Vector3(x,y,z).applyMatrix4(
core_matrix.clone().multiply(new Matrix4().fromArray(matrix)).invert()
);
x = v3.x; y = v3.y; z = v3.z;
let arr = cache[id].geo.attributes.position.array;
const rec = cache[id];
const arr = rec.geo.attributes.position.array;
// distance tolerance for click to vertex (rough distance)
let eps = 0.25;
let faces = [];
let verts = [];
let edges = [];
const eps = 0.25;
const faces = [];
const verts = [];
const edges = [];
let point;
for (let i=0, l=arr.length; i<l; ) {
// matches here are within radius of a vertex
@ -295,6 +323,11 @@ let model = {
if (faces.length === 0) {
faces.push(Math.min(a,b,c) / 3);
}
const tool = rec.tool;
if (tool && tool.sides && radians) {
const surface = tool.findConnectedSurface(faces, radians);
// console.log({surface});
}
return { faces, edges, verts, point };
},