diff --git a/sectionproperties/pre/geometry.py b/sectionproperties/pre/geometry.py index 99318241..731a1c9d 100644 --- a/sectionproperties/pre/geometry.py +++ b/sectionproperties/pre/geometry.py @@ -1,6 +1,6 @@ from __future__ import annotations from ntpath import join -from typing import List, Optional, Union, Tuple, Any +from typing import List, Optional, Union, Tuple, List, Any import copy import math @@ -479,31 +479,47 @@ def align_to( return new_geom - def align_center(self, align_to: Optional[Geometry] = None): + def align_center( + self, align_to: Optional[Union[Geometry, Tuple[float, float]]] = None + ): """ Returns a new Geometry object, translated in both x and y, so that the - center-point of the new object's centroid will be aligned with - centroid of the object in 'align_to'. If 'align_to' is None then the new - object will be aligned with it's centroid at the origin. + the new object's centroid will be aligned with the centroid of the object + in 'align_to'. If 'align_to' is an x, y coordinate, then the centroid will + be aligned to the coordinate. If 'align_to' is None then the new + object will be aligned with its centroid at the origin. :param align_to: Another Geometry to align to or None (default is None) - :type align_to: Optional[:class:`~sectionproperties.pre.geometry.Geometry`] + :type align_to: Optional[Union[:class:`~sectionproperties.pre.geometry.Geometry`, Tuple[float, float]]] :return: Geometry object translated to new alignment :rtype: :class:`~sectionproperties.pre.geometry.Geometry` """ cx, cy = list(self.geom.centroid.coords)[0] - # Suggested by Agent 6-6-6: Hard-rounding of cx and cy allows + # Suggested by @Agent6-6-6: Hard-rounding of cx and cy allows # for greater precision in placing geometry with its centroid # near [0, 0]. True [0, 0] placement will not be possible due # to floating point errors. if align_to is None: shift_x, shift_y = round(-cx, self.tol), round(-cy, self.tol) - else: + elif isinstance(align_to, Geometry): align_cx, align_cy = list(align_to.geom.centroid.coords)[0] shift_x = round(align_cx - cx, self.tol) shift_y = round(align_cy - cy, self.tol) + + else: + try: + point_x, point_y = align_to + shift_x = round(point_x - cx, self.tol) + shift_y = round(point_y - cy, self.tol) + except ( + TypeError, + ValueError, + ): # align_to not subscriptable, incorrect length, etc. + raise ValueError( + f"align_to must be either a Geometry object or an x, y coordinate, not {align_to}." + ) new_geom = self.shift_section(x_offset=shift_x, y_offset=shift_y) return new_geom @@ -1555,20 +1571,23 @@ def mirror_section( new_geom = CompoundGeometry(geoms_acc) return new_geom - def align_center(self, align_to: Optional[Geometry] = None): + def align_center( + self, align_to: Optional[Union[Geometry, Tuple[float, float]]] = None + ): """ Returns a new CompoundGeometry object, translated in both x and y, so that the center-point of the new object's material-weighted centroid will be aligned with - centroid of the object in 'align_to'. If 'align_to' is None then the new - object will be aligned with it's centroid at the origin. + centroid of the object in 'align_to'. If 'align_to' is an x, y coordinate, then + the centroid will be aligned to the coordinate. If 'align_to' is None then the new + object will be aligned with its centroid at the origin. Note: The material-weighted centroid refers to when individual geometries within the CompoundGeometry object have been assigned differing materials. The centroid of the compound geometry is calculated by using the E modulus of each geometry's assigned material. - :param align_to: Another Geometry to align to or None (default is None) - :type align_to: Optional[:class:`~sectionproperties.pre.geometry.Geometry`] + :param align_to: Another Geometry to align to, an xy coordinate, or None (default is None) + :type align_to: Optional[Union[:class:`~sectionproperties.pre.geometry.Geometry`, Tuple[float, float]]] :return: Geometry object translated to new alignment :rtype: :class:`~sectionproperties.pre.geometry.Geometry` @@ -1597,10 +1616,21 @@ def align_center(self, align_to: Optional[Geometry] = None): round(-weighted_cy, self.tol), ) - else: + elif isinstance(Geometry): align_cx, align_cy = list(align_to.geom.centroid.coords)[0] shift_x = round(align_cx - weighted_cx, self.tol) shift_y = round(align_cy - weighted_cy, self.tol) + + else: + try: + point_x, point_y = align_to + shift_x = round(point_x - weighted_cx, self.tol) + shift_y = round(point_y - weighted_cy, self.tol) + except (TypeError, ValueError): + raise ValueError( + f"align_to must be either a Geometry object or an x, y coordinate, not {align_to}." + ) + new_geom = self.shift_section(x_offset=shift_x, y_offset=shift_y) return new_geom diff --git a/sectionproperties/tests/test_sections.py b/sectionproperties/tests/test_sections.py index 02ad843d..a61484d3 100644 --- a/sectionproperties/tests/test_sections.py +++ b/sectionproperties/tests/test_sections.py @@ -532,3 +532,24 @@ def test_warping_disjoint_warning(): sec.calculate_geometric_properties() with pytest.warns(UserWarning): sec.calculate_warping_properties() + + +def test_align_center(): + rect = rectangular_section(d=200, b=70) + circ = circular_section(d=200, n=30) + rect = rect.rotate_section(-45, rot_point=[0, 0]) + rect_point = rect.points[1] + circ = circ.align_center(rect_point) + circ_x, circ_y = circ.calculate_centroid() + assert pytest.approx(circ_x) == 49.497474683057995 + assert pytest.approx(circ_y) == -49.49747468305799 + + circ = circ.align_center() + circ_x, circ_y = circ.calculate_centroid() + assert pytest.approx(circ_x) == 0 + assert pytest.approx(circ_y) == 0 + + circ = circ.align_center(rect) + circ_x, circ_y = circ.calculate_centroid() + assert pytest.approx(circ_x) == 95.45941546018399 + assert pytest.approx(circ_y) == 45.961940777125974 diff --git a/setup.cfg b/setup.cfg index 202f108f..38440504 100644 --- a/setup.cfg +++ b/setup.cfg @@ -51,4 +51,5 @@ rhino = test = rhino-shapley-interop>=0.0.4 + pytest pytest_check