Skip to content

Commit

Permalink
ADD BeamTProfileElement
Browse files Browse the repository at this point in the history
  • Loading branch information
petrasvestartas committed Jan 12, 2025
1 parent 615004c commit 6ec43c3
Show file tree
Hide file tree
Showing 4 changed files with 261 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Added `compas_model.elements.ScrewElement`.
* Added `Element.is_dirty`.
* Added `compas_model.models.BlockModel.from_barrel_vault`.
* Added `compas_model.elements.BeamTProfileElement`.


### Changed
Expand Down
19 changes: 19 additions & 0 deletions docs/examples/elements/beam.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from compas_model.elements import BeamTProfileElement
from compas_viewer import Viewer
from compas_viewer.config import Config

scale = 1
beam : BeamTProfileElement = BeamTProfileElement(width=0.2*scale, height=0.3*scale, step_height_left=0.1*scale, step_height_right=0.1*scale, step_width_left=0.05*scale, step_width_right=0.05*scale, length=6*scale)


config = Config()

config.camera.target = [0, 0.1*scale, 0]
config.camera.position = [0, -0.2*scale, 7*scale]
config.camera.near = 0.1*scale
config.camera.far = 10*scale
viewer = Viewer(config=config)
viewer.scene.add(beam.elementgeometry)

viewer.show()

2 changes: 2 additions & 0 deletions src/compas_model/elements/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .beam import BeamElement
from .beam import BeamIProfileElement
from .beam import BeamSquareElement
from .beam import BeamTProfileElement
from .column import ColumnFeature
from .column import ColumnElement
from .column import ColumnRoundElement
Expand All @@ -34,6 +35,7 @@
BeamElement,
BeamIProfileElement,
BeamSquareElement,
BeamTProfileElement,
ColumnFeature,
ColumnElement,
ColumnRoundElement,
Expand Down
239 changes: 239 additions & 0 deletions src/compas_model/elements/beam.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from compas.geometry import Transformation
from compas.geometry import bounding_box
from compas.geometry import intersection_line_plane
from compas.geometry import mirror_points_line
from compas.geometry import oriented_bounding_box
from compas.itertools import pairwise

Expand Down Expand Up @@ -405,6 +406,244 @@ def compute_elementgeometry(self) -> Mesh:
# Implementations of abstract methods
# =============================================================================

def compute_aabb(self, inflate: float = 0.0) -> Box:
"""Compute the axis-aligned bounding box of the element.
Parameters
----------
inflate : float, optional
The inflation factor of the bounding box.
Returns
-------
:class:`compas.geometry.Box`
The axis-aligned bounding box.
"""
points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore
box: Box = Box.from_bounding_box(bounding_box(points))
box.xsize += inflate
box.ysize += inflate
box.zsize += inflate
return box

def compute_obb(self, inflate: float = 0.0) -> Box:
"""Compute the oriented bounding box of the element.
Parameters
----------
inflate : float, optional
The inflation factor of the bounding box.
Returns
-------
:class:`compas.geometry.Box`
The oriented bounding box.
"""
points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore
box: Box = Box.from_bounding_box(oriented_bounding_box(points))
box.xsize += inflate
box.ysize += inflate
box.zsize += inflate
return box

def compute_collision_mesh(self) -> Mesh:
"""Compute the collision mesh of the element.
Returns
-------
:class:`compas.datastructures.Mesh`
The collision mesh.
"""
from compas.geometry import convex_hull_numpy

points: list[list[float]] = self.geometry.vertices_attributes("xyz") # type: ignore
vertices, faces = convex_hull_numpy(points)
vertices = [points[index] for index in vertices] # type: ignore
return Mesh.from_vertices_and_faces(vertices, faces)

# =============================================================================
# Constructors
# ============================================================================


class BeamTProfileElement(BeamElement):
"""Class representing a beam element with I profile.
Parameters
----------
width : float, optional
The width of the beam.
height : float, optional
The height of the beam.
step_width_left : float, optional
The step width on the left side of the beam.
step_width_right : float, optional
The step width on the right side of the beam, if None then the left side step width is used.
step_height_left : float, optional
The step height on the left side of the beam.
step_height_right : float, optional
The step height on the right side of the beam, if None then the left side step height is used.
length : float, optional
The length of the beam.
frame_bottom : :class:`compas.geometry.Plane`, optional
The frame of the bottom polygon.
frame_top : :class:`compas.geometry.Plane`, optional
The frame of the top polygon.
name : str, optional
The name of the element.
Attributes
----------
axis : :class:`compas.geometry.Line`
The axis of the beam.
section : :class:`compas.geometry.Polygon`
The section of the beam.
polygon_bottom : :class:`compas.geometry.Polygon`
The bottom polygon of the beam.
polygon_top : :class:`compas.geometry.Polygon`
The top polygon of the beam.
transformation : :class:`compas.geometry.Transformation`
The transformation applied to the beam.
material : :class:`compas_model.Material`
The material of the beam.
"""

