diff --git a/doc/changelog.d/2169.added.md b/doc/changelog.d/2169.added.md new file mode 100644 index 0000000000..a6eaa99541 --- /dev/null +++ b/doc/changelog.d/2169.added.md @@ -0,0 +1 @@ +Option to write body facets on save diff --git a/src/ansys/geometry/core/_grpc/_services/v0/conversions.py b/src/ansys/geometry/core/_grpc/_services/v0/conversions.py index b211720bda..f7dc96063e 100644 --- a/src/ansys/geometry/core/_grpc/_services/v0/conversions.py +++ b/src/ansys/geometry/core/_grpc/_services/v0/conversions.py @@ -1161,3 +1161,23 @@ def _nurbs_curves_compatibility(backend_version: "semver.Version", grpc_geometri + "26.1.0, but the current version used is " + f"{backend_version}." ) + + +def _check_write_body_facets_input(backend_version: "semver.Version", write_body_facets: bool): + """Check if the backend version is compatible with NURBS curves in sketches. + + Parameters + ---------- + backend_version : semver.Version + The version of the backend. + write_body_facets : bool + Option to write out body facets. + """ + if write_body_facets and backend_version < (26, 1, 0): + from ansys.geometry.core.logger import LOG + + LOG.warning( + "The usage of write_body_facets requires a minimum Ansys release version of " + + "26.1.0, but the current version used is " + + f"{backend_version}." + ) diff --git a/src/ansys/geometry/core/_grpc/_services/v0/designs.py b/src/ansys/geometry/core/_grpc/_services/v0/designs.py index 6fec2e0110..b74961cfc7 100644 --- a/src/ansys/geometry/core/_grpc/_services/v0/designs.py +++ b/src/ansys/geometry/core/_grpc/_services/v0/designs.py @@ -26,7 +26,11 @@ from ansys.geometry.core.errors import protect_grpc from ..base.designs import GRPCDesignsService -from .conversions import build_grpc_id, from_design_file_format_to_grpc_part_export_format +from .conversions import ( + _check_write_body_facets_input, + build_grpc_id, + from_design_file_format_to_grpc_part_export_format, +) class GRPCDesignsServiceV0(GRPCDesignsService): # pragma: no cover @@ -106,8 +110,12 @@ def put_active(self, **kwargs) -> dict: # noqa: D102 def save_as(self, **kwargs) -> dict: # noqa: D102 from ansys.api.dbu.v0.designs_pb2 import SaveAsRequest + _check_write_body_facets_input(kwargs["backend_version"], kwargs["write_body_facets"]) + # Create the request - assumes all inputs are valid and of the proper type - request = SaveAsRequest(filepath=kwargs["filepath"]) + request = SaveAsRequest( + filepath=kwargs["filepath"], write_body_facets=kwargs["write_body_facets"] + ) # Call the gRPC service _ = self.stub.SaveAs(request) @@ -119,9 +127,12 @@ def save_as(self, **kwargs) -> dict: # noqa: D102 def download_export(self, **kwargs) -> dict: # noqa: D102 from ansys.api.dbu.v0.designs_pb2 import DownloadExportFileRequest + _check_write_body_facets_input(kwargs["backend_version"], kwargs["write_body_facets"]) + # Create the request - assumes all inputs are valid and of the proper type request = DownloadExportFileRequest( - format=from_design_file_format_to_grpc_part_export_format(kwargs["format"]) + format=from_design_file_format_to_grpc_part_export_format(kwargs["format"]), + write_body_facets=kwargs["write_body_facets"], ) # Call the gRPC service @@ -136,9 +147,12 @@ def download_export(self, **kwargs) -> dict: # noqa: D102 def stream_download_export(self, **kwargs) -> dict: # noqa: D102 from ansys.api.dbu.v0.designs_pb2 import DownloadExportFileRequest + _check_write_body_facets_input(kwargs["backend_version"], kwargs["write_body_facets"]) + # Create the request - assumes all inputs are valid and of the proper type request = DownloadExportFileRequest( - format=from_design_file_format_to_grpc_part_export_format(kwargs["format"]) + format=from_design_file_format_to_grpc_part_export_format(kwargs["format"]), + write_body_facets=kwargs["write_body_facets"], ) # Call the gRPC service diff --git a/src/ansys/geometry/core/designer/design.py b/src/ansys/geometry/core/designer/design.py index 74f6f59f77..8406ad4648 100644 --- a/src/ansys/geometry/core/designer/design.py +++ b/src/ansys/geometry/core/designer/design.py @@ -229,19 +229,25 @@ def add_material(self, material: Material) -> None: @check_input_types @ensure_design_is_active - def save(self, file_location: Path | str) -> None: + def save(self, file_location: Path | str, write_body_facets: bool = False) -> None: """Save a design to disk on the active Geometry server instance. Parameters ---------- file_location : ~pathlib.Path | str Location on disk to save the file to. + write_body_facets : bool, default: False + Option to write body facets into the saved file. 26R1 and later. """ # Sanity checks on inputs if isinstance(file_location, Path): file_location = str(file_location) - self._grpc_client.services.designs.save_as(filepath=file_location) + self._grpc_client.services.designs.save_as( + filepath=file_location, + write_body_facets=write_body_facets, + backend_version=self._grpc_client.backend_version, + ) self._grpc_client.log.debug(f"Design successfully saved at location {file_location}.") @protect_grpc @@ -251,6 +257,7 @@ def download( self, file_location: Path | str, format: DesignFileFormat = DesignFileFormat.SCDOCX, + write_body_facets: bool = False, ) -> None: """Export and download the design from the server. @@ -260,6 +267,8 @@ def download( Location on disk to save the file to. format : DesignFileFormat, default: DesignFileFormat.SCDOCX Format for the file to save to. + write_body_facets : bool, default: False + Option to write body facets into the saved file. SCDOCX only, 26R1 and later. """ # Sanity checks on inputs if isinstance(file_location, str): @@ -275,7 +284,9 @@ def download( if self._modeler.client.backend_version < (25, 2, 0): received_bytes = self.__export_and_download_legacy(format=format) else: - received_bytes = self.__export_and_download(format=format) + received_bytes = self.__export_and_download( + format=format, write_body_facets=write_body_facets + ) # Write to file file_location.write_bytes(received_bytes) @@ -323,7 +334,11 @@ def __export_and_download_legacy(self, format: DesignFileFormat) -> bytes: return received_bytes - def __export_and_download(self, format: DesignFileFormat) -> bytes: + def __export_and_download( + self, + format: DesignFileFormat, + write_body_facets: bool = False, + ) -> bytes: """Export and download the design from the server. Parameters @@ -351,14 +366,22 @@ def __export_and_download(self, format: DesignFileFormat) -> bytes: DesignFileFormat.STRIDE, ]: try: - response = self._grpc_client.services.designs.download_export(format=format) + response = self._grpc_client.services.designs.download_export( + format=format, + write_body_facets=write_body_facets, + backend_version=self._grpc_client.backend_version, + ) except Exception: self._grpc_client.log.warning( f"Failed to download the file in {format} format." " Attempting to stream download." ) # Attempt to download the file via streaming - response = self._grpc_client.services.designs.stream_download_export(format=format) + response = self._grpc_client.services.designs.stream_download_export( + format=format, + write_body_facets=write_body_facets, + backend_version=self._grpc_client.backend_version, + ) else: self._grpc_client.log.warning( f"{format} format requested is not supported. Ignoring download request." diff --git a/tests/_incompatible_tests.yml b/tests/_incompatible_tests.yml index 79ee74d704..c073518bcd 100644 --- a/tests/_incompatible_tests.yml +++ b/tests/_incompatible_tests.yml @@ -102,6 +102,8 @@ backends: # Bug fix included from 26.1 onwards - tests/integration/test_design_import.py::test_named_selections_after_file_insert - tests/integration/test_design_import.py::test_named_selections_after_file_open + # Export body facets add in 26.1 + - tests/integration/test_design.py::test_write_body_facets_on_save - version: "24.2" incompatible_tests: @@ -198,6 +200,8 @@ backends: # Bug fix included from 26.1 onwards - tests/integration/test_design_import.py::test_named_selections_after_file_insert - tests/integration/test_design_import.py::test_named_selections_after_file_open + # Export body facets add in 26.1 + - tests/integration/test_design.py::test_write_body_facets_on_save - version: "25.1" incompatible_tests: @@ -263,6 +267,8 @@ backends: # Bug fix included from 26.1 onwards - tests/integration/test_design_import.py::test_named_selections_after_file_insert - tests/integration/test_design_import.py::test_named_selections_after_file_open + # Export body facets add in 26.1 + - tests/integration/test_design.py::test_write_body_facets_on_save - version: "25.2" incompatible_tests: @@ -292,3 +298,5 @@ backends: # Bug fix included from 26.1 onwards - tests/integration/test_design_import.py::test_named_selections_after_file_insert - tests/integration/test_design_import.py::test_named_selections_after_file_open + # Export body facets add in 26.1 + - tests/integration/test_design.py::test_write_body_facets_on_save diff --git a/tests/integration/test_design.py b/tests/integration/test_design.py index c05e53ecf0..17c2a5c3ba 100644 --- a/tests/integration/test_design.py +++ b/tests/integration/test_design.py @@ -23,6 +23,7 @@ import os from pathlib import Path +import zipfile import matplotlib.colors as mcolors import numpy as np @@ -3579,3 +3580,33 @@ def test_component_make_independent(modeler: Modeler): comp = design.components[0].components[-1].components[-1] # stale from update-design-in-place assert not Accuracy.length_is_equal(comp.bodies[0].volume.m, face.body.volume.m) + + +def test_write_body_facets_on_save(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory): + design = modeler.open_file(Path(FILES_DIR, "cars.scdocx")) + + # First file without body facets + filepath_no_facets = tmp_path_factory.mktemp("test_design") / "cars_no_facets.scdocx" + design.download(filepath_no_facets) + + # Second file with body facets + filepath_with_facets = tmp_path_factory.mktemp("test_design") / "cars_with_facets.scdocx" + design.download(filepath_with_facets, write_body_facets=True) + + # Compare file sizes + size_no_facets = filepath_no_facets.stat().st_size + size_with_facets = filepath_with_facets.stat().st_size + + assert size_with_facets > size_no_facets + + # Ensure facets.bin and renderlist.xml files exist + with zipfile.ZipFile(filepath_with_facets, "r") as zip_ref: + namelist = set(zip_ref.namelist()) + + expected_files = { + "SpaceClaim/Graphics/facets.bin", + "SpaceClaim/Graphics/renderlist.xml", + } + + missing = expected_files - namelist + assert not missing