1414 GeometryCollection ,
1515 LinearRing ,
1616 LineString ,
17+ MultiLineString ,
1718 MultiPolygon ,
1819 Point ,
1920 Polygon ,
2021 affinity ,
2122 box ,
23+ line_merge ,
24+ shared_paths ,
2225)
2326from shapely .ops import split , unary_union
2427
@@ -2310,34 +2313,18 @@ def offset_perimeter(
23102313
23112314 return CompoundGeometry (geoms = geoms_acc )
23122315 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 )
23412328 else :
23422329 return self
23432330
@@ -2715,6 +2702,36 @@ def check_geometry_overlaps(
27152702 return not math .isclose (union_area , sum_polygons )
27162703
27172704
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+
27182735def check_geometry_disjoint (
27192736 lop : list [Polygon ],
27202737) -> bool :
@@ -2727,15 +2744,7 @@ def check_geometry_disjoint(
27272744 Whether or not there is disjoint geometry
27282745 """
27292746 # 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 )
27392748
27402749 def walk_network (
27412750 node : int ,
@@ -2767,3 +2776,55 @@ def walk_network(
27672776 nodes_visited = [0 ]
27682777 walk_network (0 , network , nodes_visited )
27692778 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