Skip to content

compas_timber.geometry ¤

Classes¤

KDTree ¤

KDTree(points)

Wrapper around :class:scipy.spatial.KDTree that mimics the :class:compas.geometry.KDTree interface.

Parameters:

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

    The points to build the tree from.

Source code in src/compas_timber/geometry.py
def __init__(self, points):
    self._points = list(points)
    self._tree = _ScipyKDTree([[p[0], p[1], p[2]] for p in self._points])

Methods:¤

nearest_neighbors ¤
nearest_neighbors(point, num, distance_sort=True)

Return the num nearest neighbours of point.

Parameters:

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

    The query point.

  • num (int) –

    The number of neighbours to return.

  • distance_sort (bool, default: True ) –

    Unused - scipy always returns results sorted by distance.

Returns:

  • list[tuple[:class:`~compas.geometry.Point` | None, int | None, float]] –

    Each entry is (point_or_None, index_or_None, distance). When fewer than num points are in the tree the missing entries carry None for both point and index and math.inf for distance, matching the :class:compas.geometry.KDTree contract.

Source code in src/compas_timber/geometry.py
def nearest_neighbors(self, point, num, distance_sort=True):
    """Return the *num* nearest neighbours of *point*.

    Parameters
    ----------
    point : :class:`~compas.geometry.Point`
        The query point.
    num : int
        The number of neighbours to return.
    distance_sort : bool, optional
        Unused - scipy always returns results sorted by distance.

    Returns
    -------
    list[tuple[:class:`~compas.geometry.Point` | None, int | None, float]]
        Each entry is ``(point_or_None, index_or_None, distance)``.
        When fewer than *num* points are in the tree the missing entries
        carry ``None`` for both point and index and ``math.inf`` for
        distance, matching the :class:`compas.geometry.KDTree` contract.
    """
    k = min(num, len(self._points))
    if k == 0:
        return [(None, None, math.inf)] * num

    distances, indices = self._tree.query([point[0], point[1], point[2]], k=k)

    # scipy returns bare scalars when k == 1
    if k == 1:
        distances = [distances]
        indices = [indices]

    results = []
    for d, i in zip(distances, indices):
        if math.isinf(d):
            results.append((None, None, d))
        else:
            results.append((self._points[i], int(i), float(d)))

    # Pad with None entries if fewer points than requested
    while len(results) < num:
        results.append((None, None, math.inf))

    return results
query_pairs ¤
query_pairs(max_distance) -> list[tuple[int, int]]

returns a list of pairs of indices of points in the tree that are within max_distance of each other.

Source code in src/compas_timber/geometry.py
def query_pairs(self, max_distance) -> list[tuple[int, int]]:
    """returns a list of pairs of indices of points in the tree that are within *max_distance* of each other."""
    return self._tree.query_pairs(max_distance)

Functions:¤

brep_difference_first ¤

brep_difference_first(brep_a: Brep, brep_b: Brep) -> Brep

Subtracts brep_b from brep_a and returns the first resulting piece.

Parameters:

  • brep_a (Brep) –

    The Brep to subtract from.

  • brep_b (Brep) –

    The Brep to subtract.

Returns:

  • The first resulting piece. –

Raises:

  • IndexError –

    If nothing is left of brep_a after the subtraction.

Source code in src/compas_timber/geometry.py
def brep_difference_first(brep_a: Brep, brep_b: Brep) -> Brep:
    """Subtracts `brep_b` from `brep_a` and returns the first resulting piece.

    Parameters
    ----------
    brep_a :
        The Brep to subtract from.
    brep_b :
        The Brep to subtract.

    Returns
    -------
        The first resulting piece.

    Raises
    ------
    IndexError
        If nothing is left of `brep_a` after the subtraction.

    """
    return _first_solid(Brep.from_boolean_difference(brep_a, brep_b), "difference")

brep_from_outlines ¤

brep_from_outlines(
    outline_a: Polyline, outline_b: Polyline, normal: Vector | None = None
) -> Brep

Create a solid brep from two closed outlines.

Assumes outline_a and outline_b are closed polylines with the same number of vertices. Assumes outlines are parallel to each other.

Parameters:

  • outline_a (Polyline) –

    The first closed outline.

  • outline_b (Polyline) –

    The second closed outline.

  • normal (Vector | None, default: None ) –

    The normal vector around which the outlines are oriented.

Returns:

  • A Brep representing the solid defined by the two outlines. –

Raises:

  • ValueError –

    If either outline is not closed, if the outlines don't have the same number of vertices, or if the outlines are not parallel to each other.

