MoleditPy 3.2.0__tar.gz → 3.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. {moleditpy-3.2.0 → moleditpy-3.3.1}/PKG-INFO +22 -4
  2. {moleditpy-3.2.0 → moleditpy-3.3.1}/README.md +21 -3
  3. {moleditpy-3.2.0 → moleditpy-3.3.1}/pyproject.toml +1 -1
  4. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/MoleditPy.egg-info/PKG-INFO +22 -4
  5. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/MoleditPy.egg-info/SOURCES.txt +1 -0
  6. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/__init__.py +2 -0
  7. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/angle_dialog.py +27 -12
  8. moleditpy-3.3.1/src/moleditpy/ui/atom_picking.py +163 -0
  9. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/bond_length_dialog.py +13 -7
  10. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/calculation_worker.py +3 -2
  11. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/constrained_optimization_dialog.py +80 -4
  12. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/custom_interactor_style.py +180 -151
  13. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/dialog_3d_picking_mixin.py +60 -50
  14. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/dihedral_dialog.py +34 -15
  15. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/edit_3d_logic.py +23 -20
  16. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/export_logic.py +99 -106
  17. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/main_window_init.py +43 -10
  18. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/molecular_scene_handler.py +0 -2
  19. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/move_group_dialog.py +20 -73
  20. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/ui_manager.py +9 -0
  21. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/view_3d_logic.py +131 -109
  22. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/utils/constants.py +31 -1
  23. {moleditpy-3.2.0 → moleditpy-3.3.1}/LICENSE +0 -0
  24. {moleditpy-3.2.0 → moleditpy-3.3.1}/setup.cfg +0 -0
  25. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
  26. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/MoleditPy.egg-info/entry_points.txt +0 -0
  27. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/MoleditPy.egg-info/requires.txt +0 -0
  28. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/MoleditPy.egg-info/top_level.txt +0 -0
  29. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/__main__.py +0 -0
  30. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/assets/file_icon.ico +0 -0
  31. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/assets/icon.icns +0 -0
  32. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/assets/icon.ico +0 -0
  33. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/assets/icon.png +0 -0
  34. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/core/__init__.py +0 -0
  35. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/core/mol_geometry.py +0 -0
  36. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/core/molecular_data.py +0 -0
  37. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/main.py +0 -0
  38. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/plugins/__init__.py +0 -0
  39. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/plugins/plugin_interface.py +0 -0
  40. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/plugins/plugin_manager.py +0 -0
  41. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/plugins/plugin_manager_window.py +0 -0
  42. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/__init__.py +0 -0
  43. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/about_dialog.py +0 -0
  44. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/align_plane_dialog.py +0 -0
  45. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/alignment_dialog.py +0 -0
  46. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/analysis_window.py +0 -0
  47. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/app_state.py +0 -0
  48. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/atom_item.py +0 -0
  49. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/base_picking_dialog.py +0 -0
  50. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/bond_item.py +0 -0
  51. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/color_settings_dialog.py +0 -0
  52. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/compute_logic.py +0 -0
  53. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/custom_qt_interactor.py +0 -0
  54. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/dialog_logic.py +0 -0
  55. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/edit_actions_logic.py +0 -0
  56. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/geometry_base_dialog.py +0 -0
  57. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/io_logic.py +0 -0
  58. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/main_window.py +0 -0
  59. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/mirror_dialog.py +0 -0
  60. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/molecule_scene.py +0 -0
  61. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/periodic_table_dialog.py +0 -0
  62. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/planarize_dialog.py +0 -0
  63. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/settings_dialog.py +0 -0
  64. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/settings_tabs/__init__.py +0 -0
  65. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/settings_tabs/settings_2d_tab.py +0 -0
  66. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/settings_tabs/settings_3d_tabs.py +0 -0
  67. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/settings_tabs/settings_other_tab.py +0 -0
  68. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/settings_tabs/settings_tab_base.py +0 -0
  69. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/string_importers.py +0 -0
  70. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/template_preview_item.py +0 -0
  71. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/template_preview_view.py +0 -0
  72. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/translation_dialog.py +0 -0
  73. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/user_template_dialog.py +0 -0
  74. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/ui/zoomable_view.py +0 -0
  75. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/utils/__init__.py +0 -0
  76. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/utils/default_settings.py +0 -0
  77. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/utils/sip_isdeleted_safe.py +0 -0
  78. {moleditpy-3.2.0 → moleditpy-3.3.1}/src/moleditpy/utils/system_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy
3
- Version: 3.2.0
3
+ Version: 3.3.1
4
4
  Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
5
5
  Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -710,11 +710,13 @@ Dynamic: license-file
710
710
  [![Python Versions](https://img.shields.io/pypi/pyversions/MoleditPy.svg)](https://pypi.org/project/MoleditPy/)
711
711
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
712
712
  [![Build Status](https://github.com/HiroYokoyama/python_molecular_editor/actions/workflows/tests.yml/badge.svg)](https://github.com/HiroYokoyama/python_molecular_editor/actions)
713
- ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage-75%25-green)
714
- ![Overall Coverage](https://img.shields.io/badge/coverage-67%25-green)
713
+ ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage->80%25-green)
714
+ ![Overall Coverage](https://img.shields.io/badge/coverage->75%25-green)
715
715
  ![GUI Status](https://img.shields.io/badge/GUI-Manually_Verified-blue)
716
- ![Pylint Score](https://img.shields.io/badge/pylint-9%2F10-brightgreen)
716
+ ![Pylint Score](https://img.shields.io/badge/pylint->9%2F10-brightgreen)
717
717
  [![PyPI Downloads](https://static.pepy.tech/personalized-badge/moleditpy?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/moleditpy)
718
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/moleditpy?period=monthly&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=monthly+downloads)](https://pepy.tech/projects/moleditpy)
719
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/HiroYokoyama/python_molecular_editor)
718
720
 
719
721
  [🇯🇵 日本語 (Japanese)](#japanese)
720
722
 
@@ -840,6 +842,14 @@ moleditpy
840
842
 
841
843
  This project is licensed under the **GNU General Public License v3.0 (GPL-v3)**. See the `LICENSE` file for details.
842
844
 
845
+ ## Citation
846
+
847
+ If you use this software in your work, please cite it as follows:
848
+
849
+ ```
850
+ Yokoyama, H. (2026). MoleditPy — A Python-based molecular editing software. Zenodo. https://doi.org/10.5281/zenodo.17268532
851
+ ```
852
+
843
853
  -----
844
854
 
845
855
  <div id="japanese"></div>
@@ -963,3 +973,11 @@ moleditpy
963
973
  ## ライセンス
964
974
 
965
975
  このプロジェクトは **GNU General Public License v3.0 (GPL-v3)** のもとで公開されています。詳細は `LICENSE` ファイルを参照してください。
976
+
977
+ ## 引用
978
+
979
+ 本ソフトウェアを研究で使用される場合は、以下の通り引用を明記してください。
980
+
981
+ ```
982
+ Yokoyama, H. (2026). MoleditPy — A Python-based molecular editing software. Zenodo. https://doi.org/10.5281/zenodo.17268532
983
+ ```
@@ -6,11 +6,13 @@
6
6
  [![Python Versions](https://img.shields.io/pypi/pyversions/MoleditPy.svg)](https://pypi.org/project/MoleditPy/)
7
7
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
8
8
  [![Build Status](https://github.com/HiroYokoyama/python_molecular_editor/actions/workflows/tests.yml/badge.svg)](https://github.com/HiroYokoyama/python_molecular_editor/actions)
9
- ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage-75%25-green)
10
- ![Overall Coverage](https://img.shields.io/badge/coverage-67%25-green)
9
+ ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage->80%25-green)
10
+ ![Overall Coverage](https://img.shields.io/badge/coverage->75%25-green)
11
11
  ![GUI Status](https://img.shields.io/badge/GUI-Manually_Verified-blue)
12
- ![Pylint Score](https://img.shields.io/badge/pylint-9%2F10-brightgreen)
12
+ ![Pylint Score](https://img.shields.io/badge/pylint->9%2F10-brightgreen)
13
13
  [![PyPI Downloads](https://static.pepy.tech/personalized-badge/moleditpy?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/moleditpy)
14
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/moleditpy?period=monthly&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=monthly+downloads)](https://pepy.tech/projects/moleditpy)
15
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/HiroYokoyama/python_molecular_editor)
14
16
 
15
17
  [🇯🇵 日本語 (Japanese)](#japanese)
16
18
 
@@ -136,6 +138,14 @@ moleditpy
136
138
 
137
139
  This project is licensed under the **GNU General Public License v3.0 (GPL-v3)**. See the `LICENSE` file for details.
138
140
 
141
+ ## Citation
142
+
143
+ If you use this software in your work, please cite it as follows:
144
+
145
+ ```
146
+ Yokoyama, H. (2026). MoleditPy — A Python-based molecular editing software. Zenodo. https://doi.org/10.5281/zenodo.17268532
147
+ ```
148
+
139
149
  -----
140
150
 
141
151
  <div id="japanese"></div>
@@ -259,3 +269,11 @@ moleditpy
259
269
  ## ライセンス
260
270
 
261
271
  このプロジェクトは **GNU General Public License v3.0 (GPL-v3)** のもとで公開されています。詳細は `LICENSE` ファイルを参照してください。
272
+
273
+ ## 引用
274
+
275
+ 本ソフトウェアを研究で使用される場合は、以下の通り引用を明記してください。
276
+
277
+ ```
278
+ Yokoyama, H. (2026). MoleditPy — A Python-based molecular editing software. Zenodo. https://doi.org/10.5281/zenodo.17268532
279
+ ```
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "MoleditPy"
7
7
 
8
- version = "3.2.0"
8
+ version = "3.3.1"
9
9
 
10
10
  license = {file = "LICENSE"}
11
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy
3
- Version: 3.2.0
3
+ Version: 3.3.1
4
4
  Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
5
5
  Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -710,11 +710,13 @@ Dynamic: license-file
710
710
  [![Python Versions](https://img.shields.io/pypi/pyversions/MoleditPy.svg)](https://pypi.org/project/MoleditPy/)
711
711
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
712
712
  [![Build Status](https://github.com/HiroYokoyama/python_molecular_editor/actions/workflows/tests.yml/badge.svg)](https://github.com/HiroYokoyama/python_molecular_editor/actions)
713
- ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage-75%25-green)
714
- ![Overall Coverage](https://img.shields.io/badge/coverage-67%25-green)
713
+ ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage->80%25-green)
714
+ ![Overall Coverage](https://img.shields.io/badge/coverage->75%25-green)
715
715
  ![GUI Status](https://img.shields.io/badge/GUI-Manually_Verified-blue)
716
- ![Pylint Score](https://img.shields.io/badge/pylint-9%2F10-brightgreen)
716
+ ![Pylint Score](https://img.shields.io/badge/pylint->9%2F10-brightgreen)
717
717
  [![PyPI Downloads](https://static.pepy.tech/personalized-badge/moleditpy?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/moleditpy)
718
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/moleditpy?period=monthly&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=monthly+downloads)](https://pepy.tech/projects/moleditpy)
719
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/HiroYokoyama/python_molecular_editor)
718
720
 
719
721
  [🇯🇵 日本語 (Japanese)](#japanese)
720
722
 
@@ -840,6 +842,14 @@ moleditpy
840
842
 
841
843
  This project is licensed under the **GNU General Public License v3.0 (GPL-v3)**. See the `LICENSE` file for details.
842
844
 
845
+ ## Citation
846
+
847
+ If you use this software in your work, please cite it as follows:
848
+
849
+ ```
850
+ Yokoyama, H. (2026). MoleditPy — A Python-based molecular editing software. Zenodo. https://doi.org/10.5281/zenodo.17268532
851
+ ```
852
+
843
853
  -----
844
854
 
845
855
  <div id="japanese"></div>
@@ -963,3 +973,11 @@ moleditpy
963
973
  ## ライセンス
964
974
 
965
975
  このプロジェクトは **GNU General Public License v3.0 (GPL-v3)** のもとで公開されています。詳細は `LICENSE` ファイルを参照してください。
976
+
977
+ ## 引用
978
+
979
+ 本ソフトウェアを研究で使用される場合は、以下の通り引用を明記してください。
980
+
981
+ ```
982
+ Yokoyama, H. (2026). MoleditPy — A Python-based molecular editing software. Zenodo. https://doi.org/10.5281/zenodo.17268532
983
+ ```
@@ -29,6 +29,7 @@ src/moleditpy/ui/analysis_window.py
29
29
  src/moleditpy/ui/angle_dialog.py
30
30
  src/moleditpy/ui/app_state.py
31
31
  src/moleditpy/ui/atom_item.py
32
+ src/moleditpy/ui/atom_picking.py
32
33
  src/moleditpy/ui/base_picking_dialog.py
33
34
  src/moleditpy/ui/bond_item.py
34
35
  src/moleditpy/ui/bond_length_dialog.py
@@ -18,3 +18,5 @@ try:
18
18
  OBABEL_AVAILABLE = importlib.util.find_spec("openbabel") is not None
19
19
  except ImportError:
20
20
  OBABEL_AVAILABLE = False
21
+
22
+ from .utils.constants import VERSION as __version__
@@ -168,20 +168,35 @@ class AngleDialog(GeometryBaseDialog):
168
168
 
169
169
  def on_atom_picked(self, atom_idx: int) -> None:
170
170
  """Handle atom picking event in the 3D view."""
171
- if self.atom1_idx is None:
172
- self.atom1_idx = atom_idx
173
- elif self.atom2_idx is None:
174
- self.atom2_idx = atom_idx
175
- elif self.atom3_idx is None:
176
- self.atom3_idx = atom_idx
177
- # Take a fresh snapshot immediately upon completing the triad selection
178
- self._snapshot_positions = self.mol.GetConformer().GetPositions().copy()
179
- else:
180
- # Reset and start over
181
- self.atom1_idx = atom_idx
182
- self.atom2_idx = None
171
+ # Deselection logic: if already selected, remove and shift down
172
+ if atom_idx == self.atom1_idx:
173
+ self.atom1_idx = self.atom2_idx
174
+ self.atom2_idx = self.atom3_idx
183
175
  self.atom3_idx = None
184
176
  self._snapshot_positions = None
177
+ elif atom_idx == self.atom2_idx:
178
+ self.atom2_idx = self.atom3_idx
179
+ self.atom3_idx = None
180
+ self._snapshot_positions = None
181
+ elif atom_idx == self.atom3_idx:
182
+ self.atom3_idx = None
183
+ self._snapshot_positions = None
184
+ else:
185
+ # Selection logic
186
+ if self.atom1_idx is None:
187
+ self.atom1_idx = atom_idx
188
+ elif self.atom2_idx is None:
189
+ self.atom2_idx = atom_idx
190
+ elif self.atom3_idx is None:
191
+ self.atom3_idx = atom_idx
192
+ # Take a fresh snapshot immediately upon completing the triad selection
193
+ self._snapshot_positions = self.mol.GetConformer().GetPositions().copy()
194
+ else:
195
+ # Reset and start over
196
+ self.atom1_idx = atom_idx
197
+ self.atom2_idx = None
198
+ self.atom3_idx = None
199
+ self._snapshot_positions = None
185
200
 
186
201
  self.update_display()
187
202
 
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ MoleditPy — A Python-based molecular editing software
6
+
7
+ Author: Hiromichi Yokoyama
8
+ License: GPL-3.0 license
9
+ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
+ DOI: 10.5281/zenodo.17268532
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any, Optional
16
+
17
+ import numpy as np
18
+
19
+ try:
20
+ from ..utils.constants import VDW_RADII, pt
21
+ except ImportError:
22
+ from moleditpy.utils.constants import VDW_RADII, pt
23
+
24
+
25
+ def _world_to_display(renderer: Any, pos: Any) -> Optional[tuple[float, float, float]]:
26
+ try:
27
+ renderer.SetWorldPoint(float(pos[0]), float(pos[1]), float(pos[2]), 1.0)
28
+ renderer.WorldToDisplay()
29
+ display = renderer.GetDisplayPoint()
30
+ return (float(display[0]), float(display[1]), float(display[2]))
31
+ except (AttributeError, RuntimeError, TypeError, ValueError, IndexError):
32
+ return None
33
+
34
+
35
+ def _atom_world_radius(view_3d_manager: Any, mol: Any, atom_idx: int) -> float:
36
+ try:
37
+ atom = mol.GetAtomWithIdx(int(atom_idx))
38
+ symbol = atom.GetSymbol()
39
+ except (AttributeError, RuntimeError, TypeError, ValueError):
40
+ symbol = "C"
41
+
42
+ settings = {}
43
+ try:
44
+ settings = view_3d_manager.host.init_manager.settings
45
+ except (AttributeError, RuntimeError, TypeError):
46
+ pass
47
+
48
+ style = str(getattr(view_3d_manager, "current_3d_style", "ball_and_stick"))
49
+ style = style.lower().replace(" ", "_")
50
+
51
+ if style == "cpk":
52
+ scale = settings.get("cpk_atom_scale", 1.0)
53
+ try:
54
+ radius = pt.GetRvdw(pt.GetAtomicNumber(symbol))
55
+ return float(radius if radius > 0.1 else 1.5) * float(scale)
56
+ except (AttributeError, RuntimeError, TypeError, ValueError):
57
+ return 1.5 * float(scale)
58
+
59
+ if style == "stick":
60
+ return float(settings.get("stick_bond_radius", 0.15))
61
+
62
+ if style == "wireframe":
63
+ return 0.01
64
+
65
+ scale = settings.get("ball_stick_atom_scale", 1.0)
66
+ return float(VDW_RADII.get(symbol, 0.4)) * float(scale)
67
+
68
+
69
+ def _projected_radius_px(
70
+ renderer: Any, center: Any, world_radius: float
71
+ ) -> Optional[float]:
72
+ center_display = _world_to_display(renderer, center)
73
+ if center_display is None:
74
+ return None
75
+
76
+ offsets = (
77
+ (world_radius, 0.0, 0.0),
78
+ (0.0, world_radius, 0.0),
79
+ (0.0, 0.0, world_radius),
80
+ )
81
+ radius_px = 0.0
82
+ for offset in offsets:
83
+ edge = (
84
+ float(center[0]) + offset[0],
85
+ float(center[1]) + offset[1],
86
+ float(center[2]) + offset[2],
87
+ )
88
+ edge_display = _world_to_display(renderer, edge)
89
+ if edge_display is None:
90
+ continue
91
+ radius_px = max(
92
+ radius_px,
93
+ float(
94
+ np.hypot(
95
+ edge_display[0] - center_display[0],
96
+ edge_display[1] - center_display[1],
97
+ )
98
+ ),
99
+ )
100
+
101
+ return radius_px
102
+
103
+
104
+ def pick_atom_index_from_screen(
105
+ view_3d_manager: Any,
106
+ click_pos: tuple[int, int],
107
+ mol: Optional[Any] = None,
108
+ padding_px: float = 8.0,
109
+ min_radius_px: float = 14.0,
110
+ max_radius_px: float = 96.0,
111
+ ) -> Optional[int]:
112
+ """Return the atom nearest a screen click without invoking VTK cell picking."""
113
+ try:
114
+ plotter = view_3d_manager.plotter
115
+ renderer = plotter.renderer
116
+ positions = view_3d_manager.atom_positions_3d
117
+ except (AttributeError, RuntimeError, TypeError):
118
+ return None
119
+
120
+ if positions is None:
121
+ return None
122
+
123
+ try:
124
+ positions_array = np.asarray(positions, dtype=float)
125
+ except (TypeError, ValueError):
126
+ return None
127
+
128
+ if positions_array.ndim != 2 or positions_array.shape[1] < 3:
129
+ return None
130
+
131
+ if mol is None:
132
+ mol = getattr(view_3d_manager, "current_mol", None)
133
+
134
+ try:
135
+ atom_count = int(mol.GetNumAtoms()) if mol is not None else len(positions_array)
136
+ except (AttributeError, RuntimeError, TypeError, ValueError):
137
+ atom_count = len(positions_array)
138
+
139
+ best_idx: Optional[int] = None
140
+ best_score: Optional[tuple[float, float]] = None
141
+
142
+ for atom_idx in range(min(atom_count, len(positions_array))):
143
+ center = positions_array[atom_idx]
144
+ display = _world_to_display(renderer, center)
145
+ if display is None or not np.all(np.isfinite(display[:2])):
146
+ continue
147
+
148
+ world_radius = _atom_world_radius(view_3d_manager, mol, atom_idx)
149
+ projected_radius = _projected_radius_px(renderer, center, world_radius)
150
+ hit_radius = max(
151
+ float(min_radius_px),
152
+ min(float(max_radius_px), float(projected_radius or 0.0) + padding_px),
153
+ )
154
+ distance = float(np.hypot(display[0] - click_pos[0], display[1] - click_pos[1]))
155
+ if distance > hit_radius:
156
+ continue
157
+
158
+ score = (distance / hit_radius, distance)
159
+ if best_score is None or score < best_score:
160
+ best_idx = atom_idx
161
+ best_score = score
162
+
163
+ return best_idx
@@ -160,14 +160,20 @@ class BondLengthDialog(GeometryBaseDialog):
160
160
 
161
161
  def on_atom_picked(self, atom_idx: int) -> None:
162
162
  """Handle atom picking event in the 3D view."""
163
- if self.atom1_idx is None:
164
- self.atom1_idx = atom_idx
165
- elif self.atom2_idx is None:
166
- self.atom2_idx = atom_idx
167
- else:
168
- # Reset and start over
169
- self.atom1_idx = atom_idx
163
+ if atom_idx == self.atom1_idx:
164
+ self.atom1_idx = self.atom2_idx
170
165
  self.atom2_idx = None
166
+ elif atom_idx == self.atom2_idx:
167
+ self.atom2_idx = None
168
+ else:
169
+ if self.atom1_idx is None:
170
+ self.atom1_idx = atom_idx
171
+ elif self.atom2_idx is None:
172
+ self.atom2_idx = atom_idx
173
+ else:
174
+ # Reset and start over
175
+ self.atom1_idx = atom_idx
176
+ self.atom2_idx = None
171
177
 
172
178
  self.update_display()
173
179
 
@@ -16,6 +16,7 @@ import re
16
16
  import numpy as np
17
17
  import sys
18
18
  import subprocess
19
+ import time
19
20
  from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
20
21
 
21
22
  from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot
@@ -250,6 +251,8 @@ def _iterative_optimize(
250
251
  chunk = min(chunk_size, max_iters - iters_done)
251
252
  res = ff.Minimize(maxIts=chunk)
252
253
  iters_done += chunk
254
+ time.sleep(0.001)
255
+
253
256
  if res == 0:
254
257
  break
255
258
  return True
@@ -286,8 +289,6 @@ def _iterative_optimize_obabel(
286
289
  if check_halted_cb():
287
290
  raise WorkerHaltError("Halted")
288
291
  ff.ConjugateGradients(chunk_size)
289
- import time
290
-
291
292
  time.sleep(0.001)
292
293
 
293
294
  ff.GetCoordinates(ob_mol.OBMol)
@@ -13,7 +13,7 @@ DOI: 10.5281/zenodo.17268532
13
13
  import logging
14
14
  from typing import Any
15
15
 
16
- from PyQt6.QtCore import Qt
16
+ from PyQt6.QtCore import Qt, QThread, pyqtSignal
17
17
  from PyQt6.QtWidgets import (
18
18
  QAbstractItemView,
19
19
  QComboBox,
@@ -33,6 +33,23 @@ from rdkit.Chem import AllChem, rdMolTransforms
33
33
  from .dialog_3d_picking_mixin import Dialog3DPickingMixin
34
34
 
35
35
 
36
+ class ConstrainedOptimizationThread(QThread):
37
+ optimization_finished = pyqtSignal()
38
+ error_occurred = pyqtSignal(str)
39
+
40
+ def __init__(self, ff: Any, max_iters: int = 20000, parent: Any = None) -> None:
41
+ super().__init__(parent)
42
+ self.ff = ff
43
+ self.max_iters = max_iters
44
+
45
+ def run(self) -> None:
46
+ try:
47
+ self.ff.Minimize(maxIts=self.max_iters)
48
+ self.optimization_finished.emit()
49
+ except Exception as e:
50
+ self.error_occurred.emit(str(e))
51
+
52
+
36
53
  class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
37
54
  """Dialog for constrained optimization."""
38
55
 
@@ -427,16 +444,34 @@ class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
427
444
  if positions:
428
445
  plotter = self.main_window.view_3d_manager.plotter
429
446
  if plotter is not None:
447
+ # Save camera position to prevent reset
448
+ try:
449
+ cam = plotter.camera_position
450
+ except (AttributeError, RuntimeError, TypeError) as e:
451
+ logging.debug(f"Could not save camera position: {e}")
452
+ cam = None
453
+
430
454
  label_actor = plotter.add_point_labels(
431
455
  positions,
432
456
  texts,
433
- point_size=20,
457
+ point_size=0,
434
458
  font_size=12,
435
459
  text_color="cyan",
436
460
  always_visible=True,
461
+ show_points=False,
462
+ shape="rect",
463
+ shape_color="gray",
464
+ shape_opacity=0.5,
437
465
  )
438
466
  self.constraint_labels.append(label_actor)
439
467
 
468
+ # Restore camera position
469
+ if cam is not None:
470
+ try:
471
+ plotter.camera_position = cam
472
+ except (AttributeError, RuntimeError, TypeError) as e:
473
+ logging.debug(f"Could not restore camera position: {e}")
474
+
440
475
  def clear_constraint_labels(self) -> None:
441
476
  for label_actor in self.constraint_labels:
442
477
  try:
@@ -541,8 +576,30 @@ class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
541
576
  status_bar = self.main_window.statusBar()
542
577
  if status_bar is not None:
543
578
  status_bar.showMessage(f"Running constrained {ff_name} optimization...")
544
- ff.Minimize(maxIts=20000)
545
579
 
580
+ self.optimize_button.setEnabled(False)
581
+
582
+ self._opt_thread = ConstrainedOptimizationThread(ff, 20000, self)
583
+ self._opt_thread.optimization_finished.connect(
584
+ lambda: self._on_optimization_finished(ff_name, conf)
585
+ )
586
+ self._opt_thread.error_occurred.connect(self._on_optimization_error)
587
+ self._opt_thread.start()
588
+
589
+ except Exception as e:
590
+ QMessageBox.critical(self, "Error", f"Failed to start optimization: {e}")
591
+ self.optimize_button.setEnabled(True)
592
+
593
+ def _on_optimization_error(self, err_msg: str) -> None:
594
+ self.optimize_button.setEnabled(True)
595
+ status_bar = self.main_window.statusBar()
596
+ if status_bar is not None:
597
+ status_bar.showMessage(f"Optimization failed: {err_msg}")
598
+ QMessageBox.critical(self, "Error", f"Optimization error: {err_msg}")
599
+
600
+ def _on_optimization_finished(self, ff_name: str, conf: Any) -> None:
601
+ self.optimize_button.setEnabled(True)
602
+ try:
546
603
  # Apply optimized coordinates to the main window's numpy array
547
604
  cache = self.main_window.view_3d_manager.atom_positions_3d
548
605
  for i in range(self.mol.GetNumAtoms()):
@@ -558,6 +615,7 @@ class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
558
615
  self.main_window.view_3d_manager.draw_molecule_3d(self.mol)
559
616
  self.main_window.view_3d_manager.update_chiral_labels()
560
617
  self.main_window.edit_actions_manager.push_undo_state()
618
+ status_bar = self.main_window.statusBar()
561
619
  if status_bar is not None:
562
620
  status_bar.showMessage("Constrained optimization finished.")
563
621
 
@@ -680,13 +738,24 @@ class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
680
738
  if positions:
681
739
  plotter = self.main_window.view_3d_manager.plotter
682
740
  if plotter is not None:
741
+ # Save camera position to prevent reset
742
+ try:
743
+ cam = plotter.camera_position
744
+ except (AttributeError, RuntimeError, TypeError) as e:
745
+ logging.debug(f"Could not save camera position: {e}")
746
+ cam = None
747
+
683
748
  label_actor = plotter.add_point_labels(
684
749
  positions,
685
750
  texts,
686
- point_size=20,
751
+ point_size=0,
687
752
  font_size=12,
688
753
  text_color="yellow",
689
754
  always_visible=True,
755
+ show_points=False,
756
+ shape="rect",
757
+ shape_color="gray",
758
+ shape_opacity=0.5,
690
759
  )
691
760
  # Consider case where add_point_labels returns a list
692
761
  if isinstance(label_actor, list):
@@ -694,6 +763,13 @@ class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
694
763
  else:
695
764
  self.selection_labels.append(label_actor)
696
765
 
766
+ # Restore camera position
767
+ if cam is not None:
768
+ try:
769
+ plotter.camera_position = cam
770
+ except (AttributeError, RuntimeError, TypeError) as e:
771
+ logging.debug(f"Could not restore camera position: {e}")
772
+
697
773
  def on_cell_changed(self, row: int, column: int) -> None:
698
774
  """Update internal data when a table cell is edited."""
699
775