diff --git a/doc/changelog.d/2232.added.md b/doc/changelog.d/2232.added.md new file mode 100644 index 0000000000..67e9af1df9 --- /dev/null +++ b/doc/changelog.d/2232.added.md @@ -0,0 +1 @@ +Helix detection diff --git a/src/ansys/geometry/core/_grpc/_services/base/prepare_tools.py b/src/ansys/geometry/core/_grpc/_services/base/prepare_tools.py index a3b9f03136..04d31db962 100644 --- a/src/ansys/geometry/core/_grpc/_services/base/prepare_tools.py +++ b/src/ansys/geometry/core/_grpc/_services/base/prepare_tools.py @@ -78,3 +78,8 @@ def find_and_remove_logos(self, **kwargs) -> dict: def remove_logo(self, **kwargs) -> dict: """Remove logos in geometry.""" pass + + @abstractmethod + def detect_helixes(self, **kwargs) -> dict: + """Detect helixes in geometry.""" + pass diff --git a/src/ansys/geometry/core/_grpc/_services/v0/prepare_tools.py b/src/ansys/geometry/core/_grpc/_services/v0/prepare_tools.py index 0259744cbd..e2b4592a45 100644 --- a/src/ansys/geometry/core/_grpc/_services/v0/prepare_tools.py +++ b/src/ansys/geometry/core/_grpc/_services/v0/prepare_tools.py @@ -214,3 +214,64 @@ def remove_logo(self, **kwargs): # noqa: D102 # Return the response - formatted as a dictionary return {"success": response.success} + + @protect_grpc + def detect_helixes(self, **kwargs) -> dict: # noqa: D102 + from ansys.api.dbu.v0.dbumodels_pb2 import EntityIdentifier + from ansys.api.geometry.v0.models_pb2 import DetectHelixesOptions + from ansys.api.geometry.v0.preparetools_pb2 import DetectHelixesRequest + + from ansys.geometry.core.shapes.parameterization import Interval + + from ..base.conversions import ( + from_measurement_to_server_length, + to_distance, + ) + from .conversions import ( + from_grpc_curve_to_curve, + from_grpc_point_to_point3d, + ) + + # Create the request - assumes all inputs are valid and of the proper type + request = DetectHelixesRequest( + body_ids=[EntityIdentifier(id=body.id) for body in kwargs["bodies"]], + options=DetectHelixesOptions( + min_radius=from_measurement_to_server_length(kwargs["min_radius"]), + max_radius=from_measurement_to_server_length(kwargs["max_radius"]), + fit_radius_error=from_measurement_to_server_length(kwargs["fit_radius_error"]), + ), + ) + + # Call the gRPC service + response = self.stub.DetectHelixes(request) + + # If no helixes, return empty dictionary + if len(response.helixes) == 0: + return {"helixes": []} + + # Return the response - formatted as a dictionary + return { + "helixes": [ + { + "trimmed_curve": { + "geometry": from_grpc_curve_to_curve(helix.trimmed_curve.curve), + "start": from_grpc_point_to_point3d(helix.trimmed_curve.start), + "end": from_grpc_point_to_point3d(helix.trimmed_curve.end), + "interval": Interval( + helix.trimmed_curve.interval_start, helix.trimmed_curve.interval_end + ), + "length": to_distance(helix.trimmed_curve.length).value, + }, + "edges": [ + { + "id": edge.id, + "parent_id": edge.parent.id, + "curve_type": edge.curve_type, + "is_reversed": edge.is_reversed, + } + for edge in helix.edges + ], + } + for helix in response.helixes + ] + } diff --git a/src/ansys/geometry/core/_grpc/_services/v1/prepare_tools.py b/src/ansys/geometry/core/_grpc/_services/v1/prepare_tools.py index dc3beddf42..cadd8c3984 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/prepare_tools.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/prepare_tools.py @@ -78,3 +78,7 @@ def find_and_remove_logos(self, **kwargs) -> dict: # noqa: D102 @protect_grpc def remove_logo(self, **kwargs) -> dict: # noqa: D102 raise NotImplementedError + + @protect_grpc + def detect_helixes(self, **kwargs) -> dict: # noqa: D102 + raise NotImplementedError diff --git a/src/ansys/geometry/core/tools/prepare_tools.py b/src/ansys/geometry/core/tools/prepare_tools.py index 880990da17..60b42c43de 100644 --- a/src/ansys/geometry/core/tools/prepare_tools.py +++ b/src/ansys/geometry/core/tools/prepare_tools.py @@ -24,6 +24,7 @@ from typing import TYPE_CHECKING from beartype import beartype as check_input_types +from pint import Quantity from ansys.geometry.core.connection import GrpcClient from ansys.geometry.core.connection.backend import BackendType @@ -31,10 +32,13 @@ from ansys.geometry.core.logger import LOG from ansys.geometry.core.misc.auxiliary import ( get_bodies_from_ids, + get_design_from_body, get_design_from_edge, get_design_from_face, ) from ansys.geometry.core.misc.checks import check_type_all_elements_in_iterable, min_backend_version +from ansys.geometry.core.misc.measurements import Distance +from ansys.geometry.core.shapes.curves.trimmed_curve import TrimmedCurve from ansys.geometry.core.tools.problem_areas import LogoProblemArea from ansys.geometry.core.tools.repair_tool_message import RepairToolMessage from ansys.geometry.core.typing import Real @@ -412,3 +416,86 @@ def find_and_remove_logos( ) return response.get("success") + + @min_backend_version(26, 1, 0) + def detect_helixes( + self, + bodies: list["Body"], + min_radius: Distance | Quantity | Real = 0.0, + max_radius: Distance | Quantity | Real = 100.0, + fit_radius_error: Distance | Quantity | Real = 0.01, + ) -> dict["TrimmedCurve", list["Edge"]]: + """Detect helixes in the given bodies. + + Parameters + ---------- + bodies : list[Body] + List of bodies to detect helixes in. + min_radius : Distance, Quantity, or Real, default: 0.0 + Minimum radius of the helix to be detected. + max_radius : Distance, Quantity, or Real, default: 1e6 + Maximum radius of the helix to be detected. + fit_radius_error : Distance, Quantity, or Real, default: 0.01 + Maximum fit radius error of the helix to be detected. + + Returns + ------- + dict + Dictionary with key "helixes" containing a list of detected helixes. + Each helix is represented as a dictionary with keys "trimmed_curve" and "edges". + + Warnings + -------- + This method is only available starting on Ansys release 26R1. + """ + from ansys.geometry.core.designer.body import Body + from ansys.geometry.core.designer.edge import CurveType, Edge + + if not bodies: + self._grpc_client.log.info("No bodies provided...") + return {"helixes": []} + + # Verify inputs + check_type_all_elements_in_iterable(bodies, Body) + min_radius = min_radius if isinstance(min_radius, Distance) else Distance(min_radius) + max_radius = max_radius if isinstance(max_radius, Distance) else Distance(max_radius) + fit_radius_error = ( + fit_radius_error + if isinstance(fit_radius_error, Distance) + else Distance(fit_radius_error) + ) + + response = self._grpc_client._services.prepare_tools.detect_helixes( + bodies=bodies, + min_radius=min_radius, + max_radius=max_radius, + fit_radius_error=fit_radius_error, + ) + + parent_design = get_design_from_body(bodies[0]) + + return { + "helixes": [ + { + "trimmed_curve": TrimmedCurve( + helix.get("trimmed_curve").get("geometry"), + helix.get("trimmed_curve").get("start"), + helix.get("trimmed_curve").get("end"), + helix.get("trimmed_curve").get("interval"), + helix.get("trimmed_curve").get("length"), + grpc_client=self._grpc_client, + ), + "edges": [ + Edge( + edge.get("id"), + CurveType(edge.get("curve_type")), + get_bodies_from_ids(parent_design, [edge.get("parent_id")])[0], + self._grpc_client, + edge.get("is_reversed"), + ) + for edge in helix.get("edges") + ], + } + for helix in response.get("helixes") + ] + } diff --git a/tests/_incompatible_tests.yml b/tests/_incompatible_tests.yml index 4c590a6ff6..0fc2becb98 100644 --- a/tests/_incompatible_tests.yml +++ b/tests/_incompatible_tests.yml @@ -28,6 +28,7 @@ backends: - tests/integration/test_prepare_tools.py::test_volume_extract_bad_faces - tests/integration/test_prepare_tools.py::test_volume_extract_bad_edges - tests/integration/test_prepare_tools.py::test_volume_extract_bad_edges + - tests/integration/test_prepare_tools.py::test_helix_detection - tests/integration/test_repair_tools.py::test_fix_small_face - tests/integration/test_repair_tools.py::test_find_interference - tests/integration/test_repair_tools.py::test_fix_interference @@ -137,6 +138,7 @@ backends: - tests/integration/test_prepare_tools.py::test_volume_extract_bad_faces - tests/integration/test_prepare_tools.py::test_volume_extract_bad_edges - tests/integration/test_prepare_tools.py::test_volume_extract_bad_edges + - tests/integration/test_prepare_tools.py::test_helix_detection - tests/integration/test_repair_tools.py::test_fix_small_face - tests/integration/test_repair_tools.py::test_find_interference - tests/integration/test_repair_tools.py::test_fix_interference @@ -222,6 +224,7 @@ backends: # Model used is too new for this version - tests/integration/test_design.py::test_import_component_named_selections - tests/integration/test_design.py::test_component_make_independent + - tests/integration/test_prepare_tools.py::test_helix_detection - tests/integration/test_spaceclaim_tutorial_examples.py::test_combine_example - tests/integration/test_spaceclaim_tutorial_examples.py::test_pull_example - tests/integration/test_spaceclaim_tutorial_examples.py::test_intersect_example diff --git a/tests/integration/files/bolt.scdocx b/tests/integration/files/bolt.scdocx new file mode 100644 index 0000000000..a309582898 Binary files /dev/null and b/tests/integration/files/bolt.scdocx differ diff --git a/tests/integration/test_prepare_tools.py b/tests/integration/test_prepare_tools.py index aeb2de562e..d7aebe6de6 100644 --- a/tests/integration/test_prepare_tools.py +++ b/tests/integration/test_prepare_tools.py @@ -207,3 +207,30 @@ def test_volume_extract_bad_edges(modeler: Modeler): sealing_edges, ) assert len(created_bodies) == 0 + + +def test_helix_detection(modeler: Modeler): + """Test helix detection.""" + design = modeler.open_file(FILES_DIR / "bolt.scdocx") + + bodies = design.bodies + assert len(bodies) == 2 + + search_bodies = [bodies[0]] + assert len(search_bodies) == 1 + + # Test default parameters + result = modeler.prepare_tools.detect_helixes(search_bodies) + assert len(result["helixes"]) == 1 + + # Test with non-default parameters + result = modeler.prepare_tools.detect_helixes(search_bodies, 0, 10, 100) + assert len(result["helixes"]) == 1 + + # Test parameters that should yield no results + result = modeler.prepare_tools.detect_helixes(search_bodies, 5.0, 10.0, 0.01) + assert len(result["helixes"]) == 0 + + # Test with multiple bodies + result = modeler.prepare_tools.detect_helixes(bodies) + assert len(result["helixes"]) == 2