|
1 | 1 | import numpy as np |
2 | 2 | from compas.plugins import plugin |
| 3 | +import sys |
| 4 | +import platform |
3 | 5 |
|
4 | | -from compas_libigl import _meshing |
| 6 | +try: |
| 7 | + from compas_libigl import _meshing |
| 8 | + HAS_MESHING_MODULE = True |
| 9 | +except ImportError: |
| 10 | + HAS_MESHING_MODULE = False |
| 11 | + print("Warning: _meshing module could not be imported, using pure Python fallbacks") |
5 | 12 |
|
6 | 13 |
|
7 | 14 | @plugin(category="trimesh") |
@@ -41,7 +48,11 @@ def trimesh_remesh_along_isoline(M, scalars, isovalue): |
41 | 48 | S = np.asarray(scalars, dtype=np.float64) |
42 | 49 | isovalue = float(isovalue) |
43 | 50 |
|
44 | | - return _meshing.trimesh_remesh_along_isoline(V, F, S, isovalue) |
| 51 | + if HAS_MESHING_MODULE: |
| 52 | + return _meshing.trimesh_remesh_along_isoline(V, F, S, isovalue) |
| 53 | + else: |
| 54 | + # Pure Python fallback implementation |
| 55 | + return _pure_python_trimesh_remesh_along_isoline(V, F, S, isovalue) |
45 | 56 |
|
46 | 57 |
|
47 | 58 | @plugin(category="trimesh") |
@@ -83,4 +94,229 @@ def trimesh_remesh_along_isolines(M, scalars, isovalues): |
83 | 94 | S = np.asarray(scalars, dtype=np.float64) |
84 | 95 | values = np.asarray(isovalues, dtype=np.float64) |
85 | 96 |
|
86 | | - return _meshing.trimesh_remesh_along_isolines(V, F, S, values) |
| 97 | + if HAS_MESHING_MODULE: |
| 98 | + return _meshing.trimesh_remesh_along_isolines(V, F, S, values) |
| 99 | + else: |
| 100 | + # Pure Python fallback implementation |
| 101 | + return sequential_isoline_remeshing(V, F, S, values) |
| 102 | + |
| 103 | + |
| 104 | +def sequential_isoline_remeshing(V, F, S, isovalues): |
| 105 | + """Pure Python implementation of sequential isoline remeshing. |
| 106 | + |
| 107 | + This is used as a fallback when the C++ implementation is not available. |
| 108 | + """ |
| 109 | + current_V = V.copy() |
| 110 | + current_F = F.copy() |
| 111 | + current_S = S.copy() |
| 112 | + |
| 113 | + # Sort isovalues to ensure consistent results |
| 114 | + sorted_isovalues = sorted(isovalues) |
| 115 | + |
| 116 | + for isovalue in sorted_isovalues: |
| 117 | + # Remesh along current isoline |
| 118 | + if HAS_MESHING_MODULE: |
| 119 | + result = _meshing.trimesh_remesh_along_isoline(current_V, current_F, current_S, isovalue) |
| 120 | + else: |
| 121 | + result = _pure_python_trimesh_remesh_along_isoline(current_V, current_F, current_S, isovalue) |
| 122 | + |
| 123 | + # Update the current mesh for the next iteration |
| 124 | + current_V = result[0] |
| 125 | + current_F = result[1] |
| 126 | + # We need to interpolate scalar values for new vertices |
| 127 | + current_S = _interpolate_vertex_values(current_V, result[0], current_S) |
| 128 | + |
| 129 | + return current_V, current_F, current_S |
| 130 | + |
| 131 | + |
| 132 | +def _interpolate_vertex_values(old_vertices, new_vertices, old_values): |
| 133 | + """Interpolate scalar values for new vertices based on nearest original vertices.""" |
| 134 | + # Simple implementation for fallback - in practice you'd want to use proper interpolation |
| 135 | + # based on the barycentric coordinates, but this is sufficient for a fallback |
| 136 | + new_values = np.zeros(len(new_vertices)) |
| 137 | + |
| 138 | + # For each new vertex, find the closest old vertex and use its value |
| 139 | + for i, new_v in enumerate(new_vertices): |
| 140 | + min_dist = float('inf') |
| 141 | + min_idx = 0 |
| 142 | + for j, old_v in enumerate(old_vertices): |
| 143 | + dist = np.sum((new_v - old_v) ** 2) |
| 144 | + if dist < min_dist: |
| 145 | + min_dist = dist |
| 146 | + min_idx = j |
| 147 | + new_values[i] = old_values[min_idx] |
| 148 | + |
| 149 | + return new_values |
| 150 | + |
| 151 | + |
| 152 | +def _pure_python_trimesh_remesh_along_isoline(V, F, S, isovalue): |
| 153 | + """Pure Python implementation of trimesh_remesh_along_isoline. |
| 154 | + |
| 155 | + This is a simplified version that can be used when the C++ implementation is not available. |
| 156 | + It does not handle all edge cases correctly but provides basic functionality. |
| 157 | + """ |
| 158 | + from compas.geometry import intersect_line_plane |
| 159 | + from compas.datastructures import Mesh |
| 160 | + |
| 161 | + # Convert numpy arrays to lists |
| 162 | + vertices = V.tolist() |
| 163 | + faces = F.tolist() |
| 164 | + scalars = S.tolist() |
| 165 | + |
| 166 | + # Create a mesh |
| 167 | + mesh = Mesh.from_vertices_and_faces(vertices, faces) |
| 168 | + |
| 169 | + # Assign scalar values to vertices |
| 170 | + for i, value in enumerate(scalars): |
| 171 | + mesh.vertex_attribute(i, 'scalar', value) |
| 172 | + |
| 173 | + # New geometry to collect |
| 174 | + new_vertices = [] |
| 175 | + new_faces = [] |
| 176 | + labels = [] |
| 177 | + |
| 178 | + # Map original vertices to new indices |
| 179 | + vertex_map = {} |
| 180 | + |
| 181 | + # Create a plane from the isovalue |
| 182 | + # Assuming isovalue is in the z-direction for simplicity |
| 183 | + plane = ([0, 0, isovalue], [0, 0, 1]) |
| 184 | + |
| 185 | + # Process each original vertex |
| 186 | + for i in range(len(vertices)): |
| 187 | + vertex = vertices[i] |
| 188 | + scalar = scalars[i] |
| 189 | + |
| 190 | + # Add to new vertices and update map |
| 191 | + new_idx = len(new_vertices) |
| 192 | + new_vertices.append(vertex) |
| 193 | + vertex_map[i] = new_idx |
| 194 | + |
| 195 | + # Determine label based on scalar value |
| 196 | + label = 0 if scalar < isovalue else 1 |
| 197 | + labels.append(label) |
| 198 | + |
| 199 | + # Process each face to add intersection points |
| 200 | + edge_intersections = {} |
| 201 | + for face_idx, face in enumerate(faces): |
| 202 | + face_vertices = [vertices[i] for i in face] |
| 203 | + face_scalars = [scalars[i] for i in face] |
| 204 | + |
| 205 | + # Check if face crosses the isovalue |
| 206 | + crosses = False |
| 207 | + for i in range(len(face)): |
| 208 | + if (face_scalars[i] < isovalue) != (face_scalars[(i+1) % len(face)] < isovalue): |
| 209 | + crosses = True |
| 210 | + break |
| 211 | + |
| 212 | + if crosses: |
| 213 | + # For each edge, compute intersection if it crosses the isovalue |
| 214 | + for i in range(len(face)): |
| 215 | + v1_idx = face[i] |
| 216 | + v2_idx = face[(i+1) % len(face)] |
| 217 | + |
| 218 | + # Ensure consistent ordering of edge |
| 219 | + edge = tuple(sorted([v1_idx, v2_idx])) |
| 220 | + |
| 221 | + # Skip if we've already processed this edge |
| 222 | + if edge in edge_intersections: |
| 223 | + continue |
| 224 | + |
| 225 | + s1 = scalars[v1_idx] |
| 226 | + s2 = scalars[v2_idx] |
| 227 | + |
| 228 | + # Check if edge crosses isovalue |
| 229 | + if (s1 < isovalue) != (s2 < isovalue): |
| 230 | + # Calculate intersection point |
| 231 | + t = (isovalue - s1) / (s2 - s1) |
| 232 | + v1 = vertices[v1_idx] |
| 233 | + v2 = vertices[v2_idx] |
| 234 | + intersect_point = [ |
| 235 | + v1[0] + t * (v2[0] - v1[0]), |
| 236 | + v1[1] + t * (v2[1] - v1[1]), |
| 237 | + v1[2] + t * (v2[2] - v1[2]) |
| 238 | + ] |
| 239 | + |
| 240 | + # Add intersection point to new vertices |
| 241 | + intersect_idx = len(new_vertices) |
| 242 | + new_vertices.append(intersect_point) |
| 243 | + edge_intersections[edge] = intersect_idx |
| 244 | + |
| 245 | + # Assign label based on position (on the isovalue) |
| 246 | + labels.append(0.5) # 0.5 indicates on the boundary |
| 247 | + |
| 248 | + # Create new faces |
| 249 | + for face_idx, face in enumerate(faces): |
| 250 | + # Extract edges and check if any cross the isovalue |
| 251 | + edges = [(face[i], face[(i+1) % len(face)]) for i in range(len(face))] |
| 252 | + sorted_edges = [tuple(sorted(edge)) for edge in edges] |
| 253 | + crossing_edges = [edge for edge in sorted_edges if edge in edge_intersections] |
| 254 | + |
| 255 | + if len(crossing_edges) == 0: |
| 256 | + # Face doesn't cross isovalue, add it directly |
| 257 | + new_face = [vertex_map[v] for v in face] |
| 258 | + new_faces.append(new_face) |
| 259 | + elif len(crossing_edges) == 2: |
| 260 | + # Face crosses isovalue, split into two new faces |
| 261 | + # Get intersection points |
| 262 | + p1_idx = edge_intersections[crossing_edges[0]] |
| 263 | + p2_idx = edge_intersections[crossing_edges[1]] |
| 264 | + |
| 265 | + # Determine which vertices are on which side |
| 266 | + vertices_below = [vertex_map[v] for v in face if scalars[v] < isovalue] |
| 267 | + vertices_above = [vertex_map[v] for v in face if scalars[v] >= isovalue] |
| 268 | + |
| 269 | + # Create new faces |
| 270 | + if vertices_below: |
| 271 | + for v in vertices_below: |
| 272 | + new_faces.append([v, p1_idx, p2_idx]) |
| 273 | + |
| 274 | + if vertices_above: |
| 275 | + for v in vertices_above: |
| 276 | + new_faces.append([v, p1_idx, p2_idx]) |
| 277 | + |
| 278 | + return np.array(new_vertices), np.array(new_faces), np.array(labels) |
| 279 | + |
| 280 | + |
| 281 | +# Function to split a mesh by a plane |
| 282 | +def split_mesh_by_plane(mesh, plane_point, plane_normal): |
| 283 | + """Split a mesh into two parts based on a plane. |
| 284 | + |
| 285 | + Parameters |
| 286 | + ---------- |
| 287 | + mesh : compas.datastructures.Mesh |
| 288 | + The input mesh to split. |
| 289 | + plane_point : list |
| 290 | + A point on the splitting plane. |
| 291 | + plane_normal : list |
| 292 | + The normal vector of the splitting plane. |
| 293 | + |
| 294 | + Returns |
| 295 | + ------- |
| 296 | + tuple |
| 297 | + A tuple containing two meshes (above_mesh, below_mesh). |
| 298 | + """ |
| 299 | + # Normalize the plane normal |
| 300 | + import numpy as np |
| 301 | + from compas.datastructures import Mesh |
| 302 | + from compas.geometry import normalize_vector, dot_vectors, distance_point_plane |
| 303 | + |
| 304 | + plane_normal = normalize_vector(plane_normal) |
| 305 | + plane = (plane_point, plane_normal) |
| 306 | + |
| 307 | + # Generate scalar field based on signed distance to plane |
| 308 | + vertices = mesh.vertices_attributes('xyz') |
| 309 | + scalars = [distance_point_plane((x, y, z), plane) for x, y, z in vertices] |
| 310 | + |
| 311 | + # Use isoline remeshing at the zero distance (plane intersection) |
| 312 | + M = mesh.to_vertices_and_faces() |
| 313 | + V2, F2, L = trimesh_remesh_along_isoline(M, scalars, 0.0) |
| 314 | + |
| 315 | + # Create meshes for positive and negative regions |
| 316 | + pos_faces = [f for i, f in enumerate(F2) if all(L[v] >= 0 for v in f)] |
| 317 | + neg_faces = [f for i, f in enumerate(F2) if all(L[v] <= 0 for v in f)] |
| 318 | + |
| 319 | + above_mesh = Mesh.from_vertices_and_faces(V2, pos_faces) |
| 320 | + below_mesh = Mesh.from_vertices_and_faces(V2, neg_faces) |
| 321 | + |
| 322 | + return above_mesh, below_mesh |
0 commit comments