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 lengths

  • TOPO-L: Elements meeting at their ends at an angle

  • TOPO-T: One element’s end intersecting another element along its length

  • TOPO-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, or JointTopology.TOPO_I)

  • __init__() :

  • __data__ : Property returning a dictionary of joint data for serialization

  • elements : Property returning a list of participating elements

  • restore_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_index to 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() method

  • Geometry modification in the add_extensions() method

  • Compatibility 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.