diff --git a/CHANGELOG.md b/CHANGELOG.md index abf7328..c4124b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Python 3.14 support +- New wrapper and documentation for `AnimationExportOptions` (`src/moldflow/animation_export_options.py` and docs/source/components/wrapper/animation_export_options.rst). +- New wrapper and documentation for `ImageExportOptions` (`src/moldflow/image_export_options.py` and docs/source/components/wrapper/image_export_options.rst). +- Added `CADDiagnostic` wrapper and documentation (`src/moldflow/cad_diagnostic.py`, docs/source/components/wrapper/cad_diagnostic.rst). +- Added and updated unit tests covering animation/image export options, CAD diagnostic, import options, mesh editor, plot manager, study document handling, synergy, and viewer (tests/api/unit_tests/* and tests/core/test_helper.py). + +### Changed +- API improvements and helper additions across mesh editing, plotting, study documents, Synergy integration, and the viewer (`src/moldflow/mesh_editor.py`, `src/moldflow/plot_manager.py`, `src/moldflow/study_doc.py`, `src/moldflow/synergy.py`, `src/moldflow/viewer.py`). +- Added/updated component enum docs and wrapper docs (docs/source/components/enums/*, docs/source/components/wrapper/*) and updated project readme (docs/source/readme.rst). + +### Deprecated +- Deprecated several legacy Viewer functions and MeshGenerator/ImportOptions properties in preparation for Synergy 2027.0.0: + - `Viewer.save_image_legacy` + - `Viewer.save_image` + - `Viewer.save_animation` + - `ImportOptions.mdl_kernel` + - `MeshGenerator.automatic_tetra_optimization` + - `MeshGenerator.element_reduction` + - `MeshGenerator.use_fallbacks` + - `MeshGenerator.use_tetras_on_edge` + - `MeshGenerator.tetra_max_ar` +- Added deprecation warnings and clarified FillHole API redesigns + +### Removed +- N/A + +### Fixed +- N/A + +### Security +- N/A + +## [26.0.5] + +### Added +- N/A ### Changed - `MeshGenerator.cad_mesh_grading_factor` now accepts `float` values in range 0.0 to 1.0 instead of enum/integer-coded options @@ -20,12 +55,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `GradingFactor` enum - incorrectly restricted the API to discrete values when the COM API accepts continuous float values from 0.0 to 1.0 ### Fixed -- Fixed `GeomType` enum not being exposed in package `__init__.py` - users can now import it directly with `from moldflow import GeomType` - Fixed `MeshGenerator.cad_mesh_grading_factor` to properly accept float/double values matching the COM API signature instead of restricting to enum values ### Security - N/A +## [26.0.4] + +### Added +- N/A + +### Changed +- N/A + +### Deprecated +- N/A + +### Removed +- N/A + +### Fixed +- Fixed `GeomType` enum not being exposed in package `__init__.py` - users can now import it directly with `from moldflow import GeomType` +- Fixed invalid `DUAL_DOMAIN` enum value in `GeomType` - replaced with `FUSION = "Fusion"` to match valid Moldflow API values +- Fixed missing `-> bool` return type annotations for `MeshGenerator.generate()` and `MeshGenerator.save_options()` methods + +### Security +- N/A + ## [26.0.3] ### Added @@ -94,7 +150,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial version aligned with Moldflow Synergy 2026.0.1 - Python 3.10-3.13 compatibility -[Unreleased]: https://github.com/Autodesk/moldflow-api/compare/v26.0.3...HEAD +[Unreleased]: https://github.com/Autodesk/moldflow-api/compare/v26.0.5...HEAD +[26.0.5]: https://github.com/Autodesk/moldflow-api/releases/tag/v26.0.5 +[26.0.4]: https://github.com/Autodesk/moldflow-api/releases/tag/v26.0.4 [26.0.3]: https://github.com/Autodesk/moldflow-api/releases/tag/v26.0.3 [26.0.2]: https://github.com/Autodesk/moldflow-api/releases/tag/v26.0.2 [26.0.1]: https://github.com/Autodesk/moldflow-api/releases/tag/v26.0.1 diff --git a/docs/source/_static/switcher.json b/docs/source/_static/switcher.json index 33b7da0..32696ea 100644 --- a/docs/source/_static/switcher.json +++ b/docs/source/_static/switcher.json @@ -1,9 +1,15 @@ [ + { + "version": "v27.0.0", + "name": "v27.0.0 (latest)", + "url": "../v27.0.0/", + "is_latest": true + }, { "version": "v26.0.5", - "name": "v26.0.5 (latest)", + "name": "v26.0.5", "url": "../v26.0.5/", - "is_latest": true + "is_latest": false }, { "version": "v26.0.4", @@ -35,4 +41,4 @@ "url": "../v26.0.0/", "is_latest": false } -] \ No newline at end of file +] diff --git a/docs/source/components/enums/capture_modes.rst b/docs/source/components/enums/capture_modes.rst new file mode 100644 index 0000000..820acb5 --- /dev/null +++ b/docs/source/components/enums/capture_modes.rst @@ -0,0 +1,4 @@ +CaptureModes +============ + +.. autoclass:: moldflow.common::CaptureModes diff --git a/docs/source/components/enums/mdl_kernel.rst b/docs/source/components/enums/mdl_kernel.rst deleted file mode 100644 index 7f71ecf..0000000 --- a/docs/source/components/enums/mdl_kernel.rst +++ /dev/null @@ -1,4 +0,0 @@ -MDLKernel -========= - -.. autoclass:: moldflow.common::MDLKernel diff --git a/docs/source/components/wrapper/animation_export_options.rst b/docs/source/components/wrapper/animation_export_options.rst new file mode 100644 index 0000000..38bfe4d --- /dev/null +++ b/docs/source/components/wrapper/animation_export_options.rst @@ -0,0 +1,4 @@ +AnimationExportOptions +====================== + +.. automodule:: moldflow.animation_export_options diff --git a/docs/source/components/wrapper/cad_diagnostic.rst b/docs/source/components/wrapper/cad_diagnostic.rst new file mode 100644 index 0000000..02e01d1 --- /dev/null +++ b/docs/source/components/wrapper/cad_diagnostic.rst @@ -0,0 +1,4 @@ +CADDiagnostic +============= + +.. automodule:: moldflow.cad_diagnostic diff --git a/docs/source/components/wrapper/image_export_options.rst b/docs/source/components/wrapper/image_export_options.rst new file mode 100644 index 0000000..7ee9998 --- /dev/null +++ b/docs/source/components/wrapper/image_export_options.rst @@ -0,0 +1,4 @@ +ImageExportOptions +================== + +.. automodule:: moldflow.image_export_options diff --git a/docs/source/readme.rst b/docs/source/readme.rst index 12e9465..ad78177 100644 --- a/docs/source/readme.rst +++ b/docs/source/readme.rst @@ -692,7 +692,7 @@ Configure Import Options Before CAD Import .. code-block:: python from moldflow import Synergy - from moldflow.common import MeshType, ImportUnits, MDLKernel + from moldflow.common import MeshType, ImportUnits synergy = Synergy() io = synergy.import_options # ImportOptions @@ -701,7 +701,6 @@ Configure Import Options Before CAD Import io.mesh_type = MeshType.MESH_FUSION io.units = ImportUnits.MM io.use_mdl = True - io.mdl_kernel = MDLKernel.PARASOLID io.mdl_mesh = True io.mdl_surfaces = True io.mdl_auto_edge_select = True diff --git a/src/moldflow/__init__.py b/src/moldflow/__init__.py index b86ab31..3bd310b 100644 --- a/src/moldflow/__init__.py +++ b/src/moldflow/__init__.py @@ -7,8 +7,10 @@ A Python wrapper for the Autodesk Moldflow Synergy API. """ +from .animation_export_options import AnimationExportOptions from .boundary_conditions import BoundaryConditions from .boundary_list import BoundaryList +from .cad_diagnostic import CADDiagnostic from .cad_manager import CADManager from .circuit_generator import CircuitGenerator from .data_transform import DataTransform @@ -16,6 +18,7 @@ from .double_array import DoubleArray from .ent_list import EntList from .folder_manager import FolderManager +from .image_export_options import ImageExportOptions from .import_options import ImportOptions from .integer_array import IntegerArray from .layer_manager import LayerManager @@ -53,6 +56,7 @@ from .common import AnimationSpeed from .common import CADBodyProperty from .common import CADContactMesh +from .common import CaptureModes from .common import ClampForcePlotDirection from .common import ColorScaleOptions from .common import ColorTableIDs @@ -79,7 +83,6 @@ from .common import Mesher3DType from .common import MoldingProcess from .common import MDLContactMeshType -from .common import MDLKernel from .common import MeshType from .common import ModulusPlotDirection from .common import NurbsAlgorithm diff --git a/src/moldflow/animation_export_options.py b/src/moldflow/animation_export_options.py new file mode 100644 index 0000000..b6f5369 --- /dev/null +++ b/src/moldflow/animation_export_options.py @@ -0,0 +1,198 @@ +""" +Usage: + AnimationExportOptions Class API Wrapper +""" + +from .logger import process_log, LogMessage +from .helper import ( + check_type, + check_is_non_negative, + get_enum_value, + check_range, + check_file_extension, +) +from .common import AnimationSpeed, CaptureModes +from .constants import MP4_FILE_EXT, GIF_FILE_EXT, ANIMATION_SPEED_CONVERTER + + +class AnimationExportOptions: + """ + Wrapper for AnimationExportOptions class of Moldflow Synergy. + """ + + def __init__(self, _animation_export_options): + """ + Initialize the AnimationExportOptions with a AnimationExportOptions instance from COM. + + Args: + _animation_export_options: The AnimationExportOptions instance. + """ + process_log(__name__, LogMessage.CLASS_INIT, locals(), name="AnimationExportOptions") + self.animation_export_options = _animation_export_options + + @property + def file_name(self) -> str: + """ + The file name. + + :getter: Get the file name. + :setter: Set the file name. + :type: str + """ + process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="file_name") + return self.animation_export_options.FileName + + @file_name.setter + def file_name(self, value: str) -> None: + """ + Set the file name. + + Args: + value (str): The file name to set. + """ + process_log(__name__, LogMessage.PROPERTY_SET, locals(), name="file_name", value=value) + check_type(value, str) + value = check_file_extension(value, (MP4_FILE_EXT, GIF_FILE_EXT)) + self.animation_export_options.FileName = value + + # Remove the function when legacy support is removed. + def _convert_animation_speed(self, speed: AnimationSpeed | int | str) -> str: + """ + Convert animation speed to string for legacy support. + """ + speed = get_enum_value(speed, AnimationSpeed) + return ANIMATION_SPEED_CONVERTER[speed] + + @property + def animation_speed(self) -> int: + """ + Animation speed (Slow=0, Medium=1, Fast=2). + + :default: Medium(1) + :getter: Get the animation speed. + :setter: Set the animation speed. + :type: int + """ + process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="animation_speed") + return self.animation_export_options.AnimationSpeed + + @animation_speed.setter + def animation_speed(self, value: AnimationSpeed | int) -> None: + """ + The animation speed. + + Args: + value (int): The animation speed to set. + """ + process_log( + __name__, LogMessage.PROPERTY_SET, locals(), name="animation_speed", value=value + ) + if isinstance(value, AnimationSpeed): + value = self._convert_animation_speed(value) + else: + check_type(value, int) + check_range(value, 0, 2, True, True) + self.animation_export_options.AnimationSpeed = value + + @property + def show_prompts(self) -> bool: + """ + Whether to show prompts during the export process. + + :default: True + :getter: Get show_prompts. + :setter: Set show_prompts. + :type: bool + """ + process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="show_prompts") + return self.animation_export_options.ShowPrompts + + @show_prompts.setter + def show_prompts(self, value: bool) -> None: + """ + Set whether to show prompts during the export process. + + Args: + value (bool): Show prompts or not. + """ + process_log(__name__, LogMessage.PROPERTY_SET, locals(), name="show_prompts", value=value) + check_type(value, bool) + self.animation_export_options.ShowPrompts = value + + @property + def size_x(self) -> int: + """ + The X size (width) of the image. + + :default: 800 + :getter: Get the X size. + :setter: Set the X size. + :type: int (positive) + """ + process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="size_x") + return self.animation_export_options.SizeX + + @size_x.setter + def size_x(self, value: int) -> None: + """ + Set the X size (width) of the image. + + Args: + value (int): The X size to set (must be positive). + """ + process_log(__name__, LogMessage.PROPERTY_SET, locals(), name="size_x", value=value) + check_type(value, int) + check_is_non_negative(value) + self.animation_export_options.SizeX = value + + @property + def size_y(self) -> int: + """ + The Y size (height) of the image. + + :default: 600 + :getter: Get the Y size. + :setter: Set the Y size. + :type: int (positive) + """ + process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="size_y") + return self.animation_export_options.SizeY + + @size_y.setter + def size_y(self, value: int) -> None: + """ + Set the Y size (height) of the image. + + Args: + value (int): The Y size to set (must be positive). + """ + process_log(__name__, LogMessage.PROPERTY_SET, locals(), name="size_y", value=value) + check_type(value, int) + check_is_non_negative(value) + self.animation_export_options.SizeY = value + + @property + def capture_mode(self) -> int: + """ + The capture mode. + + :default: Active View(0) + :getter: Get the capture mode. + :setter: Set the capture mode. + :type: int + """ + process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="capture_mode") + return self.animation_export_options.CaptureMode + + @capture_mode.setter + def capture_mode(self, value: CaptureModes | int) -> None: + """ + Set the capture mode. + + Args: + value (CaptureModes | int): Capture mode to set. + """ + process_log(__name__, LogMessage.PROPERTY_SET, locals(), name="capture_mode", value=value) + value = get_enum_value(value, CaptureModes) + check_range(value, 0, 2, True, True) + self.animation_export_options.CaptureMode = value diff --git a/src/moldflow/cad_diagnostic.py b/src/moldflow/cad_diagnostic.py new file mode 100644 index 0000000..c8e6435 --- /dev/null +++ b/src/moldflow/cad_diagnostic.py @@ -0,0 +1,245 @@ +""" +Usage: + CADDiagnostic Class API Wrapper +""" + +from .logger import process_log, LogMessage +from .double_array import DoubleArray +from .ent_list import EntList +from .integer_array import IntegerArray +from .helper import check_type, coerce_optional_dispatch + + +class CADDiagnostic: + """ + Wrapper for CADDiagnostic class of Moldflow Synergy. + """ + + def __init__(self, _cad_diagnostic): + """ + Initialize the CADDiagnostic with a CADDiagnostic instance from COM. + + Args: + _cad_diagnostic: The CADDiagnostic instance. + """ + process_log(__name__, LogMessage.CLASS_INIT, locals(), name="CADDiagnostic") + self.cad_diagnostic = _cad_diagnostic + + def create_entity_list(self) -> EntList: + """ + Creates an empty EntList object + """ + result = self.cad_diagnostic.CreateEntityList + if result is None: + return None + return EntList(result) + + def compute(self, bodies: EntList | None) -> bool: + """ + CAD quality assessment to identify any potential geometric issues in the CAD model. + This function will identify potential geometric difficulties that may include : + - edge-to-edge intersection + - face-to-face intersection + - edge self intersection + - face self intersection + - non-manifold bodies + - non manifold edges + - toxic bodies + - sliver faces + + Args: + bodies (EntList): The bodies to compute CAD diagnostics for. + + Returns: + True if operation is successful, False otherwise. + """ + process_log(__name__, LogMessage.FUNCTION_CALL, locals(), name="compute") + if bodies is not None: + check_type(bodies, EntList) + return self.cad_diagnostic.Compute(coerce_optional_dispatch(bodies, "ent_list")) + + def get_edge_edge_intersect_diagnostic( + self, + edge_id_pair1: IntegerArray | None, + edge_id_pair2: IntegerArray | None, + intersect_coordinates: DoubleArray | None, + ) -> bool: + """ + Retrieves intersecting CAD edge-to-edge information + + Args: + edge_id_pair1 (IntegerArray): The first set of edge identifiers + edge_id_pair2 (IntegerArray): The second set of edge identifiers + intersect_coordinates (DoubleArray): The intersected coordinates + + Returns: + True if operation is successful, False otherwise. + """ + process_log( + __name__, LogMessage.FUNCTION_CALL, locals(), name="get_edge_edge_intersect_diagnostic" + ) + if edge_id_pair1 is not None: + check_type(edge_id_pair1, IntegerArray) + if edge_id_pair2 is not None: + check_type(edge_id_pair2, IntegerArray) + if intersect_coordinates is not None: + check_type(intersect_coordinates, DoubleArray) + return self.cad_diagnostic.GetEdgeEdgeIntersectDiagnostic( + coerce_optional_dispatch(edge_id_pair1, "integer_array"), + coerce_optional_dispatch(edge_id_pair2, "integer_array"), + coerce_optional_dispatch(intersect_coordinates, "double_array"), + ) + + def get_face_face_intersect_diagnostic( + self, + face_id_pair1: IntegerArray | None, + face_id_pair2: IntegerArray | None, + intersect_coordinates: DoubleArray | None, + ) -> bool: + """ + Retrieves intersecting CAD face-to-face information + + Args: + face_id_pair1 (IntegerArray): The first set of face identifiers + face_id_pair2 (IntegerArray): The second set of face identifiers + intersect_coordinates (DoubleArray): The intersected coordinates + + Returns: + True if operation is successful, False otherwise. + """ + process_log( + __name__, LogMessage.FUNCTION_CALL, locals(), name="get_face_face_intersect_diagnostic" + ) + if face_id_pair1 is not None: + check_type(face_id_pair1, IntegerArray) + if face_id_pair2 is not None: + check_type(face_id_pair2, IntegerArray) + if intersect_coordinates is not None: + check_type(intersect_coordinates, DoubleArray) + return self.cad_diagnostic.GetFaceFaceIntersectDiagnostic( + coerce_optional_dispatch(face_id_pair1, "integer_array"), + coerce_optional_dispatch(face_id_pair2, "integer_array"), + coerce_optional_dispatch(intersect_coordinates, "double_array"), + ) + + def get_edge_self_intersect_diagnostic( + self, edge_id: IntegerArray | None, intersect_coordinates: DoubleArray | None + ) -> bool: + """ + Retrieves self-intersecting CAD edges + + Args: + edge_id (IntegerArray): The edge identifiers + intersect_coordinates (DoubleArray): The intersected coordinates + + Returns: + True if operation is successful, False otherwise. + """ + process_log( + __name__, LogMessage.FUNCTION_CALL, locals(), name="get_edge_self_intersect_diagnostic" + ) + if edge_id is not None: + check_type(edge_id, IntegerArray) + if intersect_coordinates is not None: + check_type(intersect_coordinates, DoubleArray) + return self.cad_diagnostic.GetEdgeSelfIntersectDiagnostic( + coerce_optional_dispatch(edge_id, "integer_array"), + coerce_optional_dispatch(intersect_coordinates, "double_array"), + ) + + def get_face_self_intersect_diagnostic( + self, face_id: IntegerArray | None, intersect_coordinates: DoubleArray | None + ) -> bool: + """ + Retrieves self-intersecting CAD faces + + Args: + face_id (IntegerArray): The face identifiers + intersect_coordinates (DoubleArray): The intersected coordinates + + Returns: + True if operation is successful, False otherwise. + """ + process_log( + __name__, LogMessage.FUNCTION_CALL, locals(), name="get_face_self_intersect_diagnostic" + ) + if face_id is not None: + check_type(face_id, IntegerArray) + if intersect_coordinates is not None: + check_type(intersect_coordinates, DoubleArray) + return self.cad_diagnostic.GetFaceSelfIntersectDiagnostic( + coerce_optional_dispatch(face_id, "integer_array"), + coerce_optional_dispatch(intersect_coordinates, "double_array"), + ) + + def get_non_manifold_body_diagnostic(self, body_id: IntegerArray | None) -> bool: + """ + Retrieves CAD non-manifold bodies + + Args: + body_id (IntegerArray): The body identifiers + + Returns: + True if operation is successful, False otherwise. + """ + process_log( + __name__, LogMessage.FUNCTION_CALL, locals(), name="get_non_manifold_bodies_diagnostic" + ) + if body_id is not None: + check_type(body_id, IntegerArray) + return self.cad_diagnostic.GetNonManifoldBodyDiagnostic( + coerce_optional_dispatch(body_id, "integer_array") + ) + + def get_non_manifold_edge_diagnostic(self, edge_id: IntegerArray | None) -> bool: + """ + Retrieves non-manifold edges + + Args: + edge_id (IntegerArray): The edge identifiers + + Returns: + True if operation is successful, False otherwise. + """ + process_log( + __name__, LogMessage.FUNCTION_CALL, locals(), name="get_non_manifold_edge_diagnostic" + ) + if edge_id is not None: + check_type(edge_id, IntegerArray) + return self.cad_diagnostic.GetNonManifoldEdgeDiagnostic( + coerce_optional_dispatch(edge_id, "integer_array") + ) + + def get_toxic_body_diagnostic(self, body_id: IntegerArray | None) -> bool: + """ + Retrieves toxic bodies + + Args: + body_id (IntegerArray): The body identifiers + + Returns: + True if operation is successful, False otherwise. + """ + process_log(__name__, LogMessage.FUNCTION_CALL, locals(), name="get_toxic_body_diagnostic") + if body_id is not None: + check_type(body_id, IntegerArray) + return self.cad_diagnostic.GetToxicBodyDiagnostic( + coerce_optional_dispatch(body_id, "integer_array") + ) + + def get_sliver_face_diagnostic(self, face_id: IntegerArray | None) -> bool: + """ + Retrieves sliver faces + + Args: + face_id (IntegerArray): The face identifiers + + Returns: + True if operation is successful, False otherwise. + """ + process_log(__name__, LogMessage.FUNCTION_CALL, locals(), name="get_sliver_face_diagnostic") + if face_id is not None: + check_type(face_id, IntegerArray) + return self.cad_diagnostic.GetSliverFaceDiagnostic( + coerce_optional_dispatch(face_id, "integer_array") + ) diff --git a/src/moldflow/common.py b/src/moldflow/common.py index 94791fe..49a10a6 100644 --- a/src/moldflow/common.py +++ b/src/moldflow/common.py @@ -81,15 +81,6 @@ class ImportUnitIndex(Enum): IN = 3 -class MDLKernel(Enum): - """ - Enum for MDLKernel - """ - - PARAMETRIC = "Parametric" - PARASOLID = "Parasolid" - - class MDLContactMeshType(Enum): """ Enum for MDLContactMeshType @@ -140,6 +131,15 @@ class SystemUnits(Enum): STANDARD = "SI" +class WarningMessage(Enum): + """ + Enum for warning messages. + """ + + DEPRECATED = "Deprecated" + DEPRECATED_BY = "Deprecated by {replacement}" + + class ErrorMessage(Enum): """ Enum for error messages. @@ -409,7 +409,6 @@ class Mesher3DType(Enum): """ ADVANCING_FRONT = "AdvancingFront" - LEGACY = "Legacy" ADVANCING_LAYERS = "AdvancingLayers" @@ -651,6 +650,7 @@ class StandardViews(Enum): ISOMETRIC = "Isometric" +# To be updated to use int values when legacy support is removed. class AnimationSpeed(Enum): """ Enum for AnimationSpeed @@ -740,3 +740,13 @@ class PropertyType(Enum): PART_INSERT = 40907 MOLD_INSERT_SURFACE = 40908 PARTING_SURFACE = 40910 + + +class CaptureModes(Enum): + """ + Enum for CaptureModes + """ + + ACTIVE_VIEW = 0 + ALL_VIEWS = 1 + GRAPHIC_DISPLAY_AREA = 2 diff --git a/src/moldflow/constants.py b/src/moldflow/constants.py index 4851f3a..49551aa 100644 --- a/src/moldflow/constants.py +++ b/src/moldflow/constants.py @@ -24,6 +24,9 @@ LOCALE_ENVIRONMENT_VARIABLE_NAME = "MFSYN_LOCALE" LOCALE_REGISTRY_VARIABLE_NAME = "MFSYN_LOCALE" +# Animation speed constants +ANIMATION_SPEED_CONVERTER = {"Slow": 0, "Medium": 1, "Fast": 2} + # BCP-47 standard constants THREE_LETTER_TO_BCP_47 = { "chs": "zh-CN", @@ -57,3 +60,5 @@ BMP_FILE_EXT = ".bmp" TIF_FILE_EXT = ".tif" MP4_FILE_EXT = ".mp4" +GIF_FILE_EXT = ".gif" +VTK_FILE_EXT = ".vtk" diff --git a/src/moldflow/helper.py b/src/moldflow/helper.py index c2d1a90..d9c682e 100644 --- a/src/moldflow/helper.py +++ b/src/moldflow/helper.py @@ -7,10 +7,12 @@ from enum import Enum import os +import functools +import warnings from win32com.client import VARIANT import pythoncom from .errors import raise_type_error, raise_value_error, raise_index_error -from .common import ValueErrorReason, LogMessage +from .common import ValueErrorReason, LogMessage, WarningMessage from .logger import process_log @@ -240,9 +242,7 @@ def check_file_extension(file_name: str, extensions: tuple | str): Check if the file name has a valid extension. Args: file_name (str): The file name to check. - extensions (list[str]): A list of valid file extensions. - Raises: - ValueError: If the file name does not have a valid extension. + extensions (tuple[str, ...] | str): Valid file extension(s). """ process_log(__name__, LogMessage.CHECK_FILE_EXTENSION, locals(), file_name=file_name) check_type(file_name, str) @@ -337,3 +337,34 @@ def coerce_optional_dispatch(value, attr_name: str | None = None): if attr_name: value = getattr(value, attr_name) return value + + +# NOTE: Once Python 3.13 is the minimum supported version, prefer using the +# stdlib decorator warnings.deprecated instead of this helper. +# See: https://docs.python.org/3.13/library/warnings.html#warnings.deprecated +def deprecated(replacement: str | None = None, message: str | None = None): + """Decorator to mark functions as deprecated and emit a DeprecationWarning. + + Parameters: + replacement: Optional alternative function name to include in the message + message: Optional custom message; if provided, overrides default text + """ + + def _decorator(func): + if replacement: + default_msg = ( + f"{func.__qualname__}: " + f"{WarningMessage.DEPRECATED_BY.value.format(replacement=replacement)}" + ) + else: + default_msg = f"{func.__qualname__}: {WarningMessage.DEPRECATED.value}" + warn_msg = message or default_msg + + @functools.wraps(func) + def _wrapped(*args, **kwargs): + warnings.warn(warn_msg, DeprecationWarning, stacklevel=2) + return func(*args, **kwargs) + + return _wrapped + + return _decorator diff --git a/src/moldflow/image_export_options.py b/src/moldflow/image_export_options.py new file mode 100644 index 0000000..deb725a --- /dev/null +++ b/src/moldflow/image_export_options.py @@ -0,0 +1,417 @@ +""" +Usage: + ImageExportOptions Class API Wrapper +""" + +from .logger import process_log, LogMessage +from .helper import ( + check_type, + check_is_non_negative, + get_enum_value, + check_range, + check_file_extension, +) +from .common import CaptureModes +from .constants import PNG_FILE_EXT, JPG_FILE_EXT, JPEG_FILE_EXT, BMP_FILE_EXT, TIF_FILE_EXT + + +class ImageExportOptions: + """ + Wrapper for ImageExportOptions class of Moldflow Synergy. + """ + + def __init__(self, _image_export_options): + """ + Initialize the ImageExportOptions with a ImageExportOptions instance from COM. + + Args: + _image_export_options: The ImageExportOptions instance. + """ + process_log(__name__, LogMessage.CLASS_INIT, locals(), name="ImageExportOptions") + self.image_export_options = _image_export_options + + @property + def file_name(self) -> str: + """ + The file name. + + :getter: Get the file name. + :setter: Set the file name. + :type: str + """ + process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="file_name") + return self.image_export_options.FileName + + @file_name.setter + def file_name(self, value: str) -> None: + """ + The file name. + + Args: + value (str): The file name to set. + """ + process_log(__name__, LogMessage.PROPERTY_SET, locals(), name="file_name", value=value) + check_type(value, str) + value = check_file_extension( + value, (PNG_FILE_EXT, JPG_FILE_EXT, JPEG_FILE_EXT, BMP_FILE_EXT, TIF_FILE_EXT) + ) + self.image_export_options.FileName = value + + @property + def size_x(self) -> int: + """ + The X size (width) of the image. + + :default: 800 + :getter: Get the X size. + :setter: Set the X size. + :type: int (positive) + """ + process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="size_x") + return self.image_export_options.SizeX + + @size_x.setter + def size_x(self, value: int) -> None: + """ + Set the X size (width) of the image. + + Args: + value (int): The X size to set (must be positive). + """ + process_log(__name__, LogMessage.PROPERTY_SET, locals(), name="size_x", value=value) + check_type(value, int) + check_is_non_negative(value) + self.image_export_options.SizeX = value + + @property + def size_y(self) -> int: + """ + The Y size (height) of the image. + + :default: 600 + :getter: Get the Y size. + :setter: Set the Y size. + :type: int (positive) + """ + process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="size_y") + return self.image_export_options.SizeY + + @size_y.setter + def size_y(self, value: int) -> None: + """ + Set the Y size (height) of the image. + + Args: + value (int): The Y size to set (must be positive). + """ + process_log(__name__, LogMessage.PROPERTY_SET, locals(), name="size_y", value=value) + check_type(value, int) + check_is_non_negative(value) + self.image_export_options.SizeY = value + + @property + def show_result(self) -> bool: + """ + Whether to show the result. + + :default: True + :getter: Get show_result. + :setter: Set show_result. + :type: bool + """ + process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="show_result") + return self.image_export_options.ShowResult + + @show_result.setter + def show_result(self, value: bool) -> None: + """ + Set whether to show the result. + + Args: + value (bool): Show result or not. + """ + process_log(__name__, LogMessage.PROPERTY_SET, locals(), name="show_result", value=value) + check_type(value, bool) + self.image_export_options.ShowResult = value + + @property + def show_legend(self) -> bool: + """ + Whether to show the legend. + + :default: True + :getter: Get show_legend. + :setter: Set show_legend. + :type: bool + """ + process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="show_legend") + return self.image_export_options.ShowLegend + + @show_legend.setter + def show_legend(self, value: bool) -> None: + """ + Set whether to show the legend. + + Args: + value (bool): Show legend or not. + """ + process_log(__name__, LogMessage.PROPERTY_SET, locals(), name="show_legend", value=value) + check_type(value, bool) + self.image_export_options.ShowLegend = value + + @property + def show_rotation_angle(self) -> bool: + """ + Whether to show the rotation angle values. + + :default: True + :getter: Get show_rotation_angle. + :setter: Set show_rotation_angle. + :type: bool + """ + process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="show_rotation_angle") + return self.image_export_options.ShowRotationAngle + + @show_rotation_angle.setter + def show_rotation_angle(self, value: bool) -> None: + """ + Set whether to show the rotation angle values. + + Args: + value (bool): Show rotation angle values or not. + """ + process_log( + __name__, LogMessage.PROPERTY_SET, locals(), name="show_rotation_angle", value=value + ) + check_type(value, bool) + self.image_export_options.ShowRotationAngle = value + + @property + def show_rotation_axes(self) -> bool: + """ + Whether to show the rotation axes. + + :default: True + :getter: Get show_rotation_axes. + :setter: Set show_rotation_axes. + :type: bool + """ + process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="show_rotation_axes") + return self.image_export_options.ShowRotationAxes + + @show_rotation_axes.setter + def show_rotation_axes(self, value: bool) -> None: + """ + Set whether to show the rotation axes. + + Args: + value (bool): Show rotation axes or not. + """ + process_log( + __name__, LogMessage.PROPERTY_SET, locals(), name="show_rotation_axes", value=value + ) + check_type(value, bool) + self.image_export_options.ShowRotationAxes = value + + @property + def show_scale_bar(self) -> bool: + """ + Whether to show the scale bar. + + :default: True + :getter: Get show_scale_bar. + :setter: Set show_scale_bar. + :type: bool + """ + process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="show_scale_bar") + return self.image_export_options.ShowScaleBar + + @show_scale_bar.setter + def show_scale_bar(self, value: bool) -> None: + """ + Set whether to show the scale bar. + + Args: + value (bool): Show scale bar or not. + """ + process_log(__name__, LogMessage.PROPERTY_SET, locals(), name="show_scale_bar", value=value) + check_type(value, bool) + self.image_export_options.ShowScaleBar = value + + @property + def show_plot_info(self) -> bool: + """ + Whether to show the plot info. + + :default: True + :getter: Get show_plot_info. + :setter: Set show_plot_info. + :type: bool + """ + process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="show_plot_info") + return self.image_export_options.ShowPlotInfo + + @show_plot_info.setter + def show_plot_info(self, value: bool) -> None: + """ + Set whether to show the plot info. + + Args: + value (bool): Show plot info or not. + """ + process_log(__name__, LogMessage.PROPERTY_SET, locals(), name="show_plot_info", value=value) + check_type(value, bool) + self.image_export_options.ShowPlotInfo = value + + @property + def show_study_title(self) -> bool: + """ + Whether to show the study title. + + :default: True + :getter: Get show_study_title. + :setter: Set show_study_title. + :type: bool + """ + process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="show_study_title") + return self.image_export_options.ShowStudyTitle + + @show_study_title.setter + def show_study_title(self, value: bool) -> None: + """ + Set whether to show the study title. + + Args: + value (bool): Show study title or not. + """ + process_log( + __name__, LogMessage.PROPERTY_SET, locals(), name="show_study_title", value=value + ) + check_type(value, bool) + self.image_export_options.ShowStudyTitle = value + + @property + def show_ruler(self) -> bool: + """ + Whether to show the ruler. + + :default: True + :getter: Get show_ruler. + :setter: Set show_ruler. + :type: bool + """ + process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="show_ruler") + return self.image_export_options.ShowRuler + + @show_ruler.setter + def show_ruler(self, value: bool) -> None: + """ + Set whether to show the ruler. + + Args: + value (bool): Show ruler or not. + """ + process_log(__name__, LogMessage.PROPERTY_SET, locals(), name="show_ruler", value=value) + check_type(value, bool) + self.image_export_options.ShowRuler = value + + @property + def show_histogram(self) -> bool: + """ + Whether to show the histogram. + + :default: True + :getter: Get show_histogram. + :setter: Set show_histogram. + :type: bool + """ + process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="show_histogram") + return self.image_export_options.ShowHistogram + + @show_histogram.setter + def show_histogram(self, value: bool) -> None: + """ + Set whether to show the histogram. + + Args: + value (bool): Show histogram or not. + """ + process_log(__name__, LogMessage.PROPERTY_SET, locals(), name="show_histogram", value=value) + check_type(value, bool) + self.image_export_options.ShowHistogram = value + + @property + def show_min_max(self) -> bool: + """ + Whether to show the min/max. + + :default: True + :getter: Get show_min_max. + :setter: Set show_min_max. + :type: bool + """ + process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="show_min_max") + return self.image_export_options.ShowMinMax + + @show_min_max.setter + def show_min_max(self, value: bool) -> None: + """ + Set whether to show the min/max. + + Args: + value (bool): Show min/max or not. + """ + process_log(__name__, LogMessage.PROPERTY_SET, locals(), name="show_min_max", value=value) + check_type(value, bool) + self.image_export_options.ShowMinMax = value + + @property + def fit_to_screen(self) -> bool: + """ + Whether to fit the image to the screen. + + :default: True + :getter: Get fit_to_screen. + :setter: Set fit_to_screen. + :type: bool + """ + process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="fit_to_screen") + return self.image_export_options.FitToScreen + + @fit_to_screen.setter + def fit_to_screen(self, value: bool) -> None: + """ + Set whether to fit the image to the screen. + + Args: + value (bool): Fit to screen or not. + """ + process_log(__name__, LogMessage.PROPERTY_SET, locals(), name="fit_to_screen", value=value) + check_type(value, bool) + self.image_export_options.FitToScreen = value + + @property + def capture_mode(self) -> int: + """ + The capture mode. + + :default: CaptureModes.ACTIVE_VIEW/Active View/0 + :getter: Get capture_mode. + :setter: Set capture_mode. + :type: int + """ + process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="capture_mode") + return self.image_export_options.CaptureMode + + @capture_mode.setter + def capture_mode(self, value: CaptureModes | int) -> None: + """ + Set the capture mode. + + Args: + value (int): The capture mode to set. + """ + process_log(__name__, LogMessage.PROPERTY_SET, locals(), name="capture_mode", value=value) + value = get_enum_value(value, CaptureModes) + check_range(value, 0, 2, True, True) + self.image_export_options.CaptureMode = value diff --git a/src/moldflow/import_options.py b/src/moldflow/import_options.py index 175501c..1dcf2e3 100644 --- a/src/moldflow/import_options.py +++ b/src/moldflow/import_options.py @@ -7,9 +7,9 @@ """ from .logger import process_log -from .common import LogMessage, MeshType, ImportUnits, MDLKernel +from .common import LogMessage, MeshType, ImportUnits from .common import MDLContactMeshType, CADBodyProperty -from .helper import get_enum_value, check_type, check_is_non_negative +from .helper import get_enum_value, check_type, check_is_non_negative, deprecated from .com_proxy import safe_com @@ -152,28 +152,23 @@ def use_mdl(self, value: bool) -> None: self.import_options.UseMDL = value @property - def mdl_kernel(self) -> str: + @deprecated() + def mdl_kernel(self): """ - The MDL kernel. + .. deprecated:: 27.0.0 + + This property is deprecated and has no effect. Value is ignored. - :getter: Get the MDL kernel. - :setter: Set the MDL kernel. - :type: str """ - process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="mdl_kernel") - return self.import_options.MDLKernel + return "" @mdl_kernel.setter - def mdl_kernel(self, value: MDLKernel | str) -> None: + def mdl_kernel(self, value) -> None: """ - The MDL kernel. + This property is deprecated and has no effect. Value is ignored. - Args: - value (str): The MDL kernel to set. """ - process_log(__name__, LogMessage.PROPERTY_SET, locals(), name="mdl_kernel", value=value) - value = get_enum_value(value, MDLKernel) - self.import_options.MDLKernel = value + # No operation needed. @property def mdl_auto_edge_select(self) -> bool: diff --git a/src/moldflow/mesh_editor.py b/src/moldflow/mesh_editor.py index 88e207f..3c2f1ae 100644 --- a/src/moldflow/mesh_editor.py +++ b/src/moldflow/mesh_editor.py @@ -9,6 +9,7 @@ # pylint: disable=C0302 from .logger import process_log +from .helper import deprecated from .common import LogMessage from .ent_list import EntList from .vector import Vector @@ -403,10 +404,13 @@ def align_normals(self, seed_tri: EntList | None, tris: EntList | None) -> int: coerce_optional_dispatch(tris, "ent_list"), ) + @deprecated("fill_hole_from_nodes or fill_hole_from_triangles") def fill_hole(self, nodes: EntList | None, fill_type: int | None = None) -> bool: """ - Fill a "hole" in the mesh by creating triangles between given nodes - If fill_type provided, fill a "hole" in the mesh by creating new triangles + .. deprecated:: 27.0.0 + Use :py:func:`fill_hole_from_nodes` or :py:func:`fill_hole_from_triangles` instead. + Fill a "hole" in the mesh by creating triangles between given nodes. + If fill_type provided, fill a "hole" in the mesh by creating new triangles. Args: nodes (EntList | None): EntList ordered sequence of nodes defining the outer @@ -424,6 +428,41 @@ def fill_hole(self, nodes: EntList | None, fill_type: int | None = None) -> bool check_type(fill_type, int) return self.mesh_editor.FillHole2(coerce_optional_dispatch(nodes, "ent_list"), fill_type) + def fill_hole_from_nodes(self, nodes: EntList | None) -> bool: + """ + Fill a "hole" in the mesh by nodes. + + Parameters: + nodes: EntList ordered sequence of nodes defining the outer boundary of the hole + + Returns: + True if operation is successful; False otherwise + """ + process_log(__name__, LogMessage.FUNCTION_CALL, locals(), name="fill_hole_from_nodes") + if nodes is not None: + check_type(nodes, EntList) + return self.mesh_editor.FillHoleFromNodes(coerce_optional_dispatch(nodes, "ent_list")) + + def fill_hole_from_triangles(self, tris: EntList | None, apply_smoothing: bool) -> bool: + """ + Fill a "hole" in the mesh by triangles. + + Parameters: + tris: EntList of triangles around the hole + apply_smoothing: Specify True to smooth; False to disable smoothing. + + Returns: + True if operation is successful; False otherwise + """ + process_log(__name__, LogMessage.FUNCTION_CALL, locals(), name="fill_hole_from_triangles") + if tris is not None: + check_type(tris, EntList) + check_type(apply_smoothing, bool) + smooth = apply_smoothing + return self.mesh_editor.FillHoleFromTriangles( + coerce_optional_dispatch(tris, "ent_list"), bool(smooth) + ) + # pylint: disable-next=R0913, R0917 def create_tet( self, diff --git a/src/moldflow/mesh_generator.py b/src/moldflow/mesh_generator.py index 2d02a69..4ddeb22 100644 --- a/src/moldflow/mesh_generator.py +++ b/src/moldflow/mesh_generator.py @@ -17,7 +17,7 @@ Mesher3DType, CADContactMesh, ) -from .helper import check_type, check_range, get_enum_value +from .helper import check_type, check_range, get_enum_value, deprecated from .com_proxy import safe_com @@ -144,8 +144,11 @@ def smoothing(self, value: bool) -> None: self.mesh_generator.Smoothing = value @property + @deprecated() def element_reduction(self) -> bool: """ + .. deprecated:: 27.0.0 + Enables/disables automatic element size determination for fusion meshes from faceted geometry. @@ -191,8 +194,11 @@ def surface_optimization(self, value: bool) -> None: self.mesh_generator.SurfaceOptimization = value @property + @deprecated() def automatic_tetra_optimization(self) -> bool: """ + .. deprecated:: 27.0.0 + Specifies whether optimizing tetras automatically. :getter: Get the automatic tetra optimization option @@ -287,8 +293,11 @@ def tetra_layers_for_cores(self, value: int) -> None: self.mesh_generator.TetraLayersForCores = value @property + @deprecated() def tetra_max_ar(self) -> float: """ + .. deprecated:: 27.0.0 + Limit on aspect ratio for tetrahedral meshes. :getter: Get the tetra max aspect ratio option @@ -360,8 +369,11 @@ def maximum_match_distance(self, value: float) -> None: self.mesh_generator.MaximumMatchDistance = value @property + @deprecated() def use_tetras_on_edge(self) -> bool: """ + .. deprecated:: 27.0.0 + Specifies whether tetras are to be created on model edges. :getter: Get the use tetras on edge option @@ -776,8 +788,11 @@ def cad_mesh_minimum_curvature_percentage(self, value: float) -> None: self.mesh_generator.CadMeshMinimumCurvaturePercentage = value @property + @deprecated() def use_fallbacks(self) -> bool: """ + .. deprecated:: 27.0.0 + Specifies whether fallback is to be used when CAD meshing fails. :getter: Get the use fallbacks option diff --git a/src/moldflow/plot_manager.py b/src/moldflow/plot_manager.py index 69e8c68..a454811 100644 --- a/src/moldflow/plot_manager.py +++ b/src/moldflow/plot_manager.py @@ -19,7 +19,7 @@ from .helper import check_type, get_enum_value, check_file_extension, coerce_optional_dispatch from .com_proxy import safe_com from .errors import raise_save_error -from .constants import XML_FILE_EXT, SDZ_FILE_EXT, FBX_FILE_EXT, ELE_FILE_EXT +from .constants import XML_FILE_EXT, SDZ_FILE_EXT, FBX_FILE_EXT, ELE_FILE_EXT, VTK_FILE_EXT class PlotManager: @@ -1008,3 +1008,23 @@ def create_material_plot( if result is None: return None return MaterialPlot(result) + + def export_to_vtk(self, file_name: str, binary_format: bool = True) -> bool: + """ + Export the results to a VTK file. + + Args: + file_name (str): The name of the VTK file. + binary_format (bool): Use Binary (True) or ASCII (False). Default: True. + + Returns: + bool: True if successful, False otherwise. + """ + process_log(__name__, LogMessage.FUNCTION_CALL, locals(), name="export_to_vtk") + check_type(file_name, str) + check_type(binary_format, bool) + file_name = check_file_extension(file_name, VTK_FILE_EXT) + result = self.plot_manager.ExportToVTK(file_name, binary_format) + if not result: + raise_save_error(saving="Results", file_name=file_name) + return result diff --git a/src/moldflow/study_doc.py b/src/moldflow/study_doc.py index 78295dd..75fe885 100644 --- a/src/moldflow/study_doc.py +++ b/src/moldflow/study_doc.py @@ -181,6 +181,18 @@ def study_name(self) -> str: process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="study_name") return self.study_doc.StudyName + @property + def display_name(self) -> str: + """ + Value of Display Name. + + :getter: Get value of Display Name + :setter: Set value of Display Name + :type: str + """ + process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="display_name") + return self.study_doc.DisplayName + def save(self) -> bool: """ Saves the study @@ -750,3 +762,18 @@ def is_analysis_running(self) -> bool: """ process_log(__name__, LogMessage.FUNCTION_CALL, locals(), name="is_analysis_running") return self.study_doc.IsAnalysisRunning + + def get_all_cad_bodies(self, is_visible_only: bool) -> str: + """ + Retrieves the body IDs of all cad models as a string + + Args: + is_visible_only: True to examine visible CAD bodies only; + False to examine all CAD bodies + + Returns: + The body IDs of all CAD models as a string + """ + process_log(__name__, LogMessage.FUNCTION_CALL, locals(), name="get_all_cad_bodies") + check_type(is_visible_only, bool) + return self.study_doc.GetAllCadBodies(is_visible_only) diff --git a/src/moldflow/synergy.py b/src/moldflow/synergy.py index d233978..d8c0e72 100644 --- a/src/moldflow/synergy.py +++ b/src/moldflow/synergy.py @@ -9,6 +9,7 @@ import os import win32com.client from .boundary_conditions import BoundaryConditions +from .cad_diagnostic import CADDiagnostic from .cad_manager import CADManager from .circuit_generator import CircuitGenerator from .data_transform import DataTransform @@ -370,6 +371,17 @@ def boundary_conditions(self) -> BoundaryConditions: return None return BoundaryConditions(result) + @property + def cad_diagnostic(self) -> CADDiagnostic: + """ + Get the CADDiagnostic object. + """ + process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="cad_diagnostic") + result = self.synergy.CADDiagnostic + if result is None: + return None + return CADDiagnostic(result) + @property def cad_manager(self) -> CADManager: """ @@ -666,3 +678,14 @@ def version(self) -> str: """ process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="version") return self.synergy.Version + + def log(self, message: str) -> None: + """ + Log a message to the Synergy application. + + Args: + message (str): The message to log. + """ + process_log(__name__, LogMessage.FUNCTION_CALL, locals(), name="log") + check_type(message, str) + self.synergy.Log(message) diff --git a/src/moldflow/viewer.py b/src/moldflow/viewer.py index 52d974a..1eaf535 100644 --- a/src/moldflow/viewer.py +++ b/src/moldflow/viewer.py @@ -7,15 +7,19 @@ """ # pylint: disable=C0302 +from typing import Optional from win32com.client import VARIANT import pythoncom from .double_array import DoubleArray +from .image_export_options import ImageExportOptions +from .animation_export_options import AnimationExportOptions from .ent_list import EntList from .logger import process_log from .com_proxy import safe_com from .common import LogMessage, ViewModes, StandardViews, AnimationSpeed from .constants import ( MP4_FILE_EXT, + GIF_FILE_EXT, JPG_FILE_EXT, JPEG_FILE_EXT, PNG_FILE_EXT, @@ -31,6 +35,7 @@ check_is_non_negative, check_file_extension, coerce_optional_dispatch, + deprecated, ) from .errors import raise_value_error from .common import ValueErrorReason @@ -288,6 +293,7 @@ def print(self) -> None: self.viewer.Print() # pylint: disable=R0913, R0917 + @deprecated("save_image_with_options") def save_image( self, filename: str, @@ -306,6 +312,9 @@ def save_image( min_max: bool = False, ) -> bool: """ + .. deprecated:: 27.0.0 + Use :py:func:`save_image_with_options` instead. + Saves the current view as an image. Args: @@ -363,10 +372,14 @@ def save_image( min_max, ) + @deprecated("save_animation_with_options") def save_animation( self, filename: str, speed: AnimationSpeed | str, prompts: bool = False ) -> bool: """ + .. deprecated:: 27.0.0 + Use :py:func:`save_animation_with_options` instead. + Saves the current view as an animation. Args: @@ -379,13 +392,49 @@ def save_animation( """ process_log(__name__, LogMessage.FUNCTION_CALL, locals(), name="save_animation") check_type(filename, str) - filename = check_file_extension(filename, (MP4_FILE_EXT)) + filename = check_file_extension(filename, (MP4_FILE_EXT, GIF_FILE_EXT)) speed = get_enum_value(speed, AnimationSpeed) check_type(prompts, bool) return self.viewer.SaveAnimation3(filename, speed, prompts) + def animation_export_options(self) -> AnimationExportOptions: + """ + Creates a new AnimationExportOptions object for configuring animation export settings. + + Returns: + A new AnimationExportOptions object. + """ + process_log(__name__, LogMessage.FUNCTION_CALL, locals(), name="animation_export_options") + result = self.viewer.AnimationExportOptions + if result is None: + return None + return AnimationExportOptions(result) + + def save_animation_with_options(self, options: Optional[AnimationExportOptions] = None) -> bool: + """ + Saves the current view as an animation with the given options. + + Args: + options: The options to use for the animation. + If None, a new AnimationExportOptions object will be created with default settings. + + Returns: + True if successful, False otherwise. + """ + process_log( + __name__, LogMessage.FUNCTION_CALL, locals(), name="save_animation_with_options" + ) + if options is None: + options = self.animation_export_options() + check_type(options, AnimationExportOptions) + return self.viewer.SaveAnimation4(options.animation_export_options) + + @deprecated("save_image_with_options") def save_image_legacy(self, filename: str, x: int | None = None, y: int | None = None) -> bool: """ + .. deprecated:: 27.0.0 + Use :py:func:`save_image_with_options` instead. + Save image using legacy behavior only (V1/V2): - filename only -> SaveImage(filename) - filename and positive x,y -> SaveImage2(filename, x, y) @@ -423,6 +472,36 @@ def save_image_legacy(self, filename: str, x: int | None = None, y: int | None = check_is_positive(y) return self.viewer.SaveImage2(filename, x, y) + def image_export_options(self) -> ImageExportOptions: + """ + Creates a new ImageExportOptions object for configuring image export settings. + + Returns: + A new ImageExportOptions object. + """ + process_log(__name__, LogMessage.FUNCTION_CALL, locals(), name="image_export_options") + result = self.viewer.ImageExportOptions + if result is None: + return None + return ImageExportOptions(result) + + def save_image_with_options(self, options: Optional[ImageExportOptions] = None) -> bool: + """ + Saves the current view as an image with the given options. + + Args: + options (ImageExportOptions | None): The options to use for the image. + If None, a new ImageExportOptions object will be created with default settings. + + Returns: + True if successful, False otherwise. + """ + process_log(__name__, LogMessage.FUNCTION_CALL, locals(), name="save_image_with_options") + if options is None: + options = self.image_export_options() + check_type(options, ImageExportOptions) + return self.viewer.SaveImage5(options.image_export_options) + def enable_clipping_plane_by_id(self, plane_id: int, enable: bool) -> None: """ Enables or disables clipping by plane ID. diff --git a/tests/api/unit_tests/mock_container.py b/tests/api/unit_tests/mock_container.py index d12d623..86dea4a 100644 --- a/tests/api/unit_tests/mock_container.py +++ b/tests/api/unit_tests/mock_container.py @@ -41,8 +41,10 @@ def __init__(self): setattr(self, key.upper(), mock_obj) # Explicit attribute definitions for IntelliSense support + ANIMATION_EXPORT_OPTIONS: Mock BOUNDARY_CONDITIONS: Mock BOUNDARY_LIST: Mock + CAD_DIAGNOSTIC: Mock CAD_MANAGER: Mock CIRCUIT_GENERATOR: Mock DATA_TRANSFORM: Mock @@ -50,6 +52,7 @@ def __init__(self): DOUBLE_ARRAY: Mock ENT_LIST: Mock FOLDER_MANAGER: Mock + IMAGE_EXPORT_OPTIONS: Mock IMPORT_OPTIONS: Mock INTEGER_ARRAY: Mock LAYER_MANAGER: Mock diff --git a/tests/api/unit_tests/test_unit_animation_export_options.py b/tests/api/unit_tests/test_unit_animation_export_options.py new file mode 100644 index 0000000..77e3fb4 --- /dev/null +++ b/tests/api/unit_tests/test_unit_animation_export_options.py @@ -0,0 +1,177 @@ +""" +Test for AnimationExportOptions Wrapper Class of moldflow-api module. +Test Details: + +Classes: + TestUnitAnimationExportOptions: Test suite for the AnimationExportOptions class. +Fixtures: + mock_animation_export_options: Fixture to create a mock instance of AnimationExportOptions. +Test Methods: + +""" + +import pytest +from moldflow import AnimationExportOptions, CaptureModes, AnimationSpeed +from moldflow.constants import ANIMATION_SPEED_CONVERTER +from moldflow.logger import set_is_logging +from tests.conftest import ( + NON_NEGATIVE_INT, + VALID_STR, + VALID_BOOL, + INVALID_INT, + INVALID_BOOL, + INVALID_STR, + NEGATIVE_INT, +) + + +@pytest.mark.unit +class TestUnitAnimationExportOptions: + """ + Test suite for the AnimationExportOptions class. + """ + + set_is_logging(False) + + @pytest.fixture + def mock_animation_export_options(self, mock_object) -> AnimationExportOptions: + """ + Fixture to create a mock instance of AnimationExportOptions. + Args: + mock_object: Mock object for the AnimationExportOptions dependency. + Returns: + AnimationExportOptions: An instance of AnimationExportOptions with the mock object. + """ + return AnimationExportOptions(mock_object) + + @pytest.mark.parametrize( + "pascal_name, property_name, value,", + [("FileName", "file_name", x) for x in VALID_STR] + + [("AnimationSpeed", "animation_speed", x) for x in range(2)] + + [("ShowPrompts", "show_prompts", x) for x in VALID_BOOL] + + [("SizeX", "size_x", x) for x in NON_NEGATIVE_INT] + + [("SizeY", "size_y", x) for x in NON_NEGATIVE_INT] + + [("CaptureMode", "capture_mode", x.value) for x in CaptureModes], + ) + # pylint: disable-next=R0913, R0917 + def test_get_properties( + self, + mock_animation_export_options: AnimationExportOptions, + mock_object, + pascal_name, + property_name, + value, + ): + """ + Test Get properties of AnimationExportOptions. + + Args: + mock_animation_export_options: Instance of AnimationExportOptions. + property_name: Name of the property to test. + pascal_name: Pascal case name of the property. + value: Value to set and check. + """ + setattr(mock_object, pascal_name, value) + result = getattr(mock_animation_export_options, property_name) + assert isinstance(result, type(value)) + assert result == value + + @pytest.mark.parametrize( + "pascal_name, property_name, value, expected", + [ + ("FileName", "file_name", x, y) + for (x, y) in [("Test", "Test.mp4"), ("Test.mp4", "Test.mp4"), ("Test.gif", "Test.gif")] + ] + + [ + ("AnimationSpeed", "animation_speed", x, ANIMATION_SPEED_CONVERTER[x.value]) + for x in AnimationSpeed + ] + + [("AnimationSpeed", "animation_speed", x, x) for x in range(2)] + + [("ShowPrompts", "show_prompts", x, x) for x in VALID_BOOL] + + [("SizeX", "size_x", x, x) for x in NON_NEGATIVE_INT] + + [("SizeY", "size_y", x, x) for x in NON_NEGATIVE_INT] + + [("CaptureMode", "capture_mode", x, x.value) for x in CaptureModes], + ) + # pylint: disable-next=R0913, R0917 + def test_set_properties( + self, + mock_animation_export_options: AnimationExportOptions, + mock_object, + pascal_name, + property_name, + value, + expected, + ): + """ + Test properties of AnimationExportOptions. + + Args: + mock_animation_export_options: Instance of AnimationExportOptions. + property_name: Name of the property to test. + pascal_name: Pascal case name of the property. + value: Value to set and check. + """ + setattr(mock_animation_export_options, property_name, value) + result = getattr(mock_object, pascal_name) + assert isinstance(result, type(expected)) + assert result == expected + + @pytest.mark.parametrize( + "pascal_name, property_name, value", + [("FileName", "file_name", x) for x in INVALID_STR] + + [("AnimationSpeed", "animation_speed", x) for x in INVALID_INT] + + [("ShowPrompts", "show_prompts", x) for x in INVALID_BOOL] + + [("SizeX", "size_x", x) for x in INVALID_INT] + + [("SizeY", "size_y", x) for x in INVALID_INT] + + [("CaptureMode", "capture_mode", x) for x in INVALID_INT], + ) + # pylint: disable-next=R0913, R0917 + def test_invalid_properties( + self, + mock_object, + mock_animation_export_options: AnimationExportOptions, + pascal_name, + property_name, + value, + _, + ): + """ + Test invalid properties of AnimationExportOptions. + Args: + mock_animation_export_options: Instance of AnimationExportOptions. + property_name: Name of the property to test. + value: Invalid value to set and check. + """ + with pytest.raises(TypeError) as e: + setattr(mock_animation_export_options, property_name, value) + assert _("Invalid") in str(e.value) + getattr(mock_object, pascal_name).assert_not_called() + + @pytest.mark.parametrize( + "pascal_name, property_name, value", + [("SizeX", "size_x", x) for x in NEGATIVE_INT] + + [("SizeY", "size_y", x) for x in NEGATIVE_INT] + + [("AnimationSpeed", "animation_speed", x) for x in NEGATIVE_INT + [3, 4]] + + [("CaptureMode", "capture_mode", x) for x in NEGATIVE_INT + [3, 4]], + ) + # pylint: disable-next=R0913, R0917 + def test_invalid_value_properties( + self, + mock_object, + mock_animation_export_options: AnimationExportOptions, + pascal_name, + property_name, + value, + _, + ): + """ + Test invalid properties of AnimationExportOptions. + Args: + mock_animation_export_options: Instance of AnimationExportOptions. + property_name: Name of the property to test. + value: Invalid value to set and check. + """ + with pytest.raises(ValueError) as e: + setattr(mock_animation_export_options, property_name, value) + assert _("Invalid") in str(e.value) + getattr(mock_object, pascal_name).assert_not_called() diff --git a/tests/api/unit_tests/test_unit_cad_diagnostic.py b/tests/api/unit_tests/test_unit_cad_diagnostic.py new file mode 100644 index 0000000..7815e43 --- /dev/null +++ b/tests/api/unit_tests/test_unit_cad_diagnostic.py @@ -0,0 +1,263 @@ +""" +Test for CADDiagnostic Wrapper Class of moldflow-api module. +""" + +import pytest +from moldflow import CADDiagnostic, EntList +from tests.api.unit_tests.conftest import VALID_MOCK, INVALID_MOCK +from tests.conftest import pad_and_zip, VALID_BOOL + + +@pytest.mark.unit +class TestUnitCADDiagnostic: + """ + Test suite for the CADDiagnostic class. + """ + + @pytest.fixture + def mock_cad_diagnostic(self, mock_object) -> CADDiagnostic: + """ + Fixture to create a mock instance of CADDiagnostic. + Args: + mock_object: Mock object for the CADDiagnostic dependency. + Returns: + CADDiagnostic: An instance of CADDiagnostic with the mock object. + """ + return CADDiagnostic(mock_object) + + @pytest.mark.parametrize( + "pascal_name, property_name, args, expected_args, return_type, return_value", + [ + ("Compute", "compute", (x,), (x.ent_list,), bool, y) + for x, y in pad_and_zip(VALID_MOCK.ENT_LIST, VALID_BOOL) + ] + + [ + ( + "GetEdgeEdgeIntersectDiagnostic", + "get_edge_edge_intersect_diagnostic", + (x, y, z), + (x.integer_array, y.integer_array, z.double_array), + bool, + a, + ) + for x, y, z, a in pad_and_zip( + VALID_MOCK.INTEGER_ARRAY, + VALID_MOCK.INTEGER_ARRAY, + VALID_MOCK.DOUBLE_ARRAY, + VALID_BOOL, + ) + ] + + [ + ( + "GetFaceFaceIntersectDiagnostic", + "get_face_face_intersect_diagnostic", + (x, y, z), + (x.integer_array, y.integer_array, z.double_array), + bool, + a, + ) + for x, y, z, a in pad_and_zip( + VALID_MOCK.INTEGER_ARRAY, + VALID_MOCK.INTEGER_ARRAY, + VALID_MOCK.DOUBLE_ARRAY, + VALID_BOOL, + ) + ] + + [ + ( + "GetEdgeSelfIntersectDiagnostic", + "get_edge_self_intersect_diagnostic", + (x, y), + (x.integer_array, y.double_array), + bool, + z, + ) + for x, y, z in pad_and_zip( + VALID_MOCK.INTEGER_ARRAY, VALID_MOCK.DOUBLE_ARRAY, VALID_BOOL + ) + ] + + [ + ( + "GetFaceSelfIntersectDiagnostic", + "get_face_self_intersect_diagnostic", + (x, y), + (x.integer_array, y.double_array), + bool, + z, + ) + for x, y, z in pad_and_zip( + VALID_MOCK.INTEGER_ARRAY, VALID_MOCK.DOUBLE_ARRAY, VALID_BOOL + ) + ] + + [ + ( + "GetNonManifoldBodyDiagnostic", + "get_non_manifold_body_diagnostic", + (x,), + (x.integer_array,), + bool, + y, + ) + for x, y in pad_and_zip(VALID_MOCK.INTEGER_ARRAY, VALID_BOOL) + ] + + [ + ( + "GetNonManifoldEdgeDiagnostic", + "get_non_manifold_edge_diagnostic", + (x,), + (x.integer_array,), + bool, + y, + ) + for x, y in pad_and_zip(VALID_MOCK.INTEGER_ARRAY, VALID_BOOL) + ] + + [ + ( + "GetToxicBodyDiagnostic", + "get_toxic_body_diagnostic", + (x,), + (x.integer_array,), + bool, + y, + ) + for x, y in pad_and_zip(VALID_MOCK.INTEGER_ARRAY, VALID_BOOL) + ] + + [ + ( + "GetSliverFaceDiagnostic", + "get_sliver_face_diagnostic", + (x,), + (x.integer_array,), + bool, + y, + ) + for x, y in pad_and_zip(VALID_MOCK.INTEGER_ARRAY, VALID_BOOL) + ], + ) + # pylint: disable-next=R0913, R0917 + def test_functions( + self, + mock_cad_diagnostic: CADDiagnostic, + mock_object, + pascal_name, + property_name, + args, + expected_args, + return_type, + return_value, + ): + """ + Test the functions of the CADDiagnostic class. + + Args: + mock_cad_diagnostic: The mock instance of CADDiagnostic. + mock_object: The mock object for the CADDiagnostic dependency. + pascal_name: The Pascal case name of the function. + property_name: The property name to be tested. + args: Arguments to be passed to the function. + expected_args: Expected arguments after processing. + return_type: Expected return type of the function. + return_value: Expected return value of the function. + """ + getattr(mock_object, pascal_name).return_value = return_value + result = getattr(mock_cad_diagnostic, property_name)(*args) + assert isinstance(result, return_type) + assert result == return_value + getattr(mock_object, pascal_name).assert_called_once_with(*expected_args) + + @pytest.mark.parametrize( + "pascal_name, property_name, args", + [("Compute", "compute", (x,)) for x in pad_and_zip(INVALID_MOCK)] + + [ + ("GetEdgeEdgeIntersectDiagnostic", "get_edge_edge_intersect_diagnostic", (x, y, z)) + for x, y, z in pad_and_zip(INVALID_MOCK, INVALID_MOCK, INVALID_MOCK) + ] + + [ + ("GetFaceFaceIntersectDiagnostic", "get_face_face_intersect_diagnostic", (x, y, z)) + for x, y, z in pad_and_zip(INVALID_MOCK, INVALID_MOCK, INVALID_MOCK) + ] + + [ + ("GetEdgeSelfIntersectDiagnostic", "get_edge_self_intersect_diagnostic", (x, y)) + for x, y in pad_and_zip(INVALID_MOCK, INVALID_MOCK) + ] + + [ + ("GetFaceSelfIntersectDiagnostic", "get_face_self_intersect_diagnostic", (x, y)) + for x, y in pad_and_zip(INVALID_MOCK, INVALID_MOCK) + ] + + [ + ("GetNonManifoldBodyDiagnostic", "get_non_manifold_body_diagnostic", (x,)) + for x in pad_and_zip(INVALID_MOCK) + ] + + [ + ("GetNonManifoldEdgeDiagnostic", "get_non_manifold_edge_diagnostic", (x,)) + for x in pad_and_zip(INVALID_MOCK) + ] + + [ + ("GetToxicBodyDiagnostic", "get_toxic_body_diagnostic", (x,)) + for x in pad_and_zip(INVALID_MOCK) + ] + + [ + ("GetSliverFaceDiagnostic", "get_sliver_face_diagnostic", (x,)) + for x in pad_and_zip(INVALID_MOCK) + ], + ) + # pylint: disable-next=R0913, R0917 + def test_functions_invalid_type( + self, mock_cad_diagnostic: CADDiagnostic, mock_object, pascal_name, property_name, args, _ + ): + """ + Test the functions of the CADDiagnostic class with invalid types. + + Args: + mock_cad_diagnostic: The mock instance of CADDiagnostic. + mock_object: The mock object for the CADDiagnostic dependency. + pascal_name: The Pascal case name of the function. + property_name: The property name to be tested. + args: Arguments to be passed to the function. + """ + with pytest.raises(TypeError) as e: + getattr(mock_cad_diagnostic, property_name)(*args) + assert _("Invalid") in str(e.value) + getattr(mock_object, pascal_name).assert_not_called() + + @pytest.mark.parametrize( + "pascal_name, property_name, return_type, expected_return, type_instance", + [("CreateEntityList", "create_entity_list", EntList, VALID_MOCK.ENT_LIST, "ent_list")], + ) + # pylint: disable-next=R0913, R0917 + def test_function_return_classes( + self, + mock_cad_diagnostic: CADDiagnostic, + mock_object, + pascal_name, + property_name, + return_type, + expected_return, + type_instance, + ): + """ + Test the method of the CADDiagnostic class. + + Args: + mock_cad_diagnostic: The mock instance of CADDiagnostic. + mock_object: The mock object for the CADDiagnostic dependency. + """ + expected_return_instance = getattr(expected_return, type_instance) + setattr(mock_object, pascal_name, expected_return_instance) + result = getattr(mock_cad_diagnostic, property_name)() + assert isinstance(result, return_type) + assert getattr(result, type_instance) == expected_return_instance + + @pytest.mark.parametrize( + "pascal_name, property_name", [("CreateEntityList", "create_entity_list")] + ) + # pylint: disable=R0913, R0917 + def test_function_return_none( + self, mock_cad_diagnostic: CADDiagnostic, mock_object, pascal_name, property_name + ): + """ + Test the return value of the function is None. + """ + setattr(mock_object, pascal_name, None) + result = getattr(mock_cad_diagnostic, property_name)() + assert result is None diff --git a/tests/api/unit_tests/test_unit_image_export_options.py b/tests/api/unit_tests/test_unit_image_export_options.py new file mode 100644 index 0000000..5bc242b --- /dev/null +++ b/tests/api/unit_tests/test_unit_image_export_options.py @@ -0,0 +1,198 @@ +""" +Test for ImageExportOptions Wrapper Class of moldflow-api module. +Test Details: + +Classes: + TestUnitImageExportOptions: Test suite for the ImageExportOptions class. +Fixtures: + mock_image_export_options: Fixture to create a mock instance of ImageExportOptions. +Test Methods: + +""" + +import pytest +from moldflow import ImageExportOptions, CaptureModes +from moldflow.logger import set_is_logging +from tests.conftest import ( + NON_NEGATIVE_INT, + VALID_STR, + VALID_BOOL, + INVALID_INT, + INVALID_BOOL, + INVALID_STR, + NEGATIVE_INT, +) + + +@pytest.mark.unit +class TestUnitImageExportOptions: + """ + Test suite for the ImageExportOptions class. + """ + + set_is_logging(False) + + @pytest.fixture + def mock_image_export_options(self, mock_object) -> ImageExportOptions: + """ + Fixture to create a mock instance of ImageExportOptions. + Args: + mock_object: Mock object for the ImageExportOptions dependency. + Returns: + ImageExportOptions: An instance of ImageExportOptions with the mock object. + """ + return ImageExportOptions(mock_object) + + @pytest.mark.parametrize( + "pascal_name, property_name, value,", + [("FileName", "file_name", x) for x in VALID_STR] + + [("SizeX", "size_x", x) for x in NON_NEGATIVE_INT] + + [("SizeY", "size_y", x) for x in NON_NEGATIVE_INT] + + [("ShowResult", "show_result", x) for x in VALID_BOOL] + + [("ShowLegend", "show_legend", x) for x in VALID_BOOL] + + [("ShowRotationAngle", "show_rotation_angle", x) for x in VALID_BOOL] + + [("ShowRotationAxes", "show_rotation_axes", x) for x in VALID_BOOL] + + [("ShowScaleBar", "show_scale_bar", x) for x in VALID_BOOL] + + [("ShowPlotInfo", "show_plot_info", x) for x in VALID_BOOL] + + [("ShowStudyTitle", "show_study_title", x) for x in VALID_BOOL] + + [("ShowRuler", "show_ruler", x) for x in VALID_BOOL] + + [("ShowHistogram", "show_histogram", x) for x in VALID_BOOL] + + [("ShowMinMax", "show_min_max", x) for x in VALID_BOOL] + + [("FitToScreen", "fit_to_screen", x) for x in VALID_BOOL] + + [("CaptureMode", "capture_mode", x.value) for x in CaptureModes], + ) + # pylint: disable-next=R0913, R0917 + def test_get_properties( + self, + mock_image_export_options: ImageExportOptions, + mock_object, + pascal_name, + property_name, + value, + ): + """ + Test Get properties of ImageExportOptions. + + Args: + mock_image_export_options: Instance of ImageExportOptions. + property_name: Name of the property to test. + pascal_name: Pascal case name of the property. + value: Value to set and check. + """ + setattr(mock_object, pascal_name, value) + result = getattr(mock_image_export_options, property_name) + assert isinstance(result, type(value)) + assert result == value + + @pytest.mark.parametrize( + "pascal_name, property_name, value, expected", + [ + ("FileName", "file_name", x, y) + for (x, y) in [("Test", "Test.png"), ("Test.jpg", "Test.jpg")] + ] + + [("SizeX", "size_x", x, x) for x in NON_NEGATIVE_INT] + + [("SizeY", "size_y", x, x) for x in NON_NEGATIVE_INT] + + [("ShowResult", "show_result", x, x) for x in VALID_BOOL] + + [("ShowLegend", "show_legend", x, x) for x in VALID_BOOL] + + [("ShowRotationAngle", "show_rotation_angle", x, x) for x in VALID_BOOL] + + [("ShowRotationAxes", "show_rotation_axes", x, x) for x in VALID_BOOL] + + [("ShowScaleBar", "show_scale_bar", x, x) for x in VALID_BOOL] + + [("ShowPlotInfo", "show_plot_info", x, x) for x in VALID_BOOL] + + [("ShowStudyTitle", "show_study_title", x, x) for x in VALID_BOOL] + + [("ShowRuler", "show_ruler", x, x) for x in VALID_BOOL] + + [("ShowHistogram", "show_histogram", x, x) for x in VALID_BOOL] + + [("ShowMinMax", "show_min_max", x, x) for x in VALID_BOOL] + + [("FitToScreen", "fit_to_screen", x, x) for x in VALID_BOOL] + + [("CaptureMode", "capture_mode", x, x.value) for x in CaptureModes], + ) + # pylint: disable-next=R0913, R0917 + def test_set_properties( + self, + mock_image_export_options: ImageExportOptions, + mock_object, + pascal_name, + property_name, + value, + expected, + ): + """ + Test properties of ImageExportOptions. + + Args: + mock_image_export_options: Instance of ImageExportOptions. + property_name: Name of the property to test. + pascal_name: Pascal case name of the property. + value: Value to set and check. + """ + setattr(mock_image_export_options, property_name, value) + result = getattr(mock_object, pascal_name) + assert isinstance(result, type(expected)) + assert result == expected + + @pytest.mark.parametrize( + "pascal_name, property_name, value", + [("FileName", "file_name", x) for x in INVALID_STR] + + [("SizeX", "size_x", x) for x in INVALID_INT] + + [("SizeY", "size_y", x) for x in INVALID_INT] + + [("ShowResult", "show_result", x) for x in INVALID_BOOL] + + [("ShowLegend", "show_legend", x) for x in INVALID_BOOL] + + [("ShowRotationAngle", "show_rotation_angle", x) for x in INVALID_BOOL] + + [("ShowRotationAxes", "show_rotation_axes", x) for x in INVALID_BOOL] + + [("ShowScaleBar", "show_scale_bar", x) for x in INVALID_BOOL] + + [("ShowPlotInfo", "show_plot_info", x) for x in INVALID_BOOL] + + [("ShowStudyTitle", "show_study_title", x) for x in INVALID_BOOL] + + [("ShowRuler", "show_ruler", x) for x in INVALID_BOOL] + + [("ShowHistogram", "show_histogram", x) for x in INVALID_BOOL] + + [("ShowMinMax", "show_min_max", x) for x in INVALID_BOOL] + + [("FitToScreen", "fit_to_screen", x) for x in INVALID_BOOL] + + [("CaptureMode", "capture_mode", x) for x in INVALID_INT], + ) + # pylint: disable-next=R0913, R0917 + def test_invalid_properties( + self, + mock_object, + mock_image_export_options: ImageExportOptions, + pascal_name, + property_name, + value, + _, + ): + """ + Test invalid properties of ImageExportOptions. + Args: + mock_image_export_options: Instance of ImageExportOptions. + property_name: Name of the property to test. + value: Invalid value to set and check. + """ + with pytest.raises(TypeError) as e: + setattr(mock_image_export_options, property_name, value) + assert _("Invalid") in str(e.value) + getattr(mock_object, pascal_name).assert_not_called() + + @pytest.mark.parametrize( + "pascal_name, property_name, value", + [("SizeX", "size_x", x) for x in NEGATIVE_INT] + + [("SizeY", "size_y", x) for x in NEGATIVE_INT] + + [("CaptureMode", "capture_mode", x) for x in NEGATIVE_INT + [3, 4]], + ) + # pylint: disable-next=R0913, R0917 + def test_invalid_value_properties( + self, + mock_object, + mock_image_export_options: ImageExportOptions, + pascal_name, + property_name, + value, + _, + ): + """ + Test invalid properties of ImageExportOptions. + Args: + mock_image_export_options: Instance of ImageExportOptions. + property_name: Name of the property to test. + value: Invalid value to set and check. + """ + with pytest.raises(ValueError) as e: + setattr(mock_image_export_options, property_name, value) + assert _("Invalid") in str(e.value) + getattr(mock_object, pascal_name).assert_not_called() diff --git a/tests/api/unit_tests/test_unit_import_options.py b/tests/api/unit_tests/test_unit_import_options.py index dede479..3dad174 100644 --- a/tests/api/unit_tests/test_unit_import_options.py +++ b/tests/api/unit_tests/test_unit_import_options.py @@ -7,7 +7,7 @@ import pytest from moldflow import ImportOptions -from moldflow import MeshType, ImportUnits, MDLKernel, MDLContactMeshType, CADBodyProperty +from moldflow import MeshType, ImportUnits, MDLContactMeshType, CADBodyProperty @pytest.mark.unit @@ -50,10 +50,6 @@ def mock_import_options(self, mock_object) -> ImportOptions: ("MDLSurfaces", "mdl_surfaces", False), ("UseMDL", "use_mdl", True), ("UseMDL", "use_mdl", False), - ("MDLKernel", "mdl_kernel", MDLKernel.PARAMETRIC), - ("MDLKernel", "mdl_kernel", MDLKernel.PARASOLID), - ("MDLKernel", "mdl_kernel", "Parametric"), - ("MDLKernel", "mdl_kernel", "Parasolid"), ("MDLAutoEdgeSelect", "mdl_auto_edge_select", True), ("MDLAutoEdgeSelect", "mdl_auto_edge_select", False), ("MDLEdgeLength", "mdl_edge_length", 0.1), @@ -125,10 +121,6 @@ def test_get_properties( ("MDLSurfaces", "mdl_surfaces", False, False), ("UseMDL", "use_mdl", True, True), ("UseMDL", "use_mdl", False, False), - ("MDLKernel", "mdl_kernel", MDLKernel.PARAMETRIC, "Parametric"), - ("MDLKernel", "mdl_kernel", MDLKernel.PARASOLID, "Parasolid"), - ("MDLKernel", "mdl_kernel", "Parametric", "Parametric"), - ("MDLKernel", "mdl_kernel", "Parasolid", "Parasolid"), ("MDLAutoEdgeSelect", "mdl_auto_edge_select", True, True), ("MDLAutoEdgeSelect", "mdl_auto_edge_select", False, False), ("MDLEdgeLength", "mdl_edge_length", 0.1, 0.1), @@ -210,7 +202,6 @@ def test_set_properties( ("mdl_mesh", 1), ("mdl_surfaces", 1), ("use_mdl", 1), - ("mdl_kernel", 1), ("mdl_auto_edge_select", 1), ("mdl_auto_edge_select", "Test"), ("mdl_edge_length", "Test"), @@ -227,7 +218,6 @@ def test_set_properties( ("mdl_mesh", None), ("mdl_surfaces", None), ("use_mdl", None), - ("mdl_kernel", None), ("mdl_auto_edge_select", None), ("mdl_edge_length", None), ("mdl_tetra_layers", None), @@ -257,7 +247,6 @@ def test_invalid_properties(self, mock_import_options: ImportOptions, property_n [ ("mesh_type", "Test"), ("units", "Test"), - ("mdl_kernel", "Test"), ("mdl_contact_mesh_type", "Test"), ("cad_body_property", 1), ], diff --git a/tests/api/unit_tests/test_unit_mesh_editor.py b/tests/api/unit_tests/test_unit_mesh_editor.py index 8a80551..c7e9675 100644 --- a/tests/api/unit_tests/test_unit_mesh_editor.py +++ b/tests/api/unit_tests/test_unit_mesh_editor.py @@ -724,6 +724,121 @@ def test_fill_hole_invalid(self, mock_mesh_editor, mock_object, tri, fill_hole, mock_object.FillHole.assert_not_called() mock_object.FillHole2.assert_not_called() + @pytest.mark.parametrize("expected", [1, 2, 3]) + def test_fill_hole_from_nodes(self, mock_mesh_editor, mock_object, expected): + """ + Test preferred nodes-based fill hole with MeshEditor + """ + mock_object.FillHoleFromNodes.return_value = expected + nodes = Mock(spec=EntList) + nodes.ent_list = Mock() + result = mock_mesh_editor.fill_hole_from_nodes(nodes) + assert isinstance(result, int) + assert result == expected + mock_object.FillHoleFromNodes.assert_called_once_with(nodes.ent_list) + + def test_fill_hole_from_nodes_none(self, mock_mesh_editor, mock_object): + """ + Test preferred nodes-based fill hole with None input (optional dispatch) + """ + with patch( + "moldflow.helper.variant_null_idispatch", + return_value=VARIANT(pythoncom.VT_DISPATCH, None), + ) as mock_func: + expected = 5 + mock_object.FillHoleFromNodes.return_value = expected + result = mock_mesh_editor.fill_hole_from_nodes(None) + assert isinstance(result, int) + assert result == expected + mock_object.FillHoleFromNodes.assert_called_once_with(mock_func()) + + @pytest.mark.parametrize("nodes", [1, 1.0, "String", True]) + def test_fill_hole_from_nodes_invalid(self, mock_mesh_editor, mock_object, nodes, _): + """ + Test preferred nodes-based fill hole invalid arguments + """ + with pytest.raises(TypeError) as e: + mock_mesh_editor.fill_hole_from_nodes(nodes) + assert _("Invalid") in str(e.value) + mock_object.FillHoleFromNodes.assert_not_called() + + @pytest.mark.parametrize("smooth, expected", [(True, 1), (True, 2), (False, 3)]) + def test_fill_hole_from_triangles(self, mock_mesh_editor, mock_object, smooth, expected): + """ + Test preferred triangles-based fill hole with MeshEditor + """ + mock_object.FillHoleFromTriangles.return_value = expected + triangles = Mock(spec=EntList) + triangles.ent_list = Mock() + result = mock_mesh_editor.fill_hole_from_triangles(triangles, smooth) + assert isinstance(result, int) + assert result == expected + # COM now expects a boolean smoothing flag; ints map with (value != 2) + mock_object.FillHoleFromTriangles.assert_called_once_with(triangles.ent_list, smooth) + + @pytest.mark.parametrize("value, expected_bool", [(True, True), (False, False)]) + def test_fill_hole_from_triangles_bool( + self, mock_mesh_editor, mock_object, value, expected_bool, _ + ): + """Test boolean smoothing flag is forwarded to COM as-is.""" + mock_object.FillHoleFromTriangles.return_value = 11 + triangles = Mock(spec=EntList) + triangles.ent_list = Mock() + result = mock_mesh_editor.fill_hole_from_triangles(triangles, value) + assert isinstance(result, int) + assert result == 11 + mock_object.FillHoleFromTriangles.assert_called_once_with(triangles.ent_list, expected_bool) + + @pytest.mark.parametrize("value", [("BENT",)]) # string not accepted + def test_fill_hole_from_triangles_invalid_enum_string( + self, mock_mesh_editor, mock_object, value, _ + ): + """Invalid string input should raise a TypeError and not call COM.""" + with pytest.raises(TypeError): + mock_mesh_editor.fill_hole_from_triangles(Mock(spec=EntList), value) + mock_object.FillHoleFromTriangles.assert_not_called() + + # Enum is no longer supported; keep a simple int mapping test via legacy path removed + + def test_fill_hole_from_triangles_none(self, mock_mesh_editor, mock_object): + """ + Test preferred triangles-based fill hole with None tri list (optional dispatch) + """ + with patch( + "moldflow.helper.variant_null_idispatch", + return_value=VARIANT(pythoncom.VT_DISPATCH, None), + ) as mock_func: + expected = 7 + fill_type = True + mock_object.FillHoleFromTriangles.return_value = expected + result = mock_mesh_editor.fill_hole_from_triangles(None, fill_type) + assert isinstance(result, int) + assert result == expected + mock_object.FillHoleFromTriangles.assert_called_once_with(mock_func(), True) + + @pytest.mark.parametrize( + "triangles, fill_type", + [ + (1, 0), + (1.0, 0), + ("String", 0), + (Mock(spec=EntList), None), + (Mock(spec=EntList), 1.0), + (Mock(spec=EntList), "String"), + # bool is accepted now for smoothing control + ], + ) + def test_fill_hole_from_triangles_invalid( + self, mock_mesh_editor, mock_object, triangles, fill_type, _ + ): + """ + Test preferred triangles-based fill hole invalid arguments + """ + with pytest.raises(TypeError) as e: + mock_mesh_editor.fill_hole_from_triangles(triangles, fill_type) + assert _("Invalid") in str(e.value) + mock_object.FillHoleFromTriangles.assert_not_called() + @pytest.mark.parametrize("property_value", [Mock(spec=Property), None]) def test_create_tet(self, mock_mesh_editor, mock_object, property_value): """ diff --git a/tests/api/unit_tests/test_unit_plot_manager.py b/tests/api/unit_tests/test_unit_plot_manager.py index 5ff4a61..1a08e07 100644 --- a/tests/api/unit_tests/test_unit_plot_manager.py +++ b/tests/api/unit_tests/test_unit_plot_manager.py @@ -1018,7 +1018,8 @@ def test_functions_none( VALID_FLOAT, INVALID_STR, ) - ], + ] + + [("ExportToVTK", "export_to_vtk", ("export.vtk", x)) for x in INVALID_BOOL], ) # pylint: disable-next=R0913, R0917 def test_functions_invalid_type( @@ -1268,6 +1269,10 @@ def test_save_functions2_save_error( VALID_FLOAT, SystemUnits, ) + ] + + [ + ("ExportToVTK", "export_to_vtk", ("sample.vtk", x), ("sample.vtk", x)) + for x in VALID_BOOL ], ) # pylint: disable-next=R0913, R0917 @@ -1315,7 +1320,8 @@ def test_save_functions( VALID_FLOAT, SystemUnits, ) - ], + ] + + [("ExportToVTK", "export_to_vtk", ("sample.vtk", x)) for x in VALID_BOOL], ) # pylint: disable-next=R0913, R0917 def test_save_functions_save_error( diff --git a/tests/api/unit_tests/test_unit_study_doc.py b/tests/api/unit_tests/test_unit_study_doc.py index 53961d2..c374de6 100644 --- a/tests/api/unit_tests/test_unit_study_doc.py +++ b/tests/api/unit_tests/test_unit_study_doc.py @@ -9,7 +9,7 @@ import pytest from moldflow import StudyDoc, ImportOptions, EntList, StringArray, Vector from tests.api.unit_tests.conftest import VALID_MOCK -from tests.conftest import NON_NEGATIVE_INT, VALID_STR, VALID_BOOL +from tests.conftest import NON_NEGATIVE_INT, VALID_STR, VALID_BOOL, pad_and_zip, INVALID_BOOL @pytest.mark.unit @@ -51,6 +51,7 @@ def mock_ent_list(self) -> EntList: ("mesh_type", "MeshType", "3D"), ("number_of_analyses", "NumberOfAnalyses", "TestStudy"), ("study_name", "StudyName", "3D"), + ("display_name", "DisplayName", "3D"), ("notes", "GetNotes", "This is a test note."), ], ) @@ -686,13 +687,15 @@ def test_function_return_none( "pascal_name, property_name, args, expected_args, return_type, return_value", [ ("AnalysisStatus", "analysis_status", (x,), (x,), str, y) - for x in NON_NEGATIVE_INT - for y in VALID_STR + for x, y in pad_and_zip(NON_NEGATIVE_INT, VALID_STR) ] + [ ("AnalysisName", "analysis_name", (x,), (x,), str, y) - for x in NON_NEGATIVE_INT - for y in VALID_STR + for x, y in pad_and_zip(NON_NEGATIVE_INT, VALID_STR) + ] + + [ + ("GetAllCadBodies", "get_all_cad_bodies", (x,), (x,), str, y) + for x, y in pad_and_zip(VALID_BOOL, VALID_STR) ], ) # pylint: disable=R0913, R0917 @@ -716,6 +719,22 @@ def test_function( assert result == return_value getattr(mock_object, pascal_name).assert_called_once_with(*expected_args) + @pytest.mark.parametrize( + "pascal_name, property_name, args", + [("GetAllCadBodies", "get_all_cad_bodies", (x,)) for x in pad_and_zip(INVALID_BOOL)], + ) + # pylint: disable=R0913, R0917 + def test_function_invalid_type( + self, mock_study_doc: StudyDoc, mock_object, pascal_name, property_name, args, _ + ): + """ + Test the function with invalid types. + """ + with pytest.raises(TypeError) as e: + getattr(mock_study_doc, property_name)(*args) + assert _("Invalid") in str(e.value) + getattr(mock_object, pascal_name).assert_not_called() + @pytest.mark.parametrize( "pascal_name, property_name, return_type, return_value", [("MeshStatus", "mesh_status", str, x) for x in VALID_STR] diff --git a/tests/api/unit_tests/test_unit_synergy.py b/tests/api/unit_tests/test_unit_synergy.py index cc908ef..dcdf0c5 100644 --- a/tests/api/unit_tests/test_unit_synergy.py +++ b/tests/api/unit_tests/test_unit_synergy.py @@ -17,6 +17,7 @@ import pytest from moldflow import ( BoundaryConditions, + CADDiagnostic, CADManager, CircuitGenerator, DataTransform, @@ -238,7 +239,8 @@ def test_import_file_no_import_options(self, mock_synergy: Synergy, mock_object) @pytest.mark.parametrize( "pascal_name, property_name, args", - [("Silence", "silence", (x,)) for x in pad_and_zip(INVALID_BOOL)] + [("Log", "log", (x,)) for x in pad_and_zip(INVALID_STR)] + + [("Silence", "silence", (x,)) for x in pad_and_zip(INVALID_BOOL)] + [("NewProject", "new_project", (x, y)) for x, y in pad_and_zip(INVALID_STR, VALID_STR)] + [("NewProject", "new_project", (x, y)) for x, y in pad_and_zip(VALID_STR, INVALID_STR)] + [("OpenProject", "open_project", (x,)) for x in pad_and_zip(INVALID_STR)] @@ -301,7 +303,8 @@ def test_quit(self, mock_synergy: Synergy, mock_object): @pytest.mark.parametrize( "pascal_name, property_name, args, expected_args", - [("Quit", "quit", (x,), (x,)) for x in pad_and_zip(VALID_BOOL)], + [("Log", "log", (x,), (x,)) for x in pad_and_zip(VALID_STR)] + + [("Quit", "quit", (x,), (x,)) for x in pad_and_zip(VALID_BOOL)], ) # pylint: disable-next=R0913, R0917 def test_functions_no_return( @@ -530,6 +533,13 @@ def test_properties_invalid_type(self, mock_synergy: Synergy, property_name, val VALID_MOCK.BOUNDARY_CONDITIONS, "boundary_conditions", ), + ( + "CADDiagnostic", + "cad_diagnostic", + CADDiagnostic, + VALID_MOCK.CAD_DIAGNOSTIC, + "cad_diagnostic", + ), ("CADManager", "cad_manager", CADManager, VALID_MOCK.CAD_MANAGER, "cad_manager"), ( "CircuitGenerator", @@ -684,6 +694,7 @@ def test_properties_return_classes( "pascal_name, property_name", [ ("BoundaryConditions", "boundary_conditions"), + ("CADDiagnostic", "cad_diagnostic"), ("CADManager", "cad_manager"), ("CircuitGenerator", "circuit_generator"), ("DataTransform", "data_transform"), diff --git a/tests/api/unit_tests/test_unit_viewer.py b/tests/api/unit_tests/test_unit_viewer.py index b2982c1..bfd92ab 100644 --- a/tests/api/unit_tests/test_unit_viewer.py +++ b/tests/api/unit_tests/test_unit_viewer.py @@ -7,19 +7,17 @@ """ import pytest -from moldflow import Viewer, Plot +from moldflow import DoubleArray, EntList, ImageExportOptions, Vector, Viewer, Plot from moldflow.common import ViewModes, StandardViews, AnimationSpeed from moldflow.constants import ( MP4_FILE_EXT, + GIF_FILE_EXT, JPG_FILE_EXT, JPEG_FILE_EXT, PNG_FILE_EXT, BMP_FILE_EXT, TIF_FILE_EXT, ) -from moldflow.double_array import DoubleArray -from moldflow.ent_list import EntList -from moldflow.vector import Vector from tests.api.unit_tests.conftest import VALID_MOCK, INVALID_MOCK from tests.conftest import ( INVALID_BOOL, @@ -391,8 +389,16 @@ def test_function_no_return( for x in pad_and_zip(VALID_MOCK.DOUBLE_ARRAY) ] + [("get_histogram_location", "GetHistogramLocation", None, None, None)] + + [("is_play_animation", "IsPlayAnimation", bool, None, x) for x in pad_and_zip(VALID_BOOL)] + [ - ("is_play_animation", "IsPlayAnimation", bool, None, x) for x in pad_and_zip(VALID_BOOL) + ( + "image_export_options", + "ImageExportOptions", + ImageExportOptions, + "image_export_options", + a, + ) + for a in pad_and_zip(VALID_MOCK.IMAGE_EXPORT_OPTIONS) ], ) # pylint: disable=R0913,R0917 @@ -433,6 +439,18 @@ def test_function_no_args( [x + MP4_FILE_EXT for x in VALID_STR], AnimationSpeed, VALID_BOOL, VALID_BOOL ) ] + + [ + ("save_animation", "SaveAnimation3", (a, b, c), (a, b.value, c), bool, None, d) + for a, b, c, d in pad_and_zip( + [x + GIF_FILE_EXT for x in VALID_STR], AnimationSpeed, VALID_BOOL, VALID_BOOL + ) + ] + + [ + ("save_animation", "SaveAnimation3", (a, b.value, c), (a, b.value, c), bool, None, d) + for a, b, c, d in pad_and_zip( + [x + GIF_FILE_EXT for x in VALID_STR], AnimationSpeed, VALID_BOOL, VALID_BOOL + ) + ] + [ ("save_plot_scale_image", "SavePlotScaleImage", (a,), (a,), bool, None, b) for a, b in pad_and_zip(VALID_STR, VALID_BOOL) @@ -475,6 +493,30 @@ def test_function_no_args( VALID_BOOL, ) ] + + [ + ( + "save_image_with_options", + "SaveImage5", + (a,), + (a.image_export_options,), + bool, + None, + b, + ) + for a, b in pad_and_zip(VALID_MOCK.IMAGE_EXPORT_OPTIONS, VALID_BOOL) + ] + + [ + ( + "save_animation_with_options", + "SaveAnimation4", + (a,), + (a.animation_export_options,), + bool, + None, + b, + ) + for a, b in pad_and_zip(VALID_MOCK.ANIMATION_EXPORT_OPTIONS, VALID_BOOL) + ] + [ ("save_image_legacy", "SaveImage", (a,), (a,), bool, None, True) for a in [ @@ -765,6 +807,19 @@ def test_property_set( ) ) ] + + [ + ("save_image_with_options", "SaveImage5", [VALID_MOCK.IMAGE_EXPORT_OPTIONS], x) + for x in ((index, value) for index, value in enumerate([INVALID_MOCK])) + ] + + [ + ( + "save_animation_with_options", + "SaveAnimation4", + [VALID_MOCK.ANIMATION_EXPORT_OPTIONS], + x, + ) + for x in ((index, value) for index, value in enumerate([INVALID_MOCK])) + ] + [ ( "save_animation", @@ -777,6 +832,18 @@ def test_property_set( for index, value in enumerate([INVALID_STR, INVALID_STR, INVALID_BOOL]) ) ] + + [ + ( + "save_animation", + "SaveAnimation3", + [VALID_STR[0] + GIF_FILE_EXT, AnimationSpeed.FAST.value, VALID_BOOL[0]], + x, + ) + for x in ( + (index, value) + for index, value in enumerate([INVALID_STR, INVALID_STR, INVALID_BOOL]) + ) + ] + [ ( "enable_clipping_plane_by_id", diff --git a/tests/core/test_helper.py b/tests/core/test_helper.py index a1719fa..6c7155e 100644 --- a/tests/core/test_helper.py +++ b/tests/core/test_helper.py @@ -257,6 +257,8 @@ def test_check_index_invalid(self, index, min_value, max_value, _): ("test.txt", (".txt", ".csv")), ("test.txt", ".txt"), ("test\\test.png", (".txt", ".csv", ".png")), + ("animation.mp4", (".mp4", ".gif")), + ("animation.gif", (".mp4", ".gif")), ], ) def test_check_file_extension(self, file_name, extensions, _, caplog):