Skip to content

compas_timber.fabrication ¤

Classes¤

AlignmentType ¤

Enum for the alignment of the cut.

Attributes:

  • TOP (literal(top)) –

    Top alignment.

  • BOTTOM (literal(bottom)) –

    Bottom alignment.

  • LEFT (literal(left)) –

    Left alignment.

  • RIGHT (literal(right)) –

    Right alignment.

  • CENTER (literal(center)) –

    Center alignment.

BTLxFromGeometryDefinition ¤

BTLxFromGeometryDefinition(processing, geometries, elements=None, **kwargs)

Container linking a BTLx Process Type and generator function to an input geometry. This allows delaying the actual applying of features to a downstream component.

Parameters:

  • processing (class) –

    The BTLx Processing class.

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

    The geometries to be used as input for the processing.

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

    The elements to be used as input for the processing.

Attributes:

  • processing (class) –

    The BTLx Processing class.

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

    The geometries to be used as input for the processing.

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

    The elements to be used as input for the processing.

Source code in src/compas_timber/fabrication/btlx.py
def __init__(self, processing, geometries, elements=None, **kwargs):
    super(BTLxFromGeometryDefinition, self).__init__()
    self.processing = processing
    self.geometries = geometries if isinstance(geometries, list) else [geometries]
    if elements:
        self.elements = elements if isinstance(elements, list) else [elements]
    else:
        self.elements = []
    self.kwargs = kwargs or {}

BTLxPart ¤

BTLxPart(element, order_num, scale_factor=1.0)

Class representing a BTLx part. This acts as a wrapper for an Element object.

Parameters:

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

    The element object.

  • order_num (int) –

    The order number of the part.

  • scale_factor (float, default: 1.0 ) –

    The scale factor to apply to the part's dimensions. Used when the design unit is anything other than mm. Defaults to 1.0.

Attributes:

  • attr (dict) –

    The attributes of the BTLx part.

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

    The element object.

  • length (float) –

    The length of the element.

  • width (float) –

    The width of the element.

  • height (float) –

    The height of the element.

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

    The frame of the BTLxPart at the corner of the blank box that puts the blank geometry in positive coordinates.

  • processings (list) –

    A list of the processings applied to the element.

Source code in src/compas_timber/fabrication/btlx.py
def __init__(self, element, order_num, scale_factor=1.0):
    super(BTLxPart, self).__init__(order_num, element.blank_length, element.width, element.height, element.name, scale_factor)
    self.element = element
    self.processings = []
    self._shape_strings = None

Attributes¤

attr property ¤
attr

Return the attributes dictionary for the part XML element.

part_guid property ¤
part_guid

Return the GUID of the element.

shape_strings property ¤
shape_strings

Generates the shape strings for the BTLxPart. Only works in environments where the element.geometry Brep is available.

Returns:

  • list –

    A list of two strings, the first string is the brep indices string, the second string is the brep vertices string.

Methods:¤

ref_side_from_face ¤
ref_side_from_face(element_face)

Finds the one-based index of the reference side with normal that matches the normal of the given element face.

This essentially translates between the element face reference system to the BTLx side reference system.

Parameters:

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

    The frame of an element face from element.faces.

Returns:

  • key ( str ) –

    The key(index 1-6) of the reference surface.

Source code in src/compas_timber/fabrication/btlx.py
def ref_side_from_face(self, element_face):
    """Finds the one-based index of the reference side with normal that matches the normal of the given element face.

    This essentially translates between the element face reference system to the BTLx side reference system.

    Parameters
    ----------
    element_face : :class:`~compas.geometry.Frame`
        The frame of an element face from element.faces.

    Returns
    -------
    key : str
        The key(index 1-6) of the reference surface.

    """
    for index, ref_side in enumerate(self.element.ref_sides):
        angle = angle_vectors(ref_side.normal, element_face.normal, deg=True)
        if TOL.is_zero(angle):
            return index + 1  # in BTLx face indices are one-based
    raise ValueError("Given element face does not match any of the reference surfaces.")

BTLxProcessing ¤

BTLxProcessing(
    ref_side_index=None,
    priority=0,
    process_id=0,
    tool_id=None,
    counter_sink=None,
    tool_position=None,
    name=None,
    process=None,
    is_joinery=True,
)

Abstract base class for BTLx Processing.

Attributes:

  • ref_side_index (int) –

    The reference side, zero-based, index of the element to be cut. 0-5 correspond to RS1-RS6. Defaults to 0 (RS1).

  • priority (int) –

    The priority of the process.

  • process_id (int) –

    The process ID.

  • tool_id ((int, optional)) –

    The tool ID for the processing. Only used by specific processing types.

  • counter_sink ((bool, optional)) –

    If True, the processing creates a counter sink. Only used by specific processing types.

  • tool_position (:class:`~compas_timber.fabrication.AlignmentType`) –

    The position of the tool relative to the beam. Can be 'left', 'center', or 'right'. Only used by specific processing types.

  • PROCESSING_NAME (str) –

    The name of the process.

  • ATTRIBUTE_MAP (dict) –

    Mapping of BTLx XML attribute names to Python attribute names.

  • HEADER_ATTRIBUTE_MAP (dict) –

    Mapping of BTLx XML header attribute names (in XML attributes) to Python parameter names with converters.

  • is_joinery (bool) –

    If True, the process is a result of joinery process.

  • params (:class:`~compas_timber.fabrication.BTLxProcessingParams`) –

    The BTLx processing parameters for serialization.

Source code in src/compas_timber/fabrication/btlx.py
def __init__(self, ref_side_index=None, priority=0, process_id=0, tool_id=None, counter_sink=None, tool_position=None, name=None, process=None, is_joinery=True):
    super(BTLxProcessing, self).__init__()
    self._ref_side_index = None
    self._priority = priority
    self._process_id = process_id
    self.ref_side_index = ref_side_index or 0
    self.subprocessings = None
    self._is_joinery = is_joinery
    self._name = name
    self._process = process
    # Optional header attributes - set by subclasses if needed
    self._tool_id = tool_id
    self._counter_sink = counter_sink
    self._tool_position = tool_position

Attributes¤

ATTRIBUTE_MAP abstractmethod property ¤
ATTRIBUTE_MAP

Mapping of BTLx XML attribute names to Python attribute names.

PROCESSING_NAME abstractmethod property ¤
PROCESSING_NAME

The name of the processing for BTLx serialization.

Methods:¤

add_subprocessing ¤
add_subprocessing(subprocessing)

Add a nested subprocessing.

Source code in src/compas_timber/fabrication/btlx.py
def add_subprocessing(self, subprocessing):
    """Add a nested subprocessing."""
    if not self.subprocessings:
        self.subprocessings = []
    self.subprocessings.append(subprocessing)
scaled ¤
scaled(factor)

Returns a new instance of the processing with the parameters scaled by a given factor.

Parameters:

  • factor (float) –

    The scaling factor.

Returns:

  • class:`~compas_timber.fabrication.BTLxProcessing` –

    A new instance of the processing with the parameters scaled by the given factor.

Source code in src/compas_timber/fabrication/btlx.py
def scaled(self, factor):
    """Returns a new instance of the processing with the parameters scaled by a given factor.

    Parameters
    ----------
    factor : float
        The scaling factor.

    Returns
    -------
    :class:`~compas_timber.fabrication.BTLxProcessing`
        A new instance of the processing with the parameters scaled by the given factor.

    """
    # type: (float) -> BTLxProcessing
    new_instance = self.copy()
    new_instance.scale(factor)
    return new_instance

BTLxRawpart ¤

BTLxRawpart(stock, order_number, scale_factor=1.0)

Class representing a BTLx Rawpart. This acts as a wrapper for raw material information.

Parameters:

  • stock (:class:`~compas_timber.planning.Stock`) –

    The stock object representing the raw material.

  • order_number (int) –

    The order number for the rawpart.

  • scale_factor (float, default: 1.0 ) –

    The scale factor to apply to dimensions. Defaults to 1.0.

Attributes:

  • stock (:class:`~compas_timber.planning.Stock`) –

    The stock object.

  • attr (dict) –

    The attributes of the BTLx rawpart.

  • length (float) –

    The length of the rawpart.

  • width (float) –

    The width of the rawpart.

  • height (float) –

    The height of the rawpart.

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

    The frame of the BTLxRawpart.

  • part_refs (list) –

    List of part references that are nested in this rawpart.

Source code in src/compas_timber/fabrication/btlx.py
def __init__(self, stock, order_number, scale_factor=1.0):
    super(BTLxRawpart, self).__init__(
        order_number,
        stock.length,
        stock.width,
        stock.height,
        stock.name,
        scale_factor,
    )
    self.stock = stock
    self.part_refs = []
    self._part_guid = str(uuid.uuid4())  # Cache the GUID

Attributes¤

attr property ¤
attr

Return the attributes dictionary for the rawpart XML element.

et_part_refs property ¤
et_part_refs

Create the part references XML element.

part_guid property ¤
part_guid

Return the cached GUID for this rawpart.

Methods:¤

add_part_ref ¤
add_part_ref(part_guid, position_frame)

Add a part reference to this rawpart.

Parameters:

  • part_guid (str) –

    The GUID of the part to reference.

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

    The position frame of the part within the rawpart.

Source code in src/compas_timber/fabrication/btlx.py
def add_part_ref(self, part_guid, position_frame):
    """Add a part reference to this rawpart.

    Parameters
    ----------
    part_guid : str
        The GUID of the part to reference.
    position_frame : :class:`~compas.geometry.Frame`
        The position frame of the part within the rawpart.
    """
    self.part_refs.append({"guid": part_guid, "frame": position_frame})

BTLxWriter ¤

BTLxWriter(project_name=None, company_name=None, file_name=None, comment=None)

Class for writing BTLx files from a given model.

BTLx is a format used for representing timber fabrication data.

Use BTLxWriter.write() to write a BTLx file from a model and a file path.

Parameters:

  • company_name (str, default: None ) –

    The name of the company. Defaults to "Gramazio Kohler Research".

  • file_name (str, default: None ) –

    The name of the file. Defaults to None.

  • comment (str, default: None ) –

    A comment to be included in the file. Defaults to None.

Source code in src/compas_timber/fabrication/btlx.py
def __init__(self, project_name=None, company_name=None, file_name=None, comment=None):
    self.company_name = company_name
    self.file_name = file_name
    self.comment = comment
    self._project_name = project_name or "COMPAS Timber Project"
    self._tolerance = TOL
    self._errors = []

Methods:¤

model_to_xml ¤
model_to_xml(model, nesting_result=None)

Converts the model to an XML string.

Parameters:

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

    The model object.

  • nesting_result (:class:`~compas_timber.planning.NestingResult`, default: None ) –

    The nesting result object. If provided, raw parts will be created for each stock in the nesting result.

Returns:

  • str –

    The XML string of the BTLx file.

See Also

:meth:BTLxWriter.write

Source code in src/compas_timber/fabrication/btlx.py
def model_to_xml(self, model, nesting_result=None):
    """Converts the model to an XML string.

    Parameters
    ----------
    model : :class:`~compas_timber.model.TimberModel`
        The model object.
    nesting_result : :class:`~compas_timber.planning.NestingResult`, optional
        The nesting result object. If provided, raw parts will be created for each stock in the nesting result.

    Returns
    -------
    str
        The XML string of the BTLx file.

    See Also
    --------
    :meth:`BTLxWriter.write`

    """
    self._errors = []
    self._tolerance = model.tolerance
    if self._tolerance.unit == "M":
        warn("Model units are set to {} and will auto-scale to mm for BTLx. Please design in mm if you intend to use BTLx.".format(self._tolerance.unit))

    root_element = ET.Element("BTLx", self.FILE_ATTRIBUTES)
    # first child -> file_history
    file_history_element = self._create_file_history()
    # second child -> project
    project_element = self._create_project_element(model, nesting_result)
    root_element.extend([file_history_element, project_element])
    return MD.parseString(ET.tostring(root_element)).toprettyxml(indent="   ")
register_type_serializer classmethod ¤
register_type_serializer(type_, serializer)

Register a type and its serializer.

Parameters:

  • type_ (str) –

    The name of the type to be serialized, i.e. its __name__ attribute.

  • serializer (callable) –

    The serializer function. Takes an instance of the named type and returns an XML element which corresponds with it.

Source code in src/compas_timber/fabrication/btlx.py
@classmethod
def register_type_serializer(cls, type_, serializer):
    """Register a type and its serializer.

    Parameters
    ----------
    type_ : str
        The name of the type to be serialized, i.e. its ``__name__`` attribute.
    serializer : callable
        The serializer function. Takes an instance of the named type and returns an XML element which corresponds with it.

    """
    cls.SERIALIZERS[type_] = serializer
write ¤
write(model, file_path, nesting_result=None)

Writes the BTLx file to the given file path.

Parameters:

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

    The model object.

  • file_path (str) –

    The file path to write the BTLx file to.

  • nesting_result (:class:`~compas_timber.planning.NestingResult`, default: None ) –

    The nesting result object. If provided, raw parts will be created for each stock in the nesting result.

Returns:

  • str –

    The XML string of the BTLx file.

See Also

:meth:BTLxWriter.model_to_xml

Source code in src/compas_timber/fabrication/btlx.py
def write(self, model, file_path, nesting_result=None):
    """Writes the BTLx file to the given file path.

    Parameters
    ----------
    model : :class:`~compas_timber.model.TimberModel`
        The model object.
    file_path : str
        The file path to write the BTLx file to.
    nesting_result : :class:`~compas_timber.planning.NestingResult`, optional
        The nesting result object. If provided, raw parts will be created for each stock in the nesting result.

    Returns
    -------
    str
        The XML string of the BTLx file.

    See Also
    --------
    :meth:`BTLxWriter.model_to_xml`

    """
    if not file_path.endswith(".btlx"):
        file_path += ".btlx"
    btlx_string = self.model_to_xml(model, nesting_result)
    with open(file_path, "w") as file:
        file.write(btlx_string)
    return btlx_string

BirdsMouth ¤

BirdsMouth(
    orientation: OrientationType = START,
    start_x: float = 0.0,
    start_y: float = 0.0,
    start_depth: float = 20.0,
    angle: float = 90.0,
    inclination_1: float = 45.0,
    inclination_2: float = 135.0,
    depth: float = 20.0,
    width: float = 0.0,
    width_counter_part_limited: bool = False,
    width_counter_part: float = 120.0,
    height_counter_part_limited: bool = False,
    height_counter_part: float = 120.0,
    face_limited_front: bool = False,
    face_limited_back: bool = False,
    lead_angle_parallel: bool = True,
    lead_angle: float = 90.0,
    lead_inclination_parallel: bool = True,
    lead_inclination: float = 90.0,
    rafter_nail_hole: bool = False,
    **kwargs,
)

Represents a Birds Mouth feature to be made on a timber element.

Parameters:

  • orientation (str, default: START ) –

    The orientation of the cut. Must be either OrientationType.START or OrientationType.END.

  • start_x (float, default: 0.0 ) –

    The start x-coordinate of the cut in parametric space of the reference side. -100000.0 < start_x < 100000.0.

  • start_y (float, default: 0.0 ) –

    The start y-coordinate of the cut in parametric space of the reference side. -50000.0 < start_y < 50000.0.

  • start_depth (float, default: 20.0 ) –

    The start depth of the cut. 0.0 < start_depth < 50000.0.

  • angle (float, default: 90.0 ) –

    The angle of the cut in degrees. 0.1 < angle < 179.9.

  • inclination_1 (float, default: 45.0 ) –

    The first inclination angle of the cut in degrees. 0.0 < inclination_1 < 180.0.

  • inclination_2 (float, default: 135.0 ) –

    The second inclination angle of the cut in degrees. 0.0 < inclination_2 < 180.0.

  • depth (float, default: 20.0 ) –

    The depth of the cut. 0.0 < depth < 50000.0.

  • width (float, default: 0.0 ) –

    The width of the cut. 0.0 < width < 50000.0.

  • width_counter_part_limited (bool, default: False ) –

    Whether the width of the counter part is limited.

  • width_counter_part (float, default: 120.0 ) –

    The width of the counter part. 0.0 < width_counter_part < 50000.0.

  • height_counter_part_limited (bool, default: False ) –

    Whether the height of the counter part is limited.

  • height_counter_part (float, default: 120.0 ) –

    The height of the counter part. 0.0 < height_counter_part < 50000.0.

  • face_limited_front (bool, default: False ) –

    Whether the front face is limited.

  • face_limited_back (bool, default: False ) –

    Whether the back face is limited.

  • lead_angle_parallel (bool, default: True ) –

    Whether the lead angle is parallel to the reference side.

  • lead_angle (float, default: 90.0 ) –

    The lead angle in degrees. 0.1 < lead_angle < 179.9.

  • lead_inclination_parallel (bool, default: True ) –

    Whether the lead inclination is parallel to the reference side.

  • lead_inclination (float, default: 90.0 ) –

    The lead inclination angle in degrees. 0.1 < lead_inclination < 179.9.

  • rafter_nail_hole (bool, default: False ) –

    Whether a rafter nail hole is added.

Source code in src/compas_timber/fabrication/birds_mouth.py
def __init__(
    self,
    orientation: OrientationType = OrientationType.START,
    start_x: float = 0.0,
    start_y: float = 0.0,
    start_depth: float = 20.0,
    angle: float = 90.0,
    inclination_1: float = 45.0,
    inclination_2: float = 135.0,
    depth: float = 20.0,
    width: float = 0.0,
    width_counter_part_limited: bool = False,
    width_counter_part: float = 120.0,
    height_counter_part_limited: bool = False,
    height_counter_part: float = 120.0,
    face_limited_front: bool = False,
    face_limited_back: bool = False,
    lead_angle_parallel: bool = True,
    lead_angle: float = 90.0,
    lead_inclination_parallel: bool = True,
    lead_inclination: float = 90.0,
    rafter_nail_hole: bool = False,
    **kwargs
):
    super(BirdsMouth, self).__init__(**kwargs)
    self._orientation = None
    self._start_x = None
    self._start_y = None
    self._start_depth = None
    self._angle = None
    self._inclination_1 = None
    self._inclination_2 = None
    self._depth = None
    self._width = None
    self._width_counter_part_limited = None
    self._width_counter_part = None
    self._height_counter_part_limited = None
    self._height_counter_part = None
    self._face_limited_front = None
    self._face_limited_back = None
    self._lead_angle_parallel = None
    self._lead_angle = None
    self._lead_inclination_parallel = None
    self._lead_inclination = None
    self._rafter_nail_hole = None

    self.orientation = orientation
    self.start_x = start_x
    self.start_y = start_y
    self.start_depth = start_depth
    self.angle = angle
    self.inclination_1 = inclination_1
    self.inclination_2 = inclination_2
    self.depth = depth
    self.width = width
    self.width_counter_part_limited = width_counter_part_limited
    self.width_counter_part = width_counter_part
    self.height_counter_part_limited = height_counter_part_limited
    self.height_counter_part = height_counter_part
    self.face_limited_front = face_limited_front
    self.face_limited_back = face_limited_back
    self.lead_angle_parallel = lead_angle_parallel
    self.lead_angle = lead_angle
    self.lead_inclination_parallel = lead_inclination_parallel
    self.lead_inclination = lead_inclination
    self.rafter_nail_hole = rafter_nail_hole

Methods:¤

apply ¤
apply(geometry: Brep, element: TimberElement) -> Brep

Apply the feature to the timber element geometry.

Parameters:

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

    The timber element geometry to be notched in the element's local coordinate system.

  • element (:class:`compas_timber.base.TimberElement`) –

    The timber element that is notched by this instance.

Raises:

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

    If the cutting planes do not intersect with element geometry.

Returns:

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

    The resulting geometry after processing.

Source code in src/compas_timber/fabrication/birds_mouth.py
def apply(self, geometry: Brep, element: TimberElement) -> Brep:
    """Apply the feature to the timber element geometry.

    Parameters
    ----------
    geometry : :class:`~compas.geometry.Brep`
        The timber element geometry to be notched in the element's local coordinate system.
    element : :class:`compas_timber.base.TimberElement`
        The timber element that is notched by this instance.

    Raises
    ------
    :class:`~compas_timber.errors.FeatureApplicationError`
        If the cutting planes do not intersect with element geometry.

    Returns
    -------
    :class:`~compas.geometry.Brep`
        The resulting geometry after processing.

    """
    try:
        cutting_planes = self.planes_from_params_and_element(element)
    except Exception as e:
        raise FeatureApplicationError(
            None, geometry, "Failed to generate cutting planes from parameters and element: {}".format(str(e))
        )
    # convert to the local coordinates of the element
    cutting_planes = [plane.transformed(element.transformation_to_local()) for plane in cutting_planes]

    # birds mouth is always a concave (v-notch) cut
    trim_volume = element.compute_elementgeometry(False)
    try:
        for cutting_plane in cutting_planes:
            flipped_plane = Plane(cutting_plane.point, -cutting_plane.normal)
            trim_volume.trim(flipped_plane)
    except Exception as e:
        raise FeatureApplicationError(cutting_planes, trim_volume, "Failed to trim notch geometry with cutting planes: {}".format(str(e)))

    try:
        return brep_difference_first(geometry, trim_volume)
    except Exception as e:
        raise FeatureApplicationError(trim_volume, geometry, "Failed to compute final geometry difference for birds mouth notch: {}".format(str(e)))
from_planes_and_element classmethod ¤
from_planes_and_element(
    planes: list[Plane],
    element: TimberElement,
    start_y: float = 0.0,
    width: float = None,
    ref_side_index: int = None,
    **kwargs,
) -> "BirdsMouth"

Create a BirdsMouth instance from two cutting planes and the element they notch.

The intersection line of the two planes defines the ridge of the birds mouth notch. The individual planes define the two angled faces of the notch.

Parameters:

  • planes (list of :class:`~compas.geometry.Plane` or :class:`~compas.geometry.Frame`) –

    The two cutting planes that define the birds mouth notch.

  • element (:class:`~compas_timber.elements.Beam` or :class:`~compas_timber.elements.Plate`) –

    The beam or plate to be notched by this instance.

  • start_y (float, default: 0.0 ) –

    The start y-coordinate of the cut in parametric space of the reference side. Default is 0.0.

  • width (float, default: None ) –

    The width of the notch. Default is the full width of the element minus start_y.

  • ref_side_index (int, default: None ) –

    The reference side index of the element to be notched. Default is determined from the planes.

Returns:

  • class:`~compas_timber.fabrication.BirdsMouth` –
Source code in src/compas_timber/fabrication/birds_mouth.py
@classmethod
def from_planes_and_element(cls, planes:list[Plane], element:TimberElement, start_y:float=0.0, width:float=None, ref_side_index:int=None, **kwargs) -> "BirdsMouth":
    """Create a BirdsMouth instance from two cutting planes and the element they notch.

    The intersection line of the two planes defines the ridge of the birds mouth notch.
    The individual planes define the two angled faces of the notch.

    Parameters
    ----------
    planes : list of :class:`~compas.geometry.Plane` or :class:`~compas.geometry.Frame`
        The two cutting planes that define the birds mouth notch.
    element : :class:`~compas_timber.elements.Beam` or :class:`~compas_timber.elements.Plate`
        The beam or plate to be notched by this instance.
    start_y : float, optional
        The start y-coordinate of the cut in parametric space of the reference side. Default is 0.0.
    width : float, optional
        The width of the notch. Default is the full width of the element minus start_y.
    ref_side_index : int, optional
        The reference side index of the element to be notched. Default is determined from the planes.

    Returns
    -------
    :class:`~compas_timber.fabrication.BirdsMouth`

    """
    if len(planes) != 2:
        raise ValueError("Exactly two cutting planes are required to create a BirdsMouth instance.")

    # normalise to Plane
    planes = [Plane.from_frame(p) if isinstance(p, Frame) else p for p in planes]

    # determine ref_side_index
    if ref_side_index is None:
        ref_side_index = cls._get_default_ref_side_index(planes, element)
    ref_side = element.ref_sides[ref_side_index]
    element_width = element.get_dimensions_relative_to_side(ref_side_index)[0]

    if width is None:
        width = element_width - start_y

    # determine face limits based on start_y and width
    face_limited_front = TOL.is_positive(start_y)
    face_limited_back = element_width > start_y + width

    # compute notch apex and rear point by intersecting the planes with the front and back side planes of the element
    front_side_plane = Plane.from_frame(element.front_side(ref_side_index))
    back_side_plane = Plane.from_frame(element.back_side(ref_side_index))

    if face_limited_front:
        front_side_plane.translate(ref_side.yaxis * start_y)
    if face_limited_back:
        back_side_plane.translate(ref_side.yaxis * (element_width - start_y - width))

    apex_coords = intersection_plane_plane_plane(planes[0], planes[1], front_side_plane)
    rear_coords = intersection_plane_plane_plane(planes[0], planes[1], back_side_plane)
    if apex_coords is None or rear_coords is None:
        raise ValueError("Cutting planes do not intersect with the element side planes; cannot compute birds mouth ridge line.")
    apex = Point(*apex_coords)
    rear = Point(*rear_coords)
    ridge_line = Line(apex, rear)

    # orientation — which end of the element the notch faces
    orientation = cls._calculate_orientation(element, planes)

    # start_x and start_depth from apex projected onto ref_side local coords
    start_x, start_depth = cls._calculate_start_x_and_depth(ref_side, apex)

    # depth — distance from ref_side plane to rear point along -ref_side.zaxis
    depth = cls._calculate_depth(ref_side, rear)

    # angle — horizontal angle of the ridge line relative to -ref_side.xaxis (START) or ref_side.xaxis (END)
    angle = cls._calculate_angle(ref_side, ridge_line, orientation)

    # inclination1 and inclination2 — each plane's tilt relative to ref_side normal
    inclination_1, inclination_2 = cls._calculate_inclinations(ref_side, planes, orientation, ridge_line)


    return cls(
        orientation=orientation,
        start_x=start_x,
        start_y=start_y,
        start_depth=start_depth,
        angle=angle,
        inclination_1=inclination_1,
        inclination_2=inclination_2,
        depth=depth,
        width=width,
        face_limited_front=face_limited_front,
        face_limited_back=face_limited_back,
        ref_side_index=ref_side_index,
        **kwargs
    )
from_shapes_and_element classmethod ¤
from_shapes_and_element(
    plane_a: Plane | Frame, plane_b: Plane | Frame, element: TimberElement, **kwargs
) -> "BirdsMouth"

Construct a BirdsMouth process from two planes and a timber element.

Parameters:

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

    The first cutting plane.

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

    The second cutting plane.

  • element (:class:`~compas_timber.elements.Beam` or :class:`~compas_timber.elements.Plate`) –

    The beam or plate to be notched.

Returns:

  • class:`~compas_timber.fabrication.BirdsMouth` –
Source code in src/compas_timber/fabrication/birds_mouth.py
@classmethod
def from_shapes_and_element(cls, plane_a: Plane | Frame, plane_b: Plane | Frame, element: TimberElement, **kwargs) -> "BirdsMouth":
    """Construct a BirdsMouth process from two planes and a timber element.

    Parameters
    ----------
    plane_a : :class:`~compas.geometry.Plane` or :class:`~compas.geometry.Frame`
        The first cutting plane.
    plane_b : :class:`~compas.geometry.Plane` or :class:`~compas.geometry.Frame`
        The second cutting plane.
    element : :class:`~compas_timber.elements.Beam` or :class:`~compas_timber.elements.Plate`
        The beam or plate to be notched.

    Returns
    -------
    :class:`~compas_timber.fabrication.BirdsMouth`

    """
    return cls.from_planes_and_element([plane_a, plane_b], element, **kwargs)
planes_from_params_and_element ¤
planes_from_params_and_element(element: TimberElement) -> list[Plane]

Calculates the two cutting planes from the machining parameters in this instance and the given timber element.

Parameters:

  • element (:class:`compas_timber.base.TimberElement`) –

    The timber element that is notched by this instance.

Returns:

  • list of :class:`~compas.geometry.Plane` –

    The two cutting planes for this instance.

Source code in src/compas_timber/fabrication/birds_mouth.py
def planes_from_params_and_element(self, element:TimberElement) -> list[Plane]:
    """Calculates the two cutting planes from the machining parameters in this instance and the given timber element.

    Parameters
    ----------
    element : :class:`compas_timber.base.TimberElement`
        The timber element that is notched by this instance.

    Returns
    -------
    list of :class:`~compas.geometry.Plane`
        The two cutting planes for this instance.

    """
    assert self.inclination_1 is not None
    assert self.inclination_2 is not None

    ref_frame = element.ref_sides[self.ref_side_index]
    ridge_line = self._get_ridge_line_from_params_and_element(element)
    rotation_axis = ridge_line.direction if self.orientation == OrientationType.END else -ridge_line.direction

    plane_1 = Plane(ridge_line.end, -ref_frame.zaxis)
    plane_1.rotate(math.radians(self.inclination_2), rotation_axis, point=ridge_line.end)

    plane_2 = Plane(ridge_line.start, ref_frame.zaxis)
    plane_2.rotate(math.radians(self.inclination_1), rotation_axis, point=ridge_line.start)

    return [plane_1, plane_2]
scale ¤
scale(factor: float) -> None

Scale the parameters of the processing by the given factor.

Notes

Only distances are scaled, angles remain unchanged.

Parameters:

  • factor (float) –

    The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

Source code in src/compas_timber/fabrication/birds_mouth.py
def scale(self, factor: float) -> None:
    """Scale the parameters of the processing by the given factor.

    Notes
    -----
    Only distances are scaled, angles remain unchanged.

    Parameters
    ----------
    factor : float
        The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

    """
    # type: (float) -> None
    assert self.start_x is not None
    assert self.start_y is not None
    assert self.start_depth is not None
    assert self.depth is not None
    assert self.width is not None
    self._start_x *= factor
    self._start_y *= factor
    self._start_depth *= factor
    self._depth *= factor
    self._width *= factor

Contour ¤

Contour(polyline, depth, depth_bounded=True, inclination=None)

Represens the generic contour for specific free contour processings.

Todo

add point attributes for other types like NailContour

Parameters:

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

    The polyline of the contour.

  • depth (float) –

    The depth of the contour.

  • depth_bounded (bool, default: True ) –

    If True, the depth is bounded.

  • inclination (list[float], default: None ) –

    The inclination of the contour.

Source code in src/compas_timber/fabrication/btlx.py
def __init__(self, polyline, depth, depth_bounded=True, inclination=None):
    super(Contour, self).__init__()
    self.polyline = polyline
    self.depth = depth
    self.depth_bounded = depth_bounded
    self.inclination = inclination or [0]

Methods:¤

scale ¤
scale(factor)

Scale the contour by a given factor.

Parameters:

  • factor (float) –

    The scaling factor.

Source code in src/compas_timber/fabrication/btlx.py
def scale(self, factor):
    """Scale the contour by a given factor.

    Parameters
    ----------
    factor : float
        The scaling factor.

    """
    self.polyline.scale(factor)
    if self.depth is not None:
        self.depth *= factor
scaled ¤
scaled(factor)

Returns a new instance of the contour with the parameters scaled by a given factor.

Parameters:

  • factor (float) –

    The scaling factor.

Returns:

  • class:`~compas_timber.fabrication.Contour` –

    A new instance of the contour with the parameters scaled by the given factor.

Source code in src/compas_timber/fabrication/btlx.py
def scaled(self, factor):
    """Returns a new instance of the contour with the parameters scaled by a given factor.

    Parameters
    ----------
    factor : float
        The scaling factor.

    Returns
    -------
    :class:`~compas_timber.fabrication.Contour`
        A new instance of the contour with the parameters scaled by the given factor.

    """
    # type: (float) -> Contour
    new_instance = self.copy()
    new_instance.scale(factor)
    return new_instance
to_brep ¤
to_brep()

Convert the contour to a COMPAS Brep object.

Returns:

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

    The brep representation of the contour.

Source code in src/compas_timber/fabrication/btlx.py
def to_brep(self):
    """Convert the contour to a COMPAS Brep object.
    Returns
    -------
    :class:`~compas.geometry.Brep`
        The brep representation of the contour.
    """
    pline_a = self.polyline.copy()
    pline_b = pline_a.translated([0, 0, -self.depth])
    if any([i != 0 for i in self.inclination]):
        if len(self.inclination) == 1:
            inclinations = [self.inclination[0] for _ in range(len(pline_a) - 1)]
        else:
            inclinations = [i for i in self.inclination]
        for i, (seg, inclination) in enumerate(zip(pline_a.lines, inclinations)):
            plane = Plane.from_points([seg.start, seg.end, seg.start + [0, 0, -1]])
            plane.rotate(math.radians(inclination), seg.direction, seg.start)
            move_polyline_segment_to_plane(pline_b, i, plane)
    pline_a.translate([0, 0, 0.001])
    pline_b.translate([0, 0, -0.001])

    return brep_from_outlines(pline_a, pline_b)

DoubleCut ¤

DoubleCut(
    orientation=START,
    start_x=0.0,
    start_y=50.0,
    angle_1=45.0,
    inclination_1=90.0,
    angle_2=90.0,
    inclination_2=90.0,
    **kwargs,
)

Represents a Double Cut feature to be made on a beam.

Parameters:

  • orientation (int, default: START ) –

    The orientation of the cut. Must be either OrientationType.START or OrientationType.END.

  • start_x (float, default: 0.0 ) –

    The start x-coordinate of the cut in parametric space of the reference side. -100000.0 < start_x < 100000.0.

  • start_y (float, default: 50.0 ) –

    The start y-coordinate of the cut in parametric space of the reference side. 0.0 < start_y < 50000.0.

  • angle_1 (float, default: 45.0 ) –

    The horizontal angle of the first cut. 0.1 < angle_1 < 179.9.

  • inclination_1 (float, default: 90.0 ) –

    The vertical angle of the first cut. 0.1 < inclination_1 < 179.9.

  • angle_2 (float, default: 90.0 ) –

    The horizontal angle of the second cut. 0.1 < angle_2 < 179.9.

  • inclination_2 (float, default: 90.0 ) –

    The vertical angle of the second cut. 0.1 < inclination_2 < 179.9.

Source code in src/compas_timber/fabrication/double_cut.py
def __init__(
    self,
    orientation = OrientationType.START,
    start_x=0.0,
    start_y=50.0,
    angle_1=45.0,
    inclination_1=90.0,
    angle_2=90.0,
    inclination_2=90.0,
    **kwargs
):
    super(DoubleCut, self).__init__(**kwargs)
    self._orientation = None
    self._start_x = None
    self._start_y = None
    self._angle_1 = None
    self._inclination_1 = None
    self._angle_2 = None
    self._inclination_2 = None

    self.orientation = orientation
    self.start_x = start_x
    self.start_y = start_y
    self.angle_1 = angle_1
    self.inclination_1 = inclination_1
    self.angle_2 = angle_2
    self.inclination_2 = inclination_2

Methods:¤

apply ¤
apply(geometry, beam)

Apply the feature to the beam geometry.

Parameters:

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

    The beam geometry to be cut.

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

    The beam that is cut by this instance.

Raises:

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

    If the cutting plane does not intersect with beam geometry.

Returns:

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

    The resulting geometry after processing

Source code in src/compas_timber/fabrication/double_cut.py
def apply(self, geometry, beam):
    """Apply the feature to the beam geometry.

    Parameters
    ----------
    geometry : :class:`~compas.geometry.Brep`
        The beam geometry to be cut.
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Raises
    ------
    :class:`~compas_timber.errors.FeatureApplicationError`
        If the cutting plane does not intersect with beam geometry.

    Returns
    -------
    :class:`~compas.geometry.Brep`
        The resulting geometry after processing

    """
    # type: (Brep, Beam) -> Brep
    # get cutting planes from params and beam
    try:
        cutting_planes = self.planes_from_params_and_beam(beam)
    except ValueError as e:
        raise FeatureApplicationError(
            None, geometry.transformed(beam.modeltransformation), "Failed to generate cutting planes from parameters and beam: {}".format(str(e))
        )
    # convert to the local coordinates of the beam
    cutting_planes = [plane.transformed(beam.transformation_to_local()) for plane in cutting_planes]

    if self.is_concave:
        trim_volume = geometry.copy()
        for cutting_plane in cutting_planes:
            trim_volume.trim(cutting_plane)
        return brep_difference_first(geometry, trim_volume)
    else:
        for cutting_plane in cutting_planes:
            plane = Plane(cutting_plane.point, -cutting_plane.normal)
            geometry.trim(plane)
        return geometry
from_planes_and_beam classmethod ¤
from_planes_and_beam(planes, beam, ref_side_index=None, **kwargs)

Create a DoubleCut instance from two cutting planes and the beam they should cut.

Parameters:

  • planes (list of :class:`~compas.geometry.Plane` or :class:`~compas.geometry.Frame`) –

    The two cutting planes that define the double cut.

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

    The beam that is cut by this instance.

  • ref_side_index (int, default: None ) –

    The reference side index of the beam to be cut. Default is 0 (i.e. RS1).

Returns:

  • class:`~compas_timber.fabrication.DoubleCut` –
