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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ classifiers = [
]

dependencies = [
"ansys-api-geometry==0.3.3",
"ansys-api-geometry==0.3.5",
"ansys-tools-path>=0.3",
"beartype>=0.11.0",
"google-api-python-client>=1.7.11",
Expand Down
86 changes: 51 additions & 35 deletions src/ansys/geometry/core/designer/body.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
)
from ansys.api.geometry.v0.commands_pb2_grpc import CommandsStub
from beartype import beartype as check_input_types
from beartype.typing import TYPE_CHECKING, List, Optional, Tuple, Union
from beartype.typing import TYPE_CHECKING, Iterable, List, Optional, Tuple, Union
from pint import Quantity

from ansys.geometry.core.connection.client import GrpcClient
Expand Down Expand Up @@ -446,9 +446,9 @@ def plot(
"""
return

def intersect(self, other: "Body") -> None:
def intersect(self, other: Union["Body", Iterable["Body"]]) -> None:
"""
Intersect two bodies.
Intersect two (or more) bodies.

Notes
-----
Expand All @@ -469,9 +469,9 @@ def intersect(self, other: "Body") -> None:
return

@protect_grpc
def subtract(self, other: "Body") -> None:
def subtract(self, other: Union["Body", Iterable["Body"]]) -> None:
"""
Subtract two bodies.
Subtract two (or more) bodies.

Notes
-----
Expand All @@ -492,9 +492,9 @@ def subtract(self, other: "Body") -> None:
return

@protect_grpc
def unite(self, other: "Body") -> None:
def unite(self, other: Union["Body", Iterable["Body"]]) -> None:
"""
Unite two bodies.
Unite two (or more) bodies.

Notes
-----
Expand Down Expand Up @@ -803,17 +803,17 @@ def plot(
"MasterBody does not implement plot methods. Call this method on a body instead."
)

def intersect(self, other: "Body") -> None: # noqa: D102
def intersect(self, other: Union["Body", Iterable["Body"]]) -> None: # noqa: D102
raise NotImplementedError(
"MasterBody does not implement Boolean methods. Call this method on a body instead."
)

def subtract(self, other: "Body") -> None: # noqa: D102
def subtract(self, other: Union["Body", Iterable["Body"]]) -> None: # noqa: D102
raise NotImplementedError(
"MasterBody does not implement Boolean methods. Call this method on a body instead."
)

def unite(self, other: "Body") -> None:
def unite(self, other: Union["Body", Iterable["Body"]]) -> None:
# noqa: D102
raise NotImplementedError(
"MasterBody does not implement Boolean methods. Call this method on a body instead."
Expand Down Expand Up @@ -1108,40 +1108,56 @@ def plot(
self, merge_bodies=merge, screenshot=screenshot, **plotting_options
)

@protect_grpc
@reset_tessellation_cache
@ensure_design_is_active
def intersect(self, other: "Body") -> None: # noqa: D102
response = self._template._bodies_stub.Boolean(
BooleanRequest(body1=self.id, body2=other.id, method="intersect")
).empty_result
def intersect(self, other: Union["Body", Iterable["Body"]]) -> None: # noqa: D102
self.__generic_boolean_op(other, "intersect", "bodies do not intersect")

if response == 1:
raise ValueError("Bodies do not intersect.")
def subtract(self, other: Union["Body", Iterable["Body"]]) -> None: # noqa: D102
self.__generic_boolean_op(other, "subtract", "empty (complete) subtraction")

other.parent_component.delete_body(other)
def unite(self, other: Union["Body", Iterable["Body"]]) -> None: # noqa: D102
self.__generic_boolean_op(other, "unite", "union operation failed")

@protect_grpc
@reset_tessellation_cache
@ensure_design_is_active
def subtract(self, other: "Body") -> None: # noqa: D102
response = self._template._bodies_stub.Boolean(
BooleanRequest(body1=self.id, body2=other.id, method="subtract")
).empty_result
@check_input_types
def __generic_boolean_op(
self, other: Union["Body", Iterable["Body"]], type_bool_op: str, err_bool_op: str
) -> None:
grpc_other = other if isinstance(other, Iterable) else [other]
try:
response = self._template._bodies_stub.Boolean(
BooleanRequest(
body1=self.id, tool_bodies=[b.id for b in grpc_other], method=type_bool_op
)
).empty_result
except Exception as err:
# TODO: to be deleted - old versions did not have "tool_bodies" in the request
# This is a temporary fix to support old versions of the server - should be deleted
# once the server is no longer supported.
if not isinstance(other, Iterable):
response = self._template._bodies_stub.Boolean(
BooleanRequest(body1=self.id, body2=other.id, method=type_bool_op)
).empty_result
else:
all_response = []
for body2 in other:
response = self._template._bodies_stub.Boolean(
BooleanRequest(body1=self.id, body2=body2.id, method=type_bool_op)
).empty_result
all_response.append(response)

if all_response.count(1) > 0:
response = 1

if response == 1:
raise ValueError("Subtraction of bodies results in an empty (complete) subtraction.")

other.parent_component.delete_body(other)
raise ValueError(
f"Boolean operation of type '{type_bool_op}' failed: {err_bool_op}.\n"
f"Involving bodies:{self}, {grpc_other}"
)

@protect_grpc
@reset_tessellation_cache
@ensure_design_is_active
def unite(self, other: "Body") -> None: # noqa: D102
self._template._bodies_stub.Boolean(
BooleanRequest(body1=self.id, body2=other.id, method="unite")
)
other.parent_component.delete_body(other)
for b in grpc_other:
b.parent_component.delete_body(b)

def __repr__(self) -> str:
"""Represent the ``Body`` as a string."""
Expand Down
3 changes: 1 addition & 2 deletions src/ansys/geometry/core/designer/design.py
Original file line number Diff line number Diff line change
Expand Up @@ -676,10 +676,9 @@ def __read_existing_design(self) -> None:
parent.components.append(c)

# Create Bodies
# TODO: is_surface?
for body in response.bodies:
part = created_parts.get(body.parent_id)
tb = MasterBody(body.id, body.name, self._grpc_client)
tb = MasterBody(body.id, body.name, self._grpc_client, is_surface=body.is_surface)
part.bodies.append(tb)
created_bodies[body.id] = tb

Expand Down
74 changes: 72 additions & 2 deletions tests/integration/test_design.py
Original file line number Diff line number Diff line change
Expand Up @@ -1486,7 +1486,7 @@ def test_boolean_body_operations(modeler: Modeler, skip_not_on_linux_service):
# 1.a.ii
copy1 = body1.copy(comp1, "Copy1")
copy3 = body3.copy(comp3, "Copy3")
with pytest.raises(ValueError, match="Bodies do not intersect."):
with pytest.raises(ValueError, match="bodies do not intersect"):
copy1.intersect(copy3)

assert copy1.is_alive
Expand Down Expand Up @@ -1595,7 +1595,7 @@ def test_boolean_body_operations(modeler: Modeler, skip_not_on_linux_service):
# 2.a.ii
copy1 = body1.copy(comp1_i, "Copy1")
copy3 = body3.copy(comp3_i, "Copy3")
with pytest.raises(ValueError, match="Bodies do not intersect."):
with pytest.raises(ValueError, match="bodies do not intersect"):
copy1.intersect(copy3)

assert copy1.is_alive
Expand Down Expand Up @@ -1665,6 +1665,76 @@ def test_boolean_body_operations(modeler: Modeler, skip_not_on_linux_service):
assert Accuracy.length_is_equal(copy1.volume.m, 1)


def test_multiple_bodies_boolean_operations(modeler: Modeler, skip_not_on_linux_service):
"""Test boolean operations with multiple bodies."""

design = modeler.create_design("TestBooleanOperationsMultipleBodies")

comp1 = design.add_component("Comp1")
comp2 = design.add_component("Comp2")
comp3 = design.add_component("Comp3")

body1 = comp1.extrude_sketch("Body1", Sketch().box(Point2D([0, 0]), 1, 1), 1)
body2 = comp2.extrude_sketch("Body2", Sketch().box(Point2D([0.5, 0]), 1, 1), 1)
body3 = comp3.extrude_sketch("Body3", Sketch().box(Point2D([5, 0]), 1, 1), 1)

################# Check subtract operation #################
copy1_sub = body1.copy(comp1, "Copy1_subtract")
copy2_sub = body2.copy(comp2, "Copy2_subtract")
copy3_sub = body3.copy(comp3, "Copy3_subtract")
copy1_sub.subtract([copy2_sub, copy3_sub])

assert not copy2_sub.is_alive
assert not copy3_sub.is_alive
assert body2.is_alive
assert body3.is_alive
assert len(comp1.bodies) == 2
assert len(comp2.bodies) == 1
assert len(comp3.bodies) == 1

# Cleanup previous subtest
comp1.delete_body(copy1_sub)
assert len(comp1.bodies) == 1

################# Check unite operation #################
copy1_uni = body1.copy(comp1, "Copy1_unite")
copy2_uni = body2.copy(comp2, "Copy2_unite")
copy3_uni = body3.copy(comp3, "Copy3_unite")
copy1_uni.unite([copy2_uni, copy3_uni])

assert not copy2_uni.is_alive
assert not copy3_uni.is_alive
assert body2.is_alive
assert body3.is_alive
assert len(comp1.bodies) == 2
assert len(comp2.bodies) == 1
assert len(comp3.bodies) == 1

# Cleanup previous subtest
comp1.delete_body(copy1_uni)
assert len(comp1.bodies) == 1

################# Check intersect operation #################
copy1_int = body1.copy(comp1, "Copy1_intersect")
copy2_int = body2.copy(comp2, "Copy2_intersect")
copy3_int = body3.copy(comp3, "Copy3_intersect") # Body 3 does not intersect them
copy1_int.intersect([copy2_int])

assert not copy2_int.is_alive
assert copy3_int.is_alive
assert body2.is_alive
assert body3.is_alive
assert len(comp1.bodies) == 2
assert len(comp2.bodies) == 1
assert len(comp3.bodies) == 2

# Cleanup previous subtest
comp1.delete_body(copy1_int)
comp3.delete_body(copy3_int)
assert len(comp1.bodies) == 1
assert len(comp3.bodies) == 1


def test_child_component_instances(modeler: Modeler):
"""Test creation of child ``Component`` instances and check the data model reflects
that."""
Expand Down
21 changes: 21 additions & 0 deletions tests/integration/test_design_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,27 @@ def test_design_import_simple_case(modeler: Modeler):
_checker_method(read_design, design)


def test_design_import_with_surfaces_issue834(modeler: Modeler):
"""
Import a Design which is expected to contain surfaces.

For more info see https://github.com/ansys/pyansys-geometry/issues/834
"""
# TODO: to be reactivated by https://github.com/ansys/pyansys-geometry/issues/799
if modeler.client.backend_type != BackendType.LINUX_SERVICE:
# Open the design
design = modeler.open_file("./tests/integration/files/DuplicateFacesDesignBefore.scdocx")

# Check that there are two bodies
assert len(design.bodies) == 2

# Check some basic properties - whether they are surfaces or not!
assert design.bodies[0].name == "BoxBody"
assert design.bodies[0].is_surface == False
assert design.bodies[1].name == "DuplicatesSurface"
assert design.bodies[1].is_surface == True


def test_open_file(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory):
"""Test creation of a component, saving it to a file, and loading it again to a
second component and make sure they have the same properties."""
Expand Down