Skip to content

Commit e382c14

Browse files
ADD iso remeshing.
1 parent 0cfe0c5 commit e382c14

File tree

10 files changed

+522
-14
lines changed

10 files changed

+522
-14
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313

1414
### Removed
1515

16+
## [0.3.1] 2025-04-01
17+
18+
### Added
19+
20+
### Changed
21+
22+
* Pybind11 build system was updated to Nanobind.
23+
* Github actions workflow was updated for pypi support.
24+
* Documentation was updated to build without errors.
25+
26+
### Removed
27+
1628

1729
## [0.3.1] 2023-12-05
1830

docs/examples/isolines.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import math
22

33
import compas_libigl as igl
4-
from compas.colors import ColorMap
4+
from compas.colors import ColorMap, Color
55
from compas.datastructures import Mesh
66
from compas.geometry import Rotation
77
from compas.geometry import Scale
@@ -24,7 +24,7 @@
2424
# Isolines
2525
# ==============================================================================
2626

27-
scalars = mesh.vertices_attribute("y")
27+
scalars = mesh.vertices_attribute("z")
2828
minval = min(scalars)
2929
maxval = max(scalars)
3030

@@ -41,8 +41,10 @@
4141
# Visualisation
4242
# ==============================================================================
4343

44+
4445
viewer = Viewer()
4546

