add surface selection and angle preference
This commit is contained in:
parent
52388e704e
commit
a9939a4b51
7 changed files with 202 additions and 58 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
|
|
|
|||
128
src/mesh/tool.js
128
src/mesh/tool.js
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
},
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue