diff --git a/src/papermodels/__init__.py b/src/papermodels/__init__.py index d160c50..7d57ef4 100644 --- a/src/papermodels/__init__.py +++ b/src/papermodels/__init__.py @@ -4,5 +4,10 @@ from . import datatypes from . import paper +from . import scales +from . import filters +from . import collectors + +from .datatypes.geometry_graph import GeometryGraph __version__ = "0.18.0" diff --git a/src/papermodels/collectors.py b/src/papermodels/collectors.py new file mode 100644 index 0000000..ce2f658 --- /dev/null +++ b/src/papermodels/collectors.py @@ -0,0 +1 @@ +from .datatypes.joist_models import JoistArrayModel, CollectorTribModel diff --git a/src/papermodels/datatypes/element.py b/src/papermodels/datatypes/element.py index 5d43e85..b7d2041 100644 --- a/src/papermodels/datatypes/element.py +++ b/src/papermodels/datatypes/element.py @@ -277,9 +277,14 @@ def get_collector_extents(self, relative: bool = True) -> dict[str, tuple]: for idx, poly_support_geom in enumerate(support_geoms): clean_support_geom = support_geoms[idx] cleaned_supports_map.update({clean_support_geom: poly_support_geom}) - ordered_support_geoms = geom_ops.sort_supports( - self.geometry, support_geoms - ) + try: + ordered_support_geoms = geom_ops.sort_supports( + self.geometry, support_geoms + ) + except (AssertionError, ValueError): + raise geom_ops.GeometryError( + f"Element {self.tag} appears to have only one support intersection." + ) try: extents = geom_ops.get_joist_extents( self.geometry, @@ -287,7 +292,7 @@ def get_collector_extents(self, relative: bool = True) -> dict[str, tuple]: self.trib_area, extent_polygon=self.extent_polygon, ) - except (AssertionError, ValueError) as e: + except (geom_ops.GeometryError, AssertionError, ValueError) as e: raise AssertionError( f"No intersection within joist extents: {self.tag=}" ) @@ -1311,9 +1316,14 @@ def trim_cantilevers(element: Element, abs_tol: Optional[float] = 0.02): [Point(geometry.coords[0]), Point(geometry.coords[-1])] ) ) - cantilevers = geom_ops.get_cantilever_segments( - ordered_geom, support_geoms, abs_tol=abs_tol - ) + try: + cantilevers = geom_ops.get_cantilever_segments( + ordered_geom, support_geoms, abs_tol=abs_tol + ) + except (AssertionError, NotImplementedError, ValueError): + raise geom_ops.GeometryError( + f"Received an unexpected geometry for {element.tag} during cantilever trimming." + ) # ordered_geom = LineString( # geom_ops.order_nodes_positive( # [Point(geometry.coords[0]), Point(geometry.coords[-1])] diff --git a/src/papermodels/datatypes/geometry_graph.py b/src/papermodels/datatypes/geometry_graph.py index 2979607..71b5b3a 100644 --- a/src/papermodels/datatypes/geometry_graph.py +++ b/src/papermodels/datatypes/geometry_graph.py @@ -861,6 +861,7 @@ def from_pdf_file( save_tagged_pdf_file: bool = False, tag_pdf_file_mode: str = "append", show_skipped: bool = False, + show_unimplemented: bool = False, ): """ Returns a GeometryGraph built from that annotations in the provided PDF file @@ -893,8 +894,12 @@ def from_pdf_file( 'process_gravity_frame': Processes the geometry for a gravity frame by fully resolving in-plane connectivity 'show_skipped': Shows the skipped annotations that occured during pdf.load_pdf_annotations + 'show_unimplemented': Shows the annotations that were read but are not implemented in the + parser yet. """ - annotations = pdf.load_pdf_annotations(pdf_filepath, show_skipped) + annotations = pdf.load_pdf_annotations( + pdf_filepath, show_skipped, show_unimplemented + ) graph = cls.from_annotations( annotations, legend_identifier, @@ -1097,6 +1102,7 @@ def plot_elements( plot_trib_areas: bool = False, plot_extent_polygons: bool = False, plot_tags: bool = False, + plot_elems_by_tag: Optional[list[str]] = None, ): """ Plots all elements in the graph that are on 'page_idx' @@ -1110,6 +1116,7 @@ def plot_elements( plot_trib_areas=plot_trib_areas, plot_extent_polygons=plot_extent_polygons, plot_tags=plot_tags, + plot_elems_by_tag=plot_elems_by_tag, ) def create_loaded_elements(self) -> dict[str, LoadedElement]: diff --git a/src/papermodels/datatypes/joist_models.py b/src/papermodels/datatypes/joist_models.py index c3708d4..2f7465d 100644 --- a/src/papermodels/datatypes/joist_models.py +++ b/src/papermodels/datatypes/joist_models.py @@ -435,9 +435,14 @@ def __init__( break self._supports = ordered_supports else: - self._supports = geom_ops.sort_supports( - self.joist_prototype, self.joist_supports.keys() - ) + try: + self._supports = geom_ops.sort_supports( + self.joist_prototype, self.joist_supports.keys() + ) + except (AssertionError, ValueError): + raise geom_ops.GeometryError( + f"Element {self.element.tag} appears to have only one support intersection:\n{self.element.intersections_below}" + ) self.joist_support_tags = [ib.other_tag for ib in element.intersections_below] self.id = element.tag @@ -676,6 +681,18 @@ def generate_joist_geom(self, index: int): end_b, self.vector_parallel, self._cantilever_tolerance / 10 ) joist_geom = LineString([end_a, end_b]) + # TODO: + # This will superficially break one of the tests. Do I want to add this in? + + # if not all([joist_geom.intersects(support) for support in self._supports]): + # print(self.element.tag) + # end_a = geom_ops.project_node( + # end_a, -self.vector_parallel, self._cantilever_tolerance / 10 + # ) + # end_b = geom_ops.project_node( + # end_b, self.vector_parallel, self._cantilever_tolerance / 10 + # ) + # joist_geom = LineString([end_a, end_b]) if joist_geom.length <= self._cantilever_tolerance: return None return joist_geom @@ -747,7 +764,8 @@ def generate_trib_area(self, index: int) -> Polygon: ) else: trib_area_right = Polygon() - return trib_area_left | trib_area_right + trib_area = trib_area_left | trib_area_right + return trib_area def show_svg(self, use_ipython_display: bool = True): """ diff --git a/src/papermodels/filters.py b/src/papermodels/filters.py new file mode 100644 index 0000000..8ed0191 --- /dev/null +++ b/src/papermodels/filters.py @@ -0,0 +1 @@ +from .datatypes.element import create_element_filter diff --git a/src/papermodels/geometry/geom_ops.py b/src/papermodels/geometry/geom_ops.py index ed139e3..7c77674 100644 --- a/src/papermodels/geometry/geom_ops.py +++ b/src/papermodels/geometry/geom_ops.py @@ -360,8 +360,16 @@ def get_joist_extents( right_coords = [] support_intersection = intersection_all(joist_supports) for joist_support in joist_supports: - joist_support = joist_support.intersection(box(*supports_bbox), grid_size=1e-3) - + joist_support_trim = joist_support.intersection( + box(*supports_bbox), grid_size=1e-3 + ) + if not joist_support_trim.geom_type == "LineString": + raise GeometryError( + f"It seems that a support does not fully intersect with the supports bounding box.\n" + "Redraw your supports for this element to ensure they are either properly orthogonal to the element " + "or are very clearly at an angle to the element." + ) + joist_support = joist_support_trim start_coord, end_coord = joist_support.coords start_coord, end_coord = Point(start_coord), Point(end_coord) @@ -750,7 +758,8 @@ def get_system_bounds( if overlap_poly is not None: if extent_polygon is not None: overlap_poly = extent_polygon.intersection(overlap_poly) - overlap_polys.append(overlap_poly) + if overlap_poly.geom_type == "Polygon": + overlap_polys.append(overlap_poly) return MultiPolygon(overlap_polys).bounds @@ -822,6 +831,8 @@ def sort_supports( """ all_supports = MultiLineString(supports) joist_intersections = joist_prototype.intersection(all_supports, grid_size=1e-3) + if joist_intersections.geom_type == "Point": + joist_intersections = all_supports.intersection(joist_prototype) assert joist_intersections.geom_type != "Point" assert not joist_intersections.is_empty ordered_intersections = order_nodes_positive(joist_intersections.geoms) diff --git a/src/papermodels/paper/pdf.py b/src/papermodels/paper/pdf.py index 57f6ca0..8f90225 100644 --- a/src/papermodels/paper/pdf.py +++ b/src/papermodels/paper/pdf.py @@ -13,7 +13,9 @@ def load_pdf_annotations( - pdf_path: pathlib.Path | str, show_skipped: bool = False + pdf_path: pathlib.Path | str, + show_skipped: bool = False, + show_unimplemented: bool = False, ) -> list[Annotation]: """ Returns a lists of pdf annotations keyed by page index. @@ -30,7 +32,11 @@ def load_pdf_annotations( continue for annot_idx, annot in enumerate(page_data.obj.Annots): pm_annot = pike_annotation_to_pm_annotation( - annot, annot_idx, page_num, rotate + annot, + annot_idx, + page_num, + rotate, + display_unimplemented_annots=show_unimplemented, ) if pm_annot is not None: annots_in_pdf.append(pm_annot) @@ -109,7 +115,11 @@ def compare_annotations( def pike_annotation_to_pm_annotation( - annot, annot_idx: int, page_idx: int, rotate: Optional[int] = None + annot, + annot_idx: int, + page_idx: int, + rotate: Optional[int] = None, + display_unimplemented_annots: bool = False, ) -> Optional[Annotation]: """ Returns either an Annotation object or None. None is returned if: @@ -159,7 +169,8 @@ def pike_annotation_to_pm_annotation( vertices = tuple(annot.get("/L", tuple())) annot_type = "Line" else: - print(f"Cannot read (yet): {annot_type}") + if display_unimplemented_annots: + print(f"Cannot read (yet): {annot_type}") return None if rotate == 90: vertex_array = geom_ops.vertices_to_array(vertices) diff --git a/src/papermodels/paper/plot.py b/src/papermodels/paper/plot.py index b143191..4e6006f 100644 --- a/src/papermodels/paper/plot.py +++ b/src/papermodels/paper/plot.py @@ -20,6 +20,7 @@ def plot_elements( plot_trib_areas: bool = False, plot_extent_polygons: bool = False, plot_tags: bool = False, + plot_elems_by_tag: Optional[list[str]] = None, ) -> Figure: """ Plots the elements in matplotlib. Size and dpi can be adjusted @@ -61,6 +62,7 @@ def plot_elements( "extent_face": (0.8, 0.7, 0.0), "prototype_line": "yellow", } + highlight_tags = False plot_objs = [] for element in elements: plot_attrs = get_element_plotting_attributes(element, is_subelement=True) @@ -82,9 +84,17 @@ def plot_elements( max_extent = np.maximum(max_extent, np.max(xy, axis=1)) # For tagging - initial_positions_x.append(po["anchor_point"][0]) - initial_positions_y.append(po["anchor_point"][1]) - tags.append(po["tag"]) + + if plot_elems_by_tag is None: + tags.append(po["tag"]) + initial_positions_x.append(po["anchor_point"][0]) + initial_positions_y.append(po["anchor_point"][1]) + else: + if po["tag"] in plot_elems_by_tag: + highlight_tags = True + tags.append(po["tag"]) + initial_positions_x.append(po["anchor_point"][0]) + initial_positions_y.append(po["anchor_point"][1]) if po["is_poly"]: ax.add_patch( @@ -161,6 +171,9 @@ def plot_elements( min_extent[1] - plot_margin_metric * 0.05, max_extent[1] + plot_margin_metric * 0.05, ) + text_color = "k" + if highlight_tags: + text_color = "r" ta.allocate( ax=ax, x=initial_positions_x, @@ -169,7 +182,7 @@ def plot_elements( # x_lines=lines_x, # y_lines=lines_y, textsize=8, - textcolor="k", + textcolor=text_color, linecolor="k", avoid_label_lines_overlap=True, avoid_crossing_label_lines=True, @@ -246,12 +259,15 @@ def plot_annotations( figsize: int | float | tuple[int | float, int | float] = (17, 11), dpi: float = 100, plot_tags: bool = False, + plot_annots_by_tag: Optional[list[str]] = None, ) -> Figure: """ Plots that annotations, 'annots' in matplotlib. Size and dpi can be adjusted to make the plot bigger/smaller. Size is in inches and dpi stands for "dots per inch". For a biggish plot, values of size=12, dpi=200 gives good results. + + """ if isinstance(figsize, (int, float)): figsize = (figsize, figsize) diff --git a/src/papermodels/scales.py b/src/papermodels/scales.py new file mode 100644 index 0000000..9d7a176 --- /dev/null +++ b/src/papermodels/scales.py @@ -0,0 +1,89 @@ +from decimal import Decimal + +Decimal() + + +class Scale(Decimal): + + def __new__(cls, factor, name): + instance = super().__new__(cls, factor) + instance.name = name + return instance + + def __repr__(self): + return f"Scale: {self.name} | factor: {self}" + + +SCALE_1_TO_1000 = Scale( + Decimal("1") / Decimal("72") * Decimal("25.4") * Decimal("1000"), "1:1000 Scale" +) +SCALE_1_TO_500 = Scale( + Decimal("1") / Decimal("72") * Decimal("25.4") * Decimal("500"), "1:500 Scale" +) +SCALE_1_TO_200 = Scale( + Decimal("1") / Decimal("72") * Decimal("25.4") * Decimal("200"), "1:200 Scale" +) +SCALE_1_TO_100 = Scale( + Decimal("1") / Decimal("72") * Decimal("25.4") * Decimal("100"), "1:100 Scale" +) +SCALE_1_TO_50 = Scale( + Decimal("1") / Decimal("72") * Decimal("25.4") * Decimal("50"), "1:50 Scale" +) +SCALE_1_TO_20 = Scale( + Decimal("1") / Decimal("72") * Decimal("25.4") * Decimal("20"), "1:20 Scale" +) +SCALE_1_TO_10 = Scale( + Decimal("1") / Decimal("72") * Decimal("25.4") * Decimal("10"), "1:10 Scale" +) + +SCALE_1_TO_1000_M = Scale( + Decimal("1") / Decimal("72") * Decimal("25.4") * Decimal("1000") / Decimal("1000"), + "1:1000 Scale, in meters", +) +SCALE_1_TO_500_M = Scale( + Decimal("1") / Decimal("72") * Decimal("25.4") * Decimal("500") / Decimal("1000"), + "1:500 Scale, in meters", +) +SCALE_1_TO_200_M = Scale( + Decimal("1") / Decimal("72") * Decimal("25.4") * Decimal("200") / Decimal("1000"), + "1:200 Scale, in meters", +) +SCALE_1_TO_100_M = Scale( + Decimal("1") / Decimal("72") * Decimal("25.4") * Decimal("100") / Decimal("1000"), + "1:100 Scale, in meters", +) +SCALE_1_TO_50_M = Scale( + Decimal("1") / Decimal("72") * Decimal("25.4") * Decimal("50") / Decimal("1000"), + "1:50 Scale, in meters", +) +SCALE_1_TO_20_M = Scale( + Decimal("1") / Decimal("72") * Decimal("25.4") * Decimal("20") / Decimal("1000"), + "1:20 Scale, in meters", +) +SCALE_1_TO_10_M = Scale( + Decimal("1") / Decimal("72") * Decimal("25.4") * Decimal("10") / Decimal("1000"), + "1:10 Scale, in meters", +) + +SCALE_32ND_INCH = Scale(Decimal("1") / Decimal("72") * Decimal("32"), '1/32" = 1\'-0"') +SCALE_16TH_INCH = Scale(Decimal("1") / Decimal("72") * Decimal("16"), '1/16" = 1\'-0"') +SCALE_EIGHTH_INCH = Scale(Decimal("1") / Decimal("72") * Decimal("8"), '1/8" = 1\'-0"') +SCALE_QUARTER_INCH = Scale(Decimal("1") / Decimal("72") * Decimal("4"), '1/4" = 1\'-0"') +SCALE_HALF_INCH = Scale(Decimal("1") / Decimal("72") * Decimal("2"), '1/2" = 1\'-0"') +SCALE_ONE_INCH = Scale(Decimal("1") / Decimal("72"), '1" = 1\'-0"') + +SCALE_3_32ND_INCH = Scale( + Decimal("1") / Decimal("72") * Decimal("32") / Decimal("3"), '3/32" = 1\'-0"' +) +SCALE_3_16TH_INCH = Scale( + Decimal("1") / Decimal("72") * Decimal("16") / Decimal("3"), '3/16" = 1\'-0"' +) +SCALE_3_EIGHTH_INCH = Scale( + Decimal("1") / Decimal("72") * Decimal("8") / Decimal("3"), '3/8" = 1\'-0"' +) +SCALE_3_QUARTER_INCH = Scale( + Decimal("1") / Decimal("72") * Decimal("4") / Decimal("3"), '3/4" = 1\'-0"' +) +SCALE_3_HALF_INCH = Scale( + Decimal("1") / Decimal("72") * Decimal("2") / Decimal("3"), '3/2" = 1\'-0"' +)