From dd1d57a19d2ac4b8ec987bf3c8e717d42f01ef91 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Wed, 3 Apr 2024 11:06:09 -0700 Subject: [PATCH] fix(polygon2d): Add method to sense whether polygons touch one another I'm also including the bounding rectangle check inside the polygon_relationship function since we pretty much always want to perform this check first to improve the performance. --- ladybug_geometry/geometry2d/polygon.py | 117 +++++++++++++++++++++++-- ladybug_geometry/geometry3d/face.py | 39 +++++++-- tests/json/overlapping_polygons.json | 1 + tests/polygon2d_test.py | 14 +++ 4 files changed, 155 insertions(+), 16 deletions(-) create mode 100644 tests/json/overlapping_polygons.json diff --git a/ladybug_geometry/geometry2d/polygon.py b/ladybug_geometry/geometry2d/polygon.py index 49384b16..c7c07248 100644 --- a/ladybug_geometry/geometry2d/polygon.py +++ b/ladybug_geometry/geometry2d/polygon.py @@ -965,7 +965,7 @@ def is_point_inside_bound_rect(self, point, test_vector=Vector2D(1, 0.00001)): return self.is_point_inside(point, test_vector) def is_polygon_inside(self, polygon): - """"Test whether another Polygon2D lies completely inside this polygon. + """Test whether another Polygon2D lies completely inside this polygon. Args: polygon: A Polygon2D to test whether it is completely inside this one. @@ -984,7 +984,7 @@ def is_polygon_inside(self, polygon): return True def is_polygon_outside(self, polygon): - """"Test whether another Polygon2D lies completely outside this polygon. + """Test whether another Polygon2D lies completely outside this polygon. Args: polygon: A Polygon2D to test whether it is completely outside this one. @@ -1002,6 +1002,36 @@ def is_polygon_outside(self, polygon): return False return True + def does_polygon_touch(self, polygon, tolerance): + """Test whether another Polygon2D touches, overlaps or is inside this polygon. + + Args: + polygon: A Polygon2D to test whether it touches this polygon. + + Returns: + A boolean denoting whether the polygon touches (True) or not (False). + """ + # perform a bounding rectangle check to see if the polygons cannot overlap + if not Polygon2D.overlapping_bounding_rect(self, polygon, tolerance): + return False + + # first evaluate the point relationships + pt_rels1 = [self.point_relationship(pt, tolerance) for pt in polygon] + if 0 in pt_rels1 or 1 in pt_rels1: + return True # definitely touching polygons + pt_rels2 = [polygon.point_relationship(pt, tolerance) for pt in self] + if 0 in pt_rels2 or 1 in pt_rels2: + return True # definitely touching polygons + + # if any of the segments intersect the other polygon, there is overlap + for seg in self.segments: + for _s in polygon.segments: + if does_intersection_exist_line2d(seg, _s): + return True + + # we can reliably say that the polygons do not touch + return False + def polygon_relationship(self, polygon, tolerance): """Test whether another Polygon2D lies inside, outside or overlaps this one. @@ -1027,9 +1057,13 @@ def polygon_relationship(self, polygon, tolerance): This will be one of the following: * -1 = Outside this polygon - * 0 = Overlaps (intersects or contains) this polygon + * 0 = Overlaps (intersects) this polygon * +1 = Inside this polygon """ + # perform a bounding rectangle check to see if the polygons cannot overlap + if not Polygon2D.overlapping_bounding_rect(self, polygon, tolerance): + return -1 + # first evaluate the point relationships to rule out the inside case pt_rels1 = [self.point_relationship(pt, tolerance) for pt in polygon] pt_rels2 = [polygon.point_relationship(pt, tolerance) for pt in self] @@ -1056,7 +1090,7 @@ def polygon_relationship(self, polygon, tolerance): return -1 def distance_to_point(self, point): - """Get the minimum distance between this shape and the input point. + """Get the minimum distance between this shape and a point. Points that are inside the Polygon2D will return a distance of zero. If the distance of an interior point to an edge is needed, the @@ -1477,7 +1511,7 @@ def overlapping_bounding_rect(polygon1, polygon2, tolerance): @staticmethod def group_by_overlap(polygons, tolerance): - """Group Polygon2Ds that overlap one another greater than the tolerance. + """Group Polygon2Ds that overlap one another within the tolerance. This is useful as a pre-step before running Polygon2D.boolean_union_all() in order to assess whether unionizing is necessary and to ensure that @@ -1495,8 +1529,9 @@ def group_by_overlap(polygons, tolerance): A list of lists where each sub-list represents a group of polygons that all overlap with one another. """ - # sort the polygons by area to ensure larger ones grab smaller ones + # sort the polygons by area to help larger ones grab smaller ones polygons = list(sorted(polygons, key=lambda x: x.area, reverse=True)) + # loop through the polygons and check to see if it overlaps with the others grouped_polys = [[polygons[0]]] for poly in polygons[1:]: @@ -1511,7 +1546,8 @@ def group_by_overlap(polygons, tolerance): break if not group_found: # the polygon does not overlap with any of the others grouped_polys.append([poly]) # make a new group for the polygon - # if some groups were found, do several passes to merge groups + + # if some groups were found, recursively merge groups together old_group_len = len(polygons) while len(grouped_polys) != old_group_len: new_groups, g_to_remove = grouped_polys[:], [] @@ -1532,6 +1568,64 @@ def group_by_overlap(polygons, tolerance): grouped_polys = new_groups return grouped_polys + @staticmethod + def group_by_touching(polygons, tolerance): + """Group Polygon2Ds that touch or overlap one another within the tolerance. + + This is useful to group geometries together before extracting a bounding + rectangle or convex hull around multiple polygons. + + This method will return the minimal number of polygon groups + thanks to a recursive check of whether groups can be merged. + + Args: + polygons: A list of Polygon2D to be grouped by their touching. + tolerance: The minimum distance from the edge of a neighboring polygon + at which a point is considered to touch that polygon. + + Returns: + A list of lists where each sub-list represents a group of polygons + that all touch or overlap with one another. + """ + # sort the polygons by area to help larger ones grab smaller ones + polygons = list(sorted(polygons, key=lambda x: x.area, reverse=True)) + + # loop through the polygons and check to see if it overlaps with the others + grouped_polys = [[polygons[0]]] + for poly in polygons[1:]: + group_found = False + for poly_group in grouped_polys: + for oth_poly in poly_group: + if poly.does_polygon_touch(oth_poly, tolerance): + poly_group.append(poly) + group_found = True + break + if group_found: + break + if not group_found: # the polygon does not touch any of the others + grouped_polys.append([poly]) # make a new group for the polygon + + # if some groups were found, recursively merge groups together + old_group_len = len(polygons) + while len(grouped_polys) != old_group_len: + new_groups, g_to_remove = grouped_polys[:], [] + for i, group_1 in enumerate(grouped_polys): + try: + for j, group_2 in enumerate(grouped_polys[i + 1:]): + if Polygon2D._groups_touch(group_1, group_2, tolerance): + new_groups[i] = new_groups[i] + group_2 + g_to_remove.append(i + j + 1) + except IndexError: + pass # we have reached the end of the list of polygons + if len(g_to_remove) != 0: + g_to_remove = list(set(g_to_remove)) + g_to_remove.sort() + for ri in reversed(g_to_remove): + new_groups.pop(ri) + old_group_len = len(grouped_polys) + grouped_polys = new_groups + return grouped_polys + @staticmethod def _groups_overlap(group_1, group_2, tolerance): """Evaluate whether two groups of Polygons overlap with one another.""" @@ -1540,6 +1634,15 @@ def _groups_overlap(group_1, group_2, tolerance): if poly_1.polygon_relationship(poly_2, tolerance) >= 0: return True return False + + @staticmethod + def _groups_touch(group_1, group_2, tolerance): + """Evaluate whether two groups of Polygons touch with one another.""" + for poly_1 in group_1: + for poly_2 in group_2: + if poly_1.does_polygon_touch(poly_2, tolerance): + return True + return False @staticmethod def joined_intersected_boundary(polygons, tolerance): diff --git a/ladybug_geometry/geometry3d/face.py b/ladybug_geometry/geometry3d/face.py index c8877159..d1a9d8e3 100644 --- a/ladybug_geometry/geometry3d/face.py +++ b/ladybug_geometry/geometry3d/face.py @@ -1905,8 +1905,6 @@ def coplanar_difference(self, faces, tolerance, angle_tolerance): continue # test whether the two polygons have any overlap in 2D space f2_poly = Polygon2D(tuple(prim_pl.xyz_to_xy(pt) for pt in face2.boundary)) - if not Polygon2D.overlapping_bounding_rect(f1_poly, f2_poly, tolerance): - continue if f1_poly.polygon_relationship(f2_poly, tolerance) == -1: continue # snap the polygons to one another to avoid tolerance issues @@ -1963,8 +1961,6 @@ def coplanar_union(face1, face2, tolerance, angle_tolerance): # test whether the two polygons have any overlap in 2D space f1_poly = face1.boundary_polygon2d f2_poly = Polygon2D(tuple(prim_pl.xyz_to_xy(pt) for pt in face2.boundary)) - if not Polygon2D.overlapping_bounding_rect(f1_poly, f2_poly, tolerance): - return None if f1_poly.polygon_relationship(f2_poly, tolerance) == -1: return None # snap the polygons to one another to avoid tolerance issues @@ -2022,8 +2018,6 @@ def coplanar_intersection(face1, face2, tolerance, angle_tolerance): # test whether the two polygons have any overlap in 2D space f1_poly = face1.boundary_polygon2d f2_poly = Polygon2D(tuple(prim_pl.xyz_to_xy(pt) for pt in face2.boundary)) - if not Polygon2D.overlapping_bounding_rect(f1_poly, f2_poly, tolerance): - return None if f1_poly.polygon_relationship(f2_poly, tolerance) == -1: return None # snap the polygons to one another to avoid tolerance issues @@ -2084,8 +2078,6 @@ def coplanar_split(face1, face2, tolerance, angle_tolerance): # test whether the two polygons have any overlap in 2D space f1_poly = face1.boundary_polygon2d f2_poly = Polygon2D(tuple(prim_pl.xyz_to_xy(pt) for pt in face2.boundary)) - if not Polygon2D.overlapping_bounding_rect(f1_poly, f2_poly, tolerance): - return [face1], [face2] if f1_poly.polygon_relationship(f2_poly, tolerance) == -1: return [face1], [face2] # snap the polygons to one another to avoid tolerance issues @@ -2176,9 +2168,12 @@ def group_by_coplanar_overlap(faces, tolerance): """Group coplanar Face3Ds depending on whether they overlap one another. This is useful as a pre-step before running Face3D.coplanar_union() - in order to assess whether unionizing is necessary and to ensure that + in order to assess whether union-ing is necessary and to ensure that it is only performed among the necessary groups of faces. + This method will return the minimal number of overlapping polygon groups + thanks to a recursive check of whether groups can be merged. + Args: faces: A list of Face3D to be grouped by their overlapping. tolerance: The minimum distance from the edge of a neighboring Face3D @@ -2194,6 +2189,7 @@ def group_by_coplanar_overlap(faces, tolerance): r_plane = faces[0].plane polygons = [Polygon2D([r_plane.xyz_to_xy(pt) for pt in face.vertices]) for face in faces] + # loop through the polygons and check to see if it overlaps with the others grouped_polys, grouped_faces = [[polygons[0]]], [[faces[0]]] for poly, face in zip(polygons[1:], faces[1:]): @@ -2210,6 +2206,31 @@ def group_by_coplanar_overlap(faces, tolerance): if not group_found: # the polygon does not overlap with any of the others grouped_polys.append([poly]) # make a new group for the polygon grouped_faces.append([face]) # make a new group for the face + + # if some groups were found, recursively merge groups together + old_group_len = len(polygons) + while len(grouped_polys) != old_group_len: + new_poly_groups, new_face_groups = grouped_polys[:], grouped_faces[:] + g_to_remove = [] + for i, group_1 in enumerate(grouped_polys): + try: + zip_obj = zip(grouped_polys[i + 1:], grouped_faces[i + 1:]) + for j, (group_2, f2) in enumerate(zip_obj): + if Polygon2D._groups_overlap(group_1, group_2, tolerance): + new_poly_groups[i] = new_poly_groups[i] + group_2 + new_face_groups[i] = new_face_groups[i] + f2 + g_to_remove.append(i + j + 1) + except IndexError: + pass # we have reached the end of the list of polygons + if len(g_to_remove) != 0: + g_to_remove = list(set(g_to_remove)) + g_to_remove.sort() + for ri in reversed(g_to_remove): + new_poly_groups.pop(ri) + new_face_groups.pop(ri) + old_group_len = len(grouped_polys) + grouped_polys = new_poly_groups + grouped_faces = new_face_groups return grouped_faces @staticmethod diff --git a/tests/json/overlapping_polygons.json b/tests/json/overlapping_polygons.json new file mode 100644 index 00000000..d1dfb1c4 --- /dev/null +++ b/tests/json/overlapping_polygons.json @@ -0,0 +1 @@ +[{"vertices": [[1.0782614604505325, 11.089639205898733], [1.0782614604505325, 12.359864059408501], [2.670308962557908, 12.359864059408501], [2.6703089625579071, 11.089639205898733]], "type": "Polygon2D"}, {"vertices": [[1.0782614604505325, 13.630088912918266], [1.0782614604505325, 14.900313766428035], [2.670308962557908, 14.900313766428035], [2.6703089625579071, 13.630088912918266]], "type": "Polygon2D"}, {"vertices": [[1.0782614604505325, 12.359864059408498], [1.0782614604505325, 13.630088912918266], [2.670308962557908, 13.630088912918266], [2.6703089625579071, 12.359864059408498]], "type": "Polygon2D"}, {"vertices": [[1.0782614604505325, 14.900313766428033], [1.0782614604505325, 16.170538619937801], [2.670308962557908, 16.170538619937801], [2.6703089625579071, 14.900313766428033]], "type": "Polygon2D"}, {"vertices": [[1.0782614604505325, 9.5296392058987323], [1.0782614604505325, 11.089639205898733], [2.6703089625579053, 11.089639205898733], [2.6703089625579053, 11.078239205900211], [1.0847319365780956, 9.5296392058987323]], "type": "Polygon2D"}, {"vertices": [[2.5707318251360971, 14.900313766428026], [2.5707318251360971, 16.170538619937794], [4.162779327243471, 16.170538619937794], [4.162779327243471, 14.900313766428026]], "type": "Polygon2D"}, {"vertices": [[2.5707318251360971, 12.359864059408498], [2.5707318251360971, 13.630088912918266], [4.162779327243471, 13.630088912918266], [4.162779327243471, 12.359864059408498]], "type": "Polygon2D"}, {"vertices": [[2.5707318251360971, 13.630088912918266], [2.5707318251360971, 14.900313766428035], [4.162779327243471, 14.900313766428035], [4.162779327243471, 13.630088912918266]], "type": "Polygon2D"}, {"vertices": [[2.5707318251360971, 11.089639205898733], [2.5707318251360971, 12.359864059408501], [4.162779327243471, 12.359864059408501], [4.162779327243471, 11.089639205898733]], "type": "Polygon2D"}, {"vertices": [[2.5707318251360949, 16.182839205897242], [4.1563088511159085, 17.731439205898727], [4.162779327243471, 17.731439205898727], [4.162779327243471, 16.170538619937794], [2.5707318251360949, 16.170538619937794]], "type": "Polygon2D"}, {"vertices": [[1.0719203938455204, 9.5359802725037461], [1.0719203938455204, 9.5424507486313086], [2.6205203938470025, 11.052357301389966], [4.1691203938484831, 9.5424507486313086], [4.1691203938484831, 9.5359802725037461]], "type": "Polygon2D"}, {"vertices": [[2.5707318251360918, 11.078239205900211], [2.5707318251360909, 11.089639205898733], [4.162779327243471, 11.089639205898733], [4.162779327243471, 9.5296392058987323], [4.1563088511159085, 9.5296392058987323]], "type": "Polygon2D"}, {"vertices": [[1.0719203938455211, 17.718627663166149], [1.0719203938455211, 17.72509813929371], [4.169120393848484, 17.72509813929371], [4.169120393848484, 17.718627663166149], [2.6205203938470016, 16.208721110407488]], "type": "Polygon2D"}, {"vertices": [[1.0782614604505325, 16.170538619937805], [1.0782614604505325, 17.731439205898727], [1.0847319365780956, 17.731439205898727], [2.670308962557908, 16.182839205897242], [2.670308962557908, 16.170538619937805]], "type": "Polygon2D"}, {"vertices": [[1.0937614604505317, 0.13793920589873987], [1.0937614604505317, 1.4081640594085063], [2.6703089625579119, 1.4081640594085063], [2.6703089625579119, 0.13793920589873987]], "type": "Polygon2D"}, {"vertices": [[1.0937614604505317, 2.678388912918273], [1.0937614604505317, 3.9486137664280396], [2.6703089625579119, 3.9486137664280401], [2.6703089625579119, 2.678388912918273]], "type": "Polygon2D"}, {"vertices": [[1.0937614604505317, 1.408164059408507], [1.0937614604505317, 2.6783889129182734], [2.6703089625579119, 2.6783889129182734], [2.6703089625579119, 1.408164059408507]], "type": "Polygon2D"}, {"vertices": [[1.0937614604505317, 3.9486137664280405], [1.0937614604505317, 5.2188386199378067], [2.6703089625579119, 5.2188386199378067], [2.6703089625579119, 3.9486137664280401]], "type": "Polygon2D"}, {"vertices": [[1.0937614604505317, -1.4220607941012595], [1.0937614604505317, 0.13793920589873987], [2.6703089625579013, 0.13793920589873987], [2.6703089625579013, 0.11103920590021328], [1.1002319365780946, -1.4220607941012595]], "type": "Polygon2D"}, {"vertices": [[2.5707318251360749, 3.9486137664280339], [2.5707318251360749, 5.2188386199378005], [4.1472793272434547, 5.2188386199378005], [4.1472793272434547, 3.9486137664280339]], "type": "Polygon2D"}, {"vertices": [[2.5707318251360749, 1.4081640594085054], [2.5707318251360749, 2.6783889129182725], [4.1472793272434547, 2.6783889129182725], [4.1472793272434547, 1.4081640594085059]], "type": "Polygon2D"}, {"vertices": [[2.5707318251360749, 2.6783889129182716], [2.5707318251360749, 3.9486137664280383], [4.1472793272434547, 3.9486137664280383], [4.1472793272434547, 2.6783889129182716]], "type": "Polygon2D"}, {"vertices": [[2.5707318251360749, 0.13793920589873879], [2.5707318251360749, 1.4081640594085054], [4.1472793272434547, 1.4081640594085059], [4.1472793272434547, 0.13793920589873934]], "type": "Polygon2D"}, {"vertices": [[2.5707318251360873, 5.218838619937797], [2.5707318251360864, 5.2466392058972513], [4.1408088511158914, 6.7797392058987311], [4.1472793272434547, 6.7797392058987311], [4.1472793272434547, 5.218838619937797]], "type": "Polygon2D"}, {"vertices": [[1.0874203938455109, -1.4157197274962474], [1.0874203938455109, -1.4092492513686856], [2.6205203938469928, 0.085157301389967091], [4.1536203938484739, -1.4092492513686845], [4.153620393848473, -1.4157197274962474]], "type": "Polygon2D"}, {"vertices": [[2.570731825136074, 0.13793920589874095], [4.1472793272434547, 0.13793920589874109], [4.1472793272434547, -1.4220607941012595], [4.1408088511158914, -1.4220607941012595], [2.5707318251360749, 0.11103920590021545]], "type": "Polygon2D"}, {"vertices": [[1.0874203938455116, 6.7669276631661557], [1.0874203938455123, 6.773398139293719], [4.1536203938484748, 6.773398139293719], [4.1536203938484748, 6.7669276631661566], [2.6205203938469923, 5.2725211104075038]], "type": "Polygon2D"}, {"vertices": [[1.0937614604505317, 5.2188386199378085], [1.0937614604505317, 6.779739205898732], [1.1002319365780948, 6.779739205898732], [2.6703089625579057, 5.2466392058972549], [2.6703089625579057, 5.2188386199378067]], "type": "Polygon2D"}, {"vertices": [[1.0937614604504973, -10.86586079410127], [1.0937614604504973, -9.595635940591503], [2.6703089625578711, -9.595635940591503], [2.6703089625578702, -10.8617607940998], [2.6667582584010807, -10.86586079410127]], "type": "Polygon2D"}, {"vertices": [[1.0937614604504973, -8.3254110870817346], [1.0937614604504973, -7.0551862335719679], [2.6703089625578786, -7.0551862335719679], [2.6703089625578782, -8.3254110870817346]], "type": "Polygon2D"}, {"vertices": [[1.0937614604504973, -9.5956359405915013], [1.0937614604504973, -8.3254110870817346], [2.6703089625578786, -8.3254110870817346], [2.6703089625578782, -9.5956359405915013]], "type": "Polygon2D"}, {"vertices": [[1.0937614604504973, -7.0551862335719671], [1.0937614604504973, -5.7849613800622004], [2.6703089625578786, -5.7849613800622004], [2.6703089625578782, -7.0551862335719671]], "type": "Polygon2D"}, {"vertices": [[1.0937614604504973, -12.39486079410127], [1.0937614604504973, -10.86586079410127], [2.6667582584010816, -10.86586079410127], [1.1002319365780604, -12.394860794101271]], "type": "Polygon2D"}, {"vertices": [[2.5707318251360594, -7.0241862335719709], [2.5707318251360594, -5.7571607941027541], [2.5735025989723934, -5.7539613800622122], [4.1472793272434361, -5.7539613800622122], [4.1472793272434361, -7.0241862335719709]], "type": "Polygon2D"}, {"vertices": [[2.5707318251360554, -9.564635940591506], [2.5707318251360562, -8.2944110870817376], [4.1472793272434361, -8.2944110870817376], [4.1472793272434361, -9.5646359405915042]], "type": "Polygon2D"}, {"vertices": [[2.5707318251360554, -8.2944110870817376], [2.5707318251360562, -7.0241862335719718], [4.1472793272434361, -7.0241862335719718], [4.1472793272434361, -8.2944110870817376]], "type": "Polygon2D"}, {"vertices": [[2.5707318251360554, -10.834860794101273], [2.5707318251360562, -9.564635940591506], [4.1472793272434361, -9.564635940591506], [4.1472793272434361, -10.834860794101273]], "type": "Polygon2D"}, {"vertices": [[2.5735025989723934, -5.7539613800622122], [4.1408088511158736, -4.2240607941012769], [4.1472793272434361, -4.2240607941012769], [4.1472793272434361, -5.7539613800622122]], "type": "Polygon2D"}, {"vertices": [[1.0874203938454847, -12.388519727496259], [1.0874203938454847, -12.382049251368697], [2.620520393846967, -10.887642698610046], [4.1536203938484482, -12.382049251368697], [4.1536203938484473, -12.388519727496259]], "type": "Polygon2D"}, {"vertices": [[2.5707318251360549, -10.834860794101271], [4.1472793272434361, -10.834860794101271], [4.1472793272434361, -12.394860794101271], [4.1408088511158736, -12.394860794101271], [2.5707318251360554, -10.861760794099796]], "type": "Polygon2D"}, {"vertices": [[1.0874203938454856, -4.2368723368338514], [1.0874203938454863, -4.230401860706289], [4.1536203938484482, -4.2304018607062899], [4.1536203938484482, -4.2368723368338514], [2.6205203938469657, -5.7312788895925033]], "type": "Polygon2D"}, {"vertices": [[1.0937614604504973, -5.7849613800621995], [1.0937614604504973, -4.224060794101276], [1.1002319365780604, -4.224060794101276], [2.6703089625578733, -5.7571607941027541], [2.6703089625578724, -5.7849613800622004]], "type": "Polygon2D"}] \ No newline at end of file diff --git a/tests/polygon2d_test.py b/tests/polygon2d_test.py index d6276d82..6a21918d 100644 --- a/tests/polygon2d_test.py +++ b/tests/polygon2d_test.py @@ -691,6 +691,20 @@ def test_group_by_overlap(): assert len(grouped_polys[1]) in (2, 1) +def test_group_by_touching(): + """Test the group_by_touching method.""" + geo_file = './tests/json/overlapping_polygons.json' + with open(geo_file, 'r') as fp: + geo_dict = json.load(fp) + polygons = [Polygon2D.from_dict(p) for p in geo_dict] + + grouped_polys = Polygon2D.group_by_touching(polygons, 0.01) + assert len(grouped_polys) == 3 + + grouped_polys = Polygon2D.group_by_overlap(polygons, 0.01) + assert len(grouped_polys) == 13 + + def test_distance_to_point(): """Test the distance_to_point method.""" pts = (Point2D(0, 0), Point2D(4, 0), Point2D(4, 2), Point2D(2, 2),