|
| 1 | +import copy |
| 2 | + |
| 3 | +import numpy as np |
| 4 | + |
| 5 | +from .fix_mesh import fix_mesh, simp_vol, simp_qual |
| 6 | +from . import edges |
| 7 | + |
| 8 | +__all__ = [ |
| 9 | + "make_mesh_boundaries_traversable", |
| 10 | + "delete_interior_faces", |
| 11 | + "delete_exterior_faces", |
| 12 | +] |
| 13 | + |
| 14 | + |
| 15 | +def _arg_sortrows(arr): |
| 16 | + """Before a multi column sort like MATLAB's sortrows""" |
| 17 | + i = arr[:, 1].argsort() # First sort doesn't need to be stable. |
| 18 | + j = arr[i, 0].argsort(kind="mergesort") |
| 19 | + return i[j] |
| 20 | + |
| 21 | + |
| 22 | +def _face_to_face(t): |
| 23 | + """Face to face connectivity table. |
| 24 | + Face `i` is connected to faces `ftof[ix[i]:ix[i+1], 1]` |
| 25 | + By connected, I mean shares a mutual edge. |
| 26 | +
|
| 27 | + Parameters |
| 28 | + ---------- |
| 29 | + t: array-like |
| 30 | + Mesh connectivity table. |
| 31 | +
|
| 32 | + Returns |
| 33 | + ------- |
| 34 | + ftof: array-like |
| 35 | + Face numbers connected to faces. |
| 36 | + ix: array-like |
| 37 | + indices into `ftof` |
| 38 | +
|
| 39 | + """ |
| 40 | + nt = len(t) |
| 41 | + t = np.sort(t, axis=1) |
| 42 | + e = t[:, [[0, 1], [0, 2], [1, 2]]].reshape((nt * 3, 2)) |
| 43 | + trinum = np.repeat(np.arange(nt), 3) |
| 44 | + j = _arg_sortrows(e) |
| 45 | + e = e[j, :] |
| 46 | + trinum = trinum[j] |
| 47 | + k = np.argwhere(~np.diff(e, axis=0).any(axis=1)) |
| 48 | + ftof = np.concatenate((trinum[k], trinum[k + 1]), axis=1) |
| 49 | + dmy1 = ftof[:, 0].argsort() |
| 50 | + dmy2 = ftof[:, 1].argsort() |
| 51 | + tmp = np.vstack( |
| 52 | + ( |
| 53 | + ftof[dmy1, :], |
| 54 | + np.fliplr(ftof[dmy2]), |
| 55 | + np.column_stack((np.arange(nt), np.arange(nt))), |
| 56 | + ) |
| 57 | + ) |
| 58 | + j = _arg_sortrows(tmp) |
| 59 | + ftof = tmp[j, :] |
| 60 | + ix = np.argwhere(np.diff(ftof[:, 0])) + 1 |
| 61 | + ix = np.insert(ix, 0, 0) |
| 62 | + ix = np.append(ix, len(ftof)) |
| 63 | + return ftof, ix |
| 64 | + |
| 65 | + |
| 66 | +def _vertex_to_face(vertices, faces): |
| 67 | + """Determine which faces are connected to which vertices. |
| 68 | +
|
| 69 | + Parameters |
| 70 | + ---------- |
| 71 | + vertices: array-like |
| 72 | + Vertices of the mesh. |
| 73 | + faces: array-like |
| 74 | + Mesh connectivity table. |
| 75 | +
|
| 76 | + Returns |
| 77 | + ------- |
| 78 | + vtoc: array-like |
| 79 | + face numbers connected to vertices. |
| 80 | + ix: array-like |
| 81 | + indices into `vtoc` |
| 82 | +
|
| 83 | + """ |
| 84 | + num_faces = len(faces) |
| 85 | + |
| 86 | + ext = np.tile(np.arange(0, num_faces), (3, 1)).reshape(-1, order="F") |
| 87 | + ve = np.reshape(faces, (-1,)) |
| 88 | + ve = np.vstack((ve, ext)).T |
| 89 | + ve = ve[ve[:, 0].argsort(), :] |
| 90 | + |
| 91 | + idx = np.insert(np.diff(ve[:, 0]), 0, 0) |
| 92 | + vtoc_pointer = np.argwhere(idx) |
| 93 | + vtoc_pointer = np.insert(vtoc_pointer, 0, 0) |
| 94 | + vtoc_pointer = np.append(vtoc_pointer, num_faces * 3) |
| 95 | + |
| 96 | + vtoc = ve[:, 1] |
| 97 | + |
| 98 | + return vtoc, vtoc_pointer |
| 99 | + |
| 100 | + |
| 101 | +def make_mesh_boundaries_traversable(vertices, faces, dj_cutoff=0.05): |
| 102 | + """ |
| 103 | + A mesh described by vertices and faces is "cleaned" and returned. |
| 104 | + Alternates between checking "interior" and "exterior" portions |
| 105 | + of the mesh until convergence is obtained. Convergence is defined as: |
| 106 | + having no vertices connected to more than two boundary edges. |
| 107 | +
|
| 108 | + Parameters |
| 109 | + ---------- |
| 110 | + vertices: array-like |
| 111 | + The vertices of the "uncleaned" mesh. |
| 112 | + faces: array-like |
| 113 | + The "uncleaned" mesh connectivity. |
| 114 | + dj_cutoff: float |
| 115 | + A decimal percentage (max 1.0) used to decide whether to keep or remove |
| 116 | + disconnected portions of the meshing domain. |
| 117 | +
|
| 118 | +
|
| 119 | + Returns |
| 120 | + ------- |
| 121 | + vertices: array-like |
| 122 | + The vertices of the "cleaned" mesh. |
| 123 | +
|
| 124 | + faces: array-like |
| 125 | + The "cleaned" mesh connectivity. |
| 126 | +
|
| 127 | + Notes |
| 128 | + ----- |
| 129 | +
|
| 130 | + Interior Check: Deletes faces that are within the interior of the |
| 131 | + mesh so that no vertices are connected to more than two boundary edges. For |
| 132 | + example, a barrier island could become very thin in a middle portion so that you |
| 133 | + have a vertex connected to two faces but four boundary edges, in a |
| 134 | + bow-tie type formation. |
| 135 | +
|
| 136 | + This code will delete one of those connecting |
| 137 | + faces to ensure the spit is `clean` in the sense that two boundary edges |
| 138 | + are connected to that vertex. In the case of a choice between faces to |
| 139 | + delete, the one with the lowest quality is chosen. |
| 140 | +
|
| 141 | + Exterior Check: Finds small disjoint portions of the mesh and removes |
| 142 | + them using a depth-first search. The individual disjoint portions are |
| 143 | + removed based on `dj_cutoff` which is a decimal representing a fractional |
| 144 | + threshold component of the total mesh. |
| 145 | +
|
| 146 | + """ |
| 147 | + |
| 148 | + boundary_edges, boundary_vertices = _external_topology(vertices, faces) |
| 149 | + |
| 150 | + # NB: when this inequality is not met, the mesh boundary is not valid and non-manifold |
| 151 | + while len(boundary_edges) > len(boundary_vertices): |
| 152 | + |
| 153 | + faces = delete_exterior_faces(vertices, faces, dj_cutoff) |
| 154 | + vertices, faces, _ = fix_mesh(vertices, faces, delete_unused=True) |
| 155 | + |
| 156 | + faces, _ = delete_interior_faces(vertices, faces) |
| 157 | + vertices, faces, _ = fix_mesh(vertices, faces, delete_unused=True) |
| 158 | + |
| 159 | + boundary_edges, boundary_vertices = _external_topology(vertices, faces) |
| 160 | + |
| 161 | + return vertices, faces |
| 162 | + |
| 163 | + |
| 164 | +def _external_topology(vertices, faces): |
| 165 | + """Get edges and vertices that make up the boundary of the mesh""" |
| 166 | + boundary_edges = edges.get_boundary_edges(faces) |
| 167 | + boundary_vertices = vertices[np.unique(boundary_edges.reshape(-1))] |
| 168 | + return boundary_edges, boundary_vertices |
| 169 | + |
| 170 | + |
| 171 | +def delete_exterior_faces(vertices, faces, dj_cutoff): |
| 172 | + """Deletes portions of the mesh that are "outside" or not |
| 173 | + connected to the majority which represent a fractional |
| 174 | + area less than `dj_cutoff`. |
| 175 | + """ |
| 176 | + t1 = copy.deepcopy(faces) |
| 177 | + t = np.array([]) |
| 178 | + # Calculate the total area of the patch |
| 179 | + A = np.sum(simp_vol(vertices, faces)) |
| 180 | + An = A |
| 181 | + # Based on area proportion |
| 182 | + while (An / A) > dj_cutoff: |
| 183 | + # Perform the depth-First-Search to get `nflag` |
| 184 | + nflag = _depth_first_search(vertices, t1) |
| 185 | + |
| 186 | + # Get new triangulation and its area |
| 187 | + t2 = t1[nflag == 1, :] |
| 188 | + An = np.sum(simp_vol(vertices, t2)) |
| 189 | + |
| 190 | + # If large enough, retain this component |
| 191 | + if (An / A) > dj_cutoff: |
| 192 | + if len(t) == 0: |
| 193 | + t = t2 |
| 194 | + else: |
| 195 | + t = np.concatenate((t, t2)) |
| 196 | + |
| 197 | + # Delete where nflag == 1 from tmp t1 mesh |
| 198 | + t1 = np.delete(t1, nflag == 1, axis=0) |
| 199 | + print(f"ACCEPTED: Deleting {int(np.sum(nflag==0))} faces outside the main mesh") |
| 200 | + |
| 201 | + # Calculate the remaining area |
| 202 | + An = np.sum(simp_vol(vertices, t1)) |
| 203 | + |
| 204 | + return t |
| 205 | + |
| 206 | + |
| 207 | +def delete_interior_faces(vertices, faces): |
| 208 | + """Delete interior faces that have vertices with more than |
| 209 | + two vertices declared as boundary vertices |
| 210 | + """ |
| 211 | + # Get updated boundary topology |
| 212 | + boundary_edges, boundary_vertices = _external_topology(vertices, faces) |
| 213 | + etbv = boundary_edges.reshape(-1) |
| 214 | + # Count how many edges a vertex appears in. |
| 215 | + uebtv, count = np.unique(etbv, return_counts=True) |
| 216 | + # Get the faces connected to the vertices |
| 217 | + vtoc, nne = _vertex_to_face(vertices, faces) |
| 218 | + # Vertices which appear more than twice (implying they are shared by |
| 219 | + # more than two boundary edges) |
| 220 | + del_face_idx = [] |
| 221 | + for ix in uebtv[count > 2]: |
| 222 | + conn_faces = vtoc[nne[ix] : nne[ix + 1]] |
| 223 | + del_face = [] |
| 224 | + for conn_face in conn_faces: |
| 225 | + II = etbv == faces[conn_face, 0] |
| 226 | + JJ = etbv == faces[conn_face, 1] |
| 227 | + KK = etbv == faces[conn_face, 2] |
| 228 | + if np.any(II) and np.any(JJ) and np.any(KK): |
| 229 | + del_face.append(conn_face) |
| 230 | + |
| 231 | + if len(del_face) == 1: |
| 232 | + del_face_idx.append(del_face[0]) |
| 233 | + elif len(del_face) > 1: |
| 234 | + # Delete worst quality qualifying face. |
| 235 | + qual = simp_qual(vertices, faces[del_face]) |
| 236 | + idx = np.argmin(qual) |
| 237 | + del_face_idx.append(del_face[idx]) |
| 238 | + else: |
| 239 | + # No connected faces have all vertices on boundary edge so we |
| 240 | + # select the worst quality connecting face. |
| 241 | + qual = simp_qual(vertices, faces[conn_faces]) |
| 242 | + idx = np.argmin(qual) |
| 243 | + del_face_idx.append(conn_faces[idx]) |
| 244 | + |
| 245 | + print(f"ACCEPTED: Deleting {len(del_face_idx)} faces inside the main mesh") |
| 246 | + faces = np.delete(faces, del_face_idx, 0) |
| 247 | + |
| 248 | + return faces, del_face_idx |
| 249 | + |
| 250 | + |
| 251 | +def _depth_first_search(points, faces): |
| 252 | + """Depth-First-Search (DFS) across the triangulation""" |
| 253 | + |
| 254 | + # Get graph connectivity. |
| 255 | + ftof, idx = _face_to_face(faces) |
| 256 | + |
| 257 | + nt = len(faces) |
| 258 | + |
| 259 | + # select a random face |
| 260 | + selected = np.random.randint(0, nt, 1) |
| 261 | + |
| 262 | + nflag = np.zeros(nt) |
| 263 | + |
| 264 | + searching = True |
| 265 | + |
| 266 | + visited = [] |
| 267 | + visited.append(*selected) |
| 268 | + |
| 269 | + # Traverse through connected mesh |
| 270 | + while searching: |
| 271 | + searching = False |
| 272 | + for c in visited: |
| 273 | + # Flag the current face as visited |
| 274 | + nflag[c] = 1 |
| 275 | + # Search connected faces |
| 276 | + neis = [nei for nei in ftof[idx[c] : idx[c + 1], 1]] |
| 277 | + # Flag connected faces as visited |
| 278 | + for nei in neis: |
| 279 | + if nflag[nei] == 0: |
| 280 | + nflag[nei] = 1 |
| 281 | + visited.append(nei) |
| 282 | + searching = True |
| 283 | + return nflag |
0 commit comments