Skip to content

Commit 2f27a28

Browse files
feat: import surface body properly when reading design + boolean ops with multiple bodies (#846)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 06b2836 commit 2f27a28

File tree

5 files changed

+146
-40
lines changed

5 files changed

+146
-40
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ classifiers = [
2424
]
2525

2626
dependencies = [
27-
"ansys-api-geometry==0.3.3",
27+
"ansys-api-geometry==0.3.5",
2828
"ansys-tools-path>=0.3",
2929
"beartype>=0.11.0",
3030
"google-api-python-client>=1.7.11",

src/ansys/geometry/core/designer/body.py

Lines changed: 51 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
)
4141
from ansys.api.geometry.v0.commands_pb2_grpc import CommandsStub
4242
from beartype import beartype as check_input_types
43-
from beartype.typing import TYPE_CHECKING, List, Optional, Tuple, Union
43+
from beartype.typing import TYPE_CHECKING, Iterable, List, Optional, Tuple, Union
4444
from pint import Quantity
4545

4646
from ansys.geometry.core.connection.client import GrpcClient
@@ -446,9 +446,9 @@ def plot(
446446
"""
447447
return
448448

449-
def intersect(self, other: "Body") -> None:
449+
def intersect(self, other: Union["Body", Iterable["Body"]]) -> None:
450450
"""
451-
Intersect two bodies.
451+
Intersect two (or more) bodies.
452452
453453
Notes
454454
-----
@@ -469,9 +469,9 @@ def intersect(self, other: "Body") -> None:
469469
return
470470

471471
@protect_grpc
472-
def subtract(self, other: "Body") -> None:
472+
def subtract(self, other: Union["Body", Iterable["Body"]]) -> None:
473473
"""
474-
Subtract two bodies.
474+
Subtract two (or more) bodies.
475475
476476
Notes
477477
-----
@@ -492,9 +492,9 @@ def subtract(self, other: "Body") -> None:
492492
return
493493

494494
@protect_grpc
495-
def unite(self, other: "Body") -> None:
495+
def unite(self, other: Union["Body", Iterable["Body"]]) -> None:
496496
"""
497-
Unite two bodies.
497+
Unite two (or more) bodies.
498498
499499
Notes
500500
-----
@@ -803,17 +803,17 @@ def plot(
803803
"MasterBody does not implement plot methods. Call this method on a body instead."
804804
)
805805

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

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

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

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

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

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

11241120
@protect_grpc
11251121
@reset_tessellation_cache
11261122
@ensure_design_is_active
1127-
def subtract(self, other: "Body") -> None: # noqa: D102
1128-
response = self._template._bodies_stub.Boolean(
1129-
BooleanRequest(body1=self.id, body2=other.id, method="subtract")
1130-
).empty_result
1123+
@check_input_types
1124+
def __generic_boolean_op(
1125+
self, other: Union["Body", Iterable["Body"]], type_bool_op: str, err_bool_op: str
1126+
) -> None:
1127+
grpc_other = other if isinstance(other, Iterable) else [other]
1128+
try:
1129+
response = self._template._bodies_stub.Boolean(
1130+
BooleanRequest(
1131+
body1=self.id, tool_bodies=[b.id for b in grpc_other], method=type_bool_op
1132+
)
1133+
).empty_result
1134+
except Exception as err:
1135+
# TODO: to be deleted - old versions did not have "tool_bodies" in the request
1136+
# This is a temporary fix to support old versions of the server - should be deleted
1137+
# once the server is no longer supported.
1138+
if not isinstance(other, Iterable):
1139+
response = self._template._bodies_stub.Boolean(
1140+
BooleanRequest(body1=self.id, body2=other.id, method=type_bool_op)
1141+
).empty_result
1142+
else:
1143+
all_response = []
1144+
for body2 in other:
1145+
response = self._template._bodies_stub.Boolean(
1146+
BooleanRequest(body1=self.id, body2=body2.id, method=type_bool_op)
1147+
).empty_result
1148+
all_response.append(response)
1149+
1150+
if all_response.count(1) > 0:
1151+
response = 1
11311152

11321153
if response == 1:
1133-
raise ValueError("Subtraction of bodies results in an empty (complete) subtraction.")
1134-
1135-
other.parent_component.delete_body(other)
1154+
raise ValueError(
1155+
f"Boolean operation of type '{type_bool_op}' failed: {err_bool_op}.\n"
1156+
f"Involving bodies:{self}, {grpc_other}"
1157+
)
11361158

1137-
@protect_grpc
1138-
@reset_tessellation_cache
1139-
@ensure_design_is_active
1140-
def unite(self, other: "Body") -> None: # noqa: D102
1141-
self._template._bodies_stub.Boolean(
1142-
BooleanRequest(body1=self.id, body2=other.id, method="unite")
1143-
)
1144-
other.parent_component.delete_body(other)
1159+
for b in grpc_other:
1160+
b.parent_component.delete_body(b)
11451161

11461162
def __repr__(self) -> str:
11471163
"""Represent the ``Body`` as a string."""

src/ansys/geometry/core/designer/design.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -676,10 +676,9 @@ def __read_existing_design(self) -> None:
676676
parent.components.append(c)
677677

678678
# Create Bodies
679-
# TODO: is_surface?
680679
for body in response.bodies:
681680
part = created_parts.get(body.parent_id)
682-
tb = MasterBody(body.id, body.name, self._grpc_client)
681+
tb = MasterBody(body.id, body.name, self._grpc_client, is_surface=body.is_surface)
683682
part.bodies.append(tb)
684683
created_bodies[body.id] = tb
685684

tests/integration/test_design.py

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,7 +1486,7 @@ def test_boolean_body_operations(modeler: Modeler, skip_not_on_linux_service):
14861486
# 1.a.ii
14871487
copy1 = body1.copy(comp1, "Copy1")
14881488
copy3 = body3.copy(comp3, "Copy3")
1489-
with pytest.raises(ValueError, match="Bodies do not intersect."):
1489+
with pytest.raises(ValueError, match="bodies do not intersect"):
14901490
copy1.intersect(copy3)
14911491

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

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

16671667

1668+
def test_multiple_bodies_boolean_operations(modeler: Modeler, skip_not_on_linux_service):
1669+
"""Test boolean operations with multiple bodies."""
1670+
1671+
design = modeler.create_design("TestBooleanOperationsMultipleBodies")
1672+
1673+
comp1 = design.add_component("Comp1")
1674+
comp2 = design.add_component("Comp2")
1675+
comp3 = design.add_component("Comp3")
1676+
1677+
body1 = comp1.extrude_sketch("Body1", Sketch().box(Point2D([0, 0]), 1, 1), 1)
1678+
body2 = comp2.extrude_sketch("Body2", Sketch().box(Point2D([0.5, 0]), 1, 1), 1)
1679+
body3 = comp3.extrude_sketch("Body3", Sketch().box(Point2D([5, 0]), 1, 1), 1)
1680+
1681+
################# Check subtract operation #################
1682+
copy1_sub = body1.copy(comp1, "Copy1_subtract")
1683+
copy2_sub = body2.copy(comp2, "Copy2_subtract")
1684+
copy3_sub = body3.copy(comp3, "Copy3_subtract")
1685+
copy1_sub.subtract([copy2_sub, copy3_sub])
1686+
1687+
assert not copy2_sub.is_alive
1688+
assert not copy3_sub.is_alive
1689+
assert body2.is_alive
1690+
assert body3.is_alive
1691+
assert len(comp1.bodies) == 2
1692+
assert len(comp2.bodies) == 1
1693+
assert len(comp3.bodies) == 1
1694+
1695+
# Cleanup previous subtest
1696+
comp1.delete_body(copy1_sub)
1697+
assert len(comp1.bodies) == 1
1698+
1699+
################# Check unite operation #################
1700+
copy1_uni = body1.copy(comp1, "Copy1_unite")
1701+
copy2_uni = body2.copy(comp2, "Copy2_unite")
1702+
copy3_uni = body3.copy(comp3, "Copy3_unite")
1703+
copy1_uni.unite([copy2_uni, copy3_uni])
1704+
1705+
assert not copy2_uni.is_alive
1706+
assert not copy3_uni.is_alive
1707+
assert body2.is_alive
1708+
assert body3.is_alive
1709+
assert len(comp1.bodies) == 2
1710+
assert len(comp2.bodies) == 1
1711+
assert len(comp3.bodies) == 1
1712+
1713+
# Cleanup previous subtest
1714+
comp1.delete_body(copy1_uni)
1715+
assert len(comp1.bodies) == 1
1716+
1717+
################# Check intersect operation #################
1718+
copy1_int = body1.copy(comp1, "Copy1_intersect")
1719+
copy2_int = body2.copy(comp2, "Copy2_intersect")
1720+
copy3_int = body3.copy(comp3, "Copy3_intersect") # Body 3 does not intersect them
1721+
copy1_int.intersect([copy2_int])
1722+
1723+
assert not copy2_int.is_alive
1724+
assert copy3_int.is_alive
1725+
assert body2.is_alive
1726+
assert body3.is_alive
1727+
assert len(comp1.bodies) == 2
1728+
assert len(comp2.bodies) == 1
1729+
assert len(comp3.bodies) == 2
1730+
1731+
# Cleanup previous subtest
1732+
comp1.delete_body(copy1_int)
1733+
comp3.delete_body(copy3_int)
1734+
assert len(comp1.bodies) == 1
1735+
assert len(comp3.bodies) == 1
1736+
1737+
16681738
def test_child_component_instances(modeler: Modeler):
16691739
"""Test creation of child ``Component`` instances and check the data model reflects
16701740
that."""

tests/integration/test_design_import.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,27 @@ def test_design_import_simple_case(modeler: Modeler):
123123
_checker_method(read_design, design)
124124

125125

126+
def test_design_import_with_surfaces_issue834(modeler: Modeler):
127+
"""
128+
Import a Design which is expected to contain surfaces.
129+
130+
For more info see https://github.com/ansys/pyansys-geometry/issues/834
131+
"""
132+
# TODO: to be reactivated by https://github.com/ansys/pyansys-geometry/issues/799
133+
if modeler.client.backend_type != BackendType.LINUX_SERVICE:
134+
# Open the design
135+
design = modeler.open_file("./tests/integration/files/DuplicateFacesDesignBefore.scdocx")
136+
137+
# Check that there are two bodies
138+
assert len(design.bodies) == 2
139+
140+
# Check some basic properties - whether they are surfaces or not!
141+
assert design.bodies[0].name == "BoxBody"
142+
assert design.bodies[0].is_surface == False
143+
assert design.bodies[1].name == "DuplicatesSurface"
144+
assert design.bodies[1].is_surface == True
145+
146+
126147
def test_open_file(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory):
127148
"""Test creation of a component, saving it to a file, and loading it again to a
128149
second component and make sure they have the same properties."""

0 commit comments

Comments
 (0)