Skip to content

compas_timber.connections ¤

Classes¤

BallNodeJoint ¤

BallNodeJoint(beams=None, base_interface=None, ball_diameter=None, **kwargs)

Represents a ball node type joint which joins the ends of multiple beams, trimming the main beam.

Please use BallNodeJoint.create() to properly create an instance of this class and associate it with an model.

Parameters:

  • beams ( list(:class:`~compas_timber.elements.Beam`), default: None ) –

    The beams to be joined.

  • base_interface (:class:`~compas_timber.connections.FastenerTimberInterface`, default: None ) –

    Describes the interface between the fastener and each of the timber elements.

  • ball_diameter (float, default: None ) –

    The diameter of the ball node.

Attributes:

  • elements (list(:class:`~compas_timber.elements.Element`)) –

    The elements joined by this joint.

  • generated_elements (list(:class:`~compas_timber.elements.Element`)) –

    The elements generated by this joint.

Source code in src/compas_timber/connections/ball_node.py
def __init__(self, beams=None, base_interface=None, ball_diameter=None, **kwargs):
    super(BallNodeJoint, self).__init__(elements=tuple(b for b in beams), **kwargs)
    self._beam_guids = []
    self.beams = beams or []
    if ball_diameter:
        self.ball_diameter = ball_diameter
    elif beams is not None:
        self.ball_diameter = beams[0].height
    else:
        self.ball_diameter = 100

    self._node_point = None
    self._beam_guids = kwargs.get("beam_guids", None) or [str(beam.guid) for beam in self.beams]
    self._fastener_guid = kwargs.get("fastener_guid", None)
    if not self._fastener_guid:
        self.fastener = BallNodeFastener(self.node_point, self.ball_diameter)
        self.fastener.base_interface = base_interface
        self._fastener_guid = str(self.fastener.guid)

Attributes¤

node_point property ¤
node_point

Returns the point at which the beams are joined, essentially the average of their intersection points.

Methods:¤

add_features ¤
add_features()

Adds the features of the joint as generated by FastenerTimberInterface to the timber elements. In this joint, the fastener adapt to the beams, therefore, the joint creates the FastenerTimberInterface and adds it to the fastener.

Source code in src/compas_timber/connections/ball_node.py
def add_features(self):
    """Adds the features of the joint as generated by `FastenerTimberInterface` to the timber elements.
    In this joint, the fastener adapt to the beams, therefore, the joint creates the FastenerTimberInterface
    and adds it to the fastener.

    """
    assert self.fastener
    for beam in self.beams:
        interface = self.fastener.base_interface.copy()
        pt = beam.centerline.closest_point(self._node_point)
        interface.frame = Frame(pt, Vector.from_start_end(pt, beam.centerline.midpoint), beam.frame.zaxis)
        self.fastener.interfaces.append(interface)
        features = interface.get_features(beam)
        beam.add_features(features)
        self.features.extend(features)
create classmethod ¤
create(model, *elements, **kwargs)

Creates an instance of the BallNodeJoint and creates the new connection in model.

This differs fom the generic Joint.create() method in that it passes the beams to the constructor of the BallNodeJoint as a list instead of as separate arguments.

beams are expected to have been added to model before calling this method.

This code does not verify that the given beams are adjacent and/or lie in a topology which allows connecting them. This is the responsibility of the calling code.

A ValueError is raised if beams contains less than two Beam objects.

Parameters:

  • model (:class:`~compas_timber.model.TimberModel`) –

    The model to which the beams and this joing belong.

  • elements (list(:class:`~compas_timber.elements.Beam`), default: () ) –

    A list containing beams that whould be joined together

Returns:

  • class:`compas_timber.connections.Joint` –

    The instance of the created joint.

Source code in src/compas_timber/connections/ball_node.py
@classmethod
def create(cls, model, *elements, **kwargs):
    """Creates an instance of the BallNodeJoint and creates the new connection in `model`.

    This differs fom the generic `Joint.create()` method in that it passes the `beams` to
    the constructor of the BallNodeJoint as a list instead of as separate arguments.

    `beams` are expected to have been added to `model` before calling this method.

    This code does not verify that the given beams are adjacent and/or lie in a topology which allows connecting
    them. This is the responsibility of the calling code.

    A `ValueError` is raised if `beams` contains less than two `Beam` objects.

    Parameters
    ----------
    model : :class:`~compas_timber.model.TimberModel`
        The model to which the beams and this joing belong.
    elements : list(:class:`~compas_timber.elements.Beam`)
        A list containing beams that whould be joined together

    Returns
    -------
    :class:`compas_timber.connections.Joint`
        The instance of the created joint.

    """
    joint = cls(elements, **kwargs)
    model.add_joint(joint)
    return joint

ButtJoint ¤

ButtJoint(
    main_beam: Beam = None,
    cross_beam: Beam = None,
    mill_depth: float | None = None,
    butt_plane_spec: CutPlaneSpec | None = None,
    force_pocket: bool = False,
    conical_tool: bool = False,
    **kwargs,
)

Represents an L-Butt type joint which joins two beam in their ends, trimming the main beam.

This joint type is compatible with beams in L topology.

Please use LButtJoint.create() to properly create an instance of this class and associate it with a model.

Parameters:

  • main_beam (:class:`~compas_timber.elements.Beam`, default: None ) –

    The main beam to be joined.

  • cross_beam (:class:`~compas_timber.elements.Beam`, default: None ) –

    The cross beam to be joined.

  • mill_depth (float, default: None ) –

    The depth of the pocket to be milled in the cross beam. This will be ignored if butt_plane_spec is provided.

  • butt_plane_spec (:class:`~compas_timber.connections.CutPlaneSpec`, default: None ) –

    Overrides the plane used to cut the main beam. Build with :meth:~compas_timber.connections.CutPlaneSpec.from_butt_plane.

  • force_pocket (bool, default: False ) –

    If True applies a :~compas_timber.fabrication.Pocket feature instead of a :~compas_timber.fabrication.Lap on the cross beam. Default is False.

  • conical_tool (bool, default: False ) –

    If True it can apply smaller than 90 degrees angles to the TiltSide parameters of the :~compas_timber.fabrication.Pocket feature. Default is False.

Attributes:

  • main_beam (:class:`~compas_timber.elements.Beam`) –

    The main beam to be joined.

  • cross_beam (:class:`~compas_timber.elements.Beam`) –

    The cross beam to be joined.

  • beams (list[:class:`~compas_timber.elements.Beam`]) –

    A list containing the main beam and the cross beam.

  • mill_depth (float) –

    The depth of the pocket to be milled in the cross beam.

  • force_pocket (bool) –

    If True applies a :~compas_timber.fabrication.Pocket feature instead of a :~compas_timber.fabrication.Lap on the cross beam. Default is False.

  • conical_tool (bool) –

    If True it can apply smaller than 90 degrees angles to the TiltSide parameters of the :~compas_timber.fabrication.Pocket feature. Default is False.

  • features (list[:class:`~compas_timber.fabrication.BTLxProcessing`]) –

    List of features to be applied to the cross beam and main beam.

  • cross_beam_ref_side_index (int) –

    The index of the side of the cross beam relative to the main beam.

  • main_beam_ref_side_index (int) –

    The index of the side of the main beam relative to the cross beam.

Source code in src/compas_timber/connections/butt_joint.py
def __init__(
    self,
    main_beam: Beam = None,
    cross_beam: Beam = None,
    mill_depth: Optional[float] = None,
    butt_plane_spec: Optional[CutPlaneSpec] = None,
    force_pocket: bool = False,
    conical_tool: bool = False,
    **kwargs,
):
    super(ButtJoint, self).__init__(elements=(main_beam, cross_beam), **kwargs)
    self.mill_depth: float = mill_depth or 0.0
    self._butt_plane_spec: Optional[CutPlaneSpec] = butt_plane_spec
    self.force_pocket: bool = force_pocket
    self.conical_tool: bool = conical_tool

Attributes¤

butt_plane property ¤
butt_plane: Plane

The plane used to cut the main beam.

If a :class:~compas_timber.connections.CutPlaneSpec override is set, it is resolved against the cross beam. Otherwise defaults to the cross beam's side closest to the main beam, offset outward by mill_depth.

Methods:¤

add_extensions ¤
add_extensions()

Calculates and adds the necessary extensions to the beams.

Raises:

Source code in src/compas_timber/connections/butt_joint.py
def add_extensions(self):
    """Calculates and adds the necessary extensions to the beams.

    Raises
    ------
    BeamJoiningError
        If the extension could not be calculated.
    """
    assert self.main_beam and self.cross_beam
    # extend the main beam
    try:
        start, end = self.main_beam.extension_to_plane(self.butt_plane)
        extension_tolerance = 0
        # extension_tolerance = 0.01 if TOL.unit == "M" else 10
        joint_id = self.guid
        self.main_beam.add_blank_extension(start + extension_tolerance, end + extension_tolerance, joint_id)
    except AttributeError as ae:
        raise BeamJoiningError(beams=self.elements, joint=self, debug_info=str(ae), debug_geometries=[self.butt_plane])
    except Exception as ex:
        raise BeamJoiningError(beams=self.elements, joint=self, debug_info=str(ex))
add_features ¤
add_features() -> None

Removes this joint's previously generated features and adds new features to each beam.

Source code in src/compas_timber/connections/butt_joint.py
def add_features(self) -> None:
    """Removes this joint's previously generated features and adds new features to each beam."""
    assert self.main_beam and self.cross_beam

    # apply cut on the main beam
    main_feature = JackRafterCutProxy.from_plane_and_beam(self.butt_plane, self.main_beam, self.main_beam_ref_side_index)
    self.main_beam.add_features(main_feature)
    self.features.append(main_feature)

    # apply lap or pocket on the cross beam
    if self.mill_depth:
        if self.force_pocket:
            milling_volume = self._get_milling_volume_for_pocket()
            cross_feature = Pocket.from_volume_and_element(
                milling_volume,
                self.cross_beam,
                allow_undercut=self.conical_tool,
                ref_side_index=self.cross_beam_ref_side_index,
            )
        else:
            cross_cutting_plane = self.main_beam.ref_sides[self.main_beam_ref_side_index]
            lap_width = self.main_beam.get_dimensions_relative_to_side(self.main_beam_ref_side_index)[1]
            cross_feature = Lap.from_plane_and_beam(
                cross_cutting_plane,
                self.cross_beam,
                lap_width,
                self.mill_depth,
                ref_side_index=self.cross_beam_ref_side_index,
            )
        self.cross_beam.add_features(cross_feature)
        self.features.append(cross_feature)

Cluster ¤

Cluster(joints)

Represents a collection of JointCandidates grouped by geometric proximity.

Parameters:

  • joints (list[:class:`~compas_timber.connections.Joint`]) –

    The joints that are part of this cluster.

Attributes:

  • joints (list[:class:`~compas_timber.connections.Joint`]) –

    The joints that are part of this cluster.

  • elements (set[:class:`~compas_timber.elements.Element`]) –

    List of unique instances of elements that are part of this cluster.

  • location (:class:`~compas.geometry.Point`) –

    The approximated location of the cluster, effectively the location of the first joint.

Source code in src/compas_timber/connections/cluster.py
def __init__(self, joints):
    # TODO: rename to make more clear that these are JointCandidates.
    self.joints = joints
    self._elements = None
    self._topology = None

Attributes¤

topology property ¤
topology

Returns the topology of the joint if there is only one joint, otherwise TOPO_UNKNOWN.

CompositeJoint ¤

CompositeJoint(joints: list[Joint], name: str = None, **kwargs)

A joint composed of multiple pairwise sub-joints.

Instead of defining a single fabrication strategy for all joined elements, this joint delegates all feature and extension calculations to a list of pairwise sub-joints. The sub-joints are instantiated without being registered in the model; only the CompositeJoint itself is added.

Parameters:

  • joints (list[:class:`~compas_timber.connections.Joint`]) –

    The joints contained by this joint.

  • name (str, default: None ) –

    The name of the joint.

Attributes:

  • joints (list[:class:`~compas_timber.connections.Joint`]) –

    The pairwise sub-joints.

  • elements (tuple[:class:`~compas_timber.elements.Element`]) –

    The unique elements connected by this joint, derived from the sub-joints.

  • location (:class:`~compas.geometry.Point`) –

    The approximate location of the joint, the average of the self.joints locations.

  • topology (:class:`~compas_timber.connections.JointTopology`) –

    The topology of the joint, composed from the individual self.joints topologies.

Source code in src/compas_timber/connections/composite_joint.py
def __init__(self, joints: list[Joint], name: str = None, **kwargs):
    self.joints = joints
    kwargs["name"] = name
    if kwargs.get("location") is None:  # given when deserializing, when the sub-joints' elements aren't restored yet
        kwargs["location"] = sum([j.location for j in joints], Point(0, 0, 0)) / len(joints)
    kwargs["topology"] = get_topology_from_joints(joints)
    kwargs["elements"] = tuple(set([e for j in joints for e in j.elements]))
    super(CompositeJoint, self).__init__(**kwargs)

Attributes¤

features property writable ¤
features

Delegates feature calculation to each sub-joint and returns a combined list of features.

Methods:¤

add_extensions ¤
add_extensions()

Delegates extension calculation to each sub-joint.

Source code in src/compas_timber/connections/composite_joint.py
def add_extensions(self):
    """Delegates extension calculation to each sub-joint."""
    for joint in self.joints:
        joint.add_extensions()
add_features ¤
add_features()

Delegates feature calculation to each sub-joint.

Source code in src/compas_timber/connections/composite_joint.py
def add_features(self):
    """Delegates feature calculation to each sub-joint."""
    for joint in self.joints:
        joint.add_features()
clear_extensions ¤
clear_extensions()

Delegates feature removal to each sub-joint.

Source code in src/compas_timber/connections/composite_joint.py
def clear_extensions(self):
    """Delegates feature removal to each sub-joint."""
    for joint in self.joints:
        joint.clear_extensions()
clear_features ¤
clear_features()

Delegates feature removal to each sub-joint.

Source code in src/compas_timber/connections/composite_joint.py
def clear_features(self):
    """Delegates feature removal to each sub-joint."""
    for joint in self.joints:
        joint.clear_features()
restore_elements_from_keys ¤
restore_elements_from_keys(model: TimberModel)

Restores element references by delegating to each sub-joint, then rebuilds elements.

Parameters:

  • model (:class:`~compas_timber.model.TimberModel`) –

    The model from which to look up elements by GUID.

Source code in src/compas_timber/connections/composite_joint.py
def restore_elements_from_keys(self, model: TimberModel):
    """Restores element references by delegating to each sub-joint, then rebuilds elements.

    Parameters
    ----------
    model : :class:`~compas_timber.model.TimberModel`
        The model from which to look up elements by GUID.
    """
    for joint in self.joints:
        joint.restore_elements_from_keys(model)
        joint._set_unset_attributes()
    self._elements = tuple(set([e for j in self.joints for e in j.elements]))
    self._anchor_location()

ConnectionSolver ¤

Provides tools for detecting beam intersections and joint topologies.

Methods:¤

find_intersecting_pairs classmethod ¤
find_intersecting_pairs(beams, rtree=False, max_distance=0.0)

Generates candidate pairs of beams for topology checking.

This method does not test for intersection. When rtree is True, candidates are pairs of beams whose axis-aligned bounding boxes overlap; when False, all possible pairs are returned and the geometry is not consulted.

Parameters:

  • beams (list(:class:`~compas_timber.elements.Beam`)) –

    A list of beam objects.

  • rtree (bool, default: False ) –

    When set to True R-tree will be used to search for neighboring beams.

  • max_distance (float, default: 0.0 ) –

    When rtree is True, beams whose bounding boxes are up to this distance apart are still considered neighboring. Ignored when rtree is False.

Returns:

  • iterable –

    Candidate pairs of beams. A list of sets of two beams when rtree is True, an iterator of all possible pairs as tuples otherwise.

Source code in src/compas_timber/connections/solver.py
@classmethod
def find_intersecting_pairs(cls, beams, rtree=False, max_distance=0.0):
    """Generates candidate pairs of beams for topology checking.

    This method does not test for intersection. When `rtree` is True, candidates are pairs of beams
    whose axis-aligned bounding boxes overlap; when False, all possible pairs are returned and the
    geometry is not consulted.

    Parameters
    ----------
    beams : list(:class:`~compas_timber.elements.Beam`)
        A list of beam objects.
    rtree : bool
        When set to True R-tree will be used to search for neighboring beams.
    max_distance : float, optional
        When `rtree` is True, beams whose bounding boxes are up to this distance apart are still
        considered neighboring. Ignored when `rtree` is False.

    Returns
    -------
    iterable
        Candidate pairs of beams. A list of sets of two beams when `rtree` is True, an iterator of
        all possible pairs as tuples otherwise.

    """
    return find_neighboring_elements(beams, inflate_by=max_distance) if rtree else itertools.combinations(beams, 2)
find_topology ¤
find_topology(beam_a, beam_b, max_distance=None)

If beam_a and beam_b intersect within the given max_distance, return the topology type of the intersection.

If the topology is role-sensitive, the method outputs the beams in a consistent specific order (e.g. main beam first, cross beam second), otherwise, the beams are outputted in the same order as they were inputted.

Parameters:

  • beam_a (:class:`~compas_timber.elements.Beam`) –

    First beam from intersecting pair.

  • beam_b (:class:`~compas_timber.elements.Beam`) –

    Second beam from intersecting pair.

  • max_distance (float, default: None ) –

    Maximum distance, in design units, at which two beams are considered intersecting.

Returns:

  • class:`~compas_timber.connections.BeamSolverResult` –

    The topology results of the intersection between the two beams.

Source code in src/compas_timber/connections/solver.py
def find_topology(self, beam_a, beam_b, max_distance=None):
    """If `beam_a` and `beam_b` intersect within the given `max_distance`, return the topology type of the intersection.

    If the topology is role-sensitive, the method outputs the beams in a consistent specific order
    (e.g. main beam first, cross beam second), otherwise, the beams are outputted in the same
    order as they were inputted.

    Parameters
    ----------
    beam_a : :class:`~compas_timber.elements.Beam`
        First beam from intersecting pair.
    beam_b : :class:`~compas_timber.elements.Beam`
        Second beam from intersecting pair.
    max_distance : float, optional
        Maximum distance, in design units, at which two beams are considered intersecting.

    Returns
    -------
    :class:`~compas_timber.connections.BeamSolverResult`
        The topology results of the intersection between the two beams.
    """
    # first check if the beams are close enough to be considered intersecting and get the closest points on the segments
    max_distance = max_distance or TOL.absolute  # TODO: change to a unit-sensitive value
    dist, point_a, point_b = distance_segment_segment_points(beam_a.centerline, beam_b.centerline)
    if dist > max_distance:
        return BeamSolverResult(JointTopology.TOPO_UNKNOWN, beam_a, beam_b, None, None)
    point_a = Point(*point_a)
    point_b = Point(*point_b)

    # see if beams are parallel
    if TOL.is_zero(angle_vectors(beam_a.centerline.direction, beam_b.centerline.direction) % math.pi):
        # beams are parallel
        # if parallel overlap on beam_a means that beam_b is overlapped by beam_a. Only need to perform the check on beam_a
        overlap_on_a = get_segment_overlap(beam_a.centerline, beam_b.centerline)
        if overlap_on_a is None:
            return BeamSolverResult(JointTopology.TOPO_I, beam_a, beam_b, dist, (point_a + point_b) / 2.0)
        if overlap_on_a[1] < max_distance:  # overlaps on beam_a start
            pt = beam_b.endpoint_closest_to_point(beam_a.centerline.start)[1]
            dist = distance_point_point(pt, beam_a.centerline.start)
            return BeamSolverResult(JointTopology.TOPO_I, beam_a, beam_b, dist, (beam_a.centerline.start + pt) / 2.0)
        if abs(overlap_on_a[0] - beam_a.length) < max_distance:  # overlaps on beam_a end
            pt = beam_b.endpoint_closest_to_point(beam_a.centerline.end)[1]
            dist = distance_point_point(pt, beam_a.centerline.end)
            return BeamSolverResult(JointTopology.TOPO_I, beam_a, beam_b, dist, (beam_a.centerline.end + pt) / 2.0)
        else:
            return BeamSolverResult(JointTopology.TOPO_UNKNOWN, beam_a, beam_b)

    _, a_end_pt = beam_a.endpoint_closest_to_point(point_b)
    _, b_end_pt = beam_b.endpoint_closest_to_point(point_a)

    a_end = distance_point_point(a_end_pt, point_a) < max_distance
    b_end = distance_point_point(b_end_pt, point_b) < max_distance
    location = (point_a + point_b) / 2.0
    if a_end and b_end:
        return BeamSolverResult(JointTopology.TOPO_L, beam_a, beam_b, dist, location)
    if a_end:
        return BeamSolverResult(JointTopology.TOPO_T, beam_a, beam_b, dist, location)
    if b_end:
        return BeamSolverResult(JointTopology.TOPO_T, beam_b, beam_a, dist, location)
    return BeamSolverResult(JointTopology.TOPO_X, beam_a, beam_b, dist, location)

CutPlaneSpec ¤

CutPlaneSpec(local_frame: Frame)

A cutting plane stored in the local coordinate system of a beam.

The plane is kept relative to the beam's :attr:~compas_timber.elements.Beam.frame rather than its blank-derived reference sides, so it is unaffected by blank extensions (e.g. added by other joints) and follows the beam through :meth:~compas_timber.model.TimberModel.transform and serialization.

Use :meth:from_plane (or the validating :meth:from_butt_plane / :meth:from_back_plane) to build an instance from a world-coordinate plane, and :meth:to_plane to resolve it at query time.

