Skip to content

compas_timber.elements ¤

Classes¤

BallNodeFastener ¤

BallNodeFastener(node_point, ball_diameter=100, base_interface=None, **kwargs)

A class to represent timber fasteners (screws, dowels, brackets).

Todo

finish this docstring

Parameters:

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

    The node point of this fastener.

  • ball_diameter (float, default: 100 ) –

    The diameter of the ball representing this fastener. This is used for visualization and collision geometry but does not affect the geometry of the interfaces.

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

    The base interface of this fastener. If None, a default interface will be used.

Attributes:

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

    The coordinate system (frame) of this fastener.

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

    The elements that are connected with this fastener.

Source code in src/compas_timber/elements/fasteners/ball_node_fastener.py
def __init__(self, node_point, ball_diameter=100, base_interface=None, **kwargs):
    super(BallNodeFastener, self).__init__(**kwargs)
    self.node_point = node_point
    self.ball_diameter = ball_diameter
    self._base_interface = None
    if base_interface:
        self.base_interface = base_interface
    self._interface_shape = None
    self.interfaces = []
    self.attributes = {}
    self.attributes.update(kwargs)
    self.debug_info = []

Attributes¤

interface_plate property ¤
interface_plate

Generate a plate from outline_points, thickness, and holes.

interface_shape property ¤
interface_shape

Return a Brep representation of the interface located at the WorldXY origin.

Methods:¤

compute_collision_mesh ¤
compute_collision_mesh()

Computes the collision geometry of the element.

Returns:

  • class:`compas.datastructures.Mesh` –

    The collision geometry of the element.

Source code in src/compas_timber/elements/fasteners/ball_node_fastener.py
def compute_collision_mesh(self):
    # type: () -> compas.datastructures.Mesh
    """Computes the collision geometry of the element.

    Returns
    -------
    :class:`compas.datastructures.Mesh`
        The collision geometry of the element.

    """
    return self.shape.to_mesh()
compute_geometry ¤
compute_geometry()

Returns the geometry of the fastener including all interfaces.

Source code in src/compas_timber/elements/fasteners/ball_node_fastener.py
def compute_geometry(self):
    # type: () -> compas.geometry.Geometry
    """Returns the geometry of the fastener including all interfaces."""
    geometry = Brep.from_sphere(Sphere(self.ball_diameter / 2.0, point=self.node_point))

    for interface in self.interfaces:
        if self.interface_shape:
            interface_geometry = self.interface_shape.transformed(Transformation.from_frame(interface.frame))
            geometry = brep_union_first(geometry, interface_geometry)
    return geometry

Beam ¤

Beam(frame, length, width, height, **kwargs)

A class to represent timber beams (studs, slats, etc.) with rectangular cross-sections.

Parameters:

  • frame (:class:`compas.geometry.Frame`) –

    The frame representing the beam's local coordinate system in its hierarchical context: Origin is located at the starting point of the centerline. x-axis corresponds to the centerline (major axis), usually also the fibre direction in solid wood beams. y-axis corresponds to the width of the cross-section, usually the smaller dimension. z-axis corresponds to the height of the cross-section, usually the larger dimension.

  • length (float) –

    Length of the beam

  • width (float) –

    Width of the cross-section

  • height (float) –

    Height of the cross-section

Attributes:

  • transformation (:class:`~compas.geometry.Transformation`) –

    The transformation matrix representing the beam's local coordinate system in its hierarchical context. This is the internal interface for the constructor frame parameter.

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

    The coordinate system (frame) of this beam in model space. This property may be different from the constructor parameter if the beam belongs to a model hierarchy.

  • length (float) –

    Length of the beam.

  • width (float) –

    Width of the cross-section

  • height (float) –

    Height of the cross-section

  • shape (:class:`~compas.geometry.Box`) –

    A feature-less box representing the parametric geometry of this beam in global coordinates.

  • blank (:class:`~compas.geometry.Box`) –

    A feature-less box representing the material stock geometry to produce this beam in global coordinates. Compared to shape, this box includes any extensions added to the beam.

  • blank_length (float) –

    The length of the blank including any extensions added to the beam.

  • centerline (:class:`~compas.geometry.Line`) –

    A line representing the centerline of this beam.

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

    Reference frame for machining processings according to BTLx standard.

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

    A tuple containing the 6 frames representing the sides of the beam according to BTLx standard.

  • ref_edges (tuple(:class:`~compas.geometry.Line`)) –

    A tuple containing the 4 lines representing the long edges of the beam according to BTLx standard.

  • aabb (:class:`~compas.geometry.Box`) –

    An axis aligned bounding box of this beam.

  • obb (:class:`~compas.geometry.Box`) –

    An oriented bounding box of this beam.

Source code in src/compas_timber/elements/beam.py
def __init__(self, frame, length, width, height, **kwargs):
    super(Beam, self).__init__(frame=frame, length=length, width=width, height=height, **kwargs)
    self._blank_extensions = {}
    self.debug_info = []
    self._blank = None

Attributes¤

blank property ¤
blank

The blank of the beam in model space. Compared to shape, this box includes any extensions added to the beam.

centerline property ¤
centerline

The centerline of the beam in model space.

shape property ¤
shape

The shape of the beam in model space.

Methods:¤

add_blank_extension ¤
add_blank_extension(start, end, joint_key=None)

Adds a blank extension to the beam.

start : float The amount by which the start of the beam should be extended. end : float The amount by which the end of the beam should be extended. joint_key : int The key of the joint which required this extension. When the joint is removed, this extension will be removed as well.

Source code in src/compas_timber/elements/beam.py
def add_blank_extension(self, start, end, joint_key=None):
    # type: (float, float, None | int) -> None
    # TODO: rename `joint_key` to `joint_guid` for CT 3.0
    """Adds a blank extension to the beam.

    start : float
        The amount by which the start of the beam should be extended.
    end : float
        The amount by which the end of the beam should be extended.
    joint_key : int
        The key of the joint which required this extension. When the joint is removed,
        this extension will be removed as well.

    """
    clear_element_cache(self)
    if joint_key is not None and joint_key in self._blank_extensions:
        s, e = self._blank_extensions[joint_key]
        start += s
        end += e
    self._blank_extensions[joint_key] = (start, end)
compute_aabb ¤
compute_aabb(inflate=0.0)

Computes the Axis Aligned Bounding Box (AABB) of the element in global coordinates.

Parameters:

  • inflate (float, default: 0.0 ) –

    Offset of box to avoid floating point errors.

Returns:

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

    The AABB of the element.

Source code in src/compas_timber/elements/beam.py
def compute_aabb(self, inflate=0.0):
    # type: (float) -> compas.geometry.Box
    """Computes the Axis Aligned Bounding Box (AABB) of the element in global coordinates.

    Parameters
    ----------
    inflate : float, optional
        Offset of box to avoid floating point errors.

    Returns
    -------
    :class:`~compas.geometry.Box`
        The AABB of the element.

    """
    vertices, _ = self.blank.to_vertices_and_faces()
    box = Box.from_bounding_box(bounding_box(vertices))
    box.xsize += inflate
    box.ysize += inflate
    box.zsize += inflate
    return box
compute_collision_mesh ¤
compute_collision_mesh()

Computes the collision geometry of the element in global coordinates.

Returns:

  • class:`compas.datastructures.Mesh` –

    The collision geometry of the element.

Source code in src/compas_timber/elements/beam.py
def compute_collision_mesh(self):
    # type: () -> compas.datastructures.Mesh
    """Computes the collision geometry of the element in global coordinates.

    Returns
    -------
    :class:`compas.datastructures.Mesh`
        The collision geometry of the element.

    """
    return self.blank.to_mesh()
compute_elementgeometry ¤
compute_elementgeometry(include_features=True)

Compute the geometry of the element in local coordinates.

Parameters:

  • include_features (bool, default: True ) –

    If True, the features should be included in the element geometry.

Returns:

  • class:`compas.geometry.Brep` –

Raises:

  • :class:`compas_timber.errors.FeatureApplicationError` –

    If there is an error applying features to the element.

Source code in src/compas_timber/elements/beam.py
def compute_elementgeometry(self, include_features=True):
    # type: (bool) -> compas.geometry.Brep
    """Compute the geometry of the element in local coordinates.

    Parameters
    ----------
    include_features : bool, optional
        If True, the features should be included in the element geometry.

    Returns
    -------
    :class:`compas.geometry.Brep`

    Raises
    ------
    :class:`compas_timber.errors.FeatureApplicationError`
        If there is an error applying features to the element.

    """
    geometry = Brep.from_box(self.blank.transformed(self.transformation_to_local()))
    if include_features:
        for feature in self.features:
            try:
                geometry = feature.apply(geometry, self)
            except FeatureApplicationError as error:
                self.debug_info.append(error)
    return geometry
compute_obb ¤
compute_obb(inflate=0.0)

Computes the Oriented Bounding Box (OBB) of the element in global coordinates.

Parameters:

  • inflate (float, default: 0.0 ) –

    Offset of box to avoid floating point errors.

Returns:

  • class:`compas.geometry.Box` –

    The OBB of the element.

Source code in src/compas_timber/elements/beam.py
def compute_obb(self, inflate=0.0):
    # type: (float | None) -> compas.geometry.Box
    """Computes the Oriented Bounding Box (OBB) of the element in global coordinates.

    Parameters
    ----------
    inflate : float
        Offset of box to avoid floating point errors.

    Returns
    -------
    :class:`compas.geometry.Box`
        The OBB of the element.

    """
    obb = self.blank.copy()
    obb.xsize += inflate
    obb.ysize += inflate
    obb.zsize += inflate
    return obb
endpoint_closest_to_point ¤
endpoint_closest_to_point(point)

Returns which endpoint of the centerline of the beam is closer to the given point.

Parameters:

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

    The point of interest.

Returns:

  • list(str, :class:`~compas.geometry.Point`) –

    Two element list. First element is either 'start' or 'end' depending on the result. The second element is the actual endpoint of the beam's centerline which correspond to the result.

Source code in src/compas_timber/elements/beam.py
def endpoint_closest_to_point(self, point):
    # type: (Point) -> tuple[str, Point]
    """Returns which endpoint of the centerline of the beam is closer to the given point.

    Parameters
    ----------
    point : :class:`~compas.geometry.Point`
        The point of interest.

    Returns
    -------
    list(str, :class:`~compas.geometry.Point`)
        Two element list. First element is either 'start' or 'end' depending on the result.
        The second element is the actual endpoint of the beam's centerline which correspond to the result.

    """
    ps = self.centerline.start
    pe = self.centerline.end
    ds = point.distance_to_point(ps)
    de = point.distance_to_point(pe)

    if ds <= de:
        return "start", ps
    else:
        return "end", pe
extension_to_plane ¤
extension_to_plane(plane)

Returns the amount by which to extend the beam in each direction using metric units.

The extension is the minimum amount which allows all long faces of the beam to pass through the given plane.

Parameters:

  • plane (:class:`~compas.geometry.Frame` or :class:`~compas.geometry.Plane`) –

    The plane to which the beam should be extended.

Returns:

  • tuple(float, float) –

    Extension amount at start of beam, Extension amount at end of beam

Source code in src/compas_timber/elements/beam.py
def extension_to_plane(self, plane):
    # type: (Frame) -> tuple[float, float]
    """Returns the amount by which to extend the beam in each direction using metric units.

    The extension is the minimum amount which allows all long faces of the beam to pass through
    the given plane.

    Parameters
    ----------
    plane : :class:`~compas.geometry.Frame` or :class:`~compas.geometry.Plane`
        The plane to which the beam should be extended.

    Returns
    -------
    tuple(float, float)
        Extension amount at start of beam, Extension amount at end of beam

    """
    if isinstance(plane, Frame):
        plane = Plane.from_frame(plane)

    x = {}
    pts = self.shape.points
    edges = [Line(pts[0], pts[3]), Line(pts[1], pts[2]), Line(pts[4], pts[5]), Line(pts[7], pts[6])]
    for e in edges:
        p, t = intersection_line_plane_param(e, plane)
        x[t] = p

    px = intersection_line_plane_param(self.centerline, plane)[0]
    if px is None:
        raise ValueError("The plane does not intersect with the centerline of the beam.")
    side, _ = self.endpoint_closest_to_point(px)

    ds = 0.0
    de = 0.0
    if side == "start":
        tmin = min(x.keys())
        ds = tmin * self.length  # should be negative
    elif side == "end":
        tmax = max(x.keys())
        de = (tmax - 1.0) * self.length
    return -ds, de