Source code in src/compas_timber/fabrication/double_cut.py
@classmethod
def from_planes_and_beam(cls, planes, beam, ref_side_index=None, **kwargs):
    """Create a DoubleCut instance from two cutting planes and the beam they should cut.

    Parameters
    ----------
    planes : list of :class:`~compas.geometry.Plane` or :class:`~compas.geometry.Frame`
        The two cutting planes that define the double cut.
    beam : :class:`~compas_timber.elements.Beam`
        The beam that is cut by this instance.
    ref_side_index : int, optional
        The reference side index of the beam to be cut. Default is 0 (i.e. RS1).

    Returns
    -------
    :class:`~compas_timber.fabrication.DoubleCut`

    """
    # type: (list(Plane|Frame), Beam, int) -> DoubleCut
    if len(planes) != 2:
        raise ValueError("Exactly two cutting planes are required to create a DoubleCut instance.")

    # convert all frames to planes
    planes = [Plane.from_frame(plane) if isinstance(plane, Frame) else plane for plane in planes]

    # get the intersection line of cutting planes, which is used to determine whether the planes cut a concave or convex shape
    ln = intersection_plane_plane(planes[0], planes[1])
    if not ln:
        raise ValueError("The two cutting planes are parallel consider using a JackRafterCut")

    line = Line(Point(*ln[0]), Point(*ln[1]))
    intersection_points, face_indices = intersection_line_beam_param(line, beam)
    if not intersection_points:
        raise ValueError("Planes do not intersect with beam.")
    if not ref_side_index:
        ref_side_index = face_indices[0]
        ref_side = beam.ref_sides[ref_side_index]
        point_start_xy = intersection_points[0]

    else:
        if ref_side_index not in face_indices:
            raise ValueError("Planes do not intersect with selected ref_side {}.".format(ref_side_index))
        else:
            ref_side = beam.ref_sides[ref_side_index]
            index = face_indices.index(ref_side_index)
            point_start_xy = intersection_points[index]

    planes = cls._reorder_planes(planes, line, ref_side)
    orientation = cls._calculate_orientation(beam, planes)
    start_x, start_y = cls._calculate_start_x_y(ref_side, point_start_xy)
    angle_1, angle_2 = cls._calculate_angle(ref_side, planes, orientation)
    inclination_1, inclination_2 = cls._calculate_inclination(ref_side, planes)

    # TODO: evaluate if the planes should be cached for use in geometry creation.
    return cls(orientation, start_x, start_y, angle_1, inclination_1, angle_2, inclination_2, ref_side_index=ref_side_index, **kwargs)
from_shapes_and_element classmethod ¤
from_shapes_and_element(plane_a, plane_b, element, **kwargs)

Construct a DoubleCut process from a two planes and an element.

Parameters:

  • plane_a (:class:`compas.geometry.Plane`) –

    The first cutting plane.

  • plane_b (:class:`compas.geometry.Plane`) –

    The second cutting plane.

  • element (:class:`compas_timber.elements.Element`) –

    The element to be cut.

Returns:

  • class:`compas_timber.fabrication.DoubleCut` –

    The constructed double cut process.

Source code in src/compas_timber/fabrication/double_cut.py
@classmethod
def from_shapes_and_element(cls, plane_a, plane_b, element, **kwargs):
    """Construct a DoubleCut process from a two planes and an element.

    Parameters
    ----------
    plane_a : :class:`compas.geometry.Plane`
        The first cutting plane.
    plane_b : :class:`compas.geometry.Plane`
        The second cutting plane.
    element : :class:`compas_timber.elements.Element`
        The element to be cut.

    Returns
    -------
    :class:`compas_timber.fabrication.DoubleCut`
        The constructed double cut process.

    """
    return cls.from_planes_and_beam([plane_a, plane_b], element, **kwargs)
planes_from_params_and_beam ¤
planes_from_params_and_beam(beam)

Calculates the cutting planeS from the machining parameters in this instance and the given beam

Parameters:

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

    The beam that is cut by this instance.

Returns:

  • list of :class:`~compas.geometry.Plane` –

    The cutting planes for this instance

Source code in src/compas_timber/fabrication/double_cut.py
def planes_from_params_and_beam(self, beam):
    """Calculates the cutting planeS from the machining parameters in this instance and the given beam

    Parameters
    ----------
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Returns
    -------
    list of :class:`~compas.geometry.Plane`
        The cutting planes for this instance

    """
    # type: (Beam) -> list(Plane)
    assert self.angle_1 is not None
    assert self.inclination_1 is not None
    assert self.angle_2 is not None
    assert self.inclination_2 is not None

    # start with a plane aligned with the ref side but shifted to the start_x of the cut
    ref_side = beam.side_as_surface(self.ref_side_index)
    p_origin = planar_surface_point_at(ref_side, self.start_x, self.start_y)
    ref_frame = Frame(p_origin, ref_side.frame.xaxis, ref_side.frame.yaxis)

    if self.orientation == OrientationType.END:
        ref_frame.xaxis = -ref_frame.xaxis
        inclination_1 = 180-self.inclination_1
        inclination_2 = 180-self.inclination_2
    else:
        inclination_1 = self.inclination_1
        inclination_2 = self.inclination_2

    # normal pointing towards xaxis so just need the delta
    cutting_frame_1 = ref_frame.copy()
    rot_1_horiz = Rotation.from_axis_and_angle(ref_frame.zaxis, math.radians(self.angle_1), point=p_origin)
    rot_1_vert = Rotation.from_axis_and_angle(
        ref_frame.xaxis, math.radians(inclination_1), point=p_origin
    )
    cutting_frame_1.transform(rot_1_horiz * rot_1_vert)

    cutting_frame_2 = ref_frame.copy()
    rot_2_horiz = Rotation.from_axis_and_angle(ref_frame.zaxis, math.radians(self.angle_2), point=p_origin)
    rot_2_vert = Rotation.from_axis_and_angle(
        ref_frame.xaxis, math.radians(inclination_2), point=p_origin
    )
    cutting_frame_2.transform(rot_2_horiz * rot_2_vert)
    return [Plane.from_frame(cutting_frame) for cutting_frame in [cutting_frame_1, cutting_frame_2]]
scale ¤
scale(factor)

Scale the parameters of the processing by the given factor.

Notes

Only distances are scaled, angles remain unchanged.

Parameters:

  • factor (float) –

    The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

Source code in src/compas_timber/fabrication/double_cut.py
def scale(self, factor):
    """Scale the parameters of the processing by the given factor.

    Notes
    -----
    Only distances are scaled, angles remain unchanged.

    Parameters
    ----------
    factor : float
        The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

    """
    # type: (float) -> None
    assert self.start_x is not None
    assert self.start_y is not None
    self._start_x *= factor
    self._start_y *= factor

DoubleCutProxy ¤

DoubleCutProxy(planes, element, ref_side_index=None)

This object behaves like a DoubleCut except it only calculates the machining parameters once unproxified. Can also be used to defer the creation of the processing instance until it is actually needed.

Until then, it can be used to visualize the machining operation. This slightly improves performance.

Parameters:

  • planes (list of :class:`~compas.geometry.Plane` or :class:`~compas.geometry.Frame`) –

    The two cutting planes that define the double cut.

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

    The element that is cut by this instance.

  • ref_side_index (int, default: None ) –

    The reference side index of the beam to be cut. Default is None.

Source code in src/compas_timber/fabrication/double_cut.py
def __init__(self, planes, element, ref_side_index=None):
    self.planes = [plane.transformed(element.transformation_to_local()) for plane in planes]
    self.element = element
    self.ref_side_index = ref_side_index
    self._processing = None

Methods:¤

apply ¤
apply(geometry, beam)

Apply the feature to the beam geometry.

Parameters:

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

    The beam geometry to be cut.

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

    The beam that is cut by this instance.

Raises:

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

    If the cutting planes do not intersect with beam geometry.

Returns:

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

    The resulting geometry after processing.

Source code in src/compas_timber/fabrication/double_cut.py
def apply(self, geometry, beam):
    """Apply the feature to the beam geometry.

    Parameters
    ----------
    geometry : :class:`~compas.geometry.Brep`
        The beam geometry to be cut.
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Raises
    ------
    :class:`~compas_timber.errors.FeatureApplicationError`
        If the cutting planes do not intersect with beam geometry.

    Returns
    -------
    :class:`~compas.geometry.Brep`
        The resulting geometry after processing.

    """
    # type: (Brep, Beam) -> Brep
    # transform planes to beam's local coordinate system

    # determine if concave or convex based on the stored planes
    # get the intersection line of cutting planes
    if not intersection_plane_plane(self.planes[0], self.planes[1]):
        raise FeatureApplicationError(None, geometry.transformed(beam.modeltransformation), "The two cutting planes are parallel.")

    # check angle between planes to determine concavity
    angle_between = angle_vectors(self.planes[0].normal, self.planes[1].normal, deg=True)
    is_concave = angle_between < 90.0  # simplified concavity check

    if is_concave:
        trim_volume = geometry.copy()
        for cutting_plane in self.planes:
            trim_volume.trim(self.planes)
        return brep_difference_first(geometry, trim_volume)
    else:
        for cutting_plane in self.planes:
            plane = Plane(cutting_plane.point, -cutting_plane.normal)
            geometry.trim(plane)
        return geometry
from_planes_and_beam classmethod ¤
from_planes_and_beam(planes, beam, ref_side_index=None)

Create a DoubleCutProxy instance from two cutting planes and the beam they should cut.

Parameters:

  • planes (list of :class:`~compas.geometry.Plane` or :class:`~compas.geometry.Frame`) –

    The two cutting planes that define the double cut.

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

    The beam that is cut by this instance.

  • ref_side_index (int, default: None ) –

    The reference side index of the beam to be cut. Default is None.

Returns:

  • class:`~compas_timber.fabrication.DoubleCutProxy` –
Source code in src/compas_timber/fabrication/double_cut.py
@classmethod
def from_planes_and_beam(cls, planes, beam, ref_side_index=None):
    """Create a DoubleCutProxy instance from two cutting planes and the beam they should cut.

    Parameters
    ----------
    planes : list of :class:`~compas.geometry.Plane` or :class:`~compas.geometry.Frame`
        The two cutting planes that define the double cut.
    beam : :class:`~compas_timber.elements.Beam`
        The beam that is cut by this instance.
    ref_side_index : int, optional
        The reference side index of the beam to be cut. Default is None.

    Returns
    -------
    :class:`~compas_timber.fabrication.DoubleCutProxy`

    """
    if len(planes) != 2:
        raise ValueError("Exactly two cutting planes are required to create a DoubleCutProxy instance.")
    # convert all frames to planes
    planes = [Plane.from_frame(plane) if isinstance(plane, Frame) else plane for plane in planes]
    return cls(planes, beam, ref_side_index)
from_shapes_and_element classmethod ¤
from_shapes_and_element(plane_a, plane_b, element, **kwargs)

Construct a DoubleCutProxy from two planes and an element.

Parameters:

  • plane_a (:class:`compas.geometry.Plane`) –

    The first cutting plane.

  • plane_b (:class:`compas.geometry.Plane`) –

    The second cutting plane.

  • element (:class:`compas_timber.elements.Element`) –

    The element to be cut.

Returns:

  • class:`~compas_timber.fabrication.DoubleCutProxy` –

    The constructed double cut proxy.

Source code in src/compas_timber/fabrication/double_cut.py
@classmethod
def from_shapes_and_element(cls, plane_a, plane_b, element, **kwargs):
    """Construct a DoubleCutProxy from two planes and an element.

    Parameters
    ----------
    plane_a : :class:`compas.geometry.Plane`
        The first cutting plane.
    plane_b : :class:`compas.geometry.Plane`
        The second cutting plane.
    element : :class:`compas_timber.elements.Element`
        The element to be cut.

    Returns
    -------
    :class:`~compas_timber.fabrication.DoubleCutProxy`
        The constructed double cut proxy.

    """
    return cls.from_planes_and_beam([plane_a, plane_b], element, **kwargs)
unproxified ¤
unproxified()

Returns the unproxified processing instance.

Returns:

  • class:`~compas_timber.fabrication.DoubleCut` –
Source code in src/compas_timber/fabrication/double_cut.py
def unproxified(self):
    """Returns the unproxified processing instance.

    Returns
    -------
    :class:`~compas_timber.fabrication.DoubleCut`

    """
    if not self._processing:
        planes = [plane.transformed(self.element.modeltransformation) for plane in self.planes]
        self._processing = DoubleCut.from_planes_and_beam(planes, self.element, self.ref_side_index)
    return self._processing

DovetailMortise ¤

DovetailMortise(
    start_x=0.0,
    start_y=50.0,
    start_depth=0.0,
    angle=0.0,
    slope=90.0,
    inclination=90.0,
    limitation_top=LIMITED,
    length_limited_bottom=True,
    length=80.0,
    width=40.0,
    depth=28.0,
    cone_angle=15.0,
    use_flank_angle=False,
    flank_angle=15.0,
    shape=AUTOMATIC,
    shape_radius=20.0,
    **kwargs,
)

Represents a Dovetail Mortise feature to be made on a beam.

Parameters:

  • start_x (float, default: 0.0 ) –

    The start x-coordinate of the cut in parametric space of the reference side. Distance from the beam start to the reference point. -100000.0 < start_x < 100000.0.

  • start_y (float, default: 50.0 ) –

    The start y-coordinate of the cut in parametric space of the reference side. Distance from the reference edge to the reference point. -5000.0 < start_y < 5000.0.

  • start_depth (float, default: 0.0 ) –

    The start depth of the cut in parametric space of the reference side. Margin on the reference side. 0.0 < start_depth < 5000.0.

  • angle (float, default: 0.0 ) –

    The angle of the cut. Angle between edge and reference edge. -180.0 < angle < 180.0.

  • slope (float, default: 90.0 ) –

    The slope of the cut. Angle between axis along the length of the mortise and rederence side. 0.1 < slope < 179.9.

  • inclination (float, default: 90.0 ) –

    The inclination of the cut. Angle between axis along the width of the mortise and rederence side. 0.1 < inclination < 179.9.

  • limitation_top (str, default: LIMITED ) –

    The limitation type of the top length of the cut. Should be either 'limited', 'unlimited', or 'pocket'.

  • length_limited_bottom (bool, default: True ) –

    Whether the bottom length of the cut is limited. True or False.

  • length (float, default: 80.0 ) –

    The length of the cut. 0.0 < length < 5000.0.

  • width (float, default: 40.0 ) –

    The width of the cut. 0.0 < width < 1000.0.

  • depth (float, default: 28.0 ) –

    The depth of the mortise. 0.0 < depth < 1000.0.

  • cone_angle (float, default: 15.0 ) –

    The cone angle of the cut. 0.0 < cone_angle < 30.0.

  • use_flank_angle (bool, default: False ) –

    Whether the flank angle is used. True or False.

  • flank_angle (float, default: 15.0 ) –

    The flank angle of the cut. Angle of the tool. 5.0 < flank_angle < 35.0.

  • shape (str, default: AUTOMATIC ) –

    The shape of the cut. Must be either 'automatic', 'square', 'round', 'rounded', or 'radius'.

  • shape_radius (float, default: 20.0 ) –

    The radius of the shape of the cut. 0.0 < shape_radius < 1000.0.

Source code in src/compas_timber/fabrication/dovetail_mortise.py
def __init__(
    self,
    start_x=0.0,
    start_y=50.0,
    start_depth=0.0,
    angle=0.0,
    slope=90.0,
    inclination=90.0,
    limitation_top=LimitationTopType.LIMITED,
    length_limited_bottom=True,
    length=80.0,
    width=40.0,
    depth=28.0,
    cone_angle=15.0,
    use_flank_angle=False,
    flank_angle=15.0,
    shape=TenonShapeType.AUTOMATIC,
    shape_radius=20.0,
    **kwargs
):
    super(DovetailMortise, self).__init__(**kwargs)
    self._start_x = None
    self._start_y = None
    self._start_depth = None
    self._angle = None
    self._slope = None
    self._inclination = None
    self._limitation_top = None
    self._length_limited_bottom = None
    self._length = None
    self._width = None
    self._depth = None
    self._cone_angle = None
    self._use_flank_angle = None
    self._flank_angle = None
    self._shape = None
    self._shape_radius = None

    self.start_x = start_x
    self.start_y = start_y
    self.start_depth = start_depth
    self.angle = angle
    self.slope = slope
    self.inclination = inclination
    self.limitation_top = limitation_top
    self.length_limited_bottom = length_limited_bottom
    self.length = length
    self.width = width
    self.depth = depth
    self.cone_angle = cone_angle
    self.use_flank_angle = use_flank_angle
    self.flank_angle = flank_angle
    self.shape = shape
    self.shape_radius = shape_radius

Methods:¤

apply ¤
apply(geometry, beam)

Apply the feature to the beam geometry.

Parameters:

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

    The geometry to be processed.

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

    The beam that is milled by this instance.

Raises:

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

    If the cutting planes do not create a volume that itersects with beam geometry or any step fails.

Returns:

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

    The resulting geometry after processing

Source code in src/compas_timber/fabrication/dovetail_mortise.py
def apply(self, geometry, beam):
    """Apply the feature to the beam geometry.

    Parameters
    ----------
    geometry : :class:`compas.geometry.Brep`
        The geometry to be processed.

    beam : :class:`compas_timber.elements.Beam`
        The beam that is milled by this instance.

    Raises
    ------
    :class:`~compas_timber.errors.FeatureApplicationError`
        If the cutting planes do not create a volume that itersects with beam geometry or any step fails.

    Returns
    -------
    :class:`~compas.geometry.Brep`
        The resulting geometry after processing

    """
    # type: (Brep, Beam) -> Brep

    # get dovetail volume from params and beam
    try:
        dovetail_volume = self.dovetail_volume_from_params_and_beam(beam)
    except ValueError as e:
        raise FeatureApplicationError(
            None, geometry.transformed(beam.modeltransformation), "Failed to generate dovetail mortise volume from parameters and beam: {}".format(str(e))
        )

    # fillet the edges of the dovetail volume based on the shape
    if (
        self.shape != TenonShapeType.SQUARE and not self.length_limited_bottom
    ):  # TODO: Change negation to affirmation once Brep.fillet is implemented
        edge_indices = [4, 7] if self.length_limited_bottom else [5, 8]
        try:
            dovetail_volume.fillet(
                self.shape_radius, [dovetail_volume.edges[edge_indices[0]], dovetail_volume.edges[edge_indices[1]]]
            )  # TODO: NotImplementedError
        except Exception as e:
            # dovetail_volume is still in model space here (not yet localized below), geometry is local
            raise FeatureApplicationError(
                dovetail_volume,
                geometry.transformed(beam.modeltransformation),
                "Failed to fillet the edges of the dovetail volume based on the shape: {}".format(str(e)),
            )

    # transform dovetail volume to local coordinates of the beam
    dovetail_volume.transform(beam.transformation_to_local())

    # remove tenon volume from geometry
    try:
        geometry = brep_difference_first(geometry, dovetail_volume)
    except Exception as e:
        raise FeatureApplicationError(
            dovetail_volume.transformed(beam.modeltransformation),
            geometry.transformed(beam.modeltransformation),
            "Failed to subtract tenon volume from geometry: {}".format(str(e))
        )

    return geometry
define_dovetail_tool classmethod ¤
define_dovetail_tool(tool_angle, tool_diameter, tool_height)

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

Parameters:

  • tool_angle (float) –

    The angle of the dovetail cutter tool.

  • tool_diameter (float) –

    The diameter of the dovetail cutter tool.

  • tool_height (float) –

    The height of the dovetail cutter tool.

Source code in src/compas_timber/fabrication/dovetail_mortise.py
@classmethod
def define_dovetail_tool(cls, tool_angle, tool_diameter, tool_height):
    """Define the parameters for the dovetail feature based on a defined dovetail cutting tool.

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

    """
    # type: (float, float, float) -> None
    cls._DOVETAIL_TOOL_PARAMS = {
        "tool_angle": tool_angle,
        "tool_diameter": tool_diameter,
        "tool_height": tool_height,
    }
dovetail_cutting_frames_from_params_and_beam ¤
dovetail_cutting_frames_from_params_and_beam(beam)

Calculates the cutting frames for the dovetail mortise from the machining parameters in this instance and the given beam.

Parameters:

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

    The beam that is cut by this instance.

Returns:

  • list of :class:`compas.geometry.Frame` –

    The cutting frames for the dovetail mortise.

Source code in src/compas_timber/fabrication/dovetail_mortise.py
def dovetail_cutting_frames_from_params_and_beam(self, beam):
    """Calculates the cutting frames for the dovetail mortise from the machining parameters in this instance and the given beam.

    Parameters
    ----------
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Returns
    -------
    list of :class:`compas.geometry.Frame`
        The cutting frames for the dovetail mortise.
    """
    assert self.angle is not None
    assert self.depth is not None
    assert self.start_depth is not None

    # start with a plane aligned with the ref side but shifted to the start_x of the cut
    cutting_frame = self.frame_from_params_and_beam(beam)

    offseted_cutting_frame = Frame(cutting_frame.point, -cutting_frame.xaxis, cutting_frame.yaxis)
    offseted_cutting_frame.point += cutting_frame.normal * self.depth

    cutting_surface = PlanarSurface(
        xsize=beam.height,
        ysize=beam.width,
        frame=cutting_frame,
    )

    # offset the cutting surface in case of housing
    cutting_surface.translate(-cutting_frame.zaxis * self.start_depth)

    # calculate the displacement of the edge points of the dovetail from the top-center
    dx_top = self.width / 2 + self.length * abs(math.tan(math.radians(self.cone_angle)))
    dx_bottom = self.width / 2
    dy = self.length

    dovetail_profile_points = [
        planar_surface_point_at(cutting_surface, -dx_top, 0),
        planar_surface_point_at(cutting_surface, dx_top, 0),
        planar_surface_point_at(cutting_surface, dx_bottom, -dy),
        planar_surface_point_at(cutting_surface, -dx_bottom, -dy),
    ]

    dovetail_edges = [
        Line(dovetail_profile_points[0], dovetail_profile_points[1]),  # Top line
        Line(dovetail_profile_points[1], dovetail_profile_points[2]),  # Right line
        Line(dovetail_profile_points[2], dovetail_profile_points[3]),  # Bottom line
        Line(dovetail_profile_points[3], dovetail_profile_points[0]),  # Left line
    ]

    trimming_frames = []
    for i, edge in enumerate(dovetail_edges):
        # create the initial frame using the line's direction and the cutting frame's normal
        frame = Frame(edge.midpoint, edge.direction, cutting_frame.normal)

        if i != 0:
            # determine the rotation direction: right and bottom are positive, top and left are negative
            # apply the rotation based on the flank angle
            rotation = Rotation.from_axis_and_angle(edge.direction, math.radians(self.flank_angle), frame.point)
            frame.transform(rotation)

        trimming_frames.append(frame)

    # translate the top trimming frame to the top of the beam if the top is unlimited
    if self.limitation_top == LimitationTopType.UNLIMITED:
        trimming_frames[0].translate(cutting_frame.yaxis * (beam.height - TOL.relative))

    cutting_frame.xaxis = -cutting_frame.xaxis
    trimming_frames.append(cutting_frame)
    return trimming_frames
dovetail_volume_from_params_and_beam ¤
dovetail_volume_from_params_and_beam(beam)

Calculates the dovetail mortise volume from the machining parameters in this instance and the given beam.

Parameters:

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

    The beam that is cut by this instance.

Returns:

  • class:`compas.geometry.Brep` –

    The mortise volume.

Source code in src/compas_timber/fabrication/dovetail_mortise.py
def dovetail_volume_from_params_and_beam(self, beam):
    """Calculates the dovetail mortise volume from the machining parameters in this instance and the given beam.

    Parameters
    ----------
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Returns
    -------
    :class:`compas.geometry.Brep`
        The mortise volume.

    """
    # type: (Beam) -> Brep

    assert self.inclination is not None
    assert self.slope is not None
    assert self.depth is not None

    cutting_frame = self.frame_from_params_and_beam(beam)

    # create the dovetail volume by trimming a box  # TODO: PluginNotInstalledError for Brep.from_loft
    # get the box as a brep
    dovetail_volume = Brep.from_box(
        Box(
            (beam.width + (beam.width * math.sin(math.radians(self.inclination)))) * 2,
            (beam.height / math.sin(math.radians(self.slope))) * 2,
            self.depth * 2,
            cutting_frame,
        )
    )

    # get the cutting frames for the dovetail tenon
    trimming_frames = self.dovetail_cutting_frames_from_params_and_beam(beam)

    # trim the box to create the dovetail volume
    for frame in trimming_frames:
        try:
            frame.xaxis = -frame.xaxis
            dovetail_volume.trim(frame)
        except Exception as e:
            raise FeatureApplicationError(
                frame, dovetail_volume, "Failed to trim tenon volume with cutting plane: {}".format(str(e))
            )

    return dovetail_volume
frame_from_params_and_beam ¤
frame_from_params_and_beam(beam)

Calculates the cutting frame from the machining parameters in this instance and the given beam

Parameters:

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

    The beam that is cut by this instance.

Returns:

  • class:`compas.geometry.Frame` –

    The cutting frame.

Source code in src/compas_timber/fabrication/dovetail_mortise.py
def frame_from_params_and_beam(self, beam):
    """Calculates the cutting frame from the machining parameters in this instance and the given beam

    Parameters
    ----------
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Returns
    -------
    :class:`compas.geometry.Frame`
        The cutting frame.

    """
    # type: (Beam) -> Frame
    assert self.angle is not None
    assert self.inclination is not None

    # start with a plane aligned with the ref side but shifted to the start_x of the cut
    ref_side = beam.side_as_surface(self.ref_side_index)
    p_origin = planar_surface_point_at(ref_side, self.start_x, self.start_y)
    cutting_frame = Frame(p_origin, ref_side.frame.xaxis, ref_side.frame.yaxis)

    # rotate the cutting frame based on the angle
    rotation = Rotation.from_axis_and_angle(
        cutting_frame.normal, math.radians(self.angle + 90), cutting_frame.point
    )
    cutting_frame.transform(rotation)

    return cutting_frame
from_frame_and_beam classmethod ¤
from_frame_and_beam(
    frame,
    beam,
    start_depth=0.0,
    angle=0.0,
    length=80.0,
    width=40.0,
    depth=28.0,
    cone_angle=10.0,
    flank_angle=15.0,
    shape=AUTOMATIC,
    shape_radius=20.0,
    ref_side_index=0,
    **kwargs,
)

Create a DovetailMortise instance from a cutting surface and the beam it should cut. This could be the ref_side of the cross beam of a Joint and the cross beam.

Parameters:

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

    The cutting frame.

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

    The beam that is cut by this instance.

  • start_depth (float, default: 0.0 ) –

    The start depth of the cut along the y-axis of the beam. This offset is to be used in case of housing. Default is 0.0.

  • angle (float, default: 0.0 ) –

    The angle of the cut.

  • length (float, default: 80.0 ) –

    The length of the mortise.

  • width (float, default: 40.0 ) –

    The width of the mortise.

  • depth (float, default: 28.0 ) –

    The depth of the mortise. The equivalent value of the DovetailTenon BTLxProcessing is the height.

  • cone_angle (float, default: 10.0 ) –

    The cone angle of the dovetail mortise.

  • flank_angle (float, default: 15.0 ) –

    The flank angle of the dovetail mortise.

  • shape (str, default: AUTOMATIC ) –

    The shape of the dovetail mortise in regards to it's edges. Default is 'automatic'.

  • shape_radius (float, default: 20.0 ) –

    The radius of the shape of the dovetail mortise. Default is 20.0.

  • ref_side_index (int, default: 0 ) –

    The reference side index of the beam to be cut. Default is 0 (i.e. RS1).

Returns:

  • class:`~compas_timber.fabrication.DovetailMortise` –
Source code in src/compas_timber/fabrication/dovetail_mortise.py
@classmethod
def from_frame_and_beam(
    cls,
    frame,
    beam,
    start_depth=0.0,
    angle=0.0,
    length=80.0,
    width=40.0,
    depth=28.0,
    cone_angle=10.0,
    flank_angle=15.0,
    shape=TenonShapeType.AUTOMATIC,
    shape_radius=20.0,
    ref_side_index=0,
    **kwargs
):
    """Create a DovetailMortise instance from a cutting surface and the beam it should cut. This could be the ref_side of the cross beam of a Joint and the cross beam.

    Parameters
    ----------
    frame : :class:`~compas.geometry.Frame` or :class:`~compas.geometry.Plane`
        The cutting frame.
    beam : :class:`~compas_timber.elements.Beam`
        The beam that is cut by this instance.
    start_depth : float, optional
        The start depth of the cut along the y-axis of the beam. This offset is to be used in case of housing. Default is 0.0.
    angle : float, optional
        The angle of the cut.
    length : float, optional
        The length of the mortise.
    width : float, optional
        The width of the mortise.
    depth : float, optional
        The depth of the mortise. The equivalent value of the DovetailTenon BTLxProcessing is the height.
    cone_angle : float, optional
        The cone angle of the dovetail mortise.
    flank_angle : float, optional
        The flank angle of the dovetail mortise.
    shape : str, optional
        The shape of the dovetail mortise in regards to it's edges. Default is 'automatic'.
    shape_radius : float, optional
        The radius of the shape of the dovetail mortise. Default is 20.0.
    ref_side_index : int, optional
        The reference side index of the beam to be cut. Default is 0 (i.e. RS1).

    Returns
    -------
    :class:`~compas_timber.fabrication.DovetailMortise`

    """
    # type: (Frame|Plane, Beam, float, float, float, float, float, float, float, float, float, float, int) -> DovetailMortise

    if isinstance(frame, Plane):
        frame = Frame.from_plane(frame)

    # define ref_side & ref_edge
    ref_side = beam.ref_sides[ref_side_index]
    ref_edge = Line.from_point_and_vector(ref_side.point, ref_side.xaxis)

    # calculate orientation
    orientation = cls._calculate_orientation(ref_side, frame)

    # calclulate start_x and start_y
    start_x = cls._calculate_start_x(ref_side, ref_edge, frame)
    start_y = cls._calculate_start_y(ref_side, frame)

    # define angle
    angle = cls._calculate_angle(ref_side, frame, orientation)

    # define slope and inclination
    # TODO: In which cases do you want indiferent slope and inclination?
    slope = 90.0
    inclination = 90.0

    # determine if the top and bottom length of the cut is limited
    limitation_top = LimitationTopType.UNLIMITED
    length_limited_bottom = True

    use_flank_angle = True if flank_angle != 15.0 else False  # TODO: does this change anything?

    return cls(
        start_x,
        start_y,
        start_depth,
        angle,
        slope,
        inclination,
        limitation_top,
        length_limited_bottom,
        length,
        width,
        depth,
        cone_angle,
        use_flank_angle,
        flank_angle,
        shape,
        shape_radius,
        ref_side_index=ref_side_index,
        **kwargs,
    )
scale ¤
scale(factor)

Scale the parameters of this processing by a given factor.

Notes

Only distances are scaled, angles remain unchanged.

Parameters:

  • factor (float) –

    The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

Source code in src/compas_timber/fabrication/dovetail_mortise.py
def scale(self, factor):
    """Scale the parameters of this processing by a given factor.

    Notes
    -----
    Only distances are scaled, angles remain unchanged.

    Parameters
    ----------
    factor : float
        The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

    """
    self._start_x *= factor
    self._start_y *= factor
    self._start_depth *= factor
    self._length *= factor
    self._width *= factor
    self._depth *= factor
    self._shape_radius *= factor

DovetailTenon ¤

DovetailTenon(
    orientation=START,
    start_x=0.0,
    start_y=50.0,
    start_depth=50.0,
    angle=90.0,
    inclination=90.0,
    rotation=90.0,
    length_limited_top=True,
    length_limited_bottom=True,
    length=80.0,
    width=40.0,
    height=28.0,
    cone_angle=15.0,
    use_flank_angle=False,
    flank_angle=15.0,
    shape=AUTOMATIC,
    shape_radius=20.0,
    **kwargs,
)

Represents a Dovetail Tenon feature to be made on a beam.

Parameters:

  • orientation (int, default: START ) –

    The orientation of the cut. Must be either OrientationType.START or OrientationType.END.

  • start_x (float, default: 0.0 ) –

    The start x-coordinate of the cut in parametric space of the reference side. Distance from the beam start to the reference point. -100000.0 < start_x < 100000.0.

  • start_y (float, default: 50.0 ) –

    The start y-coordinate of the cut in parametric space of the reference side. Distance from the reference edge to the reference point. -5000.0 < start_y < 5000.0.

  • start_depth (float, default: 50.0 ) –

    The start depth of the cut in parametric space of the reference side. Margin on the reference side. -5000.0 < start_depth < 5000.0.

  • angle (float, default: 90.0 ) –

    The angle of the cut. Angle between edge and reference edge. 0.1 < angle < 179.9.

  • inclination (float, default: 90.0 ) –

    The inclination of the cut. Inclination between face and reference side. 0.1 < inclination < 179.9.

  • rotation (float, default: 90.0 ) –

    The rotation of the cut. Angle between axis of the tenon and rederence side. 0.1 < rotation < 179.9.

  • length_limited_top (bool, default: True ) –

    Whether the top length of the cut is limited. True or False.

  • length_limited_bottom (bool, default: True ) –

    Whether the bottom length of the cut is limited. True or False.

  • length (float, default: 80.0 ) –

    The length of the cut. 0.0 < length < 5000.0.

  • width (float, default: 40.0 ) –

    The width of the cut. 0.0 < width < 1000.0.

  • height (float, default: 28.0 ) –

    The height of the tenon. 0.0 < height < 1000.0.

  • cone_angle (float, default: 15.0 ) –

    The cone angle of the cut. 0.0 < cone_angle < 30.0.

  • use_flank_angle (bool, default: False ) –

    Whether the flank angle is used. True or False.

  • flank_angle (float, default: 15.0 ) –

    The flank angle of the cut. Angle of the tool. 5.0 < flank_angle < 35.0.

  • shape (str, default: AUTOMATIC ) –

    The shape of the cut. Must be either 'automatic', 'square', 'round', 'rounded', or 'radius'.

  • shape_radius (float, default: 20.0 ) –

    The radius of the shape of the cut. 0.0 < shape_radius < 1000.0.

Source code in src/compas_timber/fabrication/dovetail_tenon.py
def __init__(
    self,
    orientation = OrientationType.START,
    start_x=0.0,
    start_y=50.0,
    start_depth=50.0,
    angle=90.0,
    inclination=90.0,
    rotation=90.0,
    length_limited_top=True,
    length_limited_bottom=True,
    length=80.0,
    width=40.0,
    height=28.0,
    cone_angle=15.0,
    use_flank_angle=False,
    flank_angle=15.0,
    shape=TenonShapeType.AUTOMATIC,
    shape_radius=20.0,
    **kwargs
):
    super(DovetailTenon, self).__init__(**kwargs)
    self._orientation = None
    self._start_x = None
    self._start_y = None
    self._start_depth = None
    self._angle = None
    self._inclination = None
    self._rotation = None
    self._length_limited_top = None
    self._length_limited_bottom = None
    self._length = None
    self._width = None
    self._height = None
    self._cone_angle = None
    self._use_flank_angle = None
    self._flank_angle = None
    self._shape = None
    self._shape_radius = None

    self.orientation = orientation
    self.start_x = start_x
    self.start_y = start_y
    self.start_depth = start_depth
    self.angle = angle
    self.inclination = inclination
    self.rotation = rotation
    self.length_limited_top = length_limited_top
    self.length_limited_bottom = length_limited_bottom
    self.length = length
    self.width = width
    self.height = height
    self.cone_angle = cone_angle
    self.use_flank_angle = use_flank_angle
    self.flank_angle = flank_angle
    self.shape = shape
    self.shape_radius = shape_radius

Methods:¤

apply ¤
apply(geometry, beam)

Apply the feature to the beam geometry.

Parameters:

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

    The geometry to be processed.

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

    The beam that is milled by this instance.

Raises:

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

    If the cutting frames do not create a volume that itersects with beam geometry or any step fails.

Returns:

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

    The resulting geometry after processing

Source code in src/compas_timber/fabrication/dovetail_tenon.py
def apply(self, geometry, beam):
    """Apply the feature to the beam geometry.

    Parameters
    ----------
    geometry : :class:`compas.geometry.Brep`
        The geometry to be processed.

    beam : :class:`compas_timber.elements.Beam`
        The beam that is milled by this instance.

    Raises
    ------
    :class:`~compas_timber.errors.FeatureApplicationError`
        If the cutting frames do not create a volume that itersects with beam geometry or any step fails.

    Returns
    -------
    :class:`~compas.geometry.Brep`
        The resulting geometry after processing

    """
    # type: (Brep, Beam) -> Brep

    # get cutting plane from params and beam
    try:
        cutting_plane = Plane.from_frame(self.frame_from_params_and_beam(beam))
    except ValueError as e:
        raise FeatureApplicationError(
            None, geometry.transformed(beam.modeltransformation), "Failed to generate cutting frame from parameters and beam: {}".format(str(e))
        )

    # get dovetail volume from params and beam
    try:
        dovetail_volume = self.dovetail_volume_from_params_and_beam(beam)
    except ValueError as e:
        raise FeatureApplicationError(
            None, geometry.transformed(beam.modeltransformation), "Failed to generate dovetail tenon volume from parameters and beam: {}".format(str(e))
        )

    # fillet the edges of the dovetail volume based on the shape
    if (
        self.shape != TenonShapeType.SQUARE and not self.length_limited_bottom
    ):  # TODO: Change negation to affirmation once Brep.fillet is implemented
        edge_indices = [4, 7] if self.length_limited_top else [5, 8]
        try:
            dovetail_volume.fillet(
                self.shape_radius, [dovetail_volume.edges[edge_indices[0]], dovetail_volume.edges[edge_indices[1]]]
            )  # TODO: NotImplementedError
        except Exception as e:
            # dovetail_volume is still in model space here (not yet localized below), geometry is local
            raise FeatureApplicationError(
                dovetail_volume,
                geometry.transformed(beam.modeltransformation),
                "Failed to fillet the edges of the dovetail volume based on the shape: {}".format(str(e)),
            )

    # remove any parts of the dovetail tenon volume that exceed the beam geometry. Fails silently.
    for frame in beam.ref_sides[:4]:
        try:
            dovetail_volume = dovetail_volume.trimmed(frame)
        except Exception:
            pass # Fail silently since it won't be possible to trim the tenon if it doesn't exceed the beam geometry.

    # convert geometries to local coordinates of the beam
    cutting_plane, dovetail_volume = [geometry.transformed(beam.transformation_to_local()) for geometry in [cutting_plane, dovetail_volume]]

    # trim geometry with cutting planes
    try:
        geometry.trim(cutting_plane)
    except Exception as e:
        raise FeatureApplicationError(
            cutting_plane.transformed(beam.modeltransformation),
            geometry.transformed(beam.modeltransformation),
            "Failed to trim geometry with cutting plane: {}".format(str(e)),
        )

    # add tenon volume to geometry
    try:
        geometry = brep_union_first(geometry, dovetail_volume)
    except Exception as e:
        raise FeatureApplicationError(
            dovetail_volume.transformed(beam.modeltransformation),
            geometry.transformed(beam.modeltransformation),
            "Failed to add tenon volume to geometry: {}".format(str(e)),
        )

    return geometry
