From 48c0b7f0c2b899764532de9ee67af8956c5a6e60 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Thu, 18 Jan 2024 15:23:41 -0800 Subject: [PATCH] fix(polygon): Improve common_axes method to return each axis value This way, the results can be filtered without having to recompute the axes. This also gets the resulting axes to be closer to the best common axis across the segments rather than loosing accuracy by averaging the groups. --- ladybug_geometry/geometry2d/polygon.py | 61 +++++++++++--------------- tests/polygon2d_test.py | 10 ++--- 2 files changed, 31 insertions(+), 40 deletions(-) diff --git a/ladybug_geometry/geometry2d/polygon.py b/ladybug_geometry/geometry2d/polygon.py index fb1933e1..31ab515b 100644 --- a/ladybug_geometry/geometry2d/polygon.py +++ b/ladybug_geometry/geometry2d/polygon.py @@ -17,7 +17,7 @@ from ..triangulation import _linked_list, _eliminate_holes from ..intersection2d import intersect_line2d, intersect_line2d_infinite, \ does_intersection_exist_line2d, closest_point2d_on_line2d, \ - closest_end_point2d_between_line2d + closest_end_point2d_between_line2d, closest_point2d_on_line2d_infinite from ._2d import Base2DIn2D import ladybug_geometry.boolean as pb @@ -1740,10 +1740,7 @@ def gap_crossing_boundary(polygons, min_separation, tolerance): return closed_polys @staticmethod - def common_axes( - polygons, direction, min_distance, merge_distance, fraction_to_keep, - angle_tolerance - ): + def common_axes(polygons, direction, min_distance, merge_distance, angle_tolerance): """Get LineSegment2Ds for the most common axes across a set of Polygon2Ds. This is often useful as a step before aligning a set of polygons to these @@ -1764,17 +1761,20 @@ def common_axes( that are immediately adjacent to one another. When using this method to generate axes for alignment, this merge_distance should be in the range of the alignment distance. - fraction_to_keep: A number between 0 and 1 representing the fraction of - all possible axes that will be kept in the result. Depending on - the complexity of the input geometry, something between 0.1 and - 0.3 is typically appropriate. angle_tolerance: The max angle difference in radians that the polygon segments direction can differ from the input direction before the segments are not factored into this calculation of common axes. Returns: - A list of LineSegment2D objects for the common axes across the - input polygons. + A tuple with two elements. + + - common_axes: A list of LineSegment2D objects for the common + axes across the input polygons. + + - axis_values: A list of integers that aligns with the common_axes + and denotes how many segments of the input polygons each axis + relates to. Higher numbers indicate that that the axis is more + common among all of the possible axes. """ # gather the relevant segments of the input polygons min_ang, max_ang = angle_tolerance, math.pi - angle_tolerance @@ -1788,7 +1788,7 @@ def common_axes( except ZeroDivisionError: # zero length segment to ignore continue if len(rel_segs) == 0: - return [] # none of the polygon segments are relevant in the direction + return [], [] # none of the polygon segments are relevant in the direction # determine the extents around the polygons and the input direction gen_vec = direction.rotate(math.pi / 2) @@ -1823,47 +1823,38 @@ def common_axes( for axis in all_axes: axis_val = 0 for pt in mid_pts: - if axis.distance_to_point(pt) <= merge_distance: + close_pt = closest_point2d_on_line2d_infinite(pt, axis) + if close_pt.distance_to_point(pt) <= min_distance: axis_val += 1 if axis_val != 0: rel_axes.append(axis) axes_value.append(axis_val) if len(rel_axes) == 0: - return [] # none of the generated axes are relevant - - # sort the axes by how relevant they are to segments and keep a certain fraction - count_to_keep = int(len(all_axes) * fraction_to_keep) - i_to_keep = [i for _, i in sorted(zip(axes_value, range(len(rel_axes))))] - i_to_keep.reverse() - if count_to_keep == 0: - count_to_keep = 1 - elif count_to_keep > len(i_to_keep): - count_to_keep = len(i_to_keep) - rel_i = i_to_keep[:count_to_keep] - rel_i.sort() - rel_axes = [rel_axes[i] for i in rel_i] + return [], [] # none of the generated axes are relevant # group the axes by proximity last_ax = rel_axes[0] axes_groups = [[last_ax]] - for axis in rel_axes[1:]: + group_values = [[axes_value[0]]] + for axis, val in zip(rel_axes[1:], axes_value[1:]): if axis.p.distance_to_point(last_ax.p) <= merge_distance: axes_groups[-1].append(axis) + group_values[-1].append(val) else: # start a new group axes_groups.append([axis]) + group_values.append([val]) last_ax = axis # average the line segments that are within the merge_distance of one another - final_axes = [] - for ax_group in axes_groups: + axis_values = [max(val) for val in group_values] + common_axes = [] + for ax_group, grp_vals in zip(axes_groups, group_values): if len(ax_group) == 1: - final_axes.append(ax_group[0]) + common_axes.append(ax_group[0]) else: - st_pt_x = (ax_group[0].p1.x + ax_group[-1].p1.x) / 2 - st_pt_y = (ax_group[0].p1.y + ax_group[-1].p1.y) / 2 - avg_ax = LineSegment2D(Point2D(st_pt_x, st_pt_y), axis_vec) - final_axes.append(avg_ax) - return final_axes + index_max = max(range(len(grp_vals)), key=grp_vals.__getitem__) + common_axes.append(ax_group[index_max]) + return common_axes, axis_values @staticmethod def _bounding_domain_x(geometries): diff --git a/tests/polygon2d_test.py b/tests/polygon2d_test.py index 690ac0e7..d6276d82 100644 --- a/tests/polygon2d_test.py +++ b/tests/polygon2d_test.py @@ -1086,11 +1086,11 @@ def test_common_axes(): geo_dict = json.load(fp) polygons = [Polygon2D.from_dict(p) for p in geo_dict] - axes = Polygon2D.common_axes( - polygons, Vector2D(1, 0),min_distance=0.15, merge_distance=0.3, - fraction_to_keep=0.2, angle_tolerance=math.pi / 180) - - assert len(axes) == 16 + axes, values = Polygon2D.common_axes( + polygons, Vector2D(1, 0),min_distance=0.1, merge_distance=0.3, + angle_tolerance=math.pi / 180) + + assert len(axes) == 31 for item in axes: assert isinstance(item, LineSegment2D)