Skip to content

Commit c2e6731

Browse files
Merge pull request #134 from StructuralPython/features/scale_conversions
Features/scale conversions
2 parents cc8e8a8 + 3512d8c commit c2e6731

9 files changed

Lines changed: 577 additions & 708 deletions

File tree

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,5 @@ graphviz = [
3939
dev = [
4040
"black>=25.1.0",
4141
"ipykernel>=6.30.1",
42-
"pygraphviz>=1.14",
4342
"pytest-check>=2.5.4",
4443
]

src/papermodels/datatypes/element.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -284,16 +284,13 @@ def get_collector_extents(self, relative: bool = True) -> dict[str, tuple]:
284284
self.trib_area,
285285
extent_polygon=self.extent_polygon,
286286
)
287-
except (geom_ops.GeometryError, AssertionError, ValueError) as e:
288-
print(
289-
f"{GeometryCollection(ordered_support_geoms).intersection(self.geometry).wkt=}"
287+
except (geom_ops.GeometryError, AssertionError, ValueError, TypeError):
288+
support_intersections = f"{GeometryCollection(ordered_support_geoms).intersection(self.geometry).wkt=}"
289+
geometry = f"{self.geometry.wkt=}"
290+
tag = f"{self.tag=}"
291+
raise geom_ops.GeometryError(
292+
f"Debug information:{tag=}\n{geometry=}\n{support_intersections=}"
290293
)
291-
print(f"{self.geometry.wkt=}")
292-
# raise AssertionError(
293-
# f"No intersection within joist extents: {self.tag=}"
294-
# )
295-
print(f"{self.tag=}")
296-
raise e
297294
tagged_extents = {}
298295
for idx, extent in enumerate(extents):
299296
support_geom = ordered_support_geoms[idx]
@@ -730,7 +727,6 @@ def _get_transfer_loads(self, precision: int):
730727
transfer_type = intersection_above.other_reaction_type
731728
source_member = intersection_above.other_tag
732729
reaction_idx = intersection_above.other_index
733-
# print(transfer_type, source_member, reaction_idx)
734730
if reaction_idx is None:
735731
raise ValueError(
736732
"The .other_index attribute within the .intersections_above list"
@@ -1374,7 +1370,10 @@ def trim_cantilevers(element: Element, abs_tol: Optional[float] = 0.02):
13741370
start_point = cantilevers["A_intersection"]
13751371
if (cantilevers["B"] == 0.0) and (cantilevers["B"] != cantilevers["B_orig"]):
13761372
end_point = cantilevers["B_intersection"]
1377-
new_geometry = LineString([start_point, end_point]) # type: ignore
1373+
try:
1374+
new_geometry = LineString([start_point, end_point]) # type: ignore
1375+
except TypeError:
1376+
raise geom_ops.GeometryError(f"{element=}")
13781377
new_element.geometry = new_geometry
13791378
intersection_checks = [
13801379
new_geometry.intersects(ib.other_geometry)

src/papermodels/datatypes/geometry_graph.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22
from typing import Optional, TypeAlias, Callable
3+
from collections import Counter
34
from copy import deepcopy
45
from decimal import Decimal
56
import 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"

src/papermodels/datatypes/joist_models.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,11 @@ def generate_joist_geom(self, index: int):
621621
intersecting_supports, grid_size=1e-3
622622
)
623623
ordered_intersections = geom_ops.order_nodes_positive(support_locs)
624+
if len(ordered_intersections) < 2:
625+
return None
626+
raise geom_ops.GeometryError(
627+
f"Joist prototype {self.element.tag} is not intersecting correctly."
628+
)
624629
support_a_loc, support_b_loc = (
625630
ordered_intersections[0],
626631
ordered_intersections[-1],
@@ -634,12 +639,18 @@ def generate_joist_geom(self, index: int):
634639
end_a = support_a_loc = self._extents[0][0]
635640
end_b = support_b_loc = self._extents[-1][0]
636641
# stand-in values for so that the variable intersecting_supports exists
637-
intersecting_supports = [0, 1]
642+
intersecting_supports = [
643+
0,
644+
1,
645+
] # bug: These allow joists to exist beyond the edge of the support for start and end joists
638646
elif index == len(self.joist_locations) - 1:
639647
end_a = support_a_loc = self._extents[0][1]
640648
end_b = support_b_loc = self._extents[-1][1]
641649
# stand-in values for so that the variable intersecting_supports exists
642-
intersecting_supports = [0, 1]
650+
intersecting_supports = [
651+
0,
652+
1,
653+
] # bug: These allow joists to exist beyond the edge of the support for start and end joists
643654

644655
cant_a = self._cantilevers["A"]
645656
cant_b = self._cantilevers["B"]

src/papermodels/geometry/geom_ops.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -807,7 +807,9 @@ def get_joist_locations(
807807
if initial_offset:
808808
joist_locs.append(initial_offset)
809809
distance_remaining -= initial_offset
810-
while distance_remaining > spacing:
810+
while (
811+
distance_remaining > 1.5 * spacing
812+
): # Use 1.5*spacing instead of 1.0*spacing to prevent "sliver joists" at the end
811813
distance_remaining -= spacing
812814
joist_locs.append(distance - distance_remaining)
813815
else:

src/papermodels/paper/annotations.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ def parse_annotations(
103103
annot_attrs["geometry"] = annot_geom
104104
annot_attrs["page_label"] = annot.page
105105
annot_attrs["tag"] = existing_annot_tag
106+
if annot_geom is None:
107+
raise ValueError(f"{annot=}")
106108
for annot_key, annot_attr in annot_attributes.items():
107109
annot_attrs[annot_key] = str_to_int(
108110
annot_attr.split("<")[0]

src/papermodels/paper/pdf.py

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,25 @@ def pike_annotation_to_pm_annotation(
144144
annot_type = str(annot["/Subj"])
145145
else:
146146
return None
147-
if annot_type.lower() in ("polygon", "polyline", "circle", "ellipse"):
147+
if annot_type.lower() in ("polygon", "polyline"):
148148
vertices = tuple(annot.get("/Vertices", tuple()))
149+
elif annot_type.lower() in ("circle", "ellipse"):
150+
annot_type = "Polygon"
151+
rect = tuple(annot.get("/Rect", tuple()))
152+
x1, y1, x2, y2 = rect
153+
h = (x1 + x2) / 2
154+
k = (y1 + y2) / 2
155+
a = (x2 - x1) / 2
156+
b = (y2 - y1) / 2
157+
n = 16
158+
x_vertices = float(h) + float(a) * np.cos(np.linspace(0, 2 * np.pi, n))
159+
y_vertices = float(k) + float(b) * np.sin(np.linspace(0, 2 * np.pi, n))
160+
vertices = []
161+
for idx, x_vertex in enumerate(x_vertices):
162+
vertices.append(Decimal(x_vertex))
163+
y_vertex = y_vertices[idx]
164+
vertices.append(Decimal(y_vertex))
165+
149166
elif annot_type.lower() in (
150167
"rectangle",
151168
"square",
@@ -243,21 +260,29 @@ def parse_content_stream(stream: str) -> dict[str, list]:
243260
operators will only have one entry).
244261
"""
245262
commands = {}
246-
operand_with_operator = re.compile(r"[0-9.\s]+[a-zA-Z]+")
247-
operators = re.compile(f"[a-zA-Z]+")
248-
operands = re.compile(r"[\d.]+")
263+
operand_with_operator = re.compile(r"([\d+\s*|\d+\.\d+\s*]*)([A-Za-z]{1,2})\s*")
264+
operators_pattern = re.compile(r"[a-zA-Z]+")
265+
operands_pattern = re.compile(r"[\d.]+")
266+
floats_pattern = re.compile(r"^\d+\.\d+$")
267+
integers_pattern = re.compile(r"^\d+$")
249268

250269
matches = operand_with_operator.findall(stream)
251270
for match in matches:
252-
operator = operators.findall(match)[0]
253-
operand = operands.findall(match)
271+
# operator = operators.findall(match)[0]
272+
# operand = operands.findall(match)
273+
operands, operator = match
254274
numerical_operands = []
255-
for element in operand:
256-
if "." in element:
275+
operands_matches = operands_pattern.findall(operands)
276+
for element in operands_matches:
277+
element = element.strip()
278+
float_match = floats_pattern.search(element)
279+
integer_match = integers_pattern.search(element)
280+
if float_match is not None:
257281
elem = Decimal(element)
258-
else:
282+
numerical_operands.append(elem)
283+
elif integer_match is not None:
259284
elem = int(element)
260-
numerical_operands.append(elem)
285+
numerical_operands.append(elem)
261286
commands.update({operator: numerical_operands})
262287
return commands
263288

src/papermodels/scales.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,48 @@ def __repr__(self):
8787
SCALE_3_HALF_INCH = Scale(
8888
Decimal("1") / Decimal("72") * Decimal("2") / Decimal("3"), '3/2" = 1\'-0"'
8989
)
90+
91+
SCALE_32ND_INCH_M = Scale(
92+
Decimal("1") / Decimal("72") * Decimal("32") * Decimal("0.3048"),
93+
'1/32" = 1\'-0" IN METERS',
94+
)
95+
SCALE_16TH_INCH_M = Scale(
96+
Decimal("1") / Decimal("72") * Decimal("16") * Decimal("0.3048"),
97+
'1/16" = 1\'-0" IN METERS',
98+
)
99+
SCALE_EIGHTH_INCH_M = Scale(
100+
Decimal("1") / Decimal("72") * Decimal("8") * Decimal("0.3048"),
101+
'1/8" = 1\'-0" IN METERS',
102+
)
103+
SCALE_QUARTER_INCH_M = Scale(
104+
Decimal("1") / Decimal("72") * Decimal("4") * Decimal("0.3048"),
105+
'1/4" = 1\'-0" IN METERS',
106+
)
107+
SCALE_HALF_INCH_M = Scale(
108+
Decimal("1") / Decimal("72") * Decimal("2") * Decimal("0.3048"),
109+
'1/2" = 1\'-0" IN METERS',
110+
)
111+
SCALE_ONE_INCH_M = Scale(
112+
Decimal("1") / Decimal("72") * Decimal("0.3048"), '1" = 1\'-0"'
113+
)
114+
115+
SCALE_3_32ND_INCH_M = Scale(
116+
Decimal("1") / Decimal("72") * Decimal("32") / Decimal("3") * Decimal("0.3048"),
117+
'3/32" = 1\'-0" IN METERS',
118+
)
119+
SCALE_3_16TH_INCH_M = Scale(
120+
Decimal("1") / Decimal("72") * Decimal("16") / Decimal("3") * Decimal("0.3048"),
121+
'3/16" = 1\'-0" IN METERS',
122+
)
123+
SCALE_3_EIGHTH_INCH_M = Scale(
124+
Decimal("1") / Decimal("72") * Decimal("8") / Decimal("3") * Decimal("0.3048"),
125+
'3/8" = 1\'-0" IN METERS',
126+
)
127+
SCALE_3_QUARTER_INCH_M = Scale(
128+
Decimal("1") / Decimal("72") * Decimal("4") / Decimal("3") * Decimal("0.3048"),
129+
'3/4" = 1\'-0" IN METERS',
130+
)
131+
SCALE_3_HALF_INCH_M = Scale(
132+
Decimal("1") / Decimal("72") * Decimal("2") / Decimal("3") * Decimal("0.3048"),
133+
'3/2" = 1\'-0" IN METERS',
134+
)

0 commit comments

Comments
 (0)