Parameters:

  • local_frame (:class:`~compas.geometry.Frame`) –

    The plane, as a frame, in the beam's local coordinates.

Source code in src/compas_timber/connections/butt_joint.py
def __init__(self, local_frame: Frame):
    super().__init__()
    self.local_frame = local_frame

Methods:¤

from_back_plane classmethod ¤
from_back_plane(main_beam: Beam, cross_beam: Beam, plane: Plane) -> CutPlaneSpec

Store plane relative to the main beam, for use as :attr:~compas_timber.connections.LButtJoint.back_plane.

Parameters:

  • main_beam (Beam) –

    Main beam of the joint.

  • cross_beam (Beam) –

    Cross beam of the joint.

  • plane (Plane) –

    Cutting plane in world coordinates. Its normal must be perpendicular to the main beam's centerline.

Source code in src/compas_timber/connections/butt_joint.py
@classmethod
def from_back_plane(cls, main_beam: Beam, cross_beam: Beam, plane: Plane) -> CutPlaneSpec:
    """Store `plane` relative to the main beam, for use as :attr:`~compas_timber.connections.LButtJoint.back_plane`.

    Parameters
    ----------
    main_beam
        Main beam of the joint.
    cross_beam
        Cross beam of the joint.
    plane
        Cutting plane in world coordinates. Its normal must be perpendicular to the main beam's centerline.

    """
    if not TOL.is_zero(dot_vectors(main_beam.frame.xaxis, plane.normal.unitized())):
        raise ValueError("back_plane must be parallel to main_beam's centerline: its normal may not have a component along it.")
    return cls.from_plane(main_beam, plane)
from_butt_plane classmethod ¤
from_butt_plane(main_beam: Beam, cross_beam: Beam, plane: Plane) -> CutPlaneSpec

Store plane relative to the cross beam, for use as :attr:~compas_timber.connections.ButtJoint.butt_plane.

Parameters:

  • main_beam (Beam) –

    Main beam of the joint.

  • cross_beam (Beam) –

    Cross beam of the joint.

  • plane (Plane) –

    Cutting plane in world coordinates. Its normal must be perpendicular to the cross beam's centerline.

Source code in src/compas_timber/connections/butt_joint.py
@classmethod
def from_butt_plane(cls, main_beam: Beam, cross_beam: Beam, plane: Plane) -> CutPlaneSpec:
    """Store `plane` relative to the cross beam, for use as :attr:`~compas_timber.connections.ButtJoint.butt_plane`.

    Parameters
    ----------
    main_beam
        Main beam of the joint.
    cross_beam
        Cross beam of the joint.
    plane
        Cutting plane in world coordinates. Its normal must be perpendicular to the cross beam's centerline.

    """
    if not TOL.is_zero(dot_vectors(cross_beam.frame.xaxis, plane.normal.unitized())):
        raise ValueError("butt_plane must be parallel to cross_beam's centerline: its normal may not have a component along it.")
    return cls.from_plane(cross_beam, plane)
from_plane classmethod ¤
from_plane(beam: Beam, plane: Plane) -> CutPlaneSpec

Store plane (world coordinates) relative to beam. Any orientation is supported.

Source code in src/compas_timber/connections/butt_joint.py
@classmethod
def from_plane(cls, beam: Beam, plane: Plane) -> CutPlaneSpec:
    """Store `plane` (world coordinates) relative to `beam`. Any orientation is supported."""
    return cls(Frame.from_plane(plane).transformed(beam.transformation_to_local()))
to_plane ¤
to_plane(beam: Beam) -> Plane

Resolve the plane in world coordinates relative to beam.

Source code in src/compas_timber/connections/butt_joint.py
def to_plane(self, beam: Beam) -> Plane:
    """Resolve the plane in world coordinates relative to `beam`."""
    return Plane.from_frame(self.local_frame.transformed(beam.modeltransformation))

ISimpleScarf ¤

ISimpleScarf(
    main_beam: Beam | None = None,
    cross_beam: Beam | None = None,
    length: float | None = None,
    depth_ref_side: float | None = None,
    depth_opp_side: float | None = None,
    num_drill_hole: int = 0,
    drill_hole_diam: float = 20.0,
    ref_side_index: int = 0,
    **kwargs,
)

Represents a Simple Scarf joint for two parallel beams (Topology I).

Parameters:

  • main_beam (:class:`~compas_timber.elements.Beam`, default: None ) –

    The first beam of the joint.

  • cross_beam (:class:`~compas_timber.elements.Beam`, default: None ) –

    The second beam of the joint, parallel to the main beam.

  • length (float, default: None ) –

    The length of the scarf overlap along the beam axis. If not provided, defaults to DEFAULT_LENGTH_TO_HEIGHT_RATIO times the beam height.

  • depth_ref_side (float, default: None ) –

    The cut depth on the reference side of the beam. If not provided, defaults to DEFAULT_DEPTH_TO_HEIGHT_RATIO times the beam height.

  • depth_opp_side (float, default: None ) –

    The cut depth on the opposite side of the beam. If not provided, defaults to DEFAULT_DEPTH_TO_HEIGHT_RATIO times the beam height.

  • num_drill_hole (int, default: 0 ) –

    Number of drill holes through the joint (0, 1, or 2). Defaults to 0.

  • drill_hole_diam (float, default: 20.0 ) –

    Diameter of the drill holes in mm. Defaults to 20.0.

  • ref_side_index (int, default: 0 ) –

    Zero-based index of the reference side (RS1–RS6 → 0–5). Defaults to 0.

Attributes:

  • main_beam (:class:`~compas_timber.elements.Beam`) –

    The first beam of the joint (alias for element_a).

  • cross_beam (:class:`~compas_timber.elements.Beam`) –

    The second beam of the joint (alias for element_b).

  • main_beam_ref_side_index (int) –

    Reference side index for the main beam. Equal to ref_side_index.

  • cross_beam_ref_side_index (int) –

    Reference side index for the cross beam, resolved automatically from the face most aligned with the main beam's reference side normal.

  • origin (:class:`~compas.geometry.Point`) –

    The origin point of the joint (alias for location).

  • length (float) –

    The length of the scarf overlap.

  • depth_ref_side (float) –

    The cut depth on the reference side.

  • depth_opp_side (float) –

    The cut depth on the opposite side.

  • num_drill_hole (int) –

    Number of drill holes (0, 1, or 2).

  • drill_hole_diam (float) –

    Diameter of the drill holes in mm.

  • ref_side_index (int) –

    Zero-based index of the reference side used for the main beam.

  • features (list) –

    List of :class:~compas_timber.fabrication.SimpleScarf features added to the beams by this joint.

Source code in src/compas_timber/connections/i_simple_scarf.py
def __init__(
    self,
    main_beam: Optional[Beam] = None,
    cross_beam: Optional[Beam] = None,
    length: Optional[float] = None,
    depth_ref_side: Optional[float] = None,
    depth_opp_side: Optional[float] = None,
    num_drill_hole: int = 0,
    drill_hole_diam: float = 20.0,
    ref_side_index: int = 0,
    **kwargs,
):
    super(ISimpleScarf, self).__init__(elements=(main_beam, cross_beam), **kwargs)
    self.main_beam_guid = kwargs.get("main_beam_guid") or (str(main_beam.guid) if main_beam else None)
    self.cross_beam_guid = kwargs.get("cross_beam_guid") or (str(cross_beam.guid) if cross_beam else None)

    self.length = length
    self.depth_ref_side = depth_ref_side
    self.depth_opp_side = depth_opp_side
    self.num_drill_hole = num_drill_hole
    self.drill_hole_diam = drill_hole_diam
    self.ref_side_index = ref_side_index

    if self.main_beam and self.cross_beam:
        self._set_unset_attributes()

Methods:¤

add_extensions ¤
add_extensions() -> None

Calculates and adds the required blank extensions to both beams.

Source code in src/compas_timber/connections/i_simple_scarf.py
def add_extensions(self) -> None:
    """Calculates and adds the required blank extensions to both beams."""
    assert self.main_beam and self.cross_beam

    try:
        _, main_extension_frame = self.extension_plane(self.main_beam)
        main_extension_frame.translate(main_extension_frame.normal * self.length / 2)

        _, cross_extension_frame = self.extension_plane(self.cross_beam)
        cross_extension_frame.translate(cross_extension_frame.normal * self.length / 2)

        start_a, end_a = self.main_beam.extension_to_plane(Plane.from_frame(main_extension_frame))
        start_b, end_b = self.cross_beam.extension_to_plane(Plane.from_frame(cross_extension_frame))

    except AttributeError as ae:
        raise BeamJoiningError(self.elements, self, debug_info=str(ae), debug_geometries=[main_extension_frame])
    except Exception as ex:
        raise BeamJoiningError(self.elements, self, debug_info=str(ex))

    self.main_beam.add_blank_extension(start_a, end_a, self.guid)
    self.cross_beam.add_blank_extension(start_b, end_b, self.guid)
add_features ¤
add_features() -> None

Generates and assigns the SimpleScarf fabrication features.

Raises:

Source code in src/compas_timber/connections/i_simple_scarf.py
def add_features(self) -> None:
    """Generates and assigns the SimpleScarf fabrication features.

    Raises
    ------
    AssertionError
        If either beam is not set.
    """
    assert self.main_beam and self.cross_beam

    main_feature = SimpleScarf.from_beam_and_side(
        self.main_beam,
        side=self._get_beam_side(self.main_beam),
        length=self.length,
        depth_ref_side=self.depth_ref_side,
        depth_opp_side=self.depth_opp_side,
        num_drill_hole=self.num_drill_hole,
        drill_hole_diam=self.drill_hole_diam,
        ref_side_index=self.ref_side_index,
    )

    cross_feature = SimpleScarf.from_beam_and_side(
        self.cross_beam,
        side=self._get_beam_side(self.cross_beam),
        length=self.length,
        depth_ref_side=self.depth_opp_side,
        depth_opp_side=self.depth_ref_side,
        num_drill_hole=self.num_drill_hole,
        drill_hole_diam=self.drill_hole_diam,
        ref_side_index=self.cross_beam_ref_side_index,
    )

    self.main_beam.add_features(main_feature)
    self.cross_beam.add_features(cross_feature)
    self.features.extend([main_feature, cross_feature])
check_elements_compatibility classmethod ¤
check_elements_compatibility(elements: list[Beam], raise_error: bool = False) -> bool

Checks if the two beams are parallel, as required for an ISimpleScarf joint.

Parameters:

  • elements (list(:class:`~compas_timber.elements.Beam`)) –

    The two beams to check.

  • raise_error (bool, default: False ) –

    If True, raise a :class:~compas_timber.errors.BeamJoiningError instead of returning False. Defaults to False.

Returns:

  • bool –

    True if the beams are parallel, False otherwise (when raise_error is False).

Raises:

  • :class:`~compas_timber.errors.BeamJoiningError` –

    If the beams are not parallel and raise_error is True.

Source code in src/compas_timber/connections/i_simple_scarf.py
@classmethod
def check_elements_compatibility(cls, elements: List[Beam], raise_error: bool = False) -> bool:
    """Checks if the two beams are parallel, as required for an ISimpleScarf joint.

    Parameters
    ----------
    elements : list(:class:`~compas_timber.elements.Beam`)
        The two beams to check.
    raise_error : bool, optional
        If ``True``, raise a :class:`~compas_timber.errors.BeamJoiningError`
        instead of returning ``False``. Defaults to ``False``.

    Returns
    -------
    bool
        ``True`` if the beams are parallel, ``False`` otherwise
        (when ``raise_error`` is ``False``).

    Raises
    ------
    :class:`~compas_timber.errors.BeamJoiningError`
        If the beams are not parallel and ``raise_error`` is ``True``.
    """
    dot = abs(elements[0].centerline.direction.dot(elements[1].centerline.direction))
    if not TOL.is_close(dot, 1):
        if not raise_error:
            return False
        raise BeamJoiningError(elements, cls, debug_info="The two beams are not parallel to create a Simple Scarf joint.")
    return True
extension_plane ¤
extension_plane(beam: Beam) -> tuple[int, Frame]

Returns the end-face index and frame used to compute the blank extension for a beam.

The end face is RS5 (index 4) when the scarf sits at the beam start, or RS6 (index 5) when it sits at the beam end.

Parameters:

  • beam (:class:`~compas_timber.elements.Beam`) –

    The beam for which to determine the extension plane.

Returns:

  • tuple(int, :class:`~compas.geometry.Frame`) –

    A tuple of (ext_side_index, frame) where ext_side_index is the zero-based index of the end face (4 or 5) and frame is the corresponding reference-side frame.

Source code in src/compas_timber/connections/i_simple_scarf.py
def extension_plane(self, beam: Beam) -> Tuple[int, Frame]:
    """Returns the end-face index and frame used to compute the blank extension for a beam.

    The end face is RS5 (index 4) when the scarf sits at the beam start,
    or RS6 (index 5) when it sits at the beam end.

    Parameters
    ----------
    beam : :class:`~compas_timber.elements.Beam`
        The beam for which to determine the extension plane.

    Returns
    -------
    tuple(int, :class:`~compas.geometry.Frame`)
        A tuple of ``(ext_side_index, frame)`` where ``ext_side_index`` is
        the zero-based index of the end face (4 or 5) and ``frame`` is the
        corresponding reference-side frame.
    """
    ext_side_index = 4 if self._get_beam_side(beam) == "start" else 5
    return ext_side_index, beam.ref_sides[ext_side_index]

Joint ¤

Joint(
    elements=None, topology=None, location=None, name=None, element_guids=None, **kwargs
)

Base class for a joint connecting two beams.

This is a base class and should not be instantiated directly. Use the create() class method of the respective implementation of Joint instead.

Parameters:

  • topology (literal, one of :class:`JointTopology`, default: None ) –

    The topology by which the two elements connected with this joint interact.

  • location (:class:`~compas.geometry.Point`, default: None ) –

    The estimated location of the interaction point of the two elements connected with this joint.

Attributes:

  • name (str) –

    The name of the joint. Corresponds to the class name.

  • elements (tuple(:class:`~compas_model.elements.Element`)) –

    The elements joined by this joint.

  • generated_elements (list(:class:`~compas_model.elements.Element`)) –

    A list of elements that were generated by this joint.

  • ends (dict(str, str)) –

    Maps the GUID of each element to start or end, depending on which end of the element is connected by this joint.

  • interactions (list(tuple(:class:`~compas_model.elements.Element`, :class:`~compas_model.elements.Element`))) –

    A list of tuples containing the elements that are interacting with each other through this joint.

  • features (list(:class:`~compas_timber.fabrication.BTLxProcessing`)) –

    A list of features that were added to the elements by this joint.

  • topology (literal, one of :class:`JointTopology`) –

    The topology by which the elements connected with this joint interact.

  • location (:class:`~compas.geometry.Point`) –

    The estimated location of the interaction point of the two elements connected with this joint.

Source code in src/compas_timber/connections/joint.py
def __init__(self, elements=None, topology=None, location=None, name=None, element_guids=None, **kwargs):
    super().__init__(name=name)
    # filter out Nones — subclasses pass e.g. elements=(None, None) during deserialization
    elements = tuple(e for e in (elements or ()) if e is not None)

    if elements:
        # Normal creation: elements are live objects, derive guids from them
        self._elements = elements
        self.element_guids = tuple(str(e.guid) for e in elements)
    elif element_guids:
        # Deserialization: only guids available, elements are restored later
        # by TimberModel.__from_data__() via restore_elements_from_keys()
        self._elements = ()
        self.element_guids = tuple(element_guids)
    else:
        raise ValueError("Joint requires either elements or element_guids.")

    self.features = []
    self._topology = topology if topology is not None else JointTopology.TOPO_UNKNOWN
    # an explicit location is kept relative to element_a (see `location`); `_location` holds it in world
    # coordinates only until element_a is available, e.g. during deserialization
    self._location = location
    self._local_location = None
    self._anchor_location()

Attributes¤

location property writable ¤
location

The location of the joint in model coordinates.

An explicitly set location is stored relative to element_a, so it follows the elements when they move. Otherwise it is derived from the centerlines of the two elements.

Methods:¤

add_extensions ¤
add_extensions()

Adds the extensions defined by this joint to affected beam(s). This is optional and should only be implemented by joints that require it.

Notes

Extensions are added to all beams before the features are added.

Raises:

  • :class:`~compas_timber.connections.BeamJoiningError` –

    Should be raised whenever the joint was not able to calculate the extensions to be applied to the beams.

Source code in src/compas_timber/connections/joint.py
def add_extensions(self):
    """Adds the extensions defined by this joint to affected beam(s).
    This is optional and should only be implemented by joints that require it.

    Notes
    -----
    Extensions are added to all beams before the features are added.

    Raises
    ------
    :class:`~compas_timber.connections.BeamJoiningError`
        Should be raised whenever the joint was not able to calculate the extensions to be applied to the beams.

    """
    pass
add_features ¤
add_features()

Adds the features defined by this joint to affected beam(s).

Raises:

  • :class:`~compas_timber.connections.BeamJoiningError` –

    Should be raised whenever the joint was not able to calculate the features to be applied to the beams.

Source code in src/compas_timber/connections/joint.py
def add_features(self):
    """Adds the features defined by this joint to affected beam(s).

    Raises
    ------
    :class:`~compas_timber.connections.BeamJoiningError`
        Should be raised whenever the joint was not able to calculate the features to be applied to the beams.

    """
    raise NotImplementedError
check_elements_compatibility classmethod ¤
check_elements_compatibility(elements, raise_error=False)

Checks if the cluster of beams complies with the requirements for the Joint.

Parameters:

  • elements (list of :class:`~compas_timber.elements.Beam`) –

    The beams to check.

  • raise_error (bool, default: False ) –

    If True, raises a BeamJoiningError if the requirements are not met.

Returns:

  • bool –

    True if the cluster complies with the requirements, False otherwise.

Source code in src/compas_timber/connections/joint.py
@classmethod
def check_elements_compatibility(cls, elements, raise_error=False):
    """Checks if the cluster of beams complies with the requirements for the Joint.

    Parameters
    ----------
    elements : list of :class:`~compas_timber.elements.Beam`
        The beams to check.
    raise_error : bool, optional
        If True, raises a `BeamJoiningError` if the requirements are not met.

    Returns
    -------
    bool
        True if the cluster complies with the requirements, False otherwise.

    """
    return True
clear_extensions ¤
clear_extensions()

Removes the extensions defined by this joint from affected element(s).

Source code in src/compas_timber/connections/joint.py
def clear_extensions(self):
    """Removes the extensions defined by this joint from affected element(s)."""
    for e in self.elements:
        e.remove_blank_extension(self.guid)
clear_features ¤
clear_features()

Removes the features defined by this joint from affected element(s).

Source code in src/compas_timber/connections/joint.py
def clear_features(self):
    """Removes the features defined by this joint from affected element(s)."""
    if self.features:
        for e in self.elements:
            e.remove_features(self.features)
    self.features = []
create classmethod ¤
create(model, *elements, **kwargs)

Creates an instance of this joint and creates the new connection in model.

elements are expected to have been added to model before calling this method.

This code does not verify that the given elements are adjacent and/or lie in a topology which allows connecting them. This is the responsibility of the calling code.

Parameters:

  • model (:class:`~compas_timber.model.TimberModel`) –

    The model to which the elements and this joint belong.

  • *elements (:class:`~compas_model.elements.Element`, default: () ) –

    The elements to be connected by this joint. The number of elements must comply with the Joint class's MIN_ELEMENT_COUNT and MAX_ELEMENT_COUNT attributes.

  • **kwargs (dict, default: {} ) –

    Additional keyword arguments that are passed to the joint's constructor.

Returns:

  • class:`compas_timber.connections.Joint` –

    The instance of the created joint.

Source code in src/compas_timber/connections/joint.py
@classmethod
def create(cls, model, *elements, **kwargs):
    """Creates an instance of this joint and creates the new connection in `model`.

    `elements` are expected to have been added to `model` before calling this method.

    This code does not verify that the given elements are adjacent and/or lie in a topology which allows connecting
    them. This is the responsibility of the calling code.

    Parameters
    ----------
    model : :class:`~compas_timber.model.TimberModel`
        The model to which the elements and this joint belong.
    *elements : :class:`~compas_model.elements.Element`
        The elements to be connected by this joint. The number of elements must comply with the `Joint` class's
        `MIN_ELEMENT_COUNT` and `MAX_ELEMENT_COUNT` attributes.
    **kwargs : dict
        Additional keyword arguments that are passed to the joint's constructor.

    Returns
    -------
    :class:`compas_timber.connections.Joint`
        The instance of the created joint.

    """

    joint = cls(*elements, **kwargs)
    model.add_joint(joint)
    return joint
element_count_complies classmethod ¤
element_count_complies(elements)

Checks if the number of elements complies with the joint's requirements.

Parameters:

  • elements (list(:class:`~compas_model.elements.Element`)) –

    The elements to be checked.

Returns:

Source code in src/compas_timber/connections/joint.py
@classmethod
def element_count_complies(cls, elements):
    """Checks if the number of elements complies with the joint's requirements.

    Parameters
    ----------
    elements : list(:class:`~compas_model.elements.Element`)
        The elements to be checked.

    Returns
    -------
    bool

    """
    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
get_beam_direction_towards_joint ¤
get_beam_direction_towards_joint(beam: Beam) -> Vector

Returns the direction of the beam towards the joint.

This is used to determine the orientation of the beam for feature calculation.

Parameters:

  • beam (:class:`~compas_timber.elements.Beam`) –

    The beam for which to calculate the direction towards the joint.

Returns:

  • class:`~compas.geometry.Vector` –

    The direction of the beam towards the joint.

