From 709cb51510b80078c685109875e8180105804360 Mon Sep 17 00:00:00 2001 From: Jacob Kerstetter Date: Tue, 30 Sep 2025 10:00:48 -0400 Subject: [PATCH 1/6] wip --- .../core/_grpc/_services/base/bodies.py | 5 +++ .../core/_grpc/_services/v0/bodies.py | 23 ++++++++++++ .../core/designer/geometry_commands.py | 36 +++++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/src/ansys/geometry/core/_grpc/_services/base/bodies.py b/src/ansys/geometry/core/_grpc/_services/base/bodies.py index d4adb13175..fcf60c5842 100644 --- a/src/ansys/geometry/core/_grpc/_services/base/bodies.py +++ b/src/ansys/geometry/core/_grpc/_services/base/bodies.py @@ -228,3 +228,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 stitch(self, **kwargs) -> dict: + """Stitch 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 a8137d2dd8..5bf38f549e 100644 --- a/src/ansys/geometry/core/_grpc/_services/v0/bodies.py +++ b/src/ansys/geometry/core/_grpc/_services/v0/bodies.py @@ -825,3 +825,26 @@ 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 stitch(self, **kwargs) -> dict: # noqa: D102 + from ansys.api.geometry.v0.bodies_pb2 import StitchRequest, StitchRequestData + + # Create the request - assumes all inputs are valid and of the proper type + request = StitchRequest( + StitchRequestData( + ids=kwargs["body_ids"], + tolerance=from_measurement_to_server_length(kwargs["tolerance"]), + ) + ) + + # Call the gRPC service + resp = self.stub.Stitch(request=request) + + # Return the response - formatted as a dictionary + return { + "id": resp.id, + "name": resp.name, + "master_id": resp.master_id, + "is_surface": resp.is_surface, + } \ No newline at end of file diff --git a/src/ansys/geometry/core/designer/geometry_commands.py b/src/ansys/geometry/core/designer/geometry_commands.py index 588804c3c8..51c87dc162 100644 --- a/src/ansys/geometry/core/designer/geometry_commands.py +++ b/src/ansys/geometry/core/designer/geometry_commands.py @@ -1833,3 +1833,39 @@ def revolve_edges( design = get_design_from_edge(edges[0]) design._update_design_inplace() + + @min_backend_version(26, 1, 0) + def stitch_bodies(self, bodies: list["Body"], tolerance: Distance | Quantity | Real) -> bool: + """Stitch bodies together. + + Parameters + ---------- + bodies : list[Body] + Bodies to stitch. + tolerance : Distance | Quantity | Real + Tolerance to use when stitching bodies. Default units are meters. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + from ansys.geometry.core.designer.body import Body + + check_type_all_elements_in_iterable(bodies, Body) + + for body in bodies: + body._reset_tessellation_cache() + + tolerance = tolerance if isinstance(tolerance, Distance) else Distance(tolerance) + + result = self._grpc_client._services.bodies.stitch( + body_ids=[body.id for body in bodies], + tolerance=tolerance, + ) + + if result.get("success"): + design = get_design_from_body(bodies[0]) + design._update_design_inplace() + + return result.get("success") \ No newline at end of file From 4124aa6921ed064fef814cd806e36fb49e3736c2 Mon Sep 17 00:00:00 2001 From: Jacob Kerstetter Date: Fri, 3 Oct 2025 13:36:00 -0400 Subject: [PATCH 2/6] changed stitch to combine_merge to utilize already in place server method --- .../core/_grpc/_services/base/bodies.py | 4 +- .../core/_grpc/_services/v0/bodies.py | 20 +++------- .../core/_grpc/_services/v1/bodies.py | 4 ++ src/ansys/geometry/core/designer/body.py | 26 ++++++++++++- .../core/designer/geometry_commands.py | 38 +------------------ 5 files changed, 38 insertions(+), 54 deletions(-) diff --git a/src/ansys/geometry/core/_grpc/_services/base/bodies.py b/src/ansys/geometry/core/_grpc/_services/base/bodies.py index efc1ec0c0d..a3ad45a881 100644 --- a/src/ansys/geometry/core/_grpc/_services/base/bodies.py +++ b/src/ansys/geometry/core/_grpc/_services/base/bodies.py @@ -233,6 +233,6 @@ def create_body_from_loft_profiles_with_guides(self, **kwargs) -> dict: pass @abstractmethod - def stitch(self, **kwargs) -> dict: - """Stitch bodies.""" + 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 e5a1a24f5b..40e3b23cd0 100644 --- a/src/ansys/geometry/core/_grpc/_services/v0/bodies.py +++ b/src/ansys/geometry/core/_grpc/_services/v0/bodies.py @@ -892,24 +892,16 @@ def create_body_from_loft_profiles_with_guides(self, **kwargs) -> dict: # noqa: } @protect_grpc - def stitch(self, **kwargs) -> dict: # noqa: D102 - from ansys.api.geometry.v0.bodies_pb2 import StitchRequest, StitchRequestData + 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 = StitchRequest( - StitchRequestData( - ids=kwargs["body_ids"], - tolerance=from_measurement_to_server_length(kwargs["tolerance"]), - ) + request = CombineMergeBodiesRequest( + target_selection=[build_grpc_id(id) for id in kwargs["body_ids"]], ) # Call the gRPC service - resp = self.stub.Stitch(request=request) + _ = self.command_stub.CombineMergeBodies(request=request) # Return the response - formatted as a dictionary - return { - "id": resp.id, - "name": resp.name, - "master_id": resp.master_id, - "is_surface": resp.is_surface, - } \ No newline at end of file + return {} \ No newline at end of file diff --git a/src/ansys/geometry/core/_grpc/_services/v1/bodies.py b/src/ansys/geometry/core/_grpc/_services/v1/bodies.py index c06afe63b3..79364c594a 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..b9e3d0710b 100644 --- a/src/ansys/geometry/core/designer/body.py +++ b/src/ansys/geometry/core/designer/body.py @@ -818,6 +818,16 @@ def unite(self, other: Union["Body", Iterable["Body"]], keep_other: bool = False the united body is retained. """ 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. + """ + return class MasterBody(IBody): @@ -1359,6 +1369,17 @@ def remove_faces(self, selection: Face | Iterable[Face], offset: Real) -> bool: self._grpc_client.log.warning(f"Failed to remove faces from body {self.id}.") 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, @@ -1386,7 +1407,7 @@ def unite(self, other: Union["Body", Iterable["Body"]], keep_other: bool = False raise NotImplementedError( "MasterBody does not implement Boolean methods. Call this method on a body instead." ) - + def __repr__(self) -> str: """Represent the master body as a string.""" lines = [f"ansys.geometry.core.designer.MasterBody {hex(id(self))}"] @@ -1903,6 +1924,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/src/ansys/geometry/core/designer/geometry_commands.py b/src/ansys/geometry/core/designer/geometry_commands.py index 51c87dc162..7fa2db5695 100644 --- a/src/ansys/geometry/core/designer/geometry_commands.py +++ b/src/ansys/geometry/core/designer/geometry_commands.py @@ -1832,40 +1832,4 @@ def revolve_edges( ) design = get_design_from_edge(edges[0]) - design._update_design_inplace() - - @min_backend_version(26, 1, 0) - def stitch_bodies(self, bodies: list["Body"], tolerance: Distance | Quantity | Real) -> bool: - """Stitch bodies together. - - Parameters - ---------- - bodies : list[Body] - Bodies to stitch. - tolerance : Distance | Quantity | Real - Tolerance to use when stitching bodies. Default units are meters. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - from ansys.geometry.core.designer.body import Body - - check_type_all_elements_in_iterable(bodies, Body) - - for body in bodies: - body._reset_tessellation_cache() - - tolerance = tolerance if isinstance(tolerance, Distance) else Distance(tolerance) - - result = self._grpc_client._services.bodies.stitch( - body_ids=[body.id for body in bodies], - tolerance=tolerance, - ) - - if result.get("success"): - design = get_design_from_body(bodies[0]) - design._update_design_inplace() - - return result.get("success") \ No newline at end of file + design._update_design_inplace() \ No newline at end of file From aac1f951d46b3df01777dfa314e3194d50b022ed Mon Sep 17 00:00:00 2001 From: Jacob Kerstetter Date: Fri, 3 Oct 2025 14:24:28 -0400 Subject: [PATCH 3/6] added test --- tests/integration/test_design.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/integration/test_design.py b/tests/integration/test_design.py index 4db76fe19b..5381c8048b 100644 --- a/tests/integration/test_design.py +++ b/tests/integration/test_design.py @@ -3909,3 +3909,26 @@ 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) \ No newline at end of file From 5a921091af73a099ced4867411509c1d1e6d3165 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 18:36:56 +0000 Subject: [PATCH 4/6] chore: auto fixes from pre-commit hooks --- src/ansys/geometry/core/_grpc/_services/v0/bodies.py | 2 +- src/ansys/geometry/core/_grpc/_services/v1/bodies.py | 4 ++-- src/ansys/geometry/core/designer/body.py | 8 ++++---- src/ansys/geometry/core/designer/geometry_commands.py | 2 +- tests/integration/test_design.py | 3 ++- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/ansys/geometry/core/_grpc/_services/v0/bodies.py b/src/ansys/geometry/core/_grpc/_services/v0/bodies.py index 40e3b23cd0..6452108f4a 100644 --- a/src/ansys/geometry/core/_grpc/_services/v0/bodies.py +++ b/src/ansys/geometry/core/_grpc/_services/v0/bodies.py @@ -904,4 +904,4 @@ def combine_merge(self, **kwargs) -> dict: # noqa: D102 _ = self.command_stub.CombineMergeBodies(request=request) # Return the response - formatted as a dictionary - return {} \ No newline at end of file + return {} diff --git a/src/ansys/geometry/core/_grpc/_services/v1/bodies.py b/src/ansys/geometry/core/_grpc/_services/v1/bodies.py index 79364c594a..c5a13c9fad 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/bodies.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/bodies.py @@ -203,7 +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 + 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 b9e3d0710b..deabecce8a 100644 --- a/src/ansys/geometry/core/designer/body.py +++ b/src/ansys/geometry/core/designer/body.py @@ -818,10 +818,10 @@ def unite(self, other: Union["Body", Iterable["Body"]], keep_other: bool = False the united body is retained. """ 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]] @@ -1369,7 +1369,7 @@ def remove_faces(self, selection: Face | Iterable[Face], offset: Real) -> bool: self._grpc_client.log.warning(f"Failed to remove faces from body {self.id}.") return result.success - + @min_backend_version(25, 2, 0) @check_input_types def combine_merge(self, other: Union["Body", list["Body"]]) -> None: # noqa: D102 @@ -1407,7 +1407,7 @@ def unite(self, other: Union["Body", Iterable["Body"]], keep_other: bool = False raise NotImplementedError( "MasterBody does not implement Boolean methods. Call this method on a body instead." ) - + def __repr__(self) -> str: """Represent the master body as a string.""" lines = [f"ansys.geometry.core.designer.MasterBody {hex(id(self))}"] diff --git a/src/ansys/geometry/core/designer/geometry_commands.py b/src/ansys/geometry/core/designer/geometry_commands.py index 7fa2db5695..588804c3c8 100644 --- a/src/ansys/geometry/core/designer/geometry_commands.py +++ b/src/ansys/geometry/core/designer/geometry_commands.py @@ -1832,4 +1832,4 @@ def revolve_edges( ) design = get_design_from_edge(edges[0]) - design._update_design_inplace() \ No newline at end of file + design._update_design_inplace() diff --git a/tests/integration/test_design.py b/tests/integration/test_design.py index 5381c8048b..672c90ce61 100644 --- a/tests/integration/test_design.py +++ b/tests/integration/test_design.py @@ -3910,6 +3910,7 @@ 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) @@ -3931,4 +3932,4 @@ def test_combine_merge(modeler: Modeler): 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) \ No newline at end of file + assert box1.volume.m == pytest.approx(Quantity(2.5, UNITS.m**3).m, rel=1e-6, abs=1e-8) From cff873a5d08ea8862b913d539a3f02ef13b65f0f Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Fri, 3 Oct 2025 18:38:02 +0000 Subject: [PATCH 5/6] chore: adding changelog file 2282.added.md [dependabot-skip] --- doc/changelog.d/2282.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/2282.added.md 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 From a46c65aa2db8ffb5c5b761144e35b3aaa048870e Mon Sep 17 00:00:00 2001 From: Jacob Kerstetter Date: Mon, 6 Oct 2025 07:53:50 -0400 Subject: [PATCH 6/6] adding docstring note --- src/ansys/geometry/core/designer/body.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ansys/geometry/core/designer/body.py b/src/ansys/geometry/core/designer/body.py index b9e3d0710b..89c77835cc 100644 --- a/src/ansys/geometry/core/designer/body.py +++ b/src/ansys/geometry/core/designer/body.py @@ -826,6 +826,10 @@ def combine_merge(self, other: Union["Body", list["Body"]]) -> None: ---------- 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