define_dovetail_tool classmethod ¤
define_dovetail_tool(tool_angle, tool_diameter, tool_height)

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

Parameters:

  • tool_angle (float) –

    The angle of the dovetail cutter tool.

  • tool_diameter (float) –

    The diameter of the dovetail cutter tool.

  • tool_height (float) –

    The height of the dovetail cutter tool.

Source code in src/compas_timber/fabrication/dovetail_tenon.py
@classmethod
def define_dovetail_tool(cls, tool_angle, tool_diameter, tool_height):
    """Define the parameters for the dovetail feature based on a defined dovetail cutting tool.

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

    """
    # type: (float, float, float) -> None
    cls._DOVETAIL_TOOL_PARAMS = {
        "tool_angle": tool_angle,
        "tool_diameter": tool_diameter,
        "tool_height": tool_height,
    }
dovetail_cutting_frames_from_params_and_beam ¤
dovetail_cutting_frames_from_params_and_beam(beam)

Calculates the cutting frames for the dovetail tenon from the machining parameters in this instance and the given beam.

Source code in src/compas_timber/fabrication/dovetail_tenon.py
def dovetail_cutting_frames_from_params_and_beam(self, beam):
    """Calculates the cutting frames for the dovetail tenon from the machining parameters in this instance and the given beam."""

    # get the cutting frame
    cutting_frame = self.frame_from_params_and_beam(beam)

    # offset the cutting frame to create the cutting surface based on the height of the tenon. It needs to face in the opposite direction.
    offseted_cutting_frame = Frame(cutting_frame.point, cutting_frame.xaxis, cutting_frame.yaxis)
    offseted_cutting_frame.point += cutting_frame.normal * self.height

    cutting_surface = PlanarSurface(
        xsize=beam.height / math.sin(math.radians(self.inclination)),
        ysize=beam.width / math.sin(math.radians(self.angle)),
        frame=cutting_frame,
    )
    # move the cutting surface to the center
    cutting_surface.translate(-cutting_frame.xaxis * self.start_y)

    dx_top = self.width / 2 + self.length * abs(math.tan(math.radians(self.cone_angle)))
    dx_bottom = self.width / 2
    dy = -self.length

    bottom_dovetail_points = [
        planar_surface_point_at(cutting_surface, self.start_y - dx_top, 0),
        planar_surface_point_at(cutting_surface, self.start_y + dx_top, 0),
        planar_surface_point_at(cutting_surface, self.start_y + dx_bottom, dy),
        planar_surface_point_at(cutting_surface, self.start_y - dx_bottom, dy),
    ]

    dovetail_edges = [
        Line(bottom_dovetail_points[0], bottom_dovetail_points[1]),  # Top line
        Line(bottom_dovetail_points[1], bottom_dovetail_points[2]),  # Right line
        Line(bottom_dovetail_points[2], bottom_dovetail_points[3]),  # Bottom line
        Line(bottom_dovetail_points[3], bottom_dovetail_points[0]),  # Left line
    ]

    trimming_frames = []
    for i, edge in enumerate(dovetail_edges):
        # create the initial frame using the line's direction and the cutting frame's normal
        frame = Frame(edge.midpoint, -edge.direction, cutting_frame.normal)

        if i != 0:
            # determine the rotation direction: right and bottom are positive, top and left are negative
            # apply the rotation based on the flank angle
            rotation = Rotation.from_axis_and_angle(-edge.direction, math.radians(self.flank_angle), frame.point)
            frame.transform(rotation)

        trimming_frames.append(frame)

    cutting_frame.xaxis = -cutting_frame.xaxis
    trimming_frames.extend([cutting_frame, offseted_cutting_frame])
    return trimming_frames
dovetail_volume_from_params_and_beam ¤
dovetail_volume_from_params_and_beam(beam)

Calculates the dovetail tenon volume from the machining parameters in this instance and the given beam.

Parameters:

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

    The beam that is cut by this instance.

Returns:

  • class:`compas.geometry.Brep` –

    The tenon volume.

Source code in src/compas_timber/fabrication/dovetail_tenon.py
def dovetail_volume_from_params_and_beam(self, beam):
    """Calculates the dovetail tenon volume from the machining parameters in this instance and the given beam.

    Parameters
    ----------
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Returns
    -------
    :class:`compas.geometry.Brep`
        The tenon volume.

    """
    # type: (Beam) -> Brep

    assert self.inclination is not None
    assert self.rotation is not None
    assert self.height is not None
    assert self.flank_angle is not None
    assert self.shape is not None
    assert self.shape_radius is not None
    assert self.length_limited_top is not None
    assert self.length_limited_bottom is not None

    cutting_frame = self.frame_from_params_and_beam(beam)

    # create the dovetail volume by trimming a box  # TODO: PluginNotInstalledError for Brep.from_loft
    # get the box as a brep
    dovetail_volume = Brep.from_box(
        Box(
            (beam.width + (beam.width * math.sin(math.radians(self.rotation)))) * 2,
            (beam.height / math.sin(math.radians(self.inclination))) * 2,
            self.height * 2,
            cutting_frame,
        )
    )

    # get the cutting frames for the dovetail tenon
    trimming_frames = self.dovetail_cutting_frames_from_params_and_beam(beam)

    # trim the box to create the dovetail volume
    for frame in trimming_frames:
        try:
            dovetail_volume.trim(frame)
        except Exception as e:
            raise FeatureApplicationError(
                frame, dovetail_volume, "Failed to trim tenon volume with cutting frame: {}".format(str(e))
            )

    return dovetail_volume
frame_from_params_and_beam ¤
frame_from_params_and_beam(beam)

Calculates the cutting frame from the machining parameters in this instance and the given beam.

Parameters:

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

    The beam that is cut by this instance.

Returns:

  • class:`compas.geometry.Frame` –

    The cutting frame.

Source code in src/compas_timber/fabrication/dovetail_tenon.py
def frame_from_params_and_beam(self, beam):
    """
    Calculates the cutting frame from the machining parameters in this instance and the given beam.

    Parameters
    ----------
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Returns
    -------
    :class:`compas.geometry.Frame`
        The cutting frame.
    """
    assert self.angle is not None
    assert self.inclination is not None
    assert self.start_depth is not None

    # get the reference side surface of the beam
    ref_side = beam.side_as_surface(self.ref_side_index)

    # move the reference side surface to the start depth
    ref_side.translate(-ref_side.frame.normal * self.start_depth)

    # convert angles to radians
    inclination_radians = math.radians(self.inclination)
    angle_radians = math.radians(self.angle + 90)

    # calculate the point of origin based on orientation
    p_origin = planar_surface_point_at(ref_side, self.start_x, self.start_y)
    if self.orientation == OrientationType.END:
        yaxis = ref_side.frame.yaxis
    else:
        yaxis = -ref_side.frame.yaxis
        inclination_radians += math.pi

    # create the initial cutting plane
    cutting_frame = Frame(p_origin, -ref_side.frame.xaxis, yaxis)

    # apply rotations to the cutting plane based on angle and inclination parameters
    rot_a = Rotation.from_axis_and_angle(cutting_frame.zaxis, angle_radians, point=p_origin)
    rot_b = Rotation.from_axis_and_angle(cutting_frame.yaxis, inclination_radians, point=p_origin)
    cutting_frame.transform(rot_a * rot_b)

    # for simplicity align normal towards x-axis
    cutting_frame = Frame(cutting_frame.point, -cutting_frame.yaxis, cutting_frame.xaxis)

    # apply rotation based on the rotation parameter
    rot_angle = math.radians(self.rotation - 90)
    if self.orientation == OrientationType.START:
        rot_angle = -rot_angle
    rotation = Rotation.from_axis_and_angle(cutting_frame.normal, rot_angle, cutting_frame.point)
    cutting_frame.transform(rotation)

    return cutting_frame
from_plane_and_beam classmethod ¤
from_plane_and_beam(
    plane,
    beam,
    start_y=0.0,
    start_depth=50.0,
    rotation=0.0,
    length=80.0,
    width=40.0,
    height=28.0,
    cone_angle=10.0,
    flank_angle=15.0,
    shape=AUTOMATIC,
    shape_radius=20.0,
    ref_side_index=0,
)

Create a DovetailTenon instance from a cutting surface and the beam it should cut. This could be the ref_side of the cross beam of a Joint and the main beam.

Parameters:

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

    The cutting plane.

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

    The beam that is cut by this instance.

  • start_y (float, default: 0.0 ) –

    The start y-coordinate of the cut in parametric space of the reference side. Default is 0.0.

  • start_depth (float, default: 50.0 ) –

    The start depth of the tenon, which is an offset along the normal of the reference side. Default is 50.0.

  • rotation (float, default: 0.0 ) –

    The angle of rotation of the tenon. Default is 0.0.

  • length (float, default: 80.0 ) –

    The length of the tenon. Default is 80.0.

  • width (float, default: 40.0 ) –

    The width of the bottom edge of the tenon. Default is 40.0.

  • height (float, default: 28.0 ) –

    The height of the tenon. Related to the dovetail tool and can be defined using the DovetailTenon.define_dovetail_tool() method. Default is 28.0.

  • cone_angle (float, default: 10.0 ) –

    The angle of the cone of the tenon. Default is 10.0.

  • flank_angle (float, default: 15.0 ) –

    The angle of the flank of the tenon. Related to the dovetail tool and can be defined using the DovetailTenon.define_dovetail_tool() method. Default is 15.0.

  • shape (str, default: AUTOMATIC ) –

    The shape of the tenon. Default is 'automatic'.

  • shape_radius (float, default: 20.0 ) –

    The radius of the shape of the tenon. Related to the dovetail tool and can be defined using the DovetailTenon.define_dovetail_tool() method. Default is 20.0.

  • ref_side_index (int, default: 0 ) –

    The reference side index of the beam to be cut. Default is 0 (i.e. RS1).

Returns:

  • class:`~compas_timber.fabrication.DovetailTenon` –
Source code in src/compas_timber/fabrication/dovetail_tenon.py
@classmethod
def from_plane_and_beam(
    cls,
    plane,
    beam,
    start_y=0.0,
    start_depth=50.0,
    rotation=0.0,
    length=80.0,
    width=40.0,
    height=28.0,
    cone_angle=10.0,
    flank_angle=15.0,
    shape=TenonShapeType.AUTOMATIC,
    shape_radius=20.0,
    ref_side_index=0,
):
    """Create a DovetailTenon instance from a cutting surface and the beam it should cut. This could be the ref_side of the cross beam of a Joint and the main beam.

    Parameters
    ----------
    plane : :class:`~compas.geometry.Plane` or :class:`~compas.geometry.Frame`
        The cutting plane.
    beam : :class:`~compas_timber.elements.Beam`
        The beam that is cut by this instance.
    start_y : float, optional
        The start y-coordinate of the cut in parametric space of the reference side. Default is 0.0.
    start_depth : float, optional
        The start depth of the tenon, which is an offset along the normal of the reference side. Default is 50.0.
    rotation : float, optional
        The angle of rotation of the tenon. Default is 0.0.
    length : float, optional
        The length of the tenon. Default is 80.0.
    width : float, optional
        The width of the bottom edge of the tenon. Default is 40.0.
    height : float, optional
        The height of the tenon. Related to the dovetail tool and can be defined using the `DovetailTenon.define_dovetail_tool()` method. Default is 28.0.
    cone_angle : float, optional
        The angle of the cone of the tenon. Default is 10.0.
    flank_angle : float, optional
        The angle of the flank of the tenon. Related to the dovetail tool and can be defined using the `DovetailTenon.define_dovetail_tool()` method. Default is 15.0.
    shape : str, optional
        The shape of the tenon. Default is 'automatic'.
    shape_radius : float, optional
        The radius of the shape of the tenon. Related to the dovetail tool and can be defined using the `DovetailTenon.define_dovetail_tool()` method. Default is 20.0.
    ref_side_index : int, optional
        The reference side index of the beam to be cut. Default is 0 (i.e. RS1).

    Returns
    -------
    :class:`~compas_timber.fabrication.DovetailTenon`

    """
    # type: (Plane|Frame, Beam, float, float, bool, int) -> DovetailTenon

    if cls._DOVETAIL_TOOL_PARAMS:
        # get the tool parameters
        tool_angle = cls._DOVETAIL_TOOL_PARAMS["tool_angle"]
        tool_diameter = cls._DOVETAIL_TOOL_PARAMS["tool_diameter"]
        tool_height = cls._DOVETAIL_TOOL_PARAMS["tool_height"]
        tool_top_radius = tool_diameter / 2 - tool_height * (math.tan(math.radians(tool_angle)))
        # update parameters related to the tool if a tool is defined
        height = min(height, tool_height)
        flank_angle = tool_angle
        shape_radius = tool_top_radius

    # find the difference of the bottom and top radius of the frustum cone
    frustum_difference = height * math.tan(math.radians(flank_angle))

    if isinstance(plane, Frame):
        plane = Plane.from_frame(plane)
    # define ref_side & ref_edge
    ref_side = beam.ref_sides[ref_side_index]
    ref_edge = Line.from_point_and_vector(ref_side.point, ref_side.xaxis)

    # calculate orientation
    orientation = cls._calculate_orientation(ref_side, plane)

    # calculate angle
    angle = cls._calculate_angle(ref_side, plane)

    # calculate inclination
    inclination = cls._calculate_inclination(ref_side, plane, orientation, angle)

    # calculate start_y & rotation
    if orientation == OrientationType.END:
        rotation = -rotation
        start_y = -start_y
    start_y += beam.width / 2  # TODO: Should this be bound as well?
    rotation += 90

    # bound start_depth, length and width
    start_depth = cls._bound_start_depth(start_depth, inclination, height)
    length = cls._bound_length(
        ref_side, plane, beam.height, start_depth, inclination, length, height, frustum_difference
    )
    width = cls._bound_width(beam.width, angle, length, width, cone_angle, shape_radius, frustum_difference)

    # calculate start_x
    start_x = cls._calculate_start_x(
        ref_side,
        ref_edge,
        plane,
        orientation,
        start_y,
        start_depth,
        angle,
    )

    # determine if the top and bottom length of the cut is limited
    length_limited_top, length_limited_bottom = cls._calculate_length_limits(
        beam, start_depth, length, inclination
    )  # TODO: Should this instead come first and override the start_depth and length?

    use_flank_angle = True if flank_angle != 15.0 else False  # TODO: does this change anything?

    return cls(
        orientation,
        start_x,
        start_y,
        start_depth,
        angle,
        inclination,
        rotation,
        length_limited_top,
        length_limited_bottom,
        length,
        width,
        height,
        cone_angle,
        use_flank_angle,
        flank_angle,
        shape,
        shape_radius,
        ref_side_index=ref_side_index,
    )
scale ¤
scale(factor)

Scale the parameters of the processing by the given factor.

Notes

Only distances are scaled, angles remain unchanged.

Parameters:

  • factor (float) –

    The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

Source code in src/compas_timber/fabrication/dovetail_tenon.py
def scale(self, factor):
    """Scale the parameters of the processing by the given factor.

    Notes
    -----
    Only distances are scaled, angles remain unchanged.

    Parameters
    ----------
    factor : float
        The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

    """
    # type: (float) -> None
    self.start_x *= factor
    self.start_y *= factor
    self.start_depth *= factor
    self.length *= factor
    self.width *= factor
    self.height *= factor
    self.shape_radius *= factor

Drilling ¤

Drilling(
    start_x=0.0,
    start_y=0.0,
    angle=0.0,
    inclination=90.0,
    depth_limited=False,
    depth=50.0,
    diameter=20.0,
    **kwargs,
)

Represents a drilling processing.

Parameters:

  • start_x (float, default: 0.0 ) –

    The x-coordinate of the start point of the drilling. In the local coordinate system of the reference side.

  • start_y (float, default: 0.0 ) –

    The y-coordinate of the start point of the drilling. In the local coordinate system of the reference side.

  • angle (float, default: 0.0 ) –

    The rotation angle of the drilling. In degrees. Around the z-axis of the reference side.

  • inclination (float, default: 90.0 ) –

    The inclination angle of the drilling. In degrees. Around the y-axis of the reference side.

  • depth_limited (bool, default: True ) –

    If True, the drilling depth is limited to depth. Otherwise, drilling will go through the element.

  • depth (float, default: 50.0 ) –

    The depth of the drilling. In mm.

  • diameter (float, default: 20.0 ) –

    The diameter of the drilling. In mm.

Source code in src/compas_timber/fabrication/drilling.py
def __init__(self, start_x=0.0, start_y=0.0, angle=0.0, inclination=90.0, depth_limited=False, depth=50.0, diameter=20.0, **kwargs):
    super(Drilling, self).__init__(**kwargs)
    self._start_x = None
    self._start_y = None
    self._angle = None
    self._inclination = None
    self._depth_limited = None
    self._depth = None
    self._diameter = None

    self.start_x = start_x
    self.start_y = start_y
    self.angle = angle
    self.inclination = inclination
    self.depth_limited = depth_limited
    self.depth = depth
    self.diameter = diameter

Methods:¤

apply ¤
apply(geometry, element)

Apply the feature to the element geometry.

Raises:

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

    If the cutting plane does not intersect with the element geometry.

Returns:

  • class:`compas.geometry.Brep` –

    The resulting geometry after processing.

Source code in src/compas_timber/fabrication/drilling.py
def apply(self, geometry, element):
    """Apply the feature to the element geometry.

    Raises
    ------
    :class:`compas_timber.errors.FeatureApplicationError`
        If the cutting plane does not intersect with the element geometry.

    Returns
    -------
    :class:`compas.geometry.Brep`
        The resulting geometry after processing.

    """
    drill_geometry = Brep.from_cylinder(self.cylinder_from_params_and_element(element))
    drill_geometry.transform(element.transformation_to_local())

    try:
        return brep_difference_first(geometry, drill_geometry)
    except IndexError:
        raise FeatureApplicationError(
            drill_geometry.transformed(element.modeltransformation),
            geometry.transformed(element.modeltransformation),
            "The drill geometry does not intersect with element geometry.",
        )
cylinder_from_params_and_element ¤
cylinder_from_params_and_element(element)

Construct the geometry of the drilling using the parameters in this instance and the element object.

Parameters:

  • element (:class:`compas_timber.elements.Element`) –

    The element to drill.

Returns:

  • class:`compas.geometry.Cylinder` –

    The constructed cylinder.

Source code in src/compas_timber/fabrication/drilling.py
def cylinder_from_params_and_element(self, element):
    """Construct the geometry of the drilling using the parameters in this instance and the element object.

    Parameters
    ----------
    element : :class:`compas_timber.elements.Element`
        The element to drill.

    Returns
    -------
    :class:`compas.geometry.Cylinder`
        The constructed cylinder.

    """
    assert self.diameter is not None
    assert self.angle is not None
    assert self.inclination is not None
    assert self.depth is not None

    ref_surface = element.side_as_surface(self.ref_side_index)
    xy_world = planar_surface_point_at(ref_surface, self.start_x, self.start_y)

    # x and y flipped because we want z pointting down into the element, that'll be the cylinder long direction
    cylinder_frame = Frame(xy_world, ref_surface.zaxis, -ref_surface.yaxis)
    cylinder_frame.rotate(math.radians(self.angle), -ref_surface.zaxis, point=xy_world)
    cylinder_frame.rotate(math.radians(self.inclination), cylinder_frame.yaxis, point=xy_world)

    drill_line = self._calculate_drill_line(element, xy_world, cylinder_frame)

    # scale both ends so is protrudes nicely from the surface
    # TODO: this is a best-effort solution. this can be done more accurately taking the angle into account. consider doing that in the future.
    drill_line = self._scaled_line_by_factor(drill_line, 1.2)
    return Cylinder.from_line_and_radius(drill_line, self.diameter * 0.5)
from_line_and_element classmethod ¤
from_line_and_element(line, element, diameter)

Construct a drilling process from a line and diameter.

Todo

change this to point + vector instead of line. line is too fragile, it can be flipped and cause issues. make a from point alt. constructor that takes a point and a reference side and makes a straight drilling through.

Parameters:

  • line (:class:`compas.geometry.Line`) –

    The line on which the drilling is to be made.

  • element (:class:`compas_timber.elements.Element`) –

    The element to drill.

  • diameter (float) –

    The diameter of the drilling.

Returns:

  • class:`compas_timber.fabrication.Drilling` –

    The constructed drilling processing.

Source code in src/compas_timber/fabrication/drilling.py
@classmethod
def from_line_and_element(cls, line, element, diameter):
    """Construct a drilling process from a line and diameter.

    TODO: change this to point + vector instead of line. line is too fragile, it can be flipped and cause issues.
    make a from point alt. constructor that takes a point and a reference side and makes a straight drilling through.

    Parameters
    ----------
    line : :class:`compas.geometry.Line`
        The line on which the drilling is to be made.
    element : :class:`compas_timber.elements.Element`
        The element to drill.
    diameter : float
        The diameter of the drilling.

    Returns
    -------
    :class:`compas_timber.fabrication.Drilling`
        The constructed drilling processing.

    """
    ref_side_index, xy_point = cls._calculate_ref_side_index(line, element)
    line = cls._flip_line_if_start_inside(line, element, ref_side_index)
    depth_limited = cls._is_depth_limited(line, element)
    ref_surface = element.side_as_surface(ref_side_index)
    depth = cls._calculate_depth(line, ref_surface) if depth_limited else 0.0
    x_start, y_start = cls._xy_to_ref_side_space(xy_point, ref_surface)
    angle = cls._calculate_angle(ref_surface, line, xy_point)
    inclination = cls._calculate_inclination(ref_surface.frame, line, angle, xy_point)
    try:
        return cls(x_start, y_start, angle, inclination, depth_limited, depth, diameter, ref_side_index=ref_side_index)
    except ValueError as e:
        raise FeatureApplicationError(
            message=str(e),
            feature_geometry=line,
            element_geometry=element.blank,
        )
from_shapes_and_element classmethod ¤
from_shapes_and_element(line, element, diameter, **kwargs)

Construct a drilling process from a line, element and diameter.

Parameters:

  • line (:class:`compas.geometry.Line`) –

    The line on which the drilling is to be made.

  • element (:class:`compas_timber.elements.Element`) –

    The element to drill.

  • diameter (float) –

    The diameter of the drilling.

Returns:

  • class:`compas_timber.fabrication.Drilling` –

    The constructed drilling process.

Source code in src/compas_timber/fabrication/drilling.py
@classmethod
def from_shapes_and_element(cls, line, element, diameter, **kwargs):
    """Construct a drilling process from a line, element and diameter.

    Parameters
    ----------
    line : :class:`compas.geometry.Line`
        The line on which the drilling is to be made.
    element : :class:`compas_timber.elements.Element`
        The element to drill.
    diameter : float
        The diameter of the drilling.

    Returns
    -------
    :class:`compas_timber.fabrication.Drilling`
        The constructed drilling process.

    """
    if isinstance(line, list):
        line = line[0]
    return cls.from_line_and_element(line, element, float(diameter), **kwargs)
scale ¤
scale(factor)

Scale the parameters of this processing by a given factor.

Notes

Only distances are scaled, angles remain unchanged.

Parameters:

  • factor (float) –

    The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

Source code in src/compas_timber/fabrication/drilling.py
def scale(self, factor):
    """Scale the parameters of this processing by a given factor.

    Notes
    -----
    Only distances are scaled, angles remain unchanged.

    Parameters
    ----------
    factor : float
        The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

    """
    self._start_x *= factor
    self._start_y *= factor
    self._depth *= factor
    self._diameter *= factor

DrillingProxy ¤

DrillingProxy(line, element, diameter)

This object behaves like a Drilling except it only calculates the machining parameters once unproxified. Can also be used to defer the creation of the processing instance until it is actually needed.

Until then, it can be used to visualize the machining operation. This slightly improves performance.

Parameters:

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

    The line on which the drilling is to be made.

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

    The element to drill.

  • diameter (float) –

    The diameter of the drilling.

Source code in src/compas_timber/fabrication/drilling.py
def __init__(self, line, element, diameter):
    self.line = line.transformed(element.transformation_to_local())
    self.element = element
    self.diameter = diameter
    self._processing = None

Methods:¤

apply ¤
apply(geometry, element)

Apply the feature to the element geometry.

Parameters:

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

    The element geometry to drill.

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

    The element to drill.

Raises:

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

    If the drill geometry does not intersect with element geometry.

Returns:

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

    The resulting geometry after processing.

Source code in src/compas_timber/fabrication/drilling.py
def apply(self, geometry, element):
    """Apply the feature to the element geometry.

    Parameters
    ----------
    geometry : :class:`~compas.geometry.Brep`
        The element geometry to drill.
    element : :class:`~compas_timber.elements.Element`
        The element to drill.

    Raises
    ------
    :class:`~compas_timber.errors.FeatureApplicationError`
        If the drill geometry does not intersect with element geometry.

    Returns
    -------
    :class:`~compas.geometry.Brep`
        The resulting geometry after processing.

    """
    # type: (Brep, Element) -> Brep
    drill_geometry = Brep.from_cylinder(Cylinder.from_line_and_radius(self.line, self.diameter * 0.5))

    try:
        return brep_difference_first(geometry, drill_geometry)
    except IndexError:
        raise FeatureApplicationError(
            drill_geometry.transformed(element.modeltransformation),
            geometry.transformed(element.modeltransformation),
            "The drill geometry does not intersect with element geometry.",
        )
from_line_and_element classmethod ¤
from_line_and_element(line, element, diameter)

Construct a DrillingProxy from a line and diameter.

Parameters:

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

    The line on which the drilling is to be made.

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

    The element to drill.

  • diameter (float) –

    The diameter of the drilling.

Returns:

  • class:`~compas_timber.fabrication.DrillingProxy` –

    The constructed drilling proxy.

Source code in src/compas_timber/fabrication/drilling.py
@classmethod
def from_line_and_element(cls, line, element, diameter):
    """Construct a DrillingProxy from a line and diameter.

    Parameters
    ----------
    line : :class:`~compas.geometry.Line`
        The line on which the drilling is to be made.
    element : :class:`~compas_timber.elements.Element`
        The element to drill.
    diameter : float
        The diameter of the drilling.

    Returns
    -------
    :class:`~compas_timber.fabrication.DrillingProxy`
        The constructed drilling proxy.

    """
    return cls(line, element, diameter)
from_shapes_and_element classmethod ¤
from_shapes_and_element(line, element, diameter, **kwargs)

Construct a DrillingProxy from a line, element and diameter.

Parameters:

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

    The line on which the drilling is to be made.

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

    The element to drill.

  • diameter (float) –

    The diameter of the drilling.

Returns:

  • class:`~compas_timber.fabrication.DrillingProxy` –

    The constructed drilling proxy.

Source code in src/compas_timber/fabrication/drilling.py
@classmethod
def from_shapes_and_element(cls, line, element, diameter, **kwargs):
    """Construct a DrillingProxy from a line, element and diameter.

    Parameters
    ----------
    line : :class:`~compas.geometry.Line`
        The line on which the drilling is to be made.
    element : :class:`~compas_timber.elements.Element`
        The element to drill.
    diameter : float
        The diameter of the drilling.

    Returns
    -------
    :class:`~compas_timber.fabrication.DrillingProxy`
        The constructed drilling proxy.

    """
    if isinstance(line, list):
        line = line[0]
    return cls.from_line_and_element(line, element, float(diameter), **kwargs)
unproxified ¤
unproxified()

Returns the unproxified processing instance.

Returns:

  • class:`~compas_timber.fabrication.Drilling` –
Source code in src/compas_timber/fabrication/drilling.py
def unproxified(self):
    """Returns the unproxified processing instance.

    Returns
    -------
    :class:`~compas_timber.fabrication.Drilling`

    """
    if not self._processing:
        line = self.line.transformed(self.element.modeltransformation)
        self._processing = Drilling.from_line_and_element(line, self.element, self.diameter)
    return self._processing

DualContour ¤

DualContour(principal_contour, associated_contour, depth_bounded=None)

Represens the generic contour for specific free contour processings.

Todo

add point attributes for other types like NailContour

Parameters:

  • principal_contour (:class:`compas.geometry.Polyline`) –

    The principal contour of the dual contour.

  • associated_contour (:class:`compas.geometry.Polyline`) –

    The associated contour of the dual contour. Must have same number of segments as principal_contour.

  • depth_bounded (bool, default: None ) –

    If True, the depth is bounded.

Source code in src/compas_timber/fabrication/btlx.py
def __init__(self, principal_contour, associated_contour, depth_bounded=None):
    super(DualContour, self).__init__()
    self.principal_contour = principal_contour
    self.associated_contour = associated_contour
    self.depth_bounded = depth_bounded

Methods:¤

scale ¤
scale(factor)

Scale the dual contour by a given factor.

Parameters:

  • factor (float) –

    The scaling factor.

Source code in src/compas_timber/fabrication/btlx.py
def scale(self, factor):
    """Scale the dual contour by a given factor.

    Parameters
    ----------
    factor : float
        The scaling factor.

    """
    self.principal_contour.scaled(factor)
    self.associated_contour.scaled(factor)
    if self.depth_bounded is not None:
        self.depth_bounded *= factor
scaled ¤
scaled(factor)

Returns a new instance of the dual contour with the parameters scaled by a given factor.

Parameters:

  • factor (float) –

    The scaling factor.

Returns:

  • class:`~compas_timber.fabrication.DualContour` –

    A new instance of the dual contour with the parameters scaled by the given factor.

Source code in src/compas_timber/fabrication/btlx.py
def scaled(self, factor):
    """Returns a new instance of the dual contour with the parameters scaled by a given factor.

    Parameters
    ----------
    factor : float
        The scaling factor.

    Returns
    -------
    :class:`~compas_timber.fabrication.DualContour`
        A new instance of the dual contour with the parameters scaled by the given factor.

    """
    # type: (float) -> DualContour
    new_instance = self.copy()
    new_instance.scale(factor)
    return new_instance

EdgePositionType ¤

Enum for the edge position of the cut.

Attributes:

  • REFEDGE (literal(refedge)) –

    Reference edge.

  • OPPEDGE (literal(oppedge)) –

    Opposite edge.

FreeContour ¤

FreeContour(
    contour_param_object,
    tool_id=0,
    counter_sink=False,
    tool_position=LEFT,
    depth_bounded=True,
    **kwargs,
)

Represents a free contour processing.

Parameters:

  • contour_param_object (:class:`compas_timber.fabrication.btlx.Contour` or :class:`compas_timber.fabrication.btlx.DualContour`) –

    The contour parameter object.

  • tool_id (int, default: 0 ) –

    The tool ID for the processing. Default is 0.

  • counter_sink (bool, default: False ) –

    If True, the contour is a counter sink. Default is False.

  • tool_position (:class:`~compas_timber.fabrication.AlignmentType`, default: LEFT ) –

    The position of the tool relative to the beam. Can be 'left', 'center', or 'right'.

  • depth_bounded (bool, default: True ) –

    If True, the depth is bounded. Default is False, meaning the machining will cut all the way through the element.

Source code in src/compas_timber/fabrication/free_contour.py
def __init__(self, contour_param_object, tool_id=0, counter_sink=False, tool_position=AlignmentType.LEFT, depth_bounded=True, **kwargs):
    super(FreeContour, self).__init__(tool_id=tool_id, counter_sink=counter_sink, tool_position=tool_position, **kwargs)
    self._process_id = 1  # FreeContour uses process_id!=0
    self.contour_param_object = contour_param_object
    self.depth_bounded = depth_bounded

Attributes¤

params property ¤
params

Returns custom BTLx processing parameters that handle polymorphic contour serialization.

Returns:

  • class:`~compas_timber.fabrication.BTLxProcessingParams` –

    Custom processing parameters that choose between Contour/DualContour based on runtime type.

Methods:¤

apply ¤
apply(geometry, element)

Apply the feature to the beam geometry.

Raises:

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

    If the cutting plane does not intersect with the beam geometry.

Returns:

  • class:`compas.geometry.Brep` –

    The resulting geometry after processing.

Source code in src/compas_timber/fabrication/free_contour.py
def apply(self, geometry, element):
    """Apply the feature to the beam geometry.

    Raises
    ------
    :class:`compas_timber.errors.FeatureApplicationError`
        If the cutting plane does not intersect with the beam geometry.

    Returns
    -------
    :class:`compas.geometry.Brep`
        The resulting geometry after processing.
    """

    vol = self.contour_param_object.to_brep()
    # contour is defined in the ref_side local frame, need to transform first to global then to element local, where geometry is created
    transformation_ref_side_to_local = element.modeltransformation.inverse() * Transformation.from_frame(element.ref_sides[self.ref_side_index])
    vol.transform(transformation_ref_side_to_local)
    if self.counter_sink:  # contour should remove material inside of the contour
        return brep_difference_first(geometry, vol)
    else:
        return brep_intersection_first(geometry, vol)
from_polyline_and_element classmethod ¤
from_polyline_and_element(
    polyline: Polyline,
    element: TimberElement,
    depth: float | None = None,
    interior: bool | None = False,
    tool_position: str | None = None,
    ref_side_index: int | None = None,
    **kwargs,
)

Construct a Contour processing from a polyline and element.

Parameters:

  • polyline (list of :class:`compas.geometry.Point`) –

    The polyline of the contour.

  • element (:class:`compas_timber.elements.Beam` or :class:`compas_timber.elements.Plate`) –

    The element.

  • depth (float, default: None ) –

    The depth of the contour. Default is the thickness of the element.

  • interior (bool, default: False ) –

    If True, the contour is an interior contour. Default is False.

  • tool_position (AlignmentType, default: None ) –

    The position of the tool. Default is "left".

  • ref_side_index (int, default: None ) –

    The reference side index. If none is given, the function will try to find the reference side index based on the polyline and element.

Source code in src/compas_timber/fabrication/free_contour.py
@classmethod
def from_polyline_and_element(
    cls,
    polyline: Polyline,
    element: TimberElement,
    depth: Optional[float] = None,
    interior: Optional[bool] = False,
    tool_position: Optional[str] = None,
    ref_side_index: Optional[int] = None,
    **kwargs,
):
    """Construct a Contour processing from a polyline and element.

    Parameters
    ----------
    polyline : list of :class:`compas.geometry.Point`
        The polyline of the contour.
    element : :class:`compas_timber.elements.Beam` or :class:`compas_timber.elements.Plate`
        The element.
    depth : float, optional
        The depth of the contour. Default is the thickness of the element.
    interior : bool, optional
        If True, the contour is an interior contour. Default is False.
    tool_position : BTLx.AlignmentType, optional
        The position of the tool. Default is "left".
    ref_side_index : int, optional
        The reference side index. If none is given, the function will try to find the reference side index based on the polyline and element.
    """

    if ref_side_index is None:
        ref_side_index = cls.get_ref_face_index(polyline, element)
    ref_side = element.ref_sides[ref_side_index]
    tool_position = cls.parse_tool_position(polyline, ref_side, interior, tool_position)
    # get_dimensions_relative_to_side [1] returns element dimension normal to ref_side
    depth = depth or element.get_dimensions_relative_to_side(ref_side_index)[1]
    transformed_polyline = polyline.transformed(Transformation.from_frame(ref_side).inverse())
    contour = Contour(transformed_polyline, depth=depth, inclination=[0.0])
    return cls(contour, tool_position=tool_position, counter_sink=interior, ref_side_index=ref_side_index, **kwargs)
from_shapes_and_element classmethod ¤
from_shapes_and_element(polyline, element, depth=None, interior=True, **kwargs)

Construct a Contour processing from a list of shapes and element.

Parameters:

  • polyline (list of :class:`compas.geometry.Point`) –

    The polyline of the contour.

  • element (:class:`compas_timber.elements.Beam` or :class:`compas_timber.elements.Plate`) –

    The element.

  • depth (float, default: None ) –

    The depth of the contour. Default is the width of the element.

  • interior (bool, default: True ) –

    If True, the contour is an interior contour. Default is True.

Source code in src/compas_timber/fabrication/free_contour.py
@classmethod
def from_shapes_and_element(cls, polyline, element, depth=None, interior=True, **kwargs):
    """Construct a Contour processing from a list of shapes and element.

    Parameters
    ----------
    polyline : list of :class:`compas.geometry.Point`
        The polyline of the contour.
    element : :class:`compas_timber.elements.Beam` or :class:`compas_timber.elements.Plate`
        The element.
    depth : float, optional
        The depth of the contour. Default is the width of the element.
    interior : bool, optional
        If True, the contour is an interior contour. Default is True.
    """
    return cls.from_polyline_and_element(polyline, element, depth, interior, **kwargs)
from_top_bottom_and_elements classmethod ¤
from_top_bottom_and_elements(
    top_polyline,
    bottom_polyline,
    element,
    interior=False,
    tool_position=None,
    ref_side_index=None,
    **kwargs,
)