Source code in src/compas_timber/connections/joint.py
def get_beam_direction_towards_joint(self, beam: Beam) -> Vector:
    """Returns the direction of the beam towards the joint.

    This is used to determine the orientation of the beam for feature calculation.

    Parameters
    ----------
    beam : :class:`~compas_timber.elements.Beam`
        The beam for which to calculate the direction towards the joint.

    Returns
    -------
    :class:`~compas.geometry.Vector`
        The direction of the beam towards the joint.

    """
    end, _ = beam.endpoint_closest_to_point(self.location)
    if end == "start":
        beam_direction = beam.centerline.vector
    else:
        beam_direction = beam.centerline.vector * -1
    beam_direction.unitize()
    return beam_direction
promote_cluster classmethod ¤
promote_cluster(model, cluster, reordered_elements=None, **kwargs)

Creates an instance of this joint from a cluster of elements.

Parameters:

  • model (:class:`~compas_timber.model.TimberModel`) –

    The model to which the elements and this joint belong.

  • cluster (:class:`~compas_model.clusters.Cluster`) –

    The cluster containing the elements to be connected by this joint.

  • reordered_elements (list(:class:`~compas_model.elements.Element`), default: None ) –

    The elements to be connected by this joint. If not provided, the elements of the cluster will be used. This is used to explicitly define the element order.

  • **kwargs (dict, default: {} ) –

    Additional keyword arguments that are passed to the joint's constructor.

Returns:

  • class:`compas_timber.connections.Joint` –

    The instance of the created joint.

Source code in src/compas_timber/connections/joint.py
@classmethod
def promote_cluster(cls, model, cluster, reordered_elements=None, **kwargs):
    """Creates an instance of this joint from a cluster of elements.

    Parameters
    ----------
    model : :class:`~compas_timber.model.TimberModel`
        The model to which the elements and this joint belong.
    cluster : :class:`~compas_model.clusters.Cluster`
        The cluster containing the elements to be connected by this joint.
    reordered_elements : list(:class:`~compas_model.elements.Element`), optional
        The elements to be connected by this joint. If not provided, the elements of the cluster will be used.
        This is used to explicitly define the element order.
    **kwargs : dict
        Additional keyword arguments that are passed to the joint's constructor.

    Returns
    -------
    :class:`compas_timber.connections.Joint`
        The instance of the created joint.

    """
    if reordered_elements:
        if set(reordered_elements) != cluster.elements:
            raise BeamJoiningError(
                beams=reordered_elements,
                joint=cls,
                debug_info="Elements of the joint candidate must match the provided elements.",
                debug_geometries=[e.blank for e in reordered_elements],
            )
    if len(cluster.joints) == 1:
        elements = reordered_elements or cluster.joints[0].elements
        return cls.promote_joint_candidate(model, cluster.joints[0], reordered_elements=elements, **kwargs)
    else:
        elements = reordered_elements or list(cluster.elements)
    return cls.create(model, *elements, **kwargs)
promote_joint_candidate classmethod ¤
promote_joint_candidate(model, candidate, reordered_elements=None, **kwargs)

Creates an instance of this joint from a joint candidate.

Parameters:

  • model (:class:`~compas_timber.model.TimberModel`) –

    The model to which the elements and this joint belong.

  • candidate (:class:`~compas_timber.connections.JointCandidate`) –

    The joint candidate to be converted.

  • reordered_elements (list(:class:`~compas_model.elements.Element`), default: None ) –

    The elements to be connected by this joint. If not provided, the elements of the generic joint will be used. This is used to explicitly define the element order.

  • **kwargs (dict, default: {} ) –

    Additional keyword arguments that are passed to the joint's constructor.

Returns:

  • class:`compas_timber.connections.Joint` –

    The instance of the created joint.

Source code in src/compas_timber/connections/joint.py
@classmethod
def promote_joint_candidate(cls, model, candidate, reordered_elements=None, **kwargs):
    """Creates an instance of this joint from a joint candidate.

    Parameters
    ----------
    model : :class:`~compas_timber.model.TimberModel`
        The model to which the elements and this joint belong.
    candidate : :class:`~compas_timber.connections.JointCandidate`
        The joint candidate to be converted.
    reordered_elements : list(:class:`~compas_model.elements.Element`), optional
        The elements to be connected by this joint. If not provided, the elements of the generic joint will be used.
        This is used to explicitly define the element order.
    **kwargs : dict
        Additional keyword arguments that are passed to the joint's constructor.

    Returns
    -------
    :class:`compas_timber.connections.Joint`
        The instance of the created joint.

    """
    if reordered_elements:
        assert set(reordered_elements) == set(candidate.elements), "Elements of the generic joint must match the provided elements."
        elements = reordered_elements
    else:
        elements = candidate.elements
    kwargs.update({"topology": candidate.topology, "location": candidate.location})  # pass topology, distance and location from candidate
    joint = cls.create(model, *elements, **kwargs)
    return joint
reset_location ¤
reset_location()

Clear an explicitly set location so that it is derived from the beam centerlines on next access.

Source code in src/compas_timber/connections/joint.py
def reset_location(self):
    """Clear an explicitly set location so that it is derived from the beam centerlines on next access."""
    self._location = None
    self._local_location = None
restore_elements_from_keys ¤
restore_elements_from_keys(model)

Restores the reference to the elements associated with this joint.

During serialization, :class:compas_timber.elements.Beam objects are serialized by :class:compas_timber.model.Model. To avoid circular references, Joint only stores the keys of the respective elements.

This method is called by :class:compas_timber.model.TimberModel during de-serialization to restore the references. Since the roles of the elements are joint specific (e.g. main/cross beam) this method should be implemented by the concrete implementation.

Examples:

See :class:compas_timber.connections.TButtJoint.

Source code in src/compas_timber/connections/joint.py
def restore_elements_from_keys(self, model):
    """Restores the reference to the elements associated with this joint.

    During serialization, :class:`compas_timber.elements.Beam` objects
    are serialized by :class:`compas_timber.model.Model`. To avoid circular references, Joint only stores the keys
    of the respective elements.

    This method is called by :class:`compas_timber.model.TimberModel` during de-serialization to restore the references.
    Since the roles of the elements are joint specific (e.g. main/cross beam) this method should be implemented by
    the concrete implementation.

    Examples
    --------
    See :class:`compas_timber.connections.TButtJoint`.

    """
    self._elements = tuple(model[guid] for guid in self.element_guids)
    self._anchor_location()
    self._set_unset_attributes()

JointCandidate ¤

JointCandidate(
    element_a=None,
    element_b=None,
    topology=None,
    location=None,
    distance=None,
    name=None,
    element_guids=None,
    **kwargs,
)

A JointCandidate is an information-only joint, which does not add any features to the elements it connects.

It is used to create a first-pass joinery information which can be later grouped into a Clusters and then promoted to concrete joints.

Please use JointCandidate.create() to properly create an instance of this class and associate it with a model.

Parameters:

  • element_a (:class:`~compas_timber.elements.TimberElement`, default: None ) –

    First element to be joined.

  • element_b (:class:`~compas_timber.elements.TimberElement`, default: None ) –

    Second element to be joined.

  • topology (literal, one of :class:`JointTopology`, default: None ) –

    The topology by which the two elements interact. Defaults to JointTopology.TOPO_UNKNOWN.

  • location (:class:`~compas.geometry.Point`, default: None ) –

    The estimated location of the interaction point of the two elements. If not provided, it is calculated from the elements' centerlines on first access.

  • distance (float | None, default: None ) –

    Distance between the elements.

  • name (str, default: None ) –

    The name of the candidate.

  • element_guids (tuple(str, str), default: None ) –

    GUIDs of the two elements, used during deserialization when the live elements aren't available yet.

  • **kwargs (dict, default: {} ) –

    Any additional attributes are set directly on the instance and survive serialization.

Attributes:

  • element_a (:class:`~compas_timber.elements.TimberElement`) –

    First element to be joined.

  • element_b (:class:`~compas_timber.elements.TimberElement`) –

    Second element to be joined.

  • elements (tuple(:class:`~compas_model.elements.Element`)) –

    The elements joined by this candidate.

  • interactions (list(tuple(:class:`~compas_model.elements.Element`, :class:`~compas_model.elements.Element`))) –

    The element pairs this candidate connects.

  • topology (literal, one of :class:`JointTopology`) –

    The topology by which the two elements interact.

  • location (:class:`~compas.geometry.Point`) –

    The estimated location of the interaction point of the two elements.

  • distance (float | None) –

    Distance between the elements.

Source code in src/compas_timber/connections/joint_candidate.py
def __init__(
    self,
    element_a=None,
    element_b=None,
    topology=None,
    location=None,
    distance=None,
    name=None,
    element_guids=None,
    **kwargs,
):
    super(JointCandidate, self).__init__(name=name)
    elements = tuple(e for e in (element_a, element_b) if e is not None)
    if elements:
        self._elements = elements
        self.element_guids = tuple(str(e.guid) for e in elements)
    elif element_guids:
        self._elements = ()
        self.element_guids = tuple(element_guids)
    else:
        raise ValueError("JointCandidate requires either elements or element_guids.")

    self.topology = topology
    self._location = location
    self.distance = distance
    self._extra_kwargs = dict(kwargs)
    for key, value in kwargs.items():
        setattr(self, key, value)

Methods:¤

create classmethod ¤
create(model, *elements, **kwargs)

Creates an instance of this candidate and creates the new connection in model.

Parameters:

  • model (:class:`~compas_timber.model.TimberModel`) –

    The model to which the elements and this candidate belong.

  • *elements (:class:`~compas_model.elements.Element`, default: () ) –

    The elements to be connected by this candidate.

  • **kwargs (dict, default: {} ) –

    Additional keyword arguments that are passed to the candidate's constructor.

Returns:

  • class:`compas_timber.connections.JointCandidate` –

    The instance of the created candidate.

Source code in src/compas_timber/connections/joint_candidate.py
@classmethod
def create(cls, model, *elements, **kwargs):
    """Creates an instance of this candidate and creates the new connection in `model`.

    Parameters
    ----------
    model : :class:`~compas_timber.model.TimberModel`
        The model to which the elements and this candidate belong.
    *elements : :class:`~compas_model.elements.Element`
        The elements to be connected by this candidate.
    **kwargs : dict
        Additional keyword arguments that are passed to the candidate's constructor.

    Returns
    -------
    :class:`compas_timber.connections.JointCandidate`
        The instance of the created candidate.

    """
    candidate = cls(*elements, **kwargs)
    model.add_joint_candidate(candidate)
    return candidate
reset_location ¤
reset_location()

Reset cached joint.location value to None so that it will be recalculated from the beam centerlines on next access.

Source code in src/compas_timber/connections/joint_candidate.py
def reset_location(self):
    """Reset cached joint.location value to None so that it will be recalculated from the beam centerlines on next access."""
    self._location = None
restore_elements_from_keys ¤
restore_elements_from_keys(model)

Restores the reference to the elements associated with this candidate.

This method is called by :class:compas_timber.model.TimberModel during de-serialization to restore the references.

Source code in src/compas_timber/connections/joint_candidate.py
def restore_elements_from_keys(self, model):
    """Restores the reference to the elements associated with this candidate.

    This method is called by :class:`compas_timber.model.TimberModel` during de-serialization to restore the
    references.

    """
    self._elements = tuple(model[guid] for guid in self.element_guids)

JointTopology ¤

Enumeration of the possible joint topologies.

Attributes:

  • TOPO_UNKNOWN –
  • TOPO_I - end-to-end joint between two parallel beams –
  • TOPO_L - end-to-end joint between two non-parallel beams –
  • TOPO_T - end-to-middle joint between two beams –
  • TOPO_X - middle-to-middle joint between two beams –
  • TOPO_Y - joint between three or more beams where all beams meet at their ends –
  • TOPO_K - joint between three or more beams where at least one beam meet in the middle –
  • TOPO_EDGE_EDGE - joint between two plates where the edges of both plates are aligned –
  • TOPO_EDGE_FACE - joint between two plates where one plate is aligned with the face of the other –

Methods:¤

get_name classmethod ¤
get_name(value)

Returns the string representation of given topology value.

For use in logging.

Parameters:

  • value (int) –

    One of [JointTopology.TOPO_I, JointTopology.TOPO_L, JointTopology.TOPO_T, JointTopology.TOPO_X, JointTopology.TOPO_Y, JointTopology.TOPO_K, JointTopology.TOPO_EDGE_EDGE, JointTopology.TOPO_EDGE_FACE, JointTopology.TOPO_UNKNOWN]

Returns:

  • str –

    One of ["TOPO_I", "TOPO_L", "TOPO_T", "TOPO_X", "TOPO_Y", "TOPO_K", "TOPO_EDGE_EDGE", "TOPO_EDGE_FACE", "TOPO_UNKNOWN"]

Source code in src/compas_timber/connections/solver.py
@classmethod
def get_name(cls, value):
    """Returns the string representation of given topology value.

    For use in logging.

    Parameters
    ----------
    value : int
        One of [JointTopology.TOPO_I, JointTopology.TOPO_L, JointTopology.TOPO_T, JointTopology.TOPO_X, JointTopology.TOPO_Y,
        JointTopology.TOPO_K, JointTopology.TOPO_EDGE_EDGE, JointTopology.TOPO_EDGE_FACE, JointTopology.TOPO_UNKNOWN]

    Returns
    -------
    str
        One of ["TOPO_I", "TOPO_L", "TOPO_T", "TOPO_X", "TOPO_Y", "TOPO_K", "TOPO_EDGE_EDGE", "TOPO_EDGE_FACE", "TOPO_UNKNOWN"]

    """
    try:
        return {v: k for k, v in JointTopology.__dict__.items() if k.startswith("TOPO_")}[value]
    except KeyError:
        return "TOPO_UNKNOWN"

LButtJoint ¤

LButtJoint(
    main_beam=None,
    cross_beam=None,
    mill_depth=None,
    modify_cross=True,
    reject_i=False,
    butt_plane_spec: CutPlaneSpec | None = None,
    back_plane_spec: CutPlaneSpec | None = None,
    **kwargs,
)

Represents an L-Butt type joint which joins two beam in their ends, trimming the main beam.

This joint type is compatible with beams in L topology.

Please use LButtJoint.create() to properly create an instance of this class and associate it with a model.

Parameters:

  • main_beam (:class:`~compas_timber.elements.Beam`, default: None ) –

    The main beam to be joined.

  • cross_beam (:class:`~compas_timber.elements.Beam`, default: None ) –

    The cross beam to be joined.

  • mill_depth (float, default: None ) –

    The depth of the pocket to be milled in the cross beam. This will be ignored if butt_plane_spec is set.

  • small_beam_butts (bool, default: False ) –

    If True, the beam with the smaller cross-section will be trimmed. Otherwise, the main beam will be trimmed.

  • modify_cross (bool, default: True ) –

    If True, the cross beam will be extended to the opposite face of the main beam and cut with the same plane.

  • butt_plane_spec (:class:`~compas_timber.connections.CutPlaneSpec`, default: None ) –

    Overrides the plane used to cut the main beam. Build with :meth:~compas_timber.connections.CutPlaneSpec.from_butt_plane.

  • back_plane_spec (:class:`~compas_timber.connections.CutPlaneSpec`, default: None ) –

    Overrides the plane used to cut the cross beam when modify_cross is True. Build with :meth:~compas_timber.connections.CutPlaneSpec.from_back_plane.

  • reject_i (bool, default: False ) –

    If True, the joint will reject beams in I topology.

Attributes:

  • main_beam (:class:`~compas_timber.elements.Beam`) –

    The main beam to be joined.

  • cross_beam (:class:`~compas_timber.elements.Beam`) –

    The cross beam to be joined.

  • mill_depth (float) –

    The depth of the pocket to be milled in the cross beam.

  • modify_cross (bool, default True) –

    If True, the cross beam will be extended to the opposite face of the main beam and cut with the same plane.

  • butt_plane (:class:`~compas.geometry.Plane`) –

    The plane used to cut the main beam. If not overridden, the closest side of the cross beam will be used.

  • back_plane (:class:`~compas.geometry.Plane`) –

    The plane used to cut the cross beam. If not overridden, the back side of the main beam will be used.

  • reject_i (bool, default False) –

    If True, the joint will reject beams in I topology.

Source code in src/compas_timber/connections/l_butt.py
def __init__(
    self,
    main_beam=None,
    cross_beam=None,
    mill_depth=None,
    modify_cross=True,
    reject_i=False,
    butt_plane_spec: Optional[CutPlaneSpec] = None,
    back_plane_spec: Optional[CutPlaneSpec] = None,
    **kwargs,
):
    super(LButtJoint, self).__init__(main_beam=main_beam, cross_beam=cross_beam, mill_depth=mill_depth, modify_cross=modify_cross, butt_plane_spec=butt_plane_spec, **kwargs)
    self.modify_cross = modify_cross
    self.reject_i = reject_i
    self._back_plane_spec: Optional[CutPlaneSpec] = back_plane_spec

Attributes¤

back_plane property ¤
back_plane: Plane

The plane used to cut the cross beam when modify_cross is True.

If a :class:~compas_timber.connections.CutPlaneSpec override is set, it is resolved against the main beam. Otherwise defaults to the same plane as :meth:ButtJoint._back_cutting_plane.

Methods:¤

add_extensions ¤
add_extensions()

Calculates and adds the necessary extensions to the beams.

Raises:

Source code in src/compas_timber/connections/l_butt.py
def add_extensions(self):
    """Calculates and adds the necessary extensions to the beams.

    Raises
    ------
    BeamJoiningError
        If the extension could not be calculated.
    """
    super(LButtJoint, self).add_extensions()

    if self.modify_cross:
        assert self.main_beam and self.cross_beam
        try:
            start, end = self.cross_beam.extension_to_plane(self.back_plane)
            extension_tolerance = 0
            joint_id = self.guid
            self.cross_beam.add_blank_extension(start + extension_tolerance, end + extension_tolerance, joint_id)
        except AttributeError as ae:
            raise BeamJoiningError(beams=self.elements, joint=self, debug_info=str(ae), debug_geometries=[self.back_plane])
add_features ¤
add_features()

Removes this joint's previously generated features and adds new features to each beam.

Source code in src/compas_timber/connections/l_butt.py
def add_features(self):
    """Removes this joint's previously generated features and adds new features to each beam."""
    super(LButtJoint, self).add_features()

    if self.modify_cross:
        cross_refinement_feature = JackRafterCutProxy.from_plane_and_beam(self.back_plane, self.cross_beam, self.cross_beam_ref_side_index)
        self.cross_beam.add_features(cross_refinement_feature)
        self.features.append(cross_refinement_feature)

LFrenchRidgeLapJoint ¤

LFrenchRidgeLapJoint(
    beam_a=None, beam_b=None, flip_lap_side=False, drillhole_diam=None, **kwargs
)

Represents an L-FrenchRidgeLap type joint which joins two beams at their ends, by lapping them with a ridge. The joint can only be created between two beams that are aligned and have the same dimensions.

This joint type is compatible with beams in L topology.

Please use LFrenchRidgeLapJoint.create() to properly create an instance of this class and associate it with a model.

Parameters:

  • beam_a (:class:`~compas_timber.elements.Beam`, default: None ) –

    The first beam to be joined.

  • beam_b (:class:`~compas_timber.elements.Beam`, default: None ) –

    The second beam to be joined.

  • flip_lap_side (bool, default: False ) –

    If True, the lap is flipped to the other side of the beams.

  • drillhole_diam (float, default: None ) –

    Diameter of the drill hole to be made in the joint.

Attributes:

  • beam_a (:class:`~compas_timber.elements.Beam`) –

    The first beam to be joined.

  • beam_b (:class:`~compas_timber.elements.Beam`) –

    The second beam to be joined.

  • flip_lap_side (bool) –

    If True, the lap is flipped to the other side of the beams.

  • drillhole_diam (float) –

    Diameter of the drill hole to be made in the joint.

Source code in src/compas_timber/connections/l_french_ridge_lap.py
def __init__(self, beam_a=None, beam_b=None, flip_lap_side=False, drillhole_diam=None, **kwargs):
    super(LFrenchRidgeLapJoint, self).__init__(beam_a, beam_b, flip_lap_side, **kwargs)
    self.drillhole_diam = drillhole_diam

Methods:¤

add_extensions ¤
add_extensions()

Calculates and adds the necessary extensions to the beams.

This method is called during TimberModel.process_joinery(), not when the joint is created.

Raises:

Source code in src/compas_timber/connections/l_french_ridge_lap.py
def add_extensions(self):
    """Calculates and adds the necessary extensions to the beams.

    This method is called during `TimberModel.process_joinery()`, not when the joint is created.

    Raises
    ------
    BeamJoiningError
        If the extension could not be calculated.

    """
    assert self.beam_a and self.beam_b

    start_a, start_b = None, None
    try:
        start_a, end_a = self.beam_a.extension_to_plane(self.cutting_plane_a)
        start_b, end_b = self.beam_b.extension_to_plane(self.cutting_plane_b)
    except AttributeError as ae:
        # I want here just the plane that caused the error
        geometries = [self.cutting_plane_b] if start_a is not None else [self.cutting_plane_a]
        raise BeamJoiningError(self.elements, self, debug_info=str(ae), debug_geometries=geometries)
    except Exception as ex:
        raise BeamJoiningError(self.elements, self, debug_info=str(ex))
    self.beam_a.add_blank_extension(start_a, end_a, self.guid)
    self.beam_b.add_blank_extension(start_b, end_b, self.guid)
add_features ¤
add_features()

Adds the necessary features to the beams.

This method is called during TimberModel.process_joinery(), not when the joint is created. It is executed after the beam extensions have been added via Joint.add_extensions().

