diff --git a/doc/changelog.d/2282.added.md b/doc/changelog.d/2282.added.md new file mode 100644 index 0000000000..93d50d5fef --- /dev/null +++ b/doc/changelog.d/2282.added.md @@ -0,0 +1 @@ +Combine and merge bodies diff --git a/src/ansys/geometry/core/_grpc/_services/base/bodies.py b/src/ansys/geometry/core/_grpc/_services/base/bodies.py index d0a4dda485..a3ad45a881 100644 --- a/src/ansys/geometry/core/_grpc/_services/base/bodies.py +++ b/src/ansys/geometry/core/_grpc/_services/base/bodies.py @@ -231,3 +231,8 @@ def split_body(self, **kwargs) -> dict: def create_body_from_loft_profiles_with_guides(self, **kwargs) -> dict: """Create a body from loft profiles with guides.""" pass + + @abstractmethod + def combine_merge(self, **kwargs) -> dict: + """Combine and merge bodies.""" + pass diff --git a/src/ansys/geometry/core/_grpc/_services/v0/bodies.py b/src/ansys/geometry/core/_grpc/_services/v0/bodies.py index fba4d3b297..6452108f4a 100644 --- a/src/ansys/geometry/core/_grpc/_services/v0/bodies.py +++ b/src/ansys/geometry/core/_grpc/_services/v0/bodies.py @@ -890,3 +890,18 @@ def create_body_from_loft_profiles_with_guides(self, **kwargs) -> dict: # noqa: "master_id": new_body.master_id, "is_surface": new_body.is_surface, } + + @protect_grpc + def combine_merge(self, **kwargs) -> dict: # noqa: D102 + from ansys.api.geometry.v0.commands_pb2 import CombineMergeBodiesRequest + + # Create the request - assumes all inputs are valid and of the proper type + request = CombineMergeBodiesRequest( + target_selection=[build_grpc_id(id) for id in kwargs["body_ids"]], + ) + + # Call the gRPC service + _ = self.command_stub.CombineMergeBodies(request=request) + + # Return the response - formatted as a dictionary + return {} diff --git a/src/ansys/geometry/core/_grpc/_services/v1/bodies.py b/src/ansys/geometry/core/_grpc/_services/v1/bodies.py index c06afe63b3..c5a13c9fad 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/bodies.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/bodies.py @@ -203,3 +203,7 @@ def split_body(self, **kwargs) -> dict: # noqa: D102 @protect_grpc def create_body_from_loft_profiles_with_guides(self, **kwargs) -> dict: # noqa: D102 raise NotImplementedError + + @protect_grpc + def combine_merge(self, **kwargs) -> dict: # noqa: D102 + raise NotImplementedError diff --git a/src/ansys/geometry/core/designer/body.py b/src/ansys/geometry/core/designer/body.py index b1d948b977..0d6ef83692 100644 --- a/src/ansys/geometry/core/designer/body.py +++ b/src/ansys/geometry/core/designer/body.py @@ -819,6 +819,20 @@ def unite(self, other: Union["Body", Iterable["Body"]], keep_other: bool = False """ return + def combine_merge(self, other: Union["Body", list["Body"]]) -> None: + """Combine this body with another body or bodies, merging them into a single body. + + Parameters + ---------- + other : Union[Body, list[Body]] + The body or list of bodies to combine with this body. + + Notes + ----- + The ``self`` parameter is directly modified, and the ``other`` bodies are consumed. + """ + return + class MasterBody(IBody): """Represents solids and surfaces organized within the design assembly. @@ -1360,6 +1374,17 @@ def remove_faces(self, selection: Face | Iterable[Face], offset: Real) -> bool: return result.success + @min_backend_version(25, 2, 0) + @check_input_types + def combine_merge(self, other: Union["Body", list["Body"]]) -> None: # noqa: D102 + other = other if isinstance(other, list) else [other] + check_type_all_elements_in_iterable(other, Body) + + self._grpc_client.log.debug(f"Combining and merging to body {self.id}.") + self._grpc_client.services.bodies.combine_merge( + body_ids=[self.id] + [body.id for body in other] + ) + def plot( # noqa: D102 self, merge: bool = True, @@ -1903,6 +1928,9 @@ def unite(self, other: Union["Body", Iterable["Body"]], keep_other: bool = False else: self.__generic_boolean_command(other, False, "unite", "union operation failed") + def combine_merge(self, other: Union["Body", list["Body"]]) -> None: # noqa: D102 + self._template.combine_merge(other) + @reset_tessellation_cache @ensure_design_is_active @check_input_types diff --git a/tests/integration/test_design.py b/tests/integration/test_design.py index 4db76fe19b..672c90ce61 100644 --- a/tests/integration/test_design.py +++ b/tests/integration/test_design.py @@ -3909,3 +3909,27 @@ def test_write_body_facets_on_save( missing = expected_files - namelist assert not missing + + +def test_combine_merge(modeler: Modeler): + design = modeler.create_design("combine_merge") + box1 = design.extrude_sketch("box1", Sketch().box(Point2D([0, 0]), 1, 1), 1) + box2 = design.extrude_sketch("box2", Sketch().box(Point2D([0.5, 0.5]), 1, 1), 1) + assert len(design.bodies) == 2 + + # combine the two boxes and check body count and volume + box1.combine_merge([box2]) + design._update_design_inplace() + assert len(design.bodies) == 1 + assert box1.volume.m == pytest.approx(Quantity(1.75, UNITS.m**3).m, rel=1e-6, abs=1e-8) + + # create a third box + box1 = design.bodies[0] + box3 = design.extrude_sketch("box3", Sketch().box(Point2D([-0.5, -0.5]), 1, 1), 1) + assert len(design.bodies) == 2 + + # combine the two boxes and check body count and volume + box1.combine_merge([box3]) + design._update_design_inplace() + assert len(design.bodies) == 1 + assert box1.volume.m == pytest.approx(Quantity(2.5, UNITS.m**3).m, rel=1e-6, abs=1e-8)