47+
4648
minval = min(scalars) + 0.01
4749
maxval = max(scalars) - 0.01
4850

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import math
2+
import numpy as np
3+
import compas_libigl as igl
4+
from compas.colors import Color, ColorMap
5+
from compas.geometry import Plane
6+
from compas.datastructures import Mesh
7+
from compas.geometry import Rotation
8+
from compas.geometry import Scale
9+
from compas_viewer import Viewer
10+
11+
12+
# Load and transform mesh
13+
mesh = Mesh.from_off(igl.get_beetle())
14+
Rx = Rotation.from_axis_and_angle([1, 0, 0], math.radians(90))
15+
Rz = Rotation.from_axis_and_angle([0, 0, 1], math.radians(90))
16+
S = Scale.from_factors([10, 10, 10])
17+
mesh.transform(S * Rz * Rx)
18+
19+
# Set scalar values (using z-coordinate for this example)
20+
for vertex in mesh.vertices():
21+
mesh.vertex_attribute(vertex, "scalar", mesh.vertex_attribute(vertex, "z"))
22+
23+
# Get scalar range
24+
scalar_values = mesh.vertices_attribute("scalar")
25+
min_value, max_value = min(scalar_values), max(scalar_values)
26+
27+
# Define 5 isovalues evenly spaced between min and max
28+
isovalues = [min_value + i * (max_value - min_value) / 5 for i in range(1, 5)]
29+
print(f"Remeshing along {len(isovalues)} isolines: {isovalues}")
30+
31+
32+
vertices_and_faces = mesh.to_vertices_and_faces()
33+
V2, F2, S2 = igl.trimesh_remesh_along_isolines(
34+
vertices_and_faces,
35+
scalar_values,
36+
isovalues
37+
)
38+
39+
cpp_mesh = Mesh.from_vertices_and_faces(V2, F2)
40+
41+
viewer = Viewer()
42+
43+
viewer.scene.add(cpp_mesh, facecolor=Color.green(), show_lines=True, name="C++ Remeshed")
44+
viewer.show()
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import math
2+
import compas_libigl as igl
3+
from compas.colors import Color, ColorMap
4+
from compas.geometry import Plane
5+
from compas.datastructures import Mesh
6+
from compas.geometry import Rotation
7+
from compas.geometry import Scale
8+
from compas_viewer import Viewer
9+
10+
11+
def compute_plane_distance(point, plane):
12+
"""Compute signed distance from a point to a plane."""
13+
vector = plane.point - point
14+
return plane.normal.dot(vector)
15+
16+
17+
def slice_mesh_with_planes(mesh, planes, attribute_name="cut", combine="min"):
18+
"""Slice a mesh using the combined distance field from multiple planes."""
19+
if not planes:
20+
return None, None
21+
22+
# Compute signed distances for all planes
23+
all_distances = []
24+
for plane in planes:
25+
distances = []
26+
for vertex in mesh.vertices():
27+
point = mesh.vertex_attributes(vertex, "xyz")
28+
dist = compute_plane_distance(point, plane)
29+
distances.append(dist)
30+
all_distances.append(distances)
31+
32+
# Combine distances based on the chosen method
33+
if len(all_distances) == 1:
34+
combined_distances = all_distances[0]
35+
else:
36+
combined_distances = all_distances[0] # Start with first plane's distances
37+
for distances in all_distances[1:]: # Combine with remaining planes
38+
if combine == "min":
39+
combined_distances = [min(d1, d2) for d1, d2 in zip(combined_distances, distances)]
40+
elif combine == "max":
41+
combined_distances = [max(d1, d2) for d1, d2 in zip(combined_distances, distances)]
42+
elif combine == "average":
43+
combined_distances = [(d1 + d2) / 2 for d1, d2 in zip(combined_distances, distances)]
44+
elif combine == "multiply":
45+
combined_distances = [d1 * d2 for d1, d2 in zip(combined_distances, distances)]
46+
47+
for vertex, distance in zip(mesh.vertices(), combined_distances):
48+
mesh.vertex_attribute(vertex, attribute_name, distance)
49+
50+
# Remesh along the zero isoline (intersection)
51+
V2, F2, L = igl.trimesh_remesh_along_isoline(
52+
mesh.to_vertices_and_faces(),
53+
mesh.vertices_attribute(attribute_name),
54+
0
55+
)
56+
57+
# Split faces based on labels
58+
below_faces = [f for i, f in enumerate(F2) if L[i] == 0]
59+
above_faces = [f for i, f in enumerate(F2) if L[i] == 1]
60+
61+
# Get unique vertices for each part
62+
below_vertices = set()
63+
above_vertices = set()
64+
for face in below_faces:
65+
below_vertices.update(face)
66+
for face in above_faces:
67+
above_vertices.update(face)
68+
69+
# Create vertex maps for new indices
70+
below_vmap = {old: new for new, old in enumerate(sorted(below_vertices))}
71+
above_vmap = {old: new for new, old in enumerate(sorted(above_vertices))}
72+
73+
# Create new vertex lists
74+
below_verts = [V2[i] for i in sorted(below_vertices)]
75+
above_verts = [V2[i] for i in sorted(above_vertices)]
76+
77+
# Remap face indices
78+
below_faces = [[below_vmap[v] for v in face] for face in below_faces]
79+
above_faces = [[above_vmap[v] for v in face] for face in above_faces]
80+
81+
# Create new meshes
82+
below_mesh = Mesh.from_vertices_and_faces(below_verts, below_faces) if below_faces else None
83+
above_mesh = Mesh.from_vertices_and_faces(above_verts, above_faces) if above_faces else None
84+
85+
return below_mesh, above_mesh
86+
87+
88+
def recursive_slice(mesh, plane_groups, depth=0, region_id=0):
89+
"""Recursively slice mesh with groups of planes and assign region IDs."""
90+
if depth >= len(plane_groups):
91+
return [(mesh, region_id)]
92+
93+
below_mesh, above_mesh = slice_mesh_with_planes(mesh, plane_groups[depth], combine="min")
94+
95+
results = []
96+
# Process below mesh (region_id stays the same)
97+
if below_mesh and below_mesh.number_of_faces() > 0:
98+
results.extend(recursive_slice(below_mesh, plane_groups, depth + 1, region_id))
99+
100+
# Process above mesh (region_id gets modified)
101+
if above_mesh and above_mesh.number_of_faces() > 0:
102+
results.extend(recursive_slice(above_mesh, plane_groups, depth + 1, region_id + 2**depth))
103+
104+
return results
105+
106+
107+
# Load and transform mesh
108+
mesh = Mesh.from_off(igl.get_beetle())
109+
Rx = Rotation.from_axis_and_angle([1, 0, 0], math.radians(90))
110+
Rz = Rotation.from_axis_and_angle([0, 0, 1], math.radians(90))
111+
S = Scale.from_factors([10, 10, 10])
112+
mesh.transform(S * Rz * Rx)
113+
114+
# Define different directions for planes
115+
directions = [
116+
[0, 1, 1], # Direction 1
117+
# [1, 0, 1], # Direction 2
118+
# # Add more directions if needed
119+
# [1, 1, 0], # Direction 3
120+
]
121+
122+
# Create groups of planes at each offset
123+
plane_groups = []
124+
for i in range(-2, 3): # Creates 5 offset positions
125+
# At each offset, create a group with planes in different directions
126+
planes_at_offset = [Plane([0, 0, i], direction) for direction in directions]
127+
plane_groups.append(planes_at_offset)
128+
129+
# Perform recursive slicing
130+
mesh_regions = recursive_slice(mesh, plane_groups)
131+
print(f"Number of regions: {len(mesh_regions)}")
132+
133+
# Setup visualization
134+
viewer = Viewer()
135+
136+
# Create color gradient
137+
cmap = ColorMap.from_mpl("viridis")
138+
139+
# Add each region with its color
140+
for mesh_part, region_id in mesh_regions:
141+
if mesh_part and mesh_part.number_of_faces() > 0:
142+
# Normalize region_id to [0,1] for coloring
143+
color = cmap(region_id / (2**len(plane_groups)))
144+
viewer.scene.add(mesh_part, facecolor=color, show_lines=False)
145+
146+
viewer.show()