Source code in src/compas_timber/connections/l_french_ridge_lap.py
def add_features(self):
    """Adds the necessary features to the beams.

    This method is called during `TimberModel.process_joinery()`, not when the joint is created.
    It is executed after the beam extensions have been added via `Joint.add_extensions()`.

    """
    assert self.beam_a and self.beam_b

    frl_feature_a = FrenchRidgeLap.from_beam_beam_and_plane(self.beam_a, self.beam_b, self.cutting_plane_a, self.drillhole_diam, self.ref_side_index_a)
    frl_feature_b = FrenchRidgeLap.from_beam_beam_and_plane(self.beam_b, self.beam_a, self.cutting_plane_b, self.drillhole_diam, self.ref_side_index_b)
    # store the features to the beams
    self.beam_a.add_features(frl_feature_a)
    self.beam_b.add_features(frl_feature_b)
    # register the features in the joint
    self.features = [frl_feature_a, frl_feature_b]
check_elements_compatibility classmethod ¤
check_elements_compatibility(elements, raise_error=False)

Checks if the cluster of beams complies with the requirements for the LFrenchRidgeLapJoint.

Parameters:

  • elements (list(:class:`~compas_model.elements.Beam`)) –

    The elements to be checked.

  • raise_error (bool, default: False ) –

    If True, raises a :class:~compas_timber.errors.BeamJoiningError if the cluster does not comply with the requirements. If False, returns False instead.

Returns:

  • bool –

    True if the cluster complies with the requirements, False otherwise.

Source code in src/compas_timber/connections/l_french_ridge_lap.py
@classmethod
def check_elements_compatibility(cls, elements, raise_error=False):
    """Checks if the cluster of beams complies with the requirements for the LFrenchRidgeLapJoint.

    Parameters
    ----------
    elements : list(:class:`~compas_model.elements.Beam`)
        The elements to be checked.
    raise_error : bool, optional
        If True, raises a :class:`~compas_timber.errors.BeamJoiningError` if the cluster does not comply with the requirements.
        If False, returns False instead.

    Returns
    -------
    bool
        True if the cluster complies with the requirements, False otherwise.

    """
    beam_a, beam_b = elements
    if not are_beams_aligned_with_cross_vector(beam_a, beam_b):
        if not raise_error:
            return False

        raise BeamJoiningError(
            beams=elements,
            joint=cls,
            debug_info="The two beams are not coplanar to create a French Ridge Lap joint.",
            debug_geometries=[e.shape for e in elements],
        )

    # calculate widths and heights of the beams
    ref_side_index_a = cls._get_beam_ref_side_index(beam_a, beam_b, flip=False)
    ref_side_index_b = cls._get_beam_ref_side_index(beam_b, beam_a, flip=False)

    w_a, h_a = beam_a.get_dimensions_relative_to_side(ref_side_index_a)
    w_b, h_b = beam_b.get_dimensions_relative_to_side(ref_side_index_b)

    # check if the dimensions of both beams match
    if (w_a, h_a) != (w_b, h_b):
        if not raise_error:
            return False

        raise BeamJoiningError(
            elements,
            cls,
            debug_info="The two beams must have the same dimensions to create a French Ridge Lap joint.",
            debug_geometries=[e.shape for e in elements],
        )

    return True

LLapJoint ¤

LLapJoint(beam_a=None, beam_b=None, flip_lap_side=False, cut_plane_bias=0.5, **kwargs)

Represents an L-Lap type joint which joins the ends of two beams with a lap.

This joint type is compatible with beams in L topology.

Please use LLapJoint.create() to properly create an instance of this class and associate it with a model.

Parameters:

  • beam_a (:class:`~compas_timber.elements.Beam`, default: None ) –

    The first beam to be joined.

  • beam_b (:class:`~compas_timber.elements.Beam`, default: None ) –

    The second beam to be joined.

  • flip_lap_side (bool, default: False ) –

    If True, the lap is flipped to the other side of the beams.

  • cut_plane_bias (float, default: 0.5 ) –

    Allows lap to be shifted deeper into one beam or the other. Value should be between 0 and 1.0 without completely cutting through either beam. Default is 0.5.

Attributes:

  • beam_a (:class:`~compas_timber.elements.Beam`) –

    The first beam to be joined.

  • beam_b (:class:`~compas_timber.elements.Beam`) –

    The second beam to be joined.

  • flip_lap_side (bool) –

    If True, the lap is flipped to the other side of the beams.

  • cut_plane_bias (float) –

    Allows lap to be shifted deeper into one beam or the other. Value should be between 0 and 1.0 without completely cutting through either beam. Default is 0.5.

Source code in src/compas_timber/connections/lap_joint.py
def __init__(self, beam_a=None, beam_b=None, flip_lap_side=False, cut_plane_bias=0.5, **kwargs):
    super(LapJoint, self).__init__(elements=(beam_a, beam_b), **kwargs)
    self.flip_lap_side = flip_lap_side
    self.cut_plane_bias = cut_plane_bias

    self._ref_side_index_a = None
    self._ref_side_index_b = None

Methods:¤

add_extensions ¤
add_extensions()

Calculates and adds the necessary extensions to the beams.

Raises:

Source code in src/compas_timber/connections/l_lap.py
def add_extensions(self):
    """Calculates and adds the necessary extensions to the beams.

    Raises
    ------
    BeamJoiningError
        If the extension could not be calculated.

    """
    assert self.beam_a and self.beam_b

    start_a, start_b = None, None
    try:
        start_a, end_a = self.beam_a.extension_to_plane(self.cutting_plane_a)
        start_b, end_b = self.beam_b.extension_to_plane(self.cutting_plane_b)
    except AttributeError as ae:
        # I want here just the plane that caused the error
        geometries = [self.cutting_plane_b] if start_a is not None else [self.cutting_plane_a]
        raise BeamJoiningError(self.elements, self, debug_info=str(ae), debug_geometries=geometries)
    except Exception as ex:
        raise BeamJoiningError(self.elements, self, debug_info=str(ex))
    tol = TOL.absolute
    self.beam_a.add_blank_extension(start_a + tol, end_a + tol, self.guid)
    self.beam_b.add_blank_extension(start_b + tol, end_b + tol, self.guid)
add_features ¤
add_features()

Adds the required joint features to both beams.

Source code in src/compas_timber/connections/l_lap.py
def add_features(self):
    """Adds the required joint features to both beams."""
    assert self.beam_a and self.beam_b

    # create lap features
    negative_volume_a, negative_volume_b = self._create_negative_volumes(self.cut_plane_bias)
    lap_feature_a = LapProxy.from_volume_and_beam(negative_volume_a, self.beam_a, ref_side_index=self.ref_side_index_a)
    lap_feature_b = LapProxy.from_volume_and_beam(negative_volume_b, self.beam_b, ref_side_index=self.ref_side_index_b)

    # create cutoff features
    cut_feature_a = JackRafterCutProxy.from_plane_and_beam(self.cutting_plane_a, self.beam_a)
    cut_feature_b = JackRafterCutProxy.from_plane_and_beam(self.cutting_plane_b, self.beam_b)

    features_a = [cut_feature_a, lap_feature_a]
    features_b = [cut_feature_b, lap_feature_b]

    # add processings to beams
    self.beam_a.add_features(features_a)
    self.beam_b.add_features(features_b)

    # register processings to the joint
    self.features.extend(features_a + features_b)

LMiterJoint ¤

LMiterJoint(
    beam_a=None,
    beam_b=None,
    cutoff=None,
    ref_side_miter=False,
    clean=False,
    miter_plane=None,
    **kwargs,
)

Represents an L-Miter type joint which joins two beam in their ends, trimming them with a plane at the bisector angle between the beams' centerlines.

This joint type is compatible with beams in L topology.

Please use LMiterJoint.create() to properly create an instance of this class and associate it with a model. To override the miter plane, pass a :class:~compas_timber.connections.CutPlaneSpec built with :meth:~compas_timber.connections.CutPlaneSpec.from_plane.

Parameters:

  • beam_a (:class:`~compas_timber.elements.Beam`, default: None ) –

    First beam to be joined.

  • beam_b (:class:`~compas_timber.elements.Beam`, default: None ) –

    Second beam to be joined.

  • cutoff (bool, default: None ) –

    If True, the beams will be trimmed with a plane perpendicular to the bisector (miter) plane of the beams.

  • ref_side_miter (bool, default: False ) –

    If True, the miter plane will be calculated based on the reference sides of the beams instead of the bisector of the centerlines.

  • clean (bool, default: False ) –

    If True, cleaning cuts will be applied to each beam based on the back sides of the other beam.

  • miter_plane (:class:`~compas_timber.connections.CutPlaneSpec`, default: None ) –

    Overrides the automatically calculated miter plane, stored relative to beam_a. Build with CutPlaneSpec.from_plane(beam_a, plane).

Attributes:

  • beam_a (:class:`~compas_timber.elements.Beam`) –

    First beam to be joined.

  • beam_b (:class:`~compas_timber.elements.Beam`) –

    Second beam to be joined.

  • cutoff (bool) –

    If True, the beams will be trimmed with a plane perpendicular to the bisector (miter) plane of the beams.

  • miter_plane (:class:`~compas.geometry.Plane`, optional) –

    A user-defined plane that defines the miter cut location and orientation. None if not set, in which case the miter plane is calculated automatically (see ref_side_miter).

  • ref_side_miter (bool) –

    If True, the miter plane will be calculated based on the reference sides of the beams instead of the bisector of the centerlines.

  • clean (bool) –

    If True, cleaning cuts will be applied to each beam based on the back sides of the other beam.

Source code in src/compas_timber/connections/l_miter.py
def __init__(
    self,
    beam_a=None,
    beam_b=None,
    cutoff=None,
    ref_side_miter=False,
    clean=False,
    miter_plane=None,
    **kwargs,
):
    super(LMiterJoint, self).__init__(elements=(beam_a, beam_b), **kwargs)
    self._miter_plane = miter_plane
    self.ref_side_miter = ref_side_miter
    self.cutoff = cutoff
    self.clean = clean
    self._back_a_index = None
    self._back_b_index = None

Attributes¤

miter_plane property ¤
miter_plane

The user-defined miter plane in world coordinates, or None if no override is set.

When None, the cutting plane is calculated automatically from the beams' geometry (see ref_side_miter).

Methods:¤

add_extensions ¤
add_extensions()

Calculates and adds the necessary extensions to the beams.

This method is called during TimberModel.process_joinery(), not when the joint is created.

Raises:

Source code in src/compas_timber/connections/l_miter.py
def add_extensions(self):
    """Calculates and adds the necessary extensions to the beams.

    This method is called during `TimberModel.process_joinery()`, not when the joint is created.

    Raises
    ------
    BeamJoiningError
        If the extension could not be calculated.

    """
    assert self.beam_a and self.beam_b
    start_a, start_b = None, None
    try:
        if self.cutoff:
            plane_a = self._get_cutoff_plane()
            plane_b = plane_a.copy()
        else:
            plane_a, plane_b = self.cutting_planes
        start_a, end_a = self.beam_a.extension_to_plane(plane_a)
        start_b, end_b = self.beam_b.extension_to_plane(plane_b)
    except AttributeError as ae:
        # I want here just the plane that caused the error
        geometries = [plane_b] if start_a is not None and plane_b else [plane_a] if plane_a else []
        raise BeamJoiningError(self.elements, self, debug_info=str(ae), debug_geometries=geometries)
    except Exception as ex:
        raise BeamJoiningError(self.elements, self, debug_info=str(ex))
    self.beam_a.add_blank_extension(start_a, end_a, self.guid)
    self.beam_b.add_blank_extension(start_b, end_b, self.guid)
add_features ¤
add_features()

Adds the required extension and trimming features to both beams.

This method is called during TimberModel.process_joinery(), not when the joint is created.

Source code in src/compas_timber/connections/l_miter.py
def add_features(self):
    """Adds the required extension and trimming features to both beams.

    This method is called during `TimberModel.process_joinery()`, not when the joint is created.

    """
    assert self.beam_a and self.beam_b

    try:
        miter_plane_a, miter_plane_b = self.cutting_planes
    except Exception as ex:
        raise BeamJoiningError(self.elements, self, debug_info=str(ex))

    cut1 = JackRafterCutProxy.from_plane_and_beam(miter_plane_a, self.beam_a)
    cut2 = JackRafterCutProxy.from_plane_and_beam(miter_plane_b, self.beam_b)
    self.beam_a.add_features(cut1)
    self.beam_b.add_features(cut2)
    self.features = [cut1, cut2]

    # add cutoffs if necessary
    if self.cutoff:
        cutoff_plane = self._get_cutoff_plane()
        for beam in self.elements:
            cutoff = JackRafterCutProxy.from_plane_and_beam(cutoff_plane, beam)
            beam.add_features(cutoff)
            self.features.append(cutoff)

    if self.clean:

        def get_valid_trim_planes(ref_side_beam, beam_to_trim, miter_plane, back_index):  # TODO: extract to utility function if useful elsewhere.
            trim_planes = []
            vector = Vector.from_start_end(beam_to_trim.centerline.midpoint, self.location)
            for i, frame in enumerate(ref_side_beam.ref_sides[0:4]):
                if i == back_index:
                    # if self.ref_side_miter == True, one of the ref sides is used to generate the miter plane, so it isn't used for trimming.
                    continue

                test_plane = Plane.from_frame(frame)
                # only use the outside ref_sides ie those facing away from the beam to be trimmed.
                if not TOL.is_positive(dot_vectors(vector, test_plane.normal)):
                    continue
                use_plane = False
                # parse planes that don't actually intersect beam geometry
                # if any edge intersection is behind another plane, then this plane will trim the geometry and should be applied
                for edge in beam_to_trim.ref_edges:
                    pt = intersection_line_plane(edge, test_plane)
                    for plane in [miter_plane] + trim_planes:
                        if is_point_behind_plane(pt, plane):
                            trim_planes.append(test_plane)
                            use_plane = True
                            break
                    if use_plane:
                        break
            return trim_planes

        back_a = get_valid_trim_planes(self.beam_a, self.beam_b, miter_plane_b, self._back_a_index)
        back_b = get_valid_trim_planes(self.beam_b, self.beam_a, miter_plane_a, self._back_b_index)

        clean_cuts_a = [JackRafterCutProxy.from_plane_and_beam(cut, self.beam_a) for cut in back_b]
        clean_cuts_b = [JackRafterCutProxy.from_plane_and_beam(cut, self.beam_b) for cut in back_a]

        self.beam_a.add_features(clean_cuts_a)
        self.beam_b.add_features(clean_cuts_b)
        self.features.extend(clean_cuts_a)
        self.features.extend(clean_cuts_b)

LTenonMortiseJoint ¤

LTenonMortiseJoint(main_beam=None, cross_beam=None, modify_cross=False, **kwargs)

Represents a TenonMortise type joint which joins two beams, one of them at its end (main) and the other one along its centerline (cross) or both of them at their ends. A tenon is added on the main beam, and a corresponding mortise is made on the cross beam to fit the main beam's tenon.

This joint type is compatible with beams in L topology.

Please use LTenonMortiseJoint.create() to properly create an instance of this class and associate it with a model.

Parameters:

  • main_beam (:class:`~compas_timber.elements.Beam`, default: None ) –

    First beam to be joined.

  • cross_beam (:class:`~compas_timber.elements.Beam`, default: None ) –

    Second beam to be joined.

  • start_y (float) –

    Start position of the tenon along the y-axis of the main beam.

  • start_depth (float) –

    Depth of the tenon from the surface of the main beam.

  • rotation (float) –

    Rotation of the tenon around the main beam's axis.

  • length (float) –

    Length of the tenon along the main beam.

  • width (float) –

    Width of the tenon.

  • height (float) –

    Height of the tenon.

  • tenon_shape (:class:`~compas_timber.fabrication.TenonShapeType`) –

    Shape of the tenon. One of :class:~compas_timber.fabrication.TenonShapeType: AUTOMATIC, SQUARE, ROUND, ROUNDED, RADIUS. Defaults to TenonShapeType.ROUND.

  • shape_radius (float) –

    The radius used to define the shape of the tenon, if applicable.

  • modify_cross (bool, default: False ) –

    If True, the cross beam will be extended to the opposite face of the main beam and cut with the same plane.

Attributes:

  • main_beam (:class:`~compas_timber.elements.Beam`) –

    First beam to be joined.

  • cross_beam (:class:`~compas_timber.elements.Beam`) –

    Second beam to be joined.

  • main_beam_guid (str) –

    GUID of the main beam.

  • cross_beam_guid (str) –

    GUID of the cross beam.

  • start_y (float) –

    Start position of the tenon along the y-axis of the main beam.

  • start_depth (float) –

    Depth of the tenon from the surface of the main beam.

  • rotation (float) –

    Rotation of the tenon around the main beam's axis.

  • length (float) –

    Length of the tenon along the main beam.

  • width (float) –

    Width of the tenon.

  • height (float) –

    Height of the tenon.

  • tenon_shape (:class:`~compas_timber.fabrication.TenonShapeType`) –

    Shape of the tenon. One of :class:~compas_timber.fabrication.TenonShapeType: AUTOMATIC, SQUARE, ROUND, ROUNDED, RADIUS.

  • shape_radius (float) –

    The radius used to define the shape of the tenon, if applicable.

  • modify_cross (bool) –

    If True, the cross beam will be extended to the opposite face of the main beam and cut with the same plane.

  • features (list) –

    List of features or machining processings applied to the elements.

Source code in src/compas_timber/connections/l_tenon_mortise.py
def __init__(self, main_beam=None, cross_beam=None, modify_cross=False, **kwargs):
    super(LTenonMortiseJoint, self).__init__(main_beam, cross_beam, **kwargs)
    self.modify_cross = modify_cross

Methods:¤

add_extensions ¤
add_extensions()

Calculates and adds the necessary extensions to the beams.

This method is called during TimberModel.process_joinery(), not when the joint is created.

Raises:

Source code in src/compas_timber/connections/l_tenon_mortise.py
def add_extensions(self):
    """Calculates and adds the necessary extensions to the beams.

    This method is called during `TimberModel.process_joinery()`, not when the joint is created.

    Raises
    ------
    BeamJoiningError
        If the extension could not be calculated.

    """
    assert self.main_beam and self.cross_beam
    extension_tolerance = 0.01  # TODO: this should be proportional to the unit used

    # cross_beam
    try:
        cutting_plane = self.main_beam.opp_side(self.main_beam_ref_side_index)
        start_cross, end_cross = self.cross_beam.extension_to_plane(cutting_plane)
    except AttributeError as ae:
        raise BeamJoiningError(beams=self.elements, joint=self, debug_info=str(ae), debug_geometries=[cutting_plane])
    self.cross_beam.add_blank_extension(start_cross + extension_tolerance, end_cross + extension_tolerance, self.guid)
    start_main, end_main = self.get_main_extension()
    self.main_beam.add_blank_extension(
        start_main + extension_tolerance,
        end_main + extension_tolerance,
        self.guid,
    )
add_features ¤
add_features()

Adds the required trimming features to both beams.

This method is called during TimberModel.process_joinery(), not when the joint is created.

Source code in src/compas_timber/connections/l_tenon_mortise.py
def add_features(self):
    """Adds the required trimming features to both beams.

    This method is called during `TimberModel.process_joinery()`, not when the joint is created.

    """
    assert self.main_beam and self.cross_beam  # should never happen

    main_feature = self._create_tenon_feature()

    self.main_beam.add_features(main_feature)
    self.features = [main_feature]

    # generate mortise features
    cross_feature = self._create_mortise_feature(main_feature)

    cross_features = [cross_feature]

    # generate cross cut_off feature
    if self.modify_cross:
        cutting_plane = self.main_beam.opp_side(self.main_beam_ref_side_index)
        cross_cutoff_feature = JackRafterCutProxy.from_plane_and_beam(cutting_plane, self.cross_beam)
        cross_features.append(cross_cutoff_feature)

    self.cross_beam.add_features(cross_features)
    self.features.extend(cross_features)

LapJoint ¤

LapJoint(beam_a=None, beam_b=None, flip_lap_side=False, cut_plane_bias=0.5, **kwargs)

Abstract Lap type joint with functions common to L-Lap, T-Lap, and X-Lap Joints.

Do not instantiate directly. Please use **LapJoint.create() to properly create an instance of lap sub-class and associate it with an model.

Parameters:

  • beam_a (:class:`~compas_timber.elements.Beam`, default: None ) –

    The first beam to be joined.

  • beam_b (:class:`~compas_timber.elements.Beam`, default: None ) –

    The second beam to be joined.

  • flip_lap_side (bool, default: False ) –

    If True, the lap is flipped to the other side of the beams.

Attributes:

  • elements (list of :class:`~compas_timber.elements.Beam`) –

    The beams to be joined.

  • beam_a (:class:`~compas_timber.elements.Beam`) –

    The first beam to be joined.

  • beam_b (:class:`~compas_timber.elements.Beam`) –

    The second beam to be joined.

  • flip_lap_side (bool) –

    If True, the lap is flipped to the other side of the beams.

Source code in src/compas_timber/connections/lap_joint.py
def __init__(self, beam_a=None, beam_b=None, flip_lap_side=False, cut_plane_bias=0.5, **kwargs):
    super(LapJoint, self).__init__(elements=(beam_a, beam_b), **kwargs)
    self.flip_lap_side = flip_lap_side
    self.cut_plane_bias = cut_plane_bias

    self._ref_side_index_a = None
    self._ref_side_index_b = None

Attributes¤

cutting_plane_a property ¤
cutting_plane_a

The face of the beam_b that cuts the beam_a, as a plane.

cutting_plane_b property ¤
cutting_plane_b

The face of the beam_a that cuts the beam_b, as a plane.