from_box classmethod ¤
from_box(box, **kwargs)

Define the beam from a box.

Parameters:

  • box (:class:`~compas.geometry.Box`) –

    A box whose dimensions and orientation define the beam. The box's x-axis is taken as the beam's centerline direction (length), y-axis as the width direction, and z-axis as the height direction.

Returns:

  • class:`~compas_timber.elements.Beam` –
Source code in src/compas_timber/elements/beam.py
@classmethod
def from_box(cls, box, **kwargs):
    """Define the beam from a box.

    Parameters
    ----------
    box : :class:`~compas.geometry.Box`
        A box whose dimensions and orientation define the beam.
        The box's x-axis is taken as the beam's centerline direction (length),
        y-axis as the width direction, and z-axis as the height direction.

    Returns
    -------
    :class:`~compas_timber.elements.Beam`

    """
    if TOL.is_zero(box.xsize):
        raise ValueError("The given box has zero length along its x-axis. Check the box dimensions.")
    if TOL.is_zero(box.ysize):
        raise ValueError("The given box has zero width along its y-axis. Check the box dimensions.")
    if TOL.is_zero(box.zsize):
        raise ValueError("The given box has zero height along its z-axis. Check the box dimensions.")
    origin = box.frame.point + box.frame.xaxis * (-box.xsize / 2.0)
    frame = Frame(origin, box.frame.xaxis, box.frame.yaxis)
    return cls(frame, box.xsize, box.ysize, box.zsize, **kwargs)
from_centerline classmethod ¤
from_centerline(centerline, width, height, z_vector=None, **kwargs)

Define the beam from its centerline.

Parameters:

  • centerline (:class:`~compas.geometry.Line`) –

    The centerline of the beam to be created.

  • width (float) –

    Width of the cross-section.

  • height (float) –

    Height of the cross-section.

  • z_vector (:class:`~compas.geometry.Vector`, default: None ) –

    A vector indicating the height direction (z-axis) of the cross-section. Defaults to WorldZ or WorldX depending on the centerline's orientation.

Returns:

  • class:`~compas_timber.elements.Beam` –
Source code in src/compas_timber/elements/beam.py
@classmethod
def from_centerline(cls, centerline, width, height, z_vector=None, **kwargs):
    """Define the beam from its centerline.

    Parameters
    ----------
    centerline : :class:`~compas.geometry.Line`
        The centerline of the beam to be created.
    width : float
        Width of the cross-section.
    height : float
        Height of the cross-section.
    z_vector : :class:`~compas.geometry.Vector`
        A vector indicating the height direction (z-axis) of the cross-section.
        Defaults to WorldZ or WorldX depending on the centerline's orientation.

    Returns
    -------
    :class:`~compas_timber.elements.Beam`

    """
    if centerline.length < TOL.absolute:
        raise ValueError("The given centerline has zero length. Check your endpoints.")
    x_vector = centerline.vector
    z_vector = z_vector or cls._calculate_z_vector_from_centerline(x_vector)
    y_vector = Vector(*cross_vectors(x_vector, z_vector)) * -1.0
    if y_vector.length < TOL.absolute:
        raise ValueError("The given z_vector seems to be parallel to the given centerline.")
    frame = Frame(centerline.start, x_vector, y_vector)
    length = centerline.length

    return cls(frame, length, width, height, **kwargs)
from_endpoints classmethod ¤
from_endpoints(point_start, point_end, width, height, z_vector=None, **kwargs)

Creates a Beam from the given endpoints.

Parameters:

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

    The start point of a centerline

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

    The end point of a centerline

  • width (float) –

    Width of the cross-section.

  • height (float) –

    Height of the cross-section.

  • z_vector (:class:`~compas.geometry.Vector`, default: None ) –

    A vector indicating the height direction (z-axis) of the cross-section. Defaults to WorldZ or WorldX depending on the centerline's orientation.

Returns:

  • class:`~compas_timber.elements.Beam` –
Source code in src/compas_timber/elements/beam.py
@classmethod
def from_endpoints(cls, point_start, point_end, width, height, z_vector=None, **kwargs):
    """Creates a Beam from the given endpoints.

    Parameters
    ----------
    point_start : :class:`~compas.geometry.Point`
        The start point of a centerline
    point_end : :class:`~compas.geometry.Point`
        The end point of a centerline
    width : float
        Width of the cross-section.
    height : float
        Height of the cross-section.
    z_vector : :class:`~compas.geometry.Vector`
        A vector indicating the height direction (z-axis) of the cross-section.
        Defaults to WorldZ or WorldX depending on the centerline's orientation.

    Returns
    -------
    :class:`~compas_timber.elements.Beam`

    """
    line = Line(point_start, point_end)
    return cls.from_centerline(line, width, height, z_vector, **kwargs)
remove_blank_extension ¤
remove_blank_extension(joint_key=None)

Removes a blank extension from the beam.

Parameters:

  • joint_key (int, default: None ) –

    The key of the joint which required this extension.

Source code in src/compas_timber/elements/beam.py
def remove_blank_extension(self, joint_key=None):
    # type: (None | int) -> None
    # TODO: rename `joint_key` to `joint_guid` for CT 3.0

    """Removes a blank extension from the beam.

    Parameters
    ----------
    joint_key : int
        The key of the joint which required this extension.

    """
    clear_element_cache(self)
    if joint_key is None:
        self._blank_extensions = {}
    else:
        self._blank_extensions.pop(joint_key, None)

Fastener ¤

Fastener(shape=None, frame=None, **kwargs)

A class to represent timber fasteners (screws, dowels, brackets).

Todo

we should rethink this class. it is not entirely clear if it's an abstract class or a generic fastener. It inherits from TimberElement/Element but does not implement the appropriate methods.

Parameters:

  • shape (:class:`~compas.geometry.Geometry`, default: None ) –

    The geometry of the fastener.

  • frame (:class:`~compas.geometry.Frame`, default: None ) –

    The frame of the fastener in parent space.

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

    Additional keyword arguments.

Attributes:

  • shape (:class:`~compas.geometry.Geometry`) –

    The geometry of the fastener.

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

    The frame of the fastener in parent space.

  • interfaces (list) –

    A list of interfaces associated with this fastener.

  • attributes (dict) –

    Dictionary of attributes for this fastener.

  • debug_info (list) –

    A list of debug information.

  • is_fastener (bool) –

    Always True for fasteners.

  • key (int or None) –

    The graph node key of this fastener.

Source code in src/compas_timber/elements/fastener.py
def __init__(self, shape=None, frame=None, **kwargs):
    super(Fastener, self).__init__(transformation=Transformation.from_frame(frame) if frame else Transformation(), **kwargs)
    self._shape = shape
    self.interfaces = []
    self.attributes = {}
    self.attributes.update(kwargs)
    self.debug_info = []

Methods:¤

compute_elementgeometry ¤
compute_elementgeometry()

Returns the geometry of the fastener in the model.

Returns:

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

    The transformed geometry of the fastener.

Source code in src/compas_timber/elements/fastener.py
def compute_elementgeometry(self):
    """Returns the geometry of the fastener in the model.

    Returns
    -------
    :class:`~compas.geometry.Geometry`
        The transformed geometry of the fastener.
    """
    return self.shape.transformed(self.transformation)

FastenerTimberInterface ¤

FastenerTimberInterface(
    outline_points=None,
    thickness=None,
    holes=None,
    shapes=None,
    frame=None,
    element=None,
    features=None,
)

A class to represent the interface between a fastener and a timber element.

Todo

we need to put some thought as to what's the minimal representation of an interface. the responsibilities of the Fastener-based-joint vs. Fastener vs. FastenerTimberInterface should be perhaps more clearly defined prehaps it would make sense to move stuff over from here to the BallNodeJoint while it attempts to be generic, it is tightly coupled with the BallNodeJoint. what is the differece between the outline and the shapes? they seem to all just result in geometry.

Parameters:

  • outline_points (list[:class:`~compas.geometry.Point`], default: None ) –

    The points of the polyline outline of the fastener geometry.

  • thickness (float, default: None ) –

    The thickness of the fastener plate.

  • holes (list[dict], default: None ) –

    The holes of the fastener. Structure is as follows: { "point": compas.geometry.Point, "diameter": float, "vector": compas.geometry.Vector, optional, if none, the hole is assumed to be perpendicular to the frame "through": bool, optional, if True, the hole goes through the timber element }

  • shapes (list[:class:`~compas.geometry.Geometry`], default: None ) –

    Input for extra geometric elements. These should be solids that can be booleaned with the fastener geometry.

  • frame (:class:`~compas.geometry.Frame`, default: None ) –

    The frame of the instance of the fastener that is applied to the model.

  • element (object, default: None ) –

    The timber element this interface is associated with.

  • features (list[:class:`~compas_timber.fabrication.BTLxFromGeometryDefinition`], default: None ) –

    The features that are applied by this interface to the timber element. The features are defined in world coordinates.

Attributes:

  • outline_points (list[:class:`~compas.geometry.Point`]) –

    The points of the polyline outline of the fastener geometry.

  • thickness (float) –

    The thickness of the fastener plate.

  • holes (list[dict]) –

    The holes of the fastener. Structure is as follows: { "point": compas.geometry.Point, "diameter": float, "vector": compas.geometry.Vector, optional, if none, the hole is assumed to be perpendicular to the frame "through": bool, optional, if True, the hole goes through the timber element }

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

    The frame of the instance of the fastener that is applied to the model.

  • element (object) –

    The timber element this interface is associated with.

  • shapes (list[:class:`~compas.geometry.Geometry`]) –

    Input for extra geometric elements. These should be solids that can be booleaned with the fastener geometry.

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

    The features that are applied by this interface to the timber element. This returns the features in world coordinates.

Source code in src/compas_timber/elements/fastener.py
def __init__(self, outline_points=None, thickness=None, holes=None, shapes=None, frame=None, element=None, features=None):
    super(FastenerTimberInterface, self).__init__()
    self.outline_points = outline_points
    self.thickness = thickness
    self.holes = holes or []
    self.frame = frame or Frame.worldXY()
    self.element = element
    self.shapes = shapes or []
    self.features = []
    if features:
        for feat in features:
            if feat.elements:
                fc = feat.copy()
                fc.elements = None
                self.features.append(fc)
            else:
                self.features.append(feat)
    self._shape = None

Methods:¤

get_features ¤
get_features(element)

Get features to be applied to a timber element.

Parameters:

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

    The timber element to apply features to.

Returns:

  • list[:class:`~compas_timber.fabrication.Feature`] –

    A list of features to be applied to the element.

Source code in src/compas_timber/elements/fastener.py
def get_features(self, element):
    """Get features to be applied to a timber element.

    Parameters
    ----------
    element : :class:`~compas_timber.elements.TimberElement`
        The timber element to apply features to.

    Returns
    -------
    list[:class:`~compas_timber.fabrication.Feature`]
        A list of features to be applied to the element.
    """
    features = []
    for hole in self.holes:
        features.append(self._get_hole_feature(hole, element))
    # TODO: this uses the obsolete Feature classes, we should replace these with deffered BTLx
    for feature in self.features:
        feat = feature.transformed(Transformation.from_frame(self.frame))
        features.append(feat.feature_from_element(element))
    return features

Layer ¤

Layer(
    plate_geometry: PlateGeometry,
    start_offset: float,
    name: str | None = None,
    sublayers: list | None = None,
    layer_path: tuple | None = None,
    frame: Frame | None = None,
    **kwargs,
)

A resolved cross-section layer of a :class:~compas_timber.elements.Panel.