Source code in src/compas_timber/geometry.py
def brep_from_outlines(outline_a: Polyline, outline_b: Polyline, normal: Optional[Vector] = None) -> Brep:
    """Create a solid brep from two closed outlines.

    Assumes outline_a and outline_b are closed polylines with the same number of vertices.
    Assumes outlines are parallel to each other.

    Parameters
    ----------
    outline_a :
        The first closed outline.
    outline_b :
        The second closed outline.
    normal :
        The normal vector around which the outlines are oriented.

    Returns
    -------
        A Brep representing the solid defined by the two outlines.

    Raises
    ------
    ValueError
        If either outline is not closed, if the outlines don't have the same
        number of vertices, or if the outlines are not parallel to each other.
    """
    if not TOL.is_allclose(outline_a[0], outline_a[-1]):
        raise ValueError("outline_a is not closed: its first and last points must coincide.")
    if not TOL.is_allclose(outline_b[0], outline_b[-1]):
        raise ValueError("outline_b is not closed: its first and last points must coincide.")
    if len(outline_a) != len(outline_b):
        raise ValueError("outline_a and outline_b must have the same number of vertices.")

    normal_a = Polygon(outline_a.points[:-1]).normal
    normal_b = Polygon(outline_b.points[:-1]).normal
    angle = angle_vectors(normal_a, normal_b)
    if not (TOL.is_zero(angle, tol=TOL.angular) or TOL.is_zero(angle - math.pi, tol=TOL.angular)):
        raise ValueError("outline_a and outline_b are not parallel to each other.")

    normal = normal or Vector(0, 0, 1)
    outline_a = correct_polyline_direction(outline_a, normal, clockwise=True)
    outline_b = correct_polyline_direction(outline_b, normal, clockwise=True)

    vector_a = Vector.from_start_end(outline_a[0], outline_b[0])
    if vector_a.dot(normal) < 0:
        # make sure that outline_b is above (Z+) outline_a
        outline_a, outline_b = outline_b, outline_a

    polygons = [Polygon(outline_a.points[0:-1]), Polygon(outline_b.points[-2::-1])]
    for i in range(len(outline_a) - 1):
        polygons.append(Polygon([outline_a[i], outline_b[i], outline_b[i + 1], outline_a[i + 1]]))
    return Brep.from_polygons(polygons)

brep_intersection_first ¤

brep_intersection_first(brep_a: Brep, brep_b: Brep) -> Brep

Intersects brep_a with brep_b and returns the first resulting piece.

Parameters:

  • brep_a (Brep) –

    The first Brep.

  • brep_b (Brep) –

    The second Brep.

Returns:

  • The first resulting piece. –

Raises:

Source code in src/compas_timber/geometry.py
def brep_intersection_first(brep_a: Brep, brep_b: Brep) -> Brep:
    """Intersects `brep_a` with `brep_b` and returns the first resulting piece.

    Parameters
    ----------
    brep_a :
        The first Brep.
    brep_b :
        The second Brep.

    Returns
    -------
        The first resulting piece.

    Raises
    ------
    IndexError
        If the two Breps do not overlap.

    """
    return _first_solid(Brep.from_boolean_intersection(brep_a, brep_b), "intersection")

brep_union_first ¤

brep_union_first(brep_a: Brep, *breps: Brep) -> Brep

Unions brep_a with one or more other Breps and returns the first resulting piece.

Parameters:

  • brep_a (Brep) –

    The Brep to union with.

  • *breps (Brep, default: () ) –

    One or more Breps to union with brep_a.

Returns:

  • The first resulting piece. –

Raises:

Source code in src/compas_timber/geometry.py
def brep_union_first(brep_a: Brep, *breps: Brep) -> Brep:
    """Unions `brep_a` with one or more other Breps and returns the first resulting piece.

    Parameters
    ----------
    brep_a :
        The Brep to union with.
    *breps :
        One or more Breps to union with `brep_a`.

    Returns
    -------
        The first resulting piece.

    Raises
    ------
    IndexError
        If the union left no result.

    """
    if not breps:
        raise ValueError("At least one Brep to union with is required.")
    return _first_solid(Brep.from_boolean_union_multi(brep_a, *breps), "union")

oriented_polyhedron ¤

oriented_polyhedron(polyhedron: Polyhedron) -> Polyhedron

Returns the polyhedron with consistently oriented faces.

This function ensures that the normals of the polyhedron's faces are all oriented outwards by reordering the vertex indices that define each face.

Parameters:

  • polyhedron (Polyhedron) –

    The input polyhedron.

Returns:

  • A new polyhedron with its faces reordered to ensure outward-facing normals. –
