14
14
GeometryCollection ,
15
15
LinearRing ,
16
16
LineString ,
17
+ MultiLineString ,
17
18
MultiPolygon ,
18
19
Point ,
19
20
Polygon ,
20
21
affinity ,
21
22
box ,
23
+ line_merge ,
24
+ shared_paths ,
22
25
)
23
26
from shapely .ops import split , unary_union
24
27
@@ -2310,34 +2313,18 @@ def offset_perimeter(
2310
2313
2311
2314
return CompoundGeometry (geoms = geoms_acc )
2312
2315
elif amount > 0 : # Ballooning condition
2313
- # This produces predictable results up to a point.
2314
- # That point is when the offset is so great it exceeds the thickness
2315
- # of the material at an interface of two materials.
2316
- # e.g. A 50 deep plate on top of the top flange of an I Section with a
2317
- # flange depth of 10
2318
- # When the offset exceeds 10 (the depth of the flange at the intersection),
2319
- # the meshed material regions will become unpredictable.
2320
- geoms_acc = []
2321
-
2322
- for i_idx , geom in enumerate (self .geoms ):
2323
- # Offset each geom...
2324
- offset_geom = geom .offset_perimeter (
2325
- amount = amount ,
2326
- where = where ,
2327
- resolution = resolution ,
2328
- )
2329
-
2330
- for j_idx , orig_geom in enumerate (self .geoms ):
2331
- if i_idx != j_idx :
2332
- # ... then remove the parts that intersect with the other
2333
- # constituents of the compound geometry (because they are
2334
- # occupying that space already)
2335
- offset_geom = offset_geom - orig_geom
2336
-
2337
- if not offset_geom .geom .is_empty :
2338
- geoms_acc .append (offset_geom )
2339
-
2340
- return CompoundGeometry (geoms = geoms_acc )
2316
+ # The algorithm used in the compound_dilation function cannot
2317
+ # currently handle re-entrant corners between different
2318
+ # geometries (a re-entrant corner in a single geometry is fine).
2319
+ # Re-entrant corners will require the creation of a new
2320
+ # "interface line" in the overlapping region created during
2321
+ # the dilation. I have a thought on how to do this but I just
2322
+ # have not gotten to it yet (note for later: it's like rotating
2323
+ # the overlap region between shear wall corners except cutting
2324
+ # across it from the shared vertex to the opposite vertex)
2325
+ # connorferster 2024-07-18
2326
+
2327
+ return compound_dilation (self .geoms , offset = amount )
2341
2328
else :
2342
2329
return self
2343
2330
@@ -2715,6 +2702,36 @@ def check_geometry_overlaps(
2715
2702
return not math .isclose (union_area , sum_polygons )
2716
2703
2717
2704
2705
+ def compound_dilation (geoms : list [Geometry ], offset : float ) -> CompoundGeometry :
2706
+ """Returns a CompoundGeometry representing the input Geometries, dilated.
2707
+
2708
+ Args:
2709
+ geoms: List of Geometry objects
2710
+ offset: A positive ``float`` or ``int``
2711
+
2712
+ Returns:
2713
+ The geometries dilated by ``offset``
2714
+ """
2715
+ polys = [geom .geom for geom in geoms ]
2716
+ geom_network = build_geometry_network (polys )
2717
+ acc = []
2718
+
2719
+ for poly_idx , connectivity in geom_network .items ():
2720
+ poly_orig = polys [poly_idx ]
2721
+ poly_orig_exterior = poly_orig .exterior
2722
+ connected_polys = [polys [idx ].exterior for idx in connectivity ]
2723
+ mucky_shared_paths = shared_paths (poly_orig_exterior , connected_polys )
2724
+ shared_path_geometries = MultiLineString (
2725
+ extract_shared_paths (mucky_shared_paths )
2726
+ )
2727
+ source = line_merge (poly_orig_exterior - shared_path_geometries )
2728
+ buff = source .buffer (offset , cap_style = "flat" )
2729
+ new = Geometry (poly_orig | buff , material = geoms [poly_idx ].material )
2730
+ acc .append (new )
2731
+
2732
+ return CompoundGeometry (acc )
2733
+
2734
+
2718
2735
def check_geometry_disjoint (
2719
2736
lop : list [Polygon ],
2720
2737
) -> bool :
@@ -2727,15 +2744,7 @@ def check_geometry_disjoint(
2727
2744
Whether or not there is disjoint geometry
2728
2745
"""
2729
2746
# Build polygon connectivity network
2730
- network : dict [int , set [int ]] = {}
2731
-
2732
- for idx_i , poly1 in enumerate (lop ):
2733
- for idx_j , poly2 in enumerate (lop ):
2734
- if idx_i != idx_j :
2735
- connectivity = network .get (idx_i , set ())
2736
- if poly1 .intersection (poly2 ):
2737
- connectivity .add (idx_j )
2738
- network [idx_i ] = connectivity
2747
+ network = build_geometry_network (lop )
2739
2748
2740
2749
def walk_network (
2741
2750
node : int ,
@@ -2767,3 +2776,55 @@ def walk_network(
2767
2776
nodes_visited = [0 ]
2768
2777
walk_network (0 , network , nodes_visited )
2769
2778
return set (nodes_visited ) != set (network .keys ())
2779
+
2780
+
2781
+ def build_geometry_network (lop : list [Polygon ]) -> dict [int , set [int ]]:
2782
+ """Builds a geometry connectivity graph.
2783
+
2784
+ Returns a graph describing the connectivity of each polygon to each other polygon in
2785
+ ``lop``. The keys are the indexes of the polygons in ``lop`` and the values are a
2786
+ set of indexes that the key is connected to.
2787
+
2788
+ Args:
2789
+ lop: List of Polygon
2790
+
2791
+ Returns:
2792
+ A dictionary describing the connectivity graph of the polygons
2793
+ """
2794
+ network : dict [int , set [int ]] = {}
2795
+
2796
+ for idx_i , poly1 in enumerate (lop ):
2797
+ for idx_j , poly2 in enumerate (lop ):
2798
+ if idx_i != idx_j :
2799
+ connectivity = network .get (idx_i , set ())
2800
+
2801
+ if poly1 .intersection (poly2 ):
2802
+ connectivity .add (idx_j )
2803
+
2804
+ network [idx_i ] = connectivity
2805
+
2806
+ return network
2807
+
2808
+
2809
+ def extract_shared_paths (
2810
+ arr_of_geom_coll : npt .ArrayLike ,
2811
+ ) -> list [LineString ]:
2812
+ """Extracts a list of LineStrings exported by the shapely ``shared_paths`` method.
2813
+
2814
+ Args:
2815
+ arr_of_geom_coll: An array of geometry collections
2816
+
2817
+ Returns:
2818
+ List of LineStrings.
2819
+ """
2820
+ acc = []
2821
+
2822
+ for geom_col in arr_of_geom_coll : # type: ignore
2823
+ for mls in geom_col .geoms : # type: ignore
2824
+ if mls .is_empty :
2825
+ continue
2826
+
2827
+ ls = line_merge (mls )
2828
+ acc .append (ls )
2829
+
2830
+ return acc
0 commit comments