A Layer represents a slice of a parent panel between two through-thickness levels, measured from the outline_a face. It is a first-class model element that owns its own :class:~compas_timber.elements.PlateGeometry and lives as a child of the parent panel in the model tree.

Like :class:~compas_timber.elements.Panel and :class:~compas_timber.elements.Plate, a Layer is constructed directly from a :class:~compas_timber.elements.PlateGeometry -- it holds no reference to whatever produced that geometry. Use :meth:from_parent_start_end to build a Layer by interpolating the outlines of a parent :class:~compas_timber.elements.Panel or another Layer between two through-thickness levels.

Parameters:

  • plate_geometry (:class:`~compas_timber.elements.PlateGeometry`) –

    Geometry of this layer slice.

  • start_offset (float) –

    Starting offset from the panel's outline_a face, measured along the panel's thickness direction.

  • name (str, default: None ) –

    Human-readable identifier (e.g. "core", "exterior").

  • sublayers (list[:class:`Layer`], default: None ) –

    Child layers to attach immediately. Equivalent to setting :attr:sublayers after construction.

  • layer_path (tuple[int, ...], default: None ) –

    Position in the parent panel's layer tree. See :attr:layer_path.

  • frame (:class:`~compas.geometry.Frame`, default: None ) –

    The coordinate system (frame) of this layer, relative to its parent. Defaults to worldXY.

Attributes:

  • plate_geometry (:class:`~compas_timber.elements.PlateGeometry`) –

    Geometry of this layer slice.

  • start_offset (float) –

    Starting offset of this layer from the panel's outline_a face, measured along the panel's thickness direction.

  • sublayers (list[:class:`Layer`]) –

    Ordered child layers. Derived from the model tree when in a model; falls back to the stored list when standalone.

  • layer_path (tuple[int, ...] or None) –

    Position in the parent panel's layer tree, e.g. (1,) for the second root layer or (1, 0) for its first child.

  • thickness (float) –

    Layer thickness, taken from :attr:plate_geometry.

  • outline_a, outline_b (:class:`~compas.geometry.Polyline`) –

    World-space outlines of the two main faces.

  • planes (tuple[:class:`~compas.geometry.Plane`, :class:`~compas.geometry.Plane`]) –

    World-space planes of the two main faces.

  • normal (:class:`~compas.geometry.Vector`) –

    World-space normal of the layer.

  • edge_planes (dict[int, :class:`~compas.geometry.Plane`]) –

    World-space edge planes by edge index.

Source code in src/compas_timber/elements/layer.py
def __init__(
    self,
    plate_geometry: PlateGeometry,
    start_offset: float,
    name: Optional[str] = None,
    sublayers: Optional[list] = None,
    layer_path: Optional[tuple] = None,
    frame: Optional[Frame] = None,
    **kwargs,
):
    super(Layer, self).__init__(
        transformation=Transformation.from_frame(frame if frame is not None else Frame.worldXY()),
        name=name,
        **kwargs,
    )
    self.plate_geometry = plate_geometry
    self.start_offset = start_offset
    self.layer_path = tuple(layer_path) if layer_path is not None else None
    self.debug_info = []
    self._sublayers = []
    self._planes = None
    self.attributes = {}
    self.attributes.update(kwargs)

    if sublayers:
        self.sublayers = sublayers

Attributes¤

center_height property ¤
center_height

Z coordinate of the layer's mid-thickness in model space.

Methods:¤

apply_edge_extensions ¤
apply_edge_extensions()

Move edge vertices onto the extension planes, then recurse to sublayers.

Source code in src/compas_timber/elements/layer.py
def apply_edge_extensions(self):
    """Move edge vertices onto the extension planes, then recurse to sublayers."""
    self.plate_geometry.apply_edge_extensions()
    for sublayer in self.sublayers:
        sublayer.apply_edge_extensions()
define_sublayers ¤
define_sublayers(thicknesses: list, names: list | None = None) -> list

Subdivide this layer into child layers by thickness.

Parameters:

  • thicknesses (list[float | None]) –

    Thickness of each sublayer, ordered from :attr:start_offset towards the far face of this layer. At most one entry may be None; that slot absorbs whatever thickness remains after the other entries are summed. If no None is present and the sum of entries is less than :attr:thickness, an extra layer is appended at the end with the remaining thickness. The sum of all explicit (non-None) values must not exceed :attr:thickness.

  • names (list[str | None], default: None ) –

    Names for the sublayers. When given, length must match thicknesses (before any auto-appended remainder layer).

Returns:

  • list[:class:`Layer`] –

    The newly created sublayers (also stored in :attr:sublayers).

Source code in src/compas_timber/elements/layer.py
def define_sublayers(self, thicknesses: list, names: Optional[list] = None) -> list:
    """Subdivide this layer into child layers by thickness.

    Parameters
    ----------
    thicknesses : list[float | None]
        Thickness of each sublayer, ordered from :attr:`start_offset` towards the
        far face of this layer.  At most one entry may be ``None``; that slot absorbs whatever thickness
        remains after the other entries are summed.  If no ``None`` is present
        and the sum of entries is less than :attr:`thickness`, an extra layer
        is appended at the end with the remaining thickness.
        The sum of all explicit (non-``None``) values must not exceed
        :attr:`thickness`.
    names : list[str | None], optional
        Names for the sublayers.  When given, length must match *thicknesses*
        (before any auto-appended remainder layer).

    Returns
    -------
    list[:class:`Layer`]
        The newly created sublayers (also stored in :attr:`sublayers`).
    """
    none_count = sum(1 for t in thicknesses if t is None)
    if none_count > 1:
        raise ValueError("At most one None is allowed in thicknesses.")
    if names is not None and len(names) != len(thicknesses):
        raise ValueError("Length of names ({}) must match length of thicknesses ({}).".format(len(names), len(thicknesses)))

    sum_defined = sum(t for t in thicknesses if t is not None)
    remaining = self.thickness - sum_defined
    if remaining < -1e-6:
        raise ValueError("Sum of defined thicknesses ({:.6f}) exceeds layer thickness ({:.6f}).".format(sum_defined, self.thickness))

    if none_count == 1:
        thicknesses = [remaining if t is None else t for t in thicknesses]
    elif remaining > 1e-6:
        thicknesses = list(thicknesses) + [remaining]
        if names is not None:
            names = list(names) + [None]

    current_level = self.start_offset
    new_sublayers = []
    for i, t in enumerate(thicknesses):
        new_sublayers.append(
            Layer.from_parent_start_end(
                self,
                current_level,
                current_level + t,
                name=names[i] if names is not None else None,
            )
        )
        current_level += t

    self.sublayers = new_sublayers  # use setter to wire up
    return new_sublayers
from_parent_start_end classmethod ¤
from_parent_start_end(
    host: Panel | Layer,
    start_offset: float,
    end_offset: float,
    name: str | None = None,
    sublayers: list | None = None,
    layer_path: tuple | None = None,
    **kwargs,
) -> Layer

Construct a Layer by interpolating the outlines of host between two through-thickness levels.

Parameters:

  • host (:class:`~compas_timber.elements.Panel` or :class:`Layer`) –

    The panel (for root layers) or enclosing layer (for sublayers) to interpolate outlines from. Not stored: the resulting Layer holds no reference back to host.

  • start_offset (float) –

    See :class:Layer.

  • end_offset (float) –

    See :class:Layer.

  • name (optional, default: None ) –

    See :class:Layer.

  • sublayers (optional, default: None ) –

    See :class:Layer.

  • layer_path (optional, default: None ) –

    See :class:Layer.

Returns:

  • class:`Layer` –
Source code in src/compas_timber/elements/layer.py
@classmethod
def from_parent_start_end(
    cls,
    host: Union[Panel, Layer],
    start_offset: float,
    end_offset: float,
    name: Optional[str] = None,
    sublayers: Optional[list] = None,
    layer_path: Optional[tuple] = None,
    **kwargs,
) -> Layer:
    """Construct a ``Layer`` by interpolating the outlines of *host* between two through-thickness levels.

    Parameters
    ----------
    host : :class:`~compas_timber.elements.Panel` or :class:`Layer`
        The panel (for root layers) or enclosing layer (for sublayers) to
        interpolate outlines from.  Not stored: the resulting ``Layer`` holds
        no reference back to *host*.
    start_offset, end_offset : float
        See :class:`Layer`.
    name, sublayers, layer_path : optional
        See :class:`Layer`.

    Returns
    -------
    :class:`Layer`
    """
    outline_a, outline_b = cls.get_outlines_from_parent(host, start_offset, end_offset)
    frame, local_outline_a, local_outline_b = global_outlines_to_local(outline_a, outline_b, orientation=[0, 1, 0])
    plate_geometry = PlateGeometry(local_outline_a, local_outline_b)
    return cls(
        plate_geometry,
        start_offset,
        name=name,
        sublayers=sublayers,
        layer_path=layer_path,
        frame=frame,
        **kwargs,
    )
get_outlines_from_parent staticmethod ¤
get_outlines_from_parent(parent: Panel | Layer, start_offset: float, end_offset: float)

Interpolate parent's local outlines at the given panel-absolute levels.

parent is either the :class:~compas_timber.elements.Panel (for root layers) or the enclosing :class:Layer (for sublayers).

Source code in src/compas_timber/elements/layer.py
@staticmethod
def get_outlines_from_parent(parent: Union[Panel, Layer], start_offset: float, end_offset: float):
    """Interpolate *parent*'s local outlines at the given panel-absolute levels.

    *parent* is either the :class:`~compas_timber.elements.Panel` (for root
    layers) or the enclosing :class:`Layer` (for sublayers).
    """
    parent_start = getattr(parent, "start_offset", 0.0)  # Panel has no start_offset: it starts at 0.
    relative_start = start_offset - parent_start
    relative_end = end_offset - parent_start

    local_outline_a = parent.plate_geometry.outline_a
    local_outline_b = parent.plate_geometry.outline_b

    offset_a = relative_start / parent.thickness
    if offset_a:
        layer_outline_a = Polyline([pt_a * (1.0 - offset_a) + pt_b * offset_a for pt_a, pt_b in zip(local_outline_a.points, local_outline_b.points)])
    else:
        layer_outline_a = local_outline_a

    offset_b = relative_end / parent.thickness
    layer_outline_b = Polyline([pt_a * (1.0 - offset_b) + pt_b * offset_b for pt_a, pt_b in zip(local_outline_a.points, local_outline_b.points)])
    return layer_outline_a, layer_outline_b
remove_blank_extension ¤
remove_blank_extension(edge_index: int | None = None)

Remove any extension plane for the given edge index. Propagates to all sublayers.

Source code in src/compas_timber/elements/layer.py
def remove_blank_extension(self, edge_index: Optional[int] = None):
    """Remove any extension plane for the given edge index.  Propagates to all sublayers."""
    self.plate_geometry.remove_blank_extension(edge_index)
    for sublayer in self.sublayers:
        sublayer.remove_blank_extension(edge_index)
reset ¤
reset()

Resets the element to its initial state by removing extensions and debug_info.

Source code in src/compas_timber/elements/layer.py
def reset(self):
    """Resets the element to its initial state by removing extensions and debug_info."""
    self.plate_geometry.reset()  # reset outline_a and outline_b
    self.debug_info = []
    for sublayer in self.sublayers:
        sublayer.reset()
    clear_element_cache(self)
set_extension_plane ¤
set_extension_plane(edge_index: int, plane: Plane)

Set an extension plane for an edge. Propagates to all sublayers.

Source code in src/compas_timber/elements/layer.py
def set_extension_plane(self, edge_index: int, plane: Plane):
    """Set an extension plane for an edge.  Propagates to all sublayers."""
    self.plate_geometry.set_extension_plane(edge_index, plane.transformed(self.transformation_to_local()))
    for sublayer in self.sublayers:
        sublayer.set_extension_plane(edge_index, plane)
transformation_to_local ¤
transformation_to_local()

Transformation from model space to this layer's local space.

Source code in src/compas_timber/elements/layer.py
def transformation_to_local(self):
    """Transformation from model space to this layer's local space."""
    return self.modeltransformation.inverse()

LayerDefinition ¤

LayerDefinition(name=None, thickness=None, sublayer_defs=None)

Definition of a single layer slot within a :class:LayerStructure.

LayerDefinition is the panel-agnostic description of one layer: its name, thickness fraction, and optional children. A :class:LayerStructure holds a tree of LayerDefinition objects; when applied to a :class:~compas_timber.elements.Panel each LayerDefinition produces one geometry-bearing :class:Layer instance.

Parameters:

  • name (str, default: None ) –

    Human-readable identifier (e.g. "core", "exterior").

  • thickness (float, default: None ) –

    Absolute thickness of this slot measured along the panel's thickness direction. When None the slot absorbs whatever thickness remains after all fixed siblings are summed. At most one sibling at the same level may have thickness=None.

  • sublayer_defs (list[:class:`LayerDefinition`], default: None ) –

    Child slot definitions for nested layers.

Attributes:

  • name (str or None) –
  • thickness (float or None) –
  • sublayer_defs (list[:class:`LayerDefinition`]) –
  • layer_path (tuple[int, ...]) –

    Position in the :class:LayerStructure tree, e.g. (1,) for the second root slot or (1, 0) for its first child. Set automatically by :class:LayerStructure when the structure is built. Not serialized.

Source code in src/compas_timber/elements/layer.py
def __init__(self, name=None, thickness=None, sublayer_defs=None):
    super().__init__()
    self.name = name
    self.thickness = thickness
    self.sublayer_defs = list(sublayer_defs) if sublayer_defs else []
    self.layer_path = ()

LayerStructure ¤

LayerStructure(layer_defs=None)

Panel-agnostic cross-section definition.

A LayerStructure holds a tree of :class:LayerDefinition objects describing the layer slots. It is shared across panels; :meth:attach creates bound :class:Layer instances for each specific panel.

Use "core", "exterior", "interior" as :class:LayerDefinition names to enable the matching properties on :class:~compas_timber.elements.Panel. A sibling with thickness=None absorbs remaining thickness; at most one sibling at the same level may do so.

Parameters:

  • layer_defs (list[:class:`LayerDefinition`], default: None ) –

    Root-level layer definitions. Defaults to a single "core" slot spanning the full panel thickness.

Source code in src/compas_timber/elements/layer.py
def __init__(self, layer_defs=None):
    super().__init__()
    self.layer_defs = list(layer_defs) if layer_defs else [LayerDefinition(name="core")]
    self._assign_paths(self.layer_defs, ())

Methods:¤

attach ¤
attach(panel)

Create and return bound :class:Layer instances for panel.

Source code in src/compas_timber/elements/layer.py
def attach(self, panel):
    """Create and return bound :class:`Layer` instances for *panel*."""
    return self._create_layers(panel, self.layer_defs, 0.0, panel.thickness)
get_path_for_name ¤
get_path_for_name(name)

Return the :attr:~LayerDefinition.layer_path of the first definition matching name, or None.

Source code in src/compas_timber/elements/layer.py
def get_path_for_name(self, name):
    """Return the :attr:`~LayerDefinition.layer_path` of the first definition matching *name*, or ``None``."""
    return self._find_path(self.layer_defs, name, ())

Panel ¤

Panel(
    frame: Frame | None = None,
    length: float | None = None,
    width: float | None = None,
    thickness: float | None = None,
    plate_geometry: PlateGeometry | None = None,
    type: str | None = None,
    layer_structure: LayerStructure | None = None,
    **kwargs,
)

Represents a timber panel element (wall, floor, roof, etc.).

Serves as container for beams, plates, and other related elements and groups them together to form a panel. A panel is often a single unit of prefabricated timber element. It is often referred to as an enveloping body.

Parameters:

  • frame (:class:`~compas.geometry.Frame`, default: None ) –

    The coordinate system (frame) of this panel, relative to its parent. Defaults to worldXY.

  • length (float, default: None ) –

    Length of the panel. Required when no plate_geometry is provided.

  • width (float, default: None ) –

    Width of the panel. Required when no plate_geometry is provided.

  • thickness (float, default: None ) –

    Thickness of the panel. Required when no plate_geometry is provided.

  • plate_geometry (:class:`~compas_timber.elements.PlateGeometry`, default: None ) –

    A PlateGeometry object defining the panel shape in local space. When provided, dimensions must not be given.

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

    Additional keyword arguments.

Attributes:

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

    The coordinate system (frame) of this panel.

  • length (float) –

    Length of the panel.

  • width (float) –

    Width of the panel.

  • height (float) –

    Height (thickness) of the panel.

  • thickness (float) –

    Thickness of the panel.

  • planes (tuple (:class:`~compas.geometry.Plane`, :class:`~compas.geometry.Plane`)) –

    The two main planes of the panel (bottom and top).

  • normal (:class:`~compas.geometry.Vector`) –

    The normal vector of the panel.

  • edge_planes (dict[int, :class:`~compas.geometry.Plane`]) –

    The edge planes of the panel by edge index.

  • is_group_element (bool) –

    Always True for panels as they can contain other elements.

Source code in src/compas_timber/elements/panel.py
def __init__(
    self,
    frame: Optional[Frame] = None,
    length: Optional[float] = None,
    width: Optional[float] = None,
    thickness: Optional[float] = None,
    plate_geometry: Optional[PlateGeometry] = None,
    type: Optional[str] = None,
    layer_structure: Optional[LayerStructure] = None,
    **kwargs,
):
    if plate_geometry is not None and any(x is not None for x in [length, width, thickness]):
        raise ValueError("Panel cannot be instantiated with both a PlateGeometry and dimension arguments.")
    if plate_geometry is None:
        if not all(x is not None for x in [length, width, thickness]):
            raise ValueError("Panel must be instantiated with either a PlateGeometry or all of: length, width, thickness.")
        plate_geometry = PlateGeometry.from_dims(length, width, thickness)
    frame = frame if frame is not None else Frame.worldXY()

    super(Panel, self).__init__(transformation=Transformation.from_frame(frame), **kwargs)  # NOTE: Element wants a transformation, not a frame
    self.plate_geometry = plate_geometry
    self.length = plate_geometry.length
    self.width = plate_geometry.width
    self.height = plate_geometry.thickness
    self.type = type or PanelType.GENERIC
    self.attributes = {}
    self.attributes.update(kwargs)
    self._layer_structure = layer_structure or LayerStructure()
    self._root_layers = []
    self._planes = None
    self._attach_layer_structure()
    self.debug_info = []

Attributes¤

core_layer property ¤
core_layer

The layer named "core" in this panel's :attr:layer_structure, or None.

exterior_layer property ¤
exterior_layer

The layer named "exterior" in this panel's :attr:layer_structure, or None.

interfaces property ¤
interfaces

list[:class:~compas_timber.panel_features.PanelConnectionInterface]: The interfaces associated with this panel.

interior_layer property ¤
interior_layer

The layer named "interior" in this panel's :attr:layer_structure, or None.

layer_structure property writable ¤
layer_structure

The :class:~compas_timber.elements.LayerStructure definition for this panel.

layers property ¤
layers

Root layers of this panel (direct children only).

Methods:¤

apply_edge_extensions ¤
apply_edge_extensions()

adjusts segments of the outlines to lay on the edge planes created by PanelJoints.

Source code in src/compas_timber/elements/panel.py
def apply_edge_extensions(self):
    """adjusts segments of the outlines to lay on the edge planes created by PanelJoints."""
    self.plate_geometry.apply_edge_extensions()
    for layer in self.layers:
        layer.apply_edge_extensions()
compute_aabb ¤
compute_aabb(inflate: float = 0.0) -> Box

Computes the Axis Aligned Bounding Box (AABB) of the element.

Parameters:

  • inflate (float, default: 0.0 ) –

    Offset of box to avoid floating point errors.

Returns:

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

    The AABB of the element.

Source code in src/compas_timber/elements/panel.py
def compute_aabb(self, inflate: float = 0.0) -> Box:
    """Computes the Axis Aligned Bounding Box (AABB) of the element.

    Parameters
    ----------
    inflate : float, optional
        Offset of box to avoid floating point errors.

    Returns
    -------
    :class:`~compas.geometry.Box`
        The AABB of the element.

    """
    vertices = self.outline_a.points + self.outline_b.points
    box = Box.from_points(vertices)
    box.xsize += inflate
    box.ysize += inflate
    box.zsize += inflate
    return box
compute_collision_mesh ¤
compute_collision_mesh() -> Mesh

Computes the collision geometry of the element.

Returns:

  • class:`compas.datastructures.Mesh` –

    The collision geometry of the element.

Source code in src/compas_timber/elements/panel.py
def compute_collision_mesh(self) -> Mesh:
    """Computes the collision geometry of the element.

    Returns
    -------
    :class:`compas.datastructures.Mesh`
        The collision geometry of the element.

    """
    return self.obb.to_mesh()
compute_elementgeometry ¤
compute_elementgeometry(include_features: bool = True) -> Brep

Compute the geometry of the element.

Parameters:

  • include_features (bool, default: True ) –

    If True, include the features in the computed geometry. If False, return only the plate shape.

Returns:

  • class:`compas.datastructures.Mesh` | :class:`compas.geometry.Brep` –
Source code in src/compas_timber/elements/panel.py
def compute_elementgeometry(self, include_features: bool = True) -> Brep:
    """Compute the geometry of the element.

    Parameters
    ----------
    include_features : bool, optional
        If ``True``, include the features in the computed geometry.
        If ``False``, return only the plate shape.

    Returns
    -------
    :class:`compas.datastructures.Mesh` | :class:`compas.geometry.Brep`

    """

    plate_geo = self.plate_geometry.compute_shape()
    if include_features:
        for feature in self._features:
            try:
                plate_geo = feature.apply(plate_geo, self)
            except FeatureApplicationError as error:
                self.debug_info.append(error)
    return plate_geo
compute_modelgeometry ¤
compute_modelgeometry()

Same as parent but handles standalone elements.

Source code in src/compas_timber/elements/panel.py
def compute_modelgeometry(self):
    """Same as parent but handles standalone elements."""
    if not self.model:
        return self.elementgeometry.transformed(self.transformation)  # type: ignore
    return super().compute_modelgeometry()  # type: ignore
compute_modeltransformation ¤
compute_modeltransformation()

Same as parent but handles standalone elements.

Source code in src/compas_timber/elements/panel.py
def compute_modeltransformation(self):
    """Same as parent but handles standalone elements."""
    if not self.model:
        return self.transformation
    return super().compute_modeltransformation()  # type: ignore
compute_obb ¤
compute_obb(inflate: float = 0.0) -> Box

Computes the Oriented Bounding Box (OBB) of the element.

Returns:

  • class:`compas.geometry.Box` –

    The OBB of the element.

Source code in src/compas_timber/elements/panel.py
def compute_obb(self, inflate: float = 0.0) -> Box:
    """Computes the Oriented Bounding Box (OBB) of the element.

    Returns
    -------
    :class:`compas.geometry.Box`
        The OBB of the element.

    """

    obb = self.plate_geometry.compute_aabb(inflate)
    obb.transform(self.modeltransformation)
    return obb
from_brep classmethod ¤
from_brep(brep: Brep, orientation: Vector | None = None, **kwargs)

Creates a panel from a brep by automatically detecting two parallel faces.

This method identifies the two main faces of the brep using topological analysis (edge counts and adjacency) and uses them as the top and bottom faces of the panel.

Parameters:

  • brep (:class:`~compas.geometry.Brep`) –

    The brep representing the panel geometry. Must have at least 2 parallel faces.

  • orientation (:class:`~compas.geometry.Vector`, default: None ) –

    A vector indicating the desired orientation of the panel's local y-axis. If None, orientation is determined from the outline.

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

    Additional keyword arguments. These are passed to the :class:~compas_timber.elements.Panel constructor.

Returns:

  • class:`~compas_timber.elements.Panel` –

    A Panel object created from the two parallel faces of the brep.

Source code in src/compas_timber/elements/panel.py
@classmethod
def from_brep(cls, brep: Brep, orientation: Optional[Vector] = None, **kwargs):
    """Creates a panel from a brep by automatically detecting two parallel faces.

    This method identifies the two main faces of the brep using topological analysis
    (edge counts and adjacency) and uses them as the top and bottom faces of the panel.

    Parameters
    ----------
    brep : :class:`~compas.geometry.Brep`
        The brep representing the panel geometry. Must have at least 2 parallel faces.
    orientation : :class:`~compas.geometry.Vector`, optional
        A vector indicating the desired orientation of the panel's local y-axis. If None, orientation is determined from the outline.
    **kwargs : dict, optional
        Additional keyword arguments.
        These are passed to the :class:`~compas_timber.elements.Panel` constructor.

    Returns
    -------
    :class:`~compas_timber.elements.Panel`
        A Panel object created from the two parallel faces of the brep.
    """
    if len(brep.faces) < 2:
        raise ValueError("Brep must have at least 2 faces. This brep has {}".format(len(brep.faces)))
    outline_a, outline_b, openings = get_plate_geometry_outlines_from_brep(brep)
    return cls.from_outlines(outline_a, outline_b, openings=openings, orientation=orientation, **kwargs)
from_face_thickness classmethod ¤
from_face_thickness(
    brep: Brep,
    thickness: float,
    vector: Vector | None = None,
    orientation: Vector | None = None,
    **kwargs,
)

Creates a panel from a single-face brep.

Parameters:

  • brep (:class:`~compas.geometry.Brep`) –

    A single-face brep representing the panel surface.

  • thickness (float) –

    The thickness of the panel.

  • vector (:class:`~compas.geometry.Vector`, default: None ) –

    The vector in which the panel is extruded.

  • orientation (:class:`~compas.geometry.Vector`, default: None ) –

    A vector indicating the desired orientation of the panel's local y-axis. If None, orientation is determined from the outline.

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

    Additional keyword arguments. These are passed to the :class:~compas_timber.elements.Panel constructor.

Returns:

  • class:`~compas_timber.elements.Panel` –

    A Panel object representing the panel with the given brep and thickness.

Source code in src/compas_timber/elements/panel.py
@classmethod
def from_face_thickness(cls, brep: Brep, thickness: float, vector: Optional[Vector] = None, orientation: Optional[Vector] = None, **kwargs):
    """Creates a panel from a single-face brep.

    Parameters
    ----------
    brep : :class:`~compas.geometry.Brep`
        A single-face brep representing the panel surface.
    thickness : float
        The thickness of the panel.
    vector : :class:`~compas.geometry.Vector`, optional
        The vector in which the panel is extruded.
    orientation : :class:`~compas.geometry.Vector`, optional
        A vector indicating the desired orientation of the panel's local y-axis. If None, orientation is determined from the outline.
    **kwargs : dict, optional
        Additional keyword arguments.
        These are passed to the :class:`~compas_timber.elements.Panel` constructor.

    Returns
    -------
    :class:`~compas_timber.elements.Panel`
        A Panel object representing the panel with the given brep and thickness.
    """

    if len(brep.faces) > 1:
        raise ValueError("Can only use single-face breps to create a Panel. This brep has {}".format(len(brep.faces)))
    face = brep.faces[0]
    outer_polyline, inner_polylines = polylines_from_brep_face(face)
    return cls.from_outline_thickness(outer_polyline, thickness, vector=vector, openings=inner_polylines, orientation=orientation, **kwargs)
from_outline_thickness classmethod ¤
from_outline_thickness(
    outline: Polyline,
    thickness: float,
    vector: Vector | None = None,
    openings: list[Polyline] | None = None,
    orientation: Vector | None = None,
    **kwargs,
)

Constructs a Plate from a polyline outline and a thickness. The outline is the top face of the plate_geometry, and the thickness is the distance to the bottom face.

Parameters:

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

    A polyline representing the outline of the plate geometry.

  • thickness (float) –

    The thickness of the plate geometry.

  • vector (:class:`~compas.geometry.Vector`, default: None ) –

    The direction of the thickness vector. If None, the thickness vector is determined from the outline.

  • openings (list[:class:`~compas.geometry.Polyline`], default: None ) –

    A list of polyline openings to be added to the plate geometry.

  • orientation (:class:`~compas.geometry.Vector`, default: None ) –

    A vector indicating the desired orientation of the panel's local y-axis. If None, orientation is determined from the outline.

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

    Additional keyword arguments to be passed to the constructor.

Returns:

  • class:`~compas_timber.elements.Panel` –

    A Panel object representing the panel geometry with the given outline and thickness.

Source code in src/compas_timber/elements/panel.py
@classmethod
def from_outline_thickness(
    cls, outline: Polyline, thickness: float, vector: Optional[Vector] = None, openings: Optional[list[Polyline]] = None, orientation: Optional[Vector] = None, **kwargs
):
    """
    Constructs a Plate from a polyline outline and a thickness.
    The outline is the top face of the plate_geometry, and the thickness is the distance to the bottom face.

    Parameters
    ----------
    outline : :class:`~compas.geometry.Polyline`
        A polyline representing the outline of the plate geometry.
    thickness : float
        The thickness of the plate geometry.
    vector : :class:`~compas.geometry.Vector`, optional
        The direction of the thickness vector. If None, the thickness vector is determined from the outline.
    openings : list[:class:`~compas.geometry.Polyline`], optional
        A list of polyline openings to be added to the plate geometry.
    orientation : :class:`~compas.geometry.Vector`, optional
        A vector indicating the desired orientation of the panel's local y-axis. If None, orientation is determined from the outline.
    **kwargs : dict, optional
        Additional keyword arguments to be passed to the constructor.

    Returns
    -------
    :class:`~compas_timber.elements.Panel`
        A Panel object representing the panel geometry with the given outline and thickness.
    """
    # this ensure the plate geometry can always be computed
    if TOL.is_zero(thickness):
        thickness = TOL.absolute

    offset_vector = get_polyline_normal_vector(outline, vector)  # gets vector perpendicular to outline
    offset_vector *= thickness
    outline_b = Polyline(outline).translated(offset_vector)
    return cls.from_outlines(outline, outline_b, openings=openings, orientation=orientation, **kwargs)
from_outlines classmethod ¤
from_outlines(
    outline_a,
    outline_b,
    openings=None,
    recognize_doors=False,
    horizontal_openings=False,
    orientation: Vector | None = None,
    **kwargs,
)

Constructs a Panel from two polyline outlines.

Parameters:

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

    A polyline representing the principal outline of the panel geometry in global space. For exterior walls, this is the interior side.

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

    A polyline representing the associated outline of the panel geometry in global space. For exterior walls, this is the exterior side. This should have the same number of points as outline_a.

  • openings (list[:class:`~compas.geometry.Polyline`], default: None ) –

    A list of window opening polylines to be added to the panel.

  • recognize_doors (bool, default: False ) –

    If True, door features will be extracted from the outlines and added as Openings to the Panel.

  • horizontal_openings (bool, default: False ) –

    If True, openings that are not vertical or horizontal will be extruded horizontally through the Panel.

  • orientation (:class:`~compas.geometry.Vector`, default: None ) –

    A vector indicating the desired orientation of the panel's local y-axis. If None, orientation is determined from the outline.

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

    Additional keyword arguments to be passed to the constructor.

Returns:

  • class:`~compas_timber.elements.Panel` –

    A Panel object representing the panel with the given outlines.

Source code in src/compas_timber/elements/panel.py
@classmethod
def from_outlines(cls, outline_a, outline_b, openings=None, recognize_doors=False, horizontal_openings=False, orientation: Optional[Vector] = None, **kwargs):
    """
    Constructs a Panel from two polyline outlines.

    Parameters
    ----------
    outline_a : :class:`~compas.geometry.Polyline`
        A polyline representing the principal outline of the panel geometry in global space. For exterior walls, this is the interior side.
    outline_b : :class:`~compas.geometry.Polyline`
        A polyline representing the associated outline of the panel geometry in global space. For exterior walls, this is the exterior side.
        This should have the same number of points as outline_a.
    openings : list[:class:`~compas.geometry.Polyline`], optional
        A list of window opening polylines to be added to the panel.
    recognize_doors : bool
        If True, door features will be extracted from the outlines and added as Openings to the Panel.
    horizontal_openings : bool
        If True, openings that are not vertical or horizontal will be extruded horizontally through the Panel.
    orientation : :class:`~compas.geometry.Vector`, optional
        A vector indicating the desired orientation of the panel's local y-axis. If None, orientation is determined from the outline.
    **kwargs : dict, optional
        Additional keyword arguments to be passed to the constructor.

    Returns
    -------
    :class:`~compas_timber.elements.Panel`
        A Panel object representing the panel with the given outlines.
    """

    window_polylines = [o for o in openings] if openings else []
    door_polylines = []
    if recognize_doors:
        outline_a, outline_b, door_polylines = extract_door_openings(outline_a, outline_b)
    frame, local_outline_a, local_outline_b = global_outlines_to_local(outline_a, outline_b, orientation=orientation)
    plate_geometry = PlateGeometry(local_outline_a, local_outline_b)
    panel = cls(frame=frame, plate_geometry=plate_geometry, **kwargs)
    for polyline in window_polylines:
        opening = Opening.from_outline_panel(polyline, panel, opening_type=OpeningType.WINDOW, project_horizontal=horizontal_openings)
        panel.add_feature(opening)
    for polyline in door_polylines:
        opening = Opening.from_outline_panel(polyline, panel, opening_type=OpeningType.DOOR, project_horizontal=horizontal_openings)
        panel.add_feature(opening)
    return panel
get_leaf_layers ¤
get_leaf_layers()

Return all layers with no sublayers, in order from outline_a to outline_b.

Source code in src/compas_timber/elements/panel.py
def get_leaf_layers(self):
    """Return all layers with no sublayers, in order from outline_a to outline_b."""
    leaf_layers = []

    def _walk(layer):
        if not layer.sublayers:
            leaf_layers.append(layer)
        else:
            for sub in layer.sublayers:
                _walk(sub)

    for layer in self.layers:
        _walk(layer)
    return leaf_layers
merge_layer_structure ¤
merge_layer_structure(model)

Add all layers in this panel's layer structure to model as children of this panel.

Source code in src/compas_timber/elements/panel.py
def merge_layer_structure(self, model):
    """Add all layers in this panel's layer structure to *model* as children of this panel."""
    for layer in self._root_layers:
        if layer not in model.elements():
            model.add_element(layer, parent=self)
        layer.merge_sublayer_tree(model)
remove_blank_extension ¤
remove_blank_extension(edge_index: int | None = None)

Removes any extension plane for the given edge index.

Source code in src/compas_timber/elements/panel.py
def remove_blank_extension(self, edge_index: Optional[int] = None):
    """Removes any extension plane for the given edge index."""
    self.plate_geometry.remove_blank_extension(edge_index)
remove_features ¤
remove_features(features: PanelFeature | list[PanelFeature] | None = None) -> None

Removes features from the element.

Parameters:

  • features (:class:`~compas_timber.panel_features.PanelFeature` | list[:class:`~compas_timber.panel_features.PanelFeature`], default: None ) –

    The features to be removed. If None, all features will be removed.

Source code in src/compas_timber/elements/panel.py
def remove_features(self, features: Optional[Union[PanelFeature, list[PanelFeature]]] = None) -> None:
    """Removes features from the element.

    Parameters
    ----------
    features : :class:`~compas_timber.panel_features.PanelFeature` | list[:class:`~compas_timber.panel_features.PanelFeature`], optional
        The features to be removed. If None, all features will be removed.

    """
    if features is None:
        self._features = []
    else:
        feature_list = features if isinstance(features, list) else [features]
        self._features = [f for f in self.features if f not in feature_list]
    clear_element_cache(self)
reset ¤
reset()

Resets the element to its initial state by removing all features, extensions, and debug_info.