ref_side_index_a property ¤
ref_side_index_a

The reference side index of the beam_a.

ref_side_index_b property ¤
ref_side_index_b

The reference side index of the beam_b.

MortiseTenonJoint ¤

MortiseTenonJoint(
    main_beam=None,
    cross_beam=None,
    start_y=None,
    start_depth=None,
    rotation=None,
    length=None,
    width=None,
    height=None,
    tenon_shape=None,
    shape_radius=None,
    **kwargs,
)

Base class for mortise-tenon joints.

This is an abstract class and should not be instantiated directly.

This class stores shared parameter handling, serialization, and feature creation helpers used by mortise-tenon joint variants.

Parameters:

  • main_beam (:class:`~compas_timber.elements.Beam`, default: None ) –

    First beam to be joined.

  • cross_beam (:class:`~compas_timber.elements.Beam`, default: None ) –

    Second beam to be joined.

  • start_y (float, default: None ) –

    Start position of the tenon along the y-axis of the main beam.

  • start_depth (float, default: None ) –

    Depth of the tenon from the surface of the main beam.

  • rotation (float, default: None ) –

    Rotation of the tenon around the main beam's axis.

  • length (float, default: None ) –

    Length of the tenon along the main beam.

  • width (float, default: None ) –

    Width of the tenon.

  • height (float, default: None ) –

    Height of the tenon.

  • tenon_shape (str, default: None ) –

    Shape of the tenon. One of :class:~compas_timber.fabrication.TenonShapeType: AUTOMATIC, SQUARE, ROUND, ROUNDED, RADIUS. Defaults to TenonShapeType.ROUND.

  • shape_radius (float, default: None ) –

    The radius used to define the shape of the tenon, if applicable.

  • **kwargs (dict, default: {} ) –

    Additional keyword arguments.

Attributes:

  • main_beam (:class:`~compas_timber.elements.Beam`) –

    First beam to be joined.

  • cross_beam (:class:`~compas_timber.elements.Beam`) –

    Second beam to be joined.

  • main_beam_guid (str) –

    GUID of the main beam.

  • cross_beam_guid (str) –

    GUID of the cross beam.

  • start_y (float) –

    Start position of the tenon along the y-axis of the main beam.

  • start_depth (float) –

    Depth of the tenon from the surface of the main beam.

  • rotation (float) –

    Rotation of the tenon around the main beam's axis.

  • length (float) –

    Length of the tenon along the main beam.

  • width (float) –

    Width of the tenon.

  • height (float) –

    Height of the tenon.

  • tenon_shape (str) –

    Shape of the tenon. One of :class:~compas_timber.fabrication.TenonShapeType: AUTOMATIC, SQUARE, ROUND, ROUNDED, RADIUS.

  • shape_radius (float) –

    The radius used to define the shape of the tenon, if applicable.

  • features (list) –

    List of features or machining processings applied to the elements.

Source code in src/compas_timber/connections/mortise_tenon.py
def __init__(
    self,
    main_beam=None,
    cross_beam=None,
    start_y=None,
    start_depth=None,
    rotation=None,
    length=None,
    width=None,
    height=None,
    tenon_shape=None,
    shape_radius=None,
    **kwargs
):
    super(MortiseTenonJoint, self).__init__(elements=(main_beam, cross_beam), **kwargs)
    self.start_y = start_y
    self.start_depth = start_depth
    self.rotation = rotation
    self.length = length
    self.width = width
    self.height = height
    self.tenon_shape = tenon_shape
    self.shape_radius = shape_radius

    if self.main_beam and self.cross_beam:
        self._set_unset_attributes()

Methods:¤

get_main_extension ¤
get_main_extension()

Return the start/end extension lengths for the main beam.

Raises:

Source code in src/compas_timber/connections/mortise_tenon.py
def get_main_extension(self):
    """Return the start/end extension lengths for the main beam.

    Raises
    ------
    BeamJoiningError
        If the extension could not be calculated.

    """
    cutting_plane = None
    try:
        cutting_plane = self.cross_beam.ref_sides[self.cross_beam_ref_side_index]
        cutting_plane.translate(-cutting_plane.normal * self.height)
        start_main, end_main = self.main_beam.extension_to_plane(cutting_plane)
    except AttributeError as ae:
        raise BeamJoiningError(beams=self.elements, joint=self, debug_info=str(ae), debug_geometries=[cutting_plane])
    return start_main, end_main

PanelJoint ¤

PanelJoint(
    panel_a: Panel | None = None,
    panel_b: Panel | None = None,
    topology: JointTopology | None = None,
    a_segment_index: int | None = None,
    b_segment_index: int | None = None,
    **kwargs,
)

Models a plate to plate interaction.

Parameters:

  • panel_a (:class:`compas_timber.elements.Panel`, default: None ) –

    The first plate.

  • panel_b (:class:`compas_timber.elements.Panel`, default: None ) –

    The second plate.

  • topology (literal(JointTopology), default: None ) –

    The topology in which the plates are connected.

  • a_segment_index (int, default: None ) –

    The index of the segment in panel_a's outline where the plates are connected.

  • b_segment_index (int, default: None ) –

    The index of the segment in panel_b's outline where the plates are connected.

  • **kwargs (dict, default: {} ) –

    Additional keyword arguments to pass to the parent class.

Attributes:

  • panel_a (:class:`compas_timber.elements.Panel`) –

    The first plate.

  • panel_b (:class:`compas_timber.elements.Panel`) –

    The second plate.

  • panels (tuple of :class:`compas_timber.elements.Panel`) –

    The panels that are connected.

  • interface_a (:class:`compas.geometry.PlanarSurface`) –

    The interface surface of panel_a where it meets panel_b.

  • interface_b (:class:`compas.geometry.PlanarSurface`) –

    The interface surface of panel_b where it meets panel_a.

Source code in src/compas_timber/connections/panel_joint.py
def __init__(
    self,
    panel_a: Optional[Panel] = None,
    panel_b: Optional[Panel] = None,
    topology: Optional[JointTopology] = None,
    a_segment_index: Optional[int] = None,
    b_segment_index: Optional[int] = None,
    **kwargs,
):
    super(PanelJoint, self).__init__(panel_a, panel_b, topology, a_segment_index, b_segment_index, **kwargs)
    self.interface_a = None
    self.interface_b = None

PanelLButtJoint ¤

PanelLButtJoint(
    panel_a: Panel | None = None,
    panel_b: Panel | None = None,
    topology: JointTopology | None = None,
    a_segment_index: int | None = None,
    b_segment_index: int | None = None,
    **kwargs,
)

Creates a plate-to-plate butt-joint connection.

Source code in src/compas_timber/connections/panel_joint.py
def __init__(
    self,
    panel_a: Optional[Panel] = None,
    panel_b: Optional[Panel] = None,
    topology: Optional[JointTopology] = None,
    a_segment_index: Optional[int] = None,
    b_segment_index: Optional[int] = None,
    **kwargs,
):
    super(PanelJoint, self).__init__(panel_a, panel_b, topology, a_segment_index, b_segment_index, **kwargs)
    self.interface_a = None
    self.interface_b = None

Attributes¤

cross_panel property ¤
cross_panel: Panel

Return the cross plate.

main_panel property ¤
main_panel: Panel

Return the main plate.

PanelLLayerButtJoint ¤

PanelLLayerButtJoint(
    panel_a: Panel | None = None,
    panel_b: Panel | None = None,
    topology: JointTopology | None = None,
    a_segment_index: int | None = None,
    b_segment_index: int | None = None,
    **kwargs,
)

Creates a plate-to-plate butt-joint connection.

Source code in src/compas_timber/connections/panel_joint.py
def __init__(
    self,
    panel_a: Optional[Panel] = None,
    panel_b: Optional[Panel] = None,
    topology: Optional[JointTopology] = None,
    a_segment_index: Optional[int] = None,
    b_segment_index: Optional[int] = None,
    **kwargs,
):
    super(PanelJoint, self).__init__(panel_a, panel_b, topology, a_segment_index, b_segment_index, **kwargs)
    self.interface_a = None
    self.interface_b = None

Attributes¤

cross_panel property ¤
cross_panel: Panel

Return the cross plate.

main_panel property ¤
main_panel: Panel

Return the main plate.

PanelMiterJoint ¤

PanelMiterJoint(
    panel_a: Panel | None = None,
    panel_b: Panel | None = None,
    topology: JointTopology | None = None,
    a_segment_index: int | None = None,
    b_segment_index: int | None = None,
    **kwargs,
)

Creates a mitered edge plate-to-plate connection.

Source code in src/compas_timber/connections/panel_joint.py
def __init__(
    self,
    panel_a: Optional[Panel] = None,
    panel_b: Optional[Panel] = None,
    topology: Optional[JointTopology] = None,
    a_segment_index: Optional[int] = None,
    b_segment_index: Optional[int] = None,
    **kwargs,
):
    super(PanelJoint, self).__init__(panel_a, panel_b, topology, a_segment_index, b_segment_index, **kwargs)
    self.interface_a = None
    self.interface_b = None

PanelTButtJoint ¤

PanelTButtJoint(
    panel_a: Panel | None = None,
    panel_b: Panel | None = None,
    topology: JointTopology | None = None,
    a_segment_index: int | None = None,
    b_segment_index: int | None = None,
    **kwargs,
)

Creates a plate-to-plate butt-joint connection.

Source code in src/compas_timber/connections/panel_joint.py
def __init__(
    self,
    panel_a: Optional[Panel] = None,
    panel_b: Optional[Panel] = None,
    topology: Optional[JointTopology] = None,
    a_segment_index: Optional[int] = None,
    b_segment_index: Optional[int] = None,
    **kwargs,
):
    super(PanelJoint, self).__init__(panel_a, panel_b, topology, a_segment_index, b_segment_index, **kwargs)
    self.interface_a = None
    self.interface_b = None

Attributes¤

cross_panel property ¤
cross_panel: Panel

Return the cross plate.

main_panel property ¤
main_panel: Panel

Return the main plate.

PlateButtJoint ¤

PlateButtJoint(
    plate_a=None,
    plate_b=None,
    topology=None,
    a_segment_index=None,
    b_segment_index=None,
    **kwargs,
)

Creates a plate-to-plate butt-joint connection.

Source code in src/compas_timber/connections/plate_joint.py
def __init__(self, plate_a=None, plate_b=None, topology=None, a_segment_index=None, b_segment_index=None, **kwargs):
    super(PlateJoint, self).__init__(elements=(plate_a, plate_b), topology=topology, **kwargs)
    self.a_segment_index = a_segment_index
    self.b_segment_index = b_segment_index
    if self.plate_a and self.plate_b:
        if self.topology is None or (self.a_segment_index is None and self.b_segment_index is None):
            self.calculate_topology()
    self._reverse_a_planes = False
    self._reverse_b_planes = False
    self.distance = 0.0  # HACK: to pass joint rules that expect a distance attribute

Attributes¤

cross_plate property ¤
cross_plate

Return the cross plate.

cross_segment_index property writable ¤
cross_segment_index

Return the index of the segment in the cross plate outline.

main_plate property ¤
main_plate

Return the main plate.

main_segment_index property writable ¤
main_segment_index

Return the index of the segment in the main plate outline.

PlateConnectionSolver ¤

Provides tools for detecting plate intersections and joint topologies.

Methods:¤

do_segments_overlap staticmethod ¤
do_segments_overlap(segment_a, segment_b)

Checks if two segments overlap.

Parameters:

  • segment_a (:class:`~compas.geometry.Segment`) –

    The first segment.

  • segment_b (:class:`~compas.geometry.Segment`) –

    The second segment.

Returns:

  • bool –

    True if the segments overlap, False otherwise.

Source code in src/compas_timber/connections/solver.py
@staticmethod
def do_segments_overlap(segment_a, segment_b):
    """Checks if two segments overlap.

    Parameters
    ----------
    segment_a : :class:`~compas.geometry.Segment`
        The first segment.
    segment_b : :class:`~compas.geometry.Segment`
        The second segment.

    Returns
    -------
    bool
        True if the segments overlap, False otherwise.
    """
    for pt_a in [segment_a.start, segment_a.end, segment_a.point_at(0.5)]:
        dot_start = dot_vectors(segment_b.direction, Vector.from_start_end(segment_b.start, pt_a))
        dot_end = dot_vectors(segment_b.direction, Vector.from_start_end(segment_b.end, pt_a))
        if dot_start > 0 and dot_end < 0:
            return True
    for pt_b in [segment_b.start, segment_b.end, segment_b.point_at(0.5)]:
        dot_start = dot_vectors(segment_a.direction, Vector.from_start_end(segment_a.start, pt_b))
        dot_end = dot_vectors(segment_a.direction, Vector.from_start_end(segment_a.end, pt_b))
        if dot_start > 0 and dot_end < 0:
            return True
    return False
does_segment_intersect_outline staticmethod ¤
does_segment_intersect_outline(segment, polyline, tol=TOL)

Checks if a segment intersects with the outline of a polyline.

Parameters:

  • segment (:class:`~compas.geometry.Segment`) –

    The segment to check for intersection.

  • polyline (:class:`~compas.geometry.Polyline`) –

    The polyline whose outline is checked for intersection.

  • tol (float, default: TOL ) –

    Tolerance for intersection check.

Returns:

  • bool –

    True if the segment intersects with the outline of the polyline, False otherwise.

Source code in src/compas_timber/connections/solver.py
@staticmethod
def does_segment_intersect_outline(segment, polyline, tol=TOL):
    """Checks if a segment intersects with the outline of a polyline.

    Parameters
    ----------
    segment : :class:`~compas.geometry.Segment`
        The segment to check for intersection.
    polyline : :class:`~compas.geometry.Polyline`
        The polyline whose outline is checked for intersection.
    tol : float, optional
        Tolerance for intersection check.

    Returns
    -------
    bool
        True if the segment intersects with the outline of the polyline, False otherwise.
    """
    if intersection_segment_polyline(segment, polyline, tol.absolute)[0]:
        return True
    return is_point_in_polyline(segment.point_at(0.5), polyline, in_plane=False, tol=tol)
find_topology ¤
find_topology(plate_a, plate_b, max_distance=TOLERANCE, tol=TOLERANCE)

Calculates the topology of the intersection between two plates. requires that one edge of a plate lies on the plane of the other plate. When TOPOLOGY_EDGE_FACE is found, the plates are returned in reverse order, with the main plate first and the cross plate second.

Parameters:

  • plate_a (:class:`~compas_timber.elements.Plate`) –

    First potential intersecting plate.

  • plate_b (:class:`~compas_timber.elements.Plate`) –

    Second potential intersecting plate.

  • tol (float, default: TOLERANCE ) –

    General tolerance to use for mathematical computations.

  • max_distance (float, default: TOLERANCE ) –

    Maximum distance, in desigen units, at which two plates are considered intersecting.

Returns:

  • class:`~compas_timber.connections.PlateSolverResult` –
Source code in src/compas_timber/connections/solver.py
def find_topology(self, plate_a, plate_b, max_distance=TOLERANCE, tol=TOLERANCE):
    """Calculates the topology of the intersection between two plates. requires that one edge of a plate lies on the plane of the other plate.
    When TOPOLOGY_EDGE_FACE is found, the plates are returned in reverse order, with the main plate first and the cross plate second.

    parameters
    ----------
    plate_a : :class:`~compas_timber.elements.Plate`
        First potential intersecting plate.
    plate_b : :class:`~compas_timber.elements.Plate`
        Second potential intersecting plate.
    tol : float
        General tolerance to use for mathematical computations.
    max_distance : float, optional
        Maximum distance, in desigen units, at which two plates are considered intersecting.

    Returns
    -------
    :class:`~compas_timber.connections.PlateSolverResult`
    """
    plate_a_segment_index, plate_b_segment_index, dist, pt = self._find_plate_segment_indices(plate_a, plate_b, max_distance=max_distance, tol=tol)
    if plate_a_segment_index is None and plate_b_segment_index is None:
        return PlateSolverResult(JointTopology.TOPO_UNKNOWN, plate_a, plate_b, plate_a_segment_index, plate_b_segment_index, dist, pt)
    if plate_a_segment_index is not None and plate_b_segment_index is None:
        return PlateSolverResult(JointTopology.TOPO_EDGE_FACE, plate_a, plate_b, plate_a_segment_index, plate_b_segment_index, dist, pt)
    if plate_a_segment_index is None and plate_b_segment_index is not None:
        return PlateSolverResult(JointTopology.TOPO_EDGE_FACE, plate_b, plate_a, plate_b_segment_index, plate_a_segment_index, dist, pt)
    if plate_a_segment_index is not None and plate_b_segment_index is not None:
        return PlateSolverResult(JointTopology.TOPO_EDGE_EDGE, plate_a, plate_b, plate_a_segment_index, plate_b_segment_index, dist, pt)

PlateJoint ¤

PlateJoint(
    plate_a=None,
    plate_b=None,
    topology=None,
    a_segment_index=None,
    b_segment_index=None,
    **kwargs,
)

Models a plate to plate interaction.

Parameters:

  • plate_a (:class:`compas_timber.elements.Plate`, default: None ) –

    The first plate.

  • plate_b (:class:`compas_timber.elements.Plate`, default: None ) –

    The second plate.

  • topology (literal(JointTopology), default: None ) –

    The topology in which the plates are connected.

  • a_segment_index (int, default: None ) –

    The index of the segment in plate_a's outline where the plates are connected.

  • b_segment_index (int, default: None ) –

    The index of the segment in plate_b's outline where the plates are connected.

  • location (:class:`compas.geometry.Point`) –

    The location of the joint. If not provided, defaults to the origin.

  • **kwargs (dict, default: {} ) –

    Additional keyword arguments to pass to the parent class.

Attributes:

  • plate_a (:class:`compas_timber.elements.Plate`) –

    The first plate.

  • plate_b (:class:`compas_timber.elements.Plate`) –

    The second plate.

  • plates (tuple of :class:`compas_timber.elements.Plate`) –

    The plates that are connected.

Source code in src/compas_timber/connections/plate_joint.py
def __init__(self, plate_a=None, plate_b=None, topology=None, a_segment_index=None, b_segment_index=None, **kwargs):
    super(PlateJoint, self).__init__(elements=(plate_a, plate_b), topology=topology, **kwargs)
    self.a_segment_index = a_segment_index
    self.b_segment_index = b_segment_index
    if self.plate_a and self.plate_b:
        if self.topology is None or (self.a_segment_index is None and self.b_segment_index is None):
            self.calculate_topology()
    self._reverse_a_planes = False
    self._reverse_b_planes = False
    self.distance = 0.0  # HACK: to pass joint rules that expect a distance attribute

Methods:¤

add_extensions ¤
add_extensions()

Adjusts plate outlines to outer shape required for the joint.

Source code in src/compas_timber/connections/plate_joint.py
def add_extensions(self):
    """Adjusts plate outlines to outer shape required for the joint."""
    if self.plate_a and self.plate_b:
        if self.topology is None or (self.a_segment_index is None and self.b_segment_index is None):
            self.calculate_topology()
        self._reorder_planes_and_outlines()
        self._set_edge_planes()
add_features ¤
add_features()

Adds features to the plates based on the joint. this should be implemented in subclasses if needed.

Source code in src/compas_timber/connections/plate_joint.py
def add_features(self):
    """Adds features to the plates based on the joint. this should be implemented in subclasses if needed."""
    pass
calculate_topology ¤
calculate_topology(allow_reordering=False)

Calculate the topology of the joint based on the plates.

Source code in src/compas_timber/connections/plate_joint.py
def calculate_topology(self, allow_reordering=False):
    """Calculate the topology of the joint based on the plates."""
    topo_results = PlateConnectionSolver().find_topology(self.plate_a, self.plate_b)
    if topo_results.topology == JointTopology.TOPO_UNKNOWN:
        raise ValueError("Could not determine topology for plates {0} and {1}.".format(self.plate_a, self.plate_b))
    if self.plate_a != topo_results.plate_a:
        if allow_reordering:
            self.plate_a, self.plate_b = topo_results.plate_a, topo_results.plate_b
        else:
            raise BeamJoiningError(
                beams=[self.plate_a, self.plate_b],
                joint=self,
                debug_info="The order of plates is incompatible with the joint topology. Try reversing the order of the plates.",
            )
    self.topology = topo_results.topology
    self.a_segment_index = topo_results.a_segment_index
    self.b_segment_index = topo_results.b_segment_index
    return topo_results
clear_extensions ¤
clear_extensions()

Clears features from the plates based on the joint.

Source code in src/compas_timber/connections/plate_joint.py
def clear_extensions(self):
    """Clears features from the plates based on the joint."""
    if self.a_segment_index is not None:
        self.element_a.remove_blank_extension(self.a_segment_index)
    if self.b_segment_index is not None:
        self.element_b.remove_blank_extension(self.b_segment_index)
promote_joint_candidate classmethod ¤
promote_joint_candidate(model, candidate, reordered_elements=None, **kwargs)

Creates an instance of this joint from a generic joint.

Parameters:

  • model (:class:`~compas_timber.model.TimberModel`) –

    The model to which the elements and this joint belong.

  • candidate (:class:`~compas_timber.connections.Joint`) –

    The generic joint to be converted.

  • reordered_elements (list(:class:`~compas_model.elements.Element`), default: None ) –

    The elements to be connected by this joint. If not provided, the elements of the generic joint will be used. This is used to explicitly define the element order.

  • **kwargs (dict, default: {} ) –

    Additional keyword arguments that are passed to the joint's constructor.

Returns:

  • class:`compas_timber.connections.Joint` –

    The instance of the created joint.

