Skip to content

Commit

Permalink
Merge branch 'main' into new_solver_workflow
Browse files Browse the repository at this point in the history
  • Loading branch information
obucklin committed Nov 14, 2024
2 parents fdc815d + 1185ea5 commit f378ee6
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ 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 method `add_group_element` to `TimberModel`.
* Added new method `has_group` to `TimberModel`.
* Added new method `get_elements_in_group` to `TimberModel`.
* Added attribute `is_group_element` to `TimberElement`.
* Added `JointRule.joints_from_beams_and_rules()` static method
* Added `Element.reset()` method.

Expand Down
6 changes: 6 additions & 0 deletions src/compas_timber/elements/timber.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class TimberElement(Element):
True if the element is a plate.
is_wall : bool
True if the element is a wall.
is_group_element : bool
True if the element can be used as container for other elements.
"""

Expand All @@ -29,6 +31,10 @@ def is_plate(self):
def is_wall(self):
return False

@property
def is_group_element(self):
return False

def reset(self):
"""Resets the element to its initial state by removing all features, extensions, and debug_info."""
self.remove_features()
Expand Down
4 changes: 4 additions & 0 deletions src/compas_timber/elements/wall.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ def __init__(self, length, width, height, frame=None, **kwargs):
def is_wall(self):
return True

@property
def is_group_element(self):
return True

@property
def shape(self):
assert self.frame
Expand Down
99 changes: 99 additions & 0 deletions src/compas_timber/model/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,105 @@ def element_by_guid(self, guid):
"""
return self._guid_element[guid]

def add_group_element(self, element, name=None):
"""Add an element which shall contain other elements.
The container element is added to the group as well.
TODO: upstream this to compas_model, maybe?
Parameters
----------
element : :class:`~compas_timber.elements.TimberElement`
The element to add to the group.
name : str, optional
The name of the group to add the element to. If not provided, the element's name is used.
Returns
-------
:class:`~compas_model.models.GroupNode`
The group node containing the element.
Raises
------
ValueError
If the group name is not provided and the element has no name.
If a group with same name already exists in the model.
Examples
--------
>>> from compas_timber.elements import Beam, Wall
>>> from compas_timber.model import TimberModel
>>> model = TimberModel()
>>> wall1_group = model.add_group_element(Wall(5000, 200, 3000, name="wall1"))
>>> beam_a = Beam(Frame.worldXY(), 100, 200, 300)
>>> model.add_element(beam_a, parent=wall1_group)
>>> model.has_group("wall1")
True
"""
# type: (TimberElement, str) -> GroupNode
group_name = name or element.name

if not element.is_group_element:
raise ValueError("Element {} is not a group element.".format(element))

if not group_name:
raise ValueError("Group name must be provided or group element must have a name.")

if self.has_group(group_name):
raise ValueError("Group {} already exists in model.".format(group_name))

group_node = self.add_group(group_name)
self.add_element(element, parent=group_node)
return group_node

def has_group(self, group_name):
# type: (str) -> bool
"""Check if a group with `group_name` exists in the model.
TODO: upstream this to compas_model
Parameters
----------
group_name : str
The name of the group to query.
Returns
-------
bool
True if the group exists in the model.
"""
return group_name in (group.name for group in self._tree.groups)

def get_elements_in_group(self, group_name, filter_=None):
"""Get all elements in a group with `group_name`.
TODO: upstream this to compas_model
Parameters
----------
group_name : str
The name of the group to query.
filter_ : callable, optional
A filter function to apply to the elements.
Returns
-------
Generator[:class:`~compas_model.elements.Element`]
A generator of elements in the group.
"""
# type: (str, Optional[callable]) -> Generator[Element, None, None]
if not self.has_group(group_name):
raise ValueError("Group {} not found in model.".format(group_name))

filter_ = filter_ or (lambda _: True)

group = next((group for group in self._tree.groups if group.name == group_name))
elements = (node.element for node in group.children)
return filter(filter_, elements)

def add_joint(self, joint, beams):
# type: (Joint, tuple[Beam]) -> None
"""Add a joint object to the model.
Expand Down
91 changes: 91 additions & 0 deletions tests/compas_timber/test_wall.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import pytest

from compas.geometry import Frame
from compas_timber.elements import Wall
from compas_timber.elements import Beam
from compas_timber.model import TimberModel


@pytest.fixture
def model():
return TimberModel()


@pytest.fixture
def beam_list():
beam_a = Beam(Frame.worldXY(), 100, 200, 300)
beam_b = Beam(Frame.worldXY(), 102, 240, 30)
beam_c = Beam(Frame.worldXY(), 100, 200, 3)
beam_d = Beam(Frame.worldXY(), 150, 20, 60)
return [beam_a, beam_b, beam_c, beam_d]


def test_wall_groups(model):
wall1 = Wall(5000, 200, 3000, name="wall1")
wall2 = Wall(3000, 200, 3000)

model.add_group_element(wall1)
model.add_group_element(wall2, name="wall2")

assert model.has_group("wall1")
assert model.has_group("wall2")


def test_add_elements_to_group(model, beam_list):
beam_a, beam_b, beam_c, beam_d = beam_list
wall1 = Wall(5000, 200, 3000, name="wall1")
wall2 = Wall(3000, 200, 3000, name="wall2")

wall1_group = model.add_group_element(wall1)
wall2_group = model.add_group_element(wall2)

model.add_element(beam_a, parent=wall1_group)
model.add_element(beam_b, parent=wall1_group)
model.add_element(beam_c, parent=wall2_group)
model.add_element(beam_d, parent=wall2_group)

assert model.has_group("wall1")
assert model.has_group("wall2")
assert list(model.get_elements_in_group("wall1")) == [wall1, beam_a, beam_b]
assert list(model.get_elements_in_group("wall2")) == [wall2, beam_c, beam_d]


def test_get_elements_in_group_filter(model, beam_list):
beam_a, beam_b, beam_c, beam_d = beam_list

wall1 = Wall(5000, 200, 3000, name="wall1")
wall2 = Wall(3000, 200, 3000, name="wall2")

wall1_group = model.add_group_element(wall1)
wall2_group = model.add_group_element(wall2)

model.add_element(beam_a, parent=wall1_group)
model.add_element(beam_b, parent=wall1_group)
model.add_element(beam_c, parent=wall2_group)
model.add_element(beam_d, parent=wall2_group)

assert model.has_group("wall1")
assert model.has_group("wall2")
assert list(model.get_elements_in_group("wall1", filter_=lambda b: b.is_beam)) == [beam_a, beam_b]
assert list(model.get_elements_in_group("wall2", filter_=lambda b: b.is_beam)) == [beam_c, beam_d]


def test_group_does_not_exist(model):
with pytest.raises(ValueError):
list(model.get_elements_in_group("non_existent_group"))


def test_group_already_exists(model):
wall1 = Wall(5000, 200, 3000, name="wall1")

model.add_group_element(wall1)

with pytest.raises(ValueError):
model.add_group_element(wall1)


def test_not_group_element(model):
beam = Beam(Frame.worldXY(), 100, 200, 300)

with pytest.raises(ValueError):
model.add_group_element(beam)

0 comments on commit f378ee6

Please sign in to comment.