diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b64eece..fd116bdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,7 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added corner detection functionality: * `corner_vertices()` - Identifies corner vertices on boundary with configurable angle threshold * Added comprehensive mesh creation functions in `diagram_rectangular.py`: -* Added circular mesh creation functions in `diagram_circular.py`: +* Added circular mesh creation functions in `diagram_circular.py` and corrected oculus and diagonal properties. * Added arch mesh creation functions in `diagram_arch.py`: ### Changed diff --git a/src/compas_tna/diagrams/diagram_circular.py b/src/compas_tna/diagrams/diagram_circular.py index 2564edf8..cd304a5a 100644 --- a/src/compas_tna/diagrams/diagram_circular.py +++ b/src/compas_tna/diagrams/diagram_circular.py @@ -4,7 +4,7 @@ from compas.geometry import intersection_line_line_xy -def create_circular_radial_mesh(center=(5.0, 5.0), radius=5.0, n_hoops=8, n_parallels=20, r_oculus=0.0, diagonal=False, partial_diagonal=False) -> Mesh: +def create_circular_radial_mesh(center=(5.0, 5.0), radius=5.0, n_hoops=8, n_parallels=20, r_oculus=0.0, diagonal=False, diagonal_type="split") -> Mesh: """Construct a circular radial FormDiagram with hoops equally spaced in plan. Parameters @@ -21,8 +21,12 @@ def create_circular_radial_mesh(center=(5.0, 5.0), radius=5.0, n_hoops=8, n_para Value of the radius of the oculus, if no oculus is present should be set to zero, by default 0.0 diagonal : bool, optional Activate diagonal in the quads, by default False - partial_diagonal : bool, optional - Activate partial diagonal in the quads, by default False + diagonal_type : str, optional + Control how diagonals are placed in the quads Options are ["split", "straight", "right", "left"] + Default is "split", when the X diagonals will be split at their intersection. + If "straight", both quad diagonals are added as straight lines. + If "right", the diagonals will point to the right (x positive) of the diagram. + If "left", the diagonals will point to the left (x negative) of the diagram. Returns ------- @@ -73,38 +77,40 @@ def create_circular_radial_mesh(center=(5.0, 5.0), radius=5.0, n_hoops=8, n_para ya_ = yc + (r_oculus + (nr + 1) * r_div) * math.sin(theta * nc) yb_ = yc + (r_oculus + (nr + 1) * r_div) * math.sin(theta * (nc + 1)) - if partial_diagonal == "right": + if diagonal_type == "right": if nc + 1 > n_parallels / 2: lines.append([[xa, ya, 0.0], [xb_, yb_, 0.0]]) else: lines.append([[xa_, ya_, 0.0], [xb, yb, 0.0]]) - elif partial_diagonal == "left": + elif diagonal_type == "left": if nc + 1 > n_parallels / 2: lines.append([[xa_, ya_, 0.0], [xb, yb, 0.0]]) else: lines.append([[xa, ya, 0.0], [xb_, yb_, 0.0]]) - elif partial_diagonal == "rotation": - lines.append([[xa, ya, 0.0], [xb_, yb_, 0.0]]) - elif partial_diagonal == "straight": + elif diagonal_type == "straight": midx, midy, _ = intersection_line_line_xy([[xa, ya], [xb_, yb_]], [[xa_, ya_], [xb, yb]]) # type: ignore lines.append([[xa, ya, 0.0], [midx, midy, 0.0]]) lines.append([[midx, midy, 0.0], [xb_, yb_, 0.0]]) lines.append([[xa_, ya_, 0.0], [midx, midy, 0.0]]) lines.append([[midx, midy, 0.0], [xb, yb, 0.0]]) - else: + elif diagonal_type == "split": midx = (xa + xa_ + xb + xb_) / 4 midy = (ya + ya_ + yb + yb_) / 4 lines.append([[xa, ya, 0.0], [midx, midy, 0.0]]) lines.append([[midx, midy, 0.0], [xb_, yb_, 0.0]]) lines.append([[xa_, ya_, 0.0], [midx, midy, 0.0]]) lines.append([[midx, midy, 0.0], [xb, yb, 0.0]]) + else: + raise ValueError(f"Invalid diagonal type: {diagonal_type}. Choose from ['split', 'straight', 'right', 'left']") mesh = Mesh.from_lines(lines, delete_boundary_face=True) + if r_oculus > 0.0: + mesh.delete_face(1) return mesh -def create_circular_radial_spaced_mesh(center=(5.0, 5.0), radius=5.0, n_hoops=8, n_parallels=20, r_oculus=0.0, diagonal=False, partial_diagonal=False) -> Mesh: +def create_circular_radial_spaced_mesh(center=(5.0, 5.0), radius=5.0, n_hoops=8, n_parallels=20, r_oculus=0.0, diagonal=False, diagonal_type="split") -> Mesh: """Construct a circular radial FormDiagram with hoops not equally spaced in plan, but equally spaced with regards to the projection on a hemisphere. Parameters @@ -120,9 +126,13 @@ def create_circular_radial_spaced_mesh(center=(5.0, 5.0), radius=5.0, n_hoops=8, r_oculus : float, optional Value of the radius of the oculus, if no oculus is present should be set to zero, by default 0.0 diagonal : bool, optional - Activate diagonal in the quads, by default False - partial_diagonal : bool, optional - Activate partial diagonal in the quads, by default False + Activate diagonal X diagonals in the quads. See diagonal_type for more details. + diagonal_type : str, optional + Control how diagonals are placed in the quads Options are ["split", "straight", "right", "left"] + Default is "split", when the X diagonals will be split at their intersection. + If "straight", both quad diagonals are added as straight lines. + If "right", the diagonals will point to the right (x positive) of the diagram. + If "left", the diagonals will point to the left (x negative) of the diagram. Returns ------- @@ -134,6 +144,7 @@ def create_circular_radial_spaced_mesh(center=(5.0, 5.0), radius=5.0, n_hoops=8, yc = center[1] theta = 2 * math.pi / n_parallels r_div = (radius - r_oculus) / n_hoops + radius = radius - r_oculus lines = [] for nr in range(n_hoops + 1): @@ -169,31 +180,35 @@ def create_circular_radial_spaced_mesh(center=(5.0, 5.0), radius=5.0, n_hoops=8, xb_ = xc + (r_oculus + radius * math.cos((n_hoops - (nr + 1)) / n_hoops * math.pi / 2)) * math.cos(theta * (nc + 1)) ya_ = yc + (r_oculus + radius * math.cos((n_hoops - (nr + 1)) / n_hoops * math.pi / 2)) * math.sin(theta * nc) yb_ = yc + (r_oculus + radius * math.cos((n_hoops - (nr + 1)) / n_hoops * math.pi / 2)) * math.sin(theta * (nc + 1)) - if partial_diagonal == "right": + if diagonal_type == "right": if nc + 1 > n_parallels / 2: lines.append([[xa, ya, 0.0], [xb_, yb_, 0.0]]) else: lines.append([[xa_, ya_, 0.0], [xb, yb, 0.0]]) - elif partial_diagonal == "left": + elif diagonal_type == "left": if nc + 1 > n_parallels / 2: lines.append([[xa_, ya_, 0.0], [xb, yb, 0.0]]) else: lines.append([[xa, ya, 0.0], [xb_, yb_, 0.0]]) - elif partial_diagonal == "straight": + elif diagonal_type == "straight": midx, midy, _ = intersection_line_line_xy([[xa, ya], [xb_, yb_]], [[xa_, ya_], [xb, yb]]) # type: ignore lines.append([[xa, ya, 0.0], [midx, midy, 0.0]]) lines.append([[midx, midy, 0.0], [xb_, yb_, 0.0]]) lines.append([[xa_, ya_, 0.0], [midx, midy, 0.0]]) lines.append([[midx, midy, 0.0], [xb, yb, 0.0]]) - else: + elif diagonal_type == "split": midx = (xa + xa_ + xb + xb_) / 4 midy = (ya + ya_ + yb + yb_) / 4 lines.append([[xa, ya, 0.0], [midx, midy, 0.0]]) lines.append([[midx, midy, 0.0], [xb_, yb_, 0.0]]) lines.append([[xa_, ya_, 0.0], [midx, midy, 0.0]]) lines.append([[midx, midy, 0.0], [xb, yb, 0.0]]) + else: + raise ValueError(f"Invalid diagonal type: {diagonal_type}. Choose from ['split', 'straight', 'right', 'left']") mesh = Mesh.from_lines(lines, delete_boundary_face=True) + if r_oculus > 0.0: + mesh.delete_face(1) return mesh @@ -276,5 +291,7 @@ def create_circular_spiral_mesh(center=(5.0, 5.0), radius=5.0, n_hoops=8, n_para lines.append([[xa, ya, 0.0], [xb, yb, 0.0]]) mesh = Mesh.from_lines(lines, delete_boundary_face=True) + if r_oculus > 0.0: + mesh.delete_face(1) return mesh diff --git a/src/compas_tna/diagrams/formdiagram.py b/src/compas_tna/diagrams/formdiagram.py index 7c7b0b6e..f18e64ef 100644 --- a/src/compas_tna/diagrams/formdiagram.py +++ b/src/compas_tna/diagrams/formdiagram.py @@ -119,7 +119,16 @@ def __str__(self): number of faces: {} vertex degree: {}/{} face degree: {}/{} -""".format(self.name, numv, nume, numf, vmin, vmax, fmin, fmax) +""".format( + self.name, + numv, + nume, + numf, + vmin, + vmax, + fmin, + fmax, + ) @classmethod def from_lines( @@ -331,7 +340,7 @@ def create_ortho(cls, x_span=(0.0, 10.0), y_span=(0.0, 10.0), nx=10, ny=10, supp return form @classmethod - def create_circular_radial(cls, center=(5.0, 5.0), radius=5.0, n_hoops=8, n_parallels=20, r_oculus=0.0, diagonal=False, partial_diagonal=False) -> "FormDiagram": + def create_circular_radial(cls, center=(5.0, 5.0), radius=5.0, n_hoops=8, n_parallels=20, r_oculus=0.0, diagonal=False, diagonal_type="split") -> "FormDiagram": """Construct a circular radial FormDiagram with hoops not equally spaced in plan. Parameters @@ -348,9 +357,12 @@ def create_circular_radial(cls, center=(5.0, 5.0), radius=5.0, n_hoops=8, n_para Value of the radius of the oculus, if no oculus is present should be set to zero, by default 0.0 diagonal : bool, optional Activate diagonal in the quads, by default False - partial_diagonal : bool, optional - Activate partial diagonal in the quads, by default False - + diagonal_type : str, optional + Control how diagonals are placed in the quads Options are ["split", "straight", "right", "left"] + Default is "split", when the X diagonals will be split at their intersection. + If "straight", both quad diagonals are added as straight lines. + If "right", the diagonals will point to the right (x positive) of the diagram. + If "left", the diagonals will point to the left (x negative) of the diagram. Returns ------- :class:`~compas_tna.diagrams.FormDiagram` @@ -362,14 +374,14 @@ def create_circular_radial(cls, center=(5.0, 5.0), radius=5.0, n_hoops=8, n_para """ mesh = create_circular_radial_mesh( - center=center, radius=radius, n_hoops=n_hoops, n_parallels=n_parallels, r_oculus=r_oculus, diagonal=diagonal, partial_diagonal=partial_diagonal + center=center, radius=radius, n_hoops=n_hoops, n_parallels=n_parallels, r_oculus=r_oculus, diagonal=diagonal, diagonal_type=diagonal_type ) form = cls.from_mesh(mesh) form.assign_support_type("all") return form @classmethod - def create_circular_radial_spaced(cls, center=(5.0, 5.0), radius=5.0, n_hoops=8, n_parallels=20, r_oculus=0.0, diagonal=False, partial_diagonal=False) -> "FormDiagram": + def create_circular_radial_spaced(cls, center=(5.0, 5.0), radius=5.0, n_hoops=8, n_parallels=20, r_oculus=0.0, diagonal=False, diagonal_type="split") -> "FormDiagram": """Construct a circular radial FormDiagram with hoops not equally spaced in plan, but equally spaced with regards to the projection on a hemisphere. @@ -387,8 +399,12 @@ def create_circular_radial_spaced(cls, center=(5.0, 5.0), radius=5.0, n_hoops=8, Value of the radius of the oculus, if no oculus is present should be set to zero, by default 0.0 diagonal : bool, optional Activate diagonal in the quads, by default False - partial_diagonal : bool, optional - Activate partial diagonal in the quads, by default False + diagonal_type : str, optional + Control how diagonals are placed in the quads Options are ["split", "straight", "right", "left"] + Default is "split", when the X diagonals will be split at their intersection. + If "straight", both quad diagonals are added as straight lines. + If "right", the diagonals will point to the right (x positive) of the diagram. + If "left", the diagonals will point to the left (x negative) of the diagram. Returns ------- @@ -401,7 +417,7 @@ def create_circular_radial_spaced(cls, center=(5.0, 5.0), radius=5.0, n_hoops=8, """ mesh = create_circular_radial_spaced_mesh( - center=center, radius=radius, n_hoops=n_hoops, n_parallels=n_parallels, r_oculus=r_oculus, diagonal=diagonal, partial_diagonal=partial_diagonal + center=center, radius=radius, n_hoops=n_hoops, n_parallels=n_parallels, r_oculus=r_oculus, diagonal=diagonal, diagonal_type=diagonal_type ) form = cls.from_mesh(mesh) form.assign_support_type("all") diff --git a/src/compas_tna/envelope/dome.py b/src/compas_tna/envelope/dome.py index 19ce1b0e..70e4c6d4 100644 --- a/src/compas_tna/envelope/dome.py +++ b/src/compas_tna/envelope/dome.py @@ -57,7 +57,7 @@ def dome_envelope( ) xyz0, faces_i = base_topology.to_vertices_and_faces() xi, yi, _ = array(xyz0).transpose() - zt = dome_middle(xi, yi, radius_current, min_lb, center=center) + zt = dome_middle(xi, yi, radius_current, center=center) xyzt = array([xi, yi, zt.flatten()]).transpose() if radius_current == radius: @@ -75,7 +75,7 @@ def dome_envelope( return intrados, extrados, middle -def dome_middle(x, y, radius, min_lb, center=(5.0, 5.0)): +def dome_middle(x, y, radius, center=(5.0, 5.0)): """Compute middle of the dome based on the parameters. Parameters @@ -86,8 +86,6 @@ def dome_middle(x, y, radius, min_lb, center=(5.0, 5.0)): y-coordinates of the points radius : float, optional The radius of the dome, by default 5.0 - min_lb : float - Parameter for lower bound in nodes in the boundary center : tuple, optional x, y coordinates of the center of the dome, by default (5.0, 5.0) @@ -333,7 +331,7 @@ def update_envelope(self): self.middle = middle def compute_middle(self, x, y): - return dome_middle(x, y, self.radius, self.min_lb, self.center) + return dome_middle(x, y, self.radius, self.center) def compute_bounds(self, x, y, thickness=None): if thickness is None: diff --git a/src/compas_tna/envelope/parametricenvelope.py b/src/compas_tna/envelope/parametricenvelope.py index 3faf93c7..e9bb790e 100644 --- a/src/compas_tna/envelope/parametricenvelope.py +++ b/src/compas_tna/envelope/parametricenvelope.py @@ -157,9 +157,6 @@ def apply_selfweight_to_formdiagram(self, formdiagram: FormDiagram, normalize=Tr def apply_bounds_to_formdiagram(self, formdiagram: FormDiagram) -> None: """Apply envelope bounds to a form diagram based on the intrados and extrados surfaces. - This method projects the form diagram onto both intrados and extrados surfaces - and assigns the heights to 'ub' (upper bound) and 'lb' (lower bound) properties. - Parameters ---------- formdiagram : FormDiagram @@ -181,8 +178,15 @@ def apply_bounds_to_formdiagram(self, formdiagram: FormDiagram) -> None: def apply_target_heights_to_formdiagram(self, formdiagram: FormDiagram) -> None: """Apply target heights to a form diagram based on the Envelope middle surface. - This method projects the form diagram onto the Envelope middle surface - and assigns the heights to 'target' property. This assignment can later be used to compute a bestfit optimization. + Parameters + ---------- + formdiagram : FormDiagram + The form diagram to apply target heights to. + + Returns + ------- + None + The FormDiagram is modified in place. """ xy = np.array(formdiagram.vertices_attributes("xy")) @@ -192,13 +196,23 @@ def apply_target_heights_to_formdiagram(self, formdiagram: FormDiagram) -> None: # TODO: Future Cached properties could be added here def apply_reaction_bounds_to_formdiagram(self, formdiagram: FormDiagram) -> None: - """Apply reaction bounds to a form diagram based on the Envelope middle surface. + """Apply reaction bounds the supports of the form diagram based on the Envelope. - This method projects the form diagram onto the Envelope middle surface - and assigns the heights to 'target' property. This assignment can later be used to compute a bestfit optimization. + Parameters + ---------- + formdiagram : FormDiagram + The form diagram to apply reaction bounds to. + + Returns + ------- + None + The FormDiagram is modified in place. """ - raise NotImplementedError("Implement apply_reaction_bounds_to_formdiagram for specific envelope type.") - ## TODO: Implement this + fixed: list[int] = formdiagram.vertices_where({"is_support": True}) + xy = np.array(formdiagram.vertices_attributes("xy")) + bound_react = self.compute_bound_react(xy[:, 0], xy[:, 1], self.thickness, fixed) + for i, key in enumerate(fixed): + formdiagram.vertex_attribute(key, "b", bound_react[i]) def compute_middle(self, x, y): raise NotImplementedError("Implement compute_middle for specific envelope type.")