Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added generic fastener element #310

Merged
merged 21 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from 11 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: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Added new `utilities` module in `connections` package.
* Added new `compas_timber._fabrication.DoubleCut`.
* Added new `compas_timber.connections.TBirdsmouthJoint`.
* Added new `fasteners.py` module with new `Fastener` element type.
* Added unit tests for `fasteners.py` module.
chenkasirer marked this conversation as resolved.
Show resolved Hide resolved

### Changed

Expand Down
2 changes: 1 addition & 1 deletion src/compas_timber/_fabrication/step_joint.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def __init__(
tenon=False,
tenon_width=40.0,
tenon_height=40.0,
**kwargs
**kwargs,
obucklin marked this conversation as resolved.
Show resolved Hide resolved
chenkasirer marked this conversation as resolved.
Show resolved Hide resolved
):
super(StepJoint, self).__init__(**kwargs)
self._orientation = None
Expand Down
2 changes: 1 addition & 1 deletion src/compas_timber/_fabrication/step_joint_notch.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def __init__(
mortise=False,
mortise_width=40.0,
mortise_height=40.0,
**kwargs
**kwargs,
obucklin marked this conversation as resolved.
Show resolved Hide resolved
chenkasirer marked this conversation as resolved.
Show resolved Hide resolved
):
super(StepJointNotch, self).__init__(**kwargs)
self._orientation = None
Expand Down
164 changes: 164 additions & 0 deletions src/compas_timber/elements/fasteners/fastener.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
from compas_model.elements import Element
from compas_model.elements import reset_computed


class Fastener(Element):
obucklin marked this conversation as resolved.
Show resolved Hide resolved
"""
A class to represent timber fasteners (screws, dowels, brackets).

Parameters
----------
elements : list(:class:`~compas_timber.parts.Element`)
The elements that are connected with this fastener.

Attributes
----------
elements : list(:class:`~compas_timber.parts.Element`)
The elements that are connected with this fastener.

"""

def __init__(self, elements, **kwargs):
super(Fastener, self).__init__(elements, **kwargs)
self.elements = elements
obucklin marked this conversation as resolved.
Show resolved Hide resolved
self.features = []
obucklin marked this conversation as resolved.
Show resolved Hide resolved
self.attributes = {}
self.attributes.update(kwargs)
self.debug_info = []
obucklin marked this conversation as resolved.
Show resolved Hide resolved

def __repr__(self):
# type: () -> str
element_str = ["{} {}".format(element.__class__.__name__, element.key) for element in self.elements]
return "Fastener({})".format(", ".join(element_str))

# ==========================================================================
# Computed attributes
# ==========================================================================

@property
def is_fastener(self):
obucklin marked this conversation as resolved.
Show resolved Hide resolved
return True

@property
def shape(self):
# type: () -> Brep
return self._create_shape(self.frame, self.beams)

@property
def key(self):
# type: () -> int | None
return self.graph_node

def __str__(self):
element_str = ["{} {}".format(element.__class__.__name__, element.key) for element in self.elements]
return "Fastener connecting {}".format(", ".join(element_str))

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

def compute_geometry(self):
# type: (bool) -> compas.geometry.Brep
"""Compute the geometry of the fastener.

Returns
-------
:class:`compas.geometry.Brep`

"""
raise NotImplementedError

def compute_aabb(self, inflate=0.0):
# type: (float) -> compas.geometry.Box
"""Computes the Axis Aligned Bounding Box (AABB) of the element.

Parameters
----------
inflate : float, optional
Offset of box to avoid floating point errors.

Returns
-------
:class:`~compas.geometry.Box`
The AABB of the element.

"""
raise NotImplementedError

def compute_obb(self, inflate=0.0):
# type: (float | None) -> compas.geometry.Box
"""Computes the Oriented Bounding Box (OBB) of the element.

Parameters
----------
inflate : float
Offset of box to avoid floating point errors.

Returns
-------
:class:`compas.geometry.Box`
The OBB of the element.

"""
raise NotImplementedError
obucklin marked this conversation as resolved.
Show resolved Hide resolved

def compute_collision_mesh(self):
# type: () -> compas.datastructures.Mesh
"""Computes the collision geometry of the element.

Returns
-------
:class:`compas.datastructures.Mesh`
The collision geometry of the element.

"""
return self.shape.to_mesh()

# ==========================================================================
# Alternative constructors
# ==========================================================================
obucklin marked this conversation as resolved.
Show resolved Hide resolved

@staticmethod
def _create_shape(frame, beams):
# type: (Frame, list[TimberElement]) -> Brep
raise NotImplementedError
obucklin marked this conversation as resolved.
Show resolved Hide resolved

# ==========================================================================
# Featrues
# ==========================================================================

@reset_computed
def add_features(self, features):
obucklin marked this conversation as resolved.
Show resolved Hide resolved
# type: (Feature | list[Feature]) -> None
"""Adds one or more features to the fastener.

Parameters
----------
features : :class:`~compas_timber.parts.Feature` | list(:class:`~compas_timber.parts.Feature`)
The feature to be added.

"""
if not isinstance(features, list):
features = [features]
self.features.extend(features) # type: ignore

@reset_computed
def remove_features(self, features=None):
# type: (None | Feature | list[Feature]) -> None
"""Removes a feature from the fastener.

Parameters
----------
feature : :class:`~compas_timber.parts.Feature` | list(:class:`~compas_timber.parts.Feature`)
The feature to be removed. If None, all features will be removed.

"""
if features is None:
self.features = []
else:
if not isinstance(features, list):
features = [features]
self.features = [f for f in self.features if f not in features]
if not isinstance(features, list):
features = [features]
self.features = [f for f in self.features if f not in features]
58 changes: 58 additions & 0 deletions tests/compas_timber/test_fastener.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import pytest
from compas_timber.elements.fasteners.fastener import Fastener
from compas_timber.elements import Beam
from compas.geometry import Frame


@pytest.fixture
def mock_elements():
beam_1 = Beam(Frame([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]), 100.0, 10.0, 20.0)
beam_2 = Beam(Frame([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [-1.0, 0.0, 0.0]), 100.0, 10.0, 20.0)
return [beam_1, beam_2]


def test_fastener_initialization(mock_elements):
fastener = Fastener(mock_elements)
assert fastener.elements == mock_elements
assert fastener.features == []
assert fastener.attributes == {}
assert fastener.debug_info == []


def test_fastener_repr(mock_elements):
fastener = Fastener(mock_elements)
assert repr(fastener) == "Fastener(Beam None, Beam None)"


def test_fastener_str(mock_elements):
fastener = Fastener(mock_elements)
assert str(fastener) == "Fastener connecting Beam None, Beam None"


def test_fastener_is_fastener(mock_elements):
fastener = Fastener(mock_elements)
assert fastener.is_fastener is True


def test_fastener_add_features(mock_elements):
fastener = Fastener(mock_elements)
feature = "feature1"
fastener.add_features(feature)
assert fastener.features == [feature]


def test_fastener_remove_features(mock_elements):
fastener = Fastener(mock_elements)
feature = "feature1"
fastener.add_features(feature)
fastener.remove_features(feature)
assert fastener.features == []


def test_fastener_remove_all_features(mock_elements):
fastener = Fastener(mock_elements)
feature1 = "feature1"
feature2 = "feature2"
fastener.add_features([feature1, feature2])
fastener.remove_features()
assert fastener.features == []
Loading