Skip to content

Commit

Permalink
barycentric works in 2D
Browse files Browse the repository at this point in the history
  • Loading branch information
mikedh committed Jun 16, 2024
1 parent 1e6c60b commit b5b3aba
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 49 deletions.
19 changes: 15 additions & 4 deletions tests/test_triangles.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
except BaseException:
import generic as g

from trimesh.triangles import barycentric_to_points, points_to_barycentric


class TrianglesTest(g.unittest.TestCase):
def test_barycentric(self):
Expand All @@ -13,18 +15,27 @@ def test_barycentric(self):
# are the same as the conversion and back
for method in ["cross", "cramer"]:
for i in range(3):
barycentric = g.trimesh.triangles.points_to_barycentric(
barycentric = points_to_barycentric(
m.triangles, m.triangles[:, i], method=method
)
assert (
g.np.abs(barycentric - g.np.roll([1.0, 0, 0], i)) < 1e-8
).all()

points = g.trimesh.triangles.barycentric_to_points(
m.triangles, barycentric
)
points = barycentric_to_points(m.triangles, barycentric)
assert (g.np.abs(points - m.triangles[:, i]) < 1e-8).all()

def test_barycentric_2D(self):
# points_to_barycentric should work in 2D and 3D
tri = g.np.array([[[0, 0], [1, 0], [0, 1]]], dtype=g.np.float64)
points = g.np.array([[0.25, 0.25]])
b = points_to_barycentric(tri, points)
assert ((b > 0) & (b < 1.0)).all()

r = barycentric_to_points(tri, b)

assert g.np.allclose(r, points)

def test_closest(self):
closest = g.trimesh.triangles.closest_point(
triangles=g.data["triangles"]["triangles"],
Expand Down
61 changes: 31 additions & 30 deletions trimesh/path/polygons.py
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,7 @@ def projected(
apad=None,
tol_dot=0.01,
max_regions=200,
precise: bool = False,
):
"""
Project a mesh onto a plane and then extract the polygon
Expand Down Expand Up @@ -778,8 +779,8 @@ def projected(
if ignore_sign:
# for watertight mesh speed up projection by handling side with less faces
# check if face lies on front or back of normal
front = dot_face > tol_dot
back = dot_face < -tol_dot
front = dot_face > 0.0
back = dot_face < 0.0
# divide the mesh into front facing section and back facing parts
# and discard the faces perpendicular to the axis.
# since we are doing a unary_union later we can use the front *or*
Expand All @@ -804,20 +805,28 @@ def projected(
adjacency_check = side[mesh.face_adjacency].all(axis=1)
adjacency = mesh.face_adjacency[adjacency_check]

# transform from the mesh frame in 3D to the XY plane
to_2D = geometry.plane_transform(origin=origin, normal=normal)
# transform mesh vertices to 2D and clip the zero Z
vertices_2D = transform_points(mesh.vertices, to_2D)[:, :2]

if precise:
eps = 1e-10
faces = mesh.faces[side]
# just union all the polygons
return (
ops.unary_union(
[Polygon(f) for f in vertices_2D[np.column_stack((faces, faces[:, :1]))]]
)
.buffer(eps)
.buffer(-eps)
)

# a sequence of face indexes that are connected
face_groups = graph.connected_components(adjacency, nodes=np.nonzero(side)[0])

# if something is goofy we may end up with thousands of
# regions that do nothing except hang for an hour then segfault
if len(face_groups) > max_regions:
raise ValueError("too many disconnected groups!")

# reshape edges into shape length of faces for indexing
edges = mesh.edges_sorted.reshape((-1, 6))
# transform from the mesh frame in 3D to the XY plane
to_2D = geometry.plane_transform(origin=origin, normal=normal)
# transform mesh vertices to 2D and clip the zero Z
vertices_2D = transform_points(mesh.vertices, to_2D)[:, :2]

polygons = []
for faces in face_groups:
Expand All @@ -838,30 +847,22 @@ def projected(
# apply the scale-relative padding
padding += float(rpad) * scale

# some types of errors will lead to a bajillion disconnected
# regions and the union will take forever to fail
# so exit here early
if len(polygons) > max_regions:
raise ValueError("too many disconnected groups!")

# if there is only one region we don't need to run a union
elif len(polygons) == 1:
return polygons[0]
elif len(polygons) == 0:
return None
else:
# inflate each polygon before unioning to remove zero-size
# gaps then deflate the result after unioning by the same amount
# note the following provides a 25% speedup but needs
# more testing to see if it deflates to a decent looking
# result:
# polygon = ops.unary_union(
# [p.buffer(padding,
# join_style=2,
# mitre_limit=1.5)
# for p in polygons]).buffer(-padding)
polygon = ops.unary_union([p.buffer(padding) for p in polygons]).buffer(-padding)
return polygon
# inflate each polygon before unioning to remove zero-size
# gaps then deflate the result after unioning by the same amount
# note the following provides a 25% speedup but needs
# more testing to see if it deflates to a decent looking
# result:
# polygon = ops.unary_union(
# [p.buffer(padding,
# join_style=2,
# mitre_limit=1.5)
# for p in polygons]).buffer(-padding)
return ops.unary_union([p.buffer(padding) for p in polygons]).buffer(-padding)


def second_moments(polygon: Polygon, return_centered=False):
Expand Down
31 changes: 16 additions & 15 deletions trimesh/triangles.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,18 +475,10 @@ def barycentric_to_points(triangles, barycentric):
points : (m, 3) float
Points in space
"""
barycentric = np.asanyarray(barycentric, dtype=np.float64)
barycentric = np.array(barycentric, dtype=np.float64)
triangles = np.asanyarray(triangles, dtype=np.float64)

if not util.is_shape(triangles, (-1, 3, 3)):
raise ValueError("Triangles must be (n, 3, 3)!")
if barycentric.shape == (2,):
barycentric = np.ones((len(triangles), 2), dtype=np.float64) * barycentric
if util.is_shape(barycentric, (len(triangles), 2)):
barycentric = np.column_stack((barycentric, 1.0 - barycentric.sum(axis=1)))
elif not util.is_shape(barycentric, (len(triangles), 3)):
raise ValueError("Barycentric shape incorrect!")

# normalize in-place
barycentric /= barycentric.sum(axis=1).reshape((-1, 1))
points = (triangles * barycentric.reshape((-1, 3, 1))).sum(axis=1)

Expand All @@ -506,9 +498,9 @@ def points_to_barycentric(triangles, points, method="cramer"):
Parameters
-----------
triangles : (n, 3, 3) float
triangles : (n, 3, 2 | 3) float
Triangles vertices in space
points : (n, 3) float
points : (n, 2 | 3) float
Point in space associated with a triangle
method : str
Which method to compute the barycentric coordinates with:
Expand Down Expand Up @@ -550,13 +542,22 @@ def method_cramer():
# establish that input triangles and points are sane
triangles = np.asanyarray(triangles, dtype=np.float64)
points = np.asanyarray(points, dtype=np.float64)
if not util.is_shape(triangles, (-1, 3, 3)):

# triangles should be (n, 3, dimension)
if len(triangles.shape) != 3:
raise ValueError("triangles shape incorrect")
if not util.is_shape(points, (len(triangles), 3)):

# this should work for 2D and 3D triangles
dim = triangles.shape[2]
if (
len(points.shape) != 2
or points.shape[1] != dim
or points.shape[0] != triangles.shape[0]
):
raise ValueError("triangles and points must correspond")

edge_vectors = triangles[:, 1:] - triangles[:, :1]
w = points - triangles[:, 0].reshape((-1, 3))
w = points - triangles[:, 0].reshape((-1, dim))

if method == "cross":
return method_cross()
Expand Down

0 comments on commit b5b3aba

Please sign in to comment.