Source code in src/compas_timber/connections/plate_joint.py
@classmethod
def promote_joint_candidate(cls, model, candidate, reordered_elements=None, **kwargs):
    """Creates an instance of this joint from a generic joint.

    Parameters
    ----------
    model : :class:`~compas_timber.model.TimberModel`
        The model to which the elements and this joint belong.
    candidate : :class:`~compas_timber.connections.Joint`
        The generic joint to be converted.
    reordered_elements : list(:class:`~compas_model.elements.Element`), optional
        The elements to be connected by this joint. If not provided, the elements of the generic joint will be used.
        This is used to explicitly define the element order.
    **kwargs : dict
        Additional keyword arguments that are passed to the joint's constructor.

    Returns
    -------
    :class:`compas_timber.connections.Joint`
        The instance of the created joint.

    """
    if reordered_elements and candidate.elements[0] != reordered_elements[0]:  # plates are in different order, reverse segment indices
        kwargs.update({"a_segment_index": candidate.b_segment_index, "b_segment_index": candidate.a_segment_index})  # pass reversed segment indices from candidate
    else:
        kwargs.update({"a_segment_index": candidate.a_segment_index, "b_segment_index": candidate.b_segment_index})  # pass segment indices from candidate
    return super(PlateJoint, cls).promote_joint_candidate(model, candidate, reordered_elements=reordered_elements, **kwargs)

PlateJointCandidate ¤

PlateJointCandidate(plate_a=None, plate_b=None, distance=None, **kwargs)

A PlateJointCandidate is an information-only joint for plate connections.

It is used to create a first-pass joinery information which can be later used to create concrete joints.

Parameters:

  • plate_a (:class:`~compas_timber.elements.Plate`, default: None ) –

    First plate to be joined.

  • plate_b (:class:`~compas_timber.elements.Plate`, default: None ) –

    Second plate to be joined.

Attributes:

  • plate_a (:class:`~compas_timber.elements.Plate`) –

    First plate to be joined.

  • plate_b (:class:`~compas_timber.elements.Plate`) –

    Second plate to be joined.

Source code in src/compas_timber/connections/joint_candidate.py
def __init__(self, plate_a=None, plate_b=None, distance=None, **kwargs):
    # HACK: default distance to 0.0 (rather than None) to pass joint rules that expect a distance attribute,
    # matching the old `PlateJoint.__init__`'s behavior.
    super(PlateJointCandidate, self).__init__(element_a=plate_a, element_b=plate_b, distance=distance if distance is not None else 0.0, **kwargs)

PlateLButtJoint ¤

PlateLButtJoint(
    plate_a=None,
    plate_b=None,
    topology=None,
    a_segment_index=None,
    b_segment_index=None,
    **kwargs,
)

Creates a plate-to-plate butt-joint connection.

Source code in src/compas_timber/connections/plate_joint.py
def __init__(self, plate_a=None, plate_b=None, topology=None, a_segment_index=None, b_segment_index=None, **kwargs):
    super(PlateJoint, self).__init__(elements=(plate_a, plate_b), topology=topology, **kwargs)
    self.a_segment_index = a_segment_index
    self.b_segment_index = b_segment_index
    if self.plate_a and self.plate_b:
        if self.topology is None or (self.a_segment_index is None and self.b_segment_index is None):
            self.calculate_topology()
    self._reverse_a_planes = False
    self._reverse_b_planes = False
    self.distance = 0.0  # HACK: to pass joint rules that expect a distance attribute

PlateMiterJoint ¤

PlateMiterJoint(
    plate_a=None,
    plate_b=None,
    topology=None,
    a_segment_index=None,
    b_segment_index=None,
    **kwargs,
)

Creates a mitered edge plate-to-plate connection.

Source code in src/compas_timber/connections/plate_joint.py
def __init__(self, plate_a=None, plate_b=None, topology=None, a_segment_index=None, b_segment_index=None, **kwargs):
    super(PlateJoint, self).__init__(elements=(plate_a, plate_b), topology=topology, **kwargs)
    self.a_segment_index = a_segment_index
    self.b_segment_index = b_segment_index
    if self.plate_a and self.plate_b:
        if self.topology is None or (self.a_segment_index is None and self.b_segment_index is None):
            self.calculate_topology()
    self._reverse_a_planes = False
    self._reverse_b_planes = False
    self.distance = 0.0  # HACK: to pass joint rules that expect a distance attribute

PlateTButtJoint ¤

PlateTButtJoint(
    plate_a=None,
    plate_b=None,
    topology=None,
    a_segment_index=None,
    b_segment_index=None,
    **kwargs,
)

Creates a plate-to-plate butt-joint connection.

Source code in src/compas_timber/connections/plate_joint.py
def __init__(self, plate_a=None, plate_b=None, topology=None, a_segment_index=None, b_segment_index=None, **kwargs):
    super(PlateJoint, self).__init__(elements=(plate_a, plate_b), topology=topology, **kwargs)
    self.a_segment_index = a_segment_index
    self.b_segment_index = b_segment_index
    if self.plate_a and self.plate_b:
        if self.topology is None or (self.a_segment_index is None and self.b_segment_index is None):
            self.calculate_topology()
    self._reverse_a_planes = False
    self._reverse_b_planes = False
    self.distance = 0.0  # HACK: to pass joint rules that expect a distance attribute

TBirdsmouthJoint ¤

TBirdsmouthJoint(main_beam=None, cross_beam=None, mill_depth=None, **kwargs)

Represents a T-Birdsmouth type joint which joins two beams, one of them at it's end (main) and the other one along it's centerline (cross).

This joint type is compatible with beams in T topology.

Please use TBirdsmouth.create() to properly create an instance of this class and associate it with a model.

Parameters:

  • main_beam (:class:`~compas_timber.elements.Beam`, default: None ) –

    First beam to be joined.

  • cross_beam (:class:`~compas_timber.elements.Beam`, default: None ) –

    Second beam to be joined.

  • mill_depth (float, default: None ) –

    The depth of the pockets to be milled on the cross beam.

Attributes:

  • main_beam (:class:`~compas_timber.elements.Beam`) –

    First beam to be joined.

  • cross_beam (:class:`~compas_timber.elements.Beam`) –

    Second beam to be joined.

  • mill_depth (float) –

    The depth of the pockets to be milled on the cross beam.

Source code in src/compas_timber/connections/t_birdsmouth.py
def __init__(self, main_beam=None, cross_beam=None, mill_depth=None, **kwargs):
    super(TBirdsmouthJoint, self).__init__(elements=(main_beam, cross_beam), **kwargs)
    self.mill_depth = mill_depth

Methods:¤

add_extensions ¤
add_extensions()

Calculates and adds the necessary extensions to the beams.

This method is called during TimberModel.process_joinery(), not when the joint is created.

Raises:

Source code in src/compas_timber/connections/t_birdsmouth.py
def add_extensions(self):
    """Calculates and adds the necessary extensions to the beams.

    This method is called during `TimberModel.process_joinery()`, not when the joint is created.

    Raises
    ------
    BeamJoiningError
        If the extension could not be calculated.

    """
    assert self.main_beam and self.cross_beam
    start_a, end_a = None, None

    face_index = self.cross_ref_side_indices[1]
    plane = self.cross_beam.ref_sides[face_index]

    if self.mill_depth:
        plane.translate(-plane.normal * self.mill_depth)

    try:
        start_a, end_a = self.main_beam.extension_to_plane(plane)
    except Exception as ex:
        raise BeamJoiningError(self.elements, self, debug_info=str(ex))
    self.main_beam.add_blank_extension(start_a, end_a, self.guid)
add_features ¤
add_features()

Adds the required trimming features to both beams.

This method is called during TimberModel.process_joinery(), not when the joint is created.

Source code in src/compas_timber/connections/t_birdsmouth.py
def add_features(self):
    """Adds the required trimming features to both beams.

    This method is called during `TimberModel.process_joinery()`, not when the joint is created.

    """

    assert self.main_beam and self.cross_beam  # should never happen

    cutting_planes = self._get_cutting_planes()

    # generate double cut feature
    main_feature = DoubleCut.from_planes_and_beam(cutting_planes, self.main_beam)

    # register main feature to beam and joint
    self.main_beam.add_features(main_feature)
    self.features.append(main_feature)

    if self.mill_depth:
        main_ref_side_index = main_feature.ref_side_index
        lap_cutting_plane = self.main_beam.ref_sides[main_ref_side_index]
        lap_length = self.main_beam.get_dimensions_relative_to_side(main_ref_side_index)[1]

        # generate lap feature for each side of the cross beam that intersects the main beam
        # TODO: replace with Pocket once implemented in order to get a better fit
        cross_feature_1 = Lap.from_plane_and_beam(
            lap_cutting_plane,
            self.cross_beam,
            lap_length,
            self.mill_depth,
            is_pocket=True,
            ref_side_index=self.cross_ref_side_indices[0],
        )
        cross_feature_2 = Lap.from_plane_and_beam(
            lap_cutting_plane,
            self.cross_beam,
            lap_length,
            self.mill_depth,
            is_pocket=True,
            ref_side_index=self.cross_ref_side_indices[1],
        )

        # register cross features to beam and joint
        self.cross_beam.add_features([cross_feature_1, cross_feature_2])
        self.features.extend([cross_feature_1, cross_feature_2])

TButtJoint ¤

TButtJoint(
    main_beam=None,
    cross_beam=None,
    mill_depth=None,
    butt_plane_spec=None,
    force_pocket=False,
    conical_tool=False,
    fastener=None,
    **kwargs,
)

Represents a T-Butt type joint which joins the end of a beam along the length of another beam, trimming the main beam.

This joint type is compatible with beams in T topology.

Please use TButtJoint.create() to properly create an instance of this class and associate it with a model.

Parameters:

  • main_beam (:class:`~compas_timber.elements.Beam`, default: None ) –

    The main beam to be joined.

  • cross_beam (:class:`~compas_timber.elements.Beam`, default: None ) –

    The cross beam to be joined.

  • mill_depth (float, default: None ) –

    The depth of the pocket to be milled in the cross beam.

  • butt_plane_spec (:class:`~compas_timber.connections.CutPlaneSpec`, default: None ) –

    Overrides the plane used to cut the main beam. Build with :meth:~compas_timber.connections.CutPlaneSpec.from_butt_plane.

  • fastener (:class:`~compas_timber.elements.Fastener`, default: None ) –

    The fastener to be used in the joint.

Attributes:

  • main_beam (:class:`~compas_timber.elements.Beam`) –

    The main beam to be joined.

  • cross_beam (:class:`~compas_timber.elements.Beam`) –

    The cross beam to be joined.

  • mill_depth (float) –

    The depth of the pocket to be milled in the cross beam.

  • butt_plane (:class:`~compas.geometry.Plane`) –

    The plane used to cut the main beam. If not overridden, the closest side of the cross beam will be used.

Source code in src/compas_timber/connections/t_butt.py
def __init__(
    self,
    main_beam=None,
    cross_beam=None,
    mill_depth=None,
    butt_plane_spec=None,
    force_pocket=False,
    conical_tool=False,
    fastener=None,
    **kwargs,
):
    super(TButtJoint, self).__init__(
        main_beam=main_beam,
        cross_beam=cross_beam,
        mill_depth=mill_depth,
        butt_plane_spec=butt_plane_spec,
        force_pocket=force_pocket,
        conical_tool=conical_tool,
        **kwargs,
    )

    self.fasteners = []
    if fastener:
        if fastener.outline is None:
            fastener = fastener.copy()  # make a copy to avoid modifying the original fastener
            fastener.set_default(joint=self)
        self.base_fastener = fastener
        if self.base_fastener:
            self.base_fastener.place_instances(self)

Attributes¤

interactions property ¤
interactions

Returns interactions between elements used by this joint.

TDovetailJoint ¤

TDovetailJoint(
    main_beam=None,
    cross_beam=None,
    start_y=None,
    start_depth=None,
    rotation=None,
    length=None,
    width=None,
    cone_angle=None,
    dovetail_shape=None,
    tool_angle=None,
    tool_diameter=None,
    tool_height=None,
    **kwargs,
)

Represents a T-Dovetail type joint which joins two beams, one of them at its end (main) and the other one along its centerline (cross). A dovetail cut is made on the main beam, and a corresponding notch is made on the cross beam to fit the main beam.

This joint type is compatible with beams in T topology.

Please use TDovetailJoint.create() to properly create an instance of this class and associate it with a model.

Parameters:

  • main_beam (:class:`~compas_timber.elements.Beam`, default: None ) –

    First beam to be joined.

  • cross_beam (:class:`~compas_timber.elements.Beam`, default: None ) –

    Second beam to be joined.

  • start_y (float, default: None ) –

    Start position of the dovetail cut along the y-axis of the main beam.

  • start_depth (float, default: None ) –

    Depth of the dovetail cut from the surface of the main beam.

  • rotation (float, default: None ) –

    Rotation of the dovetail cut around the main beam's axis.

  • length (float, default: None ) –

    Length of the dovetail cut along the main beam.

  • width (float, default: None ) –

    Width of the dovetail cut.

  • cone_angle (float, default: None ) –

    The angle of the dovetail cut, determining the taper of the joint.

  • dovetail_shape (str, default: None ) –

    Shape of the dovetail cut. One of :class:~compas_timber.fabrication.TenonShapeType: AUTOMATIC, SQUARE, ROUND, ROUNDED, RADIUS. Defaults to TenonShapeType.RADIUS.

  • tool_angle (float, default: None ) –

    The angle of the tool used to create the dovetail cut.

  • tool_diameter (float, default: None ) –

    The diameter of the tool used to create the dovetail cut.

  • tool_height (float, default: None ) –

    The height of the tool used to create the dovetail cut.

Attributes:

  • main_beam (:class:`~compas_timber.elements.Beam`) –

    First beam to be joined.

  • cross_beam (:class:`~compas_timber.elements.Beam`) –

    Second beam to be joined.

  • start_y (float) –

    Start position of the dovetail cut along the y-axis of the main beam.

  • start_depth (float) –

    Depth of the dovetail cut from the surface of the main beam.

  • rotation (float) –

    Rotation of the dovetail cut around the main beam's axis.

  • length (float) –

    Length of the dovetail cut along the main beam.

  • width (float) –

    Width of the dovetail cut.

  • cone_angle (float) –

    The angle of the dovetail cut, determining the taper of the joint.

  • dovetail_shape (str) –

    Shape of the dovetail cut. One of :class:~compas_timber.fabrication.TenonShapeType: AUTOMATIC, SQUARE, ROUND, ROUNDED, RADIUS.

  • tool_angle (float) –

    The angle of the tool used to create the dovetail cut.

  • tool_diameter (float) –

    The diameter of the tool used to create the dovetail cut.

  • tool_height (float) –

    The height of the tool used to create the dovetail cut.

  • features (list) –

    List of features or machining processings applied to the joint.

Source code in src/compas_timber/connections/t_dovetail.py
def __init__(
    self,
    main_beam=None,
    cross_beam=None,
    start_y=None,
    start_depth=None,
    rotation=None,
    length=None,
    width=None,
    cone_angle=None,
    dovetail_shape=None,
    tool_angle=None,
    tool_diameter=None,
    tool_height=None,
    **kwargs
):
    super(TDovetailJoint, self).__init__(elements=(main_beam,cross_beam), **kwargs)
    self.start_y = start_y
    self.start_depth = start_depth
    self.rotation = rotation
    self.length = length
    self.width = width
    self.cone_angle = cone_angle
    self.dovetail_shape = dovetail_shape
    self.tool_angle = tool_angle
    self.tool_diameter = tool_diameter
    self.tool_height = tool_height

    self._height = None
    self._flank_angle = None
    self._shape_radius = None

    if self.main_beam and self.cross_beam:
        self._set_unset_attributes()

Methods:¤

add_extensions ¤
add_extensions()

Calculates and adds the necessary extensions to the beams.

This method is called during TimberModel.process_joinery(), not when the joint is created.

Raises:

Source code in src/compas_timber/connections/t_dovetail.py
def add_extensions(self):
    """Calculates and adds the necessary extensions to the beams.

    This method is called during `TimberModel.process_joinery()`, not when the joint is created.

    Raises
    ------
    BeamJoiningError
        If the extension could not be calculated.

    """
    assert self.main_beam and self.cross_beam
    try:
        cutting_plane = self.cross_beam.ref_sides[self.cross_beam_ref_side_index]
        cutting_plane.translate(-cutting_plane.normal * self.tool_height)
        start_main, end_main = self.main_beam.extension_to_plane(cutting_plane)
    except AttributeError as ae:
        raise BeamJoiningError(beams=self.elements, joint=self, debug_info=str(ae), debug_geometries=[cutting_plane])
    except Exception as ex:
        raise BeamJoiningError(beams=self.elements, joint=self, debug_info=str(ex))
    extension_tolerance = 0.01  # TODO: this should be proportional to the unit used
    self.main_beam.add_blank_extension(
        start_main + extension_tolerance,
        end_main + extension_tolerance,
        self.guid,
    )
add_features ¤
add_features()

Adds the required trimming features to both beams.

This method is called during TimberModel.process_joinery(), not when the joint is created.

Source code in src/compas_timber/connections/t_dovetail.py
def add_features(self):
    """Adds the required trimming features to both beams.

    This method is called during `TimberModel.process_joinery()`, not when the joint is created.

    """
    assert self.main_beam and self.cross_beam  # should never happen

    # define the tool parameters
    self.define_dovetail_tool(self.tool_angle, self.tool_diameter, self.tool_height)

    # generate dovetail tenon features
    main_feature = DovetailTenon.from_plane_and_beam(
        plane=self.cross_beam.ref_sides[self.cross_beam_ref_side_index],
        beam=self.main_beam,
        start_y=self.start_y,
        start_depth=self.start_depth,
        rotation=self.rotation,
        length=self.length,
        width=self.width,
        height=self._height,
        cone_angle=self.cone_angle,
        flank_angle=self._flank_angle,
        shape=self.dovetail_shape,
        shape_radius=self._shape_radius,
        ref_side_index=self.main_beam_ref_side_index,
    )

    # generate dovetail mortise features
    cross_feature = DovetailMortise.from_frame_and_beam(
        frame=main_feature.frame_from_params_and_beam(self.main_beam),
        beam=self.cross_beam,
        start_depth=0.0,  # TODO: to be updated once housing is implemented
        length=main_feature.length,
        width=main_feature.width,
        depth=main_feature.height,
        cone_angle=main_feature.cone_angle,
        flank_angle=main_feature.flank_angle,
        shape=main_feature.shape,
        shape_radius=main_feature.shape_radius,
        ref_side_index=self.cross_beam_ref_side_index,
    )

    # add features to beams
    self.main_beam.add_features(main_feature)
    self.cross_beam.add_features(cross_feature)
    # add features to joint
    self.features = [cross_feature, main_feature]
define_dovetail_tool ¤
define_dovetail_tool(tool_angle, tool_diameter, tool_height)

Define the parameters for the dovetail feature based on a defined dovetail cutting tool.

Parameters:

  • tool_angle (float) –

    The angle of the dovetail cutter tool.

  • tool_diameter (float) –

    The diameter of the dovetail cutter tool.

  • tool_height (float) –

    The height of the dovetail cutter tool.

Source code in src/compas_timber/connections/t_dovetail.py
def define_dovetail_tool(self, tool_angle, tool_diameter, tool_height):
    """Define the parameters for the dovetail feature based on a defined dovetail cutting tool.

    Parameters
    ----------
    tool_angle : float
        The angle of the dovetail cutter tool.
    tool_diameter : float
        The diameter of the dovetail cutter tool.
    tool_height : float
        The height of the dovetail cutter tool.

    """
    # type: (float, float, float) -> None
    # get the tool parameters
    tool_top_radius = tool_diameter / 2 - tool_height * (math.tan(math.radians(tool_angle)))
    # define parameters related to the tool if a tool is defined
    self._height = tool_height
    self._flank_angle = tool_angle
    self._shape_radius = tool_top_radius

TLapJoint ¤

TLapJoint(
    main_beam=None, cross_beam=None, flip_lap_side=False, cut_plane_bias=0.5, **kwargs
)

Represents a T-Lap type joint which joins the end of a beam along the length of another beam with a lap.

This joint type is compatible with beams in T topology.

Please use TLapJoint.create() to properly create an instance of this class and associate it with a model.

Parameters:

  • main_beam (:class:`~compas_timber.elements.Beam`, default: None ) –

    The main beam to be joined.

  • cross_beam (:class:`~compas_timber.elements.Beam`, default: None ) –

    The cross beam to be joined.

  • flip_lap_side (bool, default: False ) –

    If True, the lap is flipped to the other side of the beams.

  • cut_plane_bias (float, default: 0.5 ) –

    Allows lap to be shifted deeper into one beam or the other. Value should be between 0 and 1.0 without completely cutting through either beam. Default is 0.5.

Attributes:

  • main_beam (:class:`~compas_timber.elements.Beam`) –

    The main beam to be joined.

  • cross_beam (:class:`~compas_timber.elements.Beam`) –

    The cross beam to be joined.

  • flip_lap_side (bool) –

    If True, the lap is flipped to the other side of the beams.

  • cut_plane_bias (float) –

    Allows lap to be shifted deeper into one beam or the other. Value should be between 0 and 1.0 without completely cutting through either beam. Default is 0.5.

Source code in src/compas_timber/connections/t_lap.py
def __init__(self, main_beam=None, cross_beam=None, flip_lap_side=False, cut_plane_bias=0.5, **kwargs):
    super(TLapJoint, self).__init__(main_beam, cross_beam, flip_lap_side, cut_plane_bias, **kwargs)

Methods:¤

add_extensions ¤
add_extensions()

Calculates and adds the necessary extensions to the beams.

This method is called during TimberModel.process_joinery(), not when the joint is created.