Source code in src/compas_timber/elements/panel.py
def reset(self):
    """Resets the element to its initial state by removing all features, extensions, and debug_info."""
    self.plate_geometry.reset()  # reset outline_a and outline_b
    self._features = [f for f in self._features if not f.is_joinery]
    self.debug_info = []
    clear_element_cache(self)
set_extension_plane ¤
set_extension_plane(edge_index: int, plane: Plane)

Sets an extension plane for a specific edge of the plate. This is called by PanelJoints.

Source code in src/compas_timber/elements/panel.py
def set_extension_plane(self, edge_index: int, plane: Plane):
    """Sets an extension plane for a specific edge of the plate. This is called by PanelJoints."""
    self.plate_geometry.set_extension_plane(edge_index, plane.transformed(self.transformation_to_local()))
    for layer in self.layers:
        layer.set_extension_plane(edge_index, plane)
transformation_to_local ¤
transformation_to_local()

Compute the transformation from model space to local element space.

Source code in src/compas_timber/elements/panel.py
def transformation_to_local(self):
    """Compute the transformation from model space to local element space."""
    return self.modeltransformation.inverse()

Plate ¤

Plate(
    frame: Frame | None = None,
    length: float | None = None,
    width: float | None = None,
    thickness: float | None = None,
    plate_geometry: PlateGeometry | None = None,
    **kwargs,
)

A class to represent timber plates (plywood, CLT, etc.) defined by polylines on top and bottom faces of material.

Parameters:

  • frame (:class:`~compas.geometry.Frame`, default: None ) –

    The coordinate system (frame) of this plate, relative to its parent. Defaults to worldXY.

  • length (float, default: None ) –

    Length of the plate. Required when no plate_geometry is provided.

  • width (float, default: None ) –

    Width of the plate. Required when no plate_geometry is provided.

  • thickness (float, default: None ) –

    Thickness of the plate. Required when no plate_geometry is provided.

  • plate_geometry (:class:`~compas_timber.elements.PlateGeometry`, default: None ) –

    A PlateGeometry object defining the plate shape in local space. When provided, dimensions must not be given.

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

    Additional keyword arguments.

Attributes:

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

    The coordinate system (frame) of this plate.

  • length (float) –

    Length of the plate.

  • width (float) –

    Width of the plate.

  • height (float) –

    Height of the plate (same as thickness).

  • thickness (float) –

    Thickness of the plate.

  • plate_geometry (:class:`~compas_timber.elements.PlateGeometry`) –

    The plate geometry object defining the shape of this plate.

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

    A polyline representing the principal outline of this plate in global space.

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

    A polyline representing the associated outline of this plate in global space.

  • is_plate (bool) –

    Always True for plates.

  • blank (:class:`~compas.geometry.Box`) –

    A feature-less box representing the material stock geometry to produce this plate.

  • blank_length (float) –

    Length of the plate blank.

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

    List of features applied to this plate.

  • key ((int, optional)) –

    Once plate is added to a model, it will have this model-wide-unique integer key.

Source code in src/compas_timber/elements/plate.py
def __init__(
    self,
    frame: Optional[Frame] = None,
    length: Optional[float] = None,
    width: Optional[float] = None,
    thickness: Optional[float] = None,
    plate_geometry: Optional[PlateGeometry] = None,
    **kwargs,
) -> None:
    if plate_geometry is not None and any(x is not None for x in [length, width, thickness]):
        raise ValueError("Plate cannot be instantiated with both a PlateGeometry and dimension arguments.")
    if plate_geometry is None:
        if not all(x is not None for x in [length, width, thickness]):
            raise ValueError("Plate must be instantiated with either a PlateGeometry or all of: length, width, thickness.")
        plate_geometry = PlateGeometry.from_dims(length, width, thickness)
    frame = frame if frame is not None else Frame.worldXY()
    super(Plate, self).__init__(frame=frame, length=plate_geometry.length, width=plate_geometry.width, height=plate_geometry.thickness, **kwargs)
    self.plate_geometry = plate_geometry
    self._outline_feature = None
    self.attributes = {}
    self.attributes.update(kwargs)
    self.debug_info = []
    self._planes = []

Methods:¤

apply_edge_extensions ¤
apply_edge_extensions() -> None

adjusts segments of the outlines to lay on the edge planes created by plate joints.

Source code in src/compas_timber/elements/plate.py
def apply_edge_extensions(self) -> None:
    """adjusts segments of the outlines to lay on the edge planes created by plate joints."""
    self.plate_geometry.apply_edge_extensions()
compute_aabb ¤
compute_aabb(inflate: float = 0.0) -> Box

Computes the Axis Aligned Bounding Box (AABB) of the element.

Parameters:

  • inflate (float, default: 0.0 ) –

    Offset of box to avoid floating point errors.

Returns:

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

    The AABB of the element.

Source code in src/compas_timber/elements/plate.py
def compute_aabb(self, inflate: float = 0.0) -> Box:
    """Computes the Axis Aligned Bounding Box (AABB) of the element.

    Parameters
    ----------
    inflate : float, optional
        Offset of box to avoid floating point errors.

    Returns
    -------
    :class:`~compas.geometry.Box`
        The AABB of the element.

    """
    vertices = self.outline_a.points + self.outline_b.points
    box = Box.from_points(vertices)
    box.xsize += inflate
    box.ysize += inflate
    box.zsize += inflate
    return box
compute_collision_mesh ¤
compute_collision_mesh() -> Mesh

Computes the collision geometry of the element.

Returns:

  • class:`compas.datastructures.Mesh` –

    The collision geometry of the element.

Source code in src/compas_timber/elements/plate.py
def compute_collision_mesh(self) -> Mesh:
    """Computes the collision geometry of the element.

    Returns
    -------
    :class:`compas.datastructures.Mesh`
        The collision geometry of the element.

    """
    return self.obb.to_mesh()
compute_elementgeometry ¤
compute_elementgeometry(include_features: bool | None = True) -> Brep | Mesh

Compute the geometry of the element.

Parameters:

  • include_features (bool, default: True ) –

    If True, include the features in the computed geometry. If False, return only the plate shape.

Returns:

  • class:`compas.datastructures.Mesh` | :class:`compas.geometry.Brep` –
Source code in src/compas_timber/elements/plate.py
def compute_elementgeometry(self, include_features: Optional[bool] = True) -> Union[Brep, Mesh]:
    """Compute the geometry of the element.

    Parameters
    ----------
    include_features : bool, optional
        If ``True``, include the features in the computed geometry.
        If ``False``, return only the plate shape.

    Returns
    -------
    :class:`compas.datastructures.Mesh` | :class:`compas.geometry.Brep`

    """

    # TODO: consider if Brep.from_curves(curves) is faster/better
    plate_geo = self.plate_geometry.compute_shape()
    if include_features:
        for feature in self._features:
            try:
                plate_geo = feature.apply(plate_geo, self)
            except FeatureApplicationError as error:
                self.debug_info.append(error)
    return plate_geo
compute_obb ¤
compute_obb(inflate: float = 0.0) -> Box

Computes the Oriented Bounding Box (OBB) of the element.

Returns:

  • class:`compas.geometry.Box` –

    The OBB of the element.

Source code in src/compas_timber/elements/plate.py
def compute_obb(self, inflate: float = 0.0) -> Box:
    """Computes the Oriented Bounding Box (OBB) of the element.

    Returns
    -------
    :class:`compas.geometry.Box`
        The OBB of the element.

    """

    obb = self.plate_geometry.compute_aabb(inflate)
    obb.transform(self.modeltransformation)
    return obb
from_brep classmethod ¤
from_brep(brep: Brep, orientation: Vector | None = None, **kwargs)

Creates a plate from a brep by automatically detecting two parallel faces.

This method identifies the two main faces of the brep using topological analysis (edge counts and adjacency) and uses them as the top and bottom faces of the plate.

Parameters:

  • brep (:class:`~compas.geometry.Brep`) –

    The brep representing the plate geometry. Must have at least 5 faces.

  • orientation (:class:`~compas.geometry.Vector`, default: None ) –

    A vector indicating the desired orientation of the plate's local y-axis. If None, orientation is determined from the outline.

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

    Additional keyword arguments. These are passed to the :class:~compas_timber.elements.Plate constructor.

Returns:

  • class:`~compas_timber.elements.Plate` –

    A Plate object created from the two parallel faces of the brep.

Source code in src/compas_timber/elements/plate.py
@classmethod
def from_brep(cls, brep: Brep, orientation: Optional[Vector] = None, **kwargs):
    """Creates a plate from a brep by automatically detecting two parallel faces.

    This method identifies the two main faces of the brep using topological analysis
    (edge counts and adjacency) and uses them as the top and bottom faces of the plate.

    Parameters
    ----------
    brep : :class:`~compas.geometry.Brep`
        The brep representing the plate geometry. Must have at least 5 faces.
    orientation : :class:`~compas.geometry.Vector`, optional
        A vector indicating the desired orientation of the plate's local y-axis. If None, orientation is determined from the outline.
    **kwargs : dict, optional
        Additional keyword arguments.
        These are passed to the :class:`~compas_timber.elements.Plate` constructor.

    Returns
    -------
    :class:`~compas_timber.elements.Plate`
        A Plate object created from the two parallel faces of the brep.
    """
    if len(brep.faces) < 5:
        raise ValueError("Brep must have at least 5 faces (2 main + 3 side for a triangular plate). This brep has {}".format(len(brep.faces)))
    outline_a, outline_b, openings = get_plate_geometry_outlines_from_brep(brep)
    return cls.from_outlines(outline_a, outline_b, openings=openings, orientation=orientation, **kwargs)
from_face_thickness classmethod ¤
from_face_thickness(
    brep: Brep,
    thickness: float,
    vector: Vector | None = None,
    orientation: Vector | None = None,
    **kwargs,
)

Creates a plate from a single-face brep.

Parameters:

  • brep (:class:`~compas.geometry.Brep`) –

    A single-face brep representing the plate surface.

  • thickness (float) –

    The thickness of the plate.

  • vector (:class:`~compas.geometry.Vector`, default: None ) –

    The vector in which the plate is extruded.

  • orientation (:class:`~compas.geometry.Vector`, default: None ) –

    A vector indicating the desired orientation of the plate's local y-axis. If None, orientation is determined from the outline.

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

    Additional keyword arguments. These are passed to the :class:~compas_timber.elements.Plate constructor.

Returns:

  • class:`~compas_timber.elements.Plate` –

    A Plate object representing the plate with the given brep face and thickness.

Source code in src/compas_timber/elements/plate.py
@classmethod
def from_face_thickness(cls, brep: Brep, thickness: float, vector: Optional[Vector] = None, orientation: Optional[Vector] = None, **kwargs):
    """Creates a plate from a single-face brep.

    Parameters
    ----------
    brep : :class:`~compas.geometry.Brep`
        A single-face brep representing the plate surface.
    thickness : float
        The thickness of the plate.
    vector : :class:`~compas.geometry.Vector`, optional
        The vector in which the plate is extruded.
    orientation : :class:`~compas.geometry.Vector`, optional
        A vector indicating the desired orientation of the plate's local y-axis. If None, orientation is determined from the outline.
    **kwargs : dict, optional
        Additional keyword arguments.
        These are passed to the :class:`~compas_timber.elements.Plate` constructor.

    Returns
    -------
    :class:`~compas_timber.elements.Plate`
        A Plate object representing the plate with the given brep face and thickness.
    """

    if len(brep.faces) > 1:
        raise ValueError("Can only use single-face breps to create a Plate. This brep has {}".format(len(brep.faces)))
    face = brep.faces[0]
    outer_polyline, inner_polylines = polylines_from_brep_face(face)
    return cls.from_outline_thickness(outer_polyline, thickness, vector=vector, openings=inner_polylines, orientation=orientation, **kwargs)
from_outline_thickness classmethod ¤
from_outline_thickness(
    outline: Polyline,
    thickness: float,
    vector: Vector | None = None,
    openings: list[Polyline] | None = None,
    orientation: Vector | None = None,
    **kwargs,
)

Constructs a Plate from a polyline outline and a thickness. The outline is the top face of the plate_geometry, and the thickness is the distance to the bottom face.

