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

Many beam joints #331

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
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
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Added attribute `is_group_element` to `TimberElement`.
* Added `JointRule.joints_from_beams_and_rules()` static method
* Added `Element.reset()` method.

* Added new `fasteners.py` module with new `Fastener` element type.
* Added new `Joint_Rule_From_List` GH Component that takes lists of beams to create joints.
* Added `MIN_ELEMENT_COUNT` and `MAX_ELEMENT_COUNT` class attributes and `element_count_complies` class method to `Joint`.
* Added `beams`, `plates` and `fasteners` properties to `Joint`.

### Changed

Expand All @@ -52,6 +54,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Changed `model.element_by_guid()` instead of direct `elementsdict[]` access for beam retrieval in joint modules: `LMiterJoint`, `TStepJoint`, `TDovetailJoint`, `TBirdsmouthJoint`.
* Reworked the model generation pipeline.
* Reworked `comply` methods for `JointRule`s.
* Changed `DirectJointRule` to allow for more than 2 elements per joint.
* Changed `beam` objects get added to `Joint.elements` in `Joint.create()`.

### Removed

Expand Down
1 change: 1 addition & 0 deletions src/compas_timber/connections/butt_joint.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def __init__(self, main_beam=None, cross_beam=None, mill_depth=0, birdsmouth=Fal
super(ButtJoint, self).__init__(**kwargs)
self.main_beam = main_beam
self.cross_beam = cross_beam
self.elements.extend([main_beam, cross_beam])
self.main_beam_guid = str(main_beam.guid) if main_beam else None
self.cross_beam_guid = str(cross_beam.guid) if cross_beam else None
self.mill_depth = mill_depth
Expand Down
1 change: 1 addition & 0 deletions src/compas_timber/connections/french_ridge_lap.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def __init__(self, beam_a=None, beam_b=None, **kwargs):
super(FrenchRidgeLapJoint, self).__init__(beams=(beam_a, beam_b), **kwargs)
self.beam_a = beam_a
self.beam_b = beam_b
self.elements.extend([beam_a, beam_b])
self.beam_a_guid = str(beam_a.guid) if beam_a else None
self.beam_b_guid = str(beam_b.guid) if beam_b else None
self.reference_face_indices = {}
Expand Down
41 changes: 37 additions & 4 deletions src/compas_timber/connections/joint.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,37 @@ class Joint(Interaction):
"""

SUPPORTED_TOPOLOGY = JointTopology.TOPO_UNKNOWN
MIN_ELEMENT_COUNT = 2
MAX_ELEMENT_COUNT = 2

def __init__(self, **kwargs):
super(Joint, self).__init__(name=self.__class__.__name__)
self.elements = []

@property
def beams(self):
raise NotImplementedError
for element in self.elements:
if getattr(element, "is_beam", False):
yield element

@property
def plates(self):
for element in self.elements:
if getattr(element, "is_plate", False):
yield element

@property
def fasteners(self):
for element in self.elements:
if getattr(element, "is_fastener", False):
yield element

@classmethod
def element_count_complies(cls, elements):
if cls.MAX_ELEMENT_COUNT:
return len(elements) >= cls.MIN_ELEMENT_COUNT and len(elements) <= cls.MAX_ELEMENT_COUNT
else:
return len(elements) >= cls.MIN_ELEMENT_COUNT

def add_features(self):
"""Adds the features defined by this joint to affected beam(s).
Expand Down Expand Up @@ -136,10 +160,8 @@ def create(cls, model, *beams, **kwargs):

"""

if len(beams) < 2:
raise ValueError("Expected at least 2 beams. Got instead: {}".format(len(beams)))
joint = cls(*beams, **kwargs)
model.add_joint(joint, beams)
model.add_joint(joint)
return joint

@property
Expand All @@ -157,6 +179,17 @@ def ends(self):

return self._ends

@property
def interactions(self):
"""Returns all possible interactions between elements that are connected by this joint.
interaction is defined as a tuple of (element_a, element_b, joint).
"""
interactions = []
for i in range(len(self.beams)):
for j in range(i + 1, len(self.elements)):
interactions.append((self.elements[i], self.elements[j], self))
return interactions

@staticmethod
def get_face_most_towards_beam(beam_a, beam_b, ignore_ends=True):
"""Of all the faces of `beam_b`, returns the one whose normal most faces `beam_a`.
Expand Down
1 change: 1 addition & 0 deletions src/compas_timber/connections/lap_joint.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def __init__(self, main_beam=None, cross_beam=None, flip_lap_side=False, cut_pla
super(LapJoint, self).__init__()
self.main_beam = main_beam
self.cross_beam = cross_beam
self.elements.extend([main_beam, cross_beam])
self.flip_lap_side = flip_lap_side
self.cut_plane_bias = cut_plane_bias
self.main_beam_guid = str(main_beam.guid) if main_beam else None
Expand Down
1 change: 1 addition & 0 deletions src/compas_timber/connections/null_joint.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def __init__(self, beam_a=None, beam_b=None, **kwargs):
super(NullJoint, self).__init__(**kwargs)
self.beam_a = beam_a
self.beam_b = beam_b
self.elements.extend([beam_a, beam_b])
self.beam_a_guid = str(beam_a.guid) if beam_a else None
self.beam_b_guid = str(beam_b.guid) if beam_b else None

Expand Down
1 change: 1 addition & 0 deletions src/compas_timber/connections/t_birdsmouth.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def __init__(self, main_beam, cross_beam, **kwargs):
super(TBirdsmouthJoint, self).__init__(**kwargs)
self.main_beam = main_beam
self.cross_beam = cross_beam
self.elements.extend([main_beam, cross_beam])
self.main_beam_guid = kwargs.get("main_beam_guid", None) or str(main_beam.guid)
self.cross_beam_guid = kwargs.get("cross_beam_guid", None) or str(cross_beam.guid)

Expand Down
1 change: 1 addition & 0 deletions src/compas_timber/connections/t_dovetail.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ def __init__(
super(TDovetailJoint, self).__init__(**kwargs)
self.main_beam = main_beam
self.cross_beam = cross_beam
self.elements.extend([main_beam, cross_beam])
self.main_beam_guid = kwargs.get("main_beam_guid", None) or str(main_beam.guid)
self.cross_beam_guid = kwargs.get("cross_beam_guid", None) or str(cross_beam.guid)

Expand Down
1 change: 1 addition & 0 deletions src/compas_timber/connections/t_step_joint.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ def __init__(
super(TStepJoint, self).__init__(**kwargs)
self.main_beam = main_beam
self.cross_beam = cross_beam
self.elements.extend([main_beam, cross_beam])
self.main_beam_guid = kwargs.get("main_beam_guid", None) or str(main_beam.guid)
self.cross_beam_guid = kwargs.get("cross_beam_guid", None) or str(cross_beam.guid)

Expand Down
6 changes: 3 additions & 3 deletions src/compas_timber/design/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def joints_from_beams_and_rules(beams, rules, max_distance=1e-6):


class DirectRule(JointRule):
"""Creates a Joint Rule that directly joins two beams."""
"""Creates a Joint Rule that directly joins multiple elements."""

def __init__(self, joint_type, beams, **kwargs):
self.beams = beams
Expand Down Expand Up @@ -237,7 +237,7 @@ def comply(self, beams, max_distance=1e-3):


class JointDefinition(object):
"""Container for a joint type and the beam that shall be joined.
"""Container for a joint type and the elements that shall be joined.

This allows delaying the actual joining of the beams to a downstream component.

Expand Down Expand Up @@ -277,7 +277,7 @@ def match(self, beams):


class FeatureDefinition(object):
"""Container linking a feature for the beams on which it should be applied.
"""Container linking a feature to the elements on which it should be applied.

This allows delaying the actual applying of features to a downstream component.

Expand Down
1 change: 0 additions & 1 deletion src/compas_timber/elements/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@ def apply(self, element_geometry, *args, **kwargs):
The resulting geometry after processing.

"""
print("applying drill hole feature to element")
plane = Plane(point=self.line.start, normal=self.line.vector)
plane.point += plane.normal * 0.5 * self.length
drill_volume = Cylinder(frame=Frame.from_plane(plane), radius=self.diameter / 2.0, height=self.length)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ def RunScript(self, centerline, z_vector, width, height, category, updateRefObj)
beam = CTBeam.from_centerline(centerline=line, width=w, height=h, z_vector=z)
beam.attributes["rhino_guid"] = str(guid) if guid else None
beam.attributes["category"] = c
print(guid)
if updateRefObj and guid:
update_rhobj_attributes_name(guid, "width", str(w))
update_rhobj_attributes_name(guid, "height", str(h))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@
}
]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ def RunScript(self, *args):
for i, val in enumerate(args[2:]):
if val is not None:
kwargs[self.arg_names()[i + 2]] = val
print(kwargs)
if not cat_a:
self.AddRuntimeMessage(
Warning, "Input parameter {} failed to collect data.".format(self.arg_names()[0])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ def __init__(self):
super(DirectJointRule, self).__init__()
self.classes = {}
for cls in get_leaf_subclasses(Joint):
self.classes[cls.__name__] = cls
if cls.MAX_ELEMENT_COUNT == 2:
self.classes[cls.__name__] = cls

if ghenv.Component.Params.Output[0].NickName == "Rule":
self.joint_type = None
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import inspect

from ghpythonlib.componentbase import executingcomponent as component
from Grasshopper.Kernel.GH_RuntimeMessageLevel import Warning

from compas_timber.connections import Joint
from compas_timber.design import DirectRule
from compas_timber.ghpython.ghcomponent_helpers import get_leaf_subclasses
from compas_timber.ghpython.ghcomponent_helpers import manage_dynamic_params
from compas_timber.ghpython.ghcomponent_helpers import rename_gh_output


class JointRuleFromList(component):
def __init__(self):
super(JointRuleFromList, self).__init__()
self.classes = {}
for cls in get_leaf_subclasses(Joint):
self.classes[cls.__name__] = cls

if ghenv.Component.Params.Output[0].NickName == "Rule":
self.joint_type = None
else:
self.joint_type = self.classes.get(ghenv.Component.Params.Output[0].NickName, None)

def RunScript(self, *args):
if not self.joint_type:
ghenv.Component.Message = "Select joint type from context menu (right click)"
self.AddRuntimeMessage(Warning, "Select joint type from context menu (right click)")
return None
else:
ghenv.Component.Message = self.joint_type.__name__
elements = args[0]
if not elements:
self.AddRuntimeMessage(
Warning, "Input parameter {} failed to collect data.".format(self.arg_names()[0])
)
return
if not self.joint_type.element_count_complies(elements):
self.AddRuntimeMessage(
Warning,
"{} requires at least {} and at most {} elements.".format(
self.joint_type.__name__, self.joint_type.MIN_ELEMENT_COUNT, self.joint_type.MAX_ELEMENT_COUNT
),
)
return
kwargs = {}
for i, val in enumerate(args[1:]):
if val is not None:
kwargs[self.arg_names()[i + 1]] = val

return DirectRule(self.joint_type, elements, **kwargs)

def arg_names(self):
return inspect.getargspec(self.joint_type.__init__)[0][1:]

def AppendAdditionalMenuItems(self, menu):
for name in self.classes.keys():
item = menu.Items.Add(name, None, self.on_item_click)
if self.joint_type and name == self.joint_type.__name__:
item.Checked = True

def on_item_click(self, sender, event_info):
self.joint_type = self.classes[str(sender)]
rename_gh_output(self.joint_type.__name__, 0, ghenv)
manage_dynamic_params(self.arg_names(), ghenv)
ghenv.Component.ExpireSolution(True)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "Joint Rules From List",
"nickname": "JointsFromLists",
"category": "COMPAS Timber",
"subcategory": "Joint Rules",
"description": "Generates a joint between beams in a list. Use a list of lists(data tree) to define multiple joints. Has same priority as direct joints.",
"exposure": 8,
"ghpython": {
"isAdvancedMode": true,
"iconDisplay": 0,
"inputParameters": [
{
"name": "Beams",
"description": "Beams to join.",
"typeHintID": "none",
"scriptParamAccess": 1
}
],
"outputParameters": [
{
"name": "Rule",
"description": "Joint Rules."
}
]
}
}
4 changes: 2 additions & 2 deletions src/compas_timber/ghpython/components/CT_Model/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ def RunScript(self, Elements, JointRules, Features, MaxDistance, CreateGeometry)

if unmatched_pairs:
for pair in unmatched_pairs:
self.addRuntimeMessage(
Warning, "No joint rule found for beams {} and {}".format(pair[0].key, pair[1].key)
self.AddRuntimeMessage(
Warning, "No joint rule found for beams {} and {}".format(list(pair)[0].key, list(pair)[1].key)
) # TODO: add to debug_info

if joints:
Expand Down
2 changes: 2 additions & 0 deletions src/compas_timber/ghpython/ghcomponent_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ def manage_dynamic_params(input_names, ghenv, rename_count=0, permanent_param_co
The names of the input parameters.
ghenv : object
The Grasshopper environment object.
rename_count : int, optional
The number of parameters that should be renamed. Default is 0.
permanent_param_count : int, optional
The number of parameters that should not be deleted. Default is 1.

Expand Down
20 changes: 8 additions & 12 deletions src/compas_timber/model/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,12 @@ def plates(self):

@property
def joints(self):
# type: () -> Generator[Joint, None, None]
# type: () -> List[Joint, None, None]
joints = []
for interaction in self.interactions():
if isinstance(interaction, Joint):
yield interaction # TODO: consider if there are other interaction types...
joints.append(interaction)
return list(set(joints)) # remove duplicates

@property
def walls(self):
Expand Down Expand Up @@ -222,23 +224,17 @@ def get_elements_in_group(self, group_name, filter_=None):
elements = (node.element for node in group.children)
return filter(filter_, elements)

def add_joint(self, joint, beams):
# type: (Joint, tuple[Beam]) -> None
def add_joint(self, joint):
# type: (Joint) -> None
"""Add a joint object to the model.

Parameters
----------
joint : :class:`~compas_timber.connections.joint`
An instance of a Joint class.

beams : tuple(:class:`~compas_timber.elements.Beam`)
The two beams that should be joined.

"""
if len(beams) != 2:
raise ValueError("Expected 2 parts. Got instead: {}".format(len(beams)))
a, b = beams
_ = self.add_interaction(a, b, interaction=joint)
for interaction in joint.interactions:
_ = self.add_interaction(*interaction)

def remove_joint(self, joint):
# type: (Joint) -> None
Expand Down
13 changes: 13 additions & 0 deletions tests/compas_timber/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def test_add_element():
A.add_element(B)

assert B in A.beams
assert B in A.elements()
assert len(list(A.graph.nodes())) == 1
assert len(list(A.graph.edges())) == 0
assert list(A.beams)[0] is B
Expand All @@ -43,6 +44,18 @@ def test_add_joint():
assert len(list(model.joints)) == 1


def test_get_joint_from_interaction():
model = TimberModel()
b1 = Beam(Frame.worldXY(), length=1.0, width=0.1, height=0.1)
b2 = Beam(Frame.worldYZ(), length=1.0, width=0.1, height=0.1)

model.add_element(b1)
model.add_element(b2)
joint = LButtJoint.create(model, b1, b2)

assert joint is list(model.joints)[0]


def test_copy(mocker):
mocker.patch("compas_timber.connections.LButtJoint.add_features")
F1 = Frame(Point(0, 0, 0), Vector(1, 0, 0), Vector(0, 1, 0))
Expand Down
Loading