Raises:

Source code in src/compas_timber/connections/t_lap.py
def add_extensions(self):
    """Calculates and adds the necessary extensions to the beams.

    This method is called during `TimberModel.process_joinery()`, not when the joint is created.

    Raises
    ------
    BeamJoiningError
        If the extension could not be calculated.

    """
    assert self.main_beam and self.cross_beam
    try:
        start_main, end_main = self.main_beam.extension_to_plane(self.cutting_plane_a)
    except AttributeError as ae:
        raise BeamJoiningError(
            beams=self.elements,
            joint=self,
            debug_info=str(ae),
            debug_geometries=[self.cutting_plane_a],
        )
    except Exception as ex:
        raise BeamJoiningError(beams=self.elements, joint=self, debug_info=str(ex))
    extension_tolerance = 0.01  # TODO: this should be proportional to the unit used
    self.main_beam.add_blank_extension(
        start_main + extension_tolerance,
        end_main + extension_tolerance,
        self.guid,
    )
add_features ¤
add_features()

Adds the required extension and trimming features to both beams.

This method is called during TimberModel.process_joinery(), not when the joint is created.

Source code in src/compas_timber/connections/t_lap.py
def add_features(self):
    """Adds the required extension and trimming features to both beams.

    This method is called during `TimberModel.process_joinery()`, not when the joint is created.

    """
    assert self.main_beam and self.cross_beam

    # create lap features
    negative_volume_main, negative_volume_cross = self._create_negative_volumes(self.cut_plane_bias)
    main_lap_feature = LapProxy.from_volume_and_beam(negative_volume_main, self.main_beam, ref_side_index=self.ref_side_index_a)
    cross_lap_feature = LapProxy.from_volume_and_beam(negative_volume_cross, self.cross_beam, ref_side_index=self.ref_side_index_b)

    # cutoff feature for main beam
    main_cut_feature = JackRafterCutProxy.from_plane_and_beam(self.cutting_plane_a, self.main_beam)

    # add processings to the beams
    self.cross_beam.add_feature(cross_lap_feature)
    self.main_beam.add_features([main_lap_feature, main_cut_feature])

    # register processings to the joint
    self.features.extend([cross_lap_feature, main_lap_feature, main_cut_feature])

TOliGinaJoint ¤

TOliGinaJoint(main_beam, cross_beam, **kwargs)
Source code in src/compas_timber/connections/oligina.py
def __init__(
    self,
    main_beam,
    cross_beam,
    **kwargs
):
    super(TOliGinaJoint, self).__init__(main_beam, cross_beam, **kwargs)

Methods:¤

add_extensions ¤
add_extensions()

Calculates and adds the necessary extensions to the beams.

This method is called during TimberModel.process_joinery(), not when the joint is created.

Raises:

  • BeamJoiningError –

    If the extension could not be calculated.

Source code in src/compas_timber/connections/oligina.py
def add_extensions(self):
    """Calculates and adds the necessary extensions to the beams.

    This method is called during `TimberModel.process_joinery()`, not when the joint is created.

    Raises
    ------
    BeamJoiningError
        If the extension could not be calculated.

    """
    assert self.main_beam and self.cross_beam
    extension_tolerance = 0.01  # TODO: this should be proportional to the unit used

    start_main, end_main = self.get_main_extension()
    self.main_beam.add_blank_extension(
        start_main + extension_tolerance,
        end_main + extension_tolerance,
        self.guid,
    )

TStepJoint ¤

TStepJoint(
    main_beam=None,
    cross_beam=None,
    step_shape=None,
    step_depth=None,
    heel_depth=None,
    tenon_mortise_height=None,
    **kwargs,
)

Represents an T-Step type joint which joins two beams, one of them at it's end (main) and the other one along it's centerline (cross). Two or more cuts are is made on the main beam and a notch is made on the cross beam to fit the main beam.

This joint type is compatible with beams in T topology.

Please use TStepJoint.create() to properly create an instance of this class and associate it with a model.

Parameters:

  • main_beam (:class:`~compas_timber.elements.Beam`, default: None ) –

    First beam to be joined.

  • cross_beam (:class:`~compas_timber.elements.Beam`, default: None ) –

    Second beam to be joined.

  • step_shape (str, default: None ) –

    The shape of the step cut. One of :class:~compas_timber.fabrication.StepShapeType: STEP, HEEL, TAPERED_HEEL, or DOUBLE. The shape type takes priority: depths irrelevant to the chosen shape are ignored and forced to zero. Defaults to StepShapeType.STEP.

  • step_depth (float, default: None ) –

    Depth of the step cut. Applicable only to shape types with a step component (STEP, DOUBLE). Defaults to a value proportional to the cross beam's cross-section.

  • heel_depth (float, default: None ) –

    Depth of the heel cut. Applicable only to shape types with a heel component (HEEL, TAPERED_HEEL, DOUBLE). Defaults to a value proportional to the cross beam's cross-section.

  • tenon_mortise_height (float, default: None ) –

    Height of the tenon/mortise feature. If None, no tenon or mortise is created.

Attributes:

  • main_beam (:class:`~compas_timber.elements.Beam`) –

    First beam to be joined.

  • cross_beam (:class:`~compas_timber.elements.Beam`) –

    Second beam to be joined.

  • step_shape (str) –

    The shape of the step cut. One of :class:~compas_timber.fabrication.StepShapeType: STEP, HEEL, TAPERED_HEEL, or DOUBLE.

  • step_depth (float) –

    Depth of the step cut. Applicable only to shape types with a step component (STEP, DOUBLE). Zero otherwise.

  • heel_depth (float) –

    Depth of the heel cut. Applicable only to shape types with a heel component (HEEL, TAPERED_HEEL, DOUBLE). Zero otherwise.

  • tenon_mortise_height (float) –

    Height of the tenon/mortise feature. None if not created.

Source code in src/compas_timber/connections/t_step_joint.py
def __init__(
    self,
    main_beam=None,
    cross_beam=None,
    step_shape=None,
    step_depth=None,
    heel_depth=None,
    tenon_mortise_height=None,
    **kwargs
):
    super(TStepJoint, self).__init__(elements=(main_beam,cross_beam), **kwargs)
    self.step_shape = step_shape or StepShapeType.STEP
    self._tapered_heel = self.step_shape == StepShapeType.TAPERED_HEEL

    self.step_depth = step_depth
    self.heel_depth = heel_depth
    self.tenon_mortise_height = tenon_mortise_height

    if self.main_beam and self.cross_beam:
        self._set_unset_attributes()  # resolve defaults at init if beams are provided

Methods:¤

add_extensions ¤
add_extensions()

Calculates and adds the necessary extensions to the beams.

This method is called during TimberModel.process_joinery(), not when the joint is created.

Raises:

Source code in src/compas_timber/connections/t_step_joint.py
def add_extensions(self):
    """Calculates and adds the necessary extensions to the beams.

    This method is called during `TimberModel.process_joinery()`, not when the joint is created.

    Raises
    ------
    BeamJoiningError
        If the extension could not be calculated.

    """
    assert self.cross_beam and self.main_beam
    start_a = None
    plane_a = None
    try:
        plane_a = self.main_extension_plane
        start_a, end_a = self.main_beam.extension_to_plane(plane_a)
    except AttributeError as ae:
        # I want here just the plane that caused the error
        raise BeamJoiningError(self.elements, self, debug_info=str(ae), debug_geometries=[plane_a] if plane_a else [])
    except Exception as ex:
        raise BeamJoiningError(self.elements, self, debug_info=str(ex), debug_geometries=[plane_a] if plane_a else [])
    self.main_beam.add_blank_extension(start_a, end_a, self.guid)
add_features ¤
add_features()

Adds the required trimming features to both beams.

This method is called during TimberModel.process_joinery(), not when the joint is created.

Source code in src/compas_timber/connections/t_step_joint.py
def add_features(self):
    """Adds the required trimming features to both beams.

    This method is called during `TimberModel.process_joinery()`, not when the joint is created.

    """
    assert self.main_beam and self.cross_beam  # should never happen

    # get dimensions for main and cross beams
    main_width, main_height = self.main_beam.get_dimensions_relative_to_side(self.main_beam_ref_side_index)
    cross_width, _ = self.cross_beam.get_dimensions_relative_to_side(self.cross_beam_ref_side_index)

    # generate step joint features
    main_feature = StepJoint.from_plane_and_beam(
        self.cross_beam.ref_sides[self.cross_beam_ref_side_index],
        self.main_beam,
        step_depth=self.step_depth,
        heel_depth=self.heel_depth,
        tapered_heel=self._tapered_heel,
        ref_side_index=self.main_beam_ref_side_index,
    )

    # generate step joint notch features
    cross_feature = StepJointNotch.from_plane_and_beam(
        self.main_beam.ref_sides[self.main_beam_ref_side_index],
        self.cross_beam,
        start_y=(cross_width - main_width) / 2 if cross_width > main_width else 0.0,
        notch_width=main_width,
        step_depth=self.step_depth,
        heel_depth=self.heel_depth,
        strut_height=main_height,
        tapered_heel=self._tapered_heel,
        ref_side_index=self.cross_beam_ref_side_index,
    )

    # add tenon and mortise features
    if self.tenon_mortise_height:
        tenon_mortise_width = main_width/4
        cross_feature.add_mortise(tenon_mortise_width, self.tenon_mortise_height)
        main_feature.add_tenon(tenon_mortise_width, self.tenon_mortise_height)

    # add features to beams
    self.main_beam.add_features(main_feature)
    self.cross_beam.add_features(cross_feature)
    # add features to joint
    self.features = [cross_feature, main_feature]
check_elements_compatibility classmethod ¤
check_elements_compatibility(elements, raise_error=False)

Checks if the cluster of beams complies with the requirements for the TStepJoint.

Parameters:

  • elements (list of :class:`~compas_timber.model.TimberElement`) –

    The cluster of elements to be checked.

  • raise_error (bool, default: False ) –

    Whether to raise an error if the elements are not compatible. If False, the method will return False instead of raising an error.

Returns:

  • bool –

    True if the cluster complies with the requirements, False otherwise.

Source code in src/compas_timber/connections/t_step_joint.py
@classmethod
def check_elements_compatibility(cls, elements, raise_error=False):
    """Checks if the cluster of beams complies with the requirements for the TStepJoint.

    Parameters
    ----------
    elements : list of :class:`~compas_timber.model.TimberElement`
        The cluster of elements to be checked.
    raise_error : bool, optional
        Whether to raise an error if the elements are not compatible.
        If False, the method will return False instead of raising an error.

    Returns
    -------
    bool
        True if the cluster complies with the requirements, False otherwise.

    """
    cross_vect = elements[0].centerline.direction.cross(elements[1].centerline.direction)
    for beam in elements:
        beam_normal = beam.frame.normal.unitized()
        dot = abs(beam_normal.dot(cross_vect.unitized()))
        if not (TOL.is_zero(dot) or TOL.is_close(dot, 1)):
            if not raise_error:
                return False
            raise BeamJoiningError(elements, cls, debug_info="The the two beams are not aligned to create a Step joint.")

    return True

TTenonMortiseJoint ¤

TTenonMortiseJoint(
    main_beam=None,
    cross_beam=None,
    start_y=None,
    start_depth=None,
    rotation=None,
    length=None,
    width=None,
    height=None,
    tenon_shape=None,
    shape_radius=None,
    **kwargs,
)

Represents a TenonMortise type joint which joins two beams, one of them at its end (main) and the other one along its centerline (cross) or both of them at their ends. A tenon is added on the main beam, and a corresponding mortise is made on the cross beam to fit the main beam's tenon.

This joint type is compatible with beams in T topology.

Please use TTenonMortiseJoint.create() to properly create an instance of this class and associate it with a model.

Parameters:

  • main_beam (:class:`~compas_timber.elements.Beam`, default: None ) –

    First beam to be joined.

  • cross_beam (:class:`~compas_timber.elements.Beam`, default: None ) –

    Second beam to be joined.

  • start_y (float, default: None ) –

    Start position of the tenon along the y-axis of the main beam.

  • start_depth (float, default: None ) –

    Depth of the tenon from the surface of the main beam.

  • rotation (float, default: None ) –

    Rotation of the tenon around the main beam's axis.

  • length (float, default: None ) –

    Length of the tenon along the main beam.

  • width (float, default: None ) –

    Width of the tenon.

  • height (float, default: None ) –

    Height of the tenon.

  • tenon_shape (str, default: None ) –

    Shape of the tenon. One of :class:~compas_timber.fabrication.TenonShapeType: AUTOMATIC, SQUARE, ROUND, ROUNDED, RADIUS. Defaults to TenonShapeType.ROUND.

  • shape_radius (float, default: None ) –

    The radius used to define the shape of the tenon, if applicable.

Attributes:

  • main_beam (:class:`~compas_timber.elements.Beam`) –

    First beam to be joined.

  • cross_beam (:class:`~compas_timber.elements.Beam`) –

    Second beam to be joined.

  • main_beam_guid (str) –

    GUID of the main beam.

  • cross_beam_guid (str) –

    GUID of the cross beam.

  • start_y (float) –

    Start position of the tenon along the y-axis of the main beam.

  • start_depth (float) –

    Depth of the tenon from the surface of the main beam.

  • rotation (float) –

    Rotation of the tenon around the main beam's axis.

  • length (float) –

    Length of the tenon along the main beam.

  • width (float) –

    Width of the tenon.

  • height (float) –

    Height of the tenon.

  • tenon_shape (str) –

    Shape of the tenon. One of :class:~compas_timber.fabrication.TenonShapeType: AUTOMATIC, SQUARE, ROUND, ROUNDED, RADIUS.

  • shape_radius (float) –

    The radius used to define the shape of the tenon, if applicable.

  • features (list) –

    List of features or machining processings applied to the elements.

Source code in src/compas_timber/connections/mortise_tenon.py
def __init__(
    self,
    main_beam=None,
    cross_beam=None,
    start_y=None,
    start_depth=None,
    rotation=None,
    length=None,
    width=None,
    height=None,
    tenon_shape=None,
    shape_radius=None,
    **kwargs
):
    super(MortiseTenonJoint, self).__init__(elements=(main_beam, cross_beam), **kwargs)
    self.start_y = start_y
    self.start_depth = start_depth
    self.rotation = rotation
    self.length = length
    self.width = width
    self.height = height
    self.tenon_shape = tenon_shape
    self.shape_radius = shape_radius

    if self.main_beam and self.cross_beam:
        self._set_unset_attributes()

Methods:¤

add_extensions ¤
add_extensions()

Calculates and adds the necessary extensions to the beams.

This method is called during TimberModel.process_joinery(), not when the joint is created.

Raises:

  • BeamJoiningError –

    If the extension could not be calculated.

Source code in src/compas_timber/connections/t_tenon_mortise.py
def add_extensions(self):
    """Calculates and adds the necessary extensions to the beams.

    This method is called during `TimberModel.process_joinery()`, not when the joint is created.

    Raises
    ------
    BeamJoiningError
        If the extension could not be calculated.

    """
    assert self.main_beam and self.cross_beam
    extension_tolerance = 0.01  # TODO: this should be proportional to the unit used

    start_main, end_main = self.get_main_extension()
    self.main_beam.add_blank_extension(
        start_main + extension_tolerance,
        end_main + extension_tolerance,
        self.guid,
    )
add_features ¤
add_features()

Adds the required trimming features to both beams.

This method is called during TimberModel.process_joinery(), not when the joint is created.

Source code in src/compas_timber/connections/t_tenon_mortise.py
def add_features(self):
    """Adds the required trimming features to both beams.

    This method is called during `TimberModel.process_joinery()`, not when the joint is created.

    """
    assert self.main_beam and self.cross_beam  # should never happen

    main_feature = self._create_tenon_feature()
    cross_feature = self._create_mortise_feature(main_feature)

    # add features to beams
    self.main_beam.add_features(main_feature)
    self.cross_beam.add_features(cross_feature)
    # add features to joint
    self.features = [cross_feature, main_feature]

XLapJoint ¤

XLapJoint(beam_a=None, beam_b=None, flip_lap_side=False, cut_plane_bias=0.5, **kwargs)

Represents an X-Lap type joint which joins the two beams somewhere along their length with a lap.

This joint type is compatible with beams in X topology.

Please use XLapJoint.create() to properly create an instance of this class and associate it with a model.

Parameters:

  • beam_a (:class:`~compas_timber.elements.Beam`, default: None ) –

    The first beam to be joined.

  • beam_b (:class:`~compas_timber.elements.Beam`, default: None ) –

    The second beam to be joined.

  • flip_lap_side (bool, default: False ) –

    If True, the lap is flipped to the other side of the beams.

  • cut_plane_bias (float, default: 0.5 ) –

    Allows lap to be shifted deeper into one beam or the other. Value should be between 0 and 1.0 without completely cutting through either beam. Default is 0.5.

Attributes:

  • beam_a (:class:`~compas_timber.elements.Beam`) –

    The first beam to be joined.

  • beam_b (:class:`~compas_timber.elements.Beam`) –

    The second beam to be joined.

  • flip_lap_side (bool) –

    If True, the lap is flipped to the other side of the beams.

  • cut_plane_bias (float) –

    Allows lap to be shifted deeper into one beam or the other. Value should be between 0 and 1.0 without completely cutting through either beam. Default is 0.5.

Source code in src/compas_timber/connections/lap_joint.py
def __init__(self, beam_a=None, beam_b=None, flip_lap_side=False, cut_plane_bias=0.5, **kwargs):
    super(LapJoint, self).__init__(elements=(beam_a, beam_b), **kwargs)
    self.flip_lap_side = flip_lap_side
    self.cut_plane_bias = cut_plane_bias

    self._ref_side_index_a = None
    self._ref_side_index_b = None

Methods:¤

add_features ¤
add_features()

Adds the required extension and trimming features to both beams.

This method is called during TimberModel.process_joinery(), not when the joint is created.

Source code in src/compas_timber/connections/x_lap.py
def add_features(self):
    """Adds the required extension and trimming features to both beams.

    This method is called during `TimberModel.process_joinery()`, not when the joint is created.

    """
    assert self.beam_a and self.beam_b

    # create lap features
    negative_volume_a, negative_volume_b = self._create_negative_volumes(self.cut_plane_bias)

    lap_feature_a = LapProxy.from_volume_and_beam(negative_volume_a, self.beam_a, ref_side_index=self.ref_side_index_a)
    lap_feature_b = LapProxy.from_volume_and_beam(negative_volume_b, self.beam_b, ref_side_index=self.ref_side_index_b)

    # add features to the beams
    self.beam_a.add_features(lap_feature_a)
    self.beam_b.add_features(lap_feature_b)

    # register processings to the joint
    self.features.extend([lap_feature_a, lap_feature_b])

XNotchJoint ¤

XNotchJoint(notch_beam=None, solid_beam=None, **kwargs)

Represents an X-Notch type joint which joins the two beams somewhere along their length with a notch applied on the main_beam. This joint type is typically used to connect two beams whose centerlines are offseted from each other, resulting in one resting on top of the other through a notch.

This joint type is compatible with beams in X topology.

Please use XNotchJoint.create() to properly create an instance of this class and associate it with a model.

Parameters:

  • notch_beam (:class:`~compas_timber.elements.Beam`, default: None ) –

    The first beam to be joined. This beam will have a notch applied to it.

  • solid_beam (:class:`~compas_timber.elements.Beam`, default: None ) –

    The second beam to be joined. No features are applied to this beam.

Attributes:

  • notch_beam (:class:`~compas_timber.elements.Beam`) –

    The first beam to be joined. This beam will have a notch applied to it.

  • solid_beam (:class:`~compas_timber.elements.Beam`) –

    The second beam to be joined. No features are applied to this beam.

Source code in src/compas_timber/connections/x_notch.py
def __init__(self, notch_beam=None, solid_beam=None, **kwargs):
    super(XNotchJoint, self).__init__(elements=(notch_beam, solid_beam), **kwargs)
    self._main_ref_side_index = None

Attributes¤

main_ref_side_index property ¤
main_ref_side_index

The reference side index of the main beam.

Methods:¤

add_features ¤
add_features()

Adds the required extension and trimming features to both beams.

This method is called during TimberModel.process_joinery(), not when the joint is created.

Source code in src/compas_timber/connections/x_notch.py
def add_features(self):
    """Adds the required extension and trimming features to both beams.

    This method is called during `TimberModel.process_joinery()`, not when the joint is created.

    """
    assert self.notch_beam and self.solid_beam

    # create pocket features
    negative_volume = self._create_negative_volume()
    pocket_feature = PocketProxy.from_volume_and_element(negative_volume, self.notch_beam, ref_side_index=self.main_ref_side_index)

    # add features to the beams
    self.notch_beam.add_features(pocket_feature)

    # register processings to the joint
    self.features.append(pocket_feature)

YButtJoint ¤

YButtJoint(
    main_beam=None, cross_beam_a=None, cross_beam_b=None, mill_depth=None, **kwargs
)

Represents a Y-Butt type joint which joins the ends of three beams, trimming the main beam with a double cut and the cross beams with a miter cut.

Please use YButtJoint.create() to properly create an instance of this class and associate it with a model.

Parameters:

  • main_beam (:class:`~compas_timber.elements.Beam`, default: None ) –

    The main beam to be joined.

  • cross_beam_a (:class:`~compas_timber.elements.Beam`, default: None ) –

    The first cross beam to be joined.

  • cross_beam_b (:class:`~compas_timber.elements.Beam`, default: None ) –

    The second cross beam to be joined.

  • mill_depth (float, default: None ) –

    The depth of the pocket to be milled in the cross beams.

