From 4533c85713fafcb5525d44635ff13de88548b0a9 Mon Sep 17 00:00:00 2001 From: Arian Moeini Date: Sat, 3 Oct 2026 17:33:34 +0200 Subject: [PATCH] Align concave polygon normals with boundary winding --- AUTHORS.md | 1 + src/compas/geometry/polygon.py | 15 +++++---------- tests/compas/geometry/test_polygon.py | 9 +++++++++ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index b2886dbc3656..cf7ec0e6fdbe 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -46,3 +46,4 @@ - Eric Gozzi <> [@ericgozzi](https://github.com/ericgozzi) - Daniel Nunes Locatelli <> [@daniel-locatelli](https://github.com/daniel-locatelli) - Mengxi He <> [@mengxihex](https://github.com/mengxihex) +- Arian Moeini <> [@ArianMoeini](https://github.com/ArianMoeini) diff --git a/src/compas/geometry/polygon.py b/src/compas/geometry/polygon.py index 9feffed84f0c..f1d6e07a6b98 100644 --- a/src/compas/geometry/polygon.py +++ b/src/compas/geometry/polygon.py @@ -16,7 +16,7 @@ from compas.geometry import earclip_polygon from compas.geometry import is_coplanar from compas.geometry import is_polygon_convex -from compas.geometry import normal_triangle +from compas.geometry import normal_polygon from compas.geometry import transform_points from compas.itertools import pairwise from compas.tolerance import TOL @@ -180,15 +180,10 @@ def normal(self): @property def plane(self): - # by just taking the bestfit plane, - # the normal might not be aligned with the winding direciton of the polygon - # this can be solved by comparing the plane normal with the normal of one of the triangles of the polygon - # for convex polygons this is always correct - # in the case of concave polygons it may not be - # to be entirely correct, the check should be done with one of the polygon ears after earclipping - # however, this is costly - # and even then it is only correct if we assume th polygon is plat enough to have a consistent direction - normal = normal_triangle([self.centroid] + self.points[:2]) + # Align the best-fit plane with the winding of the full boundary. + # A triangle formed from the centroid and first edge can point the + # wrong way when that edge lies on a concave part of the polygon. + normal = normal_polygon(self.points) plane = Plane.from_points(self.points) if plane.normal.dot(normal) < 0: plane.normal.flip() diff --git a/tests/compas/geometry/test_polygon.py b/tests/compas/geometry/test_polygon.py index 322a59125838..9372b16fc831 100644 --- a/tests/compas/geometry/test_polygon.py +++ b/tests/compas/geometry/test_polygon.py @@ -99,6 +99,15 @@ def test_polygon_normal_direction(): assert polygon.normal.dot([0, 0, 1]) < 0 +def test_concave_polygon_normal_follows_winding_for_any_starting_vertex(): + points = [[3, 1, 0], [1, 1, 0], [1, 4, 0], [0, 4, 0], [0, 0, 0], [4, 0, 0], [4, 4, 0], [3, 4, 0]] + + for index in range(len(points)): + rotated = points[index:] + points[:index] + assert Polygon(rotated).normal.dot([0, 0, 1]) > 0 + assert Polygon(list(reversed(rotated))).normal.dot([0, 0, 1]) < 0 + + @pytest.mark.parametrize( "points", [