Joints Contribution Guide
Joints represent the interaction between two or more timber elements to form structural connections. They coordinate the application of BTLx processings (features) across participating elements to achieve the desired joint geometry.
Note
For implementing new BTLx Processings, see the BTLx Contribution Guide.
Creating a New Joint
1. Define Joint Requirements and Analyze Element Relationships
Before implementation, establish the joint specifications and study how the involved elements interact geometrically:
Joint Specifications:
The specific timber joint type you’re creating
Required BTLx processings for the joint geometry
Target elements for each processing operation
Identify Joint Topology: Determine the connection topology using standard notation:
TOPO-X: Elements both interacting somewhere along their lengthsTOPO-L: Elements meeting at their ends at an angleTOPO-T: One element’s end intersecting another element along its lengthTOPO-I: Elements joined end-to-end in a straight line
Define Element Roles: Assign specific roles to each participating element, if relevant:
Note
Some joint topologies or specific joint types require clear distinctions between participating elements (e.g., main beam vs. cross beam), while others treat all elements equally.
Consider whether your joint implementation needs element role differentiation.
2. Create the Joint Class
Create a new module in src/compas_timber/connections/ that inherits from Joint. Based on the identified topology and joint type, name the joint class accordingly (e.g., TButtJoint for a TOPO-T butt joint).
The following methods and attributes are the absolute minimum required to implement a joint:
SUPPORTED_TOPOLOGY: Class attribute matching the joint topology (JointTopology.TOPO_X,JointTopology.TOPO_L,JointTopology.TOPO_T, orJointTopology.TOPO_I)__init__():__data__: Property returning a dictionary of joint data for serializationelements: Property returning a list of participating elementsrestore_beams_from_keys(): Method for restoring beam references after deserialization
Note
Joints can inherit from a generic base class (e.g., ButtJoint) to share common logic across topology-specific implementations (e.g., LButtJoint, TButtJoint).
The base class provides shared methods while concrete classes define topology-specific behavior.
Example:
class TNewJoint(Joint):
SUPPORTED_TOPOLOGY = JointTopology.TOPO_T # need to match the joint topology
@property
def __data__(self):
data = super(TNewJoint, self).__data__
data["main_beam_guid"] = self.main_beam_guid
data["cross_beam_guid"] = self.cross_beam_guid
data["arg_a"] = self.arg_a
data["arg_b"] = self.arg_b
return data
def __init__(self, main_beam, cross_beam, arg_a=None, arg_b=None, **kwargs):
super(TNewJoint, self).__init__(**kwargs)
self.main_beam = main_beam
self.cross_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)
self.arg_a = arg_a or "default_value_a"
self.arg_b = arg_b or "default_value_b"
self.features = [] # List to hold BTLx processings (features) for this joint
@property
def elements(self):
"""Returns a list of participating elements in the joint."""
return [self.main_beam, self.cross_beam]
def restore_beams_from_keys(self):
"""After de-serialization, restores references to the main and cross beams saved in the model."""
self.main_beam = model.element_by_guid(self.main_beam_guid)
self.cross_beam = model.element_by_guid(self.cross_beam_guid)
Note
Element references cannot be directly serialized, so joints store element GUIDs for persistence and restore references during deserialization.
2. Extract Geometric Information
Identify the spatial relationships and dimensional data needed for BTLx processing alternative constructors. These may include:
Reference side selection: Determine the
ref_side_indexto specify which face of the beam the processing operates on (defines the beam’s local coordinate system)Derived geometries: Extract geometric entities from element relationships, such as cutting planes, intersection volumes, and other relevant features.
Element dimensions: Retrieve beam properties such as width, height, and length for processing parameter calculations
Note
The geometric analysis described here is essential for determining the correct parameters to use with BTLx processing alternative constructors, such as from_plane_and_beam() and from_volume_and_beam().
Consult the necessary arguments required by each BTLx processing method to ensure proper usage and integration.
Example:
class TNewJoint(Joint):
# ... other methods ...
def main_ref_side_index(self):
"""Returns the reference side index for the main beam."""
# ... Logic to determine the reference side index for the main beam ...
return main_ref_side_index
def cross_ref_side_index(self):
"""Returns the reference side index for the cross beam."""
# ... Logic to determine the reference side index for the cross beam ...
return cross_ref_side_index
def main_beam_feature_geometry(self):
"""Returns the feature geometry for the main beam."""
# ... Logic to determine the feature geometry for the main beam ...
return feature_geometry_for_main_beam
def cross_beam_feature_geometry(self):
"""Returns the feature geometry for the cross beam."""
# ... Logic to determine the feature geometry for the cross beam ...
return feature_geometry_for_cross_beam
3. Implement Core Methods
Implement the following methods in your joint class:
add_features(): Create BTLx processing instances via their alternative constructors and assign them to target elements.add_extensions(): Modify element geometry (such as extending beam lengths) to accommodate the joint requirements and ensure geometric feasibility.check_elements_compatibility(): Validate that the elements meet necessary joint requirements if applicable, such as dimensions or coplanarity.
Example:
class TNewJoint(Joint):
# ... other methods ...
def add_extensions(self):
"""Calculates and adds the necessary extensions to the beams.""""
assert self.cross_beam and self.main_beam
try:
plane_a = self.main_beam_cutting_plane() # beam should be extended to this plane
start_a, end_a = self.main_beam.extension_to_plane(plane_a) # calculate the extension lengths
except Exception as ex:
raise BeamJoiningError(self.main_beam, self, debug_info=str(ex))
self.main_beam.add_blank_extension(start_a, end_a, self.main_beam_guid) # apply the extension to the main beam
def add_features(self):
"""Adds the required features in the form of BTLxProcessings to both beams."""
assert self.cross_beam and self.main_beam
# create a BTLx processing for the main beam
main_feature = NewProcessing.from_plane_and_beam(
plane=self.main_beam_cutting_plane(),
beam=self.main_beam,
arg_a=self.arg_a,
arg_b=self.arg_b,
ref_side_index=self.main_ref_side_index()
)
self.main_beam.add_features(main_feature) # register the feature to the main beam
# create a BTLx processing for the cross beam
cross_feature = # ... Similar logic to create the BTLx processing for the cross beam ...
self.cross_beam.add_features(cross_feature) # register the feature to the cross
self.features.extend([main_feature, cross_feature]) # register the features to the joint itself
def check_elements_compatibility(self):
"""Checks if the elements are compatible for the creation of the joint."""
assert self.cross_beam and self.main_beam
are_compatible = # ... Logic to check if the main and cross beams are compatible for the joint ...
if not are_compatible:
raise BeamJoiningError(
self.elements,
self,
debug_info="The main and cross beams are not compatible for the joint."
)
Note
In the add_features() method, register each BTLx processing (feature) both to the corresponding element using element.add_features() and to the joint itself using self.features.append(feature).
This ensures features are properly associated for both element modification and joint serialization.
4. Update Module Imports
Add your new joint class to src/compas_timber/connections/__init__.py so it can be imported by other modules.
5. Add Tests
Add unit tests in tests/compas_timber/ to verify your joint works correctly. Ensure you cover:
BTLx processing creation and assignment in the
add_features()methodGeometry modification in the
add_extensions()methodCompatibility checks in the
check_elements_compatibility()method
Key Considerations
Inheritance Patterns:
Use base classes for shared joint logic across topologies.
Concrete classes should define topology-specific behavior and declare their SUPPORTED_TOPOLOGY.
Avoid code duplication between similar joint types by leveraging inheritance.
Element Ordering:
Maintain consistent element ordering in joint constructors and method signatures.
When elements have specific roles, always use the same parameter order (e.g., main_beam first, cross_beam second) across all joint methods.
Error Handling:
Use BeamJoiningError for joint-specific failures with meaningful debug information.
Include element references and joint context in error messages to aid debugging.
Serialization Requirements:
Store element GUIDs, not direct references, for persistence.
Implement proper restore_beams_from_keys() to rebuild element relationships after deserialization.
Include all joint parameters in the __data__ property for complete serialization.