Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions src/compas/geometry/polygon.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -181,14 +181,11 @@ 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])
# the normal might not be aligned with the winding direction of the polygon
# this can be solved by comparing the plane normal with the normal of the polygon
# using normal_polygon is more robust than normal_triangle for concave polygons
# as it considers all vertices instead of just a triangle formed by the centroid and first two points
normal = normal_polygon(self.points)
plane = Plane.from_points(self.points)
if plane.normal.dot(normal) < 0:
plane.normal.flip()
Expand Down
34 changes: 34 additions & 0 deletions tests/compas/geometry/test_polygon.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,37 @@ def test_polygon_normal_direction():
def test_polygon_duplicate_removal(points):
polygon = Polygon(points)
assert len(polygon.points) == 4


def test_polygon_normal_concave():
"""Test that polygon normal works correctly for concave polygons."""
# L-shape concave polygon
points = [
[0, 0, 0],
[2, 0, 0],
[2, 1, 0],
[1, 1, 0],
[1, 2, 0],
[0, 2, 0]
]
polygon = Polygon(points)
# Normal should point in positive Z direction for CCW winding
assert polygon.normal.dot([0, 0, 1]) > 0.99

# Arrow/chevron shape concave polygon
points = [
[0, 1, 0],
[0, 0, 0],
[2, 0, 0],
[3, 1, 0],
[2, 2, 0],
[0, 2, 0],
]
polygon = Polygon(points)
# Normal should point in positive Z direction for CCW winding
assert polygon.normal.dot([0, 0, 1]) > 0.99

# Reverse winding should give opposite normal
points_reversed = list(reversed(points))
polygon_reversed = Polygon(points_reversed)
assert polygon_reversed.normal.dot([0, 0, -1]) > 0.99
61 changes: 34 additions & 27 deletions tests/compas/geometry/test_triangulation_earclip.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,34 +64,41 @@ def test_earclip_polygon_wrong_winding():

faces = earclip_polygon(polygon)

# Expected faces updated after changing Polygon.normal to use normal_polygon
# instead of normal_triangle for more robust concave polygon handling.
# Previous behavior: Used normal_triangle with centroid and first two points,
# which could give incorrect normals for concave polygons.
# New behavior: Uses normal_polygon which considers all vertices, correctly
# detecting the winding direction for this complex concave polygon.
# Result: Different but equally valid triangulation orientation.
assert faces == [
[0, 28, 27],
[26, 25, 24],
[23, 22, 21],
[21, 20, 19],
[19, 18, 17],
[17, 16, 15],
[15, 14, 13],
[10, 9, 8],
[8, 7, 6],
[4, 3, 2],
[27, 26, 24],
[24, 23, 21],
[17, 15, 13],
[11, 10, 8],
[4, 2, 1],
[27, 24, 21],
[19, 17, 13],
[5, 4, 1],
[27, 21, 19],
[19, 13, 12],
[6, 5, 1],
[27, 19, 12],
[6, 1, 0],
[27, 12, 11],
[8, 6, 0],
[0, 27, 11],
[11, 8, 0],
[2, 3, 4],
[5, 6, 7],
[7, 8, 9],
[12, 13, 14],
[14, 15, 16],
[17, 18, 19],
[19, 20, 21],
[21, 22, 23],
[23, 24, 25],
[27, 28, 0],
[1, 2, 4],
[7, 9, 10],
[14, 16, 17],
[23, 25, 26],
[0, 1, 4],
[12, 14, 17],
[21, 23, 26],
[0, 4, 5],
[12, 17, 19],
[21, 26, 27],
[0, 5, 7],
[11, 12, 19],
[19, 21, 27],
[0, 7, 10],
[11, 19, 27],
[0, 10, 11],
[11, 27, 0],
]


Expand Down
Loading