Construct a Contour processing from a list of polylines and element.

Parameters:

  • top_polyline (list of list of :class:`compas.geometry.Point`) –

    The top and bottom polylines of the contour.

  • bottom_polyline (list of list of :class:`compas.geometry.Point`) –

    The bottom polyline of the contour.

  • element (:class:`compas_timber.elements.Beam` or :class:`compas_timber.elements.Plate`) –

    The element.

  • interior (bool, default: False ) –

    If True, the contour is an interior contour. Default is False.

  • tool_position (AlignmentType, default: None ) –

    The position of the tool. Default is None.

  • ref_side_index (int, default: None ) –

    The reference side index. If none is given, the function will try to find the reference side index based on the polylines and element.

Source code in src/compas_timber/fabrication/free_contour.py
@classmethod
def from_top_bottom_and_elements(cls, top_polyline, bottom_polyline, element, interior=False, tool_position=None, ref_side_index=None, **kwargs):
    # type: (Polyline, Polyline, Union[Plate, Beam], bool, str | None, int | None, dict) -> FreeContour
    """Construct a Contour processing from a list of polylines and element.

    Parameters
    ----------
    top_polyline : list of list of :class:`compas.geometry.Point`
        The top and bottom polylines of the contour.
    bottom_polyline : list of list of :class:`compas.geometry.Point`
        The bottom polyline of the contour.
    element : :class:`compas_timber.elements.Beam` or :class:`compas_timber.elements.Plate`
        The element.
    interior : bool, optional
        If True, the contour is an interior contour. Default is False.
    tool_position : BTLx.AlignmentType, optional
        The position of the tool. Default is None.
    ref_side_index : int, optional
        The reference side index. If none is given, the function will try to find the reference side index based on the polylines and element.
    """
    if len(top_polyline) != len(bottom_polyline):
        raise ValueError("The top and bottom polylines should have the same number of points.")

    if not ref_side_index:
        ref_side_index = cls.get_ref_face_index(top_polyline, element)

    ref_side = element.ref_sides[ref_side_index]
    tool_position = cls.parse_tool_position(top_polyline, ref_side, interior, tool_position)
    transformation_to_local = Transformation.from_frame(ref_side).inverse()

    top_polyline = top_polyline.transformed(transformation_to_local)
    bottom_polyline = bottom_polyline.transformed(transformation_to_local)
    if not cls.are_all_segments_parallel(top_polyline, bottom_polyline):  # use DualContour
        contour = DualContour(top_polyline, bottom_polyline)
    else:  # use Contour with inclination
        inclinations = []
        for top_line, bottom_line in zip(top_polyline.lines, bottom_polyline.lines):
            cp = bottom_line.closest_point(top_line.start)
            inclinations.append(round(angle_vectors_signed(Vector.from_start_end(top_line.start, cp), Vector(0, 0, -1), -top_line.direction, deg=True), 6))
        if len(set(inclinations)) == 1:
            inclinations = [inclinations[0]]  # all inclinations are the same, set one global inclination for FreeContour processing
        # bottom polyline is parallel to ref_side. Use Z value of first point to calculate depth
        depth = -bottom_polyline[0][2]
        contour = Contour(top_polyline, depth=depth, inclination=inclinations)

    return cls(contour, counter_sink=interior, tool_position=tool_position, ref_side_index=ref_side_index, **kwargs)  # type: ignore
scale ¤
scale(factor)

Scale the parameters of this processing by a given factor.

Notes

Only distances are scaled, angles remain unchanged.

Parameters:

  • factor (float) –

    The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

Source code in src/compas_timber/fabrication/free_contour.py
def scale(self, factor):
    """Scale the parameters of this processing by a given factor.

    Notes
    -----
    Only distances are scaled, angles remain unchanged.

    Parameters
    ----------
    factor : float
        The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

    """
    self.contour_param_object.scale(factor)

FrenchRidgeLap ¤

FrenchRidgeLap(
    orientation=START,
    start_x=0.0,
    angle=90.0,
    ref_position=REFEDGE,
    drillhole=False,
    drillhole_diam=0.0,
    **kwargs,
)

Represents a French Ridge Lap feature to be made on a beam.

Parameters:

  • orientation (int, default: START ) –

    The orientation of the cut. Must be either OrientationType.START or OrientationType.END.

  • start_x (float, default: 0.0 ) –

    The start x-coordinate of the cut in parametric space of the reference side. -100000.0 < start_x < 100000.0.

  • angle (float, default: 90.0 ) –

    The horizontal angle of the cut. 0.1 < angle < 179.9.

  • ref_position –

    The reference position of the cut. Must be either EdgePositionType.REFEDGE or EdgePositionType.OPPEDGE.

  • drillhole (bool, default: False ) –

    Flag indicating if a drill hole should be made.

  • drillhole_diam (float, default: 0.0 ) –

    The diameter of the drill hole.

Source code in src/compas_timber/fabrication/french_ridge_lap.py
def __init__(self, orientation=OrientationType.START, start_x=0.0, angle=90.0, ref_position=EdgePositionType.REFEDGE, drillhole=False, drillhole_diam=0.0, **kwargs):
    super(FrenchRidgeLap, self).__init__(**kwargs)
    self._orientation = None
    self._start_x = None
    self._angle = None
    self._ref_position = None
    self._drillhole = None
    self._drillhole_diam = None

    self.orientation = orientation
    self.start_x = start_x
    self.angle = angle
    self.ref_position = ref_position
    self.drillhole = drillhole
    self.drillhole_diam = drillhole_diam

Methods:¤

apply ¤
apply(geometry, beam)

Apply the feature to the beam geometry.

Parameters:

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

    The beam geometry to be cut.

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

    The beam that is cut by this instance.

Raises:

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

    If the cutting plane does not intersect with beam geometry.

Returns:

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

    The resulting geometry after processing

Source code in src/compas_timber/fabrication/french_ridge_lap.py
def apply(self, geometry, beam):
    """Apply the feature to the beam geometry.

    Parameters
    ----------
    geometry : :class:`~compas.geometry.Brep`
        The beam geometry to be cut.
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Raises
    ------
    :class:`~compas_timber.errors.FeatureApplicationError`
        If the cutting plane does not intersect with beam geometry.

    Returns
    -------
    :class:`~compas.geometry.Brep`
        The resulting geometry after processing

    """
    # type: (Brep, Beam) -> Brep
    # trim the beam geometry with the cutting frame
    trimming_frame = self.frame_from_params_and_beam(beam)
    trimming_frame.transform(beam.transformation_to_local())

    if not TOL.is_zero(self.angle - 90):
        try:
            geometry.trim(trimming_frame)
        except BrepTrimmingError:
            raise FeatureApplicationError(
                trimming_frame.transformed(beam.modeltransformation),
                geometry.transformed(beam.modeltransformation),
                "Could not trim the beam geometry with the cutting frame.",
            )
    # subtract the lap volume from the beam geometry
    subtracting_volume = self.lap_volume_from_params_and_beam(beam)
    subtracting_volume.transform(beam.transformation_to_local())
    try:
        return brep_difference_first(geometry, subtracting_volume)
    except IndexError:
        raise FeatureApplicationError(
            subtracting_volume.transformed(beam.modeltransformation),
            geometry.transformed(beam.modeltransformation),
            "Could not subtract the cutting volume from the beam geometry.",
        )
frame_from_params_and_beam ¤
frame_from_params_and_beam(beam)

Calculates the frame that cuts the exceeding part of the blank of the beam from the machining parameters and the given beam.

Parameters:

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

    The beam that is cut by this instance.

Returns:

  • class:`compas.geometry.Frame` –

    The cutting frame.

Source code in src/compas_timber/fabrication/french_ridge_lap.py
def frame_from_params_and_beam(self, beam):
    """Calculates the frame that cuts the exceeding part of the blank of the beam from the machining parameters and the given beam.

    Parameters
    ----------
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Returns
    -------
    :class:`compas.geometry.Frame`
        The cutting frame.

    """
    # type: (Beam) -> Frame
    assert self.orientation is not None
    assert self.start_x is not None
    assert self.angle is not None
    assert self.ref_position is not None

    ref_surface = beam.side_as_surface(self.ref_side_index)
    # get the origin of the cutting frame
    start_x = self.start_x
    start_y = ref_surface.ysize if self.ref_position == EdgePositionType.OPPEDGE else 0
    origin = planar_surface_point_at(ref_surface, start_x, start_y)

    # flip the rot_axis if the orientation is END
    rot_axis = -ref_surface.frame.normal
    if self.orientation == OrientationType.END:
        rot_axis = -rot_axis

    # get the opposite angle if the reference position is REFEDGE
    angle = self.angle
    if self.ref_position == EdgePositionType.REFEDGE:
        angle = 180 - self.angle

    ref_surface.rotate(math.radians(angle), rot_axis, origin)
    return Frame(origin, ref_surface.frame.xaxis, ref_surface.frame.normal)
from_beam_beam_and_plane classmethod ¤
from_beam_beam_and_plane(beam, other_beam, plane, drillhole_diam=0.0, ref_side_index=0)

Create a FrenchRidgeLap instance from two beams and a cutting plane. The instance is used to cut the principal beam with the other beam. The plane is the furthest reference side of the other beam that cuts the exceeding part of the principal beam.

Parameters:

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

    The beam that is cut by this instance.

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

    The beam that is used to cut the beam.

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

    The plane that cuts the principal beam.

  • drillhole_diam (float, default: 0.0 ) –

    The diameter of the drillhole.

  • ref_side_index (int, default: 0 ) –

    The reference side index of the beam to be cut. Default is 0 (i.e. RS1).

Returns:

  • class:`~compas_timber.fabrication.FrenchRidgeLap` –
Source code in src/compas_timber/fabrication/french_ridge_lap.py
@classmethod
def from_beam_beam_and_plane(cls, beam, other_beam, plane, drillhole_diam=0.0, ref_side_index=0):
    """Create a FrenchRidgeLap instance from two beams and a cutting plane. The instance is used to cut the principal beam with the other beam.
    The plane is the furthest reference side of the other beam that cuts the exceeding part of the principal beam.

    Parameters
    ----------
    beam : :class:`~compas_timber.elements.Beam`
        The beam that is cut by this instance.
    other_beam : :class:`~compas_timber.elements.Beam`
        The beam that is used to cut the beam.
    plane : :class:`~compas.geometry.Plane`
        The plane that cuts the principal beam.
    drillhole_diam : float
        The diameter of the drillhole.
    ref_side_index : int, optional
        The reference side index of the beam to be cut. Default is 0 (i.e. RS1).

    Returns
    -------
    :class:`~compas_timber.fabrication.FrenchRidgeLap`

    """
    # type: (Beam, Beam, float, int) -> FrenchRidgeLap
    if isinstance(plane, Frame):
        plane = Plane.from_frame(plane)
    # the reference side of the beam to be cut
    ref_side = beam.ref_sides[ref_side_index]
    ref_surface = beam.side_as_surface(ref_side_index)

    # calculate the orientation of the cut
    orientation = cls._calculate_orientation(ref_side, plane)

    # calculate the angle of the cut
    angle = cls._calculate_angle(beam, other_beam, ref_side, orientation)

    # determine the reference position of the edge
    ref_position = cls._calculate_ref_position(beam, other_beam, ref_side, plane, angle)

    # calculate the start_x of the cut
    start_x = cls._calculate_start_x(ref_surface, orientation, angle)

    # define the drillhole
    drillhole = True if drillhole_diam else False
    drillhole_diam = 0.0 if not drillhole else drillhole_diam

    return cls(
        orientation,
        start_x,
        angle,
        ref_position,
        drillhole,
        drillhole_diam,
        ref_side_index=ref_side_index,
    )
lap_volume_from_params_and_beam ¤
lap_volume_from_params_and_beam(beam)

Calculates the trimming volume of the french ridge lap from the machining parameters and the given beam.

Parameters:

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

    The beam that is cut by this instance.

Returns:

  • class:`compas.geometry.Brep` –

    The trimming volume of the lap.

Source code in src/compas_timber/fabrication/french_ridge_lap.py
def lap_volume_from_params_and_beam(self, beam):
    """
    Calculates the trimming volume of the french ridge lap from the machining parameters and the given beam.

    Parameters
    ----------
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Returns
    -------
    :class:`compas.geometry.Brep`
        The trimming volume of the lap.
    """
    # type: (Beam) -> Brep
    assert self.orientation is not None
    assert self.angle is not None
    assert self.ref_position is not None
    assert self.drillhole is not None
    assert self.drillhole_diam is not None

    # get the reference side and the opposite side of the beam
    ref_side = beam.side_as_surface(self.ref_side_index)
    ref_edge = Line.from_point_and_vector(ref_side.point, ref_side.xaxis)
    opp_edge = ref_edge.translated(ref_side.yaxis * ref_side.ysize)

    # get the height of the beam and the edge length of the lap
    height = beam.get_dimensions_relative_to_side(self.ref_side_index)[1]
    edge_length = ref_side.ysize / math.sin(math.radians(self.angle))
    if self.orientation == OrientationType.END:
        edge_length = -edge_length

    # side cutting frames of the lap
    frame1 = self.frame_from_params_and_beam(beam)
    frame2 = frame1.translated(ref_side.xaxis * edge_length)
    frame2.xaxis = -frame2.xaxis

    # bottom vertices
    bottom_vertices = [
        Point(*intersection_line_plane(ref_edge, Plane.from_frame(frame1))),
        Point(*intersection_line_plane(ref_edge, Plane.from_frame(frame2))),
        Point(*intersection_line_plane(opp_edge, Plane.from_frame(frame2))),
        Point(*intersection_line_plane(opp_edge, Plane.from_frame(frame1))),
    ]
    # top vertices
    top_vertices = [v.translated(-ref_side.zaxis * (height / 2)) for v in bottom_vertices]
    # translate vertices to create the french ridge lap
    if self.ref_position == EdgePositionType.OPPEDGE:
        frl_indexes = [0, 2]
    else:
        frl_indexes = [3, 1]
    top_vertices[frl_indexes[0]].translate(ref_side.zaxis * (height / 6))
    top_vertices[frl_indexes[1]].translate(-ref_side.zaxis * (height / 6))

    # create the faces of the french ridge lap
    vertices = bottom_vertices + top_vertices
    faces = [
        [0, 1, 2, 3],  # Top face
        [4, 7, 6, 5],  # Bottom face
        [0, 4, 5, 1],  # Side face 1
        [1, 5, 6, 2],  # Side face 2
        [2, 6, 7, 3],  # Side face 3
        [3, 7, 4, 0],  # Side face 4
    ]

    # reverse face orientation if the orientation is END
    if self.orientation == OrientationType.END:
        faces = [face[::-1] for face in faces]

    # generate the subtraction volume and convert it to a Brep
    subtraction_volume_mesh = Mesh.from_vertices_and_faces(vertices, faces)
    subtraction_volume = Brep.from_mesh(subtraction_volume_mesh)

    # add drilling cylinder to the subtraction_volume
    if self.drillhole:
        diagonal = Line(bottom_vertices[0], bottom_vertices[2])
        drill_frame = Frame(diagonal.midpoint, -ref_side.xaxis, ref_side.yaxis)
        drill_cylinder = Cylinder(self.drillhole_diam / 2, height * 2, drill_frame)
        subtraction_volume = brep_union_first(subtraction_volume, Brep.from_cylinder(drill_cylinder))
    return subtraction_volume
scale ¤
scale(factor)

Scale the parameters of this processing by a given factor.

Notes

Only distances are scaled, angles remain unchanged.

Parameters:

  • factor (float) –

    The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

Source code in src/compas_timber/fabrication/french_ridge_lap.py
def scale(self, factor):
    """Scale the parameters of this processing by a given factor.

    Notes
    -----
    Only distances are scaled, angles remain unchanged.

    Parameters
    ----------
    factor : float
        The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

    """
    # type: (float) -> None
    self.start_x *= factor
    self.drillhole_diam *= factor

JackRafterCut ¤

JackRafterCut(
    orientation=START,
    start_x=0.0,
    start_y=0.0,
    start_depth=0.0,
    angle=90.0,
    inclination=90.0,
    **kwargs,
)

Represents a Jack Rafter Cut feature to be made on a beam.

Parameters:

  • orientation (int, default: START ) –

    The orientation of the cut. Must be either OrientationType.START or OrientationType.END.

  • start_x (float, default: 0.0 ) –

    The start x-coordinate of the cut in parametric space of the reference side. -100000.0 < start_x < 100000.0.

  • start_y (float, default: 0.0 ) –

    The start y-coordinate of the cut in parametric space of the reference side. 0.0 < start_y < 50000.0.

  • start_depth (float, default: 0.0 ) –

    The start depth of the cut. 0.0 < start_depth < 50000.0.

  • angle (float, default: 90.0 ) –

    The horizontal angle of the cut. 0.1 < angle < 179.9.

  • inclination (float, default: 90.0 ) –

    The vertical angle of the cut. 0.1 < inclination < 179.9.

Source code in src/compas_timber/fabrication/jack_cut.py
def __init__(self, orientation=OrientationType.START, start_x=0.0, start_y=0.0, start_depth=0.0, angle=90.0, inclination=90.0, **kwargs):
    super(JackRafterCut, self).__init__(**kwargs)
    self._orientation = orientation
    self._start_x = None
    self._start_y = None
    self._start_depth = None
    self._angle = None
    self._inclination = None

    self.orientation = orientation
    self.start_x = start_x
    self.start_y = start_y
    self.start_depth = start_depth
    self.angle = angle
    self.inclination = inclination

Methods:¤

apply ¤
apply(geometry, beam)

Apply the feature to the beam geometry.

Parameters:

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

    The beam geometry to be cut.

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

    The beam that is cut by this instance.

Raises:

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

    If the cutting plane does not intersect with beam geometry.

Returns:

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

    The resulting geometry after processing

Source code in src/compas_timber/fabrication/jack_cut.py
def apply(self, geometry, beam):
    """Apply the feature to the beam geometry.

    Parameters
    ----------
    geometry : :class:`~compas.geometry.Brep`
        The beam geometry to be cut.
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Raises
    ------
    :class:`~compas_timber.errors.FeatureApplicationError`
        If the cutting plane does not intersect with beam geometry.

    Returns
    -------
    :class:`~compas.geometry.Brep`
        The resulting geometry after processing

    """
    # type: (Brep, Beam) -> Brep
    cutting_plane = self.plane_from_params_and_beam(beam).transformed(beam.transformation_to_local())
    try:
        return geometry.trimmed(cutting_plane)
    except BrepTrimmingError:
        raise FeatureApplicationError(
            cutting_plane.transformed(beam.modeltransformation),
            geometry.transformed(beam.modeltransformation),
            "The cutting plane does not intersect with beam geometry.",
        )
from_plane_and_beam classmethod ¤
from_plane_and_beam(plane, beam, ref_side_index=0, **kwargs)

Create a JackRafterCut instance from a cutting plane and the beam it should cut.

Parameters:

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

    The cutting plane.

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

    The beam that is cut by this instance.

  • ref_side_index (int, default: 0 ) –

    The reference side index of the beam to be cut. Default is 0 (i.e. RS1). The cut is fully defined by plane, so any reference side yields an equivalent processing; pass an index to pin the resulting parameters to a specific side (e.g. for post-processing on a chosen face).

Returns:

  • class:`~compas_timber.fabrication.JackRafterCut` –
Source code in src/compas_timber/fabrication/jack_cut.py
@classmethod
def from_plane_and_beam(cls, plane, beam, ref_side_index=0, **kwargs):
    """Create a JackRafterCut instance from a cutting plane and the beam it should cut.

    Parameters
    ----------
    plane : :class:`~compas.geometry.Plane` or :class:`~compas.geometry.Frame`
        The cutting plane.
    beam : :class:`~compas_timber.elements.Beam`
        The beam that is cut by this instance.
    ref_side_index : int, optional
        The reference side index of the beam to be cut. Default is 0 (i.e. RS1).
        The cut is fully defined by ``plane``, so any reference side yields an
        equivalent processing; pass an index to pin the resulting parameters
        to a specific side (e.g. for post-processing on a chosen face).

    Returns
    -------
    :class:`~compas_timber.fabrication.JackRafterCut`

    """
    # type: (Plane | Frame, Beam, int) -> JackRafterCut
    if isinstance(plane, Frame):
        plane = Plane.from_frame(plane)
    start_y = 0.0
    start_depth = 0.0
    ref_side = beam.ref_sides[ref_side_index]
    ref_edge = Line.from_point_and_vector(ref_side.point, ref_side.xaxis)
    orientation = cls._calculate_orientation(beam, plane)
    point_start_x = intersection_line_plane(ref_edge, plane)
    if point_start_x is None:
        raise ValueError("Plane does not intersect with beam.")

    start_x = dot_vectors(Vector.from_start_end(ref_edge.point, point_start_x), ref_side.xaxis)
    angle = cls._calculate_angle(ref_side, plane, orientation)
    inclination = cls._calculate_inclination(ref_side, plane, orientation)
    return cls(orientation, start_x, start_y, start_depth, angle, inclination, ref_side_index=ref_side_index, **kwargs)
from_shapes_and_element classmethod ¤
from_shapes_and_element(plane, element, **kwargs)

Construct a drilling process from a shape and a beam.

Parameters:

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

    The cutting plane.

  • element (:class:`compas_timber.elements.Element`) –

    The element to be cut.

Returns:

  • class:`compas_timber.fabrication.JackRafterCut` –

    The constructed Jack Rafter Cut process.

Source code in src/compas_timber/fabrication/jack_cut.py
@classmethod
def from_shapes_and_element(cls, plane, element, **kwargs):
    """Construct a drilling process from a shape and a beam.

    Parameters
    ----------
    plane : :class:`compas.geometry.Plane` or :class:`compas.geometry.Frame`
        The cutting plane.
    element : :class:`compas_timber.elements.Element`
        The element to be cut.

    Returns
    -------
    :class:`compas_timber.fabrication.JackRafterCut`
        The constructed Jack Rafter Cut process.

    """
    if isinstance(plane, list):
        plane = plane[0]
    return cls.from_plane_and_beam(plane, element, **kwargs)
plane_from_params_and_beam ¤
plane_from_params_and_beam(beam)

Calculates the cutting plane from the machining parameters in this instance and the given beam

Parameters:

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

    The beam that is cut by this instance.

Returns:

  • class:`compas.geometry.Plane` –

    The cutting plane.

Source code in src/compas_timber/fabrication/jack_cut.py
def plane_from_params_and_beam(self, beam):
    """Calculates the cutting plane from the machining parameters in this instance and the given beam

    Parameters
    ----------
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Returns
    -------
    :class:`compas.geometry.Plane`
        The cutting plane.

    """
    # type: (Beam) -> Plane
    assert self.angle is not None
    assert self.inclination is not None

    # start with a plane aligned with the ref side but shifted to the start_x of the cut
    ref_side = beam.side_as_surface(self.ref_side_index)
    p_origin = planar_surface_point_at(ref_side, self.start_x, 0.0)
    cutting_plane = Frame(p_origin, ref_side.frame.xaxis, ref_side.frame.yaxis)

    # normal pointing towards xaxis so just need the delta
    if self.orientation == OrientationType.END:
        horizontal_angle = math.radians(90 - self.angle)
        vertical_angle = math.radians(90 - self.inclination)
    else:
        horizontal_angle = math.radians(self.angle - 90)
        vertical_angle = math.radians(self.inclination - 90)

    rot_a = Rotation.from_axis_and_angle(cutting_plane.zaxis, horizontal_angle, point=p_origin)
    rot_b = Rotation.from_axis_and_angle(cutting_plane.yaxis, vertical_angle, point=p_origin)

    cutting_plane.transform(rot_a * rot_b)
    # for simplicity, we always start with normal pointing towards xaxis.
    # if start is cut, we need to flip the normal
    if self.orientation == OrientationType.END:
        plane_normal = cutting_plane.xaxis
    else:
        plane_normal = -cutting_plane.xaxis
    return Plane(cutting_plane.point, plane_normal)
scale ¤
scale(factor)

Scale the parameters of this processing by a given factor.

Notes

Only distances are scaled, angles remain unchanged.

Parameters:

  • factor (float) –

    The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

Source code in src/compas_timber/fabrication/jack_cut.py
def scale(self, factor):
    """Scale the parameters of this processing by a given factor.

    Notes
    -----
    Only distances are scaled, angles remain unchanged.

    Parameters
    ----------
    factor : float
        The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

    """
    self._start_x *= factor
    self._start_y *= factor
    self._start_depth *= factor

JackRafterCutProxy ¤

JackRafterCutProxy(plane, beam, ref_side_index=0)

This object behaves like a JackRafterCut except it only calculates the machining parameters once unproxified. Can also be used to defer the creation of the processing instance until it is actually needed.

Until then, it can be used to visualize the machining operation. This slightly improves performance.

Parameters:

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

    The cutting plane.

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

    The beam that is cut by this instance.

  • ref_side_index (int, default: 0 ) –

    The reference side index of the beam to be cut. Default is 0 (i.e. RS1).

Source code in src/compas_timber/fabrication/jack_cut.py
def __init__(self, plane, beam, ref_side_index=0):
    self.plane = plane.transformed(beam.transformation_to_local())
    self.beam = beam
    self.ref_side_index = ref_side_index
    self._processing = None

Methods:¤

apply ¤
apply(geometry, beam)

Apply the feature to the beam geometry.

Parameters:

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

    The beam geometry to be cut.

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

    The beam that is cut by this instance.

Raises:

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

    If the cutting plane does not intersect with beam geometry.

Returns:

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

    The resulting geometry after processing

Source code in src/compas_timber/fabrication/jack_cut.py
def apply(self, geometry, beam):
    """Apply the feature to the beam geometry.

    Parameters
    ----------
    geometry : :class:`~compas.geometry.Brep`
        The beam geometry to be cut.
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Raises
    ------
    :class:`~compas_timber.errors.FeatureApplicationError`
        If the cutting plane does not intersect with beam geometry.

    Returns
    -------
    :class:`~compas.geometry.Brep`
        The resulting geometry after processing

    """
    # type: (Brep, Beam) -> Brep
    try:
        return geometry.trimmed(self.plane)
    except BrepTrimmingError:
        raise FeatureApplicationError(
            self.plane.transformed(beam.modeltransformation),
            geometry.transformed(beam.modeltransformation),
            "The cutting plane does not intersect with beam geometry.",
        )
from_plane_and_beam classmethod ¤
from_plane_and_beam(plane, beam, ref_side_index=0)

Create a JackRafterCutProxy instance from a cutting plane and the beam it should cut.

Parameters:

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

    The cutting plane.

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

    The beam that is cut by this instance.

  • ref_side_index (int, default: 0 ) –

    The reference side index of the beam to be cut. Default is 0 (i.e. RS1). The cut is fully defined by plane, so any reference side yields an equivalent processing; pass an index to pin the resulting parameters to a specific side (e.g. for post-processing on a chosen face).

Returns:

  • class:`~compas_timber.fabrication.JackRafterCutProxy` –
Source code in src/compas_timber/fabrication/jack_cut.py
@classmethod
def from_plane_and_beam(cls, plane, beam, ref_side_index=0):
    """Create a JackRafterCutProxy instance from a cutting plane and the beam it should cut.

    Parameters
    ----------
    plane : :class:`~compas.geometry.Plane` or :class:`~compas.geometry.Frame`
        The cutting plane.
    beam : :class:`~compas_timber.elements.Beam`
        The beam that is cut by this instance.
    ref_side_index : int, optional
        The reference side index of the beam to be cut. Default is 0 (i.e. RS1).
        The cut is fully defined by ``plane``, so any reference side yields an
        equivalent processing; pass an index to pin the resulting parameters
        to a specific side (e.g. for post-processing on a chosen face).

    Returns
    -------
    :class:`~compas_timber.fabrication.JackRafterCutProxy`

    """
    return cls(plane, beam, ref_side_index)
unproxified ¤
unproxified()

Returns the unproxified processing instance.

Returns:

  • class:`~compas_timber.fabrication.JackRafterCut` –
Source code in src/compas_timber/fabrication/jack_cut.py
def unproxified(self):
    """Returns the unproxified processing instance.

    Returns
    -------
    :class:`~compas_timber.fabrication.JackRafterCut`

    """
    if not self._processing:
        plane = self.plane.transformed(self.beam.modeltransformation)
        self._processing = JackRafterCut.from_plane_and_beam(plane, self.beam, self.ref_side_index)
    return self._processing

Lap ¤

Lap(
    orientation=START,
    start_x=0.0,
    start_y=0.0,
    angle=90.0,
    inclination=90.0,
    slope=0.0,
    length=200.0,
    width=50.0,
    depth=40.0,
    lead_angle_parallel=True,
    lead_angle=90.0,
    lead_inclination_parallel=True,
    lead_inclination=90.0,
    machining_limits=None,
    **kwargs,
)

Represents a Lap feature to be made on a beam.

Parameters:

  • orientation (int, default: START ) –

    The orientation of the cut. Must be either OrientationType.START or OrientationType.END.

  • start_x (float, default: 0.0 ) –

    The start x-coordinate of the cut in parametric space of the reference side. -100000.0 < start_x < 100000.0.

  • start_y (float, default: 0.0 ) –

    The start y-coordinate of the cut in parametric space of the reference side. -50000.0 < start_y < 50000.0.

  • angle (float, default: 90.0 ) –

    The horizontal angle of the cut. 0.1 < angle < 179.9.

  • inclination (float, default: 90.0 ) –

    The vertical angle of the cut. 0.1 < inclination < 179.9.

  • slope (float, default: 0.0 ) –

    The slope of the cut. -89.9 < slope < 89.9.

  • length (float, default: 200.0 ) –

    The length of the cut. 0.0 < length < 100000.0.

  • width (float, default: 50.0 ) –

    The width of the cut. 0.0 < width < 50000.0.

  • depth (float, default: 40.0 ) –

    The depth of the cut. -50000.0 < depth < 50000.0.

  • lead_angle_parallel (bool, default: True ) –

    The lead angle is parallel to the beam axis.

  • lead_angle (float, default: 90.0 ) –

    The lead angle of the cut. 0.1 < lead_angle < 179.9.

  • lead_inclination_parallel (bool, default: True ) –

    The lead inclination is parallel to the beam axis.

  • lead_inclination (float, default: 90.0 ) –

    The lead inclination of the cut. 0.1 < lead_inclination < 179.9.

  • machining_limits (:class:`~compas_timber.fabrication.btlx.MachiningLimits` or dict, default: None ) –

    The machining limits for the cut. Default is None

Source code in src/compas_timber/fabrication/lap.py
def __init__(
    self,
    orientation=OrientationType.START,
    start_x=0.0,
    start_y=0.0,
    angle=90.0,
    inclination=90.0,
    slope=0.0,
    length=200.0,
    width=50.0,
    depth=40.0,
    lead_angle_parallel=True,
    lead_angle=90.0,
    lead_inclination_parallel=True,
    lead_inclination=90.0,
    machining_limits=None,
    **kwargs
):
    super(Lap, self).__init__(**kwargs)
    self._orientation = None
    self._start_x = None
    self._start_y = None
    self._angle = None
    self._inclination = None
    self._slope = None
    self._length = None
    self._width = None
    self._depth = None
    self._lead_angle_parallel = None
    self._lead_angle = None
    self._lead_inclination_parallel = None
    self._lead_inclination = None
    self._machining_limits = None

    self.orientation = orientation
    self.start_x = start_x
    self.start_y = start_y
    self.angle = angle
    self.inclination = inclination
    self.slope = slope
    self.length = length
    self.width = width
    self.depth = depth
    self.lead_angle_parallel = lead_angle_parallel
    self.lead_angle = lead_angle
    self.lead_inclination_parallel = lead_inclination_parallel
    self.lead_inclination = lead_inclination
    self.machining_limits = machining_limits

Methods:¤

apply ¤
apply(geometry, beam)

Apply the feature to the beam geometry.

Parameters:

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

    The beam geometry to be cut.

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

    The beam that is cut by this instance.

Raises:

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

    If the cutting plane does not intersect with beam geometry.

Returns:

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

    The resulting geometry after processing

Source code in src/compas_timber/fabrication/lap.py
def apply(self, geometry, beam):
    """Apply the feature to the beam geometry.

    Parameters
    ----------
    geometry : :class:`~compas.geometry.Brep`
        The beam geometry to be cut.
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Raises
    ------
    :class:`~compas_timber.errors.FeatureApplicationError`
        If the cutting plane does not intersect with beam geometry.

    Returns
    -------
    :class:`~compas.geometry.Brep`
        The resulting geometry after processing

    """
    # type: (Brep, Beam) -> Brep
    lap_volume = self.volume_from_params_and_beam(beam)
    lap_volume.transform(beam.transformation_to_local())

    # convert mesh to brep
    try:
        lap_volume = Brep.from_mesh(lap_volume.to_mesh())
    except Exception:
        raise FeatureApplicationError(
            lap_volume.transformed(beam.modeltransformation),
            geometry.transformed(beam.modeltransformation),
            "Could not convert the lap volume to a Brep.",
        )

    # subtract the lap volume from the beam geometry
    try:
        return brep_difference_first(geometry, lap_volume)
    except IndexError:
        raise FeatureApplicationError(
            lap_volume.transformed(beam.modeltransformation),
            geometry.transformed(beam.modeltransformation),
            "The lap volume does not intersect with the beam geometry.",
        )
from_plane_and_beam classmethod ¤
from_plane_and_beam(plane, beam, length, depth, ref_side_index=0)

Create a Lap instance from a plane and a beam. The lap is defined by the plane given and a plane parallel to that at a distance defined by the length and a given depth. This method is used to create pocket cuts.

Parameters:

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

    The plane that defines the lap.

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

    The beam that the Lap instance is applied to.

  • length (float) –

    The length of the lap.

  • depth (float) –

    The depth of the lap.

  • ref_side_index (int, default: 0 ) –

    The reference side index of the main_beam to be cut. Default is 0 (i.e. RS1).

Returns:

  • class:`~compas_timber.fabrication.Lap` –
Source code in src/compas_timber/fabrication/lap.py
@classmethod
def from_plane_and_beam(cls, plane, beam, length, depth, ref_side_index=0):
    """Create a Lap instance from a plane and a beam. The lap is defined by the plane given and a plane parallel to that at a distance defined by the length and a given depth.
    This method is used to create pocket cuts.

    Parameters
    ----------
    plane : :class:`~compas.geometry.Plane`
        The plane that defines the lap.
    beam : :class:`~compas_timber.elements.Beam`
        The beam that the Lap instance is applied to.
    length : float
        The length of the lap.
    depth : float
        The depth of the lap.
    ref_side_index : int, optional
        The reference side index of the main_beam to be cut. Default is 0 (i.e. RS1).

    Returns
    -------
    :class:`~compas_timber.fabrication.Lap`

    """
    # type: (Plane, Beam, float, float, int) -> Lap
    if isinstance(plane, Frame):
        plane = Plane.from_frame(plane)

    # create an offset plane at the depth of the lap
    offset_plane = Plane(plane.point - plane.normal * length, -plane.normal)
    planes = [plane, offset_plane]

    # get ref_side, and ref_edge from the beam
    ref_side = beam.ref_sides[ref_side_index]
    ref_side_surface = beam.side_as_surface(ref_side_index)
    ref_edge = Line.from_point_and_vector(ref_side.point, ref_side.xaxis)

    # sort the planes based on the angle between their normals and the reference side's normal
    planes.sort(key=lambda plane: abs(angle_vectors_signed(ref_side.normal, plane.normal, ref_side.yaxis, deg=True)))

    # calculate the orientation of the lap
    orientation = cls._calculate_orientation(ref_side, planes[0])

    # calculate the start_x of the lap
    start_x = cls._calculate_start_x(ref_side, ref_edge, planes, orientation, depth)

    # calculate the angle of the lap
    angle = cls._calculate_angle(ref_side, planes[0])

    # calculate the inclination of the lap
    inclination = 90.0

    # calculate the slope of the lap
    slope = 0.0

    # calculate length
    length = cls._calculate_length(planes, ref_side, ref_edge, depth, angle)

    # define the width of the lap
    width = ref_side_surface.ysize

    # define machining limits
    machining_limits = MachiningLimits()
    machining_limits.face_limited_top = False
    machining_limits.face_limited_back = False
    machining_limits.face_limited_front = False

    return cls(orientation=orientation,
               start_x=start_x,
               angle=angle,
               inclination=inclination,
               slope=slope,
               length=length,
               width=width,
               depth=depth,
               machining_limits=machining_limits,
               ref_side_index=ref_side_index)
from_shapes_and_element classmethod ¤
from_shapes_and_element(volume, element, **kwargs)

Construct a Lap feature from a volume and a TimberElement.

Parameters:

  • volume (:class:`~compas.geometry.Polyhedron` or :class:`~compas.geometry.Brep` or :class:`~compas.geometry.Mesh`) –

    The volume of the Lap. Must have 6 faces.

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

    The element that is cut by this instance.

Returns:

  • class:`~compas_timber.fabrication.Lap` –

    The Lap feature.

Source code in src/compas_timber/fabrication/lap.py
@classmethod
def from_shapes_and_element(cls, volume, element, **kwargs):
    """Construct a Lap feature from a volume and a TimberElement.

    Parameters
    ----------
    volume : :class:`~compas.geometry.Polyhedron` or :class:`~compas.geometry.Brep` or :class:`~compas.geometry.Mesh`
        The volume of the Lap. Must have 6 faces.
    element : :class:`~compas_timber.elements.Beam`
        The element that is cut by this instance.

    Returns
    -------
    :class:`~compas_timber.fabrication.Lap`
        The Lap feature.

    """
    return cls.from_volume_and_beam(volume, element, **kwargs)
