diff --git a/structuralcodes/geometry/_geometry.py b/structuralcodes/geometry/_geometry.py index d5eb3455..4b55aa49 100644 --- a/structuralcodes/geometry/_geometry.py +++ b/structuralcodes/geometry/_geometry.py @@ -824,21 +824,17 @@ def __init__( geometries, materials ) self.point_geometries = [] - # useful for representation in svg - geoms_representation = [g.polygon for g in self.geometries] - self.geom = MultiPolygon(geoms_representation) + # self.geom (svg representation) is built lazily, see the geom + # property. + self._geom = None return if isinstance(geometries, list): self.geometries, self.point_geometries = _process_geometries_list( geometries ) - # useful for representation in svg - geoms_representation = [g.polygon for g in self.geometries] - geoms_representation += [ - pg.point.buffer(pg.diameter / 2) - for pg in self.point_geometries - ] - self.geom = MultiPolygon(geoms_representation) + # self.geom (svg representation) is built lazily, see the geom + # property. + self._geom = None self._reinforced_concrete = None # we can add here static methods like @@ -846,6 +842,25 @@ def __init__( # from_ascii # ... + @property + def geom(self) -> MultiPolygon: + """MultiPolygon used for the SVG representation, built lazily. + + Point geometries are represented as buffered circles. This is only + needed for display, so it is computed on first access rather than in + __init__ -- building it eagerly buffers every point geometry on every + construction (including every rotate()), which dominated the section + calculations. + """ + if self._geom is None: + geoms_representation = [g.polygon for g in self.geometries] + geoms_representation += [ + pg.point.buffer(pg.diameter / 2) + for pg in self.point_geometries + ] + self._geom = MultiPolygon(geoms_representation) + return self._geom + def _repr_svg_(self) -> str: """Returns the svg representation.""" return str(self.geom._repr_svg_()) diff --git a/tests/test_geometry/test_geometry.py b/tests/test_geometry/test_geometry.py index f38c711b..aca03519 100644 --- a/tests/test_geometry/test_geometry.py +++ b/tests/test_geometry/test_geometry.py @@ -359,6 +359,42 @@ def test_compound_geometry(): ) +def test_compound_geometry_lazy_geom(): + """The svg ``geom`` MultiPolygon is built lazily and cached. + + ``geom`` is only needed for the SVG representation, so it is computed on + first access rather than in ``__init__``. It must still contain one polygon + per surface geometry plus a buffered circle per point geometry, and a + transformed copy must recompute its own ``geom``. + """ + C25 = ConcreteMC2010(25) + steel = ReinforcementMC2010(fyk=450, Es=210000, ftk=450, epsuk=0.03) + + poly = Polygon(((0, 0), (200, 0), (200, 400), (0, 400))) + geo = SurfaceGeometry(poly, C25) + geo = add_reinforcement_line(geo, (40, 40), (160, 40), 20, steel, n=4) + assert len(geo.geometries) == 1 + assert len(geo.point_geometries) == 4 + + # geom is not built until first access + assert geo._geom is None + + # one polygon per surface geometry + one buffered circle per point geometry + n_expected = len(geo.geometries) + len(geo.point_geometries) + assert isinstance(geo.geom, MultiPolygon) + assert len(geo.geom.geoms) == n_expected + + # the result is cached: repeated access returns the same object + assert geo._geom is not None + assert geo.geom is geo.geom + + # a transformed copy starts unbuilt and recomputes its own geom + geo_r = geo.rotate(np.pi / 2) + assert geo_r._geom is None + assert isinstance(geo_r.geom, MultiPolygon) + assert len(geo_r.geom.geoms) == n_expected + + def test_add_geometries(): """Test addition for different geometries.""" polys = []