/** Copyright Stewart Allen -- All Rights Reserved */ "use strict"; (function() { // dep: ext.earcut gapp.register("mesh.tool", [ "mesh.geom", // dep: mesh.geom ]); let mesh = self.mesh = self.mesh || {}; if (mesh.tool) return; let geom = mesh.geom; /** tool for identiying defects and healing them **/ mesh.tool = class MeshTool { constructor(params = {}) { this.precision = Math.pow(10, params.precision || 6); if (params.vertices) { this.setData(params.vertices, params.faces); } } /** * set vertices and faces of the mesh. if only vertices are provided * then the data is assumed to be un-indexed and will be indexed into * faces. if faces are provided, they are assumed to be index references * into the vertex data. removes invalid faces. * * the end result is face data mapped to an indexed set of unique vertices. * * @param {number[]} vertices * @param {number[]} faces */ setData(vertices, faces) { if (!vertices) { throw "missing vertices"; } if (vertices.length % 3 !== 0) { throw "invalid vertices"; } // when face/index data is missing, vertices need to be normalized if (!faces) { faces = []; let fcac = {}; // seen face hash let fnew = []; // accumulate vertex triplets let nuvt = []; // new vertex list let hash = {}; // find vertex matches let prec = this.precision; let dups = 0; let cull = 0; for (let i=0, l=vertices.length; i v*3); } // unique vertex array this.vertices = vertices; // unique face array. elements are offset into vertices this.faces = faces; return this; } /** * 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. */ heal(opt = { merge: true }) { let vertices = this.vertices; let faces = this.faces; let hash = {}; // key to line map let vmap = {}; // vertext to line map let lines = []; class Line { constructor(v1, v2) { this.v1 = v1; this.v2 = v2; this.faces = []; } addFace(face) { if (this.faces.indexOf(face) < 0) { this.faces.push(face); } else if (false) { console.log('adding face to line twice', this, face); } return this; } adjacentUnused() { return vmap[this.v1] .concat(vmap[this.v2]) .filter(l => l !== this && !l.used && l.faces.length === 1); } adjacentUsed() { return vmap[this.v1] .concat(vmap[this.v2]) .filter(l => l !== this && l.used && l.faces.length === 1); } touches(line) { return this.v1 === line.v1 || this.v1 === line.v2 || this.v2 === line.v1 || this.v2 === line.v2; } unitVector() { if (this.unit !== undefined) { return this.unit; } let v1 = this.v1; let v2 = this.v2; let d = [ vertices[v1] - vertices[v2], vertices[v1 + 1] - vertices[v2 + 1], vertices[v1 + 2] - vertices[v2 + 2], ]; let max = Math.max(...d.map(v => Math.abs(v))); // console.log({d, max}); return this.unit = d.map(v => v / max); } } function diffUnitVector(uv1, uv2) { let diff = 0; for (let i=0; i<3; i++) { diff += Math.abs(uv1[i] - uv2[i]); } return diff; } function getLine(v1, v2) { let s1 = vertices[v1] + vertices[v1+1]*1000 + vertices[v1+2]*100000; let s2 = vertices[v2] + vertices[v2+1]*1000 + vertices[v2+2]*100000; let key = s1 <= s2 ? [v1,v2].join('-') : [v2,v1].join('-'); let line = hash[key]; if (!line) { // point order will be swapped for reconstructed faces // to preserve chirality (assuming face normals are correct) line = hash[key] = new Line(v1, v2); line.key = key; lines.push(line); let lm1 = vmap[v1] = vmap[v1] || []; let lm2 = vmap[v2] = vmap[v2] || []; lm1.push(line); lm2.push(line); } return line; } function getXYZ(v) { return {x: vertices[v], y: vertices[v+1], z: vertices[v+2]}; } for (let i=0, l=this.faces.length; i l.faces.length === 1); // arrays of connected edges that form a closed loop let loops = this.loops = []; // connect edge lines into closed loops for (let i=0, l=edges.length; i 2) { // console.log('cannot start on a split'); continue; } // line.used = true; let loop = [ ]; // build line through adjacent lines while (true) { let adjacent = line.adjacentUnused(); if (adjacent.length === 0) { let first = loop[0]; let last = loop.last(); loop.open = loop.length < 3 || (first.v1 !== last.v1 && first.v2 !== last.v2 && first.v1 !== last.v2 && first.v2 !== last.v1); // console.log({term: line, adj: line.adjacentUsed(), open, loop}); break; } else if (adjacent.length === 1) { if (line.v2 !== adjacent[0].v1) { let tmp = adjacent[0].v1; adjacent[0].v1 = adjacent[0].v2; adjacent[0].v2 = tmp; // console.log('chirality mismatch fixed'); } line = adjacent[0]; } else if (adjacent.length === 2) { // follow edges according to chirality line = adjacent[0].v1 === line.v2 ? adjacent[0] : adjacent[1] } else { // console.log('error adjacent', line, adjacent, loops.length, loop.length); line.split = adjacent; line = adjacent[0]; } loop.push(line); line.used = true; } if (loop.length) { loops.push(loop); } } // attempt to connect open loops if (loops.length > 1) { outer: for (let i=0; i l && l.length <= 2); // console.log({edges, loops, shorts: this.shorts}); // filter out null and short loops this.loops = loops = loops.filter(l => l && l.length > 2); // console.log({loops_filtered: loops}); // rotate/flatten to Z plane and use earcut to generate faces // because earcut emits point indexes, no need to un-rotate function emitLoop(loop) { let lastPoint; let pindex = []; let points = []; let dx = 0, dy = 0, dz = 0; // gather points, find axis with least deviaton to swap with Z let loop1 = loop.slice(); loop1.push(loop[0]); for (let i=0; i= 0) { dx = Math.max(dx, Math.abs(vertices[lastPoint] - vertices[nextPoint])); dy = Math.max(dy, Math.abs(vertices[lastPoint+1] - vertices[nextPoint+1])); dz = Math.max(dz, Math.abs(vertices[lastPoint+2] - vertices[nextPoint+2])); } lastPoint = nextPoint; } // swap Z axis with least delta axis let swap = 2; if (dy < dx && dy < dz) { // swap y,z swap = 1; } else if (dx < dy && dx < dz) { // swap x,z swap = 0; } if (swap < 2) { // swap x or y for z axes for (let point of points) { let tmpz = point[2]; point[2] = point[swap]; point[swap] = tmpz; } } return { index: pindex, points: points.flat(), swap }; } // given a loop with holes, emit faces function emitFaces(loop) { let { index, points } = loop; let ec = earcut(points, undefined, 3); // map earcut point indexes back to vertex indexes return ec.map(p => index[p]); } function emitTop(rec) { let points = rec.loop.points.slice(); let index = rec.loop.index.slice(); let holesAt = index.length; for (let inner of rec.inner) { points.appendAll(inner.loop.points); index.appendAll(inner.loop.index); } let holes = index.slice(holesAt).map((v,i) => i + holesAt); let ec = earcut(points, holes, 3); // eliminate faces consisting only of hole indices let ef = ec.group(3) .filter(g => { return !( holes.contains(g[0]) && holes.contains(g[1]) && holes.contains(g[2]) ) }) .flat(); return ef.map(p => index[p]); } // new patch areas let areas = this.areas = []; if (opt.compound) { // group loops by normal (using swap as crude proxy for now) // then attempt to nest them (find holes) let polys = loops.map(loop => { return emitLoop(loop); }); // return nested mapping let nested = geom.nest(polys.map(p => p.points)); // recover and map loops from point arrays for (let rec of nested) { rec.loop = polys.filter(loop => loop.points === rec.points)[0]; // keep parent/child winding reversed let CCW = rec.depth % 2 === 0; if ((CCW && rec.area > 0) || (!CCW && rec.area < 0)) { rec.points = rec.loop.points = rec.points.group(3).reverse().flat(); rec.loop.index = rec.loop.index.reverse(); } } // extract tops which have nesting depth mod 2 === 0 let tops = nested.filter(rec => rec.depth % 2 === 0); for (let top of tops) { areas.push(emitTop(top)); } } else { // create area faces covering loops for (let loop of loops) { let l2 = emitLoop(loop); let fs = emitFaces(l2); areas.push(fs); } } // auto-merge newly found enclosed areas if (opt.merge) { this.merge(); } // de-reference loops and edges if (opt.mapped) { this.mapped = { edges: this.expandLoop(this.edges), areas: this.areas.map(a => this.expandArray(a)) }; } return this; } // add vertex data from given index into array appendVertex(index, array = []) { let vertx = this.vertices; array.push(vertx[index++]); array.push(vertx[index++]); array.push(vertx[index]); return array; } // add vertices from a vertex index list to an array expandArray(indices, array = []) { for (let i of indices) { this.appendVertex(i, array); } return array; } // turn loop into XYZ vertex array expandLoop(loop, array = []) { for (let line of loop) { this.appendVertex(line.v1, array); this.appendVertex(line.v2, array); } return array; } // merge generated poly areas into faces merge() { for (let area of this.areas || []) { this.faces.appendAll(area); } } // return non-indexed vertex list (for compatibility) unrolled() { let out = []; let vertices = this.vertices; for (let face of this.faces) { this.appendVertex(face, out); } return out; } }; })();