Parameters:

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

    A polyline representing the outline of the plate geometry.

  • thickness (float) –

    The thickness of the plate geometry.

  • vector (:class:`~compas.geometry.Vector`, default: None ) –

    The direction of the thickness vector. If None, the thickness vector is determined from the outline.

  • openings (list[:class:`~compas.geometry.Polyline`], default: None ) –

    A list of polyline openings to be added to the plate geometry.

  • orientation (:class:`~compas.geometry.Vector`, default: None ) –

    A vector indicating the desired orientation of the plate's local y-axis. If None, orientation is determined from the outline.

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

    Additional keyword arguments to be passed to the constructor.

Returns:

  • class:`~compas_timber.elements.Plate` –

    A Plate object representing the plate geometry with the given outline and thickness.

Source code in src/compas_timber/elements/plate.py
@classmethod
def from_outline_thickness(
    cls, outline: Polyline, thickness: float, vector: Optional[Vector] = None, openings: Optional[list[Polyline]] = None, orientation: Optional[Vector] = None, **kwargs
):
    """
    Constructs a Plate from a polyline outline and a thickness.
    The outline is the top face of the plate_geometry, and the thickness is the distance to the bottom face.

    Parameters
    ----------
    outline : :class:`~compas.geometry.Polyline`
        A polyline representing the outline of the plate geometry.
    thickness : float
        The thickness of the plate geometry.
    vector : :class:`~compas.geometry.Vector`, optional
        The direction of the thickness vector. If None, the thickness vector is determined from the outline.
    openings : list[:class:`~compas.geometry.Polyline`], optional
        A list of polyline openings to be added to the plate geometry.
    orientation : :class:`~compas.geometry.Vector`, optional
        A vector indicating the desired orientation of the plate's local y-axis. If None, orientation is determined from the outline.
    **kwargs : dict, optional
        Additional keyword arguments to be passed to the constructor.

    Returns
    -------
    :class:`~compas_timber.elements.Plate`
        A Plate object representing the plate geometry with the given outline and thickness.
    """
    # this ensure the plate geometry can always be computed
    if TOL.is_zero(thickness):
        thickness = TOL.absolute

    offset_vector = get_polyline_normal_vector(outline, vector)  # gets vector perpendicular to outline
    offset_vector *= thickness
    outline_b = Polyline(outline).translated(offset_vector)
    return cls.from_outlines(outline, outline_b, openings=openings, orientation=orientation, **kwargs)
from_outlines classmethod ¤
from_outlines(
    outline_a: Polyline,
    outline_b: Polyline,
    openings: list[Polyline] | None = None,
    orientation: Vector | None = None,
    **kwargs,
)

Constructs a Plate from two polyline outlines.

Parameters:

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

    A polyline representing the principal outline of the plate geometry in parent space.

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

    A polyline representing the associated outline of the plate geometry in parent space. This should have the same number of points as outline_a.

  • openings (list[:class:`~compas.geometry.Polyline`], default: None ) –

    A list of openings to be added to the plate geometry.

  • orientation (:class:`~compas.geometry.Vector`, default: None ) –

    A vector indicating the desired orientation of the plate's local y-axis. If None, orientation is determined from the outline.

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

    Additional keyword arguments to be passed to the constructor.

Returns:

  • class:`~compas_timber.elements.Plate` –

    A Plate object representing the plate geometry with the given outlines.

Source code in src/compas_timber/elements/plate.py
@classmethod
def from_outlines(cls, outline_a: Polyline, outline_b: Polyline, openings: Optional[list[Polyline]] = None, orientation: Optional[Vector] = None, **kwargs):
    """
    Constructs a Plate from two polyline outlines.

    Parameters
    ----------
    outline_a : :class:`~compas.geometry.Polyline`
        A polyline representing the principal outline of the plate geometry in parent space.
    outline_b : :class:`~compas.geometry.Polyline`
        A polyline representing the associated outline of the plate geometry in parent space.
        This should have the same number of points as outline_a.
    openings : list[:class:`~compas.geometry.Polyline`], optional
        A list of openings to be added to the plate geometry.
    orientation : :class:`~compas.geometry.Vector`, optional
        A vector indicating the desired orientation of the plate's local y-axis. If None, orientation is determined from the outline.
    **kwargs : dict, optional
        Additional keyword arguments to be passed to the constructor.

    Returns
    -------
    :class:`~compas_timber.elements.Plate`
        A Plate object representing the plate geometry with the given outlines.
    """
    frame, local_outline_a, local_outline_b = global_outlines_to_local(outline_a, outline_b, orientation=orientation)
    plate_geometry = PlateGeometry(local_outline_a, local_outline_b)
    plate = cls(frame=frame, plate_geometry=plate_geometry, **kwargs)
    if openings:
        for opening in openings:
            plate.add_feature(FreeContour.from_polyline_and_element(opening, plate, interior=True))
    return plate
remove_blank_extension ¤
remove_blank_extension(edge_index: int | None = None)

Removes any extension plane for the given edge index.

Source code in src/compas_timber/elements/plate.py
def remove_blank_extension(self, edge_index: Optional[int] = None):
    """Removes any extension plane for the given edge index."""
    self.plate_geometry.remove_blank_extension(edge_index)
reset ¤
reset()

Resets the element to its initial state by removing all features, extensions, and debug_info.

Source code in src/compas_timber/elements/plate.py
def reset(self):
    """Resets the element to its initial state by removing all features, extensions, and debug_info."""
    self.plate_geometry.reset()  # reset outline_a and outline_b
    self._features = []
    self.debug_info = []
    clear_element_cache(self)
set_extension_plane ¤
set_extension_plane(edge_index: int, plane: Plane) -> None

Sets an extension plane for a specific edge of the plate. This is called by plate joints.

Source code in src/compas_timber/elements/plate.py
def set_extension_plane(self, edge_index: int, plane: Plane) -> None:
    """Sets an extension plane for a specific edge of the plate. This is called by plate joints."""
    self.plate_geometry.set_extension_plane(edge_index, plane.transformed(self.transformation_to_local()))

PlateFastener ¤

PlateFastener(
    outline=None,
    thickness=None,
    interfaces=None,
    frame=None,
    angle=pi / 2,
    topology=None,
    cutouts=None,
    **kwargs,
)

A class to represent flat plate timber fasteners (e.g. steel plates).

