Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changelog.d/1805.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
enhanced 3D bounding box implementation
29 changes: 29 additions & 0 deletions src/ansys/geometry/core/designer/body.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
from ansys.geometry.core.connection.conversions import (
frame_to_grpc_frame,
grpc_material_to_material,
grpc_point_to_point3d,
plane_to_grpc_plane,
point3d_to_grpc_point,
sketch_shapes_to_grpc_geometries,
Expand All @@ -74,6 +75,7 @@
from ansys.geometry.core.designer.face import Face, SurfaceType
from ansys.geometry.core.errors import protect_grpc
from ansys.geometry.core.materials.material import Material
from ansys.geometry.core.math.bbox import BoundingBox
from ansys.geometry.core.math.constants import IDENTITY_MATRIX44
from ansys.geometry.core.math.frame import Frame
from ansys.geometry.core.math.matrix import Matrix44
Expand Down Expand Up @@ -282,6 +284,17 @@ def material(self) -> Material:
"""
return

@abstractmethod
def bounding_box(self) -> BoundingBox:
"""Get the bounding box of the body.

Returns
-------
BoundingBox
Bounding box of the body.
"""
return

@abstractmethod
def assign_material(self, material: Material) -> None:
"""Assign a material against the active design.
Expand Down Expand Up @@ -969,6 +982,18 @@ def material(self) -> Material: # noqa: D102
def material(self, value: Material): # noqa: D102
self.assign_material(value)

@property
@protect_grpc
@min_backend_version(25, 2, 0)
def bounding_box(self) -> BoundingBox: # noqa: D102
self._grpc_client.log.debug(f"Retrieving bounding box for body {self.id} from server.")
result = self._bodies_stub.GetBoundingBox(self._grpc_id).box

min_corner = grpc_point_to_point3d(result.min)
max_corner = grpc_point_to_point3d(result.max)
center = grpc_point_to_point3d(result.center)
return BoundingBox(min_corner, max_corner, center)

@protect_grpc
@check_input_types
def assign_material(self, material: Material) -> None: # noqa: D102
Expand Down Expand Up @@ -1555,6 +1580,10 @@ def material(self) -> Material: # noqa: D102
def material(self, value: Material): # noqa: D102
self._template.material = value

@property
def bounding_box(self) -> BoundingBox: # noqa: D102
return self._template.bounding_box

@ensure_design_is_active
def assign_material(self, material: Material) -> None: # noqa: D102
self._template.assign_material(material)
Expand Down
18 changes: 17 additions & 1 deletion src/ansys/geometry/core/designer/edge.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@
from ansys.api.dbu.v0.dbumodels_pb2 import EntityIdentifier
from ansys.api.geometry.v0.edges_pb2_grpc import EdgesStub
from ansys.geometry.core.connection.client import GrpcClient
from ansys.geometry.core.connection.conversions import grpc_curve_to_curve
from ansys.geometry.core.connection.conversions import grpc_curve_to_curve, grpc_point_to_point3d
from ansys.geometry.core.errors import GeometryRuntimeError, protect_grpc
from ansys.geometry.core.math.bbox import BoundingBox
from ansys.geometry.core.math.point import Point3D
from ansys.geometry.core.misc.checks import ensure_design_is_active, min_backend_version
from ansys.geometry.core.misc.measurements import DEFAULT_UNITS
Expand Down Expand Up @@ -206,3 +207,18 @@ def end(self) -> Point3D:
self._grpc_client.log.debug("Requesting edge end point from server.")
response = self._edges_stub.GetStartAndEndPoints(self._grpc_id)
return Point3D([response.end.x, response.end.y, response.end.z])

@property
@protect_grpc
@ensure_design_is_active
@min_backend_version(25, 2, 0)
def bounding_box(self) -> BoundingBox:
"""Bounding box of the edge."""
self._grpc_client.log.debug("Requesting bounding box from server.")

result = self._edges_stub.GetBoundingBox(self._grpc_id)

min_corner = grpc_point_to_point3d(result.min)
max_corner = grpc_point_to_point3d(result.max)
center = grpc_point_to_point3d(result.center)
return BoundingBox(min_corner, max_corner, center)
18 changes: 12 additions & 6 deletions src/ansys/geometry/core/designer/face.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
"""Module for managing a face."""

from enum import Enum, unique
from functools import cached_property
from typing import TYPE_CHECKING

from beartype import beartype as check_input_types
Expand All @@ -42,10 +41,14 @@
from ansys.api.geometry.v0.faces_pb2_grpc import FacesStub
from ansys.api.geometry.v0.models_pb2 import Edge as GRPCEdge
from ansys.geometry.core.connection.client import GrpcClient
from ansys.geometry.core.connection.conversions import grpc_curve_to_curve, grpc_surface_to_surface
from ansys.geometry.core.connection.conversions import (
grpc_curve_to_curve,
grpc_point_to_point3d,
grpc_surface_to_surface,
)
from ansys.geometry.core.designer.edge import Edge
from ansys.geometry.core.errors import GeometryRuntimeError, protect_grpc
from ansys.geometry.core.math.bbox import BoundingBox2D
from ansys.geometry.core.math.bbox import BoundingBox
from ansys.geometry.core.math.point import Point3D
from ansys.geometry.core.math.vector import UnitVector3D
from ansys.geometry.core.misc.auxiliary import (
Expand Down Expand Up @@ -335,16 +338,19 @@ def color(self, color: str | tuple[float, float, float]) -> None:
def opacity(self, opacity: float) -> None:
self.set_opacity(opacity)

@cached_property
@property
@protect_grpc
@min_backend_version(25, 2, 0)
def bounding_box(self) -> BoundingBox2D:
def bounding_box(self) -> BoundingBox:
"""Get the bounding box for the face."""
self._grpc_client.log.debug(f"Getting bounding box for {self.id}.")

result = self._faces_stub.GetBoundingBox(request=self._grpc_id)
min_point = grpc_point_to_point3d(result.min)
max_point = grpc_point_to_point3d(result.max)
center = grpc_point_to_point3d(result.center)

return BoundingBox2D(result.min.x, result.max.x, result.min.y, result.max.y)
return BoundingBox(min_point, max_point, center)

@protect_grpc
@check_input_types
Expand Down
2 changes: 1 addition & 1 deletion src/ansys/geometry/core/math/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
# SOFTWARE.
"""PyAnsys Geometry math subpackage."""

from ansys.geometry.core.math.bbox import BoundingBox2D
from ansys.geometry.core.math.bbox import BoundingBox, BoundingBox2D
from ansys.geometry.core.math.constants import (
DEFAULT_POINT2D,
DEFAULT_POINT3D,
Expand Down
158 changes: 157 additions & 1 deletion src/ansys/geometry/core/math/bbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@
from beartype import beartype as check_input_types

from ansys.geometry.core.math.misc import intersect_interval
from ansys.geometry.core.math.point import Point2D
from ansys.geometry.core.math.point import Point2D, Point3D
from ansys.geometry.core.misc.accuracy import Accuracy
from ansys.geometry.core.misc.checks import deprecated_method
from ansys.geometry.core.misc.measurements import DEFAULT_UNITS
from ansys.geometry.core.typing import Real

Expand All @@ -49,6 +50,7 @@ class BoundingBox2D:
"""

@check_input_types
@deprecated_method(alternative="BoundingBox", version="0.10.0", remove="0.11.0")
def __init__(
self,
x_min: Real = sys.float_info.max,
Expand Down Expand Up @@ -244,3 +246,157 @@ def intersect_bboxes(
return None

return BoundingBox2D(min_x, max_x, min_y, max_y)


class BoundingBox:
"""Maintains the box structure for Bounding Boxes.

Parameters
----------
min_corner : Point3D
Minimum corner for the box.
max_corner : Point3D
Maximum corner for the box.
center : Point3D
Center of the box.
"""

@check_input_types
def __init__(self, min_corner: Point3D, max_corner: Point3D, center: Point3D = None):
"""Initialize the ``BoundingBox`` class."""
self._min_corner = min_corner
self._max_corner = max_corner

if center is not None:
self._center = center
else:
mid_x = (max_corner.x.m - min_corner.x.m) / 2
mid_y = (max_corner.y.m - min_corner.y.m) / 2
mid_z = (max_corner.z.m - min_corner.z.m) / 2
self._center = Point3D([mid_x, mid_y, mid_z])

@property
def min_corner(self) -> Point3D:
"""Minimum corner of the bounding box."""
return self._min_corner

@property
def max_corner(self) -> Point3D:
"""Maximum corner of the bounding box."""
return self._max_corner

@property
def center(self) -> Point3D:
"""Center of the bounding box."""
return self._center

@check_input_types
def contains_point(self, point: Point3D) -> bool:
"""Evaluate whether a point lies within the box.

Parameters
----------
point : Point3D
Point to compare against the bounds.

Returns
-------
bool
``True`` if the point is contained in the bounding box. Otherwise, ``False``.
"""
return self.contains_point_components(
point.x.m_as(DEFAULT_UNITS.LENGTH),
point.y.m_as(DEFAULT_UNITS.LENGTH),
point.z.m_as(DEFAULT_UNITS.LENGTH),
)

@check_input_types
def contains_point_components(self, x: Real, y: Real, z: Real) -> bool:
"""Check if point components are within box.

Parameters
----------
x : Real
Point X component to compare against the bounds.
y : Real
Point Y component to compare against the bounds.
z : Real
Point Z component to compare against the bounds.

Returns
-------
bool
``True`` if the components are contained in the bounding box. Otherwise, ``False``.
"""
return (
Accuracy.length_is_greater_than_or_equal(x, self._min_corner.x.m)
and Accuracy.length_is_greater_than_or_equal(y, self._min_corner.y.m)
and Accuracy.length_is_greater_than_or_equal(z, self._min_corner.z.m)
and Accuracy.length_is_less_than_or_equal(x, self._max_corner.x.m)
and Accuracy.length_is_less_than_or_equal(y, self._max_corner.y.m)
and Accuracy.length_is_less_than_or_equal(z, self._max_corner.z.m)
)

@check_input_types
def __eq__(self, other: "BoundingBox") -> bool:
"""Equals operator for the ``BoundingBox`` class."""
return (
self._min_corner.x == other._min_corner.x
and self._max_corner.x == other._max_corner.x
and self._min_corner.y == other._min_corner.y
and self._max_corner.y == other._max_corner.y
and self._min_corner.z == other._min_corner.z
and self._max_corner.z == other._max_corner.z
)

@check_input_types
def __ne__(self, other: "BoundingBox") -> bool:
"""Not equals operator for the ``BoundingBox`` class."""
return not self == other

@staticmethod
def intersect_bboxes(box_1: "BoundingBox", box_2: "BoundingBox") -> Union[None, "BoundingBox"]:
"""Find the intersection of 2 BoundingBox objects.

Parameters
----------
box_1: BoundingBox
The box to consider the intersection of with respect to box_2.
box_2: BoundingBox
The box to consider the intersection of with respect to box_1.

Returns
-------
BoundingBox:
The box representing the intersection of the two passed in boxes.
"""
intersect, min_x, max_x = intersect_interval(
box_1._min_corner.x.m,
box_2._min_corner.x.m,
box_1._max_corner.x.m,
box_2._max_corner.x.m,
)
if not intersect:
return None

intersect, min_y, max_y = intersect_interval(
box_1._min_corner.y.m,
box_2._min_corner.y.m,
box_1._max_corner.y.m,
box_2._max_corner.y.m,
)
if not intersect:
return None

intersect, min_z, max_z = intersect_interval(
box_1._min_corner.z.m,
box_2._min_corner.z.m,
box_1._max_corner.z.m,
box_2._max_corner.z.m,
)
if not intersect:
return None

min_point = Point3D([min_x, min_y, min_z])
max_point = Point3D([max_x, max_y, max_z])
return BoundingBox(min_point, max_point)
52 changes: 52 additions & 0 deletions tests/integration/test_design.py
Original file line number Diff line number Diff line change
Expand Up @@ -3190,3 +3190,55 @@ def test_set_face_color(modeler: Modeler):
ValueError, match="Invalid color value: Opacity value must be between 0 and 1."
):
faces[3].opacity = 255


def test_get_face_bounding_box(modeler: Modeler):
"""Test getting the bounding box of a face."""
design = modeler.create_design("face_bounding_box")
body = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1)

bounding_box = body.faces[0].bounding_box
assert bounding_box.min_corner.x.m == bounding_box.min_corner.y.m == -0.5
assert bounding_box.max_corner.x.m == bounding_box.max_corner.y.m == 0.5

bounding_box = body.faces[1].bounding_box
assert bounding_box.min_corner.x.m == bounding_box.min_corner.y.m == -0.5
assert bounding_box.max_corner.x.m == bounding_box.max_corner.y.m == 0.5


def test_get_edge_bounding_box(modeler: Modeler):
"""Test getting the bounding box of an edge."""
design = modeler.create_design("edge_bounding_box")
body = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1)

# Edge 0 goes from (-0.5, -0.5, 1) to (0.5, -0.5, 1)
bounding_box = body.edges[0].bounding_box
assert bounding_box.min_corner.x.m == bounding_box.min_corner.y.m == -0.5
assert bounding_box.min_corner.z.m == 1
assert bounding_box.max_corner.x.m == 0.5
assert bounding_box.max_corner.y.m == -0.5
assert bounding_box.max_corner.z.m == 1

# Test center
center = bounding_box.center
assert center.x.m == 0
assert center.y.m == -0.5
assert center.z.m == 1


def test_get_body_bounding_box(modeler: Modeler):
"""Test getting the bounding box of a body."""
design = modeler.create_design("body_bounding_box")
body = design.extrude_sketch("box", Sketch().box(Point2D([0, 0]), 1, 1), 1)

bounding_box = body.bounding_box
assert bounding_box.min_corner.x.m == bounding_box.min_corner.y.m == -0.5
assert bounding_box.min_corner.z.m == 0
assert bounding_box.max_corner.x.m == bounding_box.max_corner.y.m == 0.5
assert bounding_box.max_corner.z.m == 1

# Test center
center = bounding_box.center
assert center.x.m == 0
assert center.y.m == 0
assert center.z.m == 0.5
Loading
Loading