from_volume_and_beam classmethod ¤
from_volume_and_beam(
    volume, beam, machining_limits=None, ref_side_index=None, **kwargs
)

Construct a Lap feature from a volume and a Beam.

Parameters:

  • volume (:class:`~compas.geometry.Polyhedron` or :class:`~compas.geometry.Brep` or :class:`~compas.geometry.Mesh`) –

    The volume of the lap. Must have 6 faces.

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

    The beam that is cut by this instance.

  • machining_limits (:class:`~compas_timber.fabrication.MachiningLimits` or dict, default: None ) –

    The machining limits for the cut. Default is None.

  • ref_side_index (int, default: None ) –

    The index of the reference side of the element. Default is 0.

Returns:

  • class:`~compas_timber.fabrication.Lap` –

    The Lap feature.

Source code in src/compas_timber/fabrication/lap.py
@classmethod
def from_volume_and_beam(cls, volume, beam, machining_limits=None, ref_side_index=None, **kwargs):
    """Construct a Lap feature from a volume and a Beam.

    Parameters
    ----------
    volume : :class:`~compas.geometry.Polyhedron` or :class:`~compas.geometry.Brep` or :class:`~compas.geometry.Mesh`
        The volume of the lap. Must have 6 faces.
    beam : :class:`~compas_timber.elements.Beam`
        The beam that is cut by this instance.
    machining_limits : :class:`~compas_timber.fabrication.MachiningLimits` or dict
        The machining limits for the cut. Default is None.
    ref_side_index : int, optional
        The index of the reference side of the element. Default is 0.

    Returns
    -------
    :class:`~compas_timber.fabrication.Lap`
        The Lap feature.

    """
    # type: (Polyhedron | Brep | Mesh, Beam, dict, int) -> Lap
    if isinstance(volume, Mesh):
        planes = [volume.face_plane(i) for i in range(volume.number_of_faces())]
    elif isinstance(volume, Polyhedron):
        volume = volume.to_mesh()
        planes = [volume.face_plane(i) for i in range(volume.number_of_faces())]
    elif isinstance(volume, Brep):
        # not `face.surface`: it ignores `is_reversed`, and it is the face's own Plane,
        # so flipping the normal in place would mutate the volume we were handed.
        planes = [Plane.from_frame(face.frame_at()) for face in volume.faces]

    else:
        raise ValueError("Volume must be either a Mesh, Brep, or Polyhedron.")

    if len(planes) != 6:
        raise ValueError("Volume must have 6 faces.")

    # get ref_side of the
    if ref_side_index is None:
        ref_side_index = cls._get_optimal_ref_side_index(beam, volume)
    ref_side = beam.ref_sides[ref_side_index]

    # sort the planes based on the reference side
    planes = cls._sort_planes(planes, ref_side)
    start_plane, end_plane, front_plane, back_plane, bottom_plane, _ = planes

    # get the intersection points
    try:
        start_point = Point(*intersection_plane_plane_plane(start_plane, front_plane, Plane.from_frame(ref_side), tol=TOL.ABSOLUTE))
        bottom_point = Point(*intersection_plane_plane_plane(start_plane, front_plane, bottom_plane, tol=TOL.ABSOLUTE))
        back_point = Point (*intersection_plane_plane_plane(start_plane, back_plane, Plane.from_frame(ref_side), tol=TOL.ABSOLUTE))
        end_point = Point(*intersection_plane_plane_plane(end_plane, front_plane, Plane.from_frame(ref_side), tol=TOL.ABSOLUTE))
    except TypeError as te:
        raise ValueError("Failed to orient the volume to the element. Consider using a different ref_side_index " + str(te))

    # for simplicity, orientation is always set to START
    orientation = OrientationType.START

    # calculate start_x, start_y
    start_x, start_y = cls._calculate_start_x_y(ref_side, start_point)

    # x"-axis and y"-axis (planar axis of the top face of the volume)
    xxaxis = Vector.from_start_end(start_point, end_point)
    yyaxis = Vector.from_start_end(start_point, back_point)
    zzaxis = Vector.from_start_end(start_point, bottom_point)

    # calculate the angle of the lap
    angle = angle_vectors_signed(-yyaxis, ref_side.xaxis, ref_side.normal, deg=True)

    # calculate the inclination of the lap
    if TOL.is_zero(yyaxis.dot(zzaxis)) or TOL.is_zero(yyaxis.dot(front_plane.normal)):  # TODO: follow changes in compas.geometry and update this accordingly
        inclination = angle_vectors_signed(zzaxis, ref_side.xaxis, ref_side.normal, deg=True)
    else:
        inclination = angle_vectors_projected(zzaxis, front_plane.normal, yyaxis, deg=True)
    inclination = 180 + inclination if inclination < 0 else inclination

    # calculate the slope of the lap
    slope = angle_vectors_projected(-ref_side.normal, bottom_plane.normal, start_plane.normal, deg=True)

    # calculate length, width and depth
    length = distance_point_plane(start_plane.point, end_plane)
    width = abs(yyaxis.dot(ref_side.yaxis))
    depth = abs(zzaxis.dot(ref_side.zaxis))

    # define lead_angle and lead_inclination parallelity
    lead_angle_parallel = cls._check_lead_parallelity(beam, ref_side_index, front_plane, back_plane)
    lead_inclination_parallel = lead_angle_parallel

    if lead_angle_parallel:
        lead_angle = 90.0
    else:
        lead_angle = angle_vectors_signed(xxaxis, yyaxis, ref_side.normal, deg=True)

    if lead_inclination_parallel:
        lead_inclination = 90.0
    else:
        lead_inclination = angle_vectors_signed(front_plane.normal, bottom_plane.normal, ref_side.xaxis, deg=True)

    # define machining limits
    if not machining_limits:
        machining_limits = cls._define_machining_limits(planes, beam, ref_side_index)

    return cls(
        orientation,
        start_x,
        start_y,
        angle,
        inclination,
        slope,
        length,
        width,
        depth,
        lead_angle_parallel,
        lead_angle,
        lead_inclination_parallel,
        lead_inclination,
        machining_limits=machining_limits,
        ref_side_index=ref_side_index,
        **kwargs
    )
scale ¤
scale(factor)

Scale the parameters of this processing by a given factor.

Notes

Only distances are scaled, angles remain unchanged.

Parameters:

  • factor (float) –

    The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

Source code in src/compas_timber/fabrication/lap.py
def scale(self, factor):
    """Scale the parameters of this processing by a given factor.

    Notes
    -----
    Only distances are scaled, angles remain unchanged.

    Parameters
    ----------
    factor : float
        The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

    """
    self.start_x *= factor
    self.start_y *= factor
    self.length *= factor
    self.width *= factor
    self.depth *= factor
volume_from_params_and_beam ¤
volume_from_params_and_beam(beam)

Calculates the subtracting volume from the machining parameters in this instance and the given beam, ensuring correct face orientation.

Parameters:

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

    The beam that is cut by this instance.

Returns:

  • class:`compas.geometry.Polyhedron` –

    The correctly oriented subtracting volume of the lap.

Source code in src/compas_timber/fabrication/lap.py
def volume_from_params_and_beam(self, beam):
    """
    Calculates the subtracting volume from the machining parameters in this instance and the given beam, ensuring correct face orientation.

    Parameters
    ----------
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Returns
    -------
    :class:`compas.geometry.Polyhedron`
        The correctly oriented subtracting volume of the lap.
    """
    # type: Beam -> Polyhedron
    # Get cutting planes
    start_plane, end_plane, top_plane, bottom_plane, front_plane, back_plane = self._planes_from_params_and_beam(beam)

    # Calculate vertices using plane-plane-plane intersection
    vertices = [
        Point(*intersection_plane_plane_plane(start_plane, bottom_plane, front_plane)),     # v0
        Point(*intersection_plane_plane_plane(start_plane, bottom_plane, back_plane)),      # v1
        Point(*intersection_plane_plane_plane(end_plane, bottom_plane, back_plane)),        # v2
        Point(*intersection_plane_plane_plane(end_plane, bottom_plane, front_plane)),       # v3
        Point(*intersection_plane_plane_plane(start_plane, top_plane, front_plane)),        # v4
        Point(*intersection_plane_plane_plane(start_plane, top_plane, back_plane)),         # v5
        Point(*intersection_plane_plane_plane(end_plane, top_plane, back_plane)),           # v6
        Point(*intersection_plane_plane_plane(end_plane, top_plane, front_plane)),          # v7
    ]

    # define faces of the trimming volume
    # ensure vertices are defined in counter-clockwise order when viewed from the outside
    faces = [
        [0, 1, 2, 3],  # Bottom face
        [7, 6, 5, 4],  # Top face
        [4, 5, 1, 0],  # Start face
        [5, 6, 2, 1],  # Back face
        [6, 7, 3, 2],  # End face
        [7, 4, 0, 3],  # Front face
    ]

    # ensure proper vertex order based on orientation
    if self.orientation == OrientationType.END:
        faces = [face[::-1] for face in faces]
    return Polyhedron(vertices, faces)

LapProxy ¤

LapProxy(volume, beam, machining_limits=None, ref_side_index=None)

This object behaves like a Lap except it only calculates the machining parameters once unproxified. Can also be used to defer the creation of the processing instance until it is actually needed.

Until then, it can be used to visualize the machining operation. This slightly improves performance.

Parameters:

  • volume (:class:`~compas.geometry.Polyhedron` or :class:`~compas.geometry.Brep`) –

    The negative volume that constitutes the lap.

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

    The beam where lap should be applied.

  • machining_limits (dict, default: None ) –

    The machining limits for the cut. Default is None.

  • ref_side_index (int, default: None ) –

    The reference side index for the Lap.

Source code in src/compas_timber/fabrication/lap.py
def __init__(self, volume, beam, machining_limits=None, ref_side_index=None):
    self.volume = volume.transformed(beam.transformation_to_local())
    self.beam = beam
    self.machining_limits = machining_limits
    self.ref_side_index = ref_side_index
    self._processing = None

Methods:¤

apply ¤
apply(geometry, _)

Apply the feature to the beam geometry.

Parameters:

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

    The beam geometry to apply the lap to.

Raises:

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

    If the lap volume does not intersect with beam geometry.

Returns:

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

    The resulting geometry after processing

Source code in src/compas_timber/fabrication/lap.py
def apply(self, geometry, _):
    """Apply the feature to the beam geometry.

    Parameters
    ----------
    geometry : :class:`~compas.geometry.Brep`
        The beam geometry to apply the lap to.

    Raises
    ------
    :class:`~compas_timber.errors.FeatureApplicationError`
        If the lap volume does not intersect with beam geometry.

    Returns
    -------
    :class:`~compas.geometry.Brep`
        The resulting geometry after processing

    """
    # type: (Brep, Beam) -> Brep
    try:
        # TODO: this is a workaround for a visual artifact where a tiny sliver of the original geometry remains after subtraction.
        # NOTE: This is likely due to numerical precision issues in the boolean operation.
        # NOTE: Ideally we just make the volume construction in LLapJoint longer at the edges but that would likely require a rework
        # NOTE: the volume construction which seems significant.
        scaling_factor = 1 + TOL.approximation
        frame_at_centroid = Frame(self.volume.centroid, self.volume.frame.xaxis, self.volume.frame.yaxis)
        inflated_brep = self.volume.transformed(Scale.from_factors([scaling_factor, scaling_factor, scaling_factor], frame=frame_at_centroid))
        return brep_difference_first(geometry, inflated_brep)
    except IndexError:
        raise FeatureApplicationError(
            self.volume.transformed(self.beam.modeltransformation),
            geometry.transformed(self.beam.modeltransformation),
            "The volume to subtract does not intersect with beam geometry.",
        )
from_volume_and_beam classmethod ¤
from_volume_and_beam(volume, beam, machining_limits=None, ref_side_index=None)

Construct a Lap feature from a volume and a Beam.

Parameters:

  • volume (:class:`~compas.geometry.Polyhedron` or :class:`~compas.geometry.Brep`) –

    The volume of the lap. Must have 6 faces.

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

    The beam that is cut by this instance.

  • machining_limits (:class:`compas_timber.fabrication.MachiningLimits()` or dict, default: None ) –

    The machining limits for the cut. Default is None.

  • ref_side_index (int, default: None ) –

    The index of the reference side of the element. Default is 0.

Returns:

  • class:`~compas_timber.fabrication.Lap` –

    The Lap feature.

Source code in src/compas_timber/fabrication/lap.py
@classmethod
def from_volume_and_beam(cls, volume, beam, machining_limits=None, ref_side_index=None):
    """Construct a Lap feature from a volume and a Beam.

    Parameters
    ----------
    volume : :class:`~compas.geometry.Polyhedron` or :class:`~compas.geometry.Brep`
        The volume of the lap. Must have 6 faces.
    beam : :class:`~compas_timber.elements.Beam`
        The beam that is cut by this instance.
    machining_limits : :class:`compas_timber.fabrication.MachiningLimits()` or dict
        The machining limits for the cut. Default is None.
    ref_side_index : int, optional
        The index of the reference side of the element. Default is 0.

    Returns
    -------
    :class:`~compas_timber.fabrication.Lap`
        The Lap feature.

    """
    if isinstance(volume, Polyhedron):
        volume = Brep.from_mesh(volume.to_mesh())
    if volume.volume < 0:
        volume.flip()
    return cls(volume, beam, machining_limits, ref_side_index)
unproxified ¤
unproxified()

Returns the unproxified processing instance.

Returns:

  • class:`~compas_timber.fabrication.Lap` –
Source code in src/compas_timber/fabrication/lap.py
def unproxified(self):
    """Returns the unproxified processing instance.

    Returns
    -------
    :class:`~compas_timber.fabrication.Lap`

    """
    if not self._processing:
        volume = self.volume.transformed(self.beam.modeltransformation)
        self._processing = Lap.from_volume_and_beam(volume, self.beam, self.machining_limits, self.ref_side_index)
    return self._processing

LimitationTopType ¤

Enum for the top limitation of the cut.

Attributes:

  • LIMITED (literal(limited)) –

    Limitation to the cut.

  • UNLIMITED (literal(unlimited)) –

    No limit to the cut.

  • POCKET (literal(pocket)) –

    Pocket like limitation to the cut.

LongitudinalCut ¤

LongitudinalCut(
    start_x=0.0,
    start_y=0.0,
    inclination=40.0,
    start_limited=False,
    end_limited=False,
    length=0.0,
    depth_limited=False,
    depth=0.0,
    angle_start=90.0,
    angle_end=90.0,
    tool_position=LEFT,
    **kwargs,
)

Represents a Longitudinal Cut feature to be made on a beam.

Parameters:

  • start_x (float, default: 0.0 ) –

    The start x-coordinate of the cut in parametric space of the reference side. -100000.0 < start_x < 100000.0.

  • start_y (float, default: 0.0 ) –

    The start y-coordinate of the cut in parametric space of the reference side. 0.0 < start_y < 50000.0.

  • inclination (float, default: 40.0 ) –

    The vertical angle of the cut. -90.0 < inclination < 90.0.

  • start_limited (bool, default: False ) –

    Whether the cut is limited at the start. If True, the cut starts at the start_x coordinate.

  • end_limited (bool, default: False ) –

    Whether the cut is limited at the end. If True, the cut ends at the start_x + length coordinate.

  • length (float, default: 0.0 ) –

    The length of the cut in parametric space of the reference side. 0.0 < length < 100000.0.

  • depth_limited (bool, default: False ) –

    Whether the cut is limited in depth. If True, the cut goes to a certain depth.

  • depth (float, default: 0.0 ) –

    The depth of the cut in parametric space of the reference side. 0.0 < depth < 50000.0.

  • angle_start (float, default: 90.0 ) –

    The angle at the start of the cut in degrees. 0.1 < angle_start < 179.9.

  • angle_end (float, default: 90.0 ) –

    The angle at the end of the cut in degrees. 0.1 < angle_end < 179.9.

  • tool_position (:class:`~compas_timber.fabrication.AlignmentType`, default: LEFT ) –

    The position of the tool relative to the beam. Can be 'left', 'center', or 'right'.

Source code in src/compas_timber/fabrication/longitudinal_cut.py
def __init__(
    self,
    start_x=0.0,
    start_y=0.0,
    inclination=40.0,
    start_limited=False,
    end_limited=False,
    length=0.0,
    depth_limited=False,
    depth=0.0,
    angle_start=90.0,
    angle_end=90.0,
    tool_position=AlignmentType.LEFT,
    **kwargs
):
    super(LongitudinalCut, self).__init__(tool_position=tool_position, **kwargs)
    self._start_x = None
    self._start_y = None
    self._inclination = None
    self._start_limited = None
    self._end_limited = None
    self._length = None
    self._depth_limited = None
    self._depth = None
    self._angle_start = None
    self._angle_end = None

    self.start_x = start_x
    self.start_y = start_y
    self.inclination = inclination
    self.start_limited = start_limited
    self.end_limited = end_limited
    self.length = length
    self.depth_limited = depth_limited
    self.depth = depth
    self.angle_start = angle_start
    self.angle_end = angle_end

Methods:¤

apply ¤
apply(geometry, beam)

Apply the feature to the beam geometry.

Parameters:

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

    The beam geometry to be cut.

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

    The beam that is cut by this instance.

Raises:

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

    If the cutting plane does not intersect with beam geometry.

Returns:

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

    The resulting geometry after processing

Source code in src/compas_timber/fabrication/longitudinal_cut.py
def apply(self, geometry, beam):
    """Apply the feature to the beam geometry.

    Parameters
    ----------
    geometry : :class:`~compas.geometry.Brep`
        The beam geometry to be cut.
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Raises
    ------
    :class:`~compas_timber.errors.FeatureApplicationError`
        If the cutting plane does not intersect with beam geometry.

    Returns
    -------
    :class:`~compas.geometry.Brep`
        The resulting geometry after processing

    """
    # type: (Brep, Beam) -> Brep
    if not any([self.start_limited, self.end_limited, self.depth_limited]):
        # if the cut is not limited, trim the geometry with the cutting plane
        cutting_plane = self.plane_from_params_and_beam(beam)
        cutting_plane.transform(beam.transformation_to_local())
        try:
            return geometry.trimmed(cutting_plane)
        except BrepTrimmingError:
            raise FeatureApplicationError(
                cutting_plane.transformed(beam.modeltransformation),
                geometry.transformed(beam.modeltransformation),
                "The trimming operation failed. The cutting plane does not intersect with beam geometry.",
            )
    else:
        # if the cut is limited, calculate the negative volume representing the cut and subtract it from the geometry
        neg_vol = self.volume_from_params_and_beam(beam)
        neg_vol.transform(beam.transformation_to_local())
        try:
            return brep_difference_first(geometry, neg_vol)
        except IndexError:
            raise FeatureApplicationError(
                neg_vol.transformed(beam.modeltransformation),
                geometry.transformed(beam.modeltransformation),
                "The boolean difference between the cutting volume and the beam geometry failed.",
            )
from_plane_and_beam classmethod ¤
from_plane_and_beam(
    plane,
    beam,
    start_x=None,
    length=None,
    depth=None,
    angle_start=90.0,
    angle_end=90.0,
    tool_position=LEFT,
    ref_side_index=None,
    **kwargs,
)

Create a LongitudinalCut instance from a cutting plane and the beam it should cut.

Parameters:

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

    The cutting plane. The normal of the plane must be perpendicular to the beam's x-axis.

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

    The beam that is cut by this instance.

  • start_x (float, default: None ) –

    The start x-coordinate of the cut in parametric space of the reference side. Default is 0.0.

  • length (float, default: None ) –

    The length of the cut in parametric space of the reference side. Default is the minimum length so that the cut goes through the entire beam..

  • depth (float, default: None ) –

    The depth of the cut in parametric space of the reference side. Default is the minimum depth so that the cut goes through the entire beam.

  • angle_start (float, default: 90.0 ) –

    The chamfered angle at the start of the cut in degrees. Default is 90.0.

  • angle_end (float, default: 90.0 ) –

    The chanfered angle at the end of the cut in degrees. Default is 90.0.

  • tool_position (:class:`~compas_timber.fabrication.AlignmentType`, default: LEFT ) –

    The position of the tool relative to the beam. Can be 'left', 'center', or 'right'. Default is 'left'.

  • ref_side_index (int, default: None ) –

    The reference side index of the beam to be cut. The default ref_side_index is calculated based on the angle between the plane's normal and each ref_side's normal.

Source code in src/compas_timber/fabrication/longitudinal_cut.py
@classmethod
def from_plane_and_beam(
    cls, plane, beam, start_x=None, length=None, depth=None, angle_start=90.0, angle_end=90.0, tool_position=AlignmentType.LEFT, ref_side_index=None, **kwargs
):
    """Create a LongitudinalCut instance from a cutting plane and the beam it should cut.

    Parameters
    ----------
    plane : :class:`~compas.geometry.Plane` or :class:`~compas.geometry.Frame`
        The cutting plane. The normal of the plane must be perpendicular to the beam's x-axis.
    beam : :class:`~compas_timber.elements.Beam`
        The beam that is cut by this instance.
    start_x : float, optional
        The start x-coordinate of the cut in parametric space of the reference side. Default is 0.0.
    length : float, optional
        The length of the cut in parametric space of the reference side. Default is the minimum length so that the cut goes through the entire beam..
    depth : float, optional
        The depth of the cut in parametric space of the reference side. Default is the minimum depth so that the cut goes through the entire beam.
    angle_start : float, optional
        The chamfered angle at the start of the cut in degrees. Default is 90.0.
    angle_end : float, optional
        The chanfered angle at the end of the cut in degrees. Default is 90.0.
    tool_position : :class:`~compas_timber.fabrication.AlignmentType`, optional
        The position of the tool relative to the beam. Can be 'left', 'center', or 'right'. Default is 'left'.
    ref_side_index : int, optional
        The reference side index of the beam to be cut. The default ref_side_index is calculated based on the angle between the plane's normal and each ref_side's normal.

    """
    # type: (Plane | Frame, Beam, float, float, float, float, float, str, int)  -> LongitudinalCut
    if isinstance(plane, Frame):
        plane = Plane.from_frame(plane)

    # get the reference side index if not provided
    if ref_side_index is None:
        ref_side_index = cls._get_default_ref_side_index(plane, beam)
    ref_side = beam.ref_sides[ref_side_index]

    # check if the cutting plane's normal is perpendicular to the beam's x-axis
    if not TOL.is_zero(plane.normal.dot(ref_side.xaxis)):
        raise ValueError("The cutting plane's normal must be perpendicular to the beam's x-axis.")

    # calculate start_x
    start_x = 0.0 if start_x is None else start_x

    # calculate start_y
    start_edge = Line.from_point_and_vector(ref_side.point, ref_side.yaxis)
    point_start_y = intersection_line_plane(start_edge, plane)
    if point_start_y is None:
        raise ValueError("Plane is parallel to the ref_side. Consider trying a different ref_side_index")
    start_y = distance_point_point(ref_side.point, point_start_y)

    # calculate inclination
    inclination = angle_vectors_signed(ref_side.zaxis, plane.normal, ref_side.xaxis, deg=True)

    # calculate length
    length = beam.length - start_x if length is None else length

    # calculate start_limited and end_limited
    start_limited = not TOL.is_zero(start_x)
    end_limited = length + start_x < beam.length

    # calculate depth
    width, height = beam.get_dimensions_relative_to_side(ref_side_index)
    if abs(inclination) == 90.0:
        max_depth = height
    elif TOL.is_positive(inclination):
        max_depth = start_y * math.tan(math.radians(inclination))
    else:
        max_depth = abs((width - start_y) * math.tan(math.radians(inclination)))


    depth_limited = depth <= max_depth if depth is not None else False
    depth = 0.0 if depth is None else depth

    # calculate tool_position
    if TOL.is_negative(plane.normal.dot(ref_side.yaxis)):
        tool_position = AlignmentType.RIGHT
    else:
        tool_position = AlignmentType.LEFT

    return cls(
        start_x,
        start_y,
        inclination,
        start_limited,
        end_limited,
        length,
        depth_limited,
        depth,
        angle_start,
        angle_end,
        tool_position,
        ref_side_index=ref_side_index,
        **kwargs,
    )
from_shapes_and_element classmethod ¤
from_shapes_and_element(plane, element, **kwargs)

Construct a Longitudinal Cut process from a shape and a beam.

Parameters:

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

    The cutting plane.

  • element (:class:`compas_timber.elements.Element`) –

    The element to be cut.

Returns:

  • class:`compas_timber.fabrication.LongitudinalCut` –

    The constructed Logitudinal Cut process.

Source code in src/compas_timber/fabrication/longitudinal_cut.py
@classmethod
def from_shapes_and_element(cls, plane, element, **kwargs):
    """Construct a Longitudinal Cut process from a shape and a beam.

    Parameters
    ----------
    plane : :class:`compas.geometry.Plane` or :class:`compas.geometry.Frame`
        The cutting plane.
    element : :class:`compas_timber.elements.Element`
        The element to be cut.

    Returns
    -------
    :class:`compas_timber.fabrication.LongitudinalCut`
        The constructed Logitudinal Cut process.

    """
    if isinstance(plane, list):
        plane = plane[0]
    return cls.from_plane_and_beam(plane, element, **kwargs)
plane_from_params_and_beam ¤
plane_from_params_and_beam(beam)

Calculates the cutting plane from the machining parameters in this instance and the given beam

Parameters:

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

    The beam that is cut by this instance.

Returns:

  • class:`compas.geometry.Plane` –

    The cutting plane plane.

Source code in src/compas_timber/fabrication/longitudinal_cut.py
def plane_from_params_and_beam(self, beam):
    """Calculates the cutting plane from the machining parameters in this instance and the given beam

    Parameters
    ----------
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Returns
    -------
    :class:`compas.geometry.Plane`
        The cutting plane plane.

    """
    # type: (Beam) -> Frame
    assert self.start_x is not None
    assert self.start_y is not None
    assert self.inclination is not None

    ref_side = beam.side_as_surface(self.ref_side_index)
    p_origin = planar_surface_point_at(ref_side, self.start_x, self.start_y)

    frame = Frame(p_origin, ref_side.xaxis, ref_side.yaxis)
    frame.rotate(math.radians(self.inclination), ref_side.xaxis, p_origin)

    return Plane.from_frame(frame)
scale ¤
scale(factor)

Scale the machining parameters of the Longitudinal Cut feature.

Parameters:

  • factor (float) –

    The scale factor.

Source code in src/compas_timber/fabrication/longitudinal_cut.py
def scale(self, factor):
    """Scale the machining parameters of the Longitudinal Cut feature.

    Parameters
    ----------
    factor : float
        The scale factor.

    """
    self.start_x *= factor
    self.start_y *= factor
    self.length *= factor
    self.depth *= factor
volume_from_params_and_beam ¤
volume_from_params_and_beam(beam)

Calculates the negative volume representing the cut from the machining parameters in this instance and the given beam.

Parameters:

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

    The beam that is cut by this instance.

Returns:

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

    The negative volume representing the cut.

Source code in src/compas_timber/fabrication/longitudinal_cut.py
def volume_from_params_and_beam(self, beam):
    """Calculates the negative volume representing the cut from the machining parameters in this instance and the given beam.

    Parameters
    ----------
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Returns
    -------
    :class:`~compas.geometry.Brep`
        The negative volume representing the cut.

    """
    # type: (Beam) -> Brep
    assert self.start_y is not None
    assert self.length is not None
    assert self.inclination is not None
    assert self.depth is not None
    assert self.angle_start is not None
    assert self.angle_end is not None

    # get the cutting plane from the parameters and the beam
    plane = self.plane_from_params_and_beam(beam)

    # calculate the start and end points of the top edge of the cut
    xaxis = beam.frame.xaxis
    p_start = plane.point
    p_end = p_start + xaxis * self.length

    # calculate the start_y position relative to the reference side
    width, height = beam.get_dimensions_relative_to_side(self.ref_side_index)
    start_y = self.start_y
    if TOL.is_negative(self.inclination):
        start_y -= width + TOL.approximation  # adjust start_y for negative inclination

    # calculate the start and end points of the bottom edge of the cut
    extr_length = start_y / math.cos(math.radians(self.inclination))
    if TOL.is_close(abs(self.inclination), 90.0):
        extr_length = height  # avoid infinite extrusion length
    start_vector = xaxis.rotated(math.radians(self.angle_start), plane.normal, p_start)
    p_angle_start = p_start - start_vector * extr_length

    end_vector = xaxis.rotated(math.radians(180 - self.angle_end), plane.normal, p_end)
    p_angle_end = p_end - end_vector * extr_length

    # create a polyline that represents the cut
    polyline = Polyline([p_start, p_angle_start, p_angle_end, p_end, p_start])
    polyline.scale(1 + TOL.relative)  # tolerance issue

    # create the negative volume by extruding the polyline along the frame's normal
    extr_vect = plane.normal * math.sin(math.radians(self.inclination)) * start_y
    neg_vol = Brep.from_extrusion(polyline, extr_vect)
    if TOL.is_negative(neg_vol.volume):
        neg_vol.flip()
    return neg_vol

LongitudinalCutProxy ¤

LongitudinalCutProxy(
    plane,
    beam,
    start_x=None,
    length=None,
    depth=None,
    angle_start=90.0,
    angle_end=90.0,
    tool_position=LEFT,
    ref_side_index=None,
    **kwargs,
)

This object behaves like a LongitudinalCut except it only calculates the machining parameters once unproxified. Can also be used to defer the creation of the processing instance until it is actually needed.

Until then, it can be used to visualize the machining operation. This slightly improves performance.

Parameters:

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

    The cutting plane.

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

    The beam that is cut by this instance.

  • start_x (float, default: None ) –

    The start x-coordinate of the cut in parametric space of the reference side. Default is 0.0.

  • length (float, default: None ) –

    The length of the cut in parametric space of the reference side. Default is the minimum length so that the cut goes through the entire beam.

  • depth (float, default: None ) –

    The depth of the cut in parametric space of the reference side. Default is the minimum depth so that the cut goes through the entire beam.

  • angle_start (float, default: 90.0 ) –

    The chamfered angle at the start of the cut in degrees. Default is 90.0.

  • angle_end (float, default: 90.0 ) –

    The chamfered angle at the end of the cut in degrees. Default is 90.0.

  • tool_position (:class:`~compas_timber.fabrication.AlignmentType`, default: LEFT ) –

    The position of the tool relative to the beam. Can be 'left', 'center', or 'right'. Default is 'left'.

  • ref_side_index (int, default: None ) –

    The reference side index of the beam to be cut. The default ref_side_index is calculated based on the angle between the plane's normal and each ref_side's normal.

Source code in src/compas_timber/fabrication/longitudinal_cut.py
def __init__(self, plane, beam, start_x=None, length=None, depth=None, angle_start=90.0, angle_end=90.0, tool_position=AlignmentType.LEFT, ref_side_index=None, **kwargs):
    self.plane = plane.transformed(beam.transformation_to_local())
    self.beam = beam
    self.start_x = start_x
    self.length = length
    self.depth = depth
    self.angle_start = angle_start
    self.angle_end = angle_end
    self.tool_position = tool_position
    self.ref_side_index = ref_side_index
    self._processing = None
    self.kwargs = kwargs

Methods:¤

apply ¤
apply(geometry, _)

Apply the feature to the beam geometry. The resulting geometry might differ from the unproxified version, based on the parameters set in this instance.

Parameters:

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

    The beam geometry to be cut.

Raises:

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

    If the cutting plane does not intersect with beam geometry.

Returns:

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

    The resulting geometry after processing.

Source code in src/compas_timber/fabrication/longitudinal_cut.py
def apply(self, geometry, _):
    """Apply the feature to the beam geometry.
    The resulting geometry might differ from the unproxified version, based on the parameters set in this instance.

    Parameters
    ----------
    geometry : :class:`~compas.geometry.Brep`
        The beam geometry to be cut.

    Raises
    ------
    :class:`~compas_timber.errors.FeatureApplicationError`
        If the cutting plane does not intersect with beam geometry.

    Returns
    -------
    :class:`~compas.geometry.Brep`
        The resulting geometry after processing.

    """
    try:
        # TODO: add geometry implementation for cuts that don't go full length of beam
        return geometry.trimmed(self.plane)
    except BrepTrimmingError:
        raise FeatureApplicationError(
            self.plane.transformed(self.beam.modeltransformation),
            geometry.transformed(self.beam.modeltransformation),
            "The trimming operation failed. The cutting plane does not intersect with beam geometry.",
        )
from_plane_and_beam classmethod ¤
from_plane_and_beam(
    plane,
    beam,
    start_x=None,
    length=None,
    depth=None,
    angle_start=90.0,
    angle_end=90.0,
    tool_position=LEFT,
    ref_side_index=None,
    **kwargs,
)

Create a LongitudinalCutProxy instance from a cutting plane and the beam it should cut.

Parameters:

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

    The cutting plane.

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

    The beam that is cut by this instance.

  • start_x (float, default: None ) –

    The start x-coordinate of the cut in parametric space of the reference side. Default is 0.0.

  • length (float, default: None ) –

    The length of the cut in parametric space of the reference side. Default is the minimum length so that the cut goes through the entire beam.

  • depth (float, default: None ) –

    The depth of the cut in parametric space of the reference side. Default is the minimum depth so that the cut goes through the entire beam.

  • angle_start (float, default: 90.0 ) –

    The chamfered angle at the start of the cut in degrees. Default is 90.0.

  • angle_end (float, default: 90.0 ) –

    The chamfered angle at the end of the cut in degrees. Default is 90.0.

  • tool_position (:class:`~compas_timber.fabrication.AlignmentType`, default: LEFT ) –

    The position of the tool relative to the beam. Can be 'left', 'center', or 'right'. Default is 'left'.

  • ref_side_index (int, default: None ) –

    The reference side index of the beam to be cut. The default ref_side_index is calculated based on the angle between the plane's normal and each ref_side's normal.

Returns:

  • class:`~compas_timber.fabrication.LongitudinalCutProxy` –
Source code in src/compas_timber/fabrication/longitudinal_cut.py
@classmethod
def from_plane_and_beam(
    cls, plane, beam, start_x=None, length=None, depth=None, angle_start=90.0, angle_end=90.0, tool_position=AlignmentType.LEFT, ref_side_index=None, **kwargs
):
    """Create a LongitudinalCutProxy instance from a cutting plane and the beam it should cut.

    Parameters
    ----------
    plane : :class:`~compas.geometry.Plane` or :class:`~compas.geometry.Frame`
        The cutting plane.
    beam : :class:`~compas_timber.elements.Beam`
        The beam that is cut by this instance.
    start_x : float, optional
        The start x-coordinate of the cut in parametric space of the reference side. Default is 0.0.
    length : float, optional
        The length of the cut in parametric space of the reference side. Default is the minimum length so that the cut goes through the entire beam.
    depth : float, optional
        The depth of the cut in parametric space of the reference side. Default is the minimum depth so that the cut goes through the entire beam.
    angle_start : float, optional
        The chamfered angle at the start of the cut in degrees. Default is 90.0.
    angle_end : float, optional
        The chamfered angle at the end of the cut in degrees. Default is 90.0.
    tool_position : :class:`~compas_timber.fabrication.AlignmentType`, optional
        The position of the tool relative to the beam. Can be 'left', 'center', or 'right'. Default is 'left'.
    ref_side_index : int, optional
        The reference side index of the beam to be cut. The default ref_side_index is calculated based on the angle between the plane's normal and each ref_side's normal.

    Returns
    -------
    :class:`~compas_timber.fabrication.LongitudinalCutProxy`

    """
    if isinstance(plane, Frame):
        plane = Plane.from_frame(plane)
    return cls(plane, beam, start_x, length, depth, angle_start, angle_end, tool_position, ref_side_index, **kwargs)
unproxified ¤
unproxified()

Returns the unproxified processing instance.

Returns:

  • class:`~compas_timber.fabrication.LongitudinalCut` –

    The unproxified LongitudinalCut instance.

Source code in src/compas_timber/fabrication/longitudinal_cut.py
def unproxified(self):
    """Returns the unproxified processing instance.

    Returns
    -------
    :class:`~compas_timber.fabrication.LongitudinalCut`
        The unproxified LongitudinalCut instance.

    """
    if not self._processing:
        plane = self.plane.transformed(self.beam.modeltransformation)
        self._processing = LongitudinalCut.from_plane_and_beam(
            plane,
            self.beam,
            self.start_x,
            self.length,
            self.depth,
            self.angle_start,
            self.angle_end,
            self.tool_position,
            self.ref_side_index,
            **self.kwargs,
        )
    return self._processing

MachiningLimits ¤

MachiningLimits(
    face_limited_start: bool = True,
    face_limited_end: bool = True,
    face_limited_front: bool = True,
    face_limited_back: bool = True,
    face_limited_top: bool = True,
    face_limited_bottom: bool = True,
)

Configuration class for the machining limits of the cut.

Attributes:

  • EXPECTED_KEYS (set) –

    The expected keys for the limits dictionary.

  • face_limited_start (bool) –

    Limit the start face.

  • face_limited_end (bool) –

    Limit the end face.

  • face_limited_front (bool) –

    Limit the front face.

  • face_limited_back (bool) –

    Limit the back face.

  • face_limited_top (bool) –

    Limit the top face.

  • face_limited_bottom (bool) –

    Limit the bottom face.

Source code in src/compas_timber/fabrication/btlx.py
def __init__(
    self,
    face_limited_start: bool = True,
    face_limited_end: bool = True,
    face_limited_front: bool = True,
    face_limited_back: bool = True,
    face_limited_top: bool = True,
    face_limited_bottom: bool = True,
):
    self.face_limited_start = face_limited_start
    self.face_limited_end = face_limited_end
    self.face_limited_front = face_limited_front
    self.face_limited_back = face_limited_back
    self.face_limited_top = face_limited_top
    self.face_limited_bottom = face_limited_bottom

