Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 34 additions & 17 deletions src/compas_tna/diagrams/diagram_circular.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
-------
Expand Down Expand Up @@ -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
Expand All @@ -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
-------
Expand All @@ -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):
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
36 changes: 26 additions & 10 deletions src/compas_tna/diagrams/formdiagram.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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`
Expand All @@ -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.

Expand All @@ -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
-------
Expand All @@ -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")
Expand Down
8 changes: 3 additions & 5 deletions src/compas_tna/envelope/dome.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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)

Expand Down Expand Up @@ -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:
Expand Down
34 changes: 24 additions & 10 deletions src/compas_tna/envelope/parametricenvelope.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"))
Expand All @@ -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.")
Expand Down
Loading