Source code in src/compas_timber/geometry.py
def oriented_polyhedron(polyhedron: Polyhedron) -> Polyhedron:
    """Returns the polyhedron with consistently oriented faces.

    This function ensures that the normals of the polyhedron's faces are all
    oriented outwards by reordering the vertex indices that define each face.

    Parameters
    ----------
    polyhedron :
        The input polyhedron.

    Returns
    -------
        A new polyhedron with its faces reordered to ensure outward-facing normals.

    """
    vertices = [Point(*vertex) for vertex in polyhedron.vertices]
    faces = polyhedron.faces

    if not vertices or not faces:
        raise ValueError("The polyhedron must have vertices and faces to ensure outward-facing normals.")

    poly_centroid = Point(*centroid_points(vertices))
    new_faces = []
    for face in faces:
        face_centroid = centroid_points([vertices[i] for i in face])
        outward = Vector.from_start_end(poly_centroid, face_centroid)

        polyline = Polyline([vertices[i] for i in face])
        clockwise = is_polyline_clockwise(polyline, outward)

        if not clockwise:
            new_faces.append(list(face))
        else:
            new_faces.append(list(reversed(face)))

    polyhedron.faces = new_faces
    return polyhedron

polyhedron_from_box_planes ¤

polyhedron_from_box_planes(
    top_plane, bottom_plane, side_a_plane, side_b_plane, end_a_plane, end_b_plane
)

Create a hexahedral :class:~compas.geometry.Polyhedron defined by 6 bounding planes.

The geometry is equivalent to the intersection of three pairs of half-spaces. Vertices are computed as all 8 triple-plane intersections from the two top/bottom planes, two side planes, and two end planes.

Parameters:

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

    The plane forming the top face of the hexahedron.

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

    The plane forming the bottom face of the hexahedron.

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

    The plane forming one lateral side of the hexahedron.

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

    The plane forming the opposite lateral side of the hexahedron.

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

    The plane forming one end face of the hexahedron.

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

    The plane forming the opposite end face of the hexahedron.

Returns:

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

    A hexahedral polyhedron with 8 vertices and 6 faces.

Source code in src/compas_timber/geometry.py
def polyhedron_from_box_planes(top_plane, bottom_plane, side_a_plane, side_b_plane, end_a_plane, end_b_plane):
    """Create a hexahedral :class:`~compas.geometry.Polyhedron` defined by 6 bounding planes.

    The geometry is equivalent to the intersection of three pairs of half-spaces.
    Vertices are computed as all 8 triple-plane intersections from the two top/bottom
    planes, two side planes, and two end planes.

    Parameters
    ----------
    top_plane : :class:`~compas.geometry.Plane`
        The plane forming the top face of the hexahedron.
    bottom_plane : :class:`~compas.geometry.Plane`
        The plane forming the bottom face of the hexahedron.
    side_a_plane : :class:`~compas.geometry.Plane`
        The plane forming one lateral side of the hexahedron.
    side_b_plane : :class:`~compas.geometry.Plane`
        The plane forming the opposite lateral side of the hexahedron.
    end_a_plane : :class:`~compas.geometry.Plane`
        The plane forming one end face of the hexahedron.
    end_b_plane : :class:`~compas.geometry.Plane`
        The plane forming the opposite end face of the hexahedron.

    Returns
    -------
    :class:`~compas.geometry.Polyhedron`
        A hexahedral polyhedron with 8 vertices and 6 faces.

    """
    vertices = [
        Point(*intersection_plane_plane_plane(top_plane, side_a_plane, end_a_plane)),
        Point(*intersection_plane_plane_plane(top_plane, side_a_plane, end_b_plane)),
        Point(*intersection_plane_plane_plane(top_plane, side_b_plane, end_b_plane)),
        Point(*intersection_plane_plane_plane(top_plane, side_b_plane, end_a_plane)),
        Point(*intersection_plane_plane_plane(bottom_plane, side_a_plane, end_a_plane)),
        Point(*intersection_plane_plane_plane(bottom_plane, side_a_plane, end_b_plane)),
        Point(*intersection_plane_plane_plane(bottom_plane, side_b_plane, end_b_plane)),
        Point(*intersection_plane_plane_plane(bottom_plane, side_b_plane, end_a_plane)),
    ]
    faces = [[0, 3, 2, 1], [1, 2, 6, 5], [2, 3, 7, 6], [0, 4, 7, 3], [0, 1, 5, 4], [4, 5, 6, 7]]
    return oriented_polyhedron(Polyhedron(vertices=vertices, faces=faces))