diff --git a/doc/changelog.d/1712.added.md b/doc/changelog.d/1712.added.md new file mode 100644 index 0000000000..0d2f01063a --- /dev/null +++ b/doc/changelog.d/1712.added.md @@ -0,0 +1 @@ +Implementation of inspect & repair geometry \ No newline at end of file diff --git a/src/ansys/geometry/core/modeler.py b/src/ansys/geometry/core/modeler.py index cce6f73c7a..7c2eb3821d 100644 --- a/src/ansys/geometry/core/modeler.py +++ b/src/ansys/geometry/core/modeler.py @@ -132,7 +132,7 @@ def __init__( self._measurement_tools = MeasurementTools(self._grpc_client) # Enabling tools/commands for all: repair and prepare tools, geometry commands - self._repair_tools = RepairTools(self._grpc_client) + self._repair_tools = RepairTools(self._grpc_client, self) self._prepare_tools = PrepareTools(self._grpc_client) self._geometry_commands = GeometryCommands(self._grpc_client) self._unsupported = UnsupportedCommands(self._grpc_client, self) diff --git a/src/ansys/geometry/core/tools/check_geometry.py b/src/ansys/geometry/core/tools/check_geometry.py new file mode 100644 index 0000000000..44357dbfba --- /dev/null +++ b/src/ansys/geometry/core/tools/check_geometry.py @@ -0,0 +1,135 @@ +# Copyright (C) 2023 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +"""Module for repair tool message.""" + +from typing import TYPE_CHECKING + +from ansys.api.geometry.v0.repairtools_pb2 import RepairGeometryRequest +from ansys.api.geometry.v0.repairtools_pb2_grpc import RepairToolsStub +from ansys.geometry.core.connection.client import GrpcClient +from ansys.geometry.core.tools.repair_tool_message import RepairToolMessage + +if TYPE_CHECKING: # pragma: no cover + from ansys.geometry.core.designer.body import Body + + +class GeometryIssue: + """Provides return message for the repair tool methods.""" + + def __init__( + self, + message_type: str, + message_id: str, + message: str, + edges: list[str], + faces: list[str], + ): + """Initialize a new instance of a geometry issue found during geometry inspect. + + Parameters + ---------- + message_type: str + Type of the message (warning, error, info). + message_id: str + Identifier for the message. + message + Message that describes the geometry issue. + edges: list[str] + List of edges (if any) that are part of the issue. + modified_bodies: list[str] + List of faces that are part of the issue. + """ + self._message_type = message_type + self._message_id = message_id + self._message = message + self._edges = edges + self._faces = faces + + @property + def message_type(self) -> str: + """The type of the message (warning, error, info).""" + return self._message_type + + @property + def message_id(self) -> str: + """The identifier for the message.""" + return self._message_id + + @property + def message(self) -> str: + """The content of the message.""" + return self._message + + @property + def edges(self) -> list[str]: + """The List of edges (if any) that are part of the issue.""" + return self._edges + + @property + def faces(self) -> list[str]: + """The List of faces (if any) that are part of the issue.""" + return self._faces + + +class InspectResult: + """Provides the result of the inspect geometry operation.""" + + def __init__(self, grpc_client: GrpcClient, body: "Body", issues: list[GeometryIssue]): + """Initialize a new instance of the result of the inspect geometry operation. + + Parameters + ---------- + body: Body + Body for which issues are found. + issues: list[GeometryIssue] + List of issues for the body. + """ + self._body = body + self._issues = issues + self._repair_stub = RepairToolsStub(grpc_client.channel) + + @property + def body(self) -> "Body": + """The body for which issues are found.""" + return self._body + + @property + def issues(self) -> list[GeometryIssue]: + """The list of issues for the body.""" + return self._issues + + def repair(self) -> RepairToolMessage: + """Repair the problem area. + + Returns + ------- + RepairToolMessage + Message containing created and/or modified bodies. + """ + if not self.body: + return RepairToolMessage(False, [], []) + + repair_result_response = self._repair_stub.RepairGeometry( + RepairGeometryRequest(bodies=[self.body._grpc_id]) + ) + + return RepairToolMessage(repair_result_response.result.success, [], []) diff --git a/src/ansys/geometry/core/tools/repair_tools.py b/src/ansys/geometry/core/tools/repair_tools.py index 5571a27576..f9cebf2fd8 100644 --- a/src/ansys/geometry/core/tools/repair_tools.py +++ b/src/ansys/geometry/core/tools/repair_tools.py @@ -26,6 +26,12 @@ from google.protobuf.wrappers_pb2 import BoolValue, DoubleValue from ansys.api.geometry.v0.bodies_pb2_grpc import BodiesStub +from ansys.api.geometry.v0.models_pb2 import ( + InspectGeometryMessageId, + InspectGeometryMessageType, + InspectGeometryResult, + InspectGeometryResultIssue, +) from ansys.api.geometry.v0.repairtools_pb2 import ( FindAdjustSimplifyRequest, FindDuplicateFacesRequest, @@ -37,6 +43,8 @@ FindSmallFacesRequest, FindSplitEdgesRequest, FindStitchFacesRequest, + InspectGeometryRequest, + RepairGeometryRequest, ) from ansys.api.geometry.v0.repairtools_pb2_grpc import RepairToolsStub from ansys.geometry.core.connection import GrpcClient @@ -52,6 +60,7 @@ check_type_all_elements_in_iterable, min_backend_version, ) +from ansys.geometry.core.tools.check_geometry import GeometryIssue, InspectResult from ansys.geometry.core.tools.problem_areas import ( DuplicateFaceProblemAreas, ExtraEdgeProblemAreas, @@ -69,16 +78,18 @@ if TYPE_CHECKING: # pragma: no cover from ansys.geometry.core.designer.body import Body + from ansys.geometry.core.modeler import Modeler class RepairTools: """Repair tools for PyAnsys Geometry.""" - def __init__(self, grpc_client: GrpcClient): + def __init__(self, grpc_client: GrpcClient, modeler: "Modeler"): """Initialize a new instance of the ``RepairTools`` class.""" self._grpc_client = grpc_client self._repair_stub = RepairToolsStub(self._grpc_client.channel) self._bodies_stub = BodiesStub(self._grpc_client.channel) + self._modeler = modeler @protect_grpc def find_split_edges( @@ -598,3 +609,91 @@ def find_and_fix_split_edges( response.modified_bodies_monikers, ) return message + + @protect_grpc + @min_backend_version(25, 2, 0) + def inspect_geometry(self, bodies: list["Body"] = None) -> list[InspectResult]: + """Return a list of geometry issues organized by body. + + This method inspects the geometry and returns a list of the issues grouped by + the body where they are found. + + Parameters + ---------- + bodies : list[Body] + List of bodies to inspect the geometry for. + All bodies are inspected if the argument is not given. + + Returns + ------- + list[IssuesByBody] + List of objects representing geometry issues and the bodies where issues are found. + """ + parent_design = self._modeler.get_active_design() + body_ids = [] if bodies is None else [body._grpc_id for body in bodies] + inspect_result_response = self._repair_stub.InspectGeometry( + InspectGeometryRequest(bodies=body_ids) + ) + return self.__create_inspect_result_from_response( + parent_design, inspect_result_response.issues_by_body + ) + + def __create_inspect_result_from_response( + self, design, inspect_geometry_results: list[InspectGeometryResult] + ) -> list[InspectResult]: + inspect_results = [] + for inspect_geometry_result in inspect_geometry_results: + body = get_bodies_from_ids(design, [inspect_geometry_result.body.id]) + issues = self.__create_issues_from_response(inspect_geometry_result.issues) + inspect_result = InspectResult( + grpc_client=self._grpc_client, body=body[0], issues=issues + ) + inspect_results.append(inspect_result) + + return inspect_results + + def __create_issues_from_response( + self, + inspect_geometry_result_issues: list[InspectGeometryResultIssue], + ) -> list[GeometryIssue]: + issues = [] + for inspect_result_issue in inspect_geometry_result_issues: + message_type = InspectGeometryMessageType.Name(inspect_result_issue.message_type) + message_id = InspectGeometryMessageId.Name(inspect_result_issue.message_id) + message = inspect_result_issue.message + + issue = GeometryIssue( + message_type=message_type, + message_id=message_id, + message=message, + faces=[face.id for face in inspect_result_issue.faces], + edges=[edge.id for edge in inspect_result_issue.edges], + ) + issues.append(issue) + return issues + + @protect_grpc + @min_backend_version(25, 2, 0) + def repair_geometry(self, bodies: list["Body"] = None) -> RepairToolMessage: + """Attempt to repair the geometry for the given bodies. + + This method inspects the geometry for the given bodies and attempts to repair them. + + Parameters + ---------- + bodies : list[Body] + List of bodies where to attempt to repair the geometry. + All bodies are repaired if the argument is not given. + + Returns + ------- + RepairToolMessage + Message containing success of the operation. + """ + body_ids = [] if bodies is None else [body._grpc_id for body in bodies] + repair_result_response = self._repair_stub.RepairGeometry( + RepairGeometryRequest(bodies=body_ids) + ) + + message = RepairToolMessage(repair_result_response.result.success, [], []) + return message diff --git a/tests/integration/files/InspectAndRepair01.scdocx b/tests/integration/files/InspectAndRepair01.scdocx new file mode 100644 index 0000000000..edc492fa50 Binary files /dev/null and b/tests/integration/files/InspectAndRepair01.scdocx differ diff --git a/tests/integration/test_repair_tools.py b/tests/integration/test_repair_tools.py index a61e3653c5..c5a3416e98 100644 --- a/tests/integration/test_repair_tools.py +++ b/tests/integration/test_repair_tools.py @@ -453,3 +453,28 @@ def test_find_and_fix_extra_edges(modeler: Modeler): for body in design.bodies: final_edge_count += len(body.edges) assert final_edge_count == 36 + + +def test_inspect_geometry(modeler: Modeler): + """Test the result of the inspect geometry query and the ability to repair one issue""" + modeler.open_file(FILES_DIR / "InspectAndRepair01.scdocx") + inspect_results = modeler.repair_tools.inspect_geometry() + assert len(inspect_results) == 1 + issues = len(inspect_results[0].issues) + assert issues == 7 + result_to_repair = inspect_results[0] + result_to_repair.repair() + # Reinspect the geometry + inspect_results = modeler.repair_tools.inspect_geometry() + # All issues should have been fixed + assert len(inspect_results) == 0 + + +def test_repair_geometry(modeler: Modeler): + """Test the ability to repair a geometry. Inspect geometry is called behind the scenes""" + modeler.open_file(FILES_DIR / "InspectAndRepair01.scdocx") + modeler.repair_tools.repair_geometry() + # Reinspect the geometry + inspect_results = modeler.repair_tools.inspect_geometry() + # All issues should have been fixed + assert len(inspect_results) == 0