diff --git a/doc/changelog.d/1953.added.md b/doc/changelog.d/1953.added.md new file mode 100644 index 0000000000..6674107dfd --- /dev/null +++ b/doc/changelog.d/1953.added.md @@ -0,0 +1 @@ +Find and Fix Stitch Faces/Missing Faces Enhancements \ No newline at end of file diff --git a/src/ansys/geometry/core/_grpc/_services/base/repair_tools.py b/src/ansys/geometry/core/_grpc/_services/base/repair_tools.py index f9fbd791cd..df44e24e1d 100644 --- a/src/ansys/geometry/core/_grpc/_services/base/repair_tools.py +++ b/src/ansys/geometry/core/_grpc/_services/base/repair_tools.py @@ -42,6 +42,7 @@ class GRPCRepairToolsService(ABC): def __init__(self, channel: grpc.Channel): """Initialize the gRPC repair tools service.""" + pass # pragma: no cover @abstractmethod def find_split_edges(self, **kwargs) -> dict: @@ -113,6 +114,11 @@ def find_and_fix_simplify(self, **kwargs) -> dict: """Identify and simplify areas in the geometry.""" pass # pragma: no cover + @abstractmethod + def find_and_fix_stitch_faces(self, **kwargs) -> dict: + """Identify and stitch faces in the geometry.""" + pass # pragma: no cover + @abstractmethod def inspect_geometry(self, **kwargs) -> dict: """Inspect the geometry for issues.""" diff --git a/src/ansys/geometry/core/_grpc/_services/v0/repair_tools.py b/src/ansys/geometry/core/_grpc/_services/v0/repair_tools.py index 5b7000e990..e7ae573c2b 100644 --- a/src/ansys/geometry/core/_grpc/_services/v0/repair_tools.py +++ b/src/ansys/geometry/core/_grpc/_services/v0/repair_tools.py @@ -58,7 +58,7 @@ def find_split_edges(self, **kwargs) -> dict: # noqa: D102 from ansys.api.geometry.v0.repairtools_pb2 import FindSplitEdgesRequest - # Create the gRPC request + # Create the request - assumes all inputs are valid and of the proper type request = FindSplitEdgesRequest( bodies_or_faces=kwargs["bodies_or_faces"], angle=DoubleValue(value=float(kwargs["angle"])), @@ -104,6 +104,7 @@ def find_extra_edges(self, **kwargs) -> dict: # noqa: D102 def find_inexact_edges(self, **kwargs) -> dict: # noqa: D102 from ansys.api.geometry.v0.repairtools_pb2 import FindInexactEdgesRequest + # Create the request - assumes all inputs are valid and of the proper type request = FindInexactEdgesRequest(selection=kwargs["selection"]) # Call the gRPC service @@ -150,6 +151,7 @@ def find_short_edges(self, **kwargs) -> dict: # noqa: D102 def find_duplicate_faces(self, **kwargs) -> dict: # noqa: D102 from ansys.api.geometry.v0.repairtools_pb2 import FindDuplicateFacesRequest + # Create the request - assumes all inputs are valid and of the proper type request = FindDuplicateFacesRequest(faces=kwargs["faces"]) # Call the gRPC service @@ -168,9 +170,26 @@ def find_duplicate_faces(self, **kwargs) -> dict: # noqa: D102 @protect_grpc def find_missing_faces(self, **kwargs) -> dict: # noqa: D102 + from google.protobuf.wrappers_pb2 import DoubleValue + from ansys.api.geometry.v0.repairtools_pb2 import FindMissingFacesRequest - request = FindMissingFacesRequest(faces=kwargs["faces"]) + from ..base.conversions import ( + from_measurement_to_server_angle, + from_measurement_to_server_length, + ) + + # Create the request - assumes all inputs are valid and of the proper type + request = FindMissingFacesRequest( + faces=kwargs["faces"], + angle=DoubleValue(value=from_measurement_to_server_angle(kwargs["angle"])) + if kwargs["angle"] is not None + else None, + distance=DoubleValue(value=from_measurement_to_server_length(kwargs["distance"])) + if kwargs["distance"] is not None + else None, + ) + # Call the gRPC service response = self.stub.FindMissingFaces(request) @@ -189,7 +208,9 @@ def find_missing_faces(self, **kwargs) -> dict: # noqa: D102 def find_small_faces(self, **kwargs) -> dict: # noqa: D102 from ansys.api.geometry.v0.repairtools_pb2 import FindSmallFacesRequest + # Create the request - assumes all inputs are valid and of the proper type request = FindSmallFacesRequest(selection=kwargs["selection"]) + # Call the gRPC service response = self.stub.FindSmallFaces(request) @@ -206,11 +227,25 @@ def find_small_faces(self, **kwargs) -> dict: # noqa: D102 @protect_grpc def find_stitch_faces(self, **kwargs) -> dict: # noqa: D102 + from google.protobuf.wrappers_pb2 import DoubleValue + from ansys.api.geometry.v0.repairtools_pb2 import FindStitchFacesRequest - request = FindStitchFacesRequest(faces=kwargs["faces"]) + from ..base.conversions import from_measurement_to_server_length + + # Create the request - assumes all inputs are valid and of the proper type + request = FindStitchFacesRequest( + faces=kwargs["faces"], + maximum_distance=DoubleValue( + value=from_measurement_to_server_length(kwargs["distance"]) + ) + if kwargs["distance"] is not None + else None, + ) + # Call the gRPC service response = self.stub.FindStitchFaces(request) + # Return the response - formatted as a dictionary return { "problems": [ @@ -226,10 +261,12 @@ def find_stitch_faces(self, **kwargs) -> dict: # noqa: D102 def find_simplify(self, **kwargs) -> dict: # noqa: D102 from ansys.api.geometry.v0.repairtools_pb2 import FindAdjustSimplifyRequest + # Create the request - assumes all inputs are valid and of the proper type request = FindAdjustSimplifyRequest(selection=kwargs["selection"]) # Call the gRPC service response = self.stub.FindAdjustSimplify(request) + # Return the response - formatted as a dictionary return { "problems": [ @@ -245,12 +282,15 @@ def find_simplify(self, **kwargs) -> dict: # noqa: D102 def find_and_fix_simplify(self, **kwargs) -> dict: # noqa: D102 from ansys.api.geometry.v0.repairtools_pb2 import FindAdjustSimplifyRequest + # Create the request - assumes all inputs are valid and of the proper type request = FindAdjustSimplifyRequest( selection=kwargs["selection"], comprehensive=kwargs["comprehensive_result"], ) + # Call the gRPC service response = self.stub.FindAndSimplify(request) + # Return the response - formatted as a dictionary return { "success": response.success, @@ -260,23 +300,54 @@ def find_and_fix_simplify(self, **kwargs) -> dict: # noqa: D102 "modified_bodies_monikers": [], } + @protect_grpc + def find_and_fix_stitch_faces(self, **kwargs) -> dict: # noqa: D102 + from google.protobuf.wrappers_pb2 import BoolValue, DoubleValue + + from ansys.api.geometry.v0.repairtools_pb2 import FindStitchFacesRequest + + # Create the request - assumes all inputs are valid and of the proper type + request = FindStitchFacesRequest( + faces=kwargs["body_ids"], + maximum_distance=DoubleValue(value=kwargs["max_distance"]) + if kwargs["max_distance"] is not None + else None, + allow_multiple_bodies=BoolValue(value=kwargs["allow_multiple_bodies"]), + maintain_components=BoolValue(value=kwargs["maintain_components"]), + check_for_coincidence=BoolValue(value=kwargs["check_for_coincidence"]), + comprehensive=kwargs["comprehensive_result"], + ) + + # Call the gRPC service + response = self.stub.FindAndFixStitchFaces(request) + + # Return the response - formatted as a dictionary + return { + "success": response.success, + "created_bodies_monikers": response.created_bodies_monikers, + "modified_bodies_monikers": response.modified_bodies_monikers, + "found": response.found, + "repaired": response.repaired, + } + @protect_grpc def inspect_geometry(self, **kwargs) -> dict: # noqa: D102 from ansys.api.geometry.v0.repairtools_pb2 import InspectGeometryRequest - # Create the gRPC request + # Create the request - assumes all inputs are valid and of the proper type request = InspectGeometryRequest(bodies=kwargs.get("bodies", [])) # Call the gRPC service inspect_result_response = self.stub.InspectGeometry(request) # Serialize and return the response - return self.serialize_inspect_result_response(inspect_result_response) + return self.__serialize_inspect_result_response(inspect_result_response) @protect_grpc def repair_geometry(self, **kwargs) -> dict: # noqa: D102 from ansys.api.geometry.v0.repairtools_pb2 import RepairGeometryRequest + # Create the request - assumes all inputs are valid and of the proper type request = RepairGeometryRequest(bodies=kwargs.get("bodies", [])) # Call the gRPC service @@ -293,6 +364,7 @@ def find_interferences(self, **kwargs) -> dict: # noqa: D102 from ansys.api.geometry.v0.repairtools_pb2 import FindInterferenceRequest + # Create the request - assumes all inputs are valid and of the proper type request = FindInterferenceRequest( bodies=kwargs["bodies"], cut_smaller_body=BoolValue(value=kwargs["cut_smaller_body"]), @@ -318,11 +390,13 @@ def find_and_fix_short_edges(self, **kwargs): # noqa: D102 from ansys.api.geometry.v0.repairtools_pb2 import FindShortEdgesRequest + # Create the request - assumes all inputs are valid and of the proper type request = FindShortEdgesRequest( selection=kwargs["selection"], max_edge_length=DoubleValue(value=kwargs["length"]), comprehensive=kwargs["comprehensive_result"], ) + # Call the gRPC service response = self.stub.FindAndFixShortEdges(request) @@ -339,10 +413,12 @@ def find_and_fix_short_edges(self, **kwargs): # noqa: D102 def find_and_fix_extra_edges(self, **kwargs) -> dict: # noqa: D102 from ansys.api.geometry.v0.repairtools_pb2 import FindExtraEdgesRequest + # Create the request - assumes all inputs are valid and of the proper type request = FindExtraEdgesRequest( selection=kwargs["selection"], comprehensive=kwargs["comprehensive_result"], ) + # Call the gRPC service response = self.stub.FindAndFixExtraEdges(request) @@ -361,6 +437,7 @@ def find_and_fix_split_edges(self, **kwargs) -> dict: # noqa: D102 from ansys.api.geometry.v0.repairtools_pb2 import FindSplitEdgesRequest + # Create the request - assumes all inputs are valid and of the proper type request = FindSplitEdgesRequest( bodies_or_faces=kwargs["bodies_or_faces"], angle=DoubleValue(value=float(kwargs["angle"])), @@ -380,8 +457,7 @@ def find_and_fix_split_edges(self, **kwargs) -> dict: # noqa: D102 "modified_bodies_monikers": [], } - @staticmethod - def serialize_inspect_result_response(response) -> dict: # noqa: D102 + def __serialize_inspect_result_response(self, response) -> dict: # noqa: D102 def serialize_body(body): return { "id": body.id, diff --git a/src/ansys/geometry/core/_grpc/_services/v1/repair_tools.py b/src/ansys/geometry/core/_grpc/_services/v1/repair_tools.py index 75c23aa7f1..a941fc7670 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/repair_tools.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/repair_tools.py @@ -19,14 +19,14 @@ # 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 containing the repair tools service implementation.""" +"""Module containing the repair tools service implementation for v1.""" from abc import ABC import grpc -class GRPCRepairToolsServiceV1(ABC): +class GRPCRepairToolsServiceV1(ABC): # pragma: no cover """Repair tools service for gRPC communication with the Geometry server. Parameters @@ -36,7 +36,9 @@ class GRPCRepairToolsServiceV1(ABC): """ def __init__(self, channel: grpc.Channel): - """Initialize the MeasurementToolsService class.""" + from ansys.api.geometry.v1.repairtools_pb2_grpc import RepairToolsStub + + self.stub = RepairToolsStub(channel) def find_split_edges(self, **kwargs) -> dict: # noqa: D102 raise NotImplementedError @@ -80,6 +82,9 @@ def find_and_fix_split_edges(self, **kwargs) -> dict: # noqa: D102 def find_and_fix_simplify(self, **kwargs) -> dict: # noqa: D102 raise NotImplementedError + def find_and_fix_stitch_faces(self, **kwargs) -> dict: # noqa: D102 + raise NotImplementedError + def inspect_geometry(self, **kwargs) -> dict: # noqa: D102 raise NotImplementedError diff --git a/src/ansys/geometry/core/tools/repair_tools.py b/src/ansys/geometry/core/tools/repair_tools.py index a0d468a70b..56e484942f 100644 --- a/src/ansys/geometry/core/tools/repair_tools.py +++ b/src/ansys/geometry/core/tools/repair_tools.py @@ -23,8 +23,9 @@ from typing import TYPE_CHECKING +import pint + from ansys.geometry.core.connection import GrpcClient -from ansys.geometry.core.errors import protect_grpc from ansys.geometry.core.misc.auxiliary import ( get_bodies_from_ids, get_design_from_body, @@ -36,6 +37,7 @@ check_type_all_elements_in_iterable, min_backend_version, ) +from ansys.geometry.core.misc.measurements import Angle, Distance from ansys.geometry.core.tools.check_geometry import GeometryIssue, InspectResult from ansys.geometry.core.tools.problem_areas import ( DuplicateFaceProblemAreas, @@ -65,7 +67,6 @@ def __init__(self, grpc_client: GrpcClient, modeler: "Modeler"): self._modeler = modeler self._grpc_client = grpc_client - @protect_grpc def find_split_edges( self, bodies: list["Body"], angle: Real = 0.0, length: Real = 0.0 ) -> list[SplitEdgeProblemAreas]: @@ -93,21 +94,20 @@ def find_split_edges( body_ids = [body.id for body in bodies] - problem_areas_response = self._grpc_client.services.repair_tools.find_split_edges( + response = self._grpc_client.services.repair_tools.find_split_edges( bodies_or_faces=body_ids, angle=angle, distance=length ) parent_design = get_design_from_body(bodies[0]) return [ SplitEdgeProblemAreas( - f"{res['id']}", + f"{res.get('id')}", self._grpc_client, - get_edges_from_ids(parent_design, res["edges"]), + get_edges_from_ids(parent_design, res.get("edges")), ) - for res in problem_areas_response["problems"] + for res in response.get("problems") ] - @protect_grpc def find_extra_edges(self, bodies: list["Body"]) -> list[ExtraEdgeProblemAreas]: """Find the extra edges in the given list of bodies. @@ -128,21 +128,18 @@ def find_extra_edges(self, bodies: list["Body"]) -> list[ExtraEdgeProblemAreas]: return [] body_ids = [body.id for body in bodies] - problem_areas_response = self._grpc_client.services.repair_tools.find_extra_edges( - selection=body_ids - ) + response = self._grpc_client.services.repair_tools.find_extra_edges(selection=body_ids) parent_design = get_design_from_body(bodies[0]) return [ ExtraEdgeProblemAreas( - f"{res['id']}", + f"{res.get('id')}", self._grpc_client, - get_edges_from_ids(parent_design, res["edges"]), + get_edges_from_ids(parent_design, res.get("edges")), ) - for res in problem_areas_response["problems"] + for res in response.get("problems") ] - @protect_grpc def find_inexact_edges(self, bodies: list["Body"]) -> list[InexactEdgeProblemAreas]: """Find inexact edges in the given list of bodies. @@ -163,22 +160,19 @@ def find_inexact_edges(self, bodies: list["Body"]) -> list[InexactEdgeProblemAre return [] body_ids = [body.id for body in bodies] - problem_areas_response = self._grpc_client.services.repair_tools.find_inexact_edges( - selection=body_ids - ) + response = self._grpc_client.services.repair_tools.find_inexact_edges(selection=body_ids) parent_design = get_design_from_body(bodies[0]) return [ InexactEdgeProblemAreas( - f"{res['id']}", + f"{res.get('id')}", self._grpc_client, - get_edges_from_ids(parent_design, res["edges"]), + get_edges_from_ids(parent_design, res.get("edges")), ) - for res in problem_areas_response["problems"] + for res in response.get("problems") ] - @protect_grpc def find_short_edges( self, bodies: list["Body"], length: Real = 0.0 ) -> list[ShortEdgeProblemAreas]: @@ -202,21 +196,20 @@ def find_short_edges( body_ids = [body.id for body in bodies] - problem_areas_response = self._grpc_client.services.repair_tools.find_short_edges( + response = self._grpc_client.services.repair_tools.find_short_edges( selection=body_ids, length=length ) parent_design = get_design_from_body(bodies[0]) return [ ShortEdgeProblemAreas( - f"{res['id']}", + f"{res.get('id')}", self._grpc_client, - get_edges_from_ids(parent_design, res["edges"]), + get_edges_from_ids(parent_design, res.get("edges")), ) - for res in problem_areas_response["problems"] + for res in response.get("problems") ] - @protect_grpc def find_duplicate_faces(self, bodies: list["Body"]) -> list[DuplicateFaceProblemAreas]: """Find the duplicate face problem areas. @@ -237,22 +230,24 @@ def find_duplicate_faces(self, bodies: list["Body"]) -> list[DuplicateFaceProble return [] body_ids = [body.id for body in bodies] - problem_areas_response = self._grpc_client.services.repair_tools.find_duplicate_faces( - faces=body_ids - ) + response = self._grpc_client.services.repair_tools.find_duplicate_faces(faces=body_ids) parent_design = get_design_from_body(bodies[0]) return [ DuplicateFaceProblemAreas( - f"{res['id']}", + f"{res.get('id')}", self._grpc_client, - get_faces_from_ids(parent_design, res["faces"]), + get_faces_from_ids(parent_design, res.get("faces")), ) - for res in problem_areas_response["problems"] + for res in response.get("problems") ] - @protect_grpc - def find_missing_faces(self, bodies: list["Body"]) -> list[MissingFaceProblemAreas]: + def find_missing_faces( + self, + bodies: list["Body"], + angle: Angle | pint.Quantity | Real | None = None, + distance: Distance | pint.Quantity | Real | None = None, + ) -> list[MissingFaceProblemAreas]: """Find the missing faces. This method find the missing face problem areas and returns a list of missing @@ -262,6 +257,10 @@ def find_missing_faces(self, bodies: list["Body"]) -> list[MissingFaceProblemAre ---------- bodies : list[Body] List of bodies that missing faces are investigated on. + angle : Angle | ~pint.Quantity | Real, optional + The minimum angle between faces. By default, None. + distance : Distance | ~pint.Quantity | Real, optional + The minimum distance between faces. By default, None. Returns ------- @@ -270,22 +269,30 @@ def find_missing_faces(self, bodies: list["Body"]) -> list[MissingFaceProblemAre """ if not bodies: return [] + + # Perform sanity check + if angle is not None: + angle = angle if isinstance(angle, Angle) else Angle(angle) + if distance is not None: + distance = distance if isinstance(distance, Distance) else Distance(distance) + body_ids = [body.id for body in bodies] - problem_areas_response = self._grpc_client.services.repair_tools.find_missing_faces( - faces=body_ids + response = self._grpc_client.services.repair_tools.find_missing_faces( + faces=body_ids, + angle=angle, + distance=distance, ) parent_design = get_design_from_body(bodies[0]) return [ MissingFaceProblemAreas( - f"{res['id']}", + f"{res.get('id')}", self._grpc_client, - get_edges_from_ids(parent_design, res["edges"]), + get_edges_from_ids(parent_design, res.get("edges")), ) - for res in problem_areas_response["problems"] + for res in response.get("problems") ] - @protect_grpc def find_small_faces(self, bodies: list["Body"]) -> list[SmallFaceProblemAreas]: """Find the small face problem areas. @@ -306,22 +313,23 @@ def find_small_faces(self, bodies: list["Body"]) -> list[SmallFaceProblemAreas]: return [] body_ids = [body.id for body in bodies] - problem_areas_response = self._grpc_client.services.repair_tools.find_small_faces( - selection=body_ids - ) + response = self._grpc_client.services.repair_tools.find_small_faces(selection=body_ids) parent_design = get_design_from_body(bodies[0]) return [ SmallFaceProblemAreas( - f"{res['id']}", + f"{res.get('id')}", self._grpc_client, - get_faces_from_ids(parent_design, res["faces"]), + get_faces_from_ids(parent_design, res.get("faces")), ) - for res in problem_areas_response["problems"] + for res in response.get("problems") ] - @protect_grpc - def find_stitch_faces(self, bodies: list["Body"]) -> list[StitchFaceProblemAreas]: + def find_stitch_faces( + self, + bodies: list["Body"], + max_distance: Distance | pint.Quantity | Real | None = None, + ) -> list[StitchFaceProblemAreas]: """Return the list of stitch face problem areas. This method find the stitch face problem areas and returns a list of ids of stitch face @@ -331,27 +339,38 @@ def find_stitch_faces(self, bodies: list["Body"]) -> list[StitchFaceProblemAreas ---------- bodies : list[Body] List of bodies that stitchable faces are investigated on. + max_distance : Distance | ~pint.Quantity | Real, optional + Maximum distance between faces. By default, None. Returns ------- list[StitchFaceProblemAreas] List of objects representing stitch face problem areas. """ + from ansys.geometry.core.designer.body import Body + + # Perform sanity check + check_type_all_elements_in_iterable(bodies, Body) + if max_distance is not None: + max_distance = ( + max_distance if isinstance(max_distance, Distance) else Distance(max_distance) + ) + body_ids = [body.id for body in bodies] - problem_areas_response = self._grpc_client.services.repair_tools.find_stitch_faces( - faces=body_ids + response = self._grpc_client.services.repair_tools.find_stitch_faces( + faces=body_ids, + distance=max_distance, ) parent_design = get_design_from_body(bodies[0]) return [ StitchFaceProblemAreas( - f"{res['id']}", + f"{res.get('id')}", self._grpc_client, - get_bodies_from_ids(parent_design, res["bodies"]), + get_bodies_from_ids(parent_design, res.get("bodies")), ) - for res in problem_areas_response["problems"] + for res in response.get("problems") ] - @protect_grpc @min_backend_version(25, 2, 0) def find_simplify(self, bodies: list["Body"]) -> list[UnsimplifiedFaceProblemAreas]: """Detect faces in a body that can be simplified. @@ -372,20 +391,17 @@ def find_simplify(self, bodies: list["Body"]) -> list[UnsimplifiedFaceProblemAre body_ids = [body.id for body in bodies] parent_design = get_design_from_body(bodies[0]) - problem_areas_response = self._grpc_client.services.repair_tools.find_simplify( - selection=body_ids - ) + response = self._grpc_client.services.repair_tools.find_simplify(selection=body_ids) return [ UnsimplifiedFaceProblemAreas( - f"{res['id']}", + f"{res.get('id')}", self._grpc_client, - get_faces_from_ids(parent_design, res["bodies"]), + get_faces_from_ids(parent_design, res.get("bodies")), ) - for res in problem_areas_response["problems"] + for res in response.get("problems") ] - @protect_grpc @min_backend_version(25, 2, 0) def find_interferences( self, bodies: list["Body"], cut_smaller_body: bool = False @@ -421,21 +437,19 @@ def find_interferences( parent_design = get_design_from_body(bodies[0]) body_ids = [body.id for body in bodies] - # cut_smaller_body_bool = BoolValue(value=cut_smaller_body) - problem_areas_response = self._grpc_client.services.repair_tools.find_interferences( + response = self._grpc_client.services.repair_tools.find_interferences( bodies=body_ids, cut_smaller_body=cut_smaller_body ) return [ InterferenceProblemAreas( - f"{res['id']}", + f"{res.get('id')}", self._grpc_client, - get_bodies_from_ids(parent_design, res["bodies"]), + get_bodies_from_ids(parent_design, res.get("bodies")), ) - for res in problem_areas_response["problems"] + for res in response.get("problems") ] - @protect_grpc @min_backend_version(25, 2, 0) def find_and_fix_short_edges( self, bodies: list["Body"], length: Real = 0.0, comprehensive_result: bool = False @@ -478,18 +492,13 @@ def find_and_fix_short_edges( comprehensive_result=comprehensive_result, ) + # Update existing design parent_design = get_design_from_body(bodies[0]) parent_design._update_design_inplace() - message = RepairToolMessage( - success=response["success"], - found=response["found"], - repaired=response["repaired"], - created_bodies=[], - modified_bodies=[], - ) - return message - @protect_grpc + # Build the response message + return self.__build_repair_tool_message(response) + @min_backend_version(25, 2, 0) def find_and_fix_extra_edges( self, bodies: list["Body"], comprehensive_result: bool = False @@ -529,18 +538,13 @@ def find_and_fix_extra_edges( comprehensive_result=comprehensive_result, ) + # Update existing design parent_design = get_design_from_body(bodies[0]) parent_design._update_design_inplace() - message = RepairToolMessage( - response["success"], - response["created_bodies_monikers"], - response["modified_bodies_monikers"], - response["found"], - response["repaired"], - ) - return message - @protect_grpc + # Build the response message + return self.__build_repair_tool_message(response) + @min_backend_version(25, 2, 0) def find_and_fix_split_edges( self, @@ -591,18 +595,13 @@ def find_and_fix_split_edges( comprehensive_result=comprehensive_result, ) + # Update existing design parent_design = get_design_from_body(bodies[0]) parent_design._update_design_inplace() - message = RepairToolMessage( - response["success"], - response["created_bodies_monikers"], - response["modified_bodies_monikers"], - response["found"], - response["repaired"], - ) - return message - @protect_grpc + # Build the response message + return self.__build_repair_tool_message(response) + @min_backend_version(25, 2, 0) def find_and_fix_simplify( self, bodies: list["Body"], comprehensive_result: bool = False @@ -641,16 +640,80 @@ def find_and_fix_simplify( comprehensive_result=comprehensive_result, ) + # Update existing design parent_design = get_design_from_body(bodies[0]) parent_design._update_design_inplace() - message = RepairToolMessage( - response["success"], - response["created_bodies_monikers"], - response["modified_bodies_monikers"], - response["found"], - response["repaired"], + + # Build the response message + return self.__build_repair_tool_message(response) + + @min_backend_version(25, 2, 0) + def find_and_fix_stitch_faces( + self, + bodies: list["Body"], + max_distance: Distance | pint.Quantity | Real | None = None, + allow_multiple_bodies: bool = False, + maintain_components: bool = True, + check_for_coincidence: bool = False, + comprehensive_result: bool = False, + ) -> RepairToolMessage: + """Find and fix the stitch face problem areas. + + Parameters + ---------- + bodies : list[Body] + List of bodies that stitchable faces are investigated on. + max_distance : Distance | ~pint.Quantity | Real, optional + The maximum distance between faces to be stitched. + By default, 0.0001. + allow_multiple_bodies : bool, optional + Whether to allow multiple bodies in the result. + By default, False. + maintain_components : bool, optional + Whether to stitch bodies within the components. + By default, True. + check_for_coincidence : bool, optional + Whether coincidence surfaces are searched. + By default, False. + comprehensive_result : bool, optional + Whether to fix all problem areas individually. + By default, False. + + Returns + ------- + RepairToolMessage + Message containing number of problem areas found/fixed, created and/or modified bodies. + + Notes + ----- + This method finds the stitchable faces and fixes them. + """ + from ansys.geometry.core.designer.body import Body + + # Perform sanity check + check_type_all_elements_in_iterable(bodies, Body) + if max_distance is not None: + max_distance = ( + max_distance if isinstance(max_distance, Distance) else Distance(max_distance) + ) + + body_ids = [body.id for body in bodies] + + response = self._grpc_client.services.repair_tools.find_and_fix_stitch_faces( + body_ids=body_ids, + max_distance=max_distance, + allow_multiple_bodies=allow_multiple_bodies, + maintain_components=maintain_components, + check_for_coincidence=check_for_coincidence, + comprehensive_result=comprehensive_result, ) - return message + + # Update existing design + parent_design = get_design_from_body(bodies[0]) + parent_design._update_design_inplace() + + # Build the response message + return self.__build_repair_tool_message(response) def inspect_geometry(self, bodies: list["Body"] = None) -> list[InspectResult]: """Return a list of geometry issues organized by body. @@ -675,7 +738,7 @@ def inspect_geometry(self, bodies: list["Body"] = None) -> list[InspectResult]: parent_design=parent_design, bodies=body_ids ) return self.__create_inspect_result_from_response( - parent_design, inspect_result_response_dict["issues_by_body"] + parent_design, inspect_result_response_dict.get("issues_by_body") ) def __create_inspect_result_from_response( @@ -684,7 +747,7 @@ def __create_inspect_result_from_response( 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"]) + issues = self.__create_issues_from_response(inspect_geometry_result.get("issues")) inspect_result = InspectResult( grpc_client=self._grpc_client, body=body[0], issues=issues ) @@ -698,9 +761,9 @@ def __create_issues_from_response( ) -> list[GeometryIssue]: issues = [] for issue in inspect_geometry_result_issues: - message_type = issue["message_type"] - message_id = issue["message_id"] - message = issue["message"] + message_type = issue.get("message_type") + message_id = issue.get("message_id") + message = issue.get("message") faces = [face["id"] for face in issue.get("faces", [])] edges = [edge["id"] for edge in issue.get("edges", [])] @@ -715,7 +778,6 @@ def __create_issues_from_response( issues.append(geometry_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. @@ -738,5 +800,26 @@ def repair_geometry(self, bodies: list["Body"] = None) -> RepairToolMessage: bodies=body_ids ) - message = RepairToolMessage(repair_result_response["success"], [], []) - return message + return self.__build_repair_tool_message(repair_result_response) + + def __build_repair_tool_message(self, response: dict) -> RepairToolMessage: + """Build a repair tool message from the service response. + + Parameters + ---------- + response : dict + The response from the service containing information about the repair operation. + + Returns + ------- + RepairToolMessage + A message containing the success status, created bodies, modified bodies, + number of found problem areas, and number of repaired problem areas. + """ + return RepairToolMessage( + success=response.get("success"), + created_bodies=response.get("created_bodies_monikers", []), + modified_bodies=response.get("modified_bodies_monikers", []), + found=response.get("found", -1), + repaired=response.get("repaired", -1), + ) diff --git a/tests/integration/files/stitch_1200_bodies.dsco b/tests/integration/files/stitch_1200_bodies.dsco new file mode 100644 index 0000000000..08b757fed9 Binary files /dev/null and b/tests/integration/files/stitch_1200_bodies.dsco differ diff --git a/tests/integration/test_repair_tools.py b/tests/integration/test_repair_tools.py index 4317691149..2895908555 100644 --- a/tests/integration/test_repair_tools.py +++ b/tests/integration/test_repair_tools.py @@ -285,6 +285,32 @@ def test_fix_interference(modeler: Modeler): assert result.success is True +def test_find_and_fix_stitch_faces(modeler: Modeler): + """Test to find and fix stitch faces and validate that we get a solid.""" + design = modeler.open_file(FILES_DIR / "stitch_1200_bodies.dsco") + assert len(design.bodies) == 3600 + + stitch_faces = modeler.repair_tools.find_and_fix_stitch_faces(design.bodies) + assert stitch_faces.found == 1 + assert stitch_faces.repaired == 1 + + assert len(design.bodies) == 1200 + + +def test_find_and_fix_stitch_faces_comprehensive(modeler: Modeler): + """Test to find and fix stitch faces and validate that we get a solid.""" + design = modeler.open_file(FILES_DIR / "stitch_1200_bodies.dsco") + assert len(design.bodies) == 3600 + + stitch_faces = modeler.repair_tools.find_and_fix_stitch_faces( + design.bodies, comprehensive_result=True + ) + assert stitch_faces.found == 1200 + assert stitch_faces.repaired == 1200 + + assert len(design.bodies) == 1200 + + def test_find_and_fix_duplicate_faces(modeler: Modeler): """Test to read geometry, find and fix duplicate faces and validate they are removed.""" design = modeler.open_file(FILES_DIR / "DuplicateFaces.scdocx")