Attributes:

  • main_beam (:class:`~compas_timber.elements.Beam`) –

    The main beam to be joined.

  • cross_beams (:class:`~compas_timber.elements.Beam`) –

    The cross beam to be joined.

  • mill_depth (float) –

    The depth of the pocket to be milled in the cross beams.

Source code in src/compas_timber/connections/y_butt.py
def __init__(self, main_beam=None, cross_beam_a=None, cross_beam_b=None, mill_depth=None, **kwargs):
    super(YButtJoint, self).__init__(elements=(main_beam, cross_beam_a, cross_beam_b), **kwargs)
    self.mill_depth = mill_depth

Methods:¤

add_extensions ¤
add_extensions()

Calculates and adds the necessary extensions to the beams.

This method is called during TimberModel.process_joinery(), not when the joint is created.

Raises:

Source code in src/compas_timber/connections/y_butt.py
def add_extensions(self):
    """Calculates and adds the necessary extensions to the beams.

    This method is called during `TimberModel.process_joinery()`, not when the joint is created.

    Raises
    ------
    BeamJoiningError
        If the extension could not be calculated.

    """
    assert self.main_beam and self.cross_beams[0] and self.cross_beams[1]
    extensions = []
    for beam in self.cross_beams:
        try:
            cutting_plane = beam.ref_sides[self.cross_beam_ref_side_index(beam)]
            if self.mill_depth:
                cutting_plane.translate(-cutting_plane.normal * self.mill_depth)
            extensions.append(self.main_beam.extension_to_plane(cutting_plane))
        except AttributeError as ae:
            raise BeamJoiningError(beams=self.elements, joint=self, debug_info=str(ae), debug_geometries=[cutting_plane])
        except Exception as ex:
            raise BeamJoiningError(beams=self.elements, joint=self, debug_info=str(ex))
    start_main, end_main = max(extensions[0][0], extensions[1][0]), max(extensions[0][1], extensions[1][1])
    extension_tolerance = 0.01  # TODO: this should be proportional to the unit used
    self.main_beam.add_blank_extension(
        start_main + extension_tolerance,
        end_main + extension_tolerance,
        self.guid,
    )

    start_a, start_b = None, None
    plane_a, plane_b, start_a, end_a, start_b, end_b = None, None, None, None, None, None
    try:
        plane_a, plane_b = self.get_miter_planes(*self.cross_beams)
        start_a, end_a = self.cross_beams[0].extension_to_plane(plane_a)
        start_b, end_b = self.cross_beams[1].extension_to_plane(plane_b)
    except AttributeError as ae:
        # I want here just the plane that caused the error
        geometries = [plane_b] if start_a is not None else [plane_a]
        raise BeamJoiningError(self.elements, self, debug_info=str(ae), debug_geometries=geometries)
    except Exception as ex:
        raise BeamJoiningError(self.elements, self, debug_info=str(ex))
    self.cross_beams[0].add_blank_extension(start_a, end_a, self.guid)
    self.cross_beams[1].add_blank_extension(start_b, end_b, self.guid)
add_features ¤
add_features()

Adds the required extension and trimming features to both beams.

This method is called during TimberModel.process_joinery(), not when the joint is created.

Source code in src/compas_timber/connections/y_butt.py
def add_features(self):
    """Adds the required extension and trimming features to both beams.

    This method is called during `TimberModel.process_joinery()`, not when the joint is created.

    """

    assert self.main_beam and self.cross_beams[0] and self.cross_beams[1]

    # get the cutting planes for the main beam
    planes = []
    for beam in self.cross_beams:
        cutting_plane = Plane.from_frame(beam.ref_sides[self.cross_beam_ref_side_index(beam)])
        if self.mill_depth:
            cutting_plane.translate(-cutting_plane.normal * self.mill_depth)
        planes.append(cutting_plane)
    for pl, b in zip(planes, self.cross_beams):
        pl.point = pl.closest_point(b.centerline.midpoint)
    if TOL.is_close(dot_vectors(planes[0].normal, planes[1].normal), 1.0):
        main_feature = JackRafterCut.from_plane_and_beam(Plane(planes[0].point, -planes[0].normal), self.main_beam)
    else:
        main_feature = DoubleCut.from_planes_and_beam(planes, self.main_beam)
    self.main_beam.add_features(main_feature)
    self.features = [main_feature]

    # apply the pockets on the cross beams
    if self.mill_depth:
        for beam in self.cross_beams:
            ref_side_index = self.main_beam_ref_side_index(beam)
            cross_cutting_plane = self.main_beam.ref_sides[ref_side_index]
            lap_length = self.main_beam.get_dimensions_relative_to_side(ref_side_index)[1]
            cross_feature = Lap.from_plane_and_beam(
                cross_cutting_plane,
                beam,
                lap_length,
                self.mill_depth,
                ref_side_index=self.cross_beam_ref_side_index(beam),
            )
            beam.add_features(cross_feature)
            self.features.append(cross_feature)

    # add miter features on cross_beams
    try:
        plane_a, plane_b = self.get_miter_planes(*self.cross_beams)
    except Exception as ex:
        raise BeamJoiningError(self.elements, self, debug_info=str(ex))
    cut1 = JackRafterCut.from_plane_and_beam(plane_a, self.cross_beams[0])
    cut2 = JackRafterCut.from_plane_and_beam(plane_b, self.cross_beams[1])
    self.cross_beams[0].add_features(cut1)
    self.cross_beams[1].add_features(cut2)
    self.features.extend([cut1, cut2])
check_elements_compatibility classmethod ¤
check_elements_compatibility(elements, raise_error=False)

Checks if the cluster of beams complies with the requirements for the YButtJoint.

Parameters:

  • elements (list of :class:`~compas_timber.elements.Beam`) –

    The beams to check.

  • raise_error (bool, default: False ) –

    If True, raises a BeamJoiningError if the requirements are not met.

Returns:

  • bool –

    True if the cluster complies with the requirements, False otherwise.

Source code in src/compas_timber/connections/y_butt.py
@classmethod
def check_elements_compatibility(cls, elements, raise_error=False):
    """Checks if the cluster of beams complies with the requirements for the YButtJoint.

    Parameters
    ----------
    elements : list of :class:`~compas_timber.elements.Beam`
        The beams to check.
    raise_error : bool, optional
        If True, raises a `BeamJoiningError` if the requirements are not met.

    Returns
    -------
    bool
        True if the cluster complies with the requirements, False otherwise.

    """
    if not are_beams_aligned_with_cross_vector(*elements[1:3]):
        if not raise_error:
            return False
        raise BeamJoiningError(
            beams=elements[1:3],
            joint=cls,
            debug_info="The two cross beams are not coplanar to create a Y-Butt joint.",
        )
    # calculate widths and heights of the cross beams
    else:
        dimensions = []
        for beam in elements[1:3]:
            ref_side_dict = beam_ref_side_incidence(elements[0], beam, ignore_ends=True)
            ref_side_index = min(ref_side_dict, key=ref_side_dict.get)
            dimensions.append(beam.get_dimensions_relative_to_side(ref_side_index)[0])  # beams only need a miter that meets in the corner. width can be different
        # check if the dimensions of both cross beams match
        if dimensions[0] != dimensions[1]:
            if not raise_error:
                return False
            raise BeamJoiningError(elements, cls, debug_info="The two cross beams must have the same dimensions to create a Y-Butt joint.")
    return True

Functions:¤

angle_and_dot_product_beam_a_and_beam_b ¤

angle_and_dot_product_beam_a_and_beam_b(
    beam_a: Beam, beam_b: Beam, joint: Joint
) -> tuple[float, float]

Computes the angle and dot product between the beam_a and the beam_b relative to their joint. The angle and dot products are computed with the direction of the beam_a going towards the joint.

Parameters:

  • beam_a (:class:`~compas_timber.elements.Beam`) –

    The main beam of the joint.

  • beam_b (:class:`~compas_timber.elements.Beam`) –

    The cross beam of the joint.

  • joint (:class:`~compas_timber.connections.joint.Joint`) –

    The joint connecting the main beam and the cross beam.

Returns:

  • tuple[float, float] –

    A tuple containing the angle (in radians) and the dot product between the main beam and the cross beam relative to their joint.

Source code in src/compas_timber/connections/utilities.py
def angle_and_dot_product_beam_a_and_beam_b(beam_a: Beam, beam_b: Beam, joint: Joint) -> tuple[float, float]:
    """
    Computes the angle and dot product between the `beam_a` and the `beam_b` relative to their joint.
    The angle and dot products are computed with the direction of the `beam_a` going towards the joint.

    Parameters
    ----------
    beam_a : :class:`~compas_timber.elements.Beam`
        The main beam of the joint.
    beam_b : :class:`~compas_timber.elements.Beam`
        The cross beam of the joint.
    joint : :class:`~compas_timber.connections.joint.Joint`
        The joint connecting the main beam and the cross beam.

    Returns
    -------
    tuple[float, float]
        A tuple containing the angle (in radians) and the dot product between the main beam and the cross beam relative to their joint.

    """
    main_beam_direction = joint.get_beam_direction_towards_joint(beam_a)
    angle = angle_vectors(main_beam_direction, beam_b.centerline.direction)
    dot = dot_vectors(main_beam_direction, beam_b.centerline.direction)
    return angle, dot

beam_ref_side_incidence ¤

beam_ref_side_incidence(beam_a, beam_b, ignore_ends=True)

Returns a map of ref_side indices of beam_b and the angle of their normal with beam_a's centerline.

This is used to find a cutting plane when joining the two beams.

Parameters:

  • beam_a (:class:`~compas_timber.elements.Beam`) –

    The beam that attaches with one of its ends to the side of beam_b.

  • beam_b (:class:`~compas_timber.elements.Beam`) –

    The other beam.

  • ignore_ends (bool, default: True ) –

    If True, only the first four ref_sides of beam_b are considered. Otherwise all ref_sides are considered.

Returns:

  • dict(int, float) –

    A map of ref_side indices of beam_b and their respective angle with beam_a's centerline.

Source code in src/compas_timber/connections/utilities.py
def beam_ref_side_incidence(beam_a, beam_b, ignore_ends=True):
    """Returns a map of ref_side indices of beam_b and the angle of their normal with beam_a's centerline.

    This is used to find a cutting plane when joining the two beams.

    Parameters
    ----------
    beam_a : :class:`~compas_timber.elements.Beam`
        The beam that attaches with one of its ends to the side of beam_b.
    beam_b : :class:`~compas_timber.elements.Beam`
        The other beam.
    ignore_ends : bool, optional
        If True, only the first four ref_sides of `beam_b` are considered. Otherwise all ref_sides are considered.

    Returns
    -------
    dict(int, float)
        A map of ref_side indices of beam_b and their respective angle with beam_a's centerline.

    """
    # find the orientation of beam_a's centerline so that it's pointing outward of the joint
    # find the closest end
    p1x, _ = intersection_line_line(beam_a.centerline, beam_b.centerline)
    if p1x is None:
        raise ValueError("The two beams do not intersect with each other")

    end, _ = beam_a.endpoint_closest_to_point(Point(*p1x))

    if end == "start":
        centerline_vec = beam_a.centerline.vector
    else:
        centerline_vec = beam_a.centerline.vector * -1

    if ignore_ends:
        beam_b_ref_sides = beam_b.ref_sides[:4]
    else:
        beam_b_ref_sides = beam_b.ref_sides

    ref_side_angles = {}
    for ref_side_index, ref_side in enumerate(beam_b_ref_sides):
        ref_side_angles[ref_side_index] = angle_vectors(ref_side.normal, centerline_vec)

    return ref_side_angles

beam_ref_side_incidence_with_vector ¤

beam_ref_side_incidence_with_vector(beam_a, vector, ignore_ends=True)

Returns a map of ref_side indices of beam_b and the angle of their normal with a given vector.

This is used to find a cutting plane when joining two beams where one beam is represented by the normal of one of it's reference sides.

Parameters:

  • beam_a (:class:`~compas_timber.elements.Beam`) –

    The beam for which ref_side angles will be calculated.

  • vector (:class:`~compas.geometry.Vector`) –

    The vector to compare against the ref_sides' normals.

  • ignore_ends (bool, default: True ) –

    If True, only the first four ref_sides of beam_a are considered. Otherwise all ref_sides are considered.

Returns:

  • dict(int, float) –

    A map of ref_side indices of beam_a and their respective angle with the given vector.

Source code in src/compas_timber/connections/utilities.py
def beam_ref_side_incidence_with_vector(beam_a, vector, ignore_ends=True):
    """
    Returns a map of ref_side indices of beam_b and the angle of their normal with a given vector.

    This is used to find a cutting plane when joining two beams where one beam is represented by the normal of one of it's reference sides.

    Parameters
    ----------
    beam_a : :class:`~compas_timber.elements.Beam`
        The beam for which ref_side angles will be calculated.
    vector : :class:`~compas.geometry.Vector`
        The vector to compare against the ref_sides' normals.
    ignore_ends : bool, optional
        If True, only the first four ref_sides of `beam_a` are considered. Otherwise all ref_sides are considered.

    Returns
    -------
    dict(int, float)
        A map of ref_side indices of beam_a and their respective angle with the given vector.

    """
    if ignore_ends:
        beam_a_ref_sides = beam_a.ref_sides[:4]
    else:
        beam_a_ref_sides = beam_a.ref_sides

    ref_side_angles = {}
    for ref_side_index, ref_side in enumerate(beam_a_ref_sides):
        ref_side_angles[ref_side_index] = angle_vectors(vector, ref_side.normal)

    return ref_side_angles

find_neighboring_elements ¤

find_neighboring_elements(elements, inflate_by=0.0)

Finds neighboring pairs of beams in the given list of beams, using R-tree search.

The inputs to the R-tree algorithm are the axis-aligned bounding boxes of the beams (beam.aabb), enlarged by the inflate_by amount. The returned elements are sets containing pairs of Beam objects.

Parameters:

  • elements (list(:class:`~compas_timber.elements.Beam`)) –

    The list of beams in which neighboring beams should be identified.

  • inflate_by ((optional, float), default: 0.0 ) –

    A value in design units by which the regarded bounding boxes should be inflated.

Returns:

  • list(set(:class:`~compas_timber.elements.Beam`, :class:`~compas_timber.elements.Beam`)) –
Notes

This is a pluggable. In order to use this function, a compatible plugin has to be available. For example, in Rhino, the function :func:~compas_timber.rhino.find_neighboring_elements will be used.

Source code in src/compas_timber/connections/solver.py
@pluggable(category="solvers")
def find_neighboring_elements(elements, inflate_by=0.0):
    """Finds neighboring pairs of beams in the given list of beams, using R-tree search.

    The inputs to the R-tree algorithm are the axis-aligned bounding boxes of the beams (beam.aabb), enlarged by the `inflate_by` amount.
    The returned elements are sets containing pairs of Beam objects.

    Parameters
    ----------
    elements : list(:class:`~compas_timber.elements.Beam`)
        The list of beams in which neighboring beams should be identified.
    inflate_by : optional, float
        A value in design units by which the regarded bounding boxes should be inflated.

    Returns
    -------
    list(set(:class:`~compas_timber.elements.Beam`, :class:`~compas_timber.elements.Beam`))

    Notes
    -----
    This is a `pluggable`. In order to use this function, a compatible `plugin` has to be available.
    For example, in Rhino, the function :func:`~compas_timber.rhino.find_neighboring_elements` will be used.

    """
    raise NotImplementedError

get_clusters_from_joint_candidates ¤

get_clusters_from_joint_candidates(candidates, max_distance=None, exclude=None)

Gets a sorted list of Cluster objects from a model's JointCandidates run model.connect_adjacent_beams() first to populate the model's joint_candidates

Parameters:

  • candiates –

    The joint candidates to be grouped into Clusters

  • max_distance (float, default: None ) –

    Maximum distance between joints to be considered co-located.

  • exclude (set[Joint] | None, default: None ) –

    Joints to exclude from clustering.

Returns:

  • list[:class:`~compas_timber.connections.Cluster`] –

    Clusters sorted largest-first.

Source code in src/compas_timber/connections/cluster.py
def get_clusters_from_joint_candidates(candidates, max_distance=None, exclude=None):
    """Gets a sorted list of Cluster objects from a model's JointCandidates
    run model.connect_adjacent_beams() first to populate the model's joint_candidates

    Parameters
    ----------
    candiates: list[:class:`~compas_timber.connections.JointCandidate`]
        The joint candidates to be grouped into Clusters
    max_distance : float
        Maximum distance between joints to be considered co-located.
    exclude : set[Joint] | None
        Joints to exclude from clustering.

    Returns
    -------
    list[:class:`~compas_timber.connections.Cluster`]
        Clusters sorted largest-first.
    """

    max_distance = max_distance or TOL.absolute
    exclude = exclude or set()
    active_joints = [joint for joint in candidates if joint not in exclude]
    active_joints.sort(key=lambda j: j.location[0])  # ensure a deterministic order for caching and testing)
    if not active_joints:
        return []

    kd_tree = KDTree([j.location for j in active_joints])

    # Get all pairs of joints whose distance is within max_distance.
    # TODO: upstream to compas.geometry.KDTree and kd_tree.query_pairs(max_distance)

    # Each joint starts as its own cluster, represented by its own index as root.
    cluster_index_per_joint = list(range(len(active_joints)))

    def get_cluster_index(joint_index):
        # if value is the index, then the joint's cluster index is the joint_index (that joint is cluster root)
        while cluster_index_per_joint[joint_index] != joint_index:
            # set this joint's cluster index to the cluster it points to.
            cluster_index_per_joint[joint_index] = cluster_index_per_joint[cluster_index_per_joint[joint_index]]
            joint_index = cluster_index_per_joint[joint_index]
        return joint_index

    def merge_clusters(joint_index_a, joint_index_b):
        cluster_index_a = get_cluster_index(joint_index_a)
        cluster_index_b = get_cluster_index(joint_index_b)
        if cluster_index_a != cluster_index_b:
            cluster_index_per_joint[cluster_index_b] = cluster_index_a

    joint_pairs = kd_tree.query_pairs(max_distance)
    for joint_index_a, joint_index_b in joint_pairs:
        merge_clusters(joint_index_a, joint_index_b)

    joints_by_cluster_index = defaultdict(list)
    for joint_index, joint in enumerate(active_joints):
        joints_by_cluster_index[get_cluster_index(joint_index)].append(joint)

    grouped_joints = sorted(joints_by_cluster_index.values(), key=len, reverse=True)

    return [Cluster(joint_group) for joint_group in grouped_joints]

get_connection_candidate ¤

get_connection_candidate(element_a, element_b, max_distance)

Builds the joint candidate for a pair of adjacent elements, or None if their type combination is unsupported or their topology is unknown.

Source code in src/compas_timber/connections/candidate_dispatch.py
def get_connection_candidate(element_a, element_b, max_distance):
    """Builds the joint candidate for a pair of adjacent elements, or ``None`` if their type combination is unsupported or their topology is unknown."""
    handler = find_connection_handler(element_a, element_b)
    if handler is None:
        return None
    return handler(element_a, element_b, max_distance)

get_topology_from_joints ¤

get_topology_from_joints(joints)

Returns the composite topology (TOPO_Y/TOPO_K) from a list of joints.

Source code in src/compas_timber/connections/cluster.py
def get_topology_from_joints(joints):
    """Returns the composite topology (TOPO_Y/TOPO_K) from a list of joints."""
    # TOPO_Y + TOPO_I = TOPO_Y
    # TOPO_Y + TOPO_L = TOPO_Y
    # TOPO_Y + TOPO_T = TOPO_K
    # TOPO_K + TOPO_I = TOPO_K
    # TOPO_K + TOPO_L = TOPO_K ...
    if len(joints) == 0:
        return JointTopology.TOPO_UNKNOWN
    if len(joints) == 1:
        return joints[0].topology
    if any([j.topology not in [JointTopology.TOPO_L, JointTopology.TOPO_I, JointTopology.TOPO_T, JointTopology.TOPO_X] for j in joints]):
        return JointTopology.TOPO_UNKNOWN
    if any([j.topology == JointTopology.TOPO_T or j.topology == JointTopology.TOPO_X for j in joints]):
        return JointTopology.TOPO_K
    return JointTopology.TOPO_Y

point_centerline_towards_joint ¤

point_centerline_towards_joint(beam_a, beam_b)

Returns the centerline vector of beam_a pointing towards the joint with beam_b.

Parameters:

  • beam_a (:class:`~compas_timber.elements.Beam`) –

    The beam that attaches with one of its ends to the side of beam_b.

  • beam_b (:class:`~compas_timber.elements.Beam`) –

    The other beam.

Returns:

  • class:`~compas.geometry.Vector` –

    The centerline vector of beam_a pointing towards the joint with beam_b.

Source code in src/compas_timber/connections/utilities.py
def point_centerline_towards_joint(beam_a, beam_b):
    """
    Returns the centerline vector of beam_a pointing towards the joint with beam_b.

    Parameters
    ----------
    beam_a : :class:`~compas_timber.elements.Beam`
        The beam that attaches with one of its ends to the side of beam_b.
    beam_b : :class:`~compas_timber.elements.Beam`
        The other beam.

    Returns
    -------
    :class:`~compas.geometry.Vector`
        The centerline vector of beam_a pointing towards the joint with beam_b.
    """

    # find the orientation of main_beams's centerline so that it's pointing towards the joint
    # find the closest end
    p1x, _ = intersection_line_line(beam_a.centerline, beam_b.centerline)
    if p1x is None:
        raise ValueError("The two beams do not intersect with each other")
    end, _ = beam_a.endpoint_closest_to_point(Point(*p1x))
    if end == "start":
        centerline_vec = beam_a.centerline.vector * -1
    else:
        centerline_vec = beam_a.centerline.vector
    return centerline_vec