@property
def __data__(self) -> dict:
return {
"width": self.width,
"height": self.height,
"step_width_left": self.step_width_left,
"step_width_right": self.step_width_right,
"step_height_left": self.step_height_left,
"step_height_right": self.step_height_right,
"length": self.length,
"inverted": self.inverted,
"frame_top": self.frame_top,
"is_support": self.is_support,
"frame": self.frame,
"transformation": self.transformation,
"features": self._features,
"name": self.name,
}

def __init__(
self,
width: float = 0.1,
height: float = 0.2,
step_width_left: float = 0.02,
step_width_right: Optional[float] = None,
step_height_left: float = 0.02,
step_height_right: Optional[float] = None,
length: float = 3.0,
inverted: bool = False,
frame_top: Optional[Plane] = None,
is_support: bool = False,
frame: Frame = Frame.worldXY(),
transformation: Optional[Transformation] = None,
features: Optional[list[BeamFeature]] = None,
name: Optional[str] = None,
) -> "BeamIProfileElement":
super().__init__(frame=frame, transformation=transformation, features=features, name=name)

self.is_support: bool = is_support

self.width: float = width
self.height: float = height
self.step_width_left: float = step_width_left
self.step_width_right: float = step_width_right if step_width_right is not None else step_width_left
self.step_height_left: float = step_height_left
self.step_height_right: float = step_height_right if step_height_right is not None else step_height_left
self.inverted: bool = inverted
self._length: float = length

self.points: list[float] = [
[self.width * 0.5, -self.height * 0.5, 0],
[-self.width * 0.5, -self.height * 0.5, 0],
[-self.width * 0.5, -self.height * 0.5 + self.step_height_left, 0],
[-self.width * 0.5 + self.step_width_left, -self.height * 0.5 + self.step_height_left, 0],
[-self.width * 0.5 + self.step_width_left, self.height * 0.5, 0],
[self.width * 0.5 - self.step_width_right, self.height * 0.5, 0],
[self.width * 0.5 - self.step_width_right, -self.height * 0.5 + self.step_height_right, 0],
[self.width * 0.5, -self.height * 0.5 + self.step_height_right, 0],
]

if inverted:
mirror_line: Line = Line([0, 0, 0], [1, 0, 0])
self.points = mirror_points_line(self.points, mirror_line)

# Create the polygon of the T profile
self.section: Polygon = Polygon(list(self.points)).translated([0, 0, 0.5 * length])

self.axis: Line = Line([0, 0, 0], [0, 0, length]).translated([0, 0, 0.5 * length])
self.frame_top: Frame = frame_top or Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis)
self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons()

@property
def length(self) -> float:
return self._length

@length.setter
def length(self, length: float):
self._length = length

# Create the polygon of the I profile
self.section = Polygon(list(self.points)).translated([0, 0, 0.5 * length])

self.axis = Line([0, 0, 0], [0, 0, length]).translated([0, 0, 0.5 * length])
self.frame_top = Frame(self.frame.point + self.axis.vector, self.frame.xaxis, self.frame.yaxis)
self.polygon_bottom, self.polygon_top = self.compute_top_and_bottom_polygons()

@property
def face_polygons(self) -> list[Polygon]:
return [self.geometry.face_polygon(face) for face in self.geometry.faces()] # type: ignore

def compute_top_and_bottom_polygons(self) -> tuple[Polygon, Polygon]:
"""Compute the top and bottom polygons of the beam.
Returns
-------
tuple[:class:`compas.geometry.Polygon`, :class:`compas.geometry.Polygon`]
"""

plane0: Plane = Plane.from_frame(self.frame)
plane1: Plane = Plane.from_frame(self.frame_top)
points0: list[list[float]] = []
points1: list[list[float]] = []
for i in range(len(self.section.points)):
line: Line = Line(self.section.points[i], self.section.points[i] + self.axis.vector)
result0: Optional[list[float]] = intersection_line_plane(line, plane0)
result1: Optional[list[float]] = intersection_line_plane(line, plane1)
if not result0 or not result1:
raise ValueError("The line does not intersect the plane")
points0.append(result0)
points1.append(result1)
return Polygon(points0), Polygon(points1)

def compute_elementgeometry(self) -> Mesh:
"""Compute the shape of the beam from the given polygons .
This shape is relative to the frame of the element.
Returns
-------
:class:`compas.datastructures.Mesh`
"""

offset: int = len(self.polygon_bottom)
vertices: list[Point] = self.polygon_bottom.points + self.polygon_top.points # type: ignore
bottom: list[int] = list(range(offset))
top: list[int] = [i + offset for i in bottom]
faces: list[list[int]] = [bottom[::-1], top]
for (a, b), (c, d) in zip(pairwise(bottom + bottom[:1]), pairwise(top + top[:1])):
faces.append([a, b, d, c])
mesh: Mesh = Mesh.from_vertices_and_faces(vertices, faces)
return mesh

# =============================================================================
# Implementations of abstract methods
# =============================================================================

def compute_aabb(self, inflate: float = 0.0) -> Box:
"""Compute the axis-aligned bounding box of the element.
Expand Down

0 comments on commit 6ec43c3

Please sign in to comment.