From b5b3aba6638fb85cc030658e2a643b75231f7d1f Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Sat, 15 Jun 2024 23:36:21 -0400 Subject: [PATCH] barycentric works in 2D --- tests/test_triangles.py | 19 ++++++++++--- trimesh/path/polygons.py | 61 ++++++++++++++++++++-------------------- trimesh/triangles.py | 31 ++++++++++---------- 3 files changed, 62 insertions(+), 49 deletions(-) diff --git a/tests/test_triangles.py b/tests/test_triangles.py index d94821d63..6d995984f 100644 --- a/tests/test_triangles.py +++ b/tests/test_triangles.py @@ -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): @@ -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"], diff --git a/trimesh/path/polygons.py b/trimesh/path/polygons.py index 7ede9daae..cb5f62bed 100644 --- a/trimesh/path/polygons.py +++ b/trimesh/path/polygons.py @@ -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 @@ -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* @@ -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: @@ -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): diff --git a/trimesh/triangles.py b/trimesh/triangles.py index 55e339635..75e9c07e4 100644 --- a/trimesh/triangles.py +++ b/trimesh/triangles.py @@ -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) @@ -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: @@ -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()