11from __future__ import annotations
22from typing import Optional , TypeAlias , Callable
3+ from collections import Counter
34from copy import deepcopy
45from decimal import Decimal
56import pathlib
@@ -261,6 +262,11 @@ def remove_excess_correspondent_load_paths(self):
261262 frame element transferring load to the supporting frame element. The correct load
262263 path should be |FB0.1 -> FB0.2 -> column| instead of |FB0.1 -> column| with
263264 |FB0.1 -> FB0.2 -> column| also.
265+ 4. A Polygon node, with "linear" reaction type, that is intersecting with LineString
266+ elements that run perpendicular to it. This can occur if a beam is drawn to
267+ transfer out a wall from above but that same beam has other beams framing into
268+ it perpendicular. We do not want the wall to transfer out to these other beams
269+ at the intersection points.
264270
265271 Modifications to the implementation of this function can adjust how load paths are
266272 conceptually created. For example, to implement baloon framing, the second rule
@@ -382,9 +388,26 @@ def remove_excess_correspondent_load_paths(self):
382388 element .geometry .geom_type == "LineString"
383389 and intersection_points_in_polygon_below
384390 ):
385- for edge in intersection_points_in_polygon_below :
391+ inters_below_set = set (intersection_points_in_polygon_below )
392+ for edge in inters_below_set :
386393 self .remove_edge (* edge )
387394
395+ # Rule 4
396+ if (
397+ element .geometry .geom_type == "Polygon"
398+ and element .reaction_type == "linear"
399+ and "intersection" in edge_properties
400+ ):
401+ center_line = geom .get_rectangle_centerline (element .geometry )
402+ for idx , dep in enumerate (dependents ):
403+ dep_geom = self .nodes [dep ]["element" ].geometry
404+ if dep_geom .geom_type == "LineString" :
405+ is_roughly_parallel = geom .check_2d_linestring_parallel (
406+ center_line , dep_geom , tol = 0.01
407+ )
408+ if not is_roughly_parallel :
409+ self .remove_edge (element .tag , dep )
410+
388411 def add_intersection_indexes_below (self ):
389412 sorted_nodes = nx .topological_sort (self )
390413 orphaned_nodes = self .orphaned_elements
@@ -1036,6 +1059,7 @@ def from_annotations(
10361059 structural_element_entries = {}
10371060 parsed_annotations_acc = {}
10381061 raw_annotations_acc = {}
1062+ tag_checker = []
10391063 for annots_in_page in annots_by_page :
10401064 if scale is not None :
10411065 scaled_annots_in_page = scale_annotations (annots_in_page , scale )
@@ -1050,18 +1074,27 @@ def from_annotations(
10501074 parsed_annotations_acc = parsed_annotations | parsed_annotations_acc
10511075 raw_annotations_acc = raw_annotations | raw_annotations_acc
10521076 for annot , annot_attrs in parsed_annotations .items ():
1077+ tag = annot_attrs ["tag" ]
10531078 if "occupancy" in annot_attrs :
10541079 load_entries .update ({annot : annot_attrs })
10551080 elif "trib area" in annot_attrs .get ("type" , "" ).lower ():
10561081 trib_area_entries .update ({annot : annot_attrs })
10571082 elif "extent" in annot_attrs .get ("type" , "" ).lower ():
10581083 extent_entries .update ({annot : annot_attrs })
10591084 else :
1085+ tag_checker .append (tag )
10601086 structural_element_entries .update ({annot : annot_attrs })
10611087 structural_element_entries = correlate_extents (
10621088 structural_element_entries , extent_entries
10631089 )
1064-
1090+ tag_counter = Counter (tag_checker )
1091+ tag_counter .pop (None ) # Exclude None tags from the check
1092+ duplicate_tags = [tag for tag in tag_counter if tag_counter [tag ] > 1 ]
1093+ if duplicate_tags :
1094+ raise ValueError (
1095+ "Geometry graph could not be built because the following"
1096+ f" duplicate tags were found: { duplicate_tags } "
1097+ )
10651098 if not structural_element_entries and not legend_entries :
10661099 raise ValueError (
10671100 "No structural element entities were found.\n "
0 commit comments