Methods:¤

as_dict ¤
as_dict()

Dynamically generate the limits dictionary with boolean values from instance attributes.

Source code in src/compas_timber/fabrication/btlx.py
def as_dict(self):
    """Dynamically generate the limits dictionary with boolean values from instance attributes."""
    return {
        "FaceLimitedStart": self.face_limited_start,
        "FaceLimitedEnd": self.face_limited_end,
        "FaceLimitedFront": self.face_limited_front,
        "FaceLimitedBack": self.face_limited_back,
        "FaceLimitedTop": self.face_limited_top,
        "FaceLimitedBottom": self.face_limited_bottom,
    }

Mortise ¤

Mortise(
    start_x=0.0,
    start_y=50.0,
    start_depth=0.0,
    angle=0.0,
    slope=90.0,
    inclination=90.0,
    length_limited_top=True,
    length_limited_bottom=True,
    length=80.0,
    width=40.0,
    depth=28.0,
    shape=AUTOMATIC,
    shape_radius=20.0,
    **kwargs,
)

Represents a Mortise feature to be made on a beam.

Parameters:

  • start_x (float, default: 0.0 ) –

    The start x-coordinate of the cut in parametric space of the reference side. Distance from the beam start to the reference point. -100000.0 < start_x < 100000.0.

  • start_y (float, default: 50.0 ) –

    The start y-coordinate of the cut in parametric space of the reference side. Distance from the reference edge to the reference point. -5000.0 < start_y < 5000.0.

  • start_depth (float, default: 0.0 ) –

    The start depth of the cut in parametric space of the reference side. Margin on the reference side. 0.0 < start_depth < 5000.0.

  • angle (float, default: 0.0 ) –

    The angle of the cut. Angle between edge and reference edge. -180.0 < angle < 180.0.

  • slope (float, default: 90.0 ) –

    The slope of the cut. Angle between axis along the length of the mortise and rederence side. 0.1 < slope < 179.9.

  • inclination (float, default: 90.0 ) –

    The inclination of the cut. Angle between axis along the width of the mortise and rederence side. 0.1 < inclination < 179.9.

  • length_limited_top (bool, default: True ) –

    Whether the top length of the cut is limited. True or False.

  • length_limited_bottom (bool, default: True ) –

    Whether the bottom length of the cut is limited. True or False.

  • length (float, default: 80.0 ) –

    The length of the cut. 0.0 < length < 5000.0.

  • width (float, default: 40.0 ) –

    The width of the cut. 0.0 < width < 1000.0.

  • depth (float, default: 28.0 ) –

    The depth of the mortise. 0.0 < depth < 1000.0.

  • shape (str, default: AUTOMATIC ) –

    The shape of the cut. Must be either 'automatic', 'square', 'round', 'rounded', or 'radius'.

  • shape_radius (float, default: 20.0 ) –

    The radius of the shape of the cut. 0.0 < shape_radius < 1000.0.

Source code in src/compas_timber/fabrication/mortise.py
def __init__(
    self,
    start_x=0.0,
    start_y=50.0,
    start_depth=0.0,
    angle=0.0,
    slope=90.0,
    inclination=90.0,
    length_limited_top=True,
    length_limited_bottom=True,
    length=80.0,
    width=40.0,
    depth=28.0,
    shape=TenonShapeType.AUTOMATIC,
    shape_radius=20.0,
    **kwargs
):
    super(Mortise, self).__init__(**kwargs)
    self._start_x = None
    self._start_y = None
    self._start_depth = None
    self._angle = None
    self._slope = None
    self._inclination = None
    self._length_limited_top = None
    self._length_limited_bottom = None
    self._length = None
    self._width = None
    self._depth = None
    self._shape = None
    self._shape_radius = None

    self.start_x = start_x
    self.start_y = start_y
    self.start_depth = start_depth
    self.angle = angle
    self.slope = slope
    self.inclination = inclination
    self.length_limited_top = length_limited_top
    self.length_limited_bottom = length_limited_bottom
    self.length = length
    self.width = width
    self.depth = depth
    self.shape = shape
    self.shape_radius = shape_radius

Methods:¤

apply ¤
apply(geometry, beam)

Apply the feature to the beam geometry.

Parameters:

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

    The geometry to be processed.

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

    The beam that is milled by this instance.

Raises:

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

    If the cutting planes do not create a volume that itersects with beam geometry or any step fails.

Returns:

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

    The resulting geometry after processing

Source code in src/compas_timber/fabrication/mortise.py
def apply(self, geometry, beam):
    """Apply the feature to the beam geometry.

    Parameters
    ----------
    geometry : :class:`compas.geometry.Brep`
        The geometry to be processed.

    beam : :class:`compas_timber.elements.Beam`
        The beam that is milled by this instance.

    Raises
    ------
    :class:`~compas_timber.errors.FeatureApplicationError`
        If the cutting planes do not create a volume that itersects with beam geometry or any step fails.

    Returns
    -------
    :class:`~compas.geometry.Brep`
        The resulting geometry after processing

    """
    # type: (Brep, Beam) -> Brep

    # get mortise volume from params and beam
    try:
        mortise_volume = self.volume_from_params_and_beam(beam)
    except ValueError as e:
        raise FeatureApplicationError(
            None, geometry.transformed(beam.modeltransformation), "Failed to generate mortise volume from parameters and beam: {}".format(str(e))
        )

    # fillet the edges of the mortise volume based on the shape
    if self.shape != TenonShapeType.SQUARE:
        try:
            edges = mortise_volume.edges[:8]
            mortise_volume.fillet(self.shape_radius, edges)
        except Exception as e:
            # mortise_volume is still in model space here (not yet localized below), geometry is local
            raise FeatureApplicationError(
                mortise_volume,
                geometry.transformed(beam.modeltransformation),
                "Failed to fillet the edges of the tenon volume based on the shape: {}".format(str(e)),
            )
    # transform mortise volume to local coordinates of the beam
    mortise_volume.transform(beam.transformation_to_local())

    # remove tenon volume to geometry
    try:
        return brep_difference_first(geometry, mortise_volume)
    except Exception as e:
        raise FeatureApplicationError(
            mortise_volume.transformed(beam.modeltransformation),
            geometry.transformed(beam.modeltransformation),
            "Failed to remove mortise volume from geometry: {}".format(str(e)),
        )
frame_from_params_and_beam ¤
frame_from_params_and_beam(beam)

Calculates the cutting frame from the machining parameters in this instance and the given beam

Parameters:

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

    The beam that is cut by this instance.

Returns:

  • class:`compas.geometry.Frame` –

    The cutting frame.

Source code in src/compas_timber/fabrication/mortise.py
def frame_from_params_and_beam(self, beam):
    """Calculates the cutting frame from the machining parameters in this instance and the given beam

    Parameters
    ----------
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Returns
    -------
    :class:`compas.geometry.Frame`
        The cutting frame.

    """
    # type: (Beam) -> Frame
    assert self.angle is not None
    assert self.inclination is not None

    # start with a plane aligned with the ref side but shifted to the start_x of the cut
    ref_side = beam.side_as_surface(self.ref_side_index)
    p_origin = planar_surface_point_at(ref_side, self.start_x, self.start_y)
    cutting_frame = Frame(p_origin, ref_side.frame.xaxis, ref_side.frame.yaxis)

    # rotate the cutting frame based on the angle
    angle_rotation = Rotation.from_axis_and_angle(
        cutting_frame.normal, math.radians(self.angle + 90), cutting_frame.point
    )
    cutting_frame.transform(angle_rotation)
    return cutting_frame
from_frame_and_beam classmethod ¤
from_frame_and_beam(
    frame,
    beam,
    start_depth=0.0,
    length=80.0,
    width=40.0,
    depth=28.0,
    shape=AUTOMATIC,
    shape_radius=20.0,
    ref_side_index=0,
)

Create a Mortise instance from a cutting frame and the beam it should cut. This could be the ref_side of the main beam of a Joint and the cross beam.

Parameters:

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

    The cutting frame.

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

    The beam that is cut by this instance.

  • start_depth (float, default: 0.0 ) –

    The start depth of the cut along the y-axis of the beam. This offset is to be used in case of housing. Default is 0.0.

  • length (float, default: 80.0 ) –

    The length of the mortise.

  • width (float, default: 40.0 ) –

    The width of the mortise.

  • depth (float, default: 28.0 ) –

    The depth of the mortise. The equivalent value of the Tenon BTLxProcessing is the height.

  • shape (str, default: AUTOMATIC ) –

    The shape of the mortise in regards to it's edges. Default is 'automatic'.

  • shape_radius (float, default: 20.0 ) –

    The radius of the shape of the mortise. Default is 20.0.

  • ref_side_index (int, default: 0 ) –

    The reference side index of the beam to be cut. Default is 0 (i.e. RS1).

Returns:

  • class:`~compas_timber.fabrication.Mortise` –
Source code in src/compas_timber/fabrication/mortise.py
@classmethod
def from_frame_and_beam(
    cls,
    frame,
    beam,
    start_depth=0.0,
    length=80.0,
    width=40.0,
    depth=28.0,
    shape=TenonShapeType.AUTOMATIC,
    shape_radius=20.0,
    ref_side_index=0,
):
    """Create a Mortise instance from a cutting frame and the beam it should cut. This could be the ref_side of the main beam of a Joint and the cross beam.

    Parameters
    ----------
    frame : :class:`~compas.geometry.Frame` or :class:`~compas.geometry.Plane`
        The cutting frame.
    beam : :class:`~compas_timber.elements.Beam`
        The beam that is cut by this instance.
    start_depth : float, optional
        The start depth of the cut along the y-axis of the beam. This offset is to be used in case of housing. Default is 0.0.
    length : float, optional
        The length of the mortise.
    width : float, optional
        The width of the mortise.
    depth : float, optional
        The depth of the mortise. The equivalent value of the Tenon BTLxProcessing is the height.
    shape : str, optional
        The shape of the mortise in regards to it's edges. Default is 'automatic'.
    shape_radius : float, optional
        The radius of the shape of the mortise. Default is 20.0.
    ref_side_index : int, optional
        The reference side index of the beam to be cut. Default is 0 (i.e. RS1).

    Returns
    -------
    :class:`~compas_timber.fabrication.Mortise`

    """
    # type: (Frame|Plane, Beam, float, float, float, float, float, str, float, int) -> Mortise

    if isinstance(frame, Plane):
        frame = Frame.from_plane(frame)

    # define ref_side & ref_edge
    ref_side = beam.ref_sides[ref_side_index]
    ref_edge = Line.from_point_and_vector(ref_side.point, ref_side.xaxis)

    # calculate orientation
    orientation = cls._calculate_orientation(ref_side, frame)

    # calclulate start_x and start_y
    start_x = cls._calculate_start_x(ref_side, ref_edge, frame)
    start_y = cls._calculate_start_y(ref_side, frame)

    # define angle
    angle = cls._calculate_angle(ref_side, frame, orientation)

    # define slope and inclination
    # TODO: In which cases do you want indiferent slope and inclination?
    slope = 90.0
    inclination = 90.0

    # determine if the top and bottom length of the cut is limited
    length_limited_top = True
    length_limited_bottom = True

    return cls(
        start_x,
        start_y,
        start_depth,
        angle,
        slope,
        inclination,
        length_limited_top,
        length_limited_bottom,
        length,
        width,
        depth,
        shape,
        shape_radius,
        ref_side_index=ref_side_index,
    )
scale ¤
scale(factor)

Scale the parameters of this processing by a given factor.

Notes

Only distances are scaled, angles remain unchanged.

Parameters:

  • factor (float) –

    The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

Source code in src/compas_timber/fabrication/mortise.py
def scale(self, factor):
    """Scale the parameters of this processing by a given factor.

    Notes
    -----
    Only distances are scaled, angles remain unchanged.

    Parameters
    ----------
    factor : float
        The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

    """
    self.start_x *= factor
    self.start_y *= factor
    self.start_depth *= factor
    self.length *= factor
    self.width *= factor
    self.depth *= factor
    self.shape_radius *= factor
volume_from_params_and_beam ¤
volume_from_params_and_beam(beam)

Calculates the mortise volume from the machining parameters in this instance and the given beam.

Parameters:

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

    The beam that is cut by this instance.

Returns:

  • class:`compas.geometry.Brep` –

    The mortise volume.

Source code in src/compas_timber/fabrication/mortise.py
def volume_from_params_and_beam(self, beam):
    """Calculates the mortise volume from the machining parameters in this instance and the given beam.

    Parameters
    ----------
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Returns
    -------
    :class:`compas.geometry.Brep`
        The mortise volume.

    """
    # type: (Beam) -> Brep
    assert self.length is not None
    assert self.width is not None
    assert self.depth is not None

    cutting_frame = self.frame_from_params_and_beam(beam)
    # translate the cutting frame to the center of the mortise
    translation_vector = (-cutting_frame.normal * self.depth - cutting_frame.yaxis * self.length)
    cutting_frame.translate(translation_vector * 0.5)

    # get the tenon as a box
    tenon_box = Box(self.width, self.length, self.depth, cutting_frame)
    return Brep.from_box(tenon_box)

OrientationType ¤

Enum for the orientation of the cut.

Attributes:

  • START (literal(start)) –

    The start of the element is cut away.

  • END (literal(end)) –

    The end of the element is cut away.

Pocket ¤

Pocket(
    start_x: float = 0.0,
    start_y: float = 0.0,
    start_depth: float = 0.0,
    angle: float = 0.0,
    inclination: float = 0.0,
    slope: float = 0.0,
    length: float = 200.0,
    width: float = 50.0,
    internal_angle: float = 90.0,
    tilt_ref_side: float = 90.0,
    tilt_end_side: float = 90.0,
    tilt_opp_side: float = 90.0,
    tilt_start_side: float = 90.0,
    machining_limits: MachiningLimits | None = None,
    **kwargs,
)

Represents a Pocket feature to be made on a beam.

Parameters:

  • start_x (float, default: 0.0 ) –

    The start x-coordinate of the cut in parametric space of the reference side. -100000.0 < start_x < 100000.0.

  • start_y (float, default: 0.0 ) –

    The start y-coordinate of the cut in parametric space of the reference side. -50000.0 < start_y < 50000.0.

  • start_depth (float, default: 0.0 ) –

    The start depth of the cut. -50000.0 < start_depth < 50000.0.

  • angle (float, default: 0.0 ) –

    The horizontal angle of the cut. -179.9 < angle < 179.9.

  • inclination (float, default: 0.0 ) –

    The vertical angle of the cut. -179.9 < inclination < 179.9.

  • slope (float, default: 0.0 ) –

    The slope of the cut. -179.9 < slope < 179.9.

  • length (float, default: 200.0 ) –

    The length of the cut. 0.0 < length < 100000.0.

  • width (float, default: 50.0 ) –

    The width of the cut. 0.0 < width < 50000.0.

  • internal_angle (float, default: 90.0 ) –

    The internal angle of the cut. 0.1 < internal_angle < 179.9.

  • tilt_ref_side (float, default: 90.0 ) –

    The tilt angle of the reference side. 0.1 < tilt_ref_side < 179.9.

  • tilt_end_side (float, default: 90.0 ) –

    The tilt angle of the end side. 0.1 < tilt_end_side < 179.9.

  • tilt_opp_side (float, default: 90.0 ) –

    The tilt angle of the opposing side. 0.1 < tilt_opp_side < 179.9.

  • tilt_start_side (float, default: 90.0 ) –

    The tilt angle of the start side. 0.1 < tilt_start_side < 179.9.

  • machining_limits (:class:`~compas_timber.fabrication.MachiningLimits` or dict, default: None ) –

    The machining limits for the cut. Default is None

Source code in src/compas_timber/fabrication/pocket.py
def __init__(
    self,
    start_x: float =0.0,
    start_y: float =0.0,
    start_depth: float =0.0,
    angle: float = 0.0,
    inclination: float = 0.0,
    slope: float = 0.0,
    length: float = 200.0,
    width: float = 50.0,
    internal_angle: float = 90.0,
    tilt_ref_side: float = 90.0,
    tilt_end_side: float = 90.0,
    tilt_opp_side: float = 90.0,
    tilt_start_side: float = 90.0,
    machining_limits: Optional[MachiningLimits] = None,
    **kwargs
):
    super(Pocket, self).__init__(**kwargs)
    self._start_x = None
    self._start_y = None
    self._start_depth = None
    self._angle = None
    self._inclination = None
    self._slope = None
    self._length = None
    self._width = None
    self._internal_angle = None
    self._tilt_ref_side = None
    self._tilt_end_side = None
    self._tilt_opp_side = None
    self._tilt_start_side = None
    self._machining_limits = None

    self.start_x: float = start_x
    self.start_y: float = start_y
    self.start_depth: float = start_depth
    self.angle: float = angle
    self.inclination: float = inclination
    self.slope: float = slope
    self.length: float = length
    self.width: float = width
    self.internal_angle: float = internal_angle
    self.tilt_ref_side: float = tilt_ref_side
    self.tilt_end_side: float = tilt_end_side
    self.tilt_opp_side: float = tilt_opp_side
    self.tilt_start_side: float = tilt_start_side
    self.machining_limits: MachiningLimits = machining_limits

Methods:¤

apply ¤
apply(geometry: Brep, element: TimberElement) -> Brep

Apply the feature to the element geometry.

Parameters:

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

    The geometry of the elements to be processed.

  • element (:class:`compas_timber.base.TimberElement`) –

    The element that is processed by this instance.

Raises:

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

    If the cutting plane does not intersect with element geometry.

Returns:

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

    The resulting geometry after processing

Source code in src/compas_timber/fabrication/pocket.py
def apply(self, geometry: Brep, element: TimberElement) -> Brep:
    """Apply the feature to the element geometry.

    Parameters
    ----------
    geometry : :class:`~compas.geometry.Brep`
        The geometry of the elements to be processed.
    element : :class:`compas_timber.base.TimberElement`
        The element that is processed by this instance.

    Raises
    ------
    :class:`~compas_timber.errors.FeatureApplicationError`
        If the cutting plane does not intersect with element geometry.

    Returns
    -------
    :class:`~compas.geometry.Brep`
        The resulting geometry after processing

    """
    # a negative start_depth means the "bottom" plane of the pocket volume sits outside the
    # element's material on the ref_side's outward side (the volume passed to
    # from_volume_and_element never actually reached the material, e.g. a plate's shank
    # embedded entirely in a beam it doesn't share a face with). face_limited_top=False then
    # pins the "top" plane to the element's own ref_side, so the resulting hexahedron spans
    # from the ref_side surface *outward* rather than into the material - subtracting it can
    # corrupt (or entirely erase) the element's geometry instead of being a no-op. There's
    # nothing to cut in that case, so skip it rather than let it reach the boolean below.
    if self.start_depth < -TOL.absolute:
        raise FeatureApplicationError(
            None,
            geometry.transformed(element.modeltransformation),
            "Pocket's start_depth ({:.4f}) is negative: the pocket volume lies entirely outside "
            "{}'s material on the ref_side's outward side, so there is nothing to cut.".format(self.start_depth, element),
        )

    # get the pocket volume as a polyhedron
    polyhedron_volume = self.volume_from_params_and_element(element)
    polyhedron_volume.transform(element.transformation_to_local())

    # convert the polyhedron to a brep
    try:
        pocket_volume = Brep.from_mesh(polyhedron_volume.to_mesh())
    except Exception as e:
        raise FeatureApplicationError(
            polyhedron_volume.transformed(element.modeltransformation),
            geometry.transformed(element.modeltransformation),
            "The pocket volume could not be converted to a Brep." + str(e),
        )
    try:
        return brep_difference_first(geometry, pocket_volume)
    except Exception as e:
        raise FeatureApplicationError(
            pocket_volume.transformed(element.modeltransformation),
            geometry.transformed(element.modeltransformation),
            "The pocket volume does not intersect with the element geometry." + str(e),
        )
from_shapes_and_element classmethod ¤
from_shapes_and_element(volume, element, **kwargs) -> Pocket

Construct a Pocket feature from a volume and a TimberElement.

Parameters:

  • volume (:class:`~compas.geometry.Polyhedron` or :class:`~compas.geometry.Brep` or :class:`~compas.geometry.Mesh`) –

    The volume of the pocket. Must have 6 faces.

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

    The element that is cut by this instance.

Returns:

  • class:`~compas_timber.fabrication.Pocket` –

    The Pocket feature.

Source code in src/compas_timber/fabrication/pocket.py
@classmethod
def from_shapes_and_element(cls, volume, element, **kwargs) -> Pocket:
    """Construct a Pocket feature from a volume and a TimberElement.

    Parameters
    ----------
    volume : :class:`~compas.geometry.Polyhedron` or :class:`~compas.geometry.Brep` or :class:`~compas.geometry.Mesh`
        The volume of the pocket. Must have 6 faces.
    element : :class:`~compas_timber.base.TimberElement`
        The element that is cut by this instance.

    Returns
    -------
    :class:`~compas_timber.fabrication.Pocket`
        The Pocket feature.

    """
    return cls.from_volume_and_element(volume, element, **kwargs)
from_volume_and_element classmethod ¤
from_volume_and_element(
    volume: Polyhedron | Brep | Mesh,
    element: TimberElement,
    allow_undercut: bool = True,
    machining_limits: dict | None = None,
    ref_side_index: int | None = None,
) -> Pocket

Construct a Pocket feature from a volume and a TimberElement.

Parameters:

  • volume (:class:`~compas.geometry.Polyhedron` or :class:`~compas.geometry.Brep` or :class:`~compas.geometry.Mesh`) –

    The volume of the pocket. Must have 6 faces.

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

    The element that is cut by this instance.

  • allow_undercut (bool, default: True ) –

    Whether to allow undercuts in the pocket. If False, tilt angles will be clamped to 90 degrees. Default is True.

  • machining_limits (:class:`~compas_timber.fabrication.btlx.MachiningLimits` or dict, default: None ) –

    The machining limits for the cut. Default is None.

  • ref_side_index (int, default: None ) –

    The index of the reference side of the element. Default is 0.

Returns:

  • class:`~compas_timber.fabrication.Pocket` –

    The Pocket feature.

Source code in src/compas_timber/fabrication/pocket.py
@classmethod
def from_volume_and_element(
    cls,
    volume: Union[Polyhedron, Brep, Mesh],
    element: TimberElement,
    allow_undercut: bool = True,
    machining_limits: Optional[dict] = None,
    ref_side_index: Optional[int]=None
) -> Pocket:
    """Construct a Pocket feature from a volume and a TimberElement.

    Parameters
    ----------
    volume : :class:`~compas.geometry.Polyhedron` or :class:`~compas.geometry.Brep` or :class:`~compas.geometry.Mesh`
        The volume of the pocket. Must have 6 faces.
    element : :class:`~compas_timber.base.TimberElement`
        The element that is cut by this instance.
    allow_undercut : bool, optional
        Whether to allow undercuts in the pocket. If False, tilt angles will be clamped to 90 degrees. Default is True.
    machining_limits : :class:`~compas_timber.fabrication.btlx.MachiningLimits` or dict, optional
        The machining limits for the cut. Default is None.
    ref_side_index : int, optional
        The index of the reference side of the element. Default is 0.

    Returns
    -------
    :class:`~compas_timber.fabrication.Pocket`
        The Pocket feature.

    """
    if isinstance(volume, Mesh):
        planes = [volume.face_plane(i) for i in range(volume.number_of_faces())]
    elif isinstance(volume, Polyhedron):
        volume = volume.to_mesh()
        planes = [volume.face_plane(i) for i in range(volume.number_of_faces())]
    elif isinstance(volume, Brep):
        # not `face.surface`: it ignores `is_reversed`, and it is the face's own Plane,
        # so flipping the normal in place would mutate the volume we were handed.
        planes = [Plane.from_frame(face.frame_at()) for face in volume.faces]
    else:
        raise ValueError("Volume must be either a Mesh, Brep, or Polyhedron.")

    if len(planes) != 6:
        raise ValueError("Volume must have 6 faces.")

    # get ref_side of the element
    if ref_side_index is None:
        ref_side_index = cls._get_optimal_ref_side_index(element, volume)
    ref_side = element.ref_sides[ref_side_index]

    # sort the planes based on the reference side
    planes = cls._sort_planes(planes, ref_side)
    start_plane, end_plane, front_plane, back_plane, bottom_plane, _ = planes

    # get the intersection points
    try:
        start_point = Point(*intersection_plane_plane_plane(start_plane, front_plane, bottom_plane, tol=TOL.ABSOLUTE))
        back_point = Point (*intersection_plane_plane_plane(start_plane, back_plane, bottom_plane, tol=TOL.ABSOLUTE))
        end_point = Point(*intersection_plane_plane_plane(end_plane, front_plane, bottom_plane, tol=TOL.ABSOLUTE))
    except TypeError as te:
        raise ValueError("Failed to orient the volume to the element. Consider using a different ref_side_index " + str(te))

    ## params calculations
    # calculate start_x, start_y, start_depth
    start_x, start_y, start_depth = cls._calculate_start_x_y_depth(ref_side, start_point)

    # x"-axis and y"-axis (planar axis of the bottom face of the volume)
    xxaxis = Vector.from_start_end(start_point, end_point)
    yyaxis = Vector.from_start_end(start_point, back_point)

    # calculate the angle of the pocket
    angle = angle_vectors_projected(ref_side.xaxis, xxaxis, ref_side.normal, deg=True)
    # x'-axis and y'-axis (see BTLx Documentation p.46)
    xaxis = ref_side.xaxis.rotated(math.radians(angle), ref_side.normal)
    yaxis = ref_side.yaxis.rotated(math.radians(angle), ref_side.normal)

    # calculate the inclination of the pocket
    inclination = angle_vectors_projected(xaxis, xxaxis, yaxis, deg=True)

    # calculate the slope of the pocket
    slope = angle_vectors_projected(yaxis, yyaxis, xxaxis, deg=True)

    # calculate internal_angle
    internal_angle = angle_vectors_signed(xxaxis, yyaxis, ref_side.normal, deg=True)

    # calculate length and width
    length = xxaxis.length*math.sin(math.radians(internal_angle))
    width = yyaxis.length*math.sin(math.radians(internal_angle))

    # calculate tilt angles
    tilt_ref_side = cls._calculate_tilt_angle(bottom_plane, front_plane)
    tilt_end_side = cls._calculate_tilt_angle(bottom_plane, end_plane)
    tilt_opp_side = cls._calculate_tilt_angle(bottom_plane, back_plane)
    tilt_start_side = cls._calculate_tilt_angle(bottom_plane, start_plane)

    # clamp tilt angles to 90 degrees for non-conical (flat-bottom) tools
    if not allow_undercut:
        tilt_ref_side = max(tilt_ref_side, 90.0)
        tilt_end_side = max(tilt_end_side, 90.0)
        tilt_opp_side = max(tilt_opp_side, 90.0)
        tilt_start_side = max(tilt_start_side, 90.0)

    # define machining limits
    if not machining_limits:
        machining_limits = cls._define_machining_limits(planes, element, ref_side_index)

    return cls(
        start_x,
        start_y,
        start_depth,
        angle,
        inclination,
        slope,
        length,
        width,
        internal_angle,
        tilt_ref_side,
        tilt_end_side,
        tilt_opp_side,
        tilt_start_side,
        machining_limits,
        ref_side_index=ref_side_index)
scale ¤
scale(factor: float) -> None

Scale the parameters of this processing by a given factor.

Notes

Only distances are scaled, angles remain unchanged.

Parameters:

  • factor (float) –

    The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

Source code in src/compas_timber/fabrication/pocket.py
def scale(self, factor: float) -> None:
    """Scale the parameters of this processing by a given factor.

    Notes
    -----
    Only distances are scaled, angles remain unchanged.

    Parameters
    ----------
    factor : float
        The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

    """
    self.start_x *= factor
    self.start_y *= factor
    self.start_depth *= factor
    self.length *= factor
    self.width *= factor
volume_from_params_and_element ¤
volume_from_params_and_element(element: TimberElement) -> Polyhedron

Calculates the subtracting volume from the machining parameters in this instance and the given element, ensuring correct face orientation.

Parameters:

  • element (:class:`compas_timber.base.TimberElement`) –

    The element that is cut by this instance.

Returns:

  • class:`compas.geometry.Polyhedron` –

    The correctly oriented subtracting volume of the pocket.

Source code in src/compas_timber/fabrication/pocket.py
def volume_from_params_and_element(self, element: TimberElement) -> Polyhedron:
    """
    Calculates the subtracting volume from the machining parameters in this instance and the given element, ensuring correct face orientation.

    Parameters
    ----------
    element : :class:`compas_timber.base.TimberElement`
        The element that is cut by this instance.

    Returns
    -------
    :class:`compas.geometry.Polyhedron`
        The correctly oriented subtracting volume of the pocket.
    """
    # Get cutting planes
    start_plane, end_plane, top_plane, bottom_plane, front_plane, back_plane = self._planes_from_params_and_element(element)

    # Calculate vertices using plane-plane-plane intersection
    vertices = [
        Point(*intersection_plane_plane_plane(start_plane, bottom_plane, front_plane)),     # v0
        Point(*intersection_plane_plane_plane(start_plane, bottom_plane, back_plane)),      # v1
        Point(*intersection_plane_plane_plane(end_plane, bottom_plane, back_plane)),        # v2
        Point(*intersection_plane_plane_plane(end_plane, bottom_plane, front_plane)),       # v3
        Point(*intersection_plane_plane_plane(start_plane, top_plane, front_plane)),        # v4
        Point(*intersection_plane_plane_plane(start_plane, top_plane, back_plane)),         # v5
        Point(*intersection_plane_plane_plane(end_plane, top_plane, back_plane)),           # v6
        Point(*intersection_plane_plane_plane(end_plane, top_plane, front_plane)),          # v7
    ]

    # define faces of the trimming volume
    # ensure vertices are defined in counter-clockwise order when viewed from the outside
    faces = [
        [0, 1, 2, 3],  # Bottom face
        [7, 6, 5, 4],  # Top face
        [4, 5, 1, 0],  # Start face
        [5, 6, 2, 1],  # Back face
        [6, 7, 3, 2],  # End face
        [7, 4, 0, 3],  # Front face
    ]

    return Polyhedron(vertices, faces)

PocketProxy ¤

PocketProxy(volume, element, machining_limits=None, ref_side_index=None)

This object behaves like a Pocket except it only calculates the machining parameters once unproxified. Can also be used to defer the creation of the processing instance until it is actually needed.

Until then, it can be used to visualize the machining operation. This slightly improves performance.

Parameters:

  • volume (:class:`~compas.geometry.Polyhedron` or :class:`~compas.geometry.Brep` or :class:`~compas.geometry.Mesh`) –

    The volume of the pocket. Must have 6 faces.

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

    The element that is cut by this instance.

  • machining_limits (:class:`~compas_timber.fabrication.MachiningLimits` or dict, default: None ) –

    The machining limits for the cut. Default is None.

  • ref_side_index (int, default: None ) –

    The index of the reference side of the element. Default is 0.

Returns:

  • class:`~compas_timber.fabrication.Pocket` –

    The Pocket feature.

Source code in src/compas_timber/fabrication/pocket.py
def __init__(self, volume, element, machining_limits=None, ref_side_index=None):
    self.volume = volume.transformed(element.transformation_to_local())
    self.element = element
    self.machining_limits = machining_limits
    self.ref_side_index = ref_side_index
    self._processing = None

Methods:¤

apply ¤
apply(geometry, _)

Apply the feature to the beam geometry.

Parameters:

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

    The beam geometry to apply the pocket to.

Raises:

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

    If the pocket volume does not intersect with element geometry.

Returns:

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

    The resulting geometry after processing

Source code in src/compas_timber/fabrication/pocket.py
def apply(self, geometry, _):
    """Apply the feature to the beam geometry.

    Parameters
    ----------
    geometry : :class:`~compas.geometry.Brep`
        The beam geometry to apply the pocket to.

    Raises
    ------
    :class:`~compas_timber.errors.FeatureApplicationError`
        If the pocket volume does not intersect with element geometry.

    Returns
    -------
    :class:`~compas.geometry.Brep`
        The resulting geometry after processing

    """
    # type: (Brep, Element) -> Brep
    try:
        return brep_difference_first(geometry, self.volume)
    except IndexError:
        raise FeatureApplicationError(
            self.volume.transformed(self.element.modeltransformation),
            geometry.transformed(self.element.modeltransformation),
            "The volume to subtract does not intersect with element geometry.",
        )
from_volume_and_element classmethod ¤
from_volume_and_element(volume, element, machining_limits=None, ref_side_index=None)

Construct a Pocket feature from a volume and a TimberElement.

Parameters:

  • volume (:class:`~compas.geometry.Polyhedron` or :class:`~compas.geometry.Brep` or :class:`~compas.geometry.Mesh`) –

    The volume of the pocket. Must have 6 faces.

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

    The element that is cut by this instance.

  • machining_limits (:class:`compas_timber.fabrication.MachiningLimits` or dict, default: None ) –

    The machining limits for the cut. Default is None.

  • ref_side_index (int, default: None ) –

    The index of the reference side of the element. Default is 0.

Returns:

  • class:`~compas_timber.fabrication.Pocket` –

    The Pocket feature.

Source code in src/compas_timber/fabrication/pocket.py
@classmethod
def from_volume_and_element(cls, volume, element, machining_limits=None, ref_side_index=None):
    """Construct a Pocket feature from a volume and a TimberElement.

    Parameters
    ----------
    volume : :class:`~compas.geometry.Polyhedron` or :class:`~compas.geometry.Brep` or :class:`~compas.geometry.Mesh`
        The volume of the pocket. Must have 6 faces.
    element : :class:`~compas_timber.base.TimberElement`
        The element that is cut by this instance.
    machining_limits : :class:`compas_timber.fabrication.MachiningLimits` or dict, optional
        The machining limits for the cut. Default is None.
    ref_side_index : int, optional
        The index of the reference side of the element. Default is 0.

    Returns
    -------
    :class:`~compas_timber.fabrication.Pocket`
        The Pocket feature.

    """
    if isinstance(volume, Polyhedron):
        volume = Brep.from_mesh(volume.to_mesh())
    if TOL.is_negative(volume.volume):
        volume.flip()
    return cls(volume, element, machining_limits, ref_side_index)
unproxified ¤
unproxified()

Returns the unproxified processing instance.

Returns:

  • class:`~compas_timber.fabrication.Pocket` –
Source code in src/compas_timber/fabrication/pocket.py
def unproxified(self):
    """Returns the unproxified processing instance.

    Returns
    -------
    :class:`~compas_timber.fabrication.Pocket`

    """
    if not self._processing:
        volume = self.volume.transformed(self.element.modeltransformation)
        self._processing = Pocket.from_volume_and_element(
            volume, self.element, allow_undercut=True, machining_limits=self.machining_limits, ref_side_index=self.ref_side_index
        )
    return self._processing

SimpleScarf ¤

SimpleScarf(
    orientation: str = START,
    start_x: float = 0.0,
    length: float = 200.0,
    depth_ref_side: float = 20.0,
    depth_opp_side: float = 20.0,
    num_drill_hole: int = 0,
    drill_hole_diam_1: float = 20.0,
    drill_hole_diam_2: float = 20.0,
    **kwargs,
)
Source code in src/compas_timber/fabrication/simple_scarf.py
def __init__(
    self,
    orientation: str = OrientationType.START,
    start_x: float = 0.0,
    length: float = 200.0,
    depth_ref_side: float = 20.0,
    depth_opp_side: float = 20.0,
    num_drill_hole: int = 0,
    drill_hole_diam_1: float = 20.0,
    drill_hole_diam_2: float = 20.0,
    **kwargs
):
    super(SimpleScarf, self).__init__(**kwargs)
    self._orientation = None
    self._start_x = None
    self._length = None
    self._depth_ref_side = None
    self._depth_opp_side = None
    self._num_drill_hole = None
    self._drill_hole_diam_1 = None
    self._drill_hole_diam_2 = None

    self.orientation = orientation
    self.start_x = start_x
    self.length = length
    self.depth_ref_side = depth_ref_side
    self.depth_opp_side = depth_opp_side
    self.num_drill_hole = num_drill_hole
    self.drill_hole_diam_1 = drill_hole_diam_1
    self.drill_hole_diam_2 = drill_hole_diam_2

Methods:¤

apply ¤
apply(geometry: Brep, beam: Beam) -> Brep

Apply the feature to the beam geometry.

Parameters:

  • geometry (:class:`~compas_brep.Brep`) –

    The geometry to be processed.

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

    The beam that is milled by this instance.

Raises:

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

    If the scarf or drill volumes cannot be converted to Breps, or if the scarf volume does not intersect with the beam geometry during boolean subtraction

Returns:

  • class:`~compas_brep.Brep` –

    The resulting geometry after processing.