Parameters:

  • outline (list of :class:`~compas.geometry.Point`, default: None ) –

    The outline of the plate in the local XY plane.

  • thickness (float, default: None ) –

    Thickness of the plate.

  • interfaces (list of :class:`~compas_timber.elements.FastenerTimberInterface, default: None ) –

    The interfaces of the fastener. The order of the interfaces should correspond to the order of the beams in the joint.

  • frame (:class:`~compas.geometry.Frame`, default: None ) –

    The frame of the instance of the fastener that is applied to the model. The fastener should be defined at the XY plane origin with the x-axis pointing in the direction of the main_beam.

  • angle ((float, optional(default=pi / 2)), default: pi / 2 ) –

    The angle of the fastener. The angle between the beam elements must be the same.

  • topology (int or list of int, default: None ) –

    The supported topology of the joint.

  • cutouts (list of :class:`~compas.geometry.Polyline`, default: None ) –

    A list of cutouts to be subtracted from the base shape of the fastener.

Attributes:

  • geometry (:class:`~compas.geometry.Geometry`) –

    The geometry of the fastener.

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

    The frame of the fastener.

Source code in src/compas_timber/elements/fasteners/plate_fastener.py
def __init__(self, outline=None, thickness=None, interfaces=None, frame=None, angle=math.pi / 2, topology=None, cutouts=None, **kwargs):
    super(PlateFastener, self).__init__(**kwargs)
    self.outline = outline
    self.thickness = thickness
    self.interfaces = interfaces
    self.frame = frame or Frame.worldXY()
    self.angle = angle
    self.topology = topology
    self.cutouts = cutouts
    self.attributes = {}
    self.attributes.update(kwargs)
    self.debug_info = []

Attributes¤

shape property ¤
shape

Constructs the base shape of the fastener.This is located at the origin of the XY plane with the x-axis pointing in the direction of the main_beam.

Returns:

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

Methods:¤

add_features ¤
add_features()

Adds the geometric features to the beams. TODO: add btlx features in separate function.

Source code in src/compas_timber/elements/fasteners/plate_fastener.py
def add_features(self):
    """Adds the geometric features to the beams. TODO: add btlx features in separate function."""
    for beam, interface in zip([self.beam_a, self.beam_b], self.interfaces):
        interface.add_features(beam)
compute_geometry ¤
compute_geometry()

Constructs the geometry of the fastener as oriented in space.

Returns:

  • class:`~compas.geometry.Brep` –
Source code in src/compas_timber/elements/fasteners/plate_fastener.py
def compute_geometry(self):
    """Constructs the geometry of the fastener as oriented in space.

    Returns
    -------
    :class:`~compas.geometry.Brep`

    """
    return self.shape.transformed(Transformation.from_frame(self.frame)) if self.shape else None
get_fastener_frames ¤
get_fastener_frames(joint)

Calculates the frames of the fasteners.

Returns:

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

    The frames of the fasteners with the x-axis along the main_beam.centerline and the y-axis along the cross_beam.centerline, offset to lay on the beam_faces.

Source code in src/compas_timber/elements/fasteners/plate_fastener.py
def get_fastener_frames(self, joint):
    """Calculates the frames of the fasteners.

    Returns
    -------
    :class:`~compas.geometry.Frame`
        The frames of the fasteners with the x-axis along the main_beam.centerline and the y-axis along the cross_beam.centerline, offset to lay on the beam_faces.

    """
    front_face_index, back_face_index = self.validate_fastener_beam_compatibility(joint)
    beam_a, beam_b = joint.elements[0:2]
    (main_point, main_param), (cross_point, _) = intersection_line_line_param(beam_a.centerline, beam_b.centerline)
    int_point = (main_point + cross_point) * 0.5
    front_face = beam_a.ref_sides[front_face_index]
    front_point = Plane.from_frame(front_face).closest_point(int_point)
    front_frame = Frame(
        front_point,
        beam_a.centerline.direction if main_param < 0.5 else -beam_a.centerline.direction,
        front_face.normal,
    )
    front_frame.rotate(-math.pi / 2, front_frame.xaxis, front_point)
    back_face = beam_a.ref_sides[back_face_index]
    back_point = Plane.from_frame(back_face).closest_point(int_point)

    back_frame = Frame(
        back_point,
        beam_a.centerline.direction if main_param < 0.5 else -beam_a.centerline.direction,
        back_face.normal,
    )
    back_frame.rotate(-math.pi / 2, back_frame.xaxis, back_point)
    return [front_frame, back_frame]
place_instances ¤
place_instances(joint)

Adds the fasteners to the joint.

This method is automatically called when joint is created by the call to Joint.create().

Source code in src/compas_timber/elements/fasteners/plate_fastener.py
def place_instances(self, joint):
    """Adds the fasteners to the joint.

    This method is automatically called when joint is created by the call to `Joint.create()`.

    """
    frames = self.get_fastener_frames(joint)
    for frame in frames:
        fastener = self.copy()
        fastener.frame = Frame(frame.point, frame.xaxis, frame.yaxis)
        for interface, element in zip(fastener.interfaces, joint.elements):
            interface.element = element
        joint.fasteners.append(fastener)
validate_fastener_beam_compatibility ¤
validate_fastener_beam_compatibility(joint)

Checks if the beams are compatible with the joint and sets the front and back face indices.

returns the front and back face indices of the cross beam.

Raises:

  • BeamJoiningError –

    If the beams are not compatible.

Source code in src/compas_timber/elements/fasteners/plate_fastener.py
def validate_fastener_beam_compatibility(self, joint):
    """Checks if the beams are compatible with the joint and sets the front and back face indices.

    returns the front and back face indices of the cross beam.

    Raises
    ------
    BeamJoiningError
        If the beams are not compatible.

    """
    beam_a, beam_b = joint.elements[0:2]
    if not TOL.is_zero(angle_vectors(beam_a.frame.xaxis, beam_b.frame.xaxis) - self.angle):
        raise FastenerApplicationError(elements=[beam_a, beam_b], fastener=self, message="Beams are not perpendicular")

    cross_vector = cross_vectors(beam_a.centerline.direction, beam_b.centerline.direction)
    main_faces = beam_ref_side_incidence_with_vector(beam_a, cross_vector)
    cross_faces = beam_ref_side_incidence_with_vector(beam_b, cross_vector)
    front_face_index = min(main_faces, key=main_faces.get)
    cross_face_index = min(cross_faces, key=cross_faces.get)
    if not TOL.is_zero(main_faces[front_face_index]):
        raise FastenerApplicationError(
            elements=[beam_a, beam_b],
            fastener=self,
            message="beam_a is not perpendicular to the cross vector",
        )
    if not TOL.is_zero(cross_faces[cross_face_index]):
        raise FastenerApplicationError(
            elements=[beam_a, beam_b],
            fastener=self,
            message="Cross beam is not perpendicular to the cross vector",
        )
    if not TOL.is_zero(
        distance_point_plane(
            beam_a.ref_sides[front_face_index].point,
            Plane.from_frame(beam_b.ref_sides[cross_face_index]),
        )
    ):
        raise FastenerApplicationError(elements=[beam_a, beam_b], fastener=self, message="beam faces are not coplanar")
    back_face_index = (front_face_index + 2) % 4
    cross_back_face_index = (cross_face_index + 2) % 4
    if not TOL.is_zero(
        distance_point_plane(
            beam_a.ref_sides[back_face_index].point,
            Plane.from_frame(beam_b.ref_sides[cross_back_face_index]),
        )
    ):
        raise FastenerApplicationError(elements=[beam_a, beam_b], fastener=self, message="beam faces are not coplanar")
    return front_face_index, back_face_index

PlateGeometry ¤

PlateGeometry(local_outline_a: Polyline, local_outline_b: Polyline, **kwargs)

A class to represent plate-like objects (plate, panel, etc.) defined by polylines on top and bottom faces of shape.

Parameters:

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

    The principal outline of the plate in local space (projected on worldXY, starting at origin).

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

    The associated outline of the plate in local space. Must have the same number of points as outline_a, be parallel to outline_a, and be offset in the +Z direction.

Attributes:

  • length (float) –

    Length of the plate along the local x-axis.

  • width (float) –

    Width of the plate along the local y-axis.

  • thickness (float) –

    Thickness of the plate along the local z-axis.

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

    The principal outline of the plate in local space (mutable, affected by edge extensions).

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

    The associated outline of the plate in local space (mutable, affected by edge extensions).

  • outlines (tuple[:class:`~compas.geometry.Polyline`, :class:`~compas.geometry.Polyline`]) –

    A tuple containing both outline_a and outline_b.

  • edge_planes (list[:class:`~compas.geometry.Frame`]) –

    Frames representing the edge planes of the plate.

  • shape (:class:`~compas.geometry.Brep`) –

    The geometry of the Plate before other machining features are applied.

Source code in src/compas_timber/elements/plate_geometry.py
def __init__(self, local_outline_a: Polyline, local_outline_b: Polyline, **kwargs):
    super().__init__(**kwargs)
    self._check_outlines(local_outline_a, local_outline_b)
    box = Box.from_points(local_outline_a.points + local_outline_b.points)
    self.length = box.xsize
    self.width = box.ysize
    self.thickness = box.zsize
    self._original_outlines = None
    self._mutable_outlines = None
    self._original_edge_planes = {}
    self._set_original_attributes(local_outline_a, local_outline_b)
    self._extension_planes = {}

Methods:¤

apply_edge_extensions ¤
apply_edge_extensions() -> None

adjusts segments of the outlines to lay on the edge planes created by plate joints.

Source code in src/compas_timber/elements/plate_geometry.py
def apply_edge_extensions(self) -> None:
    """adjusts segments of the outlines to lay on the edge planes created by plate joints."""
    # TODO: Add an optional edge_index argument to only apply a specific edge extension for performance?
    for edge_index, plane in self.edge_planes.items():
        for polyline in self._mutable_outlines:
            move_polyline_segment_to_plane(polyline, edge_index, plane)
compute_shape ¤
compute_shape() -> Brep

The shape of the plate before other features area applied.

Returns:

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

    The shape of the element.

Source code in src/compas_timber/elements/plate_geometry.py
def compute_shape(self) -> Brep:
    """The shape of the plate before other features area applied.

    Returns
    -------
    :class:`~compas.geometry.Brep`
        The shape of the element.

    """
    self.apply_edge_extensions()
    plate_geo = brep_from_outlines(self.outline_a, self.outline_b)

    return plate_geo
from_dims classmethod ¤
from_dims(length: float, width: float, thickness: float) -> PlateGeometry

Creates a PlateGeometry with a rectangular outline from dimensions.

Parameters:

  • length (float) –

    Length of the plate along the local x-axis.

  • width (float) –

    Width of the plate along the local y-axis.

  • thickness (float) –

    Thickness of the plate along the local z-axis.

Returns:

  • class:`PlateGeometry` –
Source code in src/compas_timber/elements/plate_geometry.py
@classmethod
def from_dims(cls, length: float, width: float, thickness: float) -> "PlateGeometry":
    """Creates a PlateGeometry with a rectangular outline from dimensions.

    Parameters
    ----------
    length : float
        Length of the plate along the local x-axis.
    width : float
        Width of the plate along the local y-axis.
    thickness : float
        Thickness of the plate along the local z-axis.

    Returns
    -------
    :class:`PlateGeometry`
    """
    local_outline_a = Polyline([Point(0, 0, 0), Point(length, 0, 0), Point(length, width, 0), Point(0, width, 0), Point(0, 0, 0)])
    local_outline_b = Polyline([Point(p[0], p[1], thickness) for p in local_outline_a.points])
    return cls(local_outline_a, local_outline_b)
remove_blank_extension ¤
remove_blank_extension(edge_index: int | None = None)

Reverts any extension plane for the given edge index to the original and adjusts that .

Source code in src/compas_timber/elements/plate_geometry.py
def remove_blank_extension(self, edge_index: Optional[int] = None):
    """Reverts any extension plane for the given edge index to the original and adjusts that ."""
    if edge_index is None:
        # reset all edges to original
        self.reset()
        return
    if edge_index > len(self._mutable_outlines[0]) - 1:
        raise ValueError("Edge index out of range.")
    if edge_index in self._extension_planes:
        # delete the externally set extension plane
        del self._extension_planes[edge_index]
    for pl in self._mutable_outlines:
        # revert the polyline segment to the original edge plane
        move_polyline_segment_to_plane(pl, edge_index, self._original_edge_planes[edge_index])
reset ¤
reset()

Resets the element outlines to their initial state.

Source code in src/compas_timber/elements/plate_geometry.py
def reset(self):
    """Resets the element outlines to their initial state."""
    self._mutable_outlines = (self._original_outlines[0].copy(), self._original_outlines[1].copy())
    self._extension_planes = {}
set_extension_plane ¤
set_extension_plane(edge_index: int, plane: Plane) -> None

Sets an extension plane for a specific edge of the plate. This is called by plate joints.

Source code in src/compas_timber/elements/plate_geometry.py
def set_extension_plane(self, edge_index: int, plane: Plane) -> None:
    """Sets an extension plane for a specific edge of the plate. This is called by plate joints."""
    self._extension_planes[edge_index] = self._corrected_edge_plane(edge_index, plane)

Functions:¤

global_outlines_to_local ¤

global_outlines_to_local(
    outline_a: Polyline, outline_b: Polyline, orientation: Vector | None = None
) -> tuple[Frame, Polyline, Polyline]

Splits two plate outlines in global (world) space into the plate's local frame and its local outlines.

The local outlines lie on worldXY and start at the origin, as :class:PlateGeometry expects them. The frame places them back in global space, and is what the owning element takes as its frame.

Parameters:

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

    Principal outline of the plate in global space.

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

    Associated outline of the plate in global space. Must be parallel to outline_a and offset in the plate's normal direction.

  • orientation (:class:`~compas.geometry.Vector`, default: None ) –

    A vector that controls the direction of the local coordinate frame. When provided, the vector is projected onto the outline plane and used to define the local y-axis direction, overriding the frame derived automatically from the outline geometry. If None, the frame is determined from the outline.

Returns:

  • tuple[:class:`~compas.geometry.Frame`, :class:`~compas.geometry.Polyline`, :class:`~compas.geometry.Polyline`] –

    The local coordinate frame of the plate in global space, and outline_a and outline_b in local space.

Source code in src/compas_timber/elements/plate_geometry.py
def global_outlines_to_local(outline_a: Polyline, outline_b: Polyline, orientation: Optional[Vector] = None) -> tuple[Frame, Polyline, Polyline]:
    """Splits two plate outlines in global (world) space into the plate's local frame and its local outlines.

    The local outlines lie on worldXY and start at the origin, as :class:`PlateGeometry` expects them.
    The frame places them back in global space, and is what the owning element takes as its ``frame``.

    Parameters
    ----------
    outline_a : :class:`~compas.geometry.Polyline`
        Principal outline of the plate in global space.
    outline_b : :class:`~compas.geometry.Polyline`
        Associated outline of the plate in global space. Must be parallel to outline_a
        and offset in the plate's normal direction.
    orientation : :class:`~compas.geometry.Vector`, optional
        A vector that controls the direction of the local coordinate frame.
        When provided, the vector is projected onto the outline plane and used to define
        the local y-axis direction, overriding the frame derived automatically from the
        outline geometry. If ``None``, the frame is determined from the outline.

    Returns
    -------
    tuple[:class:`~compas.geometry.Frame`, :class:`~compas.geometry.Polyline`, :class:`~compas.geometry.Polyline`]
        The local coordinate frame of the plate in global space, and outline_a and outline_b in local space.
    """
    # get 3 non-colinear points
    pt_c = None
    vector_a = Vector.from_start_end(outline_a[0], outline_a[1])
    for pt in outline_a.points[-1:1:-1]:  # walk bakwards along outline_a.points
        vector_b = Vector.from_start_end(outline_a[0], pt)
        if not TOL.is_allclose(cross_vectors(vector_a, vector_b), [0, 0, 0]):  # cross_vectors returns [0,0,0] for parallel vectors
            pt_c = pt
    if not pt_c:
        raise ValueError("outline_a appears to only have colinear points")

    # get initial frame for rebasing to XY-plane
    frame = Frame.from_points(outline_a[0], outline_a[1], pt_c)

    if orientation:
        frame_x = cross_vectors(orientation, frame.normal)
        frame_y = cross_vectors(frame.normal, frame_x)
        frame = Frame(outline_a[0], frame_x, frame_y)  # create new frame based on orientation

    if dot_vectors(Vector.from_start_end(outline_a[0], outline_b[0]), frame.normal) < 0:
        frame = Frame(frame.point, -frame.xaxis, frame.yaxis)  # flip frame if outline_b in -z space
    transform_to_world_xy = Transformation.from_frame_to_frame(frame, Frame.worldXY())

    # move polylines to XY
    rebased_pline_a = Polyline([pt.transformed(transform_to_world_xy) for pt in outline_a.points])
    rebased_pline_b = Polyline([pt.transformed(transform_to_world_xy) for pt in outline_b.points])
    # TODO: rebasing to positive XY space (code below) shouldn't be strictly necessary.
    box = Box.from_points(rebased_pline_a.points + rebased_pline_b.points)
    frame = Frame(box.points[0], Vector(1, 0, 0), Vector(0, 1, 0))  # frame at bounding box corner
    frame.transform(transform_to_world_xy.inverse())
    vector_to_xy = Vector.from_start_end(box.points[0], Point(0, 0, 0))
    # move polylines to positive XY space
    local_outline_a = Polyline([pt.translated(vector_to_xy) for pt in rebased_pline_a.points])
    local_outline_b = Polyline([pt.translated(vector_to_xy) for pt in rebased_pline_b.points])
    return frame, local_outline_a, local_outline_b