docs/examples/meshing_plane.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import math
2+
import compas_libigl as igl
3+
from compas.colors import Color
4+
from compas.geometry import Plane
5+
from compas.datastructures import Mesh
6+
from compas.geometry import Rotation, Scale
7+
from compas_viewer import Viewer
8+
9+
10+
def split_mesh_by_plane(mesh, plane):
11+
"""Split a mesh into two parts using a plane.
12+
13+
Parameters
14+
----------
15+
mesh : compas.datastructures.Mesh
16+
The input mesh to split
17+
plane : compas.geometry.Plane
18+
The plane to split with
19+
20+
Returns
21+
-------
22+
tuple
23+
Two meshes (below_mesh, above_mesh), representing parts on each side of the plane
24+
"""
25+
# Calculate signed distance to the plane for all vertices
26+
distances = []
27+
for vertex in mesh.vertices():
28+
point = mesh.vertex_attributes(vertex, "xyz")
29+
vector = plane.point - point
30+
distance = plane.normal.dot(vector)
31+
distances.append(distance)
32+
mesh.vertex_attribute(vertex, "distance", distance)
33+
34+
# Remesh along the zero isoline (plane intersection)
35+
V2, F2, L = igl.trimesh_remesh_along_isoline(
36+
mesh.to_vertices_and_faces(),
37+
distances,
38+
0
39+
)
40+
41+
# Split faces based on labels (0 = below, 1 = above)
42+
below_faces = [f for i, f in enumerate(F2) if L[i] == 0]
43+
above_faces = [f for i, f in enumerate(F2) if L[i] == 1]
44+
45+
# Get unique vertices for each part
46+
below_vertices = set()
47+
above_vertices = set()
48+
for face in below_faces:
49+
below_vertices.update(face)
50+
for face in above_faces:
51+
above_vertices.update(face)
52+
53+
# Create vertex maps for new indices
54+
below_vmap = {old: new for new, old in enumerate(sorted(below_vertices))}
55+
above_vmap = {old: new for new, old in enumerate(sorted(above_vertices))}
56+
57+
# Create new vertex lists
58+
below_verts = [V2[i] for i in sorted(below_vertices)]
59+
above_verts = [V2[i] for i in sorted(above_vertices)]
60+
61+
# Remap face indices
62+
below_faces = [[below_vmap[v] for v in face] for face in below_faces]
63+
above_faces = [[above_vmap[v] for v in face] for face in above_faces]
64+
65+
# Create new meshes for each part
66+
below_mesh = Mesh.from_vertices_and_faces(below_verts, below_faces) if below_faces else None
67+
above_mesh = Mesh.from_vertices_and_faces(above_verts, above_faces) if above_faces else None
68+
69+
return below_mesh, above_mesh
70+
71+
72+
# Load and transform mesh
73+
mesh = Mesh.from_off(igl.get_beetle())
74+
R = Rotation.from_axis_and_angle([1, 0, 0], math.radians(90))
75+
S = Scale.from_factors([10, 10, 10])
76+
mesh.transform(S * R)
77+
78+
# Define a single cutting plane (horizontal at z=0)
79+
cutting_plane = Plane([0, 0, 0], [0, 1, 1])
80+
81+
# Split the mesh
82+
below_mesh, above_mesh = split_mesh_by_plane(mesh, cutting_plane)
83+
print(f"Original mesh: {mesh.number_of_vertices()} vertices, {mesh.number_of_faces()} faces")
84+
print(f"Below mesh: {below_mesh.number_of_vertices()} vertices, {below_mesh.number_of_faces()} faces")
85+
print(f"Above mesh: {above_mesh.number_of_vertices()} vertices, {above_mesh.number_of_faces()} faces")
86+
87+
# Setup visualization
88+
viewer = Viewer()
89+
90+
# Add both mesh parts with different colors
91+
viewer.scene.add(below_mesh, facecolor=Color.red(), name="Below Plane")
92+
viewer.scene.add(above_mesh, facecolor=Color.blue(), name="Above Plane")
93+
94+
viewer.show()

src/compas.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include <Eigen/Dense>
2222
#include <Eigen/Sparse>
2323
#include <Eigen/Geometry>
24+
#include <Eigen/StdVector>
2425

2526
// libigl includes
2627
#include <igl/boundary_loop.h>
@@ -48,6 +49,8 @@
4849

4950
#include <igl/planarize_quad_mesh.h>
5051

52+
#include <igl/remesh_along_isoline.h>
53+
5154
#include <igl/doublearea.h>
5255
#include <igl/grad.h>
5356
#include <igl/per_vertex_normals.h>

src/compas_libigl/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
from .massmatrix import trimesh_massmatrix
1010
from .parametrisation import trimesh_harmonic, trimesh_lscm
1111
from .planarize import quadmesh_planarize
12-
13-
# from .meshing import trimesh_remesh_along_isoline
12+
from .meshing import trimesh_remesh_along_isoline, trimesh_remesh_along_isolines
1413

1514

1615
__author__ = ["tom van mele", "petras vestartas"]
@@ -91,7 +90,7 @@ def get_armadillo():
9190
"compas_libigl.massmatrix",
9291
"compas_libigl.parametrisation",
9392
"compas_libigl.planarize",
94-
# "compas_libigl.meshing",
93+
"compas_libigl.meshing",
9594
]
9695

9796
__all__ = [
@@ -116,5 +115,6 @@ def get_armadillo():
116115
"trimesh_harmonic",
117116
"trimesh_lscm",
118117
"quadmesh_planarize",
119-
# "trimesh_remesh_along_isoline",
118+
"trimesh_remesh_along_isoline",
119+
"trimesh_remesh_along_isolines",
120120
]

0 commit comments

Comments
 (0)