Source code in src/compas_timber/fabrication/simple_scarf.py
def apply(self, geometry: Brep, beam: Beam) -> Brep:
    """Apply the feature to the beam geometry.

    Parameters
    ----------
    geometry : :class:`~compas_brep.Brep`
        The geometry to be processed.

    beam : :class:`~compas_timber.elements.Beam`
        The beam that is milled by this instance.

    Raises
    ------
    :class:`~compas_timber.errors.FeatureApplicationError`
        If the scarf or drill volumes cannot be converted to Breps, or if
        the scarf volume does not intersect with the beam geometry during
        boolean subtraction

    Returns
    -------
    :class:`~compas_brep.Brep`
        The resulting geometry after processing.

    """
    scarf_volume = self.volume_from_params_and_beam(beam)
    scarf_volume.transform(beam.transformation_to_local())
    drill_volumes = self.drill_hole_volumes_from_params_and_beam(beam)
    drill_volumes = [dv.transformed(beam.transformation_to_local()) for dv in drill_volumes]

    try:
        scarf_volume = Brep.from_mesh(scarf_volume.to_mesh())
        drill_volumes = [Brep.from_cylinder(dv) for dv in drill_volumes]
    except Exception:
        raise FeatureApplicationError(
            scarf_volume.transformed(beam.modeltransformation),
            geometry.transformed(beam.modeltransformation),
            "Could not convert the scarf volume mesh to a Brep."
        )

    try:
        for sub_brep in Brep.from_boolean_difference(geometry, scarf_volume):
            if sub_brep.contains(beam.centerline.midpoint.transformed(beam.transformation_to_local())):
                for dv in drill_volumes:
                    sub_brep = Brep.from_boolean_difference(sub_brep, dv)[0]
                return sub_brep
    except IndexError:
        raise FeatureApplicationError(
            scarf_volume.transformed(beam.modeltransformation),
            geometry.transformed(beam.modeltransformation),
            "The scarf volume does not intersect with the beam geometry."
        )
    raise FeatureApplicationError(
        scarf_volume.transformed(beam.modeltransformation),
        geometry.transformed(beam.modeltransformation),
        "No valid result solid found after boolean difference: the beam midpoint is not contained in any result Brep.",
    )
scale ¤
scale(factor: float) -> None

Scale the parameters of this processing by a given factor.

Note

Only distances are scaled, angles remain unchanged.

Parameters:

  • factor (float) –

    The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

Source code in src/compas_timber/fabrication/simple_scarf.py
def scale(self, factor: float) -> None:
    """Scale the parameters of this processing by a given factor.

    Note
    ----
    Only distances are scaled, angles remain unchanged.

    Parameters
    ----------
    factor : float
        The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

    """
    self.start_x *= factor
    self.length *= factor
    self.depth_ref_side *= factor
    self.depth_opp_side *= factor
    self.drill_hole_diam_1 *= factor
    self.drill_hole_diam_2 *= factor
volume_from_params_and_beam ¤
volume_from_params_and_beam(beam: Beam) -> Polyhedron

Generates a Polyhedron representing the volume to be removed from the beam.

Parameters:

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

    The beam that is milled by this instance.

Returns:

  • class:`~compas_geometry.Polyhedron` –

    The Polyhedron representing the volume to be removed from the beam.

Source code in src/compas_timber/fabrication/simple_scarf.py
def volume_from_params_and_beam(self, beam: Beam) -> Polyhedron:
    """Generates a Polyhedron representing the volume to be removed from the beam.

    Parameters
    ----------
    beam : :class:`~compas_timber.elements.Beam`
        The beam that is milled by this instance.

    Returns
    -------
    :class:`~compas_geometry.Polyhedron`
        The Polyhedron representing the volume to be removed from the beam.

    """

    top_plane, ref_middle_plane, start_plane, blank_plane, end_plane, opp_middle_plane, bottom_plane, front_plane, back_plane = self._planes_from_params_and_beam(beam)

    vertices = [
        Point(*intersection_plane_plane_plane(top_plane, blank_plane, front_plane)),            #v0
        Point(*intersection_plane_plane_plane(bottom_plane, blank_plane, front_plane)),         #v1
        Point(*intersection_plane_plane_plane(bottom_plane, end_plane, front_plane)),           #v2
        Point(*intersection_plane_plane_plane(opp_middle_plane, end_plane, front_plane)),       #v3
        Point(*intersection_plane_plane_plane(ref_middle_plane, start_plane, front_plane)),     #v4
        Point(*intersection_plane_plane_plane(top_plane, start_plane, front_plane)),            #v5
        Point(*intersection_plane_plane_plane(top_plane, blank_plane, back_plane)),             #v6
        Point(*intersection_plane_plane_plane(top_plane, start_plane, back_plane)),             #v7
        Point(*intersection_plane_plane_plane(ref_middle_plane, start_plane, back_plane)),      #v8
        Point(*intersection_plane_plane_plane(opp_middle_plane, end_plane, back_plane)),        #v9
        Point(*intersection_plane_plane_plane(bottom_plane, end_plane, back_plane)),            #v10
        Point(*intersection_plane_plane_plane(bottom_plane, blank_plane, back_plane)),          #v11
    ]

    faces = [
        [0,1,4,5],          # Front face 1
        [1,2,3,4],          # Front face 2
        [6,7,8,11],         # Back face 1
        [8,9,10,11],        # Back face 2
        [0,5,7,6],          # Top face
        [1,11,10,2],        # Bottom face
        [0,6,11,1],         # Start face
        [9,3,2,10],         # End face
        [4,3,9,8],          # Middle face
        [5,4,8,7]           #  face
    ]

    if self.orientation == OrientationType.END:
        faces = [face[::-1] for face in faces]
    return Polyhedron(vertices, faces)

Slot ¤

Slot(
    orientation=START,
    start_x=0.0,
    start_y=0.0,
    start_depth=0.0,
    angle=90.0,
    inclination=90.0,
    length=200.0,
    depth=10.0,
    thickness=10.0,
    angle_ref_point=90.0,
    angle_opp_point=90.0,
    add_angle_opp_point=0.0,
    machining_limits=None,
    **kwargs,
)
Source code in src/compas_timber/fabrication/slot.py
def __init__(
    self,
    orientation=OrientationType.START,
    start_x=0.0,
    start_y=0.0,
    start_depth=0.0,
    angle=90.0,
    inclination=90.0,
    length=200.0,
    depth=10.0,
    thickness=10.0,
    angle_ref_point=90.0,
    angle_opp_point=90.0,
    add_angle_opp_point=0.0,
    machining_limits=None,
    **kwargs
):
    super(Slot, self).__init__(**kwargs)
    self._orientation = None
    self._start_x = None
    self._start_y = None
    self._start_depth = None
    self._angle = None
    self._inclination = None
    self._length = None
    self._depth = None
    self._thickness = None
    self._angle_ref_point = None
    self._angle_opp_point = None
    self._add_angle_opp_point = None
    self._machining_limits = None

    self.orientation = orientation
    self.start_x = start_x
    self.start_y = start_y
    self.start_depth = start_depth
    self.angle = angle
    self.inclination = inclination
    self.length = length
    self.depth = depth
    self.thickness = thickness
    self.angle_ref_point = angle_ref_point
    self.angle_opp_point = angle_opp_point
    self.add_angle_opp_point = add_angle_opp_point
    self.machining_limits = machining_limits

Methods:¤

apply ¤
apply(geometry: Brep, beam: Beam) -> Brep

Apply the feature to the beam geometry.

Parameters:

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

    The beam geometry to be cut.

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

    The beam that is cut by this instance.

Raises:

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

    If the cutting plane does not intersect with beam geometry.

Returns:

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

    The resulting geometry after processing

Source code in src/compas_timber/fabrication/slot.py
def apply(self, geometry: Brep, beam: Beam) -> Brep:
    """Apply the feature to the beam geometry.

    Parameters
    ----------
    geometry : :class:`~compas.geometry.Brep`
        The beam geometry to be cut.
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Raises
    ------
    :class:`~compas_timber.errors.FeatureApplicationError`
        If the cutting plane does not intersect with beam geometry.

    Returns
    -------
    :class:`~compas.geometry.Brep`
        The resulting geometry after processing

    """
    subtraction_volume = self.volume_from_params_and_beam(beam)
    subtraction_volume.transform(beam.transformation_to_local())
    subtraction_volume = Brep.from_mesh(subtraction_volume.to_mesh())
    if self.orientation == OrientationType.END:
        subtraction_volume.flip()

    try:
        cutted_geometry = brep_difference_first(geometry, subtraction_volume)
        return cutted_geometry

    except Exception:
        raise FeatureApplicationError(
            subtraction_volume.transformed(beam.modeltransformation),
            geometry.transformed(beam.modeltransformation),
            "The slot subtracting volume does not intersect with the beam geometry."
            )
from_plane_and_beam classmethod ¤
from_plane_and_beam(plane, beam, depth, thickness)

Makes a full horizontal or vertical slot accross one of the end faces of the beam.

Therefore, the provided plane must cut the beam at one of its ends, and it must intersect with exactly two parallel small edges of the side face. The length of the slot is equal to the full length accross.

Parameters:

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

    The plane which specifies the orientation and depth of the Slot.

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

    The beam that is cut by this instance.

Returns:

  • class:`~compas_timber.fabrication.Slot` –

    The constructed Slot feature.

Source code in src/compas_timber/fabrication/slot.py
@classmethod
def from_plane_and_beam(cls, plane, beam, depth, thickness):
    """Makes a full horizontal or vertical slot accross one of the end faces of the beam.

    Therefore, the provided plane must cut the beam at one of its ends, and it must intersect with exactly two parallel small edges of the side face.
    The length of the slot is equal to the full length accross.

    Parameters
    ----------
    plane : :class:`~compas.geometry.Plane`
        The plane which specifies the orientation and depth of the Slot.
    beam : :class:`~compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Returns
    -------
    :class:`~compas_timber.fabrication.Slot`
        The constructed Slot feature.

    """
    # this only really matters if `start_depth` != 0. which we're not dealing with quite yet
    orientation = OrientationType.START

    # angle of rotation is bound to between -90 and 90. the rotation point and axis is determined by start_x and start_y

    # 1. find out of the plane cuts horizontally or vertically
    #   check intersections with the small edges. There should be two intersections with parallel small edges
    #   start_x and start_y are depending on this classification
    # 2. find the angle

    # find relevant end surface
    distance_from_start = distance_point_point(plane.point, beam.centerline.start)
    distance_from_end = distance_point_point(plane.point, beam.centerline.end)
    if distance_from_start < distance_from_end:
        ref_side_index = 4
    else:
        ref_side_index = 5

    ref_side = beam.side_as_surface(ref_side_index)
    # find 2 points of intersection
    # TODO: shove this into some function. what are we? savages?
    small_edge_bottom = Line(planar_surface_point_at(ref_side, 0, 0), planar_surface_point_at(ref_side, beam.width, 0))
    small_edge_top = Line(planar_surface_point_at(ref_side, 0, beam.height), planar_surface_point_at(ref_side, beam.width, beam.height))
    small_edge_left = Line(planar_surface_point_at(ref_side, 0, 0), planar_surface_point_at(ref_side, 0, beam.height))
    small_edge_right = Line(planar_surface_point_at(ref_side, beam.width, 0), planar_surface_point_at(ref_side, beam.width, beam.height))

    slot_plane = Plane.from_frame(plane)
    intersection_bottom = intersection_segment_plane(small_edge_bottom, slot_plane)
    intersection_top = intersection_segment_plane(small_edge_top, slot_plane)
    intersection_left = intersection_segment_plane(small_edge_left, slot_plane)
    intersection_right = intersection_segment_plane(small_edge_right, slot_plane)

    # find inclination angle
    # look at the jack rafter cut. but idea might be to cross normal and one of the
    # axes of the ref frame to get the yaw angle between ref_side.xaxis and the horizontal direction of the plane
    yaw_vector = plane.normal.cross(ref_side.yaxis)
    inclination = angle_vectors_signed(yaw_vector, ref_side.xaxis, -ref_side.yaxis, deg=True)

    roll_vector = plane.normal.cross(ref_side.zaxis)
    angle = angle_vectors_signed(ref_side.xaxis, roll_vector, ref_side.zaxis, deg=True)
    print("calculated non-signed angle: {}".format(angle_vectors(ref_side.xaxis, roll_vector, deg=True)))
    print("calculated signed angle: {}".format(angle))

    # adjust 0-360 to -90 to 90
    if angle > 90:
        angle = (180 - angle) * -1
    elif angle < -90:
        angle = (180 + angle) * -1
    print("adjusted angle: {}".format(angle))

    ref_frame = ref_side.frame
    if intersection_bottom and intersection_top:
        length = distance_point_point(intersection_bottom, intersection_top)
        if angle < 0:
            slot_start_point = Point(*intersection_top)
        else:
            slot_start_point = Point(*intersection_bottom)
    elif intersection_left and intersection_right:
        length = distance_point_point(intersection_left, intersection_right)
        slot_start_point = Point(*intersection_left)
    else:
        raise ValueError("The slot plane must fully cross one of the beam's end faces")

    local_intersection = ref_frame.to_local_coordinates(Point(*slot_start_point))
    start_x = local_intersection.x
    start_y = local_intersection.y

    return cls(
        orientation,
        start_x=start_x,
        start_y=start_y,
        angle=angle,
        inclination=inclination,
        length=length,
        depth=depth,
        thickness=thickness,
        ref_side_index=ref_side_index,
    )
scale ¤
scale(factor)

Scale the parameters of this processing by a given factor.

Notes

Only distances are scaled, angles remain unchanged.

Parameters:

  • factor (float) –

    The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

Source code in src/compas_timber/fabrication/slot.py
def scale(self, factor):
    """Scale the parameters of this processing by a given factor.

    Notes
    -----
    Only distances are scaled, angles remain unchanged.

    Parameters
    ----------
    factor : float
        The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

    """
    self.start_x *= factor
    self.start_y *= factor
    self.start_depth *= factor
    self.length *= factor
    self.depth *= factor
    self.thickness *= factor
volume_from_params_and_beam ¤
volume_from_params_and_beam(beam: Beam) -> Polyhedron

Computes the cutting volume of the slot based on the machining limits and parameters.

Parameters:

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

    The beam that is cut by this instance.

Returns:

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

    The cutting volume of the slot.

Source code in src/compas_timber/fabrication/slot.py
def volume_from_params_and_beam(self, beam: Beam) -> Polyhedron:
    """
    Computes the cutting volume of the slot based on the machining limits and parameters.

    Parameters
    ----------
    beam : :class:`~compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Returns
    -------
    :class:`~compas.geometry.Polyhedron`
        The cutting volume of the slot.
    """
    origin_point = self._origin_point(beam)
    origin_frame = self._origin_frame(beam)
    p1 = self._find_p1(origin_point, origin_frame)
    slot_frame = self._compute_slot_frame(p1, origin_frame)
    p4 = self._find_p4(p1, slot_frame)
    p3 = self._find_p3(p1, p4, slot_frame)

    start_frame = self._start_frame(beam, slot_frame, p3)
    end_frame = self._end_frame(beam, slot_frame, p3)
    top_frame = self._top_frame(beam, slot_frame, p3)
    bottom_frame = self._bottom_frame(beam, slot_frame, p3)
    front_frame = self._front_frame(beam, slot_frame)
    back_frame = self._back_frame(beam, slot_frame)

    start_plane = Plane.from_frame(start_frame)
    end_plane = Plane.from_frame(end_frame)
    top_plane = Plane.from_frame(top_frame)
    bottom_plane = Plane.from_frame(bottom_frame)
    front_plane = Plane.from_frame(front_frame)
    back_plane = Plane.from_frame(back_frame)

    vertices = [
        Point(*intersection_plane_plane_plane(start_plane, bottom_plane, front_plane)), # v0
        Point(*intersection_plane_plane_plane(bottom_plane, end_plane, front_plane)),  # v1
        Point(*intersection_plane_plane_plane(end_plane, top_plane, front_plane)),    # v2
        Point(*intersection_plane_plane_plane(top_plane, start_plane, front_plane)),   # v3
        Point(*intersection_plane_plane_plane(start_plane, bottom_plane, back_plane)), # v4
        Point(*intersection_plane_plane_plane(bottom_plane, end_plane, back_plane)),  # v5
        Point(*intersection_plane_plane_plane(end_plane, top_plane, back_plane)),    # v6
        Point(*intersection_plane_plane_plane(top_plane, start_plane, back_plane)),   # v7
    ]
    faces = [
        [0, 1, 2, 3],  # front face
        [4, 7, 6, 5],  # back face
        [0, 4, 5, 1],  # bottom face
        [3, 2, 6, 7],  # top face
        [1, 5, 6, 2],  # right face
        [0, 3, 7, 4],  # left face
    ]
    return Polyhedron(vertices, faces)

StepJoint ¤

StepJoint(
    orientation=START,
    start_x=0.0,
    strut_inclination=90.0,
    step_depth=20.0,
    heel_depth=20.0,
    step_shape=DOUBLE,
    tenon=False,
    tenon_width=40.0,
    tenon_height=40.0,
    **kwargs,
)

Represents a Step Joint feature to be made on a beam.

Parameters:

  • orientation (int, default: START ) –

    The orientation of the cut. Must be either OrientationType.START or OrientationType.END.

  • start_x (float, default: 0.0 ) –

    The start x-coordinate of the cut in parametric space of the reference side. -100000.0 < start_x < 100000.0.

  • strut_inclination (float, default: 90.0 ) –

    The inclination of the strut. 0.1 < strut_inclination < 179.9.

  • step_depth (float, default: 20.0 ) –

    The depth of the step. step_depth < 50000.0.

  • heel_depth (float, default: 20.0 ) –

    The depth of the heel. heel_depth < 50000.0.

  • step_shape (str, default: DOUBLE ) –

    The shape of the step. Must be either 'double', 'step', 'heel', or 'taperedheel'.

  • tenon (str, default: False ) –

    The presence of a tenon. Must be either 'no' or 'yes'.

  • tenon_width (float, default: 40.0 ) –

    The width of the tenon. tenon_width < 1000.0.

  • tenon_height (float, default: 40.0 ) –

    The height of the tenon. tenon_height < 1000.0.

Source code in src/compas_timber/fabrication/step_joint.py
def __init__(
    self,
    orientation=OrientationType.START,
    start_x=0.0,
    strut_inclination=90.0,
    step_depth=20.0,
    heel_depth=20.0,
    step_shape=StepShapeType.DOUBLE,
    tenon=False,
    tenon_width=40.0,
    tenon_height=40.0,
    **kwargs
):
    super(StepJoint, self).__init__(**kwargs)
    self._orientation = None
    self._start_x = None
    self._strut_inclination = None
    self._step_depth = None
    self._heel_depth = None
    self._step_shape = None
    self._tenon = None
    self._tenon_width = None
    self._tenon_height = None

    self.orientation = orientation
    self.start_x = start_x
    self.strut_inclination = strut_inclination
    self.step_depth = step_depth
    self.heel_depth = heel_depth
    self.step_shape = step_shape
    self.tenon = tenon
    self.tenon_width = tenon_width
    self.tenon_height = tenon_height

Methods:¤

add_tenon ¤
add_tenon(tenon_width, tenon_height)

Add a tenon to the existing StepJointNotch instance.

Parameters:

  • tenon_width (float) –

    The width of the tenon. tenon_width < 1000.0.

  • tenon_height (float) –

    The height of the tenon. tenon_height < 1000.0.

Source code in src/compas_timber/fabrication/step_joint.py
def add_tenon(self, tenon_width, tenon_height):
    """Add a tenon to the existing StepJointNotch instance.

    Parameters
    ----------
    tenon_width : float
        The width of the tenon. tenon_width < 1000.0.
    tenon_height : float
        The height of the tenon. tenon_height < 1000.0.
    """
    self.tenon = True
    self.tenon_width = tenon_width
    self.tenon_height = (
        self.step_depth if tenon_height < self.step_depth else tenon_height
    )  # TODO: should this be constrained?
apply ¤
apply(geometry, beam)

Apply the feature to the beam geometry.

Parameters:

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

    The geometry to be processed.

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

    The beam that is milled by this instance.

Raises:

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

    If the cutting planes do not create a volume that itersects with beam geometry or any step fails.

Returns:

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

    The resulting geometry after processing

Source code in src/compas_timber/fabrication/step_joint.py
def apply(self, geometry, beam):
    """Apply the feature to the beam geometry.

    Parameters
    ----------
    geometry : :class:`compas.geometry.Brep`
        The geometry to be processed.

    beam : :class:`compas_timber.elements.Beam`
        The beam that is milled by this instance.

    Raises
    ------
    :class:`~compas_timber.errors.FeatureApplicationError`
        If the cutting planes do not create a volume that itersects with beam geometry or any step fails.

    Returns
    -------
    :class:`~compas.geometry.Brep`
        The resulting geometry after processing

    """
    # type: (Brep, Beam) -> Brep

    # get cutting planes from params and beam
    try:
        cutting_planes = self.planes_from_params_and_beam(beam)
    except ValueError as e:
        raise FeatureApplicationError(
            None, geometry.transformed(beam.modeltransformation), "Failed to generate cutting planes from parameters and beam: {}".format(str(e))
        )
    cutting_planes = [plane.transformed(beam.transformation_to_local()) for plane in cutting_planes]

    if self.step_shape == StepShapeType.STEP:
        for cutting_plane in cutting_planes:
            cutting_plane.normal = cutting_plane.normal * -1
            try:
                geometry.trim(cutting_plane)
            except Exception as e:
                raise FeatureApplicationError(
                    cutting_plane.transformed(beam.modeltransformation),
                    geometry.transformed(beam.modeltransformation),
                    "Failed to trim geometry with cutting planes: {}".format(str(e)),
                )

    elif self.step_shape == StepShapeType.HEEL:
        trimmed_geometies = []
        for cutting_plane in cutting_planes:
            cutting_plane.normal = cutting_plane.normal * -1
            try:
                trimmed_geometies.append(geometry.trimmed(cutting_plane))
            except Exception as e:
                raise FeatureApplicationError(
                    cutting_plane.transformed(beam.modeltransformation),
                    geometry.transformed(beam.modeltransformation),
                    "Failed to trim geometry with cutting plane: {}".format(str(e)),
                )
        try:
            geometry = (
                trimmed_geometies[0] + trimmed_geometies[1]
            )  # TODO: should be swed (.sew()) for a cleaner Brep
        except Exception as e:
            raise FeatureApplicationError(
                [g.transformed(beam.modeltransformation) for g in trimmed_geometies],
                geometry.transformed(beam.modeltransformation),
                "Failed to union trimmed geometries: {}".format(str(e)),
            )

    elif self.step_shape == StepShapeType.TAPERED_HEEL:
        try:
            cutting_plane = cutting_planes[0]
            cutting_plane.normal = cutting_plane.normal * -1
            geometry.trim(cutting_plane)
        except Exception as e:
            raise FeatureApplicationError(
                [p.transformed(beam.modeltransformation) for p in cutting_planes],
                geometry.transformed(beam.modeltransformation),
                "Failed to trim geometry with cutting plane: {}".format(str(e)),
            )

    elif self.step_shape == StepShapeType.DOUBLE:
        # trim geometry with last cutting plane
        cutting_planes[-1].normal = cutting_planes[-1].normal * -1
        try:
            geometry.trim(cutting_planes[-1])
        except Exception as e:
            raise FeatureApplicationError(
                cutting_planes[-1].transformed(beam.modeltransformation),
                geometry.transformed(beam.modeltransformation),
                "Failed to trim geometry with cutting plane: {}".format(str(e)),
            )
        # trim geometry with first two cutting planes
        trimmed_geometies = []
        for cutting_plane in cutting_planes[:2]:
            cutting_plane.normal = cutting_plane.normal * -1
            try:
                trimmed_geometies.append(geometry.trimmed(cutting_plane))
            except Exception as e:
                raise FeatureApplicationError(
                    cutting_plane.transformed(beam.modeltransformation),
                    geometry.transformed(beam.modeltransformation),
                    "Failed to trim geometry with cutting plane: {}".format(str(e)),
                )
        try:
            geometry = (
                trimmed_geometies[0] + trimmed_geometies[1]
            )  # TODO: should be swed (.sew()) for a cleaner Brep
        except Exception as e:
            raise FeatureApplicationError(
                [g.transformed(beam.modeltransformation) for g in trimmed_geometies],
                geometry.transformed(beam.modeltransformation),
                "Failed to union trimmed geometries: {}".format(str(e)),
            )

    if self.tenon and self.step_shape != StepShapeType.DOUBLE:  # TODO: check if tenon applies only to step in BTLx
        # create tenon volume and subtract from brep
        tenon_volume = self.tenon_volume_from_params_and_beam(beam)
        tenon_volume.transform(beam.transformation_to_local())
        cutting_planes[0].normal = cutting_planes[0].normal * -1
        if self.step_shape == StepShapeType.STEP:
            # trim tenon volume with cutting plane
            try:
                tenon_volume.trim(cutting_planes[0])
            except Exception as e:
                raise FeatureApplicationError(
                    cutting_planes[0].transformed(beam.modeltransformation),
                    tenon_volume.transformed(beam.modeltransformation),
                    "Failed to trim tenon volume with cutting plane: {}".format(str(e)),
                )
            # trim tenon volume with second cutting plane if tenon height is greater than step depth
            if self.tenon_height > self.step_depth:
                try:
                    tenon_volume.trim(cutting_planes[1])
                except Exception as e:
                    raise FeatureApplicationError(
                        cutting_planes[1].transformed(beam.modeltransformation),
                        tenon_volume.transformed(beam.modeltransformation),
                        "Failed to trim tenon volume with second cutting plane: {}".format(str(e)),
                    )
        # add tenon volume to geometry
        try:
            geometry = brep_union_first(geometry, tenon_volume)
        except Exception as e:
            raise FeatureApplicationError(
                tenon_volume.transformed(beam.modeltransformation),
                geometry.transformed(beam.modeltransformation),
                "Failed to add tenon volume to geometry: {}".format(str(e)),
            )

    return geometry
from_plane_and_beam classmethod ¤
from_plane_and_beam(
    plane, beam, step_depth=20.0, heel_depth=0.0, tapered_heel=False, ref_side_index=0
)

Create a StepJoint instance from a cutting surface and the beam it should cut. This could be the ref_side of the cross beam of a Joint and the main beam.

Parameters:

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

    The cutting plane.

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

    The beam that is cut by this instance.

  • ref_side_index (int, default: 0 ) –

    The reference side index of the beam to be cut. Default is 0 (i.e. RS1).

Returns:

  • class:`~compas_timber.fabrication.StepJoint` –
Source code in src/compas_timber/fabrication/step_joint.py
@classmethod
def from_plane_and_beam(cls, plane, beam, step_depth=20.0, heel_depth=0.0, tapered_heel=False, ref_side_index=0):
    """Create a StepJoint instance from a cutting surface and the beam it should cut. This could be the ref_side of the cross beam of a Joint and the main beam.

    Parameters
    ----------
    plane : :class:`~compas.geometry.Plane` or :class:`~compas.geometry.Frame`
        The cutting plane.
    beam : :class:`~compas_timber.elements.Beam`
        The beam that is cut by this instance.
    ref_side_index : int, optional
        The reference side index of the beam to be cut. Default is 0 (i.e. RS1).

    Returns
    -------
    :class:`~compas_timber.fabrication.StepJoint`

    """
    # type: (Plane|Frame, Beam, float, float, bool, int) -> StepJoint

    if isinstance(plane, Frame):
        plane = Plane.from_frame(plane)
    plane.normal = plane.normal * -1  # flip the plane normal to point towards the beam
    # define ref_side & ref_edge
    ref_side = beam.ref_sides[ref_side_index]
    ref_edge = Line.from_point_and_vector(ref_side.point, ref_side.xaxis)

    # calculate orientation
    orientation = cls._calculate_orientation(ref_side, plane)

    # calculate start_x
    point_start_x = intersection_line_plane(ref_edge, plane)
    if point_start_x is None:
        raise ValueError("Plane does not intersect with beam.")
    start_x = distance_point_point(ref_side.point, point_start_x)

    # calculate strut_inclination
    strut_inclination = cls._calculate_strut_inclination(ref_side, plane)

    # define step_shape
    step_shape = cls._define_step_shape(step_depth, heel_depth, tapered_heel)

    return cls(
        orientation, start_x, strut_inclination, step_depth, heel_depth, step_shape, ref_side_index=ref_side_index
    )
planes_from_params_and_beam ¤
planes_from_params_and_beam(beam)

Calculates the cutting planes from the machining parameters in this instance and the given beam

Parameters:

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

    The beam that is cut by this instance.

Returns:

  • class:`compas.geometry.Plane` –

    The cutting planes.

Source code in src/compas_timber/fabrication/step_joint.py
def planes_from_params_and_beam(self, beam):
    """Calculates the cutting planes from the machining parameters in this instance and the given beam

    Parameters
    ----------
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Returns
    -------
    :class:`compas.geometry.Plane`
        The cutting planes.
    """
    assert self.orientation is not None
    assert self.strut_inclination is not None
    assert self.step_shape is not None

    # first cut
    ref_side = beam.side_as_surface(self.ref_side_index)
    # second cut
    opp_side = beam.side_as_surface((self.ref_side_index + 2) % 4)

    _, beam_width = beam.get_dimensions_relative_to_side(self.ref_side_index)

    # Calculate the displacements for the cutting planes along the y-axis and x-axis
    y_displacement_end = self._calculate_y_displacement_end(beam_width, self.strut_inclination)
    x_displacement_end = self._calculate_x_displacement_end(beam_width, self.strut_inclination, self.orientation)
    x_displacement_heel = self._calculate_x_displacement_heel(
        self.heel_depth, self.strut_inclination, self.orientation
    )

    # Get the points at the start of the step, at the end and at the heel
    p_ref = planar_surface_point_at(ref_side, self.start_x, 0)
    p_opp = planar_surface_point_at(opp_side, self.start_x + x_displacement_end, beam.width)
    p_heel = planar_surface_point_at(ref_side, self.start_x + x_displacement_heel, 0)
    # Create cutting planes at the start of the step, at the end and at the heel
    cutting_plane_ref = Frame(p_ref, ref_side.frame.xaxis, ref_side.frame.yaxis)
    cutting_plane_opp = Frame(p_opp, ref_side.frame.xaxis, ref_side.frame.yaxis)
    cutting_plane_heel = Frame(p_heel, ref_side.frame.xaxis, ref_side.frame.yaxis)

    if self.orientation == OrientationType.START:
        rot_axis = cutting_plane_ref.yaxis
    else:
        rot_axis = -cutting_plane_ref.yaxis

    if self.step_shape == StepShapeType.STEP:
        return self._calculate_step_planes(cutting_plane_ref, cutting_plane_opp, y_displacement_end, rot_axis)
    elif self.step_shape == StepShapeType.HEEL:
        return self._calculate_heel_planes(cutting_plane_heel, cutting_plane_opp, rot_axis)
    elif self.step_shape == StepShapeType.TAPERED_HEEL:
        return self._calculate_heel_tapered_planes(cutting_plane_opp, y_displacement_end, rot_axis)
    elif self.step_shape == StepShapeType.DOUBLE:
        return self._calculate_double_planes(
            cutting_plane_heel, cutting_plane_opp, y_displacement_end, x_displacement_heel, rot_axis
        )
scale ¤
scale(factor)

Scale the parameters of this processing by a given factor.

Notes

Only distances are scaled, angles remain unchanged.

Parameters:

  • factor (float) –

    The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

Source code in src/compas_timber/fabrication/step_joint.py
def scale(self, factor):
    """Scale the parameters of this processing by a given factor.

    Notes
    -----
    Only distances are scaled, angles remain unchanged.

    Parameters
    ----------
    factor : float
        The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

    """
    self.start_x *= factor
    self.step_depth *= factor
    self.heel_depth *= factor
    self.tenon_width *= factor
    self.tenon_height *= factor
tenon_volume_from_params_and_beam ¤
tenon_volume_from_params_and_beam(beam)

Calculates the tenon volume from the machining parameters in this instance and the given beam

Parameters:

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

    The beam that is cut by this instance.

Returns:

  • class:`compas.geometry.Brep` –

    The tenon volume.

Source code in src/compas_timber/fabrication/step_joint.py
def tenon_volume_from_params_and_beam(self, beam):
    """Calculates the tenon volume from the machining parameters in this instance and the given beam

    Parameters
    ----------
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Returns
    -------
    :class:`compas.geometry.Brep`
        The tenon volume.

    """
    # type: (Beam) -> Brep
    assert self.tenon_width is not None
    assert self.tenon_height is not None

    # start with a plane aligned with the ref side but shifted to the start of the first cut
    ref_side = beam.ref_sides[self.ref_side_index]
    ref_surface = beam.side_as_surface(self.ref_side_index)
    beam_height = beam.side_as_surface((self.ref_side_index+1)%4).ysize

    # calculate the dx and dy of the tenon volume
    dx = beam_height/math.sin(math.radians(self.strut_inclination))
    dy = self.tenon_width

    start_x = self.start_x
    start_y = ref_surface.ysize/2

    # create a box volume for the tenon
    box_origin = planar_surface_point_at(ref_surface, start_x, start_y)
    box_frame = Frame(box_origin, ref_side.xaxis, ref_side.yaxis)
    tenon_box = Box(dx, dy, self.tenon_height, box_frame)

    # translate the box to the correct position and rotate it to the strut inclination
    xaxis_vector = box_frame.xaxis * (dx/2)
    zaxis_vector = -box_frame.zaxis * (self.tenon_height/2)
    if self.orientation == OrientationType.END:
        inclination = math.radians(180-self.strut_inclination)
        zaxis_vector = -zaxis_vector
    else:
        inclination = math.radians(self.strut_inclination)
    tenon_box.translate(xaxis_vector + zaxis_vector)

    # rotate the tenon volume to the strut inclination
    rotation = Rotation.from_axis_and_angle(ref_side.yaxis, inclination, box_frame.point)
    tenon_box.transform(rotation)

    # convert to Brep and trim with ref_side
    tenon_brep = Brep.from_box(tenon_box)
    try:
        tenon_brep.trim(ref_side)
    except Exception as e:
        raise FeatureApplicationError(
            None, tenon_brep, "Failed to trim tenon volume with cutting planes: {}".format(str(e))
        )
    return tenon_brep

StepJointNotch ¤

StepJointNotch(
    orientation=START,
    start_x=0.0,
    start_y=0.0,
    strut_inclination=90.0,
    notch_limited=False,
    notch_width=20.0,
    step_depth=20.0,
    heel_depth=20.0,
    strut_height=20.0,
    step_shape=DOUBLE,
    mortise=False,
    mortise_width=40.0,
    mortise_height=40.0,
    **kwargs,
)

Represents a Step Joint Notch feature to be made on a beam.

Parameters:

  • orientation (int, default: START ) –

    The orientation of the cut. Must be either OrientationType.START or OrientationType.END.

  • start_x (float, default: 0.0 ) –

    The start x-coordinate of the cut in parametric space of the reference side. -100000.0 < start_x < 100000.0.

  • start_y (float, default: 0.0 ) –

    The start y-coordinate of the notch in parametric space of the reference side. -50000.0 < start_y < 50000.0.

  • strut_inclination (float, default: 90.0 ) –

    The inclination of the strut. 0.1 < strut_inclination < 179.9.

  • notch_limited (bool, default: False ) –

    Whether the notch is limited. If True, the notch is limited by the start_y and notch_width values.

  • notch_width (float, default: 20.0 ) –

    The width of the notch. notch_width < 50000.0.

  • step_depth (float, default: 20.0 ) –

    The depth of the step. step_depth < 50000.0.

  • heel_depth (float, default: 20.0 ) –

    The depth of the heel. heel_depth < 50000.0.

  • strut_height (float, default: 20.0 ) –

    The height of the strut. It is the cross beam's height. strut_height < 50000.0.

  • step_shape (str, default: DOUBLE ) –

    The shape of the step. Must be either StepShapeType.DOUBLE, StepShapeType.STEP, StepShapeType.HEEL or StepShapeType.TAPERED_HEEL.

  • mortise (str, default: False ) –

    The presence of a mortise. Must be either 'no' or 'yes'.

  • mortise_width (float, default: 40.0 ) –

    The width of the mortise. mortise_width < 1000.0.

  • mortise_height (float, default: 40.0 ) –

    The height of the mortise. mortise_height < 1000.0.

Source code in src/compas_timber/fabrication/step_joint_notch.py
def __init__(
    self,
    orientation=OrientationType.START,
    start_x=0.0,
    start_y=0.0,
    strut_inclination=90.0,
    notch_limited=False,
    notch_width=20.0,
    step_depth=20.0,
    heel_depth=20.0,
    strut_height=20.0,
    step_shape=StepShapeType.DOUBLE,
    mortise=False,
    mortise_width=40.0,
    mortise_height=40.0,
    **kwargs
):
    super(StepJointNotch, self).__init__(**kwargs)
    self._orientation = None
    self._start_x = None
    self._start_y = None
    self._strut_inclination = None
    self._notch_limited = None
    self._notch_width = None
    self._step_depth = None
    self._heel_depth = None
    self._strut_height = None
    self._step_shape = None
    self._mortise = None
    self._mortise_width = None
    self._mortise_height = None

    self.orientation = orientation
    self.start_x = start_x
    self.start_y = start_y
    self.strut_inclination = strut_inclination
    self.notch_limited = notch_limited
    self.notch_width = notch_width
    self.step_depth = step_depth
    self.heel_depth = heel_depth
    self.strut_height = strut_height
    self.step_shape = step_shape
    self.mortise = mortise
    self.mortise_width = mortise_width
    self.mortise_height = mortise_height

Methods:¤

add_mortise ¤
add_mortise(mortise_width, mortise_height)

Add a mortise to the existing StepJointNotch instance.

Parameters:

  • mortise_width (float) –

    The width of the mortise. mortise_width < 1000.0.

  • mortise_height (float) –

    The height of the mortise. mortise_height < 1000.0.

Source code in src/compas_timber/fabrication/step_joint_notch.py
def add_mortise(self, mortise_width, mortise_height):
    """Add a mortise to the existing StepJointNotch instance.

    Parameters
    ----------
    mortise_width : float
        The width of the mortise. mortise_width < 1000.0.
    mortise_height : float
        The height of the mortise. mortise_height < 1000.0.
    """
    self.mortise = True
    self.mortise_width = mortise_width
    self.mortise_height = mortise_height if mortise_height > self.step_depth else self.step_depth
apply ¤
apply(geometry, beam)

Apply the feature to the beam geometry.

Parameters:

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

    The beam geometry to be processed.

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

    The beam that is milled by this instance.

