From 7b38ba7e90e506e71cbbb9d9f2a6266ee30e68f4 Mon Sep 17 00:00:00 2001 From: osinjoku <49887472+osinjoku@users.noreply.github.com> Date: Fri, 12 Sep 2025 23:49:18 +1000 Subject: [PATCH 01/26] Fix return types and color band range (#5) * fix types and remove non-existing function * fix tests * fix color band range * increment patch number * fix range and tests * changelog * bring back function * more tests * missing include * typo * better docstrings * correctness * ws --- CHANGELOG.md | 36 +++++++++++++------ src/moldflow/constants.py | 2 +- src/moldflow/double_array.py | 7 ++-- src/moldflow/integer_array.py | 7 ++-- src/moldflow/plot.py | 15 ++++---- src/moldflow/string_array.py | 8 +++-- .../api/unit_tests/test_unit_double_array.py | 6 +++- .../api/unit_tests/test_unit_integer_array.py | 5 ++- tests/api/unit_tests/test_unit_plot.py | 2 +- .../api/unit_tests/test_unit_string_array.py | 5 ++- version.json | 2 +- 11 files changed, 63 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8cb0c1..fcf9136 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- Initial public release -- Python API wrapper for Moldflow Synergy -- Comprehensive test suite -- Documentation with examples -- CI/CD pipeline for automated testing and publishing +- N/A ### Changed - N/A @@ -29,13 +25,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security - N/A -## [26.0.0] - 2025-XX-XX +## [26.0.1] - 2025-09-12 + +### Added +- N/A + +### Changed +- N/A + +### Deprecated +- N/A + +### Removed +- N/A + +### Fixed +- Fix return types for `from_list` functions in data classes +- Fix color band range options to 1 to 256 + +### Security +- N/A + +## [26.0.0] - 2025-09-01 ### Added -- Initial version aligned with Moldflow Synergy 2026 -- Basic API functionality -- Windows support +- 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.0...HEAD +[Unreleased]: https://github.com/Autodesk/moldflow-api/compare/v26.0.1...HEAD +[26.0.1]: https://github.com/Autodesk/moldflow-api/releases/tag/v26.0.1 [26.0.0]: https://github.com/Autodesk/moldflow-api/releases/tag/v26.0.0 diff --git a/src/moldflow/constants.py b/src/moldflow/constants.py index c785166..4851f3a 100644 --- a/src/moldflow/constants.py +++ b/src/moldflow/constants.py @@ -7,7 +7,7 @@ import os # Constants for color bands -COLOR_BAND_RANGE = tuple(list(range(2, 65)) + [256]) +COLOR_BAND_RANGE = tuple(range(1, 257)) # Localization constants DEFAULT_THREE_LETTER_CODE = "enu" diff --git a/src/moldflow/double_array.py b/src/moldflow/double_array.py index e2d16c5..be3cd28 100644 --- a/src/moldflow/double_array.py +++ b/src/moldflow/double_array.py @@ -67,12 +67,15 @@ def to_list(self) -> list[float]: vb_array = self.double_array.ToVBSArray() return list(vb_array) - def from_list(self, values: list[float]) -> None: + def from_list(self, values: list[float]) -> int: """ Convert a list of floats to a double array. Args: values (list[float]): The list of floats to convert. + + Returns: + int: The number of elements added to the array. """ process_log(__name__, LogMessage.FUNCTION_CALL, locals(), name="from_list") @@ -80,7 +83,7 @@ def from_list(self, values: list[float]) -> None: for value in values: check_type(value, (int, float)) - self.double_array.FromVBSArray(list(values)) + return self.double_array.FromVBSArray(list(values)) @property def size(self) -> int: diff --git a/src/moldflow/integer_array.py b/src/moldflow/integer_array.py index f28221c..ab4c4a4 100644 --- a/src/moldflow/integer_array.py +++ b/src/moldflow/integer_array.py @@ -67,12 +67,15 @@ def to_list(self) -> list[int]: vb_array = self.integer_array.ToVBSArray() return list(vb_array) - def from_list(self, values: list[int]) -> None: + def from_list(self, values: list[int]) -> int: """ Convert a list of integers to an integer array. Args: values (list[int]): The list of integers to convert. + + Returns: + int: The number of elements added to the array. """ process_log(__name__, LogMessage.FUNCTION_CALL, locals(), name="from_list") @@ -80,7 +83,7 @@ def from_list(self, values: list[int]) -> None: for value in values: check_type(value, int) - self.integer_array.FromVBSArray(list(values)) + return self.integer_array.FromVBSArray(list(values)) @property def size(self) -> int: diff --git a/src/moldflow/plot.py b/src/moldflow/plot.py index 1baeb13..0cdbf24 100644 --- a/src/moldflow/plot.py +++ b/src/moldflow/plot.py @@ -456,13 +456,11 @@ def extended_color(self, value: bool) -> None: @property def color_bands(self) -> int: """ - The number of color bands or smooth coloring. - - values between 2 through 64: banded coloring with this number of colors - - 256: smooth coloring + The number of color bands. + - values between 1 through 256: banded coloring with this number of colors - - :getter: Get the number of color bands or smooth coloring. - :setter: Set the number of color bands or smooth coloring. + :getter: Get the number of color bands. + :setter: Set the number of color bands. :type: int """ process_log(__name__, LogMessage.PROPERTY_GET, locals(), name="color_bands") @@ -471,9 +469,8 @@ def color_bands(self) -> int: @color_bands.setter def color_bands(self, value: int) -> None: """ - The number of color bands or smooth coloring. - - values between 2 through 64: banded coloring with this number of colors - - 256: smooth coloring + The number of color bands. + - values between 1 through 256: banded coloring with this number of colors Args: value (int): number of color bands or smooth coloring to set. diff --git a/src/moldflow/string_array.py b/src/moldflow/string_array.py index ca18c30..dcb953d 100644 --- a/src/moldflow/string_array.py +++ b/src/moldflow/string_array.py @@ -57,19 +57,21 @@ def add_string(self, value: str) -> None: def to_list(self) -> list[str]: """ Convert the string array to a list of strings. - Returns: list[str]: The list of strings. """ process_log(__name__, LogMessage.FUNCTION_CALL, locals(), name="to_list") return _mf_array_to_list(self) - def from_list(self, values: list[str]) -> None: + def from_list(self, values: list[str]) -> int: """ Convert a list of strings to a string array. Args: values (list[str]): The list of strings to convert. + + Returns: + int: The number of elements added to the array. """ process_log(__name__, LogMessage.FUNCTION_CALL, locals(), name="from_list") check_type(values, (list, tuple)) @@ -77,7 +79,7 @@ def from_list(self, values: list[str]) -> None: for value in values: check_type(value, str) - self.string_array.FromVBSArray(list(values)) + return self.string_array.FromVBSArray(list(values)) @property def size(self) -> int: diff --git a/tests/api/unit_tests/test_unit_double_array.py b/tests/api/unit_tests/test_unit_double_array.py index d19a2e6..37d913b 100644 --- a/tests/api/unit_tests/test_unit_double_array.py +++ b/tests/api/unit_tests/test_unit_double_array.py @@ -72,7 +72,11 @@ def test_to_list(self, mock_double_array, mock_object, values): # pylint: disable=R0801 def test_from_list(self, mock_double_array, mock_object, values): """Test the from_list method of the DoubleArray class.""" - mock_double_array.from_list(values) + mock_object.FromVBSArray.return_value = len(values) + result = mock_double_array.from_list(values) + + assert isinstance(result, int) + assert result == len(values) mock_object.FromVBSArray.assert_called_once_with(list(values)) @pytest.mark.parametrize("invalid_values", INVALID_MOCK_WITH_NONE) diff --git a/tests/api/unit_tests/test_unit_integer_array.py b/tests/api/unit_tests/test_unit_integer_array.py index a6b838a..05a8aa8 100644 --- a/tests/api/unit_tests/test_unit_integer_array.py +++ b/tests/api/unit_tests/test_unit_integer_array.py @@ -65,8 +65,11 @@ def test_to_list(self, mock_integer_array, mock_object, values): # pylint: disable=R0801 def test_from_list(self, mock_integer_array, mock_object, values): """Test the from_list method of the IntegerArray class.""" - mock_integer_array.from_list(values) + mock_object.FromVBSArray.return_value = len(values) + result = mock_integer_array.from_list(values) + assert isinstance(result, int) + assert result == len(values) mock_object.FromVBSArray.assert_called_once_with(list(values)) @pytest.mark.parametrize("invalid_values", INVALID_MOCK_WITH_NONE) diff --git a/tests/api/unit_tests/test_unit_plot.py b/tests/api/unit_tests/test_unit_plot.py index 81870c1..1ffe6c1 100644 --- a/tests/api/unit_tests/test_unit_plot.py +++ b/tests/api/unit_tests/test_unit_plot.py @@ -441,7 +441,7 @@ def test_invalid_properties( @pytest.mark.parametrize( "pascal_name, property_name, value", [("SetMeshFill", "mesh_fill", x) for x in [-1.0, 2.0, 3.0]] - + [("SetColorBands", "color_bands", x) for x in [1, 65, 128]] + + [("SetColorBands", "color_bands", x) for x in [-1, 0, 257, 300]] + [("SetHistogramNumberOfBars", "histogram_number_of_bars", x) for x in [-1, -2, -3, -4]], ) # pylint: disable-next=R0913, R0917 diff --git a/tests/api/unit_tests/test_unit_string_array.py b/tests/api/unit_tests/test_unit_string_array.py index 05fe8de..fca95fa 100644 --- a/tests/api/unit_tests/test_unit_string_array.py +++ b/tests/api/unit_tests/test_unit_string_array.py @@ -82,8 +82,11 @@ def test_to_list(self, mock_string_array, mock_object, size, values): # pylint: disable=R0801 def test_from_list(self, mock_string_array, mock_object, values): """Test the from_list method of the StringArray class.""" - mock_string_array.from_list(values) + mock_object.FromVBSArray.return_value = len(values) + result = mock_string_array.from_list(values) + assert isinstance(result, int) + assert result == len(values) mock_object.FromVBSArray.assert_called_once() @pytest.mark.parametrize("invalid_values", INVALID_MOCK_WITH_NONE) diff --git a/version.json b/version.json index 38caf99..9e2172b 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { "major": "26", "minor": "0", - "patch": "0" + "patch": "1" } From cb5f8d19dbff42497511e8d9627927507bb53355 Mon Sep 17 00:00:00 2001 From: osinjoku <49887472+osinjoku@users.noreply.github.com> Date: Mon, 22 Sep 2025 15:41:46 +1000 Subject: [PATCH 02/26] point to pypi (#3) --- .github/workflows/publish.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c07ad12..ad6cd11 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -84,7 +84,7 @@ jobs: $packageName = 'moldflow' try { - $resp = Invoke-WebRequest -Uri "https://test.pypi.org/pypi/$packageName/json" -UseBasicParsing -ErrorAction Stop + $resp = Invoke-WebRequest -Uri "https://pypi.org/pypi/$packageName/json" -UseBasicParsing -ErrorAction Stop $data = $resp.Content | ConvertFrom-Json $versions = @($data.releases.PSObject.Properties.Name) } catch { @@ -118,9 +118,9 @@ jobs: if: steps.pypi_check.outputs.exists == 'false' env: TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }} + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} run: | - python run.py publish --skip-build --testpypi + python run.py publish --skip-build - name: Create GitHub Release if: steps.pypi_check.outputs.exists == 'false' From de531bf178cb9ecb69004aced283f0c8fe973b01 Mon Sep 17 00:00:00 2001 From: osinjoku <49887472+osinjoku@users.noreply.github.com> Date: Fri, 10 Oct 2025 15:35:44 +1100 Subject: [PATCH 03/26] Bring relevant changes to the v26 release branch (#24) * Improve examples in docs (#16) * Fix return types and color band range (#5) * fix types and remove non-existing function * fix tests * fix color band range * increment patch number * fix range and tests * changelog * bring back function * more tests * missing include * typo * better docstrings * correctness * ws * docs only publish * more examples * point to pypi (#3) * remove multi-statement lines * remove vb references * better explanation * simpler * cleanup and make sure the scripts work * Update readme.rst * remove unnecessary fallback * Add convenience message boxes and input dialogs via Win32 (#15) * progress * order * allow ok for localisation * add integration test * format * lint * fix test * working * lint * i18n * format * lint * fnish lint * review * placement * omit test coverage for win32 ui heavy class * add to changelog * fix coverage * more friendly win32 error handling * Update src/moldflow/message_box.py Co-authored-by: Sankalp Shrivastava --------- Co-authored-by: Sankalp Shrivastava * Improve MessageBox documentation (#21) * progress * order * allow ok for localisation * add integration test * format * lint * fix test * working * lint * i18n * format * lint * fnish lint * review * placement * omit test coverage for win32 ui heavy class * add to changelog * fix coverage * more friendly win32 error handling * Update src/moldflow/message_box.py Co-authored-by: Sankalp Shrivastava * better doc --------- Co-authored-by: Sankalp Shrivastava * bump version * update changelog --------- Co-authored-by: Sankalp Shrivastava --- .coverage-config | 6 + .github/workflows/publish.yml | 22 +- CHANGELOG.md | 24 +- .../source/components/wrapper/message_box.rst | 139 ++ docs/source/readme.rst | 442 +++++- scripts/check_localization.py | 13 +- src/moldflow/__init__.py | 12 +- .../locale/de-DE/LC_MESSAGES/locale.de-DE.po | 9 + .../locale/en-US/LC_MESSAGES/locale.en-US.po | 9 + .../locale/es-ES/LC_MESSAGES/locale.es-ES.po | 9 + .../locale/fr-FR/LC_MESSAGES/locale.fr-FR.po | 9 + .../locale/it-IT/LC_MESSAGES/locale.it-IT.po | 9 + .../locale/ja-JP/LC_MESSAGES/locale.ja-JP.po | 9 + .../locale/ko-KR/LC_MESSAGES/locale.ko-KR.po | 9 + .../locale/pt-PT/LC_MESSAGES/locale.pt-PT.po | 9 + .../locale/zh-CN/LC_MESSAGES/locale.zh-CN.po | 9 + .../locale/zh-TW/LC_MESSAGES/locale.zh-TW.po | 9 + src/moldflow/message_box.py | 1379 +++++++++++++++++ .../test_message_box_permutations.py | 196 +++ version.json | 2 +- 20 files changed, 2302 insertions(+), 23 deletions(-) create mode 100644 docs/source/components/wrapper/message_box.rst create mode 100644 src/moldflow/message_box.py create mode 100644 tests/api/integration_tests/test_message_box_permutations.py diff --git a/.coverage-config b/.coverage-config index 290b639..742b67f 100644 --- a/.coverage-config +++ b/.coverage-config @@ -2,6 +2,9 @@ branch = True source_pkgs = moldflow +omit = + src/moldflow/message_box.py + */site-packages/moldflow/message_box.py [paths] source = @@ -12,6 +15,9 @@ source = fail_under = 93 show_missing = True precision = 2 +omit = + src/moldflow/message_box.py + */site-packages/moldflow/message_box.py [html] title = Moldflow API Unit Test Coverage diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ad6cd11..18a5e7c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,6 +7,11 @@ on: description: "Confirm publish package" required: true type: boolean + docs_only: + description: "Only publish documentation (skip package publish)" + required: false + type: boolean + default: false jobs: guard-ci-success: @@ -77,9 +82,16 @@ jobs: with: python-version: '3.13' + - name: Install dependencies for docs + if: ${{ github.event.inputs.docs_only == 'true' }} + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Check if version exists on PyPI id: pypi_check shell: pwsh + if: ${{ github.event.inputs.docs_only != 'true' }} run: | $packageName = 'moldflow' @@ -100,22 +112,22 @@ jobs: "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 - name: Skip publish, version already exists - if: steps.pypi_check.outputs.exists == 'true' + if: ${{ github.event.inputs.docs_only != 'true' && steps.pypi_check.outputs.exists == 'true' }} run: Write-Output "Version ${{ steps.pypi_check.outputs.version }} already exists on PyPI. Skipping publish." - name: Install build dependencies - if: steps.pypi_check.outputs.exists == 'false' + if: ${{ github.event.inputs.docs_only != 'true' && steps.pypi_check.outputs.exists == 'false' }} run: | python -m pip install --upgrade pip pip install -r requirements.txt - name: Build package - if: steps.pypi_check.outputs.exists == 'false' + if: ${{ github.event.inputs.docs_only != 'true' && steps.pypi_check.outputs.exists == 'false' }} run: | python run.py build - name: Publish to PyPI - if: steps.pypi_check.outputs.exists == 'false' + if: ${{ github.event.inputs.docs_only != 'true' && steps.pypi_check.outputs.exists == 'false' }} env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} @@ -123,7 +135,7 @@ jobs: python run.py publish --skip-build - name: Create GitHub Release - if: steps.pypi_check.outputs.exists == 'false' + if: ${{ github.event.inputs.docs_only != 'true' && steps.pypi_check.outputs.exists == 'false' }} run: | python run.py release env: diff --git a/CHANGELOG.md b/CHANGELOG.md index fcf9136..9806e3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security - N/A +## [26.0.2] - 2025-10-10 + +### Added +- Added convenience class for showing message boxes and text input dialogs via Win32 +- Add more examples in the documentation + +### Changed +- N/A + +### Deprecated +- N/A + +### Removed +- N/A + +### Fixed +- N/A + +### Security +- N/A + ## [26.0.1] - 2025-09-12 ### Added @@ -52,6 +73,7 @@ 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.1...HEAD +[Unreleased]: https://github.com/Autodesk/moldflow-api/compare/v26.0.2...HEAD +[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 [26.0.0]: https://github.com/Autodesk/moldflow-api/releases/tag/v26.0.0 diff --git a/docs/source/components/wrapper/message_box.rst b/docs/source/components/wrapper/message_box.rst new file mode 100644 index 0000000..7644b3c --- /dev/null +++ b/docs/source/components/wrapper/message_box.rst @@ -0,0 +1,139 @@ +MessageBox +========== + +Convenience wrapper to display message boxes and a simple +text input dialog from Python scripts using the ``moldflow`` package. + +Usage +----- + +.. code-block:: python + + from moldflow import ( + MessageBox, + MessageBoxType, + MessageBoxResult, + MessageBoxOptions, + MessageBoxIcon, + MessageBoxDefaultButton, + MessageBoxModality, + ) + + # Informational message + MessageBox("Operation completed.", MessageBoxType.INFO).show() + + # Confirmation + result = MessageBox("Proceed with analysis?", MessageBoxType.YES_NO).show() + if result == MessageBoxResult.YES: + pass + + # Text input + material_id = MessageBox("Enter your material ID:", MessageBoxType.INPUT).show() + if material_id: + pass + + # Advanced options + opts = MessageBoxOptions( + icon=MessageBoxIcon.WARNING, + default_button=MessageBoxDefaultButton.BUTTON2, + modality=MessageBoxModality.TASK, + topmost=True, + right_align=False, + rtl_reading=False, + help_button=False, + set_foreground=True, + owner_hwnd=None, + ) + result = MessageBox( + "Retry failed operation?", + MessageBoxType.RETRY_CANCEL, + title="Moldflow", + options=opts, + ).show() + +Convenience methods +------------------- + +.. code-block:: python + + MessageBox.info("Saved") + MessageBox.warning("Low disk space") + MessageBox.error("Failed to save") + if MessageBox.confirm_yes_no("Proceed?") == MessageBoxResult.YES: + pass + + # Prompt text with validation + def is_nonempty(s: str) -> bool: + return bool(s.strip()) + + value = MessageBox.prompt_text( + "Enter ID:", + default_text="", + placeholder="e.g. MAT-123", + validator=is_nonempty, + ) + if value is not None: + pass + +Options +------- + +.. list-table:: MessageBoxOptions + :header-rows: 1 + + * - Parameter + - Type + - Description + * - icon + - MessageBoxIcon | None + - Override default icon + * - default_button + - MessageBoxDefaultButton | None + - Set default button (2/3/4). Validated vs type + * - modality + - MessageBoxModality | None + - Application (default), Task-modal, System-modal + * - topmost + - bool + - Keep message box on top (standard MessageBox only) + * - set_foreground + - bool + - Force foreground (standard MessageBox only) + * - right_align / rtl_reading + - bool + - Layout flags for right-to-left locales (standard MessageBox only) + * - help_button + - bool + - Show Help button + * - owner_hwnd + - int | None + - Owner window handle (standard MessageBox only, improves modality/Z-order) + * - default_text / placeholder + - str | None + - Prefill text and cue banner for input dialog + * - is_password + - bool + - Mask input characters + * - char_limit + - int | None + - Maximum characters accepted (client-side) + * - width_dlu / height_dlu + - int | None + - Size the input dialog (pixels; DLUs in legacy template path) + * - validator + - Callable[[str], bool] | None + - Enable OK only when input satisfies predicate + * - font_face / font_size_pt + - str / int + - Font for legacy template; CreateWindowEx path uses system dialog font + +API +--- + +.. automodule:: moldflow.message_box + +Notes +----- + +- Localization: action button captions (e.g., "OK", "Cancel", "Submit") are localized via the package i18n system. Title and prompt are not localized automatically. +- Return type: ``MessageBox.show()`` returns ``MessageBoxReturn`` (``MessageBoxResult | str | None``). diff --git a/docs/source/readme.rst b/docs/source/readme.rst index a6e47b9..8abbc6c 100644 --- a/docs/source/readme.rst +++ b/docs/source/readme.rst @@ -480,6 +480,212 @@ Iterate Plots and Export Overlays More Advanced Examples ====================== +Derived results from existing plots (absolute temperature) +---------------------------------------------------------- + +.. code-block:: python + + from moldflow import Synergy + from moldflow.common import UserPlotType, TransformScalarOperations + + synergy = Synergy() + pm = synergy.plot_manager + dt = synergy.data_transform + + # Absolute temperature:K = Bulk temperature:13 + 273 + ds_id = pm.find_dataset_id_by_name("Bulk temperature") + all_times = synergy.create_double_array() + pm.get_indp_values(ds_id, all_times) + target_time = 13.0 + closest_time = min(all_times.to_list(), key=lambda t: abs(t - target_time)) + indp = synergy.create_double_array() + indp.add_double(closest_time) + + ent = synergy.create_integer_array() + vals = synergy.create_double_array() + pm.get_scalar_data(ds_id, indp, ent, vals) + + dt.scalar(ent, vals, TransformScalarOperations.ADD, 273.0, ent, vals) + + up = pm.create_user_plot() + up.set_name("Absolute Temperature") + up.set_data_type(UserPlotType.ELEMENT_DATA) + up.set_dept_unit_name("K") + up.set_scalar_data(ent, vals) + up.build() + +Vector difference with absolute value +------------------------------------- + +.. code-block:: python + + from moldflow import Synergy + from moldflow.common import TransformOperations, TransformFunctions, UserPlotType + + synergy = Synergy() + pm = synergy.plot_manager + dt = synergy.data_transform + + name = "Average velocity" # 3-component vector + ds_id = pm.find_dataset_id_by_name(name) + + times = synergy.create_double_array() + pm.get_indp_values(ds_id, times) + t1, t2 = 1.1, 2.0 + c1 = min(times.to_list(), key=lambda t: abs(t - t1)) + c2 = min(times.to_list(), key=lambda t: abs(t - t2)) + + def get_vec_at(t): + indp = synergy.create_double_array() + indp.add_double(t) + ent = synergy.create_integer_array() + vx = synergy.create_double_array() + vy = synergy.create_double_array() + vz = synergy.create_double_array() + pm.get_vector_data(ds_id, indp, ent, vx, vy, vz) + return ent, (vx, vy, vz) + + ent1, (v1x, v1y, v1z) = get_vec_at(c1) + ent2, (v2x, v2y, v2z) = get_vec_at(c2) + + vdx = synergy.create_double_array() + vdy = synergy.create_double_array() + vdz = synergy.create_double_array() + for a, b, out in [(v1x, v2x, vdx), (v1y, v2y, vdy), (v1z, v2z, vdz)]: + dt.op(ent1, a, TransformOperations.SUBTRACT, ent2, b, ent1, out) + dt.func(TransformFunctions.ABSOLUTE, ent1, out, ent1, out) + + up = pm.create_user_plot() + up.set_name("Difference in Velocity") + up.set_data_type(UserPlotType.ELEMENT_DATA) + up.set_vector_as_displacement(False) + up.set_dept_unit_name("m/s") + up.set_vector_data(ent1, vdx, vdy, vdz) + up.build() + +Operate on current plot +----------------------- + +.. code-block:: python + + from moldflow import Synergy + from moldflow.common import TransformFunctions, UserPlotType + + synergy = Synergy() + pm = synergy.plot_manager + viewer = synergy.viewer + dt = synergy.data_transform + + active = viewer.active_plot + if active is None: + raise RuntimeError("No active plot") + ds_id = active.data_id + + cols = pm.get_data_nb_components(ds_id) + dtype = pm.get_data_type(ds_id) + + times = synergy.create_double_array() + pm.get_indp_values(ds_id, times) + indp = None + if times.size > 0: + indp = synergy.create_double_array() + indp.add_double(times.to_list()[0]) + + ent = synergy.create_integer_array() + arrs = [synergy.create_double_array() for _ in range(max(cols, 1))] + + if cols == 1: + pm.get_scalar_data(ds_id, indp, ent, arrs[0]) + elif cols == 3: + pm.get_vector_data(ds_id, indp, ent, arrs[0], arrs[1], arrs[2]) + elif cols == 6: + pm.get_tensor_data(ds_id, indp, ent, *arrs) + else: + raise RuntimeError("Unsupported component count") + + for a in arrs[:cols]: + dt.func(TransformFunctions.ABSOLUTE, ent, a, ent, a) + + up = pm.create_user_plot() + up.set_name("ABS of current plot") + up.set_data_type(UserPlotType.ELEMENT_DATA if dtype == "ELDT" else UserPlotType.NODE_DATA) + up.set_vector_as_displacement(dtype == "NDDT") + if cols == 1: + up.set_scalar_data(ent, arrs[0]) + elif cols == 3: + up.set_vector_data(ent, arrs[0], arrs[1], arrs[2]) + elif cols == 6: + up.set_tensor_data(ent, *arrs[:6]) + up.build() + +Normalize by max at a time +-------------------------- + +.. code-block:: python + + from moldflow import Synergy + from moldflow.common import TransformScalarOperations, UserPlotType + + synergy = Synergy() + pm = synergy.plot_manager + dt = synergy.data_transform + + name = "Pressure" + ds_id = pm.find_dataset_id_by_name(name) + + times = synergy.create_double_array() + pm.get_indp_values(ds_id, times) + target_time = 3.5 + closest = min(times.to_list(), key=lambda t: abs(t - target_time)) + indp = synergy.create_double_array() + indp.add_double(closest) + + ent = synergy.create_integer_array() + vals = synergy.create_double_array() + pm.get_scalar_data(ds_id, indp, ent, vals) + + max_v = max(vals.to_list()) + out = synergy.create_double_array() + dt.scalar(ent, vals, TransformScalarOperations.DIVIDE, max_v, ent, out) + + up = pm.create_user_plot() + up.set_name("Pre/Max") + up.set_data_type(UserPlotType.ELEMENT_DATA) + up.set_dept_unit_name("%") + up.set_scalar_data(ent, out) + up.build() + +Average/min/max diagnostics to console +-------------------------------------- + +.. code-block:: python + + from moldflow import Synergy + + synergy = Synergy() + diag = synergy.diagnosis_manager + + ent = synergy.create_integer_array() + vals = synergy.create_double_array() + count = diag.get_aspect_ratio_diagnosis(0.0, 0.0, True, False, ent, vals) + if count > 0: + data = vals.to_list() + ave = sum(data) / len(data) + print(f"Average aspect ratio: {ave:.3f}") + +Entity selection strings +------------------------ + +.. code-block:: python + + from moldflow import Synergy + + synergy = Synergy() + el = synergy.study_doc.create_entity_list() + el.select_from_string("N1:2 N5 N8:9") + print(el.size) + print(el.convert_to_string()) + Configure Import Options Before CAD Import ------------------------------------------ @@ -622,9 +828,12 @@ Data Transform synergy = Synergy() dt = synergy.data_transform - ia = synergy.create_integer_array(); ia.from_list([1, 2, 3]) - da = synergy.create_double_array(); da.from_list([0.5, 1.5, 2.5]) - ib = synergy.create_integer_array(); db = synergy.create_double_array() + ia = synergy.create_integer_array() + ia.from_list([1, 2, 3]) + da = synergy.create_double_array() + da.from_list([0.5, 1.5, 2.5]) + ib = synergy.create_integer_array() + db = synergy.create_double_array() ok = dt.scalar(ia, da, TransformScalarOperations.MULTIPLY, 2.0, ib, db) print(f"Scalar transform ok: {ok}") @@ -715,7 +924,8 @@ Modeler synergy = Synergy() modeler = synergy.modeler - v = synergy.create_vector(); v.set_xyz(0.0, 0.0, 0.0) + v = synergy.create_vector() + v.set_xyz(0.0, 0.0, 0.0) node_list = modeler.create_node_by_xyz(v) print("Created node list") @@ -729,7 +939,8 @@ Mold Surface Generator synergy = Synergy() msg = synergy.mold_surface_generator msg.centered = True - dim = synergy.create_vector(); dim.set_xyz(100.0, 80.0, 60.0) + dim = synergy.create_vector() + dim.set_xyz(100.0, 80.0, 60.0) msg.dimensions = dim ok = msg.generate() print(f"Mold surface generated: {ok}") @@ -755,11 +966,209 @@ Runner Generator synergy = Synergy() rg = synergy.runner_generator - rg.sprue_x = 0.0; rg.sprue_y = 0.0; rg.sprue_length = 30.0 - rg.sprue_diameter = 6.0; rg.sprue_taper_angle = 2.0 + rg.sprue_x = 0.0 + rg.sprue_y = 0.0 + rg.sprue_length = 30.0 + rg.sprue_diameter = 6.0 + rg.sprue_taper_angle = 2.0 ok = rg.generate() print(f"Runner generated: {ok}") +Build PowerPoint Report (using python-pptx) +------------------------------------------- + +This example emulates a report workflow directly from Python, using python-pptx to author a PPTX. See python-pptx on PyPI: https://pypi.org/project/python-pptx + +.. code-block:: python + + import os + import tempfile + from moldflow import Synergy + from moldflow.common import StandardViews + + # pip install python-pptx + from pptx import Presentation + from pptx.util import Inches, Pt + + synergy = Synergy() + sd = synergy.study_doc + pm = synergy.plot_manager + viewer = synergy.viewer + project = synergy.project + + # Ensure a visible viewport for captures + viewer.set_view_size(1600, 900) + + # Result overlay flags + PLOT_RESULT = True + PLOT_LEGEND = True + PLOT_AXIS = True + PLOT_ROTATION = True + PLOT_SCALE_BAR = True + PLOT_PLOT_INFO = True + PLOT_STUDY_TITLE = True + PLOT_RULER = True + PLOT_LOGO = True + + def capture_plot_image(plot, orientation: str | None, width_px=1600, height_px=900) -> str: + # Apply orientation and fit the view + if orientation and orientation.upper() != "CURRENT": + target = orientation.title() if isinstance(orientation, str) else orientation + viewer.go_to_standard_view(target) + # Show plot (if provided) or ensure geometry-only capture + if plot is not None: + viewer.show_plot(plot) + # Ensure plot is generated and a valid frame is visible + try: + plot.regenerate() + except Exception: + pass + try: + # Show the last frame if available + frames = viewer.get_number_frames_by_name(plot.name) + if isinstance(frames, int) and frames > 0: + viewer.show_plot_frame(plot, frames - 1) + except Exception: + pass + else: + # Hide any active plot to avoid empty result layer when capturing geometry + ap = viewer.active_plot + if ap is not None: + viewer.hide_plot(ap) + viewer.fit() + + tmp = tempfile.NamedTemporaryFile(prefix="mf_plot_", suffix=".png", delete=False) + tmp.close() + + # Use full overlay flags when capturing results; turn them off for geometry-only + if plot is not None: + viewer.save_image( + tmp.name, + x=width_px, + y=height_px, + result=PLOT_RESULT, + legend=PLOT_LEGEND, + axis=PLOT_AXIS, + rotation=PLOT_ROTATION, + scale_bar=PLOT_SCALE_BAR, + plot_info=PLOT_PLOT_INFO, + study_title=PLOT_STUDY_TITLE, + ruler=PLOT_RULER, + logo=PLOT_LOGO, + ) + else: + viewer.save_image(tmp.name, x=width_px, y=height_px, result=False, legend=False, axis=False) + + return tmp.name + + prs = Presentation() + title_layout = prs.slide_layouts[0] + title_only_layout = prs.slide_layouts[5] + + def add_title(slide, text: str): + if slide.shapes.title is not None: + slide.shapes.title.text = text + slide.shapes.title.text_frame.paragraphs[0].font.size = Pt(28) + + def add_picture_centered(slide, image_path: str, max_width_in=10.0, top_in=1.5): + pic = slide.shapes.add_picture(image_path, Inches(0), Inches(top_in)) + page_w = prs.slide_width + max_w = Inches(max_width_in) + if pic.width > max_w: + scale = max_w / pic.width + pic.width = int(pic.width * scale) + pic.height = int(pic.height * scale) + pic.left = int((page_w - pic.width) / 2) + return pic + + slide = prs.slides.add_slide(title_layout) + add_title(slide, f"Report: {sd.study_name}") + if slide.placeholders and len(slide.placeholders) > 1: + slide.placeholders[1].text = f"Project: {project.path}" + + geo_slide = prs.slides.add_slide(title_only_layout) + add_title(geo_slide, "Geometry Overview") + viewer.reset() + viewer.go_to_standard_view(StandardViews.ISOMETRIC) + viewer.fit() + geo_img = capture_plot_image(plot=None, orientation="CURRENT") + add_picture_centered(geo_slide, geo_img) + + diag = synergy.diagnosis_manager + summary = diag.get_mesh_summary(element_only=False) + mesh_slide = prs.slides.add_slide(title_only_layout) + add_title(mesh_slide, "Mesh Summary") + tf = mesh_slide.shapes.add_textbox(Inches(1), Inches(1.5), Inches(8), Inches(3)).text_frame + tf.word_wrap = True + lines = [ + f"Nodes: {summary.nodes_count}", + f"Triangles: {summary.triangles_count}", + f"Tetras: {summary.tetras_count}", + f"Beams: {summary.beams_count}", + f"AspectRatio avg/min/max: {summary.ave_aspect_ratio:.3f} / {summary.min_aspect_ratio:.3f} / {summary.max_aspect_ratio:.3f}", + ] + for i, line in enumerate(lines): + p = tf.add_paragraph() if i else tf.paragraphs[0] + p.text = line + p.font.size = Pt(16) + + def add_plot_slide(plot_obj, title: str, orientation: str = "ISOMETRIC"): + slide = prs.slides.add_slide(title_only_layout) + add_title(slide, title) + img = capture_plot_image(plot_obj, orientation) + if img: + add_picture_centered(slide, img) + try: + os.remove(img) + except Exception: + pass + else: + tx = slide.shapes.add_textbox(Inches(1), Inches(1.5), Inches(8), Inches(1.0)).text_frame + tx.text = "Skipped non-mesh/unsupported plot for 3D capture" + tx.paragraphs[0].font.size = Pt(14) + + def add_plot_four_views(plot_obj, title: str): + slide = prs.slides.add_slide(title_only_layout) + add_title(slide, f"{title} - Four Views") + views = ["ISOMETRIC", "FRONT", "LEFT", "TOP"] + cols = 2 + x0, y0 = Inches(0.7), Inches(1.3) + cell_w, cell_h = Inches(4.5), Inches(3.2) + for idx, v in enumerate(views): + img = capture_plot_image(plot_obj, v, width_px=1000, height_px=600) + col = idx % cols + row = idx // cols + left = x0 + col * (cell_w + Inches(0.2)) + top = y0 + row * (cell_h + Inches(0.2)) + if img: + pic = slide.shapes.add_picture(img, left, top) + if pic.width > cell_w: + scale = cell_w / pic.width + pic.width = int(pic.width * scale) + pic.height = int(pic.height * scale) + if pic.height > cell_h: + scale = cell_h / pic.height + pic.width = int(pic.width * scale) + pic.height = int(pic.height * scale) + try: + os.remove(img) + except Exception: + pass + + plot = pm.get_first_plot() + count = 0 + while plot: + name = plot.name + add_plot_slide(plot, name, orientation="ISOMETRIC") + if count < 2: + add_plot_four_views(plot, name) + plot = pm.get_next_plot(plot) + count += 1 + + out_path = os.path.join(project.path, f"{os.path.splitext(sd.study_name)[0]}_report.pptx") + prs.save(out_path) + print(f"Report saved: {out_path}") + Study Document -------------- @@ -783,8 +1192,10 @@ System Messages synergy = Synergy() sm = synergy.system_message - sa = synergy.create_string_array(); sa.from_list(["Diameter", "Length"]) - da = synergy.create_double_array(); da.from_list([6.0, 30.0]) + sa = synergy.create_string_array() + sa.from_list(["Diameter", "Length"]) + da = synergy.create_double_array() + da.from_list([6.0, 30.0]) msg = sm.get_data_message(100, sa, da, SystemUnits.METRIC) print(msg) @@ -796,10 +1207,15 @@ Arrays and Geometry Helpers from moldflow import Synergy synergy = Synergy() - ia = synergy.create_integer_array(); ia.from_list([1, 2, 3]); print(ia.size) - da = synergy.create_double_array(); da.from_list([0.1, 0.2]) - sa = synergy.create_string_array(); sa.from_list(["A", "B"]) - vec = synergy.create_vector(); vec.set_xyz(1.0, 2.0, 3.0) + ia = synergy.create_integer_array() + ia.from_list([1, 2, 3]) + print(ia.size) + da = synergy.create_double_array() + da.from_list([0.1, 0.2]) + sa = synergy.create_string_array() + sa.from_list(["A", "B"]) + vec = synergy.create_vector() + vec.set_xyz(1.0, 2.0, 3.0) va = synergy.create_vector_array() API Reference diff --git a/scripts/check_localization.py b/scripts/check_localization.py index c0be77b..18eab31 100644 --- a/scripts/check_localization.py +++ b/scripts/check_localization.py @@ -38,6 +38,11 @@ from typing import Dict, List, Tuple +# Strings that are acceptable to remain identical across locales +ALLOW_EQUAL_MSGSTR: set[str] = { + "OK", +} + @dataclass class PoEntry: msgid: str @@ -357,9 +362,13 @@ def check_translation_gaps(self) -> List[str]: if not po_parser.has_string(msgid): missing_translations.append(msgid) else: - # Check if translation is empty or same as source (untranslated) + # Check if translation is empty or same as source (untranslated), + # with an allowlist for locale-invariant tokens like "OK" target_entry = po_parser.entries[msgid] - if not target_entry.msgstr.strip() or target_entry.msgstr == msgid: + if ( + not target_entry.msgstr.strip() + or (target_entry.msgstr == msgid and msgid not in ALLOW_EQUAL_MSGSTR) + ): empty_translations.append(msgid) locale_stats[locale_name] = { diff --git a/src/moldflow/__init__.py b/src/moldflow/__init__.py index d875960..0a8f1cc 100644 --- a/src/moldflow/__init__.py +++ b/src/moldflow/__init__.py @@ -102,10 +102,20 @@ from .common import ViewModes from .common import UserPlotType +from .message_box import ( + MessageBox, + MessageBoxType, + MessageBoxResult, + MessageBoxOptions, + MessageBoxIcon, + MessageBoxModality, + MessageBoxDefaultButton, + MessageBoxReturn, +) + # Version checking and update functionality from .version_check import get_version, check_for_updates_on_import - # Check for updates on import unless disabled check_for_updates_on_import() diff --git a/src/moldflow/locale/de-DE/LC_MESSAGES/locale.de-DE.po b/src/moldflow/locale/de-DE/LC_MESSAGES/locale.de-DE.po index 4203e8c..3828e8a 100644 --- a/src/moldflow/locale/de-DE/LC_MESSAGES/locale.de-DE.po +++ b/src/moldflow/locale/de-DE/LC_MESSAGES/locale.de-DE.po @@ -3,6 +3,9 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Language: de-DE\n" +msgid "Cancel" +msgstr "Abbrechen" + msgid "Checking file extension {file_name}" msgstr "Überprüfen der Dateierweiterung {file_name}" @@ -72,6 +75,9 @@ msgstr "Ungültiger Wert: {reason}" msgid "Logger was not setup" msgstr "Logger wurde nicht eingerichtet" +msgid "OK" +msgstr "OK" + msgid "Save Error" msgstr "Speicherfehler" @@ -84,6 +90,9 @@ msgstr "Speicherfehler: Speichern von {saving} in {file_name} fehlgeschlagen" msgid "Setting {name} to {value}" msgstr "Einstellung {name} auf {value}" +msgid "Submit" +msgstr "Senden" + msgid "Test String" msgstr "Testzeichenfolge" diff --git a/src/moldflow/locale/en-US/LC_MESSAGES/locale.en-US.po b/src/moldflow/locale/en-US/LC_MESSAGES/locale.en-US.po index 792704b..d0d1571 100644 --- a/src/moldflow/locale/en-US/LC_MESSAGES/locale.en-US.po +++ b/src/moldflow/locale/en-US/LC_MESSAGES/locale.en-US.po @@ -3,6 +3,9 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Language: en-US\n" +msgid "Cancel" +msgstr "Cancel" + msgid "Checking file extension {file_name}" msgstr "Checking file extension {file_name}" @@ -72,6 +75,9 @@ msgstr "Invalid Value: {reason}" msgid "Logger was not setup" msgstr "Logger was not setup" +msgid "OK" +msgstr "OK" + msgid "Save Error" msgstr "Save Error" @@ -84,6 +90,9 @@ msgstr "Save Error: Failed to save {saving} to {file_name}" msgid "Setting {name} to {value}" msgstr "Setting {name} to {value}" +msgid "Submit" +msgstr "Submit" + msgid "Test String" msgstr "Test String" diff --git a/src/moldflow/locale/es-ES/LC_MESSAGES/locale.es-ES.po b/src/moldflow/locale/es-ES/LC_MESSAGES/locale.es-ES.po index ca8a638..ce166eb 100644 --- a/src/moldflow/locale/es-ES/LC_MESSAGES/locale.es-ES.po +++ b/src/moldflow/locale/es-ES/LC_MESSAGES/locale.es-ES.po @@ -3,6 +3,9 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Language: es-ES\n" +msgid "Cancel" +msgstr "Cancelar" + msgid "Checking file extension {file_name}" msgstr "Comprobando la extensión del archivo {file_name}" @@ -72,6 +75,9 @@ msgstr "Valor no válido: {reason}" msgid "Logger was not setup" msgstr "Logger no estaba configurado" +msgid "OK" +msgstr "Aceptar" + msgid "Save Error" msgstr "Error al guardar" @@ -84,6 +90,9 @@ msgstr "Error al guardar: No se pudo guardar {saving} en {file_name}" msgid "Setting {name} to {value}" msgstr "Configurar {name} a {value}" +msgid "Submit" +msgstr "Aceptar" + msgid "Test String" msgstr "Cadena de prueba" diff --git a/src/moldflow/locale/fr-FR/LC_MESSAGES/locale.fr-FR.po b/src/moldflow/locale/fr-FR/LC_MESSAGES/locale.fr-FR.po index 43fce62..f45060e 100644 --- a/src/moldflow/locale/fr-FR/LC_MESSAGES/locale.fr-FR.po +++ b/src/moldflow/locale/fr-FR/LC_MESSAGES/locale.fr-FR.po @@ -3,6 +3,9 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Language: fr-FR\n" +msgid "Cancel" +msgstr "Annuler" + msgid "Checking file extension {file_name}" msgstr "Vérification de l'extension du fichier {file_name}" @@ -72,6 +75,9 @@ msgstr "Valeur non valide: {reason}" msgid "Logger was not setup" msgstr "Logger n'était pas configuré" +msgid "OK" +msgstr "OK" + msgid "Save Error" msgstr "Erreur d'enregistrement" @@ -84,6 +90,9 @@ msgstr "Erreur d'enregistrement : échec de l'enregistrement de {saving} dans {f msgid "Setting {name} to {value}" msgstr "Définition de {name} sur {value}" +msgid "Submit" +msgstr "Valider" + msgid "Test String" msgstr "Chaîne de test" diff --git a/src/moldflow/locale/it-IT/LC_MESSAGES/locale.it-IT.po b/src/moldflow/locale/it-IT/LC_MESSAGES/locale.it-IT.po index 3b257d0..693f216 100644 --- a/src/moldflow/locale/it-IT/LC_MESSAGES/locale.it-IT.po +++ b/src/moldflow/locale/it-IT/LC_MESSAGES/locale.it-IT.po @@ -3,6 +3,9 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Language: it-IT\n" +msgid "Cancel" +msgstr "Annulla" + msgid "Checking file extension {file_name}" msgstr "Verifica dell'estensione del file {file_name}" @@ -72,6 +75,9 @@ msgstr "Valore non valido: {reason}" msgid "Logger was not setup" msgstr "Logger non è stato configurato" +msgid "OK" +msgstr "OK" + msgid "Save Error" msgstr "Errore di salvataggio" @@ -84,6 +90,9 @@ msgstr "Errore di salvataggio: salvataggio di {saving} in {file_name} non riusci msgid "Setting {name} to {value}" msgstr "Impostazione di {name} su {value}" +msgid "Submit" +msgstr "Conferma" + msgid "Test String" msgstr "Stringa di prova" diff --git a/src/moldflow/locale/ja-JP/LC_MESSAGES/locale.ja-JP.po b/src/moldflow/locale/ja-JP/LC_MESSAGES/locale.ja-JP.po index 5f367eb..3a1dfc1 100644 --- a/src/moldflow/locale/ja-JP/LC_MESSAGES/locale.ja-JP.po +++ b/src/moldflow/locale/ja-JP/LC_MESSAGES/locale.ja-JP.po @@ -3,6 +3,9 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Language: ja-JP\n" +msgid "Cancel" +msgstr "キャンセル" + msgid "Checking file extension {file_name}" msgstr "ファイル拡張子 {file_name} を確認しています" @@ -72,6 +75,9 @@ msgstr "無効な値: {reason}" msgid "Logger was not setup" msgstr "ロガーが設定されていませんでした" +msgid "OK" +msgstr "OK" + msgid "Save Error" msgstr "保存エラー" @@ -84,6 +90,9 @@ msgstr "保存エラー: {saving} を {file_name} に保存できませんでし msgid "Setting {name} to {value}" msgstr "{name} を {value} に設定しています" +msgid "Submit" +msgstr "OK" + msgid "Test String" msgstr "テスト文字列" diff --git a/src/moldflow/locale/ko-KR/LC_MESSAGES/locale.ko-KR.po b/src/moldflow/locale/ko-KR/LC_MESSAGES/locale.ko-KR.po index 48752cd..5f6976a 100644 --- a/src/moldflow/locale/ko-KR/LC_MESSAGES/locale.ko-KR.po +++ b/src/moldflow/locale/ko-KR/LC_MESSAGES/locale.ko-KR.po @@ -3,6 +3,9 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Language: ko-KR\n" +msgid "Cancel" +msgstr "취소" + msgid "Checking file extension {file_name}" msgstr "파일 확장자 {file_name} 확인 중" @@ -72,6 +75,9 @@ msgstr "잘못된 값: {reason}" msgid "Logger was not setup" msgstr "로거가 설정되지 않았습니다" +msgid "OK" +msgstr "확인" + msgid "Save Error" msgstr "저장 오류" @@ -84,6 +90,9 @@ msgstr "저장 오류: {saving}을(를) {file_name}에 저장하지 못했습니 msgid "Setting {name} to {value}" msgstr "{name}을(를) {value}(으)로 설정 중" +msgid "Submit" +msgstr "확인" + msgid "Test String" msgstr "테스트 문자열" diff --git a/src/moldflow/locale/pt-PT/LC_MESSAGES/locale.pt-PT.po b/src/moldflow/locale/pt-PT/LC_MESSAGES/locale.pt-PT.po index 9b6c70a..9b1a92a 100644 --- a/src/moldflow/locale/pt-PT/LC_MESSAGES/locale.pt-PT.po +++ b/src/moldflow/locale/pt-PT/LC_MESSAGES/locale.pt-PT.po @@ -3,6 +3,9 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Language: pt-PT\n" +msgid "Cancel" +msgstr "Cancelar" + msgid "Checking file extension {file_name}" msgstr "A verificar a extensão do ficheiro {file_name}" @@ -72,6 +75,9 @@ msgstr "Valor inválido: {reason}" msgid "Logger was not setup" msgstr "Logger não foi configurado" +msgid "OK" +msgstr "OK" + msgid "Save Error" msgstr "Erro ao guardar" @@ -84,6 +90,9 @@ msgstr "Erro ao guardar: falha ao guardar {saving} em {file_name}" msgid "Setting {name} to {value}" msgstr "Configuração {name} para {value}" +msgid "Submit" +msgstr "Submeter" + msgid "Test String" msgstr "String de teste" diff --git a/src/moldflow/locale/zh-CN/LC_MESSAGES/locale.zh-CN.po b/src/moldflow/locale/zh-CN/LC_MESSAGES/locale.zh-CN.po index 8bc6aab..a73c1e2 100644 --- a/src/moldflow/locale/zh-CN/LC_MESSAGES/locale.zh-CN.po +++ b/src/moldflow/locale/zh-CN/LC_MESSAGES/locale.zh-CN.po @@ -3,6 +3,9 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Language: zh-CN\n" +msgid "Cancel" +msgstr "取消" + msgid "Checking file extension {file_name}" msgstr "正在检查文件扩展名{file_name}" @@ -72,6 +75,9 @@ msgstr "无效的值:{reason}" msgid "Logger was not setup" msgstr "没有设置记录器" +msgid "OK" +msgstr "确定" + msgid "Save Error" msgstr "保存错误" @@ -84,6 +90,9 @@ msgstr "保存错误:将{saving}保存到{file_name}失败" msgid "Setting {name} to {value}" msgstr "将{name}设置为{value}" +msgid "Submit" +msgstr "确定" + msgid "Test String" msgstr "测试字符串" diff --git a/src/moldflow/locale/zh-TW/LC_MESSAGES/locale.zh-TW.po b/src/moldflow/locale/zh-TW/LC_MESSAGES/locale.zh-TW.po index cd4019f..4ebf02b 100644 --- a/src/moldflow/locale/zh-TW/LC_MESSAGES/locale.zh-TW.po +++ b/src/moldflow/locale/zh-TW/LC_MESSAGES/locale.zh-TW.po @@ -3,6 +3,9 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Language: zh-TW\n" +msgid "Cancel" +msgstr "取消" + msgid "Checking file extension {file_name}" msgstr "正在檢查檔案副檔名 {file_name}" @@ -72,6 +75,9 @@ msgstr "無效的值:{reason}" msgid "Logger was not setup" msgstr "記錄器未設定" +msgid "OK" +msgstr "確定" + msgid "Save Error" msgstr "儲存錯誤" @@ -84,6 +90,9 @@ msgstr "儲存錯誤:將 {saving} 儲存到 {file_name} 失敗" msgid "Setting {name} to {value}" msgstr "正在將 {name} 設定為 {value}" +msgid "Submit" +msgstr "確定" + msgid "Test String" msgstr "測試字串" diff --git a/src/moldflow/message_box.py b/src/moldflow/message_box.py new file mode 100644 index 0000000..0817a51 --- /dev/null +++ b/src/moldflow/message_box.py @@ -0,0 +1,1379 @@ +# SPDX-FileCopyrightText: 2025 Autodesk, Inc. +# SPDX-License-Identifier: Apache-2.0 + +""" +MessageBox convenience wrapper for Moldflow scripts. + +Provides simple info/warning/error dialogs, confirmation prompts, and a text +input dialog. Uses Win32 MessageBox for standard dialogs and a lightweight +custom Win32 dialog (ctypes) for text input. +""" + +from enum import Enum, auto +from typing import Optional, Union, Callable, TypeAlias +from dataclasses import dataclass +import ctypes +import platform +from ctypes import windll, wintypes, byref, create_unicode_buffer, c_int, c_wchar_p, WINFUNCTYPE +import signal +import struct +from .i18n import get_text +from .logger import get_logger + +# This module intentionally contains a large amount of Windows interop glue +# and UI layout code. +# pylint: disable=C0301,C0302,R0902,W0212,R0911,R0914,R0902,W0201 + +# Fallbacks for missing wintypes aliases on some Python versions +if not hasattr(wintypes, "LRESULT"): + # LONG_PTR + wintypes.LRESULT = ctypes.c_ssize_t # type: ignore[attr-defined] +if not hasattr(wintypes, "HMENU"): + wintypes.HMENU = ctypes.c_void_p # type: ignore[attr-defined] +if not hasattr(wintypes, "HCURSOR"): + wintypes.HCURSOR = ctypes.c_void_p # type: ignore[attr-defined] +if not hasattr(wintypes, "HICON"): + wintypes.HICON = ctypes.c_void_p # type: ignore[attr-defined] +if not hasattr(wintypes, "HBRUSH"): + wintypes.HBRUSH = ctypes.c_void_p # type: ignore[attr-defined] +if not hasattr(wintypes, "HINSTANCE"): + wintypes.HINSTANCE = ctypes.c_void_p # type: ignore[attr-defined] + +# Extra Win32 constants used by CreateWindowEx path +WIN_WM_SETFONT = 0x0030 +WIN_WS_EX_DLGMODALFRAME = 0x00000001 +WIN_WS_EX_CONTROLPARENT = 0x00010000 +WIN_DEFAULT_CHARSET = 1 +WIN_OUT_DEFAULT_PRECIS = 0 +WIN_CLIP_DEFAULT_PRECIS = 0 +WIN_CLEARTYPE_QUALITY = 5 +WIN_DEFAULT_PITCH = 0 +WIN_FF_DONTCARE = 0 +WIN_FW_NORMAL = 400 +WIN_LOGPIXELSY = 90 +WIN_WM_CLOSE = 0x0010 +WIN_WM_KEYDOWN = 0x0100 +WIN_VK_RETURN = 0x0D +WIN_VK_ESCAPE = 0x1B + +# Helper alias for pointer-sized integer type used by Win32 callbacks +# Return type for DLGPROC should be an integer type matching pointer size, +# not a pointer type. Using a pointer type here can corrupt the stack on 64-bit. +# pylint: disable=invalid-name +INT_PTR = ctypes.c_ssize_t + + +# Win32 MessageBox flags (from winuser.h) +WIN_MB_OK = 0x00000000 +WIN_MB_OKCANCEL = 0x00000001 +WIN_MB_ABORTRETRYIGNORE = 0x00000002 +WIN_MB_YESNOCANCEL = 0x00000003 +WIN_MB_YESNO = 0x00000004 +WIN_MB_RETRYCANCEL = 0x00000005 +WIN_MB_CANCELTRYCONTINUE = 0x00000006 + +WIN_MB_ICONERROR = 0x00000010 +WIN_MB_ICONQUESTION = 0x00000020 +WIN_MB_ICONWARNING = 0x00000030 +WIN_MB_ICONINFORMATION = 0x00000040 + +WIN_MB_DEFBUTTON2 = 0x00000100 +WIN_MB_DEFBUTTON3 = 0x00000200 +WIN_MB_DEFBUTTON4 = 0x00000300 + +WIN_MB_SYSTEMMODAL = 0x00001000 +WIN_MB_TASKMODAL = 0x00002000 +WIN_MB_HELP = 0x00004000 +WIN_MB_SETFOREGROUND = 0x00010000 +WIN_MB_TOPMOST = 0x00040000 +WIN_MB_RIGHT = 0x00080000 +WIN_MB_RTLREADING = 0x00100000 + +# Win32 MessageBox return IDs +WIN_IDOK = 1 +WIN_IDCANCEL = 2 +WIN_IDABORT = 3 +WIN_IDRETRY = 4 +WIN_IDIGNORE = 5 +WIN_IDYES = 6 +WIN_IDNO = 7 +WIN_IDTRYAGAIN = 10 +WIN_IDCONTINUE = 11 + +# Win32 dialog and control style flags (used by input dialog) +WIN_DS_SETFONT = 0x00000040 +WIN_DS_MODALFRAME = 0x00000080 +WIN_WS_CAPTION = 0x00C00000 +WIN_WS_SYSMENU = 0x00080000 +WIN_WS_POPUP = 0x80000000 + +WIN_WS_CHILD = 0x40000000 +WIN_WS_VISIBLE = 0x10000000 +WIN_WS_TABSTOP = 0x00010000 +WIN_WS_GROUP = 0x00020000 +WIN_WS_BORDER = 0x00800000 +WIN_WS_THICKFRAME = 0x00040000 +WIN_WS_MINIMIZEBOX = 0x00020000 +WIN_WS_MAXIMIZEBOX = 0x00010000 + +WIN_ES_AUTOHSCROLL = 0x00000080 +WIN_ES_PASSWORD = 0x00000020 +WIN_SS_LEFT = 0x00000000 +WIN_BS_DEFPUSHBUTTON = 0x00000001 +WIN_BS_PUSHBUTTON = 0x00000000 + +# Window messages +WIN_WM_INITDIALOG = 0x0110 +WIN_WM_COMMAND = 0x0111 +WIN_WM_CTLCOLORSTATIC = 0x0138 + +# Edit control helpers +WIN_EM_SETCUEBANNER = 0x1501 +WIN_EN_CHANGE = 0x0300 +WIN_EM_LIMITTEXT = 0x00C5 + +# DrawText flags +WIN_DT_WORDBREAK = 0x0010 +WIN_DT_CALCRECT = 0x0400 +WIN_DT_NOPREFIX = 0x0800 + +# SetWindowPos flags and system metrics +WIN_SWP_NOSIZE = 0x0001 +WIN_SWP_NOZORDER = 0x0004 +WIN_SWP_NOACTIVATE = 0x0010 +WIN_SM_CXSCREEN = 0 +WIN_SM_CYSCREEN = 1 + +# Predefined control classes (atoms from winuser.h) +# 0x0080: BUTTON, 0x0081: EDIT, 0x0082: STATIC +WIN_CLASS_BUTTON = 0x0080 +WIN_CLASS_EDIT = 0x0081 +WIN_CLASS_STATIC = 0x0082 + +# Control IDs +WIN_ID_EDIT = 1001 +WIN_ID_OK = 1 +WIN_ID_CANCEL = 2 + +# Defaults +DEFAULT_TITLE = "Moldflow" + + +class MessageBoxType(Enum): + """ + Message box types supported by the convenience API. + + - INFO: Informational message with OK button + - WARNING: Warning message with OK button + - ERROR: Error message with OK button + - YES_NO: Confirmation dialog with Yes/No buttons + - YES_NO_CANCEL: Confirmation dialog with Yes/No/Cancel buttons + - OK_CANCEL: Prompt with OK/Cancel buttons + - RETRY_CANCEL: Prompt with Retry/Cancel buttons + - ABORT_RETRY_IGNORE: Prompt with Abort/Retry/Ignore buttons + - CANCEL_TRY_CONTINUE: Prompt with Cancel/Try Again/Continue buttons + - INPUT: Text input dialog returning a string + """ + + INFO = auto() + WARNING = auto() + ERROR = auto() + YES_NO = auto() + YES_NO_CANCEL = auto() + OK_CANCEL = auto() + RETRY_CANCEL = auto() + ABORT_RETRY_IGNORE = auto() + CANCEL_TRY_CONTINUE = auto() + INPUT = auto() + + +class MessageBoxResult(Enum): + """ + Result of a message box interaction. + + For INPUT type, the MessageBox.show() method returns a string rather than + a MessageBoxResult. For other types, it returns one of these values. + """ + + OK = auto() + CANCEL = auto() + YES = auto() + NO = auto() + RETRY = auto() + ABORT = auto() + IGNORE = auto() + TRY_AGAIN = auto() + CONTINUE = auto() + + +# Public type alias for show() return value +MessageBoxReturn: TypeAlias = Union[MessageBoxResult, Optional[str]] + + +class MessageBoxIcon(Enum): + """ + Icon to display on the message box. If not provided, a sensible default is + chosen based on the MessageBoxType. + """ + + NONE = auto() + INFORMATION = auto() + WARNING = auto() + ERROR = auto() + QUESTION = auto() + + +class MessageBoxModality(Enum): + """Modality for the message box window.""" + + APPLICATION = auto() # Default Win32 behavior (no explicit flag) + SYSTEM = auto() + TASK = auto() + + +class MessageBoxDefaultButton(Enum): + """Which button is the default (activated by Enter).""" + + BUTTON1 = auto() + BUTTON2 = auto() + BUTTON3 = auto() + BUTTON4 = auto() + + +# Mapping dictionaries (module-level) for flags and results +MAPPING_MESSAGEBOX_TYPE = { + MessageBoxType.INFO: (WIN_MB_OK, MessageBoxIcon.INFORMATION, 1), + MessageBoxType.WARNING: (WIN_MB_OK, MessageBoxIcon.WARNING, 1), + MessageBoxType.ERROR: (WIN_MB_OK, MessageBoxIcon.ERROR, 1), + MessageBoxType.YES_NO: (WIN_MB_YESNO, MessageBoxIcon.QUESTION, 2), + MessageBoxType.YES_NO_CANCEL: (WIN_MB_YESNOCANCEL, MessageBoxIcon.QUESTION, 3), + MessageBoxType.OK_CANCEL: (WIN_MB_OKCANCEL, MessageBoxIcon.INFORMATION, 2), + MessageBoxType.RETRY_CANCEL: (WIN_MB_RETRYCANCEL, MessageBoxIcon.WARNING, 2), + MessageBoxType.ABORT_RETRY_IGNORE: (WIN_MB_ABORTRETRYIGNORE, MessageBoxIcon.ERROR, 3), + MessageBoxType.CANCEL_TRY_CONTINUE: (WIN_MB_CANCELTRYCONTINUE, MessageBoxIcon.WARNING, 3), +} + +ICON_TO_FLAG = { + MessageBoxIcon.INFORMATION: WIN_MB_ICONINFORMATION, + MessageBoxIcon.WARNING: WIN_MB_ICONWARNING, + MessageBoxIcon.ERROR: WIN_MB_ICONERROR, + MessageBoxIcon.QUESTION: WIN_MB_ICONQUESTION, +} + +DEFAULT_BUTTON_TO_FLAG = { + MessageBoxDefaultButton.BUTTON2: (WIN_MB_DEFBUTTON2, 2), + MessageBoxDefaultButton.BUTTON3: (WIN_MB_DEFBUTTON3, 3), + MessageBoxDefaultButton.BUTTON4: (WIN_MB_DEFBUTTON4, 4), +} + +MODALITY_TO_FLAG = { + MessageBoxModality.SYSTEM: WIN_MB_SYSTEMMODAL, + MessageBoxModality.TASK: WIN_MB_TASKMODAL, +} + +ID_TO_RESULT = { + WIN_IDOK: MessageBoxResult.OK, + WIN_IDCANCEL: MessageBoxResult.CANCEL, + WIN_IDYES: MessageBoxResult.YES, + WIN_IDNO: MessageBoxResult.NO, + WIN_IDRETRY: MessageBoxResult.RETRY, + WIN_IDABORT: MessageBoxResult.ABORT, + WIN_IDIGNORE: MessageBoxResult.IGNORE, + WIN_IDTRYAGAIN: MessageBoxResult.TRY_AGAIN, + WIN_IDCONTINUE: MessageBoxResult.CONTINUE, +} + + +@dataclass(frozen=True) +class MessageBoxOptions: # pylint: disable=too-many-instance-attributes + """ + Optional advanced options for MessageBox. + + - icon: Overrides the default icon + - default_button: Choose default button (2/3/4). BUTTON1 is implicit default + - topmost: Keep message box on top of other windows + - modality: Application (default), Task-modal, or System-modal + - rtl_reading: Use right-to-left reading order + - right_align: Right align the message text + - help_button: Show a Help button + - set_foreground: Force the message box to the foreground + """ + + icon: Optional[MessageBoxIcon] = None + default_button: Optional[MessageBoxDefaultButton] = None + topmost: bool = False + modality: Optional[MessageBoxModality] = None + rtl_reading: bool = False + right_align: bool = False + help_button: bool = False + set_foreground: bool = False + owner_hwnd: Optional[int] = None + # Input dialog enhancements + default_text: Optional[str] = None + placeholder: Optional[str] = None + validator: Optional[Callable[[str], bool]] = None + font_face: str = "Segoe UI" + font_size_pt: int = 9 + is_password: bool = False + char_limit: Optional[int] = None + width_dlu: Optional[int] = None + height_dlu: Optional[int] = None + + def __post_init__(self) -> None: + # Normalize strings + normalized_face = (self.font_face or "Segoe UI").strip() + object.__setattr__(self, "font_face", normalized_face or "Segoe UI") + + # Clamp font size + size = self.font_size_pt + if not isinstance(size, int): + try: + size = int(size) + except Exception as exc: + logger = get_logger("message_box") + if logger: + logger.debug("Font size parse failed; defaulting to 9: %s", exc) + size = 9 + # Clamp font size between sensible bounds + size = max(6, min(size, 24)) + object.__setattr__(self, "font_size_pt", size) + + # Owner HWND must be non-negative + if self.owner_hwnd is not None and self.owner_hwnd < 0: + object.__setattr__(self, "owner_hwnd", 0) + + # Normalize default_text/placeholder + if self.default_text is not None: + object.__setattr__(self, "default_text", str(self.default_text)) + if self.placeholder is not None: + object.__setattr__(self, "placeholder", str(self.placeholder)) + + # Validate char_limit + if self.char_limit is not None and self.char_limit < 0: + object.__setattr__(self, "char_limit", 0) + + +class MessageBox: + """ + MessageBox convenience class. + + Example: + .. code-block:: python + + from moldflow import MessageBox, MessageBoxType + + # Information message + MessageBox("Operation completed.", MessageBoxType.INFO).show() + + # Yes/No prompt + result = MessageBox("Proceed with analysis?", MessageBoxType.YES_NO).show() + if result == MessageBoxResult.YES: + ... + + # Text input + material_id = MessageBox("Enter your material ID:", MessageBoxType.INPUT).show() + if material_id: + ... + """ + + def __init__( + self, + text: str, + box_type: MessageBoxType = MessageBoxType.INFO, + title: Optional[str] = None, + options: Optional[MessageBoxOptions] = None, + ) -> None: + if platform.system() != "Windows": + raise OSError("MessageBox is only supported on Windows.") + self.text = str(text) + self.box_type = box_type + self.title = title or DEFAULT_TITLE + self.options = options or MessageBoxOptions() + + def show(self) -> MessageBoxReturn: + """ + Show the message box. + + Returns: + - MessageBoxResult for INFO/WARNING/ERROR/YES_NO/OK_CANCEL + - str | None for INPUT (user-entered text or None if cancelled) + """ + + if self.box_type == MessageBoxType.INPUT: + return self._show_input_dialog() + return self._show_standard_dialog() + + @classmethod + def info( + cls, text: str, title: Optional[str] = None, options: Optional[MessageBoxOptions] = None + ) -> MessageBoxResult: + """ + Show an informational message box with an OK button. + """ + inst = cls(text, MessageBoxType.INFO, title, options) + return inst.show() # type: ignore[return-value] + + @classmethod + def warning( + cls, text: str, title: Optional[str] = None, options: Optional[MessageBoxOptions] = None + ) -> MessageBoxResult: + """ + Show a warning message box with an OK button. + """ + inst = cls(text, MessageBoxType.WARNING, title, options) + return inst.show() # type: ignore[return-value] + + @classmethod + def error( + cls, text: str, title: Optional[str] = None, options: Optional[MessageBoxOptions] = None + ) -> MessageBoxResult: + """ + Show an error message box with an OK button. + """ + inst = cls(text, MessageBoxType.ERROR, title, options) + return inst.show() # type: ignore[return-value] + + @classmethod + def confirm_yes_no( + cls, text: str, title: Optional[str] = None, options: Optional[MessageBoxOptions] = None + ) -> MessageBoxResult: + """ + Show a confirmation message box with Yes/No buttons. + """ + return cls(text, MessageBoxType.YES_NO, title, options).show() # type: ignore[return-value] + + @classmethod + def prompt_text( # pylint: disable=too-many-arguments,too-many-positional-arguments + cls, + prompt: str, + title: Optional[str] = None, + default_text: Optional[str] = None, + placeholder: Optional[str] = None, + validator: Optional[Callable[[str], bool]] = None, + options: Optional[MessageBoxOptions] = None, + ) -> Optional[str]: + """ + Show a text input dialog. + """ + opts = options or MessageBoxOptions() + # Merge provided options with overrides for input UX + opts = MessageBoxOptions( + icon=opts.icon, + default_button=opts.default_button, + topmost=opts.topmost, + modality=opts.modality, + rtl_reading=opts.rtl_reading, + right_align=opts.right_align, + help_button=opts.help_button, + set_foreground=opts.set_foreground, + owner_hwnd=opts.owner_hwnd, + default_text=default_text if default_text is not None else opts.default_text, + placeholder=placeholder if placeholder is not None else opts.placeholder, + validator=validator if validator is not None else opts.validator, + font_face=opts.font_face, + font_size_pt=opts.font_size_pt, + ) + return cls(prompt, MessageBoxType.INPUT, title, opts).show() # type: ignore[return-value] + + def _show_standard_dialog(self) -> MessageBoxResult: + """ + Show a standard Win32 MessageBox dialog and return the result. + """ + # Use module-level ctypes imports to avoid reimport and name shadowing + + # Base type from box_type via module-level mapping dict + base_tuple = MAPPING_MESSAGEBOX_TYPE.get( + self.box_type, (WIN_MB_OK, MessageBoxIcon.INFORMATION, 1) + ) + u_type, default_icon, button_count = base_tuple + + # Icon selection (options override default) + icon = self.options.icon or default_icon + u_type |= ICON_TO_FLAG.get(icon, 0) + # NONE -> no icon flag + + # Default button + if self.options.default_button: + flag, required = DEFAULT_BUTTON_TO_FLAG.get(self.options.default_button, (0, 1)) + if button_count < required: + # The error message is intentionally descriptive; allow a + # slightly longer line here rather than make it unreadable. + # pylint: disable=line-too-long + raise ValueError( + f"default_button {self.options.default_button.name} requires >={required} buttons for {self.box_type.name}" + ) + u_type |= flag + + # Modality + if self.options.modality: + u_type |= MODALITY_TO_FLAG.get(self.options.modality, 0) + + # Z-order / positioning + if self.options.topmost: + u_type |= WIN_MB_TOPMOST + if self.options.set_foreground: + u_type |= WIN_MB_SETFOREGROUND + + # Layout + if self.options.right_align: + u_type |= WIN_MB_RIGHT + if self.options.rtl_reading: + u_type |= WIN_MB_RTLREADING + + # Help button + if self.options.help_button: + u_type |= WIN_MB_HELP + + owner = self.options.owner_hwnd or 0 + # Trim whitespace to avoid accidental spaces + text = (self.text or "").strip() + # Do not translate titles + title = (self.title or "").strip() + result = windll.user32.MessageBoxW(owner, c_wchar_p(text), c_wchar_p(title), c_int(u_type)) + if result == -1: + err = windll.kernel32.GetLastError() + raise ctypes.WinError(err) + + if result in ID_TO_RESULT: + return ID_TO_RESULT[result] + # Fallback + return MessageBoxResult.CANCEL + + def _show_input_dialog(self) -> Optional[str]: + """ + Show a text input dialog. + """ + dialog = _Win32InputDialog(self.title, self.text, self.options) + return dialog.run() + + +class _Win32InputDialog: + """ + Modal input dialog using DialogBoxIndirectParamW with an in-memory DLGTEMPLATE. + """ + + ID_EDIT = WIN_ID_EDIT + ID_OK = WIN_ID_OK + ID_CANCEL = WIN_ID_CANCEL + + DS_SETFONT = WIN_DS_SETFONT + DS_MODALFRAME = WIN_DS_MODALFRAME + WS_CAPTION = WIN_WS_CAPTION + WS_SYSMENU = WIN_WS_SYSMENU + + WS_CHILD = WIN_WS_CHILD + WS_VISIBLE = WIN_WS_VISIBLE + WS_TABSTOP = WIN_WS_TABSTOP + WS_GROUP = WIN_WS_GROUP + WS_BORDER = WIN_WS_BORDER + + ES_AUTOHSCROLL = WIN_ES_AUTOHSCROLL + ES_PASSWORD = WIN_ES_PASSWORD + SS_LEFT = WIN_SS_LEFT + BS_DEFPUSHBUTTON = WIN_BS_DEFPUSHBUTTON + BS_PUSHBUTTON = WIN_BS_PUSHBUTTON + + WM_INITDIALOG = WIN_WM_INITDIALOG + WM_COMMAND = WIN_WM_COMMAND + + def __init__(self, title: str, prompt: str, options: MessageBoxOptions) -> None: + self.title = title + self.prompt = prompt + self.options = options + self._result_text: Optional[str] = None + # Template buffer is created when running the dialog; initialize attribute + self._template_buffer: Optional[bytes] = None + + def _wcs(self, s: str) -> bytes: + """Return a UTF-16LE encoded, null-terminated bytestring for s.""" + return s.encode("utf-16le") + b"\x00\x00" + + def _align_dword(self, buf: bytearray) -> None: + """Pad buffer until its length is a multiple of 4 (DWORD alignment).""" + while len(buf) % 4 != 0: + buf += b"\x00" + + def _pack_word(self, buf: bytearray, val: int) -> None: + """Pack a 16-bit unsigned value into the buffer.""" + buf += struct.pack(" None: + """Pack a 32-bit unsigned value into the buffer.""" + buf += struct.pack(" None: + """Pack a 16-bit signed value into the buffer.""" + buf += struct.pack(" bytes: + # The dialog template is relatively verbose; allow pylint to accept the + # complexity here rather than refactor the Win32 packing code. + # pylint: disable=too-many-locals,too-many-statements + # Dialog units and layout + cx = self.options.width_dlu if self.options.width_dlu is not None else 240 + cy = self.options.height_dlu if self.options.height_dlu is not None else 70 + margin = 7 + static_h = 8 + edit_h = 12 + btn_w, btn_h = 50, 14 + spacing = 4 + + ok_x = cx - margin - (btn_w * 2 + spacing) + cancel_x = cx - margin - btn_w + # Position the edit box a bit lower from the label + edit_y = margin + static_h + 8 + # Move the buttons up: place them below the edit with extra spacing + btn_y = edit_y + edit_h + spacing * 2 + + buf = bytearray() + + style = ( + self.DS_MODALFRAME | self.DS_SETFONT | self.WS_CAPTION | self.WS_SYSMENU | WIN_WS_POPUP + ) + self._pack_dword(buf, style) # style + self._pack_dword(buf, 0) # dwExtendedStyle + self._pack_word(buf, 4) # cdit: static, edit, OK, Cancel + self._pack_short(buf, margin) # x + self._pack_short(buf, margin) # y + self._pack_short(buf, cx) # cx + self._pack_short(buf, cy) # cy + + self._pack_word(buf, 0) # menu = 0 + self._pack_word(buf, 0) # windowClass = 0 (default) + # Do not translate titles + buf += self._wcs(self.title) # title + + # Font (since DS_SETFONT) + self._pack_word(buf, max(6, int(self.options.font_size_pt))) # point size + buf += self._wcs(self.options.font_face or "Segoe UI") + + # DLGITEMTEMPLATEs must be DWORD-aligned + # 1) Static: prompt + self._align_dword(buf) + self._pack_dword(buf, self.WS_CHILD | self.WS_VISIBLE) + self._pack_dword(buf, 0) # ex style + self._pack_short(buf, margin) + self._pack_short(buf, margin) + self._pack_short(buf, cx - 2 * margin) + self._pack_short(buf, static_h) + self._pack_word(buf, 0) # id for static is usually 0 + # class: 0xFFFF, 0x0082 (STATIC) + self._pack_word(buf, 0xFFFF) + self._pack_word(buf, WIN_CLASS_STATIC) + # Do not translate prompt; callers pass text explicitly + buf += self._wcs(self.prompt) # title + self._pack_word(buf, 0) # no extra data + + # 2) Edit control + self._align_dword(buf) + edit_style = ( + self.WS_CHILD | self.WS_VISIBLE | self.WS_BORDER | self.ES_AUTOHSCROLL | self.WS_TABSTOP + ) + if self.options.is_password: + edit_style |= self.ES_PASSWORD + self._pack_dword(buf, edit_style) + self._pack_dword(buf, 0) + self._pack_short(buf, margin) + self._pack_short(buf, margin + static_h + 2) + self._pack_short(buf, cx - 2 * margin) + self._pack_short(buf, edit_h) + self._pack_word(buf, self.ID_EDIT) + # class: 0xFFFF, 0x0080 EDIT + self._pack_word(buf, 0xFFFF) + self._pack_word(buf, WIN_CLASS_EDIT) + self._pack_word(buf, 0) # empty text + self._pack_word(buf, 0) # no extra data + + _ = get_text() + + # 3) OK button (default) + self._align_dword(buf) + self._pack_dword( + buf, + self.WS_CHILD + | self.WS_VISIBLE + | self.WS_TABSTOP + | self.WS_GROUP + | self.BS_DEFPUSHBUTTON, + ) + self._pack_dword(buf, 0) + self._pack_short(buf, ok_x) + self._pack_short(buf, btn_y) + self._pack_short(buf, btn_w) + self._pack_short(buf, btn_h) + self._pack_word(buf, self.ID_OK) + self._pack_word(buf, 0xFFFF) + self._pack_word(buf, WIN_CLASS_BUTTON) + buf += self._wcs(_("OK")) + self._pack_word(buf, 0) + + # 4) Cancel button + self._align_dword(buf) + self._pack_dword( + buf, self.WS_CHILD | self.WS_VISIBLE | self.WS_TABSTOP | self.BS_PUSHBUTTON + ) + self._pack_dword(buf, 0) + self._pack_short(buf, cancel_x) + self._pack_short(buf, btn_y) + self._pack_short(buf, btn_w) + self._pack_short(buf, btn_h) + self._pack_word(buf, self.ID_CANCEL) + self._pack_word(buf, 0xFFFF) + self._pack_word(buf, WIN_CLASS_BUTTON) + buf += self._wcs(_("Cancel")) + self._pack_word(buf, 0) + + self._align_dword(buf) + return bytes(buf) + + def run(self) -> Optional[str]: + """Create and run a modal input window using CreateWindowEx.""" + # pylint: disable=too-many-locals,too-many-branches,too-many-statements,invalid-name + user32 = windll.user32 + gdi32 = windll.gdi32 + kernel32 = windll.kernel32 + + # Win32 function prototypes used + try: + user32.CreateWindowExW.restype = wintypes.HWND + user32.CreateWindowExW.argtypes = [ + wintypes.DWORD, + c_wchar_p, + c_wchar_p, + wintypes.DWORD, + ctypes.c_int, + ctypes.c_int, + ctypes.c_int, + ctypes.c_int, + wintypes.HWND, + wintypes.HMENU, + wintypes.HINSTANCE, + wintypes.LPVOID, + ] + user32.DefWindowProcW.restype = wintypes.LRESULT + user32.DefWindowProcW.argtypes = [ + wintypes.HWND, + wintypes.UINT, + wintypes.WPARAM, + wintypes.LPARAM, + ] + user32.RegisterClassW.restype = wintypes.ATOM + except Exception as exc: + logger = get_logger("message_box") + if logger: + logger.debug("Win32 prototype binding failed: %s", exc) + + # Register window class once + class_name = "MF_InputDialogWindow" + if not hasattr(_Win32InputDialog, "_class_registered"): + WNDPROC = WINFUNCTYPE( + wintypes.LRESULT, wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM + ) + + @WNDPROC + def _wndproc(hwnd, msg, wparam, lparam): + # Retrieve instance from map if present + inst = _Win32InputDialog._hwnd_to_inst.get(hwnd) + if msg == WIN_WM_CLOSE: + windll.user32.DestroyWindow(hwnd) + return 0 + if msg == WIN_WM_KEYDOWN and inst is not None: + if wparam == WIN_VK_RETURN: + inst._on_ok() + return 0 + if wparam == WIN_VK_ESCAPE: + inst._on_cancel() + return 0 + if msg == 0x0002: # WM_DESTROY + if inst is not None: + # Defer destruction finalization slightly to allow any + # late WM_COMMAND or automation posts to drain safely. + try: + inst._on_destroy() + except Exception as exc: + logger = get_logger("message_box") + if logger: + logger.debug("_on_destroy raised: %s", exc) + return 0 + if msg == 0x0082: # WM_NCDESTROY + try: + if inst is not None: + inst._done = True # type: ignore[attr-defined] + _Win32InputDialog._hwnd_to_inst.pop(hwnd, None) + # Ensure the modal loop unblocks even if no further messages arrive + user32.PostQuitMessage(0) + except Exception as exc: + logger = get_logger("message_box") + if logger: + logger.debug("WM_NCDESTROY cleanup failed: %s", exc) + return 0 + if inst is None: + return user32.DefWindowProcW(hwnd, msg, wparam, lparam) + if msg == 0x0005: # WM_SIZE + inst._on_size() + return 0 + if msg == WIN_WM_CTLCOLORSTATIC: + # Make label background match dialog background for a flat look + try: + windll.gdi32.SetBkMode(wparam, 1) # TRANSPARENT + except Exception as exc: + logger = get_logger("message_box") + if logger: + logger.debug("SetBkMode transparent failed: %s", exc) + return getattr(_Win32InputDialog, "_bg_brush", 0) + if msg == _Win32InputDialog.WM_COMMAND: + cid = wparam & 0xFFFF + notify = (wparam >> 16) & 0xFFFF + # Ignore commands from unknown HWNDs to avoid processing + # stale messages after controls are destroyed. + if lparam not in (inst.h_edit, inst.h_ok, inst.h_cancel): + return 0 + if ( + notify == WIN_EN_CHANGE + and inst.options.validator is not None + and lparam == inst.h_edit + ): + inst._validate_live() + return 0 + if cid == inst.ID_OK: + inst._on_ok() + return 0 + if cid == inst.ID_CANCEL: + inst._on_cancel() + return 0 + return user32.DefWindowProcW(hwnd, msg, wparam, lparam) + + _Win32InputDialog._WNDPROC = _wndproc # type: ignore[attr-defined] + + class WNDCLASSEX(ctypes.Structure): + """WNDCLASSEX structure""" + + _fields_ = [ + ("cbSize", wintypes.UINT), + ("style", wintypes.UINT), + ("lpfnWndProc", WNDPROC), + ("cbClsExtra", ctypes.c_int), + ("cbWndExtra", ctypes.c_int), + ("hInstance", wintypes.HINSTANCE), + ("hIcon", wintypes.HICON), + ("hCursor", wintypes.HCURSOR), + ("hbrBackground", wintypes.HBRUSH), + ("lpszMenuName", c_wchar_p), + ("lpszClassName", c_wchar_p), + ("hIconSm", wintypes.HICON), + ] + + # Prototypes for class registration + try: + user32.RegisterClassExW.restype = wintypes.ATOM + user32.RegisterClassExW.argtypes = [ctypes.POINTER(WNDCLASSEX)] + user32.LoadCursorW.restype = wintypes.HCURSOR + # Second parameter is MAKEINTRESOURCE on system cursors; accept as void* + user32.LoadCursorW.argtypes = [wintypes.HINSTANCE, ctypes.c_void_p] + except Exception as exc: + logger = get_logger("message_box") + if logger: + logger.debug("RegisterClassEx/LoadCursor prototype bind failed: %s", exc) + + hInstance = kernel32.GetModuleHandleW(None) + wcx = WNDCLASSEX() + wcx.cbSize = ctypes.sizeof(WNDCLASSEX) + wcx.style = 0 + wcx.lpfnWndProc = _Win32InputDialog._WNDPROC # type: ignore[attr-defined] + wcx.cbClsExtra = 0 + wcx.cbWndExtra = 0 + wcx.hInstance = hInstance + wcx.hIcon = None + # IDC_ARROW = 32512 (0x7F00). Pass as MAKEINTRESOURCE via c_void_p + wcx.hCursor = windll.user32.LoadCursorW(None, ctypes.c_void_p(32512)) + # Use COLOR_WINDOW+1 to avoid theme brush quirks under automation + wcx.hbrBackground = ctypes.c_void_p(5 + 1) + wcx.lpszMenuName = None + wcx.lpszClassName = class_name + wcx.hIconSm = None + res = user32.RegisterClassExW(ctypes.byref(wcx)) + # If already registered, res==0 with last error 1410 (ERROR_CLASS_ALREADY_EXISTS) + if not res: + err = kernel32.GetLastError() + if err != 1410: # ERROR_CLASS_ALREADY_EXISTS + raise ctypes.WinError(err) + _Win32InputDialog._class_registered = True # type: ignore[attr-defined] + _Win32InputDialog._class_name = class_name # type: ignore[attr-defined] + _Win32InputDialog._hwnd_to_inst = {} # type: ignore[attr-defined] + # Cache background brush so STATIC controls can paint with same bg + try: + _Win32InputDialog._bg_brush = int(wcx.hbrBackground) # type: ignore[attr-defined] + except Exception as exc: + _Win32InputDialog._bg_brush = 0 # type: ignore[attr-defined] + logger = get_logger("message_box") + if logger: + logger.debug("Caching bg brush failed: %s", exc) + + # Create window + style = ( + self.WS_CAPTION + | self.WS_SYSMENU + | WIN_WS_POPUP + | WIN_WS_THICKFRAME + | WIN_WS_MINIMIZEBOX + | WIN_WS_MAXIMIZEBOX + ) + ex_style = WIN_WS_EX_DLGMODALFRAME | WIN_WS_EX_CONTROLPARENT + # Avoid cross-thread/process owner interactions; keep window independent + owner = 0 + + # Size and layout (pixels) + # Slightly larger default size so action buttons are always visible + cx = int(self.options.width_dlu if self.options.width_dlu is not None else 420) + cy = int(self.options.height_dlu if self.options.height_dlu is not None else 220) + margin = 36 + static_h = 22 + edit_h = 22 + btn_w, btn_h = 96, 28 + spacing = 16 + + ok_x = cx - margin - (btn_w * 2 + spacing) + cancel_x = cx - margin - btn_w + edit_y = margin + static_h + 8 + btn_y = edit_y + edit_h + spacing * 2 + + # Persist layout metrics for resize handling + self._layout_margin = margin # type: ignore[attr-defined] + self._layout_spacing = spacing # type: ignore[attr-defined] + self._layout_edit_h = edit_h # type: ignore[attr-defined] + self._layout_btn_w = btn_w # type: ignore[attr-defined] + self._layout_btn_h = btn_h # type: ignore[attr-defined] + + hInstance = kernel32.GetModuleHandleW(None) + hwnd = user32.CreateWindowExW( + ex_style, + c_wchar_p(getattr(_Win32InputDialog, "_class_name", class_name)), + c_wchar_p(self.title), + style, + 100, + 100, + cx, + cy, + None, + None, + hInstance, + None, + ) + if not hwnd: + err = kernel32.GetLastError() + raise ctypes.WinError(err) + + # Map hwnd to instance + _Win32InputDialog._hwnd_to_inst[hwnd] = self # type: ignore[attr-defined] + self.hwnd = hwnd # type: ignore[attr-defined] + + # Allow Ctrl+C in the console to close the window gracefully (both + # Python-level SIGINT and native console control handler for immediate response) + def _sigint_handler(_signum, _frame): + try: + user32.PostMessageW(self.hwnd, WIN_WM_CLOSE, 0, 0) # type: ignore[attr-defined] + except Exception as exc: + logger = get_logger("message_box") + if logger: + logger.debug("Posting WM_CLOSE on SIGINT failed: %s", exc) + + try: + self._prev_sigint = signal.getsignal(signal.SIGINT) # type: ignore[attr-defined] + signal.signal(signal.SIGINT, _sigint_handler) + except Exception as exc: + logger = get_logger("message_box") + if logger: + logger.debug("Setting SIGINT handler failed: %s", exc) + + # Native console control handler (fires immediately even while Python blocks) + try: + HANDLER_ROUTINE = WINFUNCTYPE(wintypes.BOOL, wintypes.DWORD) + + @HANDLER_ROUTINE + def _console_ctrl_handler(_ctrl_type): # CTRL_C_EVENT, CTRL_BREAK_EVENT, etc. + try: + user32.PostMessageW(self.hwnd, WIN_WM_CLOSE, 0, 0) # type: ignore[attr-defined] + except Exception as exc: + logger = get_logger("message_box") + if logger: + logger.debug("Posting WM_CLOSE on console control failed: %s", exc) + return True + + kernel32.SetConsoleCtrlHandler.restype = wintypes.BOOL + kernel32.SetConsoleCtrlHandler.argtypes = [HANDLER_ROUTINE, wintypes.BOOL] + kernel32.SetConsoleCtrlHandler(_console_ctrl_handler, True) + self._console_ctrl_handler = _console_ctrl_handler # type: ignore[attr-defined] + except Exception as exc: + logger = get_logger("message_box") + if logger: + logger.debug("Setting console control handler failed: %s", exc) + + # Create child controls + self.h_static = user32.CreateWindowExW( # type: ignore[attr-defined] + 0, + c_wchar_p("STATIC"), + c_wchar_p(self.prompt), + self.WS_CHILD | self.WS_VISIBLE | self.SS_LEFT, + margin, + margin, + cx - 2 * margin, + static_h, + hwnd, + wintypes.HMENU(0), + hInstance, + None, + ) + edit_style = ( + self.WS_CHILD | self.WS_VISIBLE | self.WS_BORDER | self.ES_AUTOHSCROLL | self.WS_TABSTOP + ) + if self.options.is_password: + edit_style |= self.ES_PASSWORD + self.h_edit = user32.CreateWindowExW( # type: ignore[attr-defined] + 0, + c_wchar_p("EDIT"), + c_wchar_p(""), + edit_style, + margin, + edit_y, + cx - 2 * margin, + edit_h, + hwnd, + wintypes.HMENU(self.ID_EDIT), + hInstance, + None, + ) + _ = get_text() + self.h_ok = user32.CreateWindowExW( # type: ignore[attr-defined] + 0, + c_wchar_p("BUTTON"), + c_wchar_p(_("Submit")), + self.WS_CHILD | self.WS_VISIBLE | self.WS_TABSTOP | self.BS_DEFPUSHBUTTON, + ok_x, + btn_y, + btn_w, + btn_h, + hwnd, + wintypes.HMENU(self.ID_OK), + hInstance, + None, + ) + self.h_cancel = user32.CreateWindowExW( # type: ignore[attr-defined] + 0, + c_wchar_p("BUTTON"), + c_wchar_p(_("Cancel")), + self.WS_CHILD | self.WS_VISIBLE | self.WS_TABSTOP | self.BS_PUSHBUTTON, + cancel_x, + btn_y, + btn_w, + btn_h, + hwnd, + wintypes.HMENU(self.ID_CANCEL), + hInstance, + None, + ) + + # Apply a system dialog font for consistent look and spacing + try: + DEFAULT_GUI_FONT = 17 + hfont = windll.gdi32.GetStockObject(DEFAULT_GUI_FONT) + if hfont: + # Send WM_SETFONT to children so they repaint with the font + for hchild in (self.h_static, self.h_edit, self.h_ok, self.h_cancel): # type: ignore[attr-defined] + if hchild: + user32.SendMessageW(hchild, WIN_WM_SETFONT, hfont, 1) + # Keep a reference so it survives until window is destroyed + self._hfont = hfont # type: ignore[attr-defined] + + # Adjust edit height to match font metrics so caret is visually centered + class TEXTMETRICW(ctypes.Structure): + """TEXTMETRICW structure""" + + _fields_ = [ + ("tmHeight", ctypes.c_long), + ("tmAscent", ctypes.c_long), + ("tmDescent", ctypes.c_long), + ("tmInternalLeading", ctypes.c_long), + ("tmExternalLeading", ctypes.c_long), + ("tmAveCharWidth", ctypes.c_long), + ("tmMaxCharWidth", ctypes.c_long), + ("tmWeight", ctypes.c_long), + ("tmOverhang", ctypes.c_long), + ("tmDigitizedAspectX", ctypes.c_long), + ("tmDigitizedAspectY", ctypes.c_long), + # Next four fields are WCHAR in the Win32 API, keep for structure parity + ("tmFirstChar", ctypes.c_wchar), + ("tmLastChar", ctypes.c_wchar), + ("tmDefaultChar", ctypes.c_wchar), + ("tmBreakChar", ctypes.c_wchar), + ("tmItalic", ctypes.c_ubyte), + ("tmUnderlined", ctypes.c_ubyte), + ("tmStruckOut", ctypes.c_ubyte), + ("tmPitchAndFamily", ctypes.c_ubyte), + ("tmCharSet", ctypes.c_ubyte), + ] + + hdc_edit = user32.GetDC(self.h_edit) + if hdc_edit: + try: + prev = gdi32.SelectObject(hdc_edit, hfont) + tm = TEXTMETRICW() + if gdi32.GetTextMetricsW(hdc_edit, ctypes.byref(tm)): + desired_h = int(tm.tmHeight + tm.tmExternalLeading + 6) + desired_h = max(desired_h, 18) + # Resize edit control to the desired height and keep x/width constant + user32.SetWindowPos( + self.h_edit, + 0, + margin, + edit_y, + cx - 2 * margin, + desired_h, + WIN_SWP_NOZORDER, + ) + # Reposition buttons directly below the edit + new_btn_y = edit_y + desired_h + spacing * 2 + user32.SetWindowPos( + self.h_ok, + 0, + ok_x, + new_btn_y, + 0, + 0, + WIN_SWP_NOSIZE | WIN_SWP_NOZORDER, + ) + user32.SetWindowPos( + self.h_cancel, + 0, + cancel_x, + new_btn_y, + 0, + 0, + WIN_SWP_NOSIZE | WIN_SWP_NOZORDER, + ) + if prev: + gdi32.SelectObject(hdc_edit, prev) + finally: + user32.ReleaseDC(self.h_edit, hdc_edit) + + # Recalculate static height for long titles and wrap + hdc_static = user32.GetDC(self.h_static) + if hdc_static: + try: + prev2 = gdi32.SelectObject(hdc_static, hfont) + rect = wintypes.RECT() + rect.left = 0 + rect.top = 0 + rect.right = cx - 2 * margin + rect.bottom = 1000 + user32.DrawTextW( + hdc_static, + c_wchar_p(self.prompt), + -1, + byref(rect), + WIN_DT_WORDBREAK | WIN_DT_CALCRECT | WIN_DT_NOPREFIX, + ) + new_static_h = max(static_h, rect.bottom - rect.top) + if new_static_h != static_h: + # Resize static and move controls below it + user32.SetWindowPos( + self.h_static, + 0, + margin, + margin, + cx - 2 * margin, + new_static_h, + WIN_SWP_NOZORDER, + ) + new_edit_y = margin + new_static_h + 8 + user32.SetWindowPos( + self.h_edit, + 0, + margin, + new_edit_y, + 0, + 0, + WIN_SWP_NOSIZE | WIN_SWP_NOZORDER, + ) + new_btn_y = new_edit_y + edit_h + spacing * 2 + user32.SetWindowPos( + self.h_ok, + 0, + ok_x, + new_btn_y, + 0, + 0, + WIN_SWP_NOSIZE | WIN_SWP_NOZORDER, + ) + user32.SetWindowPos( + self.h_cancel, + 0, + cancel_x, + new_btn_y, + 0, + 0, + WIN_SWP_NOSIZE | WIN_SWP_NOZORDER, + ) + if prev2: + gdi32.SelectObject(hdc_static, prev2) + finally: + user32.ReleaseDC(self.h_static, hdc_static) + except Exception as exc: + logger = get_logger("message_box") + if logger: + logger.debug("Applying default GUI font failed: %s", exc) + + # Defaults + if self.options.default_text: + user32.SetWindowTextW(self.h_edit, c_wchar_p(self.options.default_text)) + if self.options.placeholder: + try: + user32.SendMessageW( + self.h_edit, WIN_EM_SETCUEBANNER, 1, c_wchar_p(self.options.placeholder) + ) + except Exception as exc: + logger = get_logger("message_box") + if logger: + logger.debug("Setting placeholder text failed: %s", exc) + if self.options.char_limit is not None: + user32.SendMessageW(self.h_edit, WIN_EM_LIMITTEXT, self.options.char_limit, 0) + + # Initial validation + if self.options.validator is not None: + self._validate_live() + + # Center over owner + try: + owner_hwnd = owner or user32.GetActiveWindow() + if owner_hwnd: + rect = wintypes.RECT() + user32.GetWindowRect(owner_hwnd, byref(rect)) + owner_cx = rect.right - rect.left + owner_cy = rect.bottom - rect.top + wnd_rect = wintypes.RECT() + user32.GetWindowRect(hwnd, byref(wnd_rect)) + x = rect.left + (owner_cx - (wnd_rect.right - wnd_rect.left)) // 2 + y = rect.top + (owner_cy - (wnd_rect.bottom - wnd_rect.top)) // 2 + user32.SetWindowPos( + hwnd, 0, x, y, 0, 0, WIN_SWP_NOSIZE | WIN_SWP_NOZORDER | WIN_SWP_NOACTIVATE + ) + except Exception as exc: + logger = get_logger("message_box") + if logger: + logger.debug("Centering dialog over owner failed: %s", exc) + + user32.ShowWindow(hwnd, 5) # SW_SHOW + try: + user32.UpdateWindow(hwnd) + except Exception as exc: + logger = get_logger("message_box") + if logger: + logger.debug("UpdateWindow failed: %s", exc) + if self.h_edit: + user32.SetFocus(self.h_edit) + + # Modal loop + self._done = False # type: ignore[attr-defined] + msg = wintypes.MSG() + while not self._done: + ret = user32.GetMessageW(byref(msg), 0, 0, 0) + if ret == 0: # WM_QUIT + break + if ret == -1: + break + # Let the system process default button (Enter), Esc, and Tab order + if not user32.IsDialogMessageW(hwnd, byref(msg)): + user32.TranslateMessage(byref(msg)) + user32.DispatchMessageW(byref(msg)) + + # No owner to restore + # Restore previous SIGINT handler + try: + prev = getattr(self, "_prev_sigint", None) + if prev is not None: + signal.signal(signal.SIGINT, prev) + except Exception as exc: + logger = get_logger("message_box") + if logger: + logger.debug("Restoring SIGINT handler failed: %s", exc) + # Remove native console handler + try: + handler = getattr(self, "_console_ctrl_handler", None) + if handler is not None: + kernel32.SetConsoleCtrlHandler(handler, False) + except Exception as exc: + logger = get_logger("message_box") + if logger: + logger.debug("Removing console control handler failed: %s", exc) + return self._result_text + + # Helper methods for WNDPROC + def _on_ok(self) -> None: + user32 = windll.user32 + length = user32.GetWindowTextLengthW(self.h_edit) # type: ignore[attr-defined] + buf = create_unicode_buffer(length + 1) + user32.GetWindowTextW(self.h_edit, buf, length + 1) # type: ignore[attr-defined] + self._result_text = buf.value + user32.DestroyWindow(self.hwnd) # type: ignore[attr-defined] + self._done = True # type: ignore[attr-defined] + + def _on_cancel(self) -> None: + user32 = windll.user32 + self._result_text = None + user32.DestroyWindow(self.hwnd) # type: ignore[attr-defined] + self._done = True # type: ignore[attr-defined] + + def _on_destroy(self) -> None: + self._done = True # type: ignore[attr-defined] + + def _on_size(self) -> None: + # Reflow controls on window resize + try: + user32 = windll.user32 + rect = wintypes.RECT() + user32.GetClientRect(self.hwnd, byref(rect)) # type: ignore[attr-defined] + cx = rect.right - rect.left + + margin = getattr(self, "_layout_margin", 24) + spacing = getattr(self, "_layout_spacing", 12) + btn_w = getattr(self, "_layout_btn_w", 88) + btn_h = getattr(self, "_layout_btn_h", 26) + + # Static keeps same height; stretch width + # Measure static height + static_rect = wintypes.RECT() + user32.GetWindowRect(self.h_static, byref(static_rect)) # type: ignore[attr-defined] + static_h = static_rect.bottom - static_rect.top + user32.SetWindowPos(self.h_static, 0, margin, margin, cx - 2 * margin, static_h, WIN_SWP_NOZORDER) # type: ignore[attr-defined] + + # Edit stretches horizontally, stays below static + edit_y = margin + static_h + 8 + # Preserve current edit height + cur_edit_rect = wintypes.RECT() + user32.GetWindowRect(self.h_edit, byref(cur_edit_rect)) # type: ignore[attr-defined] + cur_edit_h = cur_edit_rect.bottom - cur_edit_rect.top + user32.SetWindowPos(self.h_edit, 0, margin, edit_y, cx - 2 * margin, cur_edit_h, WIN_SWP_NOZORDER) # type: ignore[attr-defined] + + # Buttons right-aligned + cancel_x = cx - margin - btn_w + ok_x = cancel_x - spacing - btn_w + btn_y = edit_y + cur_edit_h + spacing * 2 + user32.SetWindowPos(self.h_ok, 0, ok_x, btn_y, btn_w, btn_h, WIN_SWP_NOZORDER) # type: ignore[attr-defined] + user32.SetWindowPos(self.h_cancel, 0, cancel_x, btn_y, btn_w, btn_h, WIN_SWP_NOZORDER) # type: ignore[attr-defined] + except Exception as exc: + logger = get_logger("message_box") + if logger: + logger.debug("Resize reflow failed: %s", exc) + + def _validate_live(self) -> None: + user32 = windll.user32 + length = user32.GetWindowTextLengthW(self.h_edit) # type: ignore[attr-defined] + buf = create_unicode_buffer(length + 1) + user32.GetWindowTextW(self.h_edit, buf, length + 1) # type: ignore[attr-defined] + try: + is_valid = bool(self.options.validator(buf.value)) if self.options.validator else True + except Exception as exc: + logger = get_logger("message_box") + if logger: + logger.debug("Validator raised exception: %s", exc) + is_valid = True + user32.EnableWindow(self.h_ok, wintypes.BOOL(1 if is_valid else 0)) # type: ignore[attr-defined] diff --git a/tests/api/integration_tests/test_message_box_permutations.py b/tests/api/integration_tests/test_message_box_permutations.py new file mode 100644 index 0000000..a29bddc --- /dev/null +++ b/tests/api/integration_tests/test_message_box_permutations.py @@ -0,0 +1,196 @@ +"""Integration tests for message box permutations (Windows only).""" + +# These tests perform UI automation and include short-lived closures and +# complex helper logic. Suppress a few linter warnings that are noisy for +# this test file and do not improve correctness: +# pylint: disable=cell-var-from-loop,unused-argument,too-many-branches,too-many-statements + +import threading +import time +import ctypes +from ctypes import windll, c_wchar_p, wintypes + +from moldflow import ( + MessageBox, + MessageBoxType, + MessageBoxResult, + MessageBoxOptions, + MessageBoxIcon, + MessageBoxDefaultButton, + MessageBoxModality, +) + +# Win32 constants for automation +WM_COMMAND = 0x0111 +BM_CLICK = 0x00F5 +IDOK = 1 +IDCANCEL = 2 +IDYES = 6 +IDNO = 7 +IDRETRY = 4 + + +def _click_dialog_button_async(dialog_title: str, button_id: int, delay_s: float = 0.4) -> None: + """Helper: simulate clicking a button on a dialog by title after a small delay.""" + + def _worker(): + user32 = windll.user32 + # Wait a moment for the dialog to appear + time.sleep(delay_s) + # Try to find and click for up to ~5 seconds + for _ in range(100): + hwnd = user32.FindWindowW(None, c_wchar_p(dialog_title)) + if hwnd: + # Try to find child button control and click it directly + try: + hbtn = user32.GetDlgItem(hwnd, button_id) + if hbtn: + # Prefer PostMessage to avoid synchronous reentrancy + user32.PostMessageW(hbtn, BM_CLICK, 0, 0) + return + + children = [] + + @ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM) + def _child_enum_proc(hchild, _): + # Get class name + cname_buf = ctypes.create_unicode_buffer(256) + user32.GetClassNameW(hchild, cname_buf, 256) + cname = cname_buf.value + # Get text + tbuf = ctypes.create_unicode_buffer(512) + user32.GetWindowTextW(hchild, tbuf, 512) + text = tbuf.value + # Get control id + try: + cid = user32.GetDlgCtrlID(hchild) + except Exception: + cid = 0 + children.append((hchild, cname, text, cid)) + return True + + user32.EnumChildWindows(hwnd, _child_enum_proc, 0) + + # Try to click first Button child + for hchild, cname, _, _ in children: + if cname and cname.lower().startswith("button"): + user32.PostMessageW(hchild, BM_CLICK, 0, 0) + return + + except Exception: + # Fallback to posting WM_COMMAND + try: + user32.PostMessageW(hwnd, WM_COMMAND, button_id, 0) + return + except Exception: + pass + else: + # Fallback: enumerate top-level windows and try to find one whose + # title contains the dialog title as a substring (more tolerant). + try: + found = [] + + @ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM) + def _enum_proc(h, _): + buf = ctypes.create_unicode_buffer(512) + user32.GetWindowTextW(h, buf, 512) + txt = buf.value + if txt and dialog_title in txt: + found.append(h) + return False # stop enumeration + return True + + user32.EnumWindows(_enum_proc, 0) + if found: + hwnd = found[0] + try: + hbtn = user32.GetDlgItem(hwnd, button_id) + if hbtn: + user32.PostMessageW(hbtn, BM_CLICK, 0, 0) + return + except Exception: + try: + user32.PostMessageW(hwnd, WM_COMMAND, button_id, 0) + return + except Exception: + pass + except Exception: + # EnumWindows may fail in some restricted contexts; ignore + pass + time.sleep(0.1) + + threading.Thread(target=_worker, daemon=True).start() + + +def _iter_types_and_defaults(): + """Yield (type, valid default_button flags, button_id_to_click).""" + mapping = { + MessageBoxType.INFO: (1, IDOK), + MessageBoxType.WARNING: (1, IDOK), + MessageBoxType.ERROR: (1, IDOK), + MessageBoxType.OK_CANCEL: (2, IDCANCEL), + MessageBoxType.YES_NO: (2, IDYES), + MessageBoxType.RETRY_CANCEL: (2, IDCANCEL), + MessageBoxType.YES_NO_CANCEL: (3, IDYES), + MessageBoxType.ABORT_RETRY_IGNORE: (3, IDRETRY), + MessageBoxType.CANCEL_TRY_CONTINUE: (3, IDCANCEL), + } + for t, (count, click_id) in mapping.items(): + defaults = [None] + if count >= 2: + defaults.append(MessageBoxDefaultButton.BUTTON2) + if count >= 3: + defaults.append(MessageBoxDefaultButton.BUTTON3) + if count >= 4: + defaults.append(MessageBoxDefaultButton.BUTTON4) + yield t, defaults, click_id + + +def test_message_box_permutations(): + """Exercise combinations of types, icons, default buttons and modality.""" + + icons = [ + None, + MessageBoxIcon.INFORMATION, + MessageBoxIcon.WARNING, + MessageBoxIcon.ERROR, + MessageBoxIcon.QUESTION, + ] + modalities = [None, MessageBoxModality.TASK, MessageBoxModality.SYSTEM] + + for box_type, default_buttons, click_id in _iter_types_and_defaults(): + for icon in icons: + for default_button in default_buttons: + for modality in modalities: + opts = MessageBoxOptions( + icon=icon, default_button=default_button, modality=modality + ) + title = f"Test: {box_type.name}" + # Auto click to allow unattended run + _click_dialog_button_async(title, click_id) + msg = ( + f"{box_type.name} - {getattr(icon, 'name', 'NONE')} - " + f"{getattr(default_button, 'name', 'BUTTON1')} - " + f"{getattr(modality, 'name', 'APPLICATION')}" + ) + result = MessageBox(msg, box_type, title=title, options=opts).show() + assert isinstance(result, MessageBoxResult) + + +def test_message_box_input_variants(): + """ + Exercise input dialog with various options. + """ + variants = [ + MessageBoxOptions(default_text="auto"), + MessageBoxOptions(default_text="auto", is_password=True), + MessageBoxOptions(default_text="auto", char_limit=10), + MessageBoxOptions(default_text="auto", width_dlu=280, height_dlu=90), + ] + for i, opts in enumerate(variants, 1): + title = f"Test: INPUT #{i}" + _click_dialog_button_async(title, IDOK) + value = MessageBox( + "Enter sample text", MessageBoxType.INPUT, title=title, options=opts + ).show() + assert isinstance(value, (str, type(None))) diff --git a/version.json b/version.json index 9e2172b..14ab214 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { "major": "26", "minor": "0", - "patch": "1" + "patch": "2" } From c8e07890fd6d856be1bd8906de74bcd5c80fa584 Mon Sep 17 00:00:00 2001 From: osinjoku <49887472+osinjoku@users.noreply.github.com> Date: Wed, 29 Oct 2025 09:24:50 +1100 Subject: [PATCH 04/26] Fix readme links on pypi release 26 (#28) * Fix links in README (#27) * fix links * changelog * new pypi deployment --- CHANGELOG.md | 20 ++++++++++++++++++++ README.md | 11 +++++------ version.json | 2 +- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9806e3c..787134a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security - N/A +## [26.0.3] + +### Added +- N/A + +### Changed +- N/A + +### Deprecated +- N/A + +### Removed +- N/A + +### Fixed +- Fixed README links + +### Security +- N/A + ## [26.0.2] - 2025-10-10 ### Added diff --git a/README.md b/README.md index 4632ecf..349d83d 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ python run.py build-docs Options: - `--skip-build` (`-s`): Skip building before generating docs -The documentation can be accessed locally by opening the [index.html](docs/build/html/index.html) in the [html](docs/build/html/) folder. +The documentation can be accessed locally by opening the index.html in the docs/build/html/ folder. ### Running the Formatter @@ -145,7 +145,7 @@ Key modules include: ## Contributing -We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details on how to contribute to this project. Here's a quick overview: +We welcome contributions! Please see our [Contributing Guide](https://github.com/Autodesk/moldflow-api/blob/main/CONTRIBUTING.md) for details on how to contribute to this project. Here's a quick overview: 1. Fork the repository 2. Create a feature branch (`git checkout -b feature/amazing-feature`) @@ -161,15 +161,14 @@ We use [Semantic Versioning](https://semver.org/). For available versions, see t ## License -This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. +This project is licensed under the Apache License 2.0 - see the [LICENSE](https://github.com/Autodesk/moldflow-api/blob/main/LICENSE) file for details. ## Support - **Documentation**: [Full documentation available online](https://autodesk.github.io/moldflow-api) - **Issues**: Report bugs and request features through [GitHub Issues](https://github.com/Autodesk/moldflow-api/issues) -- **Security**: For security issues, please see our [Security Policy](SECURITY.md) -- **Discussions**: Join our [GitHub Discussions](https://github.com/Autodesk/moldflow-api/discussions) for questions and community support +- **Security**: For security issues, please see our [Security Policy](https://github.com/Autodesk/moldflow-api/blob/main/SECURITY.md) ## Code of Conduct -This project adheres to the Contributor Covenant [code of conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. +This project adheres to the Contributor Covenant [code of conduct](https://github.com/Autodesk/moldflow-api/blob/main/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. diff --git a/version.json b/version.json index 14ab214..e6f778a 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { "major": "26", "minor": "0", - "patch": "2" + "patch": "3" } From 86035b80bdbbc781bcb614dffe30b3d1c9cfad82 Mon Sep 17 00:00:00 2001 From: Sankalp Shrivastava Date: Thu, 11 Dec 2025 16:00:47 +0530 Subject: [PATCH 05/26] Expose GeomType enum in package __init__.py and Enum Value correction (#41) * Expose GeomType enum in package __init__.py (#40) * Initial plan * Add GeomType to package exports in __init__.py - Added GeomType import to src/moldflow/__init__.py in alphabetical order - Updated test_unit_mesh_generator.py to import GeomType from moldflow instead of moldflow.common - Users can now use: from moldflow import GeomType Co-authored-by: sankalps0549 <230025240+sankalps0549@users.noreply.github.com> * Update CHANGELOG.md with GeomType fix Added entry under Unreleased > Fixed section documenting the GeomType enum exposure fix Co-authored-by: osinjoku <49887472+osinjoku@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sankalps0549 <230025240+sankalps0549@users.noreply.github.com> Co-authored-by: osinjoku <49887472+osinjoku@users.noreply.github.com> * Version bump * Fix GeomType enum value and add missing return types to MeshGenerator (#43) * Initial plan * Fix GeomType enum and add return types to MeshGenerator methods Co-authored-by: sankalps0549 <230025240+sankalps0549@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sankalps0549 <230025240+sankalps0549@users.noreply.github.com> * Update CHANGELOG.md with complete entries for PR#41 fixes (#44) * Initial plan * Add issue references to CHANGELOG.md for GeomType fix Co-authored-by: sankalps0549 <230025240+sankalps0549@users.noreply.github.com> * Add complete changelog entries for all PR#41 fixes Co-authored-by: sankalps0549 <230025240+sankalps0549@users.noreply.github.com> * Remove issue reference links from CHANGELOG.md Co-authored-by: sankalps0549 <230025240+sankalps0549@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sankalps0549 <230025240+sankalps0549@users.noreply.github.com> * Update CHANGELOG for release 26.0.4 (#45) * Initial plan * Update CHANGELOG.md to reflect release 26.0.4 Co-authored-by: osinjoku <49887472+osinjoku@users.noreply.github.com> * Update version links to use release tags instead of compare URLs Co-authored-by: osinjoku <49887472+osinjoku@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: osinjoku <49887472+osinjoku@users.noreply.github.com> --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: sankalps0549 <230025240+sankalps0549@users.noreply.github.com> Co-authored-by: osinjoku <49887472+osinjoku@users.noreply.github.com> --- CHANGELOG.md | 26 ++++++++++++++++++- src/moldflow/__init__.py | 1 + src/moldflow/common.py | 2 +- src/moldflow/mesh_generator.py | 10 +++++-- .../unit_tests/test_unit_mesh_generator.py | 4 +-- version.json | 2 +- 6 files changed, 38 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 787134a..7301023 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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 @@ -93,7 +115,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.2...HEAD +[Unreleased]: https://github.com/Autodesk/moldflow-api/compare/v26.0.4...HEAD +[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 [26.0.0]: https://github.com/Autodesk/moldflow-api/releases/tag/v26.0.0 diff --git a/src/moldflow/__init__.py b/src/moldflow/__init__.py index 0a8f1cc..d43f07a 100644 --- a/src/moldflow/__init__.py +++ b/src/moldflow/__init__.py @@ -69,6 +69,7 @@ from .common import EdgeDisplayOptions from .common import EntityType from .common import GradingFactor +from .common import GeomType from .common import ImportUnitIndex from .common import ImportUnits from .common import ItemType diff --git a/src/moldflow/common.py b/src/moldflow/common.py index 60569c1..d55b5cd 100644 --- a/src/moldflow/common.py +++ b/src/moldflow/common.py @@ -408,7 +408,7 @@ class GeomType(Enum): """ AUTO_DETECT = "Auto-Detect" - DUAL_DOMAIN = "Dual Domain" + FUSION = "Fusion" MIDPLANE = "Midplane" diff --git a/src/moldflow/mesh_generator.py b/src/moldflow/mesh_generator.py index 8de811c..25e6813 100644 --- a/src/moldflow/mesh_generator.py +++ b/src/moldflow/mesh_generator.py @@ -37,16 +37,22 @@ def __init__(self, _mesh_generator): process_log(__name__, LogMessage.CLASS_INIT, locals(), name="MeshGenerator") self.mesh_generator = safe_com(_mesh_generator) - def generate(self): + def generate(self) -> bool: """ Generate the mesh using the MeshGenerator instance. + + Returns: + bool: True if mesh generation was successful, False otherwise. """ process_log(__name__, LogMessage.FUNCTION_CALL, locals(), name="generate") return self.mesh_generator.Generate - def save_options(self): + def save_options(self) -> bool: """ Save the mesh generation options. + + Returns: + bool: True if options were saved successfully, False otherwise. """ process_log(__name__, LogMessage.FUNCTION_CALL, locals(), name="save_options") return self.mesh_generator.SaveOptions diff --git a/tests/api/unit_tests/test_unit_mesh_generator.py b/tests/api/unit_tests/test_unit_mesh_generator.py index fdc2697..3dda457 100644 --- a/tests/api/unit_tests/test_unit_mesh_generator.py +++ b/tests/api/unit_tests/test_unit_mesh_generator.py @@ -6,8 +6,8 @@ """ import pytest -from moldflow import MeshGenerator -from moldflow.common import ( +from moldflow import ( + MeshGenerator, GeomType, NurbsAlgorithm, CoolType, diff --git a/version.json b/version.json index e6f778a..a0e2533 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { "major": "26", "minor": "0", - "patch": "3" + "patch": "4" } From a9996077ece31158714dc830b91e3e78487a0da4 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 10:25:31 +0530 Subject: [PATCH 06/26] Fix cad_mesh_grading_factor to accept float values and remove incorrect GradingFactor enum (#47) * Initial plan * Fix cad_mesh_grading_factor to accept float and remove GradingFactor enum Co-authored-by: sankalps0549 <230025240+sankalps0549@users.noreply.github.com> * Fix check_type and check_range calls to use correct signatures Co-authored-by: sankalps0549 <230025240+sankalps0549@users.noreply.github.com> * Remove cad_mesh_grading_factor from invalid enum test since it's no longer an enum Co-authored-by: sankalps0549 <230025240+sankalps0549@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sankalps0549 <230025240+sankalps0549@users.noreply.github.com> --- CHANGELOG.md | 20 +++++++++++++++++ .../components/enums/grading_factor.rst | 4 ---- src/moldflow/__init__.py | 1 - src/moldflow/common.py | 9 -------- src/moldflow/mesh_generator.py | 8 +++---- .../unit_tests/test_unit_mesh_generator.py | 22 ++++++------------- version.json | 2 +- 7 files changed, 32 insertions(+), 34 deletions(-) delete mode 100644 docs/source/components/enums/grading_factor.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index 7301023..6a0d2e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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 + +### Deprecated +- N/A + +### Removed +- `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 `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 diff --git a/docs/source/components/enums/grading_factor.rst b/docs/source/components/enums/grading_factor.rst deleted file mode 100644 index 159f4b3..0000000 --- a/docs/source/components/enums/grading_factor.rst +++ /dev/null @@ -1,4 +0,0 @@ -GradingFactor -============= - -.. autoclass:: moldflow.common::GradingFactor diff --git a/src/moldflow/__init__.py b/src/moldflow/__init__.py index d43f07a..b86ab31 100644 --- a/src/moldflow/__init__.py +++ b/src/moldflow/__init__.py @@ -68,7 +68,6 @@ from .common import DuplicateOption from .common import EdgeDisplayOptions from .common import EntityType -from .common import GradingFactor from .common import GeomType from .common import ImportUnitIndex from .common import ImportUnits diff --git a/src/moldflow/common.py b/src/moldflow/common.py index d55b5cd..94791fe 100644 --- a/src/moldflow/common.py +++ b/src/moldflow/common.py @@ -393,15 +393,6 @@ class TriClassification(Enum): PRESERVE_ALL = 2 -class GradingFactor(Enum): - """ - Enum for GradingFactor - """ - - SLOW = 0 - FAST = 1 - - class GeomType(Enum): """ Enum for GeomType diff --git a/src/moldflow/mesh_generator.py b/src/moldflow/mesh_generator.py index 25e6813..2d02a69 100644 --- a/src/moldflow/mesh_generator.py +++ b/src/moldflow/mesh_generator.py @@ -13,7 +13,6 @@ NurbsAlgorithm, CoolType, TriClassification, - GradingFactor, GeomType, Mesher3DType, CADContactMesh, @@ -732,15 +731,16 @@ def cad_mesh_grading_factor(self) -> float: return self.mesh_generator.CadMeshGradingFactor @cad_mesh_grading_factor.setter - def cad_mesh_grading_factor(self, value: GradingFactor | int) -> None: + def cad_mesh_grading_factor(self, value: float) -> None: """ Set the CAD mesh grading factor option. """ process_log( __name__, LogMessage.PROPERTY_SET, locals(), name="cad_mesh_grading_factor", value=value ) - value = get_enum_value(value, GradingFactor) - self.mesh_generator.CadMeshGradingFactor = value + check_type(value, (int, float)) + check_range(value, 0, 1, True, True) + self.mesh_generator.CadMeshGradingFactor = float(value) @property def cad_mesh_minimum_curvature_percentage(self) -> float: diff --git a/tests/api/unit_tests/test_unit_mesh_generator.py b/tests/api/unit_tests/test_unit_mesh_generator.py index 3dda457..813f46a 100644 --- a/tests/api/unit_tests/test_unit_mesh_generator.py +++ b/tests/api/unit_tests/test_unit_mesh_generator.py @@ -12,7 +12,6 @@ NurbsAlgorithm, CoolType, TriClassification, - GradingFactor, Mesher3DType, CADContactMesh, ) @@ -134,8 +133,9 @@ def test_functions_no_args( ("cad_auto_size_scale", "CadAutoSizeScale", 50, (int, float)), ("cad_sliver_remove", "CadSliverRemove", True, bool), ("cad_sliver_remove", "CadSliverRemove", False, bool), - ("cad_mesh_grading_factor", "CadMeshGradingFactor", 0, int), - ("cad_mesh_grading_factor", "CadMeshGradingFactor", 1, int), + ("cad_mesh_grading_factor", "CadMeshGradingFactor", 0.0, (int, float)), + ("cad_mesh_grading_factor", "CadMeshGradingFactor", 0.5, (int, float)), + ("cad_mesh_grading_factor", "CadMeshGradingFactor", 1.0, (int, float)), ( "cad_mesh_minimum_curvature_percentage", "CadMeshMinimumCurvaturePercentage", @@ -312,8 +312,9 @@ def test_get_properties( ("cad_auto_size_scale", "CadAutoSizeScale", 50, (int, float)), ("cad_sliver_remove", "CadSliverRemove", True, bool), ("cad_sliver_remove", "CadSliverRemove", False, bool), - ("cad_mesh_grading_factor", "CadMeshGradingFactor", 0, int), - ("cad_mesh_grading_factor", "CadMeshGradingFactor", 1, int), + ("cad_mesh_grading_factor", "CadMeshGradingFactor", 0.0, (int, float)), + ("cad_mesh_grading_factor", "CadMeshGradingFactor", 0.5, (int, float)), + ("cad_mesh_grading_factor", "CadMeshGradingFactor", 1.0, (int, float)), ( "cad_mesh_minimum_curvature_percentage", "CadMeshMinimumCurvaturePercentage", @@ -450,7 +451,7 @@ def test_set_properties( + [("use_auto_size", x) for x in ["abc", -1, 1.0, 1, 1.5, None]] + [("cad_auto_size_scale", x) for x in ["abc", True, None]] + [("cad_sliver_remove", x) for x in ["abc", -1, 1.0, 1, 1.5, None]] - + [("cad_mesh_grading_factor", x) for x in ["abc", True, 1.5, None]] + + [("cad_mesh_grading_factor", x) for x in ["abc", True, None]] + [("cad_mesh_minimum_curvature_percentage", x) for x in ["abc", True, None]] + [("use_fallbacks", x) for x in ["abc", -1, 1.0, 1, 1.5, None]] + [("max_edge_length_in_thickness_direction", x) for x in ["abc", True, None]] @@ -505,7 +506,6 @@ def test_set_properties_invalid_value( "property_name, invalid_value", [("nurbs_mesher", x) for x in [-1, 10, 5]] + [("cad_contact_mesh_type", x) for x in ["abc", "Something"]] - + [("cad_mesh_grading_factor", x) for x in [-1, 10, 5]] + [("source_geom_type", x) for x in ["Hello", "abc"]] + [("cool_type", x) for x in [-1, 10, 5]] + [("tri_classification_opt", x) for x in [-1, 10, 5]], @@ -552,14 +552,6 @@ def test_gel_false(self, mock_mesh_generator, mock_object): ("tri_classification_opt", "TriClassificationOpt", x.value, x.value, int) for x in TriClassification ] - + [ - ("cad_mesh_grading_factor", "CadMeshGradingFactor", x, x.value, int) - for x in GradingFactor - ] - + [ - ("cad_mesh_grading_factor", "CadMeshGradingFactor", x.value, x.value, int) - for x in GradingFactor - ] + [("mesher_3d", "Mesher3D", x, x.value, str) for x in Mesher3DType] + [("mesher_3d", "Mesher3D", x.value, x.value, str) for x in Mesher3DType] + [("source_geom_type", "SourceGeomType", x, x.value, str) for x in GeomType] diff --git a/version.json b/version.json index a0e2533..1d65ba4 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { "major": "26", "minor": "0", - "patch": "4" + "patch": "5" } From 9fb0b05839d21e24b35f9c323261f0405aaa1b72 Mon Sep 17 00:00:00 2001 From: Sankalp Shrivastava Date: Wed, 14 Jan 2026 08:56:48 +0530 Subject: [PATCH 07/26] Introduce Sphinx Versioning (#58) * Introduce Sphinx Versioning (#57) * Sphinx Versioning * Linking latest dir rather than copying * Function signaturevariable name fixed * source validaton for latest * Try catch for git tags absence Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Lint and format issue solve * lint fix * Indentation fix Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Lint fix --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 4 ++ docs/source/_static/switcher.json | 38 +++++++++++ docs/source/conf.py | 43 ++++++++++++- docs/source/readme.rst | 7 ++- requirements.txt | 1 + run.py | 101 +++++++++++++++++++++++++----- 6 files changed, 178 insertions(+), 16 deletions(-) create mode 100644 docs/source/_static/switcher.json diff --git a/README.md b/README.md index 349d83d..5b2b78a 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,9 @@ python run.py build python run.py build-docs ``` +> ***Note: When releasing a new version, update ``switcher.json`` in ``docs/source/_static/`` + to include the new tag in the version dropdown for documentation.*** + Options: - `--skip-build` (`-s`): Skip building before generating docs @@ -159,6 +162,7 @@ We welcome contributions! Please see our [Contributing Guide](https://github.com We use [Semantic Versioning](https://semver.org/). For available versions, see the [tags on this repository](https://github.com/Autodesk/moldflow-api/tags). + ## License This project is licensed under the Apache License 2.0 - see the [LICENSE](https://github.com/Autodesk/moldflow-api/blob/main/LICENSE) file for details. diff --git a/docs/source/_static/switcher.json b/docs/source/_static/switcher.json new file mode 100644 index 0000000..33b7da0 --- /dev/null +++ b/docs/source/_static/switcher.json @@ -0,0 +1,38 @@ +[ + { + "version": "v26.0.5", + "name": "v26.0.5 (latest)", + "url": "../v26.0.5/", + "is_latest": true + }, + { + "version": "v26.0.4", + "name": "v26.0.4", + "url": "../v26.0.4/", + "is_latest": false + }, + { + "version": "v26.0.3", + "name": "v26.0.3", + "url": "../v26.0.3/", + "is_latest": false + }, + { + "version": "v26.0.2", + "name": "v26.0.2", + "url": "../v26.0.2/", + "is_latest": false + }, + { + "version": "v26.0.1", + "name": "v26.0.1", + "url": "../v26.0.1/", + "is_latest": false + }, + { + "version": "v26.0.0", + "name": "v26.0.0", + "url": "../v26.0.0/", + "is_latest": false + } +] \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index c9c2b29..71053fd 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -8,6 +8,7 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html import os import sys +from pathlib import Path sys.path.insert( 0, os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'src', 'moldflow') @@ -29,9 +30,15 @@ 'sphinx.ext.napoleon', # Supports Google-style/Numpy-style docstrings 'sphinx.ext.viewcode', 'sphinx_autodoc_typehints', + 'sphinx_multiversion', ] templates_path = ['_templates'] +smv_tag_whitelist = r'^v?\d+\.\d+\.\d+$' +smv_branch_whitelist = r'^$' +smv_remote_whitelist = r'^origin$' +smv_latest_version = 'latest' + exclude_patterns = [] # -- Options for autodoc ----------------------------------------------------- @@ -53,9 +60,14 @@ html_theme_options = { "back_to_top_button": False, "github_url": "https://github.com/Autodesk/moldflow-api", - "external_links": "", + "external_links": [ + {"name": "Changelog", "url": "https://github.com/Autodesk/moldflow-api/releases"} + ], "footer_end": "", "footer_start": "copyright", + "navbar_start": ["navbar-logo", "version-switcher"], + "navbar_end": ["theme-switcher", "navbar-icon-links"], + "switcher": {"json_url": "_static/switcher.json", "version_match": smv_latest_version}, } html_static_path = ['_static'] html_title = "Moldflow API" @@ -72,5 +84,34 @@ def skip_member(app, what, name, obj, skip, options): return skip +def set_context_switcher(app, pagename, templatename, context, doctree): + """Set version_match in the switcher to show the correct version label.""" + + # Try output directory name (e.g., v26.0.2) - most reliable for sphinx-multiversion + version_name = None + if hasattr(app, "builder") and hasattr(app.builder, "outdir"): + outdir_name = Path(app.builder.outdir).name + if outdir_name and outdir_name != "html": + version_name = outdir_name + + # Fallback to sphinx-multiversion context + if not version_name: + current = context.get("current_version") + if current: + version_name = getattr(current, "name", None) or ( + current.get("name") if isinstance(current, dict) else None + ) + + # Final fallback + if not version_name: + version_name = smv_latest_version + + # Update switcher config for this page + switcher = dict(app.config.html_theme_options.get("switcher", {})) + switcher["version_match"] = version_name + context["theme_switcher"] = switcher + + def setup(app): app.connect("autodoc-skip-member", skip_member) + app.connect("html-page-context", set_context_switcher) diff --git a/docs/source/readme.rst b/docs/source/readme.rst index 8abbc6c..5855a49 100644 --- a/docs/source/readme.rst +++ b/docs/source/readme.rst @@ -1255,7 +1255,12 @@ The project includes a ``run.py`` script with several useful commands: - ``python run.py test`` - Run tests - ``python run.py lint`` - Run code linting - ``python run.py format`` - Format code with black -- ``python run.py build-docs`` - Build documentation +- ``python run.py build-docs`` - Build versioned documentation (HTML uses git tags for the + navigation dropdown; run ``git fetch --tags`` locally before building) + +.. note:: + When releasing a new version, update ``switcher.json`` in ``docs/source/_static/`` + to include the new tag in the version dropdown. Contributing ============ diff --git a/requirements.txt b/requirements.txt index ee98e74..2e1dad7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,6 +10,7 @@ pydata-sphinx-theme==0.16.1 pylint==3.3.4 pytest==8.3.4 sphinx==8.1.3 +sphinx-multiversion==0.2.4 sphinx-autodoc-typehints==3.0.1 twine==6.1.0 PyGithub==2.7.0 diff --git a/run.py b/run.py index 61a32db..06882d1 100644 --- a/run.py +++ b/run.py @@ -58,10 +58,10 @@ import platform import subprocess import shutil - import docopt from github import Github import polib +from packaging.version import InvalidVersion, Version WINDOWS = platform.system() == 'Windows' @@ -83,7 +83,9 @@ LOCALE_DIR = os.path.join(MOLDFLOW_DIR, 'locale') DOCS_DIR = os.path.join(ROOT_DIR, 'docs') DOCS_SOURCE_DIR = os.path.join(DOCS_DIR, 'source') +DOCS_STATIC_DIR = os.path.join(DOCS_SOURCE_DIR, '_static') DOCS_BUILD_DIR = os.path.join(DOCS_DIR, 'build') +DOCS_HTML_DIR = os.path.join(DOCS_BUILD_DIR, 'html') COVERAGE_HTML_DIR = os.path.join(ROOT_DIR, 'htmlcov') DIST_DIR = os.path.join(ROOT_DIR, 'dist') @@ -97,6 +99,7 @@ VERSION_FILE = os.path.join(ROOT_DIR, VERSION_JSON) DIST_FILES = os.path.join(ROOT_DIR, 'dist', '*') PYTHON_FILES = [MOLDFLOW_DIR, DOCS_SOURCE_DIR, TEST_DIR, "run.py"] +SWITCHER_JSON = os.path.join(DOCS_STATIC_DIR, 'switcher.json') def run_command(args, cwd=os.getcwd(), extra_env=None): @@ -292,6 +295,51 @@ def build_mo(): ) +def create_latest_alias(build_output: str) -> None: + """Create a 'latest' alias pointing to the newest version using symlinks when possible.""" + version_dirs = [d for d in os.listdir(build_output) if d.startswith('v')] + if not version_dirs: + return + + def version_key(v): + try: + return Version(v.lstrip('v')) + except InvalidVersion: + return Version("0.0.0") + + sorted_versions = sorted(version_dirs, key=version_key, reverse=True) + latest_version = sorted_versions[0] + latest_src = os.path.join(build_output, latest_version) + latest_dest = os.path.join(build_output, 'latest') + + # Verify source exists before proceeding + if not os.path.exists(latest_src): + logging.error("Source directory for 'latest' alias does not exist: %s", latest_src) + return + + # Clean up any existing 'latest' entry first + if os.path.islink(latest_dest): + os.unlink(latest_dest) + elif os.path.isdir(latest_dest): + shutil.rmtree(latest_dest) + elif os.path.exists(latest_dest): + os.remove(latest_dest) + + # Try creating a symbolic link first (most efficient) + logging.info("Creating 'latest' alias for %s", latest_version) + try: + os.symlink(latest_src, latest_dest, target_is_directory=True) + logging.info("Created symbolic link: latest -> %s", latest_version) + except (OSError, NotImplementedError) as err: + # Fall back to copying if symlinks aren't supported + logging.warning( + "Could not create symbolic link for 'latest' alias (%s); " + "falling back to copying documentation.", + err, + ) + shutil.copytree(latest_src, latest_dest) + + def build_docs(target, skip_build): """Build Documentation""" @@ -305,19 +353,44 @@ def build_docs(target, skip_build): shutil.rmtree(DOCS_BUILD_DIR) try: - run_command( - [ - sys.executable, - '-m', - 'sphinx', - 'build', - '-M', - target, - DOCS_SOURCE_DIR, - DOCS_BUILD_DIR, - ], - ROOT_DIR, - ) + if target == 'html': + build_output = os.path.join(DOCS_BUILD_DIR, 'html') + try: + # fmt: off + run_command( + [ + sys.executable, '-m', 'sphinx_multiversion', + DOCS_SOURCE_DIR, build_output + ], + ROOT_DIR, + ) + except Exception as err: + logging.error( + "Failed to build documentation with sphinx_multiversion.\n" + "This can happen if no Git tags or branches match your version pattern.\n" + "Try running 'git fetch --tags' and ensure version tags exist in the repo.\n" + "Underlying error: %s", + str(err), + ) + # Re-raise so the outer handler can log the general failure as well. + raise + # fmt: on + create_latest_alias(build_output) + else: + # For other targets such as latex, pdf, etc. + run_command( + [ + sys.executable, + '-m', + 'sphinx', + 'build', + '-M', + target, + DOCS_SOURCE_DIR, + DOCS_BUILD_DIR, + ], + ROOT_DIR, + ) logging.info('Sphinx documentation built successfully.') except Exception as err: logging.error( From a2a423e62d8a140be3a10c07f9b0260bb845d0c5 Mon Sep 17 00:00:00 2001 From: Sankalp Shrivastava Date: Wed, 14 Jan 2026 14:00:29 +0530 Subject: [PATCH 08/26] Updating publish workflow (#59) * Introduce Sphinx Versioning (#57) * Sphinx Versioning * Linking latest dir rather than copying * Function signaturevariable name fixed * source validaton for latest * Try catch for git tags absence Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Lint and format issue solve * lint fix * Indentation fix Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Lint fix * fetch tags in ci * Readme fix * Remove force --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/publish.yml | 5 +++++ README.md | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 18a5e7c..55038a9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -76,6 +76,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v5 + with: + fetch-depth: 0 # Fetch all history for sphinx-multiversion - name: Set up Python uses: actions/setup-python@v5 @@ -144,6 +146,9 @@ jobs: - name: Configure Pages uses: actions/configure-pages@v5 + - name: Fetch Git tags + run: git fetch --tags + - name: Build documentation run: python run.py build-docs diff --git a/README.md b/README.md index 5b2b78a..425d5f9 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,7 @@ python run.py build python run.py build-docs ``` -> ***Note: When releasing a new version, update ``switcher.json`` in ``docs/source/_static/`` - to include the new tag in the version dropdown for documentation.*** +> ***Note: When releasing a new version, update ``switcher.json`` in ``docs/source/_static/`` to include the new tag in the version dropdown for documentation.*** Options: - `--skip-build` (`-s`): Skip building before generating docs From 48ec7ad0d2db5422efac3cfbcbf8fe1339ee5742 Mon Sep 17 00:00:00 2001 From: Sankalp Shrivastava Date: Fri, 16 Jan 2026 12:11:30 +0530 Subject: [PATCH 09/26] Support local builds for a single version and redirect root to latest (#60) * Introduce Sphinx Versioning (#57) * Sphinx Versioning * Linking latest dir rather than copying * Function signaturevariable name fixed * source validaton for latest * Try catch for git tags absence Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Lint and format issue solve * lint fix * Indentation fix Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Lint fix * fetch tags in ci * Readme fix * Remove force * Local build single version * Redirect the root to latest * Documentation update * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 15 ++++++++++++++- docs/source/readme.rst | 21 +++++++++++++++++++++ run.py | 36 ++++++++++++++++++++++++++++++++---- 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 425d5f9..aba8f2d 100644 --- a/README.md +++ b/README.md @@ -72,8 +72,21 @@ python run.py build-docs Options: - `--skip-build` (`-s`): Skip building before generating docs +- `--local` (`-l`): Build documentation locally for a single version (skips multi-version build) -The documentation can be accessed locally by opening the index.html in the docs/build/html/ folder. +The documentation can be accessed locally by serving the docs/build/html/ folder: +```sh +cd docs/build/html +python -m http.server 8000 +``` + +Then open http://localhost:8000 in your browser. The root automatically redirects to the latest version documentation. + +**Versioned Documentation:** +- Each git tag creates a separate documentation version (e.g., `/v26.0.5/`) +- A `/latest/` directory points to the newest version +- Root (`/`) automatically redirects to `/latest/` +- Run `git fetch --tags` before building to ensure all version tags are available ### Running the Formatter diff --git a/docs/source/readme.rst b/docs/source/readme.rst index 5855a49..12e9465 100644 --- a/docs/source/readme.rst +++ b/docs/source/readme.rst @@ -1258,10 +1258,31 @@ The project includes a ``run.py`` script with several useful commands: - ``python run.py build-docs`` - Build versioned documentation (HTML uses git tags for the navigation dropdown; run ``git fetch --tags`` locally before building) + - ``--skip-build`` (``-s``): Skip building the package before generating docs + - ``--local`` (``-l``): Build documentation locally for a single version (skips multi-version build) + .. note:: When releasing a new version, update ``switcher.json`` in ``docs/source/_static/`` to include the new tag in the version dropdown. +**Viewing Documentation Locally** + +After building, serve the documentation using Python's built-in HTTP server: + +.. code-block:: bash + + cd docs/build/html + python -m http.server 8000 + +Then open http://localhost:8000 in your browser. + +**Versioned Documentation Features:** + +- Each git tag creates a separate documentation version (e.g., ``/v26.0.5/``) +- A ``/latest/`` directory points to the newest version (symlink on Unix, copy on Windows) +- Root (``/``) automatically redirects to ``/latest/`` for convenience +- Version switcher dropdown in the navigation bar allows switching between versions + Contributing ============ diff --git a/run.py b/run.py index 06882d1..9dd1a7a 100644 --- a/run.py +++ b/run.py @@ -7,7 +7,7 @@ Usage: run.py clean-up run.py build [-P | --publish] [-i | --install] - run.py build-docs [-t | --target=] [-s | --skip-build] + run.py build-docs [-t | --target=] [-s | --skip-build] [-l | --local] run.py format [--check] run.py install [-s | --skip-build] run.py install-package-requirements @@ -49,6 +49,7 @@ --unit Run only unit tests. --integration Run only integration tests. --all Run all tests. + -l, --local Build documentation locally (single version). """ import os @@ -295,6 +296,31 @@ def build_mo(): ) +def create_root_redirect(build_output: str) -> None: + """Create an index.html in the versioned HTML build output root that redirects to /latest/.""" + index_path = os.path.join(build_output, 'index.html') + redirect_html = """ + + + + Redirecting to latest documentation... + + + + + +

Redirecting to latest documentation...

+ + +""" + try: + with open(index_path, 'w', encoding='utf-8') as f: + f.write(redirect_html) + logging.info("Created root redirect: index.html -> latest/") + except Exception as err: + logging.warning("Could not create root redirect index.html: %s", err) + + def create_latest_alias(build_output: str) -> None: """Create a 'latest' alias pointing to the newest version using symlinks when possible.""" version_dirs = [d for d in os.listdir(build_output) if d.startswith('v')] @@ -340,7 +366,7 @@ def version_key(v): shutil.copytree(latest_src, latest_dest) -def build_docs(target, skip_build): +def build_docs(target, skip_build, local=False): """Build Documentation""" if not skip_build: @@ -353,7 +379,7 @@ def build_docs(target, skip_build): shutil.rmtree(DOCS_BUILD_DIR) try: - if target == 'html': + if target == 'html' and not local: build_output = os.path.join(DOCS_BUILD_DIR, 'html') try: # fmt: off @@ -376,6 +402,7 @@ def build_docs(target, skip_build): raise # fmt: on create_latest_alias(build_output) + create_root_redirect(build_output) else: # For other targets such as latex, pdf, etc. run_command( @@ -705,8 +732,9 @@ def main(): elif args.get('build-docs'): target = args.get('--target') or args.get('-t') or 'html' skip_build = args.get('--skip-build') or args.get('-s') + local = args.get('--local') or args.get('-l') - build_docs(target=target, skip_build=skip_build) + build_docs(target=target, skip_build=skip_build, local=local) elif args.get('install-package-requirements'): install_package(target_path=os.path.join(ROOT_DIR, SITE_PACKAGES)) From 6db44daff010d00c1830e7876e0934018b56bbdb Mon Sep 17 00:00:00 2001 From: Internal Sync Date: Mon, 19 Jan 2026 19:31:01 +1100 Subject: [PATCH 10/26] Squashed internal changes from origin/internal/main into public/main\n\nAudit: see scripts/publish_audit/publish_internal_to_public_20260119T083059Z.txt"\n\nOriginal commits:\n806aacb05c88ddca318d03f3ff705ad1992d4361 Jenkins Mon Jan 19 08:06:57 2026 +0000 Sync: create internal/main from public/main - 2026-01-19T08:06:57Z\n 11ff801fb00217a9d605a2a5781218e48e943409 Jenkins Sun Jan 18 20:06:35 2026 +0000 Sync: create internal/main from public/main - 2026-01-18T20:06:35Z\n de7723d4f2a4305a1f31744e041ff5ae3015aae7 Jenkins Sun Jan 18 08:06:40 2026 +0000 Sync: create internal/main from public/main - 2026-01-18T08:06:40Z\n b5a0f0071604c657aeea29365591f9b396d21b02 Jenkins Sat Jan 17 20:06:39 2026 +0000 Sync: create internal/main from public/main - 2026-01-17T20:06:39Z\n 3a9ae772c4f3722a4b1004ebf2cee0fc60497206 Jenkins Sat Jan 17 08:07:57 2026 +0000 Sync: create internal/main from public/main - 2026-01-17T08:07:57Z\n fd5b978006f1ebb671a2ddacc534d3f45d3c9216 Jenkins Fri Jan 16 20:06:44 2026 +0000 Sync: create internal/main from public/main - 2026-01-16T20:06:44Z\n e0961a7c7b3f2cd0a380653ed50577aa006ef9d6 Jenkins Fri Jan 16 08:06:42 2026 +0000 Sync: create internal/main from public/main - 2026-01-16T08:06:42Z\n 2dcf8e995d493909f9da48c5f87e5a0338d4a75e Sankalp Shrivastava Fri Jan 16 03:17:19 2026 +0530 [IM-11420] Deprecate legacy Moldflow API functions for Synergy 2027.0.0 (#110)\n d9b3fba826ef32d232275f89e9ab3008f1964148 Jenkins Thu Jan 15 20:06:37 2026 +0000 Sync: create internal/main from public/main - 2026-01-15T20:06:37Z\n 5296ee641da2463d18d2e5726470d73904ef3d97 Jenkins Thu Jan 15 08:07:10 2026 +0000 Sync: create internal/main from public/main - 2026-01-15T08:07:10Z\n 7cd6c6f7d117a176d891d58710959b50fcca211a Jenkins Wed Jan 14 20:06:37 2026 +0000 Sync: create internal/main from public/main - 2026-01-14T20:06:37Z\n e4d5f960b1de842bd6ce01611bc09ab58f63ea94 Jenkins Wed Jan 14 08:06:35 2026 +0000 Sync: create internal/main from public/main - 2026-01-14T08:06:35Z\n 779ade1cf7f5ffbec6abbe84e9a3c52fc417ea59 Kah Chong Tue Jan 13 16:19:28 2026 -0800 IM-11283: Import script recorded in AMI 2026.0.1 fails in 2027.0 (#108)\n 40decd2fef9a20094ecc830bdc9d69c3085ab984 Jenkins Tue Jan 13 20:06:36 2026 +0000 Sync: create internal/main from public/main - 2026-01-13T20:06:36Z\n 769099c1a2df86b151ee18d468ef5b9786fd3dac Jenkins Tue Jan 13 08:06:49 2026 +0000 Sync: create internal/main from public/main - 2026-01-13T08:06:48Z\n 368862b86288affeaee5e51ff1b0a89370b4e4c2 Jenkins Mon Jan 12 20:06:56 2026 +0000 Sync: create internal/main from public/main - 2026-01-12T20:06:56Z\n 1bf5f393ffad78fc5b332709df036e9ff21b5af8 Jenkins Mon Jan 12 08:06:31 2026 +0000 Sync: create internal/main from public/main - 2026-01-12T08:06:31Z\n f46dea35a10002ca9f0b13c8a46087d841e543bb Kyungmo Kim Mon Jan 12 16:48:40 2026 +1100 IM-11377 Add log() method to Synergy class (#109)\n f97878d0101c4ed489bb9336748f34007693c99d Jenkins Sun Jan 11 20:06:33 2026 +0000 Sync: create internal/main from public/main - 2026-01-11T20:06:33Z\n ebf6bdfbea6caf06518ae40d70097d51772c1e5e Jenkins Sun Jan 11 08:06:38 2026 +0000 Sync: create internal/main from public/main - 2026-01-11T08:06:38Z\n 926911a801fd7459f0907b69807acf528bb83c67 Jenkins Sat Jan 10 20:06:28 2026 +0000 Sync: create internal/main from public/main - 2026-01-10T20:06:28Z\n 07e92fef0a1bba68a3c3a055adca5ab123dc5f2c Jenkins Sat Jan 10 08:09:12 2026 +0000 Sync: create internal/main from public/main - 2026-01-10T08:09:12Z\n a689b0164b9b866616890d7b115913bf87cf2f00 Jenkins Fri Jan 9 20:06:34 2026 +0000 Sync: create internal/main from public/main - 2026-01-09T20:06:34Z\n 1d83b39d95ec0f023af307b60a010a9676aac6ae Jenkins Fri Jan 9 08:07:18 2026 +0000 Sync: create internal/main from public/main - 2026-01-09T08:07:18Z\n 353e464694cf2fba5b35f953afc0293324079d1d Jenkins Thu Jan 8 20:06:38 2026 +0000 Sync: create internal/main from public/main - 2026-01-08T20:06:38Z\n e6b272d96300fea3681f151bf2c77053224e5f04 Jenkins Thu Jan 8 08:06:28 2026 +0000 Sync: create internal/main from public/main - 2026-01-08T08:06:28Z\n f197c30fd7bce14f79ea4c6b8a257b9594ede770 Jenkins Wed Jan 7 20:06:33 2026 +0000 Sync: create internal/main from public/main - 2026-01-07T20:06:33Z\n 97078fbd488be1d9e8d5b6ef6c99f30e94300486 Jenkins Wed Jan 7 08:07:14 2026 +0000 Sync: create internal/main from public/main - 2026-01-07T08:07:14Z\n 0c006cdbfeef72560fcc34777df32b34e151ec60 Jenkins Tue Jan 6 20:06:36 2026 +0000 Sync: create internal/main from public/main - 2026-01-06T20:06:36Z\n d15b4b91ed482fa8f4ab556416c208ae539e426a Jenkins Tue Jan 6 08:06:34 2026 +0000 Sync: create internal/main from public/main - 2026-01-06T08:06:34Z\n 2db758b3089172699ec118c5bb5e7355a0bc6581 Jenkins Mon Jan 5 20:06:50 2026 +0000 Sync: create internal/main from public/main - 2026-01-05T20:06:50Z\n e194d1e638e90cce032b91b05afc06b7d001145e Jenkins Mon Jan 5 08:06:39 2026 +0000 Sync: create internal/main from public/main - 2026-01-05T08:06:39Z\n 6e64ca3f77b147b1b0a9f9e8e547c17e8c83f168 Jenkins Sun Jan 4 20:06:39 2026 +0000 Sync: create internal/main from public/main - 2026-01-04T20:06:39Z\n bd0c09687b1d85940306e99bf51e0df484690a91 Jenkins Sun Jan 4 08:06:28 2026 +0000 Sync: create internal/main from public/main - 2026-01-04T08:06:28Z\n 0a058738167dc716d4b55976b6c3d33f21288b4f Jenkins Sat Jan 3 20:06:24 2026 +0000 Sync: create internal/main from public/main - 2026-01-03T20:06:24Z\n 970dc468ecb8ab1e271b3212f28ee277f77f967b Jenkins Sat Jan 3 08:06:16 2026 +0000 Sync: create internal/main from public/main - 2026-01-03T08:06:16Z\n 40799c8da817948856208f74a3d8af29a0adab8b Jenkins Fri Jan 2 20:06:40 2026 +0000 Sync: create internal/main from public/main - 2026-01-02T20:06:40Z\n 93e2431b1aa46faaa1c77923284caea08b4315c3 Jenkins Fri Jan 2 08:06:30 2026 +0000 Sync: create internal/main from public/main - 2026-01-02T08:06:30Z\n 35dd339f555aa3ae607c77ae33b8ab5aed063085 Jenkins Thu Jan 1 20:06:33 2026 +0000 Sync: create internal/main from public/main - 2026-01-01T20:06:33Z\n dcdae325e1bcbbdab4aa7a23c62d03c1de0c7f2a Jenkins Thu Jan 1 08:06:31 2026 +0000 Sync: create internal/main from public/main - 2026-01-01T08:06:31Z\n e29abe8bf5462ad86bd8d57ab3d368c468da1965 Jenkins Wed Dec 31 20:06:29 2025 +0000 Sync: create internal/main from public/main - 2025-12-31T20:06:29Z\n d39f1e45b8aa56bcf4741f1b68493357c28ac27a Jenkins Wed Dec 31 08:06:30 2025 +0000 Sync: create internal/main from public/main - 2025-12-31T08:06:30Z\n 24da6acdb84496e2616849abe9c0ab6773c7dc04 Jenkins Tue Dec 30 20:06:42 2025 +0000 Sync: create internal/main from public/main - 2025-12-30T20:06:42Z\n d3f26ab5127112875e5751c24b6edcd97528093e Jenkins Tue Dec 30 08:06:26 2025 +0000 Sync: create internal/main from public/main - 2025-12-30T08:06:26Z\n 968b392c8b060fe38da99f21aff592c5cb30db00 Jenkins Mon Dec 29 20:06:48 2025 +0000 Sync: create internal/main from public/main - 2025-12-29T20:06:48Z\n 7dd2c3904e99ec3de57be3c5b303dd003acbc471 Jenkins Mon Dec 29 08:06:27 2025 +0000 Sync: create internal/main from public/main - 2025-12-29T08:06:27Z\n 20713b76969c020e8d9922c6a8f189f05e3c2c4c Jenkins Sun Dec 28 20:06:39 2025 +0000 Sync: create internal/main from public/main - 2025-12-28T20:06:39Z\n ee2ff31d750162b4f96e8ba91129220e703421a5 Jenkins Sun Dec 28 08:06:43 2025 +0000 Sync: create internal/main from public/main - 2025-12-28T08:06:43Z\n b60c4dd7ea146b1d43871af3855d83d1701ca560 Jenkins Sat Dec 27 20:06:30 2025 +0000 Sync: create internal/main from public/main - 2025-12-27T20:06:30Z\n b4b921a08adaba332c653e4f39eaa3221216e7d0 Jenkins Sat Dec 27 08:06:37 2025 +0000 Sync: create internal/main from public/main - 2025-12-27T08:06:37Z\n 291ddf38220215d7a6ac004401126b10b8b427fa Jenkins Fri Dec 26 20:06:38 2025 +0000 Sync: create internal/main from public/main - 2025-12-26T20:06:38Z\n e031409303f524c220068ef81537579e1618a67b Jenkins Fri Dec 26 08:06:28 2025 +0000 Sync: create internal/main from public/main - 2025-12-26T08:06:28Z\n c48350770b2a31610aba483bfc16ad0c80ddc5b7 Jenkins Thu Dec 25 20:06:38 2025 +0000 Sync: create internal/main from public/main - 2025-12-25T20:06:38Z\n d68698bec2554dc99b243556cc8bb7d2b9f1a775 Jenkins Thu Dec 25 08:06:27 2025 +0000 Sync: create internal/main from public/main - 2025-12-25T08:06:27Z\n e11329df5c1b481ef1300d0ced46195b9083c395 Jenkins Wed Dec 24 20:06:31 2025 +0000 Sync: create internal/main from public/main - 2025-12-24T20:06:31Z\n e7f38bd20329d623a87688261b3a53eca49f907b Jenkins Wed Dec 24 08:06:22 2025 +0000 Sync: create internal/main from public/main - 2025-12-24T08:06:22Z\n 460a14d7f2ab1327f0b1ff664e5cd8b4df5992e4 Jenkins Tue Dec 23 20:06:35 2025 +0000 Sync: create internal/main from public/main - 2025-12-23T20:06:35Z\n 46445698dab70678db956daaa581c720dfb90f12 Jenkins Tue Dec 23 08:06:37 2025 +0000 Sync: create internal/main from public/main - 2025-12-23T08:06:37Z\n c4dcef7f0f8390eb0e82558db77969563fceb230 Jenkins Mon Dec 22 20:06:35 2025 +0000 Sync: create internal/main from public/main - 2025-12-22T20:06:35Z\n 3c471c7c80b84c178d3bea65edd4f5b245d10848 Jenkins Mon Dec 22 08:06:40 2025 +0000 Sync: create internal/main from public/main - 2025-12-22T08:06:40Z\n 078062d26f3d517c9fbae2cb21ebbf23985e74ae Jenkins Sun Dec 21 20:06:42 2025 +0000 Sync: create internal/main from public/main - 2025-12-21T20:06:42Z\n 74c5cffb0374fbae5e26ebffc196be1405111b89 Jenkins Sun Dec 21 08:06:36 2025 +0000 Sync: create internal/main from public/main - 2025-12-21T08:06:36Z\n d0caba2c1eef4207a615a85f5065c985d0ac9add Jenkins Sat Dec 20 20:06:39 2025 +0000 Sync: create internal/main from public/main - 2025-12-20T20:06:39Z\n 82c2363caa6e6262e30ff14ba0d452216f19094d Jenkins Sat Dec 20 08:06:17 2025 +0000 Sync: create internal/main from public/main - 2025-12-20T08:06:17Z\n 28f187ded3fc3cd674aa9acd8bcfb0a9a444015c Jenkins Fri Dec 19 20:07:01 2025 +0000 Sync: create internal/main from public/main - 2025-12-19T20:07:01Z\n 058790df19825e0704376d55a18a84d49fbe97d5 Jenkins Fri Dec 19 08:06:32 2025 +0000 Sync: create internal/main from public/main - 2025-12-19T08:06:32Z\n ed6aee6162851ad54bdcfed19b8c51ef971573e8 Jenkins Thu Dec 18 20:06:34 2025 +0000 Sync: create internal/main from public/main - 2025-12-18T20:06:34Z\n d3639b492d44c0adfd1b1d69970b97a17b6b8b9b Jenkins Thu Dec 18 08:06:44 2025 +0000 Sync: create internal/main from public/main - 2025-12-18T08:06:44Z\n 69c2eb59324b201c221bfb3f3f3c323aa74f5cda Jenkins Wed Dec 17 20:06:23 2025 +0000 Sync: create internal/main from public/main - 2025-12-17T20:06:23Z\n 15b1a483530ecccde74c96e80920f131e4b08cd7 Jenkins Wed Dec 17 08:06:50 2025 +0000 Sync: create internal/main from public/main - 2025-12-17T08:06:50Z\n 04482c2824b783b4e0b5084bee62ef94766d1fdf Janvi Gupta Wed Dec 17 13:16:33 2025 +0530 IM-10976: Raise patch number (#107)\n 5784573dd6bd972ca743191761a97a27fac9fb29 Janvi Gupta Wed Dec 17 10:40:54 2025 +0530 IM-10976: Add GIF file support (#106)\n 0d831aca6d836582c08aa4615550962b124fd0ad Jenkins Tue Dec 16 20:07:12 2025 +0000 Sync: create internal/main from public/main - 2025-12-16T20:07:12Z\n f2a4e2eee75e12a74b7b916ae13b5bc9ffe29d34 Jenkins Tue Dec 16 08:06:38 2025 +0000 Sync: create internal/main from public/main - 2025-12-16T08:06:38Z\n 4c6141bc64286dd7b7debc7db353f3310e001927 Jenkins Mon Dec 15 20:45:40 2025 +0000 Sync: create internal/main from public/main - 2025-12-15T20:45:40Z\n 0f48230e2060b093fa82e2c5fe5948902e34019e Jenkins Mon Dec 15 08:10:40 2025 +0000 Sync: create internal/main from public/main - 2025-12-15T08:10:40Z\n 7f81739d4aef959c0c978220eb9f80e144b577e0 Jenkins Sun Dec 14 20:07:51 2025 +0000 Sync: create internal/main from public/main - 2025-12-14T20:07:51Z\n c9b3eeb09629035f31dcb8ab057d477d3753e28c Jenkins Sun Dec 14 08:07:16 2025 +0000 Sync: create internal/main from public/main - 2025-12-14T08:07:16Z\n 52ed6b32b35deb2f6932a95510a28859f58e29d6 Jenkins Sat Dec 13 20:07:19 2025 +0000 Sync: create internal/main from public/main - 2025-12-13T20:07:19Z\n fac1c4bb90156444afb9e4628d213fc61575f43d Jenkins Sat Dec 13 08:09:35 2025 +0000 Sync: create internal/main from public/main - 2025-12-13T08:09:35Z\n 7b3cc11d164d1752dc9c1aee66963b9b05200451 Jenkins Fri Dec 12 20:07:50 2025 +0000 Sync: create internal/main from public/main - 2025-12-12T20:07:50Z\n afcc4ec7229dc21562625569547bbd9ed47698a2 Jenkins Fri Dec 12 08:08:46 2025 +0000 Sync: create internal/main from public/main - 2025-12-12T08:08:46Z\n bd4246925f535c2bffc4a7a1f15245b49e11c808 Jenkins Thu Dec 11 20:07:26 2025 +0000 Sync: create internal/main from public/main - 2025-12-11T20:07:26Z\n 3c6f1706411faac1baf43a4e39ca7217d47c7487 Jenkins Thu Dec 11 08:07:23 2025 +0000 Sync: create internal/main from public/main - 2025-12-11T08:07:23Z\n 45cb96b8b3d92b775d2609046a0986d37e81b945 Jenkins Wed Dec 10 20:07:30 2025 +0000 Sync: create internal/main from public/main - 2025-12-10T20:07:30Z\n 646c70f5bdea00ffb5dd4aed86859470590e6d64 Jenkins Wed Dec 10 08:06:57 2025 +0000 Sync: create internal/main from public/main - 2025-12-10T08:06:57Z\n 12c3c17ac1986fd31d50df6d204c6ea75da5a8e1 Jenkins Tue Dec 9 20:07:02 2025 +0000 Sync: create internal/main from public/main - 2025-12-09T20:07:02Z\n 1f9749296b37663646c6253634a7d7cd8a419b43 Jenkins Tue Dec 9 08:07:25 2025 +0000 Sync: create internal/main from public/main - 2025-12-09T08:07:25Z\n 113398bf1dc0899e34c3ea44159d735a8c280385 Jenkins Mon Dec 8 20:07:20 2025 +0000 Sync: create internal/main from public/main - 2025-12-08T20:07:20Z\n 09c16b88e1659b7f2d4288ec5a8b34f90499a2a0 Jenkins Mon Dec 8 08:07:20 2025 +0000 Sync: create internal/main from public/main - 2025-12-08T08:07:20Z\n 8d98386ba3d5be36fb30ba33af49b7beb013ec78 Jenkins Sun Dec 7 20:07:18 2025 +0000 Sync: create internal/main from public/main - 2025-12-07T20:07:18Z\n 59fe1db9916a85016eb1ad0bd6ac0d9727801632 Jenkins Sun Dec 7 08:07:22 2025 +0000 Sync: create internal/main from public/main - 2025-12-07T08:07:22Z\n 40965b627dd3b337d200e071e897f4c6ac653b12 Jenkins Sat Dec 6 20:07:12 2025 +0000 Sync: create internal/main from public/main - 2025-12-06T20:07:12Z\n 15e0038b55f2c53407c460c42be123f0cd4f0fe7 Jenkins Sat Dec 6 08:06:18 2025 +0000 Sync: create internal/main from public/main - 2025-12-06T08:06:18Z\n 6a146773e850844c6462922b2f7e8a1a4310fa49 Jenkins Fri Dec 5 20:07:34 2025 +0000 Sync: create internal/main from public/main - 2025-12-05T20:07:34Z\n 4bf925be70b27eebce9c230af8295ada279bf078 Jenkins Fri Dec 5 08:07:26 2025 +0000 Sync: create internal/main from public/main - 2025-12-05T08:07:26Z\n b638a146fe4fb76594f8c8d9303bcfd8439f5d29 Jenkins Thu Dec 4 20:07:50 2025 +0000 Sync: create internal/main from public/main - 2025-12-04T20:07:50Z\n be770a7273b596edccf985cc4b31866e0d9019d5 Jenkins Thu Dec 4 08:12:37 2025 +0000 Sync: create internal/main from public/main - 2025-12-04T08:12:37Z\n 1074775b47df702eff56e1ad01fb6f407b714514 Jenkins Wed Dec 3 20:06:14 2025 +0000 Sync: create internal/main from public/main - 2025-12-03T20:06:14Z\n fd1e74e33a5b82d13fe21c3f70c79cf21dfe6f0c Jenkins Wed Dec 3 08:07:43 2025 +0000 Sync: create internal/main from public/main - 2025-12-03T08:07:43Z\n ab638d3f5fe89f94add25796b03f52b01f458cd2 Jenkins Tue Dec 2 20:07:30 2025 +0000 Sync: create internal/main from public/main - 2025-12-02T20:07:30Z\n bd59dedb94066686b6f57bdf5cd5cc3e081180ba Jenkins Tue Dec 2 08:07:27 2025 +0000 Sync: create internal/main from public/main - 2025-12-02T08:07:27Z\n 074910714964a99daeff3fbafb0847a47c9589f1 Jenkins Mon Dec 1 20:06:12 2025 +0000 Sync: create internal/main from public/main - 2025-12-01T20:06:12Z\n 0674eaa91135e2edbe08a5148c9040ef91d9da0c Jenkins Mon Dec 1 08:07:41 2025 +0000 Sync: create internal/main from public/main - 2025-12-01T08:07:41Z\n 03beb8fec1f349722024c0f4b10a7f8b7b17d198 Jenkins Sun Nov 30 20:07:16 2025 +0000 Sync: create internal/main from public/main - 2025-11-30T20:07:16Z\n 3dc9241fd7212b2ea64c4525ced34c041c53f2a3 Jenkins Sun Nov 30 08:07:08 2025 +0000 Sync: create internal/main from public/main - 2025-11-30T08:07:08Z\n 7999ac263b482d5dd2a5f6243adc0fd3c366869e Jenkins Sat Nov 29 20:07:15 2025 +0000 Sync: create internal/main from public/main - 2025-11-29T20:07:15Z\n 60dee5e879ed3c6d976061a53f2cc37328e0211b Jenkins Sat Nov 29 08:07:20 2025 +0000 Sync: create internal/main from public/main - 2025-11-29T08:07:20Z\n 0045bbb10145b524022778a756b9e92ab9e6583b Jenkins Fri Nov 28 20:07:17 2025 +0000 Sync: create internal/main from public/main - 2025-11-28T20:07:17Z\n 4cfc50eb0f2f1a075c546cc1bd521f889a6d8d4d Jenkins Fri Nov 28 08:07:28 2025 +0000 Sync: create internal/main from public/main - 2025-11-28T08:07:28Z\n b70ac3414733315ec95b55880e696a9a5c9c05f9 Jenkins Thu Nov 27 20:07:14 2025 +0000 Sync: create internal/main from public/main - 2025-11-27T20:07:14Z\n ac670282c01179d21fd0a68a1ebf93e583171922 Jenkins Thu Nov 27 08:07:29 2025 +0000 Sync: create internal/main from public/main - 2025-11-27T08:07:29Z\n f6280e89786f743b4bef6765efad1f2b1b7f50e4 Jenkins Wed Nov 26 20:07:21 2025 +0000 Sync: create internal/main from public/main - 2025-11-26T20:07:21Z\n 688dbd44fac79dfb55c02a0dd7a3c46ad002a5bb Jenkins Wed Nov 26 08:07:40 2025 +0000 Sync: create internal/main from public/main - 2025-11-26T08:07:40Z\n 00a155d2c10ae7a88a38e7fda0fcc9725943a601 Jenkins Tue Nov 25 20:07:37 2025 +0000 Sync: create internal/main from public/main - 2025-11-25T20:07:37Z\n 463292f03cf69fd8f4ec33ce1e476c62fd4d9a35 Kah Chong Tue Nov 25 03:38:02 2025 -0800 IM-10853 Moldflow-api: removed redundant import option (#103)\n b43ce695258e964408d190f17e6bfe4b4df9c304 Jenkins Tue Nov 25 08:07:50 2025 +0000 Sync: create internal/main from public/main - 2025-11-25T08:07:50Z\n ddab067c1512df8e722e26a08c2d6f990c9bc10e Jenkins Mon Nov 24 20:07:24 2025 +0000 Sync: create internal/main from public/main - 2025-11-24T20:07:24Z\n 7516896e2d67b51160ff0099c2aad4b0893b1dcb Jenkins Mon Nov 24 08:07:47 2025 +0000 Sync: create internal/main from public/main - 2025-11-24T08:07:47Z\n d571915c6b7130ed3c4a7865e43bc9bfaad44d9a Jenkins Sun Nov 23 20:07:18 2025 +0000 Sync: create internal/main from public/main - 2025-11-23T20:07:18Z\n 331ac2285d5b220466c3d9666d1d08cc30fcc46a Jenkins Sun Nov 23 08:07:16 2025 +0000 Sync: create internal/main from public/main - 2025-11-23T08:07:16Z\n b37402eb8cab4f84c5b5c401e440f745be13ffdc Jenkins Sat Nov 22 20:07:18 2025 +0000 Sync: create internal/main from public/main - 2025-11-22T20:07:17Z\n c821a58a53cdc7acd04796ce4de9b0983b45e8bb Jenkins Sat Nov 22 08:06:38 2025 +0000 Sync: create internal/main from public/main - 2025-11-22T08:06:38Z\n b9d297c7545c5e36e76792b6dd1f66216e1ec7a1 Jenkins Fri Nov 21 20:07:24 2025 +0000 Sync: create internal/main from public/main - 2025-11-21T20:07:24Z\n 998740a37d3949b846a221e6a1180eefa362943f Jenkins Fri Nov 21 08:07:35 2025 +0000 Sync: create internal/main from public/main - 2025-11-21T08:07:35Z\n 36cebe134050d91f404158bccc4e65895381ed89 Jenkins Thu Nov 20 20:07:24 2025 +0000 Sync: create internal/main from public/main - 2025-11-20T20:07:24Z\n 408a81a89c728b4e506d5085fbf29f03ee5b4b83 Jenkins Thu Nov 20 08:06:41 2025 +0000 Sync: create internal/main from public/main - 2025-11-20T08:06:41Z\n cec7a47d34a9aadd2ae2279e6f9404dc57929135 Jenkins Wed Nov 19 20:07:22 2025 +0000 Sync: create internal/main from public/main - 2025-11-19T20:07:22Z\n 3c1471af61f7c1264fef678068c702526ba5aca0 Jenkins Wed Nov 19 08:07:38 2025 +0000 Sync: create internal/main from public/main - 2025-11-19T08:07:38Z\n f2370990bb53be38f0b6495765e26c2faeeb2963 Evan Liaw Wed Nov 19 17:04:58 2025 +1100 Add DisplayName API (#105)\n 36716149d3eacd79e4b9092705e77639929fc5e3 Jenkins Tue Nov 18 20:07:29 2025 +0000 Sync: create internal/main from public/main - 2025-11-18T20:07:29Z\n ace383daac3b9f0f8ca10d3406e618753c48fb5e Jenkins Tue Nov 18 08:07:41 2025 +0000 Sync: create internal/main from public/main - 2025-11-18T08:07:41Z\n 8caedb6cc7f3accc5ccd1f9cedb6d09d4af30705 Jenkins Mon Nov 17 20:06:11 2025 +0000 Sync: create internal/main from public/main - 2025-11-17T20:06:11Z\n 1e4a146d3f38430934f788d8f491262311292c24 Jenkins Mon Nov 17 08:06:11 2025 +0000 Sync: create internal/main from public/main - 2025-11-17T08:06:11Z\n bf8158ecd6a124945811ed51fbdae7f19a82c93c Jenkins Sun Nov 16 20:06:10 2025 +0000 Sync: create internal/main from public/main - 2025-11-16T20:06:10Z\n 48824a200de103c0b2ff58f3459407fd8859222d Jenkins Sun Nov 16 08:06:10 2025 +0000 Sync: create internal/main from public/main - 2025-11-16T08:06:10Z\n b0d4896c4f7695b8e4be64d3c308a8d08a986ce5 Jenkins Sat Nov 15 20:06:10 2025 +0000 Sync: create internal/main from public/main - 2025-11-15T20:06:10Z\n 3ac0a4219c43bf8a3f3e5b1d3c1fa90471d6e85e Jenkins Sat Nov 15 08:06:13 2025 +0000 Sync: create internal/main from public/main - 2025-11-15T08:06:13Z\n 39a7425fd36b7d479c2f225d25f6849fd367bd20 Jenkins Fri Nov 14 20:06:13 2025 +0000 Sync: create internal/main from public/main - 2025-11-14T20:06:13Z\n 50c8f6f0d6f6d052944142a36daabf4d9c6b071c Jenkins Fri Nov 14 08:06:17 2025 +0000 Sync: create internal/main from public/main - 2025-11-14T08:06:17Z\n 244e6d2fda4e0fa6477ae76d776f4174faf26381 Jenkins Thu Nov 13 20:07:40 2025 +0000 Sync: create internal/main from public/main - 2025-11-13T20:07:40Z\n 3bd1d9f6e4b11585241c3855e830f8deaa4f8311 Jenkins Thu Nov 13 08:14:42 2025 +0000 Sync: create internal/main from public/main - 2025-11-13T08:14:42Z\n 1d51cb3284f7e8cdea5bcfed6c1367a4dd5e2276 Jenkins Wed Nov 12 20:07:26 2025 +0000 Sync: create internal/main from public/main - 2025-11-12T20:07:26Z\n 840a37b6519f5166584681042a9984c57958a1aa Jenkins Wed Nov 12 08:07:33 2025 +0000 Sync: create internal/main from public/main - 2025-11-12T08:07:33Z\n 56bdd387c1deb529af456fa8f661f9ed04deaadf Jenkins Tue Nov 11 20:07:50 2025 +0000 Sync: create internal/main from public/main - 2025-11-11T20:07:50Z\n a72ad7b04108810710eee23c927489a60188b394 Jenkins Tue Nov 11 08:09:22 2025 +0000 Sync: create internal/main from public/main - 2025-11-11T08:09:22Z\n 849ae4bb1364c0886d8361c3c35eb066b502f927 Jenkins Mon Nov 10 20:07:51 2025 +0000 Sync: create internal/main from public/main - 2025-11-10T20:07:51Z\n b3191601f449df918d30eb45cc12e77b4429496c Pradeepkumar Jagdale Mon Nov 10 18:24:09 2025 +0530 Rename axis and rotation properties in ImageExportOptions (#104)\n c65d2c8438f6f865dbca1aab906be516d4ff6276 Jenkins Mon Nov 10 08:07:15 2025 +0000 Sync: create internal/main from public/main - 2025-11-10T08:07:15Z\n 7a6e877858bf93d8b2b0dc661f1a26b0e33c774e Jenkins Sun Nov 9 20:07:26 2025 +0000 Sync: create internal/main from public/main - 2025-11-09T20:07:26Z\n 8037995c71c5917123c0a4fef51f3cd1c8a0e896 Jenkins Sun Nov 9 08:07:35 2025 +0000 Sync: create internal/main from public/main - 2025-11-09T08:07:35Z\n 8fefb8110bc34f60c20120c3a08fd6a7d7549943 Jenkins Sat Nov 8 20:07:37 2025 +0000 Sync: create internal/main from public/main - 2025-11-08T20:07:37Z\n 937f57e12a499e3880540efe97d5321c04b5bb21 Jenkins Sat Nov 8 08:09:26 2025 +0000 Sync: create internal/main from public/main - 2025-11-08T08:09:26Z\n c83d4a2e4e50acc1cf5f20f85f3a9257315ff23e Jenkins Fri Nov 7 20:07:48 2025 +0000 Sync: create internal/main from public/main - 2025-11-07T20:07:48Z\n 338d1e20d162643a0652cb5a46fd915ae8270fb0 Jenkins Fri Nov 7 08:06:38 2025 +0000 Sync: create internal/main from public/main - 2025-11-07T08:06:38Z\n 5a8ddf97940b79520e35cc11b2b6bfa48c859110 Jenkins Thu Nov 6 20:08:03 2025 +0000 Sync: create internal/main from public/main - 2025-11-06T20:08:03Z\n ffb06fa6bbd8a87908f8cad3876f632a7f97c2b4 Jenkins Thu Nov 6 08:09:19 2025 +0000 Sync: create internal/main from public/main - 2025-11-06T08:09:19Z\n ad5474587d6b9de4239ca8264c2283ff3aad04cf Jenkins Wed Nov 5 20:07:30 2025 +0000 Sync: create internal/main from public/main - 2025-11-05T20:07:30Z\n f2436ec2c9cd504ef75c6c6d1f01b0a95c5e7626 Jenkins Wed Nov 5 08:09:04 2025 +0000 Sync: create internal/main from public/main - 2025-11-05T08:09:04Z\n 0a0dc6fb8294c64edb49dbc21cfd84991802cadd Kumar Dev Wed Nov 5 08:02:12 2025 +0530 [IM-10793] Add Export VTK Code to Moldflow API (#102)\n 6f2e3b8dbfe289e19a388c131d4a9429d91fd3b7 Jenkins Tue Nov 4 20:07:56 2025 +0000 Sync: create internal/main from public/main - 2025-11-04T20:07:56Z\n 8f1efc70d6cea25b68e41fe5b298c4c43826d95b Jenkins Tue Nov 4 08:07:27 2025 +0000 Sync: create internal/main from public/main - 2025-11-04T08:07:27Z\n a58d1079629298088252db6aaa8bb1361e2dfbbe Jenkins Mon Nov 3 20:07:40 2025 +0000 Sync: create internal/main from public/main - 2025-11-03T20:07:40Z\n dcac5678c218988444d937a2731ef228ccbacdee Jenkins Mon Nov 3 08:08:59 2025 +0000 Sync: create internal/main from public/main - 2025-11-03T08:08:58Z\n 1b6c6535c6c19ba099a7ddbe843d0c0d8f8c18ab Jenkins Sun Nov 2 20:07:27 2025 +0000 Sync: create internal/main from public/main - 2025-11-02T20:07:27Z\n a433f998828513e0020cf8bcc9cd91d7167b68a9 Jenkins Sun Nov 2 07:07:35 2025 +0000 Sync: create internal/main from public/main - 2025-11-02T07:07:35Z\n 44cfc19c5b650e2da1de76ecc3ecbb4748064a36 Jenkins Sat Nov 1 19:07:27 2025 +0000 Sync: create internal/main from public/main - 2025-11-01T19:07:27Z\n 8cb02189530c0a6404a96c4c3753a57f6bf698d1 Jenkins Fri Oct 31 19:07:21 2025 +0000 Sync: create internal/main from public/main - 2025-10-31T19:07:21Z\n 7603aee489d41b0997c1f561fac102117acdf9be Jenkins Fri Oct 31 07:08:47 2025 +0000 Sync: create internal/main from public/main - 2025-10-31T07:08:47Z\n 1b892a83f5d64371e80d9029f545499cbbaadc1b Jenkins Thu Oct 30 19:08:23 2025 +0000 Sync: create internal/main from public/main - 2025-10-30T19:08:22Z\n 654a188ea1094aa77dd75cb21cff472ac0000cbe Jenkins Thu Oct 30 07:08:57 2025 +0000 Sync: create internal/main from public/main - 2025-10-30T07:08:57Z\n d8862fc9b4879d4305953a9a0929fc0b58800a5b Jenkins Wed Oct 29 19:07:33 2025 +0000 Sync: create internal/main from public/main - 2025-10-29T19:07:33Z\n 3cb676da72ee9fe561e0ebb3248c417e6021a795 Jenkins Wed Oct 29 07:07:59 2025 +0000 Sync: create internal/main from public/main - 2025-10-29T07:07:59Z\n a23a483bbbcb2db12316f77815afda5a3c68126c Jenkins Tue Oct 28 19:07:19 2025 +0000 Sync: create internal/main from public/main - 2025-10-28T19:07:19Z\n d0b7608482e64115028dd86984f54ce4abee37b2 Sankalp Shrivastava Tue Oct 28 12:47:46 2025 +0530 [IM-10547] Expose AutoScale parameter as FitToScreen in ImageExportOptions class (#101)\n f563622c627a87ce1f1fec7a7949959beb4e72cf Jenkins Tue Oct 28 07:09:01 2025 +0000 Sync: create internal/main from public/main - 2025-10-28T07:09:01Z\n 8b49ba37df19e62c85d33e29ce4b242bd04828f5 Jenkins Mon Oct 27 19:08:07 2025 +0000 Sync: create internal/main from public/main - 2025-10-27T19:08:07Z\n c93310196ab2fb52d06ff6b9c29dd3b83f4fb7a5 Jenkins Mon Oct 27 07:07:49 2025 +0000 Sync: create internal/main from public/main - 2025-10-27T07:07:49Z\n 8eabe35e90e58f54832dd7c536442b6bc71ed32c Jenkins Sun Oct 26 19:07:22 2025 +0000 Sync: create internal/main from public/main - 2025-10-26T19:07:22Z\n 0d3fa10f20d7285f4977bf998da293c9df31c277 Jenkins Sun Oct 26 07:07:28 2025 +0000 Sync: create internal/main from public/main - 2025-10-26T07:07:28Z\n 46bcec0594f3024edf1a99cdbc34a3db8e80ba37 Jenkins Sat Oct 25 19:07:25 2025 +0000 Sync: create internal/main from public/main - 2025-10-25T19:07:25Z\n 296e8b925740cb5655c97e99a7db0b4a1c0adf7e Jenkins Sat Oct 25 07:07:36 2025 +0000 Sync: create internal/main from public/main - 2025-10-25T07:07:36Z\n 1386b383b4a3ac181cd886a76439bcd5404e7363 Jenkins Fri Oct 24 19:07:39 2025 +0000 Sync: create internal/main from public/main - 2025-10-24T19:07:39Z\n 21de316a771f2f2868c06a567c25c45d466e0f0e Jenkins Fri Oct 24 07:08:18 2025 +0000 Sync: create internal/main from public/main - 2025-10-24T07:08:18Z\n 2e14d70880873ab051231b6c82ad2623e1ed1c95 Jenkins Thu Oct 23 19:08:00 2025 +0000 Sync: create internal/main from public/main - 2025-10-23T19:08:00Z\n c34cb274dcdfacf11d41dd6d109fb321503277df Jenkins Thu Oct 23 07:07:38 2025 +0000 Sync: create internal/main from public/main - 2025-10-23T07:07:38Z\n 8c2140d88d6639277b851c6aea2ec18d2e6681f4 Jenkins Wed Oct 22 19:07:28 2025 +0000 Sync: create internal/main from public/main - 2025-10-22T19:07:28Z\n 011aa80588cd0c8f4d52be5cfbc7d8f89d23d490 Jenkins Wed Oct 22 07:08:39 2025 +0000 Sync: create internal/main from public/main - 2025-10-22T07:08:39Z\n af0272dc99eda3ac93bf85a8524e4d6e8bdf1b13 Jenkins Tue Oct 21 19:07:25 2025 +0000 Sync: create internal/main from public/main - 2025-10-21T19:07:25Z\n 98932f13588753f0a20fb8b4bda052eea6c85dec Jenkins Tue Oct 21 07:07:51 2025 +0000 Sync: create internal/main from public/main - 2025-10-21T07:07:51Z\n 3766d5dd3482e550b7ff934433d963875ce1a125 Jenkins Mon Oct 20 19:07:40 2025 +0000 Sync: create internal/main from public/main - 2025-10-20T19:07:40Z\n 3f3438c6f3a33c46530e11ddb4b904f66cfc4140 Jenkins Mon Oct 20 07:07:13 2025 +0000 Sync: create internal/main from public/main - 2025-10-20T07:07:13Z\n b69c44fdcf00bd9700fb58122dbf4b47c82198e3 Jenkins Sun Oct 19 19:07:35 2025 +0000 Sync: create internal/main from public/main - 2025-10-19T19:07:35Z\n ca17f95156b58279e1977383fb6e612553f58fc4 Jenkins Sun Oct 19 07:07:33 2025 +0000 Sync: create internal/main from public/main - 2025-10-19T07:07:33Z\n daecaafe3a64addf2679a8f813e5cd2382c5c00e Jenkins Sat Oct 18 19:07:27 2025 +0000 Sync: create internal/main from public/main - 2025-10-18T19:07:27Z\n 1381d4d3028ef5cd277c18759b20054f8b793c54 Jenkins Sat Oct 18 07:07:42 2025 +0000 Sync: create internal/main from public/main - 2025-10-18T07:07:42Z\n 9c749e3ae0e791b5fe98fe08142c4812d4965b9c Jenkins Fri Oct 17 19:07:37 2025 +0000 Sync: create internal/main from public/main - 2025-10-17T19:07:37Z\n 8c7d910bcf4c02178aafce5ca43357f9ff879079 Jenkins Fri Oct 17 07:09:36 2025 +0000 Sync: create internal/main from public/main - 2025-10-17T07:09:36Z\n 38321853683591c93e9c11ec2097526c507332c5 Jenkins Thu Oct 16 19:07:31 2025 +0000 Sync: create internal/main from public/main - 2025-10-16T19:07:31Z\n 152aea9d8bb393331bce4ec28aa2611666d32be2 Jenkins Thu Oct 16 07:09:03 2025 +0000 Sync: create internal/main from public/main - 2025-10-16T07:09:03Z\n b2ccd36eca9221f176c94bef489625110edb12b7 Jenkins Wed Oct 15 19:07:38 2025 +0000 Sync: create internal/main from public/main - 2025-10-15T19:07:38Z\n 90a3fc09f05002055284cf7faa0b0e239a76f1a4 Jenkins Wed Oct 15 07:06:19 2025 +0000 Sync: create internal/main from public/main - 2025-10-15T07:06:19Z\n 6310e4ec12807a107b0de590951547731ac86a75 Evan Liaw Wed Oct 15 15:22:25 2025 +1100 Remove Legacy Option (#100)\n 8ec6f23d78b5377bd8b66dbce3fb1a4b9d643ed4 Jenkins Tue Oct 14 19:07:39 2025 +0000 Sync: create internal/main from public/main - 2025-10-14T19:07:39Z\n 0ea88e1b4020645eabced93dbaff787ae595add7 Jenkins Tue Oct 14 07:09:27 2025 +0000 Sync: create internal/main from public/main - 2025-10-14T07:09:27Z\n 42f1de1879b22eea524f183c028ac69361ae2967 Jenkins Mon Oct 13 19:07:19 2025 +0000 Sync: create internal/main from public/main - 2025-10-13T19:07:19Z\n 2159fcf054c2efa15737f5c02d118bc0f7c6b302 Jenkins Mon Oct 13 07:06:55 2025 +0000 Sync: create internal/main from public/main - 2025-10-13T07:06:55Z\n 85c687e7426669a390a981463514a535235c9e8e Jenkins Sun Oct 12 19:06:11 2025 +0000 Sync: create internal/main from public/main - 2025-10-12T19:06:11Z\n d8fd808717fd19672e38aa1c535f81acf34106f9 Jenkins Sun Oct 12 07:06:12 2025 +0000 Sync: create internal/main from public/main - 2025-10-12T07:06:12Z\n 288f0aabfd130e3c001ee29946dd2fbde87e9cd6 Jenkins Sat Oct 11 19:06:12 2025 +0000 Sync: create internal/main from public/main - 2025-10-11T19:06:12Z\n 0f8d70a0c8a4f186390653a355ab3433821642d4 Jenkins Sat Oct 11 07:06:15 2025 +0000 Sync: create internal/main from public/main - 2025-10-11T07:06:15Z\n 13da453cc3a731421d80848d69abb045149c398e Jenkins Fri Oct 10 19:06:58 2025 +0000 Sync: create internal/main from public/main - 2025-10-10T19:06:58Z\n 2894e7797f207e58e696a7713f072708e81bcc02 Jenkins Fri Oct 10 07:08:24 2025 +0000 Sync: create internal/main from public/main - 2025-10-10T07:08:24Z\n a8b205946250194e9db2f6f009960626498534c1 Jenkins Thu Oct 9 19:07:14 2025 +0000 Sync: create internal/main from public/main - 2025-10-09T19:07:14Z\n 1253dca45c9c7df128f4157f1c83683a14d5da9d Sankalp Shrivastava Thu Oct 9 13:17:49 2025 +0530 [IM-8675 IM-8676] ImageExportOptions and AnimationExportOptions class Implementation in moldflow-api (#94)\n 572eb77595c2dff4e74c4cdacf65cffb47deb2b3 Jenkins Thu Oct 9 07:07:22 2025 +0000 Sync: create internal/main from public/main - 2025-10-09T07:07:22Z\n 29c79dab1579a933e60c61e172a68314f6299bf5 Isaac Carr Thu Oct 9 17:47:09 2025 +1100 IM-10248, IM-9707 - Clarify and redesign FillHole APIs for 2027.0 to reduce confusion (#97)\n 456e72981a6196a0b92dd6d89d4b852c1563c1ad Jenkins Thu Oct 9 06:38:17 2025 +0000 Sync: create internal/main from public/main - 2025-10-09T06:38:17Z\n 763485e60824567b8f6732765ed8b39133d0810d Jenkins Thu Oct 9 04:14:54 2025 +0000 Sync: create internal/main from public/main - 2025-10-09T04:14:54Z\n 0629e290e9b40a479ca81887b76d36c663e469ab Jenkins Thu Oct 9 03:56:45 2025 +0000 Sync: create internal/main from public/main - 2025-10-09T03:56:45Z\n b6ce3fd6258eaf566b6fb7f1ade7b01801224d58 Jenkins Thu Oct 9 03:56:32 2025 +0000 Sync: create internal/main from public/main - 2025-10-09T03:56:32Z\n 4684057706ab171d03c5a05251f02117bae0ef9f Jenkins Thu Oct 9 03:53:01 2025 +0000 Sync: create internal/main from public/main - 2025-10-09T03:53:01Z\n 2937dafc05e50f02a8ae79de2d253f3b01ba9d55 Sankalp Shrivastava Thu Oct 9 08:12:58 2025 +0530 CADDiagnostic class Implementation (#95)\n 8d31725c1c47a78a93621ee1a641c3aef53ce155 Jenkins Thu Oct 9 02:38:54 2025 +0000 Sync: create internal/main from public/main - 2025-10-09T02:38:54Z\n 5b88de6cecf98f2125eefe98bfe5337046698b18 Jenkins Wed Oct 8 19:06:12 2025 +0000 Sync: create internal/main from public/main - 2025-10-08T19:06:12Z\n b66e344050f88b9a71fc0cfc3ec5e678c4e2c11d Jenkins Wed Oct 8 07:06:51 2025 +0000 Sync: create internal/main from public/main - 2025-10-08T07:06:51Z\n 79d6fa899076223e291df671f9ea4dd7f45617bc Jenkins Wed Oct 8 03:20:59 2025 +0000 Sync: create internal/main from public/main - 2025-10-08T03:20:59Z\n c26559627de1049234d5ad392963e4e62fdcf912 Jenkins Tue Oct 7 19:12:09 2025 +0000 Sync: create internal/main from public/main - 2025-10-07T19:12:09Z\n cb0b31981226b59a1782a2d489437693f1c1cbaa Jenkins Tue Oct 7 15:36:20 2025 +0000 Sync: create internal/main from public/main - 2025-10-07T15:36:20Z\n bfdd9d294f0c230d149f349934d011f8edf51c5e Osi Njoku Wed Oct 8 01:58:44 2025 +1100 Sync: merge public main into internal internal/main - 2025-10-07T14:58:44Z\n fa7e0ceaecc45628e377365b0a8797fa22ce19da Jenkins Sun Oct 5 19:06:11 2025 +0000 Sync: integrate existing origin/internal/main\n 6d027a7b39721d0d064c843d362085eb82b01e48 Jenkins Sun Oct 5 19:06:11 2025 +0000 Sync: set internal Jenkinsfile\n e0b7c17014e9abad81d5b500d87aae347a8fea20 Jenkins Sun Oct 5 19:06:11 2025 +0000 Sync: drop public .github/workflows for internal\n 1cc641ea6e597dcbba26b9217c8ff895eb16db9f Jenkins Sun Oct 5 07:06:10 2025 +0000 Sync: integrate existing origin/internal/main\n 83599da81f67f62e7be7db9abed46eb0263b25f6 Jenkins Sun Oct 5 07:06:10 2025 +0000 Sync: set internal Jenkinsfile\n cb0b08326e862ebe4291dfb8b2b55013905004a8 Jenkins Sun Oct 5 07:06:10 2025 +0000 Sync: drop public .github/workflows for internal\n c927c9085b724cc7d79cb05c87e645b214612320 Jenkins Sat Oct 4 19:06:13 2025 +0000 Sync: integrate existing origin/internal/main\n 6dd625492183d20a9f7e564a0f90896b025eb386 Jenkins Sat Oct 4 19:06:13 2025 +0000 Sync: set internal Jenkinsfile\n 05e64997121ebeb2e81937043e84b34b8238dc89 Jenkins Sat Oct 4 19:06:13 2025 +0000 Sync: drop public .github/workflows for internal\n 1b20398747a79ec2c7e471d21b7575a00f5bb481 Jenkins Sat Oct 4 07:06:16 2025 +0000 Sync: integrate existing origin/internal/main\n b0b46a2332cec003248fcc1aeb4756c36209eace Jenkins Sat Oct 4 07:06:16 2025 +0000 Sync: set internal Jenkinsfile\n 8eed179ec8f63ed1fc29443582d86e2cc05f21cc Jenkins Sat Oct 4 07:06:16 2025 +0000 Sync: drop public .github/workflows for internal\n 825be74cfc82fde9edb86d472a141dbd8d36bf52 Jenkins Fri Oct 3 19:06:11 2025 +0000 Sync: integrate existing origin/internal/main\n fa7ac62622aa3979b4e859de629a0e9ebaff8d32 Jenkins Fri Oct 3 19:06:11 2025 +0000 Sync: set internal Jenkinsfile\n 6d6f4c3762b75678d185842bd1449de5660b9370 Jenkins Fri Oct 3 19:06:11 2025 +0000 Sync: drop public .github/workflows for internal\n 8800888e05cf41995971230084b7dc75f36a1760 Jenkins Fri Oct 3 07:06:12 2025 +0000 Sync: integrate existing origin/internal/main\n c991a5aca86125da9b91b66628e5cf5540903630 Jenkins Fri Oct 3 07:06:12 2025 +0000 Sync: set internal Jenkinsfile\n 924d56097318ad0967529d85048c544c6d01cf2b Jenkins Fri Oct 3 07:06:12 2025 +0000 Sync: drop public .github/workflows for internal\n e8cc30f6ee9f5ae6066c1232c1749d67855c5c25 Jenkins Thu Oct 2 19:06:13 2025 +0000 Sync: integrate existing origin/internal/main\n 90f5dd56f80011c168c54ca2d834ea1859c34931 Jenkins Thu Oct 2 19:06:12 2025 +0000 Sync: set internal Jenkinsfile\n 4c22def97358d67dc36f4cdac63c7f1b81cfeebb Jenkins Thu Oct 2 19:06:12 2025 +0000 Sync: drop public .github/workflows for internal\n aa08abeb1f20362e82537a750f6834028e7aa951 Jenkins Thu Oct 2 07:07:08 2025 +0000 Sync: integrate existing origin/internal/main\n 54f6dba8881763856956e4d637b9f7dcefc7f835 Jenkins Thu Oct 2 07:07:08 2025 +0000 Sync: set internal Jenkinsfile\n acf990ce10d1f24711943b2728849cdb3386f2ae Jenkins Thu Oct 2 07:07:08 2025 +0000 Sync: drop public .github/workflows for internal\n e65d7b2f348335cb63e279c54881f8011485cb53 Jenkins Wed Oct 1 19:07:08 2025 +0000 Sync: integrate existing origin/internal/main\n 5ed5f22f04496f2c55463e61a656bec23920a97b Jenkins Wed Oct 1 19:07:08 2025 +0000 Sync: set internal Jenkinsfile\n 95de1e4830835cbf7e2e3055587a035f0ee33950 Jenkins Wed Oct 1 19:07:08 2025 +0000 Sync: drop public .github/workflows for internal\n f4279fca5c8fc2cedc0465fed68b89cd7423e816 Jenkins Wed Oct 1 07:07:29 2025 +0000 Sync: integrate existing origin/internal/main\n a27967db94021791d2a8f9a05bd6a660ced12dfd Jenkins Wed Oct 1 07:07:29 2025 +0000 Sync: set internal Jenkinsfile\n f0d0d0400574d1e1023d9d624914d095f0ec44ee Jenkins Wed Oct 1 07:07:29 2025 +0000 Sync: drop public .github/workflows for internal\n b1288e85932fdd5cf7b89593ecc79edc22f08beb Jenkins Tue Sep 30 19:07:15 2025 +0000 Sync: integrate existing origin/internal/main\n bf1d3b2a126c2e69b213473fa5bca28063d62729 Jenkins Tue Sep 30 19:07:14 2025 +0000 Sync: set internal Jenkinsfile\n 76648728e09d29e07996519ff47a56942a067902 Jenkins Tue Sep 30 19:07:14 2025 +0000 Sync: drop public .github/workflows for internal\n a2270b07b8b15d5e28fa2361240d461e79e6255b Jenkins Tue Sep 30 13:33:58 2025 +0000 Sync: integrate existing origin/internal/main\n e49afa63af3f2b270dbfa26a59f138b57dd8bdd4 Jenkins Tue Sep 30 13:33:55 2025 +0000 Sync: set internal Jenkinsfile\n 48628cc394b69c7bdadee4b2f7774a8f67fba672 Jenkins Tue Sep 30 13:33:55 2025 +0000 Sync: drop public .github/workflows for internal\n fcf36aec02e70f43bbf5e3cda5b70b4c443881ef Osi Njoku Tue Sep 30 21:20:05 2025 +1000 Sync: set internal Jenkinsfile\n d1bfc89c83e8451318412ac0278f6a2f110028d2 Osi Njoku Tue Sep 30 20:26:53 2025 +1000 Sync: drop public .github/workflows for internal\n --- .../source/components/enums/capture_modes.rst | 4 + docs/source/components/enums/mdl_kernel.rst | 4 - .../wrapper/animation_export_options.rst | 4 + .../components/wrapper/cad_diagnostic.rst | 4 + .../wrapper/image_export_options.rst | 4 + docs/source/readme.rst | 3 +- src/moldflow/__init__.py | 5 +- src/moldflow/animation_export_options.py | 198 +++++++++ src/moldflow/cad_diagnostic.py | 245 ++++++++++ src/moldflow/common.py | 30 +- src/moldflow/constants.py | 5 + src/moldflow/helper.py | 37 +- src/moldflow/image_export_options.py | 417 ++++++++++++++++++ src/moldflow/import_options.py | 24 +- src/moldflow/mesh_editor.py | 41 +- src/moldflow/mesh_generator.py | 10 + src/moldflow/plot_manager.py | 22 +- src/moldflow/study_doc.py | 27 ++ src/moldflow/synergy.py | 23 + src/moldflow/viewer.py | 77 +++- tests/api/unit_tests/mock_container.py | 3 + .../test_unit_animation_export_options.py | 177 ++++++++ .../unit_tests/test_unit_cad_diagnostic.py | 263 +++++++++++ .../test_unit_image_export_options.py | 198 +++++++++ .../unit_tests/test_unit_import_options.py | 13 +- tests/api/unit_tests/test_unit_mesh_editor.py | 115 +++++ .../api/unit_tests/test_unit_plot_manager.py | 10 +- tests/api/unit_tests/test_unit_study_doc.py | 29 +- tests/api/unit_tests/test_unit_synergy.py | 15 +- tests/api/unit_tests/test_unit_viewer.py | 77 +++- tests/core/test_helper.py | 2 + version.json | 2 +- 32 files changed, 2022 insertions(+), 66 deletions(-) create mode 100644 docs/source/components/enums/capture_modes.rst delete mode 100644 docs/source/components/enums/mdl_kernel.rst create mode 100644 docs/source/components/wrapper/animation_export_options.rst create mode 100644 docs/source/components/wrapper/cad_diagnostic.rst create mode 100644 docs/source/components/wrapper/image_export_options.rst create mode 100644 src/moldflow/animation_export_options.py create mode 100644 src/moldflow/cad_diagnostic.py create mode 100644 src/moldflow/image_export_options.py create mode 100644 tests/api/unit_tests/test_unit_animation_export_options.py create mode 100644 tests/api/unit_tests/test_unit_cad_diagnostic.py create mode 100644 tests/api/unit_tests/test_unit_image_export_options.py 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..2e67e39 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 @@ -241,8 +243,6 @@ def check_file_extension(file_name: str, extensions: tuple | str): 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. """ 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..e508ea4 100644 --- a/src/moldflow/import_options.py +++ b/src/moldflow/import_options.py @@ -7,7 +7,7 @@ """ 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 .com_proxy import safe_com @@ -152,28 +152,22 @@ def use_mdl(self, value: bool) -> None: self.import_options.UseMDL = value @property - def mdl_kernel(self) -> str: + 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..2e270c9 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,11 @@ 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 + 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 +426,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..a6a6fd2 100644 --- a/src/moldflow/mesh_generator.py +++ b/src/moldflow/mesh_generator.py @@ -146,6 +146,8 @@ def smoothing(self, value: bool) -> None: @property def element_reduction(self) -> bool: """ + .. deprecated:: 27.0.0 + Enables/disables automatic element size determination for fusion meshes from faceted geometry. @@ -193,6 +195,8 @@ def surface_optimization(self, value: bool) -> None: @property def automatic_tetra_optimization(self) -> bool: """ + .. deprecated:: 27.0.0 + Specifies whether optimizing tetras automatically. :getter: Get the automatic tetra optimization option @@ -289,6 +293,8 @@ def tetra_layers_for_cores(self, value: int) -> None: @property 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 @@ -362,6 +368,8 @@ def maximum_match_distance(self, value: float) -> None: @property 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 @@ -778,6 +786,8 @@ def cad_mesh_minimum_curvature_percentage(self, value: float) -> None: @property 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..1463736 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, @@ -306,6 +310,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: @@ -367,6 +374,9 @@ 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 +389,48 @@ 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) + 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 +468,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): diff --git a/version.json b/version.json index 57c7a4d..87ea328 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { "major": "27", "minor": "0", - "patch": "0" + "patch": "10" } From 60f6630f14851e3c7e7809394446507114e3c79c Mon Sep 17 00:00:00 2001 From: Osi Njoku Date: Mon, 19 Jan 2026 20:07:50 +1100 Subject: [PATCH 11/26] fix version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 87ea328..57c7a4d 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { "major": "27", "minor": "0", - "patch": "10" + "patch": "0" } From 3ebf9f2ea457bfc1f309e3682de6783036409918 Mon Sep 17 00:00:00 2001 From: Osi Njoku Date: Mon, 19 Jan 2026 20:46:42 +1100 Subject: [PATCH 12/26] =?UTF-8?q?docs:=20Update=20CHANGELOG=20for=20PR=20#?= =?UTF-8?q?62=20=E2=80=94=20document=20removal=20of=20GradingFactor,=20Mes?= =?UTF-8?q?hGenerator=20grading=20factor=20change,=20and=20GeomType=20expo?= =?UTF-8?q?sure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 67c63c72bdce7448aa836d43a834649299c18d24 Mon Sep 17 00:00:00 2001 From: Osi Njoku Date: Mon, 19 Jan 2026 20:51:27 +1100 Subject: [PATCH 13/26] docs: Expand Unreleased changelog to reflect all changes in PR #62 --- CHANGELOG.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abf7328..339f37e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,19 +9,27 @@ 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). ### Changed -- `MeshGenerator.cad_mesh_grading_factor` now accepts `float` values in range 0.0 to 1.0 instead of enum/integer-coded options +- `MeshGenerator.cad_mesh_grading_factor` now accepts `float` values in range 0.0 to 1.0 instead of enum/integer-coded options (`src/moldflow/mesh_generator.py`). +- 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`). -### Deprecated -- N/A +### Documentation +- Added/updated component enum docs and wrapper docs (docs/source/components/enums/*, docs/source/components/wrapper/*). +- Updated project readme/documentation index to include the new wrappers and examples (docs/source/readme.rst). ### Removed - `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 +- Exposed `GeomType` enum in package `__init__.py` so users can `from moldflow import GeomType` (`src/moldflow/__init__.py`). +- Fixed `MeshGenerator.cad_mesh_grading_factor` to properly accept float/double values matching the COM API signature instead of restricting to enum values. + +### Tests +- 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). ### Security - N/A From a1a202f1bdd7cd2a1b961c6df30242cb8d499baf Mon Sep 17 00:00:00 2001 From: Osi Njoku Date: Tue, 20 Jan 2026 00:10:11 +1100 Subject: [PATCH 14/26] docs: Restore original changelog section headings and organize new entries --- CHANGELOG.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 339f37e..f2972d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,14 +12,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 - `MeshGenerator.cad_mesh_grading_factor` now accepts `float` values in range 0.0 to 1.0 instead of enum/integer-coded options (`src/moldflow/mesh_generator.py`). - 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). -### Documentation -- Added/updated component enum docs and wrapper docs (docs/source/components/enums/*, docs/source/components/wrapper/*). -- Updated project readme/documentation index to include the new wrappers and examples (docs/source/readme.rst). +### Deprecated +- N/A ### Removed - `GradingFactor` enum - incorrectly restricted the API to discrete values when the COM API accepts continuous float values from 0.0 to 1.0 @@ -28,9 +29,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Exposed `GeomType` enum in package `__init__.py` so users can `from moldflow import GeomType` (`src/moldflow/__init__.py`). - Fixed `MeshGenerator.cad_mesh_grading_factor` to properly accept float/double values matching the COM API signature instead of restricting to enum values. -### Tests -- 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). - ### Security - N/A From 0c5986dbbb0cd7ae12c234d6c7c2c4c357ed3c7a Mon Sep 17 00:00:00 2001 From: Osi Njoku Date: Tue, 20 Jan 2026 00:17:17 +1100 Subject: [PATCH 15/26] docs: Add deprecations from PRs 110 and 97 to Unreleased section --- CHANGELOG.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2972d3..ea7510e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 -- N/A +- Deprecated several legacy Viewer functions and MeshGenerator/ImportOptions properties in preparation for Synergy 2027.0.0 (see PR #110): + - `Viewer.save_image_legacy` (deprecated) + - `Viewer.save_image` (deprecated) + - `Viewer.save_animation` (deprecated) + - `ImportOptions.mdl_kernel` (deprecated) + - `MeshGenerator.automatic_tetra_optimization` (deprecated) + - `MeshGenerator.element_reduction` (deprecated) + - `MeshGenerator.use_fallbacks` (deprecated) + - `MeshGenerator.use_tetras_on_edge` (deprecated) + - `MeshGenerator.tetra_max_ar` (deprecated) +- Added deprecation warnings and clarified FillHole API redesigns (see PR #97 and IM-9707/IM-10248). ### Removed - `GradingFactor` enum - incorrectly restricted the API to discrete values when the COM API accepts continuous float values from 0.0 to 1.0 From 585607646d0737c6d006a87e82066158e2791d39 Mon Sep 17 00:00:00 2001 From: Osi Njoku Date: Tue, 20 Jan 2026 00:18:32 +1100 Subject: [PATCH 16/26] update cl --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea7510e..7ab6f55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 (see PR #110): +- Deprecated several legacy Viewer functions and MeshGenerator/ImportOptions properties: - `Viewer.save_image_legacy` (deprecated) - `Viewer.save_image` (deprecated) - `Viewer.save_animation` (deprecated) @@ -30,7 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `MeshGenerator.use_fallbacks` (deprecated) - `MeshGenerator.use_tetras_on_edge` (deprecated) - `MeshGenerator.tetra_max_ar` (deprecated) -- Added deprecation warnings and clarified FillHole API redesigns (see PR #97 and IM-9707/IM-10248). +- Added deprecation warnings and clarified FillHole API redesigns. ### Removed - `GradingFactor` enum - incorrectly restricted the API to discrete values when the COM API accepts continuous float values from 0.0 to 1.0 From 3a1a51214ed6f9ed8209e1eb25e6384903da409f Mon Sep 17 00:00:00 2001 From: Osi Njoku Date: Tue, 20 Jan 2026 00:26:16 +1100 Subject: [PATCH 17/26] docs version --- docs/source/_static/switcher.json | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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 +] From 907378b2a0ffd2784b57b0b57f4df8ad83f0db67 Mon Sep 17 00:00:00 2001 From: Osi Njoku Date: Tue, 20 Jan 2026 17:09:56 +1100 Subject: [PATCH 18/26] docs: Normalize CHANGELOG (remove duplicated block after merge with release/26) --- CHANGELOG.md | 229 ++++++++++++++++++++++++++------------------------- 1 file changed, 118 insertions(+), 111 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4516f17..cf8aa3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,157 +48,164 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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 (see PRs and Jira tickets for details) + # Changelog - ### Removed - - `GradingFactor` enum - incorrectly restricted the API to discrete values when the COM API accepts continuous float values from 0.0 to 1.0 + All notable changes to this project will be documented in this file. - ### Fixed - - Exposed `GeomType` enum in package `__init__.py` so users can `from moldflow import GeomType` (`src/moldflow/__init__.py`) - - Fixed `MeshGenerator.cad_mesh_grading_factor` to properly accept float/double values matching the COM API signature instead of restricting to enum values + The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), + and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - ### Security - - N/A + ## [Unreleased] - ## [26.0.5] + ### 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` and 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) - ### 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 (`src/moldflow/mesh_generator.py`) + - 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) - ### Changed - - `MeshGenerator.cad_mesh_grading_factor` now accepts `float` values in range 0.0 to 1.0 instead of enum/integer-coded options + ### 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 (see PRs and Jira tickets for details) - ### Deprecated - - N/A + ### Removed + - `GradingFactor` enum - incorrectly restricted the API to discrete values when the COM API accepts continuous float values from 0.0 to 1.0 - ### Removed - - `GradingFactor` enum - incorrectly restricted the API to discrete values when the COM API accepts continuous float values from 0.0 to 1.0 + ### Fixed + - Exposed `GeomType` enum in package `__init__.py` so users can `from moldflow import GeomType` (`src/moldflow/__init__.py`) + - Fixed `MeshGenerator.cad_mesh_grading_factor` to properly accept float/double values matching the COM API signature instead of restricting to enum values - ### Fixed - - 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 - ### Security - - N/A + ## [26.0.5] - ## [26.0.4] + ### Added + - N/A - ### 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 - ### Changed - - N/A + ### Deprecated + - N/A - ### Deprecated - - N/A + ### Removed + - `GradingFactor` enum - incorrectly restricted the API to discrete values when the COM API accepts continuous float values from 0.0 to 1.0 - ### Removed - - N/A + ### Fixed + - Fixed `MeshGenerator.cad_mesh_grading_factor` to properly accept float/double values matching the COM API signature instead of restricting to enum values - ### 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 - ### Security - - N/A + ## [26.0.4] - ## [26.0.3] + ### Added + - N/A - ### Added - - N/A + ### Changed + - N/A - ### Changed - - N/A + ### Deprecated + - N/A - ### Deprecated - - N/A + ### Removed + - 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 - ### Fixed - - Fixed README links + ### Security + - N/A - ### Security - - N/A + ## [26.0.3] - ## [26.0.2] - 2025-10-10 + ### Added + - N/A - ### Added - - Added convenience class for showing message boxes and text input dialogs via Win32 - - Add more examples in the documentation + ### Changed + - N/A - ### Changed - - N/A + ### Deprecated + - N/A - ### Deprecated - - N/A + ### Removed + - N/A - ### Removed - - N/A + ### Fixed + - Fixed README links - ### Fixed - - N/A + ### Security + - N/A - ### Security - - N/A + ## [26.0.2] - 2025-10-10 - ## [26.0.1] - 2025-09-12 + ### Added + - Added convenience class for showing message boxes and text input dialogs via Win32 + - Add more examples in the documentation - ### Added - - N/A + ### Changed + - N/A - ### Changed - - N/A + ### Deprecated + - N/A - ### Deprecated - - N/A + ### Removed + - N/A - ### Removed - - N/A + ### Fixed + - N/A - ### Fixed - - Fix return types for `from_list` functions in data classes - - Fix color band range options to 1 to 256 + ### Security + - N/A - ### Security - - N/A + ## [26.0.1] - 2025-09-12 - ## [26.0.0] - 2025-09-01 + ### Added + - N/A - ### Added - - Initial version aligned with Moldflow Synergy 2026.0.1 - - Python 3.10-3.13 compatibility + ### Changed + - N/A - [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 - [26.0.0]: https://github.com/Autodesk/moldflow-api/releases/tag/v26.0.0 -### Security -- N/A + ### Deprecated + - N/A -## [26.0.0] - 2025-09-01 + ### Removed + - N/A -### Added -- 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.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 -[26.0.0]: https://github.com/Autodesk/moldflow-api/releases/tag/v26.0.0 + ### Fixed + - Fix return types for `from_list` functions in data classes + - Fix color band range options to 1 to 256 + + ### Security + - N/A + + ## [26.0.0] - 2025-09-01 + + ### Added + - 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.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 + [26.0.0]: https://github.com/Autodesk/moldflow-api/releases/tag/v26.0.0 From 50ec64fd50b9fbcdfc82b79cc88e79263e27bfc1 Mon Sep 17 00:00:00 2001 From: Osi Njoku Date: Tue, 20 Jan 2026 17:13:34 +1100 Subject: [PATCH 19/26] docs: Fix CHANGELOG duplication and indentation --- CHANGELOG.md | 247 +++++++++++++++++++++------------------------------ 1 file changed, 99 insertions(+), 148 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf8aa3f..815f561 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,192 +20,143 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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: - - `Viewer.save_image_legacy` (deprecated) - - `Viewer.save_image` (deprecated) - - `Viewer.save_animation` (deprecated) - - `ImportOptions.mdl_kernel` (deprecated) - # Changelog - All notable changes to this project will be documented in this file. +- 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 - The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +### Removed +- `GradingFactor` enum - incorrectly restricted the API to discrete values when the COM API accepts continuous float values from 0.0 to 1.0 - ## [Unreleased] +### Fixed +- Exposed `GeomType` enum in package `__init__.py` so users can `from moldflow import GeomType` (`src/moldflow/__init__.py`) +- Fixed `MeshGenerator.cad_mesh_grading_factor` to properly accept float/double values matching the COM API signature instead of restricting to enum values - ### 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` and 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) +### Security +- 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 (`src/moldflow/mesh_generator.py`) - - 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) +## [26.0.5] - ### Deprecated - - Deprecated several legacy Viewer functions and MeshGenerator/ImportOptions properties in preparation for Synergy 2027.0.0: - # Changelog - - All notable changes to this project will be documented in this file. - - The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - - ## [Unreleased] - - ### 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` and 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 - - `MeshGenerator.cad_mesh_grading_factor` now accepts `float` values in range 0.0 to 1.0 instead of enum/integer-coded options (`src/moldflow/mesh_generator.py`) - - 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 (see PRs and Jira tickets for details) - - ### Removed - - `GradingFactor` enum - incorrectly restricted the API to discrete values when the COM API accepts continuous float values from 0.0 to 1.0 - - ### Fixed - - Exposed `GeomType` enum in package `__init__.py` so users can `from moldflow import GeomType` (`src/moldflow/__init__.py`) - - 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.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 +### Added +- N/A - ### Deprecated - - 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 - ### Removed - - `GradingFactor` enum - incorrectly restricted the API to discrete values when the COM API accepts continuous float values from 0.0 to 1.0 +### Deprecated +- N/A - ### Fixed - - Fixed `MeshGenerator.cad_mesh_grading_factor` to properly accept float/double values matching the COM API signature instead of restricting to enum values +### Removed +- `GradingFactor` enum - incorrectly restricted the API to discrete values when the COM API accepts continuous float values from 0.0 to 1.0 - ### Security - - N/A +### Fixed +- Fixed `MeshGenerator.cad_mesh_grading_factor` to properly accept float/double values matching the COM API signature instead of restricting to enum values - ## [26.0.4] +### Security +- N/A - ### Added - - N/A +## [26.0.4] - ### Changed - - N/A +### Added +- N/A - ### Deprecated - - N/A +### Changed +- N/A - ### Removed - - N/A +### Deprecated +- 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 +### Removed +- N/A - ### Security - - 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 - ## [26.0.3] +### Security +- N/A - ### Added - - N/A +## [26.0.3] - ### Changed - - N/A +### Added +- N/A - ### Deprecated - - N/A +### Changed +- N/A - ### Removed - - N/A +### Deprecated +- N/A - ### Fixed - - Fixed README links +### Removed +- N/A - ### Security - - N/A +### Fixed +- Fixed README links - ## [26.0.2] - 2025-10-10 +### Security +- N/A - ### Added - - Added convenience class for showing message boxes and text input dialogs via Win32 - - Add more examples in the documentation +## [26.0.2] - 2025-10-10 - ### Changed - - N/A +### Added +- Added convenience class for showing message boxes and text input dialogs via Win32 +- Add more examples in the documentation - ### Deprecated - - N/A +### Changed +- N/A - ### Removed - - N/A +### Deprecated +- N/A - ### Fixed - - N/A +### Removed +- N/A - ### Security - - N/A +### Fixed +- N/A - ## [26.0.1] - 2025-09-12 +### Security +- N/A - ### Added - - N/A +## [26.0.1] - 2025-09-12 - ### Changed - - N/A +### Added +- N/A - ### Deprecated - - N/A +### Changed +- N/A - ### Removed - - N/A +### Deprecated +- N/A - ### Fixed - - Fix return types for `from_list` functions in data classes - - Fix color band range options to 1 to 256 +### Removed +- N/A - ### Security - - N/A +### Fixed +- Fix return types for `from_list` functions in data classes +- Fix color band range options to 1 to 256 - ## [26.0.0] - 2025-09-01 +### Security +- N/A - ### Added - - Initial version aligned with Moldflow Synergy 2026.0.1 - - Python 3.10-3.13 compatibility +## [26.0.0] - 2025-09-01 - [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 - [26.0.0]: https://github.com/Autodesk/moldflow-api/releases/tag/v26.0.0 +### Added +- 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.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 +[26.0.0]: https://github.com/Autodesk/moldflow-api/releases/tag/v26.0.0 From f44b8196baaacd7cafa272df2387bf83d2c336ac Mon Sep 17 00:00:00 2001 From: Osi Njoku Date: Tue, 20 Jan 2026 17:13:48 +1100 Subject: [PATCH 20/26] docs: Tidy CHANGELOG formatting --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 815f561..b96fe8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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` From 79d3d5be58129c777839d30460e3da2588b0b341 Mon Sep 17 00:00:00 2001 From: Osi Njoku Date: Tue, 20 Jan 2026 17:24:43 +1100 Subject: [PATCH 21/26] normalise deprecations --- src/moldflow/import_options.py | 3 ++- src/moldflow/mesh_editor.py | 2 ++ src/moldflow/mesh_generator.py | 7 ++++++- src/moldflow/viewer.py | 4 ++++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/moldflow/import_options.py b/src/moldflow/import_options.py index e508ea4..1dcf2e3 100644 --- a/src/moldflow/import_options.py +++ b/src/moldflow/import_options.py @@ -9,7 +9,7 @@ from .logger import process_log 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,6 +152,7 @@ def use_mdl(self, value: bool) -> None: self.import_options.UseMDL = value @property + @deprecated() def mdl_kernel(self): """ .. deprecated:: 27.0.0 diff --git a/src/moldflow/mesh_editor.py b/src/moldflow/mesh_editor.py index 2e270c9..be243c7 100644 --- a/src/moldflow/mesh_editor.py +++ b/src/moldflow/mesh_editor.py @@ -407,6 +407,8 @@ def align_normals(self, seed_tri: EntList | None, tris: EntList | None) -> int: @deprecated("fill_hole_from_nodes or fill_hole_from_triangles") def fill_hole(self, nodes: EntList | None, fill_type: int | None = None) -> bool: """ + .. deprecated:: 27.0.0 + 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. diff --git a/src/moldflow/mesh_generator.py b/src/moldflow/mesh_generator.py index a6a6fd2..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,6 +144,7 @@ def smoothing(self, value: bool) -> None: self.mesh_generator.Smoothing = value @property + @deprecated() def element_reduction(self) -> bool: """ .. deprecated:: 27.0.0 @@ -193,6 +194,7 @@ def surface_optimization(self, value: bool) -> None: self.mesh_generator.SurfaceOptimization = value @property + @deprecated() def automatic_tetra_optimization(self) -> bool: """ .. deprecated:: 27.0.0 @@ -291,6 +293,7 @@ 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 @@ -366,6 +369,7 @@ 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 @@ -784,6 +788,7 @@ 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 diff --git a/src/moldflow/viewer.py b/src/moldflow/viewer.py index 1463736..1eaf535 100644 --- a/src/moldflow/viewer.py +++ b/src/moldflow/viewer.py @@ -35,6 +35,7 @@ check_is_non_negative, check_file_extension, coerce_optional_dispatch, + deprecated, ) from .errors import raise_value_error from .common import ValueErrorReason @@ -292,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, @@ -370,6 +372,7 @@ def save_image( min_max, ) + @deprecated("save_animation_with_options") def save_animation( self, filename: str, speed: AnimationSpeed | str, prompts: bool = False ) -> bool: @@ -426,6 +429,7 @@ def save_animation_with_options(self, options: Optional[AnimationExportOptions] 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 From c171a1abd7feb57ea3fdd4ec1e3dd73375ce63c3 Mon Sep 17 00:00:00 2001 From: osinjoku <49887472+osinjoku@users.noreply.github.com> Date: Tue, 20 Jan 2026 17:29:33 +1100 Subject: [PATCH 22/26] Update CHANGELOG.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b96fe8d..281dd7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,7 +36,6 @@ 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 -- Exposed `GeomType` enum in package `__init__.py` so users can `from moldflow import GeomType` (`src/moldflow/__init__.py`) - Fixed `MeshGenerator.cad_mesh_grading_factor` to properly accept float/double values matching the COM API signature instead of restricting to enum values ### Security From 3e31cc85992d2884e64a8b2a68068a7c6e404376 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 Jan 2026 17:53:05 +1100 Subject: [PATCH 23/26] Remove duplicate CHANGELOG entry from Unreleased section (#63) * Initial plan * Remove duplicate CHANGELOG entry from Unreleased section Co-authored-by: osinjoku <49887472+osinjoku@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: osinjoku <49887472+osinjoku@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 281dd7a..b64f115 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,7 +36,7 @@ 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 `MeshGenerator.cad_mesh_grading_factor` to properly accept float/double values matching the COM API signature instead of restricting to enum values +- N/A ### Security - N/A From b6da6d5a11776abb208b766337b2c691ac7fb39e Mon Sep 17 00:00:00 2001 From: osinjoku <49887472+osinjoku@users.noreply.github.com> Date: Tue, 20 Jan 2026 17:57:58 +1100 Subject: [PATCH 24/26] Update src/moldflow/helper.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/moldflow/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/moldflow/helper.py b/src/moldflow/helper.py index 2e67e39..d9c682e 100644 --- a/src/moldflow/helper.py +++ b/src/moldflow/helper.py @@ -242,7 +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. + 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) From 54a963b32f00e28d431b60d7202a442fde0cdcb3 Mon Sep 17 00:00:00 2001 From: osinjoku <49887472+osinjoku@users.noreply.github.com> Date: Tue, 20 Jan 2026 20:15:29 +1100 Subject: [PATCH 25/26] Update src/moldflow/mesh_editor.py Co-authored-by: Sankalp Shrivastava --- src/moldflow/mesh_editor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/moldflow/mesh_editor.py b/src/moldflow/mesh_editor.py index be243c7..3c2f1ae 100644 --- a/src/moldflow/mesh_editor.py +++ b/src/moldflow/mesh_editor.py @@ -408,7 +408,7 @@ def align_normals(self, seed_tri: EntList | None, tris: EntList | None) -> int: def fill_hole(self, nodes: EntList | None, fill_type: int | None = None) -> bool: """ .. 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. From 78fc0ef42bbc2871a29e495548c0f7549f438475 Mon Sep 17 00:00:00 2001 From: Osi Njoku Date: Tue, 20 Jan 2026 20:18:49 +1100 Subject: [PATCH 26/26] remove changelog duplicates --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b64f115..c4124b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 -- `MeshGenerator.cad_mesh_grading_factor` now accepts `float` values in range 0.0 to 1.0 instead of enum/integer-coded options (`src/moldflow/mesh_generator.py`). - 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). @@ -33,7 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added deprecation warnings and clarified FillHole API redesigns ### Removed -- `GradingFactor` enum - incorrectly restricted the API to discrete values when the COM API accepts continuous float values from 0.0 to 1.0 +- N/A ### Fixed - N/A