Raises:

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

    If the cutting planes do not create a volume that itersects with beam geometry or any step fails.

Returns:

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

    The resulting geometry after processing

Source code in src/compas_timber/fabrication/step_joint_notch.py
def apply(self, geometry, beam):
    """Apply the feature to the beam geometry.

    Parameters
    ----------
    geometry : :class:`~compas.geometry.Brep`
        The beam geometry to be processed.
    beam : :class:`compas_timber.elements.Beam`
        The beam that is milled by this instance.

    Raises
    ------
    :class:`~compas_timber.errors.FeatureApplicationError`
        If the cutting planes do not create a volume that itersects with beam geometry or any step fails.

    Returns
    -------
    :class:`~compas.geometry.Brep`
        The resulting geometry after processing

    """
    # type: (Brep, Beam) -> Brep

    # get cutting planes from params
    try:
        cutting_planes = self.planes_from_params_and_beam(beam)
    except ValueError as e:
        raise FeatureApplicationError(
            None, geometry.transformed(beam.modeltransformation), "Failed to generate cutting planes from parameters and beam: {}".format(str(e))
        )

    [plane.transform(beam.transformation_to_local()) for plane in cutting_planes]

    # get notch volume
    subtraction_volume = geometry.copy()
    if self.step_shape == StepShapeType.DOUBLE:
        # trim geometry with first and last cutting plane
        try:
            for cutting_plane in [cutting_planes[1], cutting_planes[3]]:
                cutting_plane.normal = cutting_plane.normal * -1
                subtraction_volume.trim(cutting_plane)
        except Exception as e:
            raise FeatureApplicationError(
                cutting_planes[-1].transformed(beam.modeltransformation),
                subtraction_volume.transformed(beam.modeltransformation),
                "Failed to trim geometry with cutting plane: {}".format(str(e)),
            )
        # trim geometry with two middle cutting planes
        trimmed_geometies = []
        for cutting_plane in [cutting_planes[0], cutting_planes[2]]:
            cutting_plane.normal = cutting_plane.normal * -1
            try:
                trimmed_geometies.append(subtraction_volume.trimmed(cutting_plane))
            except Exception as e:
                raise FeatureApplicationError(
                    cutting_plane.transformed(beam.modeltransformation),
                    subtraction_volume.transformed(beam.modeltransformation),
                    "Failed to trim geometry with cutting plane: {}".format(str(e)),
                )
        subtraction_volume = trimmed_geometies

    else:
        for cutting_plane in cutting_planes:
            cutting_plane.normal = cutting_plane.normal * -1
            try:
                subtraction_volume.trim(cutting_plane)
            except Exception as e:
                raise FeatureApplicationError(
                    cutting_plane.transformed(beam.modeltransformation),
                    subtraction_volume.transformed(beam.modeltransformation),
                    "Failed to trim geometry with cutting planes: {}".format(str(e)),
                )
    ## subtract volume from geometry
    if isinstance(subtraction_volume, list):
        for sub_vol in subtraction_volume:
            try:
                geometry = brep_difference_first(geometry, sub_vol)
            except Exception as e:
                raise FeatureApplicationError(
                    sub_vol.transformed(beam.modeltransformation),
                    geometry.transformed(beam.modeltransformation),
                    "Failed to subtract volume from geometry: {}".format(str(e)),
                )
    else:
        geometry = brep_difference_first(geometry, subtraction_volume)

    ## add mortise
    if self.mortise and self.step_shape != StepShapeType.DOUBLE: # TODO: check if mortise applies only to step in BTLx
        # create mortise box and convert to Brep
        mortise_box = self.mortise_volume_from_params_and_beam(beam)
        mortise_volume = Brep.from_box(mortise_box)
        mortise_volume.transform(beam.transformation_to_local())

        # trim mortise volume at step
        origin = intersection_plane_plane(cutting_planes[0], cutting_planes[1])[0]
        normal = mortise_box.frame.xaxis
        if self.step_shape != StepShapeType.STEP:
            normal = -normal
        cutting_plane_step = Plane(origin, normal)
        try:
            mortise_volume.trim(cutting_plane_step)
        except Exception as e:
            raise FeatureApplicationError(
                cutting_plane_step.transformed(beam.modeltransformation),
                mortise_volume.transformed(beam.modeltransformation),
                "Failed to trim mortise volume with step cutting plane: {}".format(str(e)),
            )
        try:
            geometry = brep_difference_first(geometry, mortise_volume)
        except Exception as e:
            raise FeatureApplicationError(
                mortise_volume.transformed(beam.modeltransformation),
                subtraction_volume.transformed(beam.modeltransformation),
                "Failed to subtract mortise volume from geometry: {}".format(str(e)),
            )
    return geometry
from_plane_and_beam classmethod ¤
from_plane_and_beam(
    plane,
    beam,
    start_y=0.0,
    notch_limited=False,
    notch_width=20.0,
    step_depth=20.0,
    heel_depth=0.0,
    strut_height=20.0,
    tapered_heel=False,
    ref_side_index=0,
)

Create a StepJointNotch instance from a cutting surface and the beam it should cut. This could be the ref_side of the main beam of a Joint and the cross beam.

Parameters:

  • plane –

    The cutting plane.

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

    The beam that is cut by this instance.

  • ref_side_index (int, default: 0 ) –

    The reference side index of the beam to be cut. Default is 0 (i.e. RS1).

Returns:

  • class:`~compas_timber.fabrication.StepJointNotch` –
Source code in src/compas_timber/fabrication/step_joint_notch.py
@classmethod
def from_plane_and_beam(
    cls,
    plane,
    beam,
    start_y=0.0,
    notch_limited=False,
    notch_width=20.0,
    step_depth=20.0,
    heel_depth=0.0,
    strut_height=20.0,
    tapered_heel=False,
    ref_side_index=0,
):
    """Create a StepJointNotch instance from a cutting surface and the beam it should cut. This could be the ref_side of the main beam of a Joint and the cross beam.

    Parameters
    ----------
    plane: :class:`~compas.geometry.Planae` or :class:`~compas.geometry.Frame`
        The cutting plane.
    beam : :class:`~compas_timber.elements.Beam`
        The beam that is cut by this instance.
    ref_side_index : int, optional
        The reference side index of the beam to be cut. Default is 0 (i.e. RS1).

    Returns
    -------
    :class:`~compas_timber.fabrication.StepJointNotch`

    """
    # type: (Plane|Frame, Beam, float, bool, float, float, float, float, bool, int) -> StepJointNotch

    if isinstance(plane, Frame):
        plane = Plane.from_frame(plane)
    # define ref_side & ref_edge
    ref_side = beam.ref_sides[ref_side_index]
    ref_edge = Line.from_point_and_vector(ref_side.point, ref_side.xaxis)

    # calculate orientation
    orientation = cls._calculate_orientation(ref_side, plane)

    # calculate start_x
    point_start_x = intersection_line_plane(ref_edge, plane)
    if point_start_x is None:
        raise ValueError("Surface does not intersect with beam.")
    start_x = distance_point_point(ref_side.point, point_start_x)

    # calculate strut_inclination
    strut_inclination = cls._calculate_strut_inclination(ref_side, plane, orientation)

    # calculate notch_width
    if notch_limited:
        notch_width = notch_width if notch_width < beam.width else beam.width
    else:
        notch_width = beam.width
        start_y = 0.0

    # define step_shape
    step_shape = cls._define_step_shape(step_depth, heel_depth, tapered_heel)

    return cls(
        orientation,
        start_x,
        start_y,
        strut_inclination,
        notch_limited,
        notch_width,
        step_depth,
        heel_depth,
        strut_height,
        step_shape,
        ref_side_index=ref_side_index,
    )
mortise_volume_from_params_and_beam ¤
mortise_volume_from_params_and_beam(beam)

Calculates the mortise volume from the machining parameters in this instance and the given beam

Parameters:

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

    The beam that is cut by this instance.

Returns:

  • class:`compas.geometry.Box` –

    The mortise volume.

Source code in src/compas_timber/fabrication/step_joint_notch.py
def mortise_volume_from_params_and_beam(self, beam):
    """Calculates the mortise volume from the machining parameters in this instance and the given beam

    Parameters
    ----------
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Returns
    -------
    :class:`compas.geometry.Box`
        The mortise volume.

    """
    # type: (Beam) -> Box

    assert self.strut_inclination is not None
    assert self.step_shape is not None
    assert self.strut_height is not None
    assert self.notch_width is not None
    assert self.mortise_width is not None
    assert self.mortise_height is not None

    # start with a plane aligned with the ref side but shifted to the start of the first cut
    ref_side = beam.side_as_surface(self.ref_side_index)
    ref_side.point -= ref_side.zaxis * (self.mortise_height / 2)

    dx = self.strut_height / math.sin(math.radians(self.strut_inclination))
    dy = self.mortise_width

    start_x = self.start_x + dx/2 if self.orientation == OrientationType.START else self.start_x - dx/2
    start_y = ref_side.ysize / 2

    box_origin = planar_surface_point_at(ref_side, start_x, start_y)
    box_frame =  Frame(box_origin, ref_side.frame.xaxis, ref_side.frame.yaxis)
    if self.orientation == OrientationType.END:
        box_frame.xaxis = -box_frame.xaxis

    return Box(dx, dy, self.mortise_height, box_frame)
planes_from_params_and_beam ¤
planes_from_params_and_beam(beam)

Calculates the cutting planes from the machining parameters in this instance and the given beam

Parameters:

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

    The beam that is cut by this instance.

Returns:

  • class:`compas.geometry.Plane` –

    The cutting planes.

Source code in src/compas_timber/fabrication/step_joint_notch.py
def planes_from_params_and_beam(self, beam):
    """Calculates the cutting planes from the machining parameters in this instance and the given beam

    Parameters
    ----------
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Returns
    -------
    :class:`compas.geometry.Plane`
        The cutting planes.
    """
    assert self.strut_inclination is not None
    assert self.step_shape is not None
    assert self.strut_height is not None

    # Start with a plane aligned with the ref side but shifted to the start of the first cut
    ref_side = beam.side_as_surface(self.ref_side_index)

    if self.step_shape == StepShapeType.STEP:
        return self._calculate_step_planes(ref_side)
    elif self.step_shape == StepShapeType.HEEL:
        return self._calculate_heel_planes(ref_side)
    elif self.step_shape == StepShapeType.TAPERED_HEEL:
        return self._calculate_tapered_heel_planes(ref_side)
    elif self.step_shape == StepShapeType.DOUBLE:
        return self._calculate_double_planes(ref_side)
scale ¤
scale(factor)

Scale the parameters of this processing by a given factor.

Notes

Only distances are scaled, angles remain unchanged.

Parameters:

  • factor (float) –

    The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

Source code in src/compas_timber/fabrication/step_joint_notch.py
def scale(self, factor):
    """Scale the parameters of this processing by a given factor.

    Notes
    -----
    Only distances are scaled, angles remain unchanged.

    Parameters
    ----------
    factor : float
        The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

    """
    self.start_x *= factor
    self.start_y *= factor
    self.notch_width *= factor
    self.step_depth *= factor
    self.heel_depth *= factor
    self.strut_height *= factor
    self.mortise_width *= factor
    self.mortise_height *= factor

StepShapeType ¤

Enum for the step shape of the cut.

Attributes:

  • STEP (literal(step)) –

    A step shape.

  • HEEL (literal(heel)) –

    A heel shape.

  • TAPERED_HEEL (literal(taperedheel)) –

    A tapered heel shape.

  • DOUBLE (literal(double)) –

    A double shape.

Tenon ¤

Tenon(
    orientation=START,
    start_x=0.0,
    start_y=50.0,
    start_depth=50.0,
    angle=90.0,
    inclination=90.0,
    rotation=90.0,
    length_limited_top=True,
    length_limited_bottom=True,
    length=80.0,
    width=40.0,
    height=28.0,
    shape=AUTOMATIC,
    shape_radius=20.0,
    chamfer=False,
    **kwargs,
)

Represents a Tenon feature to be made on a beam.

Parameters:

  • orientation (int, default: START ) –

    The orientation of the cut. Must be either OrientationType.START or OrientationType.END.

  • start_x (float, default: 0.0 ) –

    The start x-coordinate of the cut in parametric space of the reference side. Distance from the beam start to the reference point. -100000.0 < start_x < 100000.0.

  • start_y (float, default: 50.0 ) –

    The start y-coordinate of the cut in parametric space of the reference side. Distance from the reference edge to the reference point. -5000.0 < start_y < 5000.0.

  • start_depth (float, default: 50.0 ) –

    The start depth of the cut in parametric space of the reference side. Margin on the reference side. -5000.0 < start_depth < 5000.0.

  • angle (float, default: 90.0 ) –

    The angle of the cut. Angle between edge and reference edge. 0.1 < angle < 179.9.

  • inclination (float, default: 90.0 ) –

    The inclination of the cut. Inclination between face and reference side. 0.1 < inclination < 179.9.

  • rotation (float, default: 90.0 ) –

    The rotation of the cut. Angle between axis of the tenon and rederence side. 0.1 < rotation < 179.9.

  • length_limited_top (bool, default: True ) –

    Whether the top length of the cut is limited. True or False.

  • length_limited_bottom (bool, default: True ) –

    Whether the bottom length of the cut is limited. True or False.

  • length (float, default: 80.0 ) –

    The length of the cut. 0.0 < length < 5000.0.

  • width (float, default: 40.0 ) –

    The width of the cut. 0.0 < width < 1000.0.

  • height (float, default: 28.0 ) –

    The height of the tenon. 0.0 < height < 1000.0.

  • shape (str, default: AUTOMATIC ) –

    The shape of the cut. Must be either 'automatic', 'square', 'round', 'rounded', or 'radius'.

  • shape_radius (float, default: 20.0 ) –

    The radius of the shape of the cut. 0.0 < shape_radius < 1000.0.

  • chamfer (bool, default: False ) –

    Whether the edges of the tenon are chamfered. True or False.

Source code in src/compas_timber/fabrication/tenon.py
def __init__(
    self,
    orientation=OrientationType.START,
    start_x=0.0,
    start_y=50.0,
    start_depth=50.0,
    angle=90.0,
    inclination=90.0,
    rotation=90.0,
    length_limited_top=True,
    length_limited_bottom=True,
    length=80.0,
    width=40.0,
    height=28.0,
    shape=TenonShapeType.AUTOMATIC,
    shape_radius=20.0,
    chamfer=False,
    **kwargs
):
    super(Tenon, self).__init__(**kwargs)
    self._orientation = None
    self._start_x = None
    self._start_y = None
    self._start_depth = None
    self._angle = None
    self._inclination = None
    self._rotation = None
    self._length_limited_top = None
    self._length_limited_bottom = None
    self._length = None
    self._width = None
    self._height = None
    self._shape = None
    self._shape_radius = None
    self._chamfer = None

    self.orientation = orientation
    self.start_x = start_x
    self.start_y = start_y
    self.start_depth = start_depth
    self.angle = angle
    self.inclination = inclination
    self.rotation = rotation
    self.length_limited_top = length_limited_top
    self.length_limited_bottom = length_limited_bottom
    self.length = length
    self.width = width
    self.height = height
    self.shape = shape
    self.shape_radius = shape_radius
    self.chamfer = chamfer

Methods:¤

apply ¤
apply(geometry, beam)

Apply the feature to the beam geometry.

Parameters:

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

    The geometry to be processed.

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

    The beam that is milled by this instance.

Raises:

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

    If the cutting frames do not create a volume that itersects with beam geometry or any step fails.

Returns:

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

    The resulting geometry after processing

Source code in src/compas_timber/fabrication/tenon.py
def apply(self, geometry, beam):
    """Apply the feature to the beam geometry.

    Parameters
    ----------
    geometry : :class:`compas.geometry.Brep`
        The geometry to be processed.

    beam : :class:`compas_timber.elements.Beam`
        The beam that is milled by this instance.

    Raises
    ------
    :class:`~compas_timber.errors.FeatureApplicationError`
        If the cutting frames do not create a volume that itersects with beam geometry or any step fails.

    Returns
    -------
    :class:`~compas.geometry.Brep`
        The resulting geometry after processing

    """
    # type: (Brep, Beam) -> Brep
    # get cutting plane from params and beam
    try:
        cutting_plane = Plane.from_frame(self.frame_from_params_and_beam(beam))
    except ValueError as e:
        raise FeatureApplicationError(
            None, geometry.transformed(beam.modeltransformation), "Failed to generate cutting frame from parameters and beam: {}".format(str(e))
        )

    cutting_plane.transform(beam.transformation_to_local())

    # trim geometry with cutting plane
    try:
        geometry.trim(cutting_plane)
    except Exception as e:
        raise FeatureApplicationError(
            cutting_plane.transformed(beam.modeltransformation),
            geometry.transformed(beam.modeltransformation),
            "Failed to trim geometry with cutting plane: {}".format(str(e)),
        )
    # get tenon volume from params and beam
    try:
        tenon_volume = self.volume_from_params_and_beam(beam)
    except ValueError as e:
        raise FeatureApplicationError(
            None, geometry.transformed(beam.modeltransformation), "Failed to generate tenon volume from parameters and beam: {}".format(str(e))
        )
    # fillet the edges of the volume based on the shape
    if self.shape != TenonShapeType.SQUARE:
        try:
            edges = tenon_volume.edges[:8]
            tenon_volume.fillet(self.shape_radius, edges)
        except Exception as e:
            # tenon_volume is still in model space here (not yet localized below), geometry is local
            raise FeatureApplicationError(
                tenon_volume,
                geometry.transformed(beam.modeltransformation),
                "Failed to fillet the edges of the tenon volume based on the shape: {}".format(str(e)),
            )

    tenon_volume.transform(beam.transformation_to_local())

    # remove any parts of the volume that exceed the beam geometry. Fails silently.
    for frame in beam.ref_sides[:4]:
        try:
            tenon_volume = tenon_volume.trimmed(frame)
        except Exception:
            pass # Fail silently since it won't be possible to trim the tenon if it doesn't exceed the beam geometry.
    # add tenon volume to geometry
    try:
        geometry = brep_union_first(geometry, tenon_volume)
    except Exception as e:
        raise FeatureApplicationError(
            tenon_volume.transformed(beam.modeltransformation),
            geometry.transformed(beam.modeltransformation),
            "Failed to add tenon volume to geometry: {}".format(str(e)),
        )
    return geometry
frame_from_params_and_beam ¤
frame_from_params_and_beam(beam)

Calculates the cutting plane from the machining parameters in this instance and the given beam.

Parameters:

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

    The beam that is cut by this instance.

Returns:

  • class:`compas.geometry.Frame` –

    The cutting Frame.

Source code in src/compas_timber/fabrication/tenon.py
def frame_from_params_and_beam(self, beam):
    """
    Calculates the cutting plane from the machining parameters in this instance and the given beam.

    Parameters
    ----------
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Returns
    -------
    :class:`compas.geometry.Frame`
        The cutting Frame.
    """
    # type: (Beam) -> Frame
    assert self.orientation is not None
    assert self.start_x is not None
    assert self.start_y is not None
    assert self.angle is not None
    assert self.inclination is not None
    assert self.rotation is not None
    assert self.start_depth is not None

    # get the reference side surface of the beam
    ref_side = beam.side_as_surface(self.ref_side_index)

    # move the reference side surface to the start depth
    ref_side.translate(-ref_side.frame.normal * self.start_depth)

    # convert angles to radians
    inclination_radians = math.radians(self.inclination)
    angle_radians = math.radians(self.angle + 90)

    # calculate the point of origin based on orientation
    p_origin = planar_surface_point_at(ref_side, self.start_x, self.start_y)
    if self.orientation == OrientationType.END:
        yaxis = ref_side.frame.yaxis
    else:
        yaxis = -ref_side.frame.yaxis
        inclination_radians += math.pi

    # create the initial cutting plane
    cutting_frame = Frame(p_origin, -ref_side.frame.xaxis, yaxis)

    # apply rotations to the cutting plane based on angle and inclination parameters
    rot_a = Rotation.from_axis_and_angle(cutting_frame.zaxis, angle_radians, point=p_origin)
    rot_b = Rotation.from_axis_and_angle(cutting_frame.yaxis, inclination_radians, point=p_origin)
    cutting_frame.transform(rot_a * rot_b)

    # for simplicity align normal towards x-axis
    cutting_frame = Frame(cutting_frame.point, -cutting_frame.yaxis, cutting_frame.xaxis)

    # apply rotation based on the rotation parameter
    rot_angle = math.radians(self.rotation - 90)
    if self.orientation == OrientationType.START:
        rot_angle = -rot_angle
    rotation = Rotation.from_axis_and_angle(cutting_frame.normal, rot_angle, cutting_frame.point)
    cutting_frame.transform(rotation)

    return cutting_frame
from_plane_and_beam classmethod ¤
from_plane_and_beam(
    plane,
    beam,
    start_y=0.0,
    start_depth=0.0,
    rotation=0.0,
    length_limited_top=True,
    length_limited_bottom=True,
    length=80.0,
    width=40.0,
    height=40.0,
    shape=AUTOMATIC,
    shape_radius=20.0,
    chamfer=False,
    ref_side_index=0,
)

Create a Tenon instance from a cutting plane and the beam it should cut. This could be the ref_side of the cross beam of a Joint and the main beam.

Parameters:

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

    The cutting plane.

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

    The beam that is cut by this instance.

  • start_y (float, default: 0.0 ) –

    The start y-coordinate of the cut in parametric space of the reference side. Default is 0.0.

  • start_depth (float, default: 0.0 ) –

    The start depth of the tenon, which is an offset along the normal of the reference side. Default is 50.0.

  • rotation (float, default: 0.0 ) –

    The angle of rotation of the tenon. Default is 0.0.

  • length_limited_top (bool, default: True ) –

    Whether the top length of the tenon is limited. Default is True.

  • length_limited_bottom (bool, default: True ) –

    Whether the bottom length of the tenon is limited. Default is True.

  • length (float, default: 80.0 ) –

    The length of the tenon. Default is 80.0.

  • width (float, default: 40.0 ) –

    The width of the bottom edge of the tenon. Default is 40.0.

  • height (float, default: 40.0 ) –

    The height of the tenon. Related to the dovetail tool and can be defined using the DovetailTenon.define_dovetail_tool() method. Default is 28.0.

  • shape (str, default: AUTOMATIC ) –

    The shape of the tenon. Default is 'automatic'.

  • shape_radius (float, default: 20.0 ) –

    The radius of the shape of the tenon. Related to the dovetail tool and can be defined using the DovetailTenon.define_dovetail_tool() method. Default is 20.0.

  • chamfer (bool, default: False ) –

    Whether the edges of the tenon are chamfered. Default is False.

  • ref_side_index (int, default: 0 ) –

    The reference side index of the beam to be cut. Default is 0 (i.e. RS1).

Returns:

  • class:`~compas_timber.fabrication.Tenon` –
Source code in src/compas_timber/fabrication/tenon.py
@classmethod
def from_plane_and_beam(
    cls,
    plane,
    beam,
    start_y=0.0,
    start_depth=0.0,
    rotation=0.0,
    length_limited_top=True,
    length_limited_bottom=True,
    length=80.0,
    width=40.0,
    height=40.0,
    shape=TenonShapeType.AUTOMATIC,
    shape_radius=20.0,
    chamfer=False,
    ref_side_index=0,
):
    """Create a Tenon instance from a cutting plane and the beam it should cut. This could be the ref_side of the cross beam of a Joint and the main beam.

    Parameters
    ----------
    plane : :class:`~compas.geometry.Plane` or :class:`~compas.geometry.Frame`
        The cutting plane.
    beam : :class:`~compas_timber.elements.Beam`
        The beam that is cut by this instance.
    start_y : float, optional
        The start y-coordinate of the cut in parametric space of the reference side. Default is 0.0.
    start_depth : float, optional
        The start depth of the tenon, which is an offset along the normal of the reference side. Default is 50.0.
    rotation : float, optional
        The angle of rotation of the tenon. Default is 0.0.
    length_limited_top : bool, optional
        Whether the top length of the tenon is limited. Default is True.
    length_limited_bottom : bool, optional
        Whether the bottom length of the tenon is limited. Default is True.
    length : float, optional
        The length of the tenon. Default is 80.0.
    width : float, optional
        The width of the bottom edge of the tenon. Default is 40.0.
    height : float, optional
        The height of the tenon. Related to the dovetail tool and can be defined using the `DovetailTenon.define_dovetail_tool()` method. Default is 28.0.
    shape : str, optional
        The shape of the tenon. Default is 'automatic'.
    shape_radius : float, optional
        The radius of the shape of the tenon. Related to the dovetail tool and can be defined using the `DovetailTenon.define_dovetail_tool()` method. Default is 20.0.
    chamfer : bool, optional
        Whether the edges of the tenon are chamfered. Default is False.
    ref_side_index : int, optional
        The reference side index of the beam to be cut. Default is 0 (i.e. RS1).

    Returns
    -------
    :class:`~compas_timber.fabrication.Tenon`

    """
    # type: (Plane|Frame, Beam, float, float, float, bool, bool, float, float, float, str, float, bool, int) -> Tenon
    if isinstance(plane, Frame):
        plane = Plane.from_frame(plane)

    # get ref_side & ref_edge
    ref_side = beam.ref_sides[ref_side_index]
    ref_edge = Line.from_point_and_vector(ref_side.point, ref_side.xaxis)

    # calculate orientation
    orientation = cls._calculate_orientation(ref_side, plane)

    # calculate angle
    angle = cls._calculate_angle(ref_side, plane)

    # calculate inclination
    inclination = cls._calculate_inclination(ref_side, plane, orientation, angle)

    # calculate rotation
    rotation = cls._calculate_rotation(orientation, rotation) # TODO: calculate and include the rotation of the beam and the plane

    # calculate start_y
    start_y = cls._calculate_start_y(beam, orientation, start_y, ref_side_index)

    # calculate start_depth
    start_depth += cls._calculate_start_depth(beam, inclination, length, ref_side_index)

    # override start_depth and length if not limited
    if not length_limited_top:
        start_depth = 0.0
    if not length_limited_bottom:
        beam_height = beam.get_dimensions_relative_to_side(ref_side_index)[1]
        length = beam_height / math.sin(math.radians(inclination)) - start_depth

    # calculate start_x
    start_x = cls._calculate_start_x(
        ref_side,
        ref_edge,
        plane,
        orientation,
        start_y,
        start_depth,
        angle,
    )

    # calculate radius based on shape
    if shape == TenonShapeType.ROUND:
        shape_radius = width / 2

    return cls(
        orientation,
        start_x,
        start_y,
        start_depth,
        angle,
        inclination,
        rotation,
        length_limited_top,
        length_limited_bottom,
        length,
        width,
        height,
        shape,
        shape_radius,
        chamfer,
        ref_side_index=ref_side_index,
    )
scale ¤
scale(factor)

Scale the parameters of this processing by a given factor.

Notes

Only distances are scaled, angles remain unchanged.

Parameters:

  • factor (float) –

    The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

Source code in src/compas_timber/fabrication/tenon.py
def scale(self, factor):
    """Scale the parameters of this processing by a given factor.

    Notes
    -----
    Only distances are scaled, angles remain unchanged.

    Parameters
    ----------
    factor : float
        The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

    """
    self.start_x *= factor
    self.start_y *= factor
    self.start_depth *= factor
    self.length *= factor
    self.width *= factor
    self.height *= factor
    self.shape_radius *= factor
volume_from_params_and_beam ¤
volume_from_params_and_beam(beam)

Calculates the tenon volume from the machining parameters in this instance and the given beam.

Parameters:

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

    The beam that is cut by this instance.

Returns:

  • class:`compas.geometry.Brep` –

    The tenon volume.

Source code in src/compas_timber/fabrication/tenon.py
def volume_from_params_and_beam(self, beam):
    """Calculates the tenon volume from the machining parameters in this instance and the given beam.

    Parameters
    ----------
    beam : :class:`compas_timber.elements.Beam`
        The beam that is cut by this instance.

    Returns
    -------
    :class:`compas.geometry.Brep`
        The tenon volume.

    """
    # type: (Beam) -> Brep
    assert self.length is not None
    assert self.width is not None
    assert self.height is not None

    cutting_frame = self.frame_from_params_and_beam(beam)
    # translate the cutting frame to the center of the tenon
    translation_vector = (cutting_frame.normal * self.height - cutting_frame.yaxis * self.length)
    cutting_frame.translate(translation_vector * 0.5)

    # get the tenon as a box
    tenon_box = Box(self.width, self.length, self.height, cutting_frame)
    return Brep.from_box(tenon_box)

TenonShapeType ¤

Enum for the tenon shape of the cut.

Attributes:

  • AUTOMATIC (literal(automatic)) –

    Automatic tenon shape.

  • SQUARE (literal(square)) –

    Square tenon shape.

  • ROUND (literal(round)) –

    Round tenon shape.

  • ROUNDED (literal(rounded)) –

    Rounded tenon shape.

  • RADIUS (literal(radius)) –

    Radius tenon shape.

Text ¤

Text(
    text,
    start_x=0.0,
    start_y=0.0,
    angle=0.0,
    alignment_vertical=BOTTOM,
    alignment_horizontal=LEFT,
    alignment_multiline=LEFT,
    stacked_marking=False,
    text_height_auto=True,
    text_height=20.0,
    **kwargs,
)

Represents a Text feature to be made on a beam.

Parameters:

  • text (str) –

    The text to be engraved on the beam.

  • start_x (float, default: 0.0 ) –

    The start x-coordinate of the cut in parametric space of the reference side. -100000.0 < start_x < 100000.0. Default is 0.0.

  • start_y (float, default: 0.0 ) –

    The start y-coordinate of the cut in parametric space of the reference side. -50000.0 < start_y < 50000.0. Default is 0.0.

  • angle (float, default: 0.0 ) –

    The horizontal angle of the first cut. -180.0 < angle < 180.0. Default is 0.0.

  • alignment_vertical (`AlignmentType.BOTTOM`, `AlignmentType.CENTER` or `AlignmentType.TOP`, default: `AlignmentType.BOTTOM` ) –

    The vertical alignment of the text. Default is AlignmentType.BOTTOM.

  • alignment_horizontal (`AlignmentType.LEFT`, `AlignmentType.CENTER` or `AlignmentType.RIGHT`, default: `AlignmentType.LEFT` ) –

    The horizontal alignment of the text. Default is AlignmentType.LEFT.

  • alignment_multiline (`AlignmentType.LEFT`, `AlignmentType.CENTER` or `AlignmentType.RIGHT`, default: `AlignmentType.LEFT` ) –

    The alignment of the text in multiline mode. Default is AlignmentType.LEFT.

  • stacked_marking (bool, default: False ) –

    If the text is a stacked marking. Default is False.

  • text_height_auto (bool, default: True ) –

    If the text height is automatically calculated. Default is True.

  • text_height (float, default: 20.0 ) –

    The height of the text. 0 < text_height < 50000.0. Default is 20.0.

Source code in src/compas_timber/fabrication/text.py
def __init__(
    self,
    text,
    start_x=0.0,
    start_y=0.0,
    angle=0.0,
    alignment_vertical=AlignmentType.BOTTOM,
    alignment_horizontal=AlignmentType.LEFT,
    alignment_multiline=AlignmentType.LEFT,
    stacked_marking=False,
    text_height_auto=True,
    text_height=20.0,
    **kwargs
):
    super(Text, self).__init__(**kwargs)
    self._start_x = None
    self._start_y = None
    self._angle = None
    self._alignment_vertical = None
    self._alignment_horizontal = None
    self._alignment_multiline = None
    self._stacked_marking = None
    self._text_height_auto = None
    self._text_height = None
    self._text = None

    self.start_x = start_x
    self.start_y = start_y
    self.angle = angle
    self.alignment_vertical = alignment_vertical
    self.alignment_horizontal = alignment_horizontal
    self.alignment_multiline = alignment_multiline
    self.stacked_marking = stacked_marking
    self.text_height_auto = text_height_auto
    self.text_height = text_height
    self.text = text

Methods:¤

apply ¤
apply(geometry, element)

Apply the feature to the element geometry: engrave the text into it.

Every straight stroke of the text becomes a groove, ENGRAVING times the text height wide and deep. Strokes that miss the element (running off the face, or over a cut) are left out.

Parameters:

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

    The element geometry to engrave, in the element's local coordinates.

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

    The element the text is engraved on.

Returns:

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

    The resulting geometry after processing.

Source code in src/compas_timber/fabrication/text.py
def apply(self, geometry, element):
    """Apply the feature to the element geometry: engrave the text into it.

    Every straight stroke of the text becomes a groove, `ENGRAVING` times the text height wide and deep.
    Strokes that miss the element (running off the face, or over a cut) are left out.

    Parameters
    ----------
    geometry : :class:`~compas.geometry.Brep`
        The element geometry to engrave, in the element's local coordinates.
    element : :class:`~compas_timber.elements.Beam`
        The element the text is engraved on.

    Returns
    -------
    :class:`~compas.geometry.Brep`
        The resulting geometry after processing.

    """
    to_local = element.transformation_to_local()
    for box in self.grooves_for_element(element):
        groove = Brep.from_box(box.transformed(to_local))
        try:
            geometry = brep_difference_first(geometry, groove)
        except IndexError:
            # a groove is far too small to remove the whole element: an empty result means
            # this stroke misses it (it runs off the face, or over a cut), so there is nothing to engrave
            continue
    return geometry
create_text_curves_for_element ¤
create_text_curves_for_element(element)

This returns translated and scaled curves which correspond to the text and the element the text is engraved on.

Parameters:

  • element (:class:`compas_timber.elements.Beam`) –

    The beam on which the text is engraved.

Returns:

  • list[:class:`compas.geometry.Curve`] –

    The curves representing the text.

Source code in src/compas_timber/fabrication/text.py
def create_text_curves_for_element(self, element):
    """This returns translated and scaled curves which correspond to the text and the element the text is engraved on.

    Parameters
    ----------
    element : :class:`compas_timber.elements.Beam`
        The beam on which the text is engraved.

    Returns
    -------
    list[:class:`compas.geometry.Curve`]
        The curves representing the text.
    """
    assert self.text
    assert self.text_height is not None
    assert self.start_x is not None
    assert self.start_y is not None

    self._load_character_dict()

    ref_side_index = self.ref_side_index or 0
    face = element.ref_sides[ref_side_index]
    string_curves = []
    x_pos = 0
    for char in self.text:
        curves = Text._CHARACTER_DICT[char]["curves"]
        # the glyphs come with some space of their own on the left: move each one so its
        # first stroke starts at x_pos, so every pair of letters is `LETTER_SPACING` apart
        left = min((point[0] for crv in curves for point in crv.points), default=0.0)
        string_curves.extend(crv.translated([x_pos - left, 0, 0]) for crv in curves)
        x_pos += Text._CHARACTER_DICT[char]["width"] + self.LETTER_SPACING
    x_pos -= self.LETTER_SPACING  # no space after the last letter
    x_pos *= self.text_height

    if self.alignment_vertical == AlignmentType.BOTTOM:
        y_offset = 0
    elif self.alignment_vertical == AlignmentType.CENTER:
        y_offset = -self.text_height / 2
    elif self.alignment_vertical == AlignmentType.TOP:
        y_offset = -self.text_height

    if self.alignment_horizontal == AlignmentType.LEFT:
        x_offset = 0
    elif self.alignment_horizontal == AlignmentType.CENTER:
        x_offset = -x_pos / 2
    elif self.alignment_horizontal == AlignmentType.RIGHT:
        x_offset = -x_pos


    for crv in string_curves:
        for pt in crv.points:
            pt.transform(Scale.from_factors([self.text_height]*3))
            pt.translate([self.start_x+x_offset, self.start_y+y_offset, 0])
            pt.transform(Transformation.from_frame_to_frame(Frame.worldXY(), face))
    return string_curves
grooves_for_element ¤
grooves_for_element(element)

The volumes engraved by this text: one box per straight stroke, centered on the reference side.

Parameters:

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

    The element the text is engraved on.

Returns:

  • list[:class:`~compas.geometry.Box`] –

    One groove per stroke, in model coordinates.

Source code in src/compas_timber/fabrication/text.py
def grooves_for_element(self, element):
    """The volumes engraved by this text: one box per straight stroke, centered on the reference side.

    Parameters
    ----------
    element : :class:`~compas_timber.elements.Beam`
        The element the text is engraved on.

    Returns
    -------
    list[:class:`~compas.geometry.Box`]
        One groove per stroke, in model coordinates.

    """
    face = element.ref_sides[self.ref_side_index or 0]
    size = self.text_height * self.ENGRAVING
    grooves = []
    for curve in self.create_text_curves_for_element(element):
        for start, end in zip(curve.points, curve.points[1:]):
            stroke = Vector.from_start_end(start, end)
            xaxis = stroke.unitized() if stroke.length > 0 else face.xaxis  # a dot is a square groove
            frame = Frame(start + stroke * 0.5, xaxis, face.normal.cross(xaxis))
            # longer by `size` so the strokes overlap where they meet; reaching `size` into the face
            grooves.append(Box(stroke.length + size, size, 2 * size, frame))
    return grooves
scale ¤
scale(factor)

Scale the parameters of this processing by a given factor.

Notes

Only distances are scaled, angles remain unchanged.

Parameters:

  • factor (float) –

    The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

Source code in src/compas_timber/fabrication/text.py
def scale(self, factor):
    """Scale the parameters of this processing by a given factor.

    Notes
    -----
    Only distances are scaled, angles remain unchanged.

    Parameters
    ----------
    factor : float
        The scaling factor. A value of 1.0 means no scaling, while a value of 2.0 means doubling the size.

    """
    self.start_x *= factor
    self.start_y *= factor
    self.text_height *= factor