MoleditPy 3.3.0__tar.gz → 3.4.0__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.3.0 → moleditpy-3.4.0}/PKG-INFO +20 -2
  2. {moleditpy-3.3.0 → moleditpy-3.4.0}/README.md +19 -1
  3. {moleditpy-3.3.0 → moleditpy-3.4.0}/pyproject.toml +1 -1
  4. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/MoleditPy.egg-info/PKG-INFO +20 -2
  5. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/__init__.py +2 -0
  6. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/main.py +2 -1
  7. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/angle_dialog.py +47 -22
  8. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/app_state.py +7 -9
  9. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/atom_item.py +77 -27
  10. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/atom_picking.py +163 -163
  11. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/base_picking_dialog.py +19 -5
  12. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/bond_item.py +93 -16
  13. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/bond_length_dialog.py +25 -10
  14. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/calculation_worker.py +1 -1
  15. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/color_settings_dialog.py +12 -12
  16. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/compute_logic.py +9 -9
  17. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/constrained_optimization_dialog.py +4 -4
  18. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/custom_interactor_style.py +6 -6
  19. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/dialog_logic.py +2 -2
  20. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/dihedral_dialog.py +31 -17
  21. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/edit_3d_logic.py +4 -4
  22. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/edit_actions_logic.py +12 -12
  23. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/export_logic.py +9 -2
  24. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/geometry_base_dialog.py +2 -2
  25. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/io_logic.py +6 -6
  26. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/main_window_init.py +7 -7
  27. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/molecular_scene_handler.py +10 -10
  28. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/molecule_scene.py +8 -8
  29. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/move_group_dialog.py +3 -5
  30. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/planarize_dialog.py +1 -0
  31. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/settings_dialog.py +7 -7
  32. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/ui_manager.py +10 -10
  33. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/user_template_dialog.py +3 -3
  34. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/view_3d_logic.py +4 -4
  35. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/utils/constants.py +31 -1
  36. {moleditpy-3.3.0 → moleditpy-3.4.0}/LICENSE +0 -0
  37. {moleditpy-3.3.0 → moleditpy-3.4.0}/setup.cfg +0 -0
  38. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/MoleditPy.egg-info/SOURCES.txt +0 -0
  39. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
  40. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/MoleditPy.egg-info/entry_points.txt +0 -0
  41. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/MoleditPy.egg-info/requires.txt +0 -0
  42. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/MoleditPy.egg-info/top_level.txt +0 -0
  43. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/__main__.py +0 -0
  44. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/assets/file_icon.ico +0 -0
  45. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/assets/icon.icns +0 -0
  46. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/assets/icon.ico +0 -0
  47. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/assets/icon.png +0 -0
  48. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/core/__init__.py +0 -0
  49. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/core/mol_geometry.py +0 -0
  50. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/core/molecular_data.py +0 -0
  51. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/plugins/__init__.py +0 -0
  52. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/plugins/plugin_interface.py +0 -0
  53. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/plugins/plugin_manager.py +0 -0
  54. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/plugins/plugin_manager_window.py +0 -0
  55. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/__init__.py +0 -0
  56. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/about_dialog.py +0 -0
  57. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/align_plane_dialog.py +0 -0
  58. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/alignment_dialog.py +0 -0
  59. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/analysis_window.py +0 -0
  60. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/custom_qt_interactor.py +0 -0
  61. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/dialog_3d_picking_mixin.py +0 -0
  62. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/main_window.py +0 -0
  63. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/mirror_dialog.py +0 -0
  64. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/periodic_table_dialog.py +0 -0
  65. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/settings_tabs/__init__.py +0 -0
  66. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/settings_tabs/settings_2d_tab.py +0 -0
  67. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/settings_tabs/settings_3d_tabs.py +0 -0
  68. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/settings_tabs/settings_other_tab.py +0 -0
  69. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/settings_tabs/settings_tab_base.py +0 -0
  70. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/string_importers.py +0 -0
  71. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/template_preview_item.py +0 -0
  72. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/template_preview_view.py +0 -0
  73. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/translation_dialog.py +0 -0
  74. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/ui/zoomable_view.py +0 -0
  75. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/utils/__init__.py +0 -0
  76. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/utils/default_settings.py +0 -0
  77. {moleditpy-3.3.0 → moleditpy-3.4.0}/src/moleditpy/utils/sip_isdeleted_safe.py +0 -0
  78. {moleditpy-3.3.0 → moleditpy-3.4.0}/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.3.0
3
+ Version: 3.4.0
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
@@ -713,8 +713,10 @@ Dynamic: license-file
713
713
  ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage->80%25-green)
714
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
+ ```
@@ -9,8 +9,10 @@
9
9
  ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage->80%25-green)
10
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.3.0"
8
+ version = "3.4.0"
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.3.0
3
+ Version: 3.4.0
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
@@ -713,8 +713,10 @@ Dynamic: license-file
713
713
  ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage->80%25-green)
714
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
+ ```
@@ -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__ # noqa: F401
@@ -68,7 +68,8 @@ def main() -> None:
68
68
  ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
69
69
 
70
70
  parser = argparse.ArgumentParser(
71
- prog="moleditpy", description="MoleditPy molecular editor"
71
+ prog="moleditpy",
72
+ description="MoleditPy — A Python-based molecular editing software",
72
73
  )
73
74
  parser.add_argument("file", nargs="?", default=None, help="File to open on startup")
74
75
  _variant = " (Linux)" if "moleditpy_linux" in (__file__ or "") else ""
@@ -189,8 +189,10 @@ class AngleDialog(GeometryBaseDialog):
189
189
  self.atom2_idx = atom_idx
190
190
  elif self.atom3_idx is None:
191
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()
192
+ # Capture the ABSOLUTE baseline for this selection session.
193
+ # This prevents 'direction change' drift during multiple slider drags.
194
+ self._baseline_positions = self.mol.GetConformer().GetPositions().copy()
195
+ self._snapshot_positions = self._baseline_positions.copy()
194
196
  else:
195
197
  # Reset and start over
196
198
  self.atom1_idx = atom_idx
@@ -205,6 +207,7 @@ class AngleDialog(GeometryBaseDialog):
205
207
  self.atom1_idx = None
206
208
  self.atom2_idx = None # vertex atom
207
209
  self.atom3_idx = None
210
+ self._baseline_positions = None
208
211
  self._snapshot_positions = None
209
212
  self.clear_selection_labels()
210
213
  self.update_display()
@@ -212,7 +215,7 @@ class AngleDialog(GeometryBaseDialog):
212
215
  def show_atom_labels(self) -> None:
213
216
  """Display labels on the selected atoms."""
214
217
  selected_atoms = [self.atom1_idx, self.atom2_idx, self.atom3_idx]
215
- labels = ["1st", "2nd (vertex)", "3rd"]
218
+ labels = ["1", "2", "3"]
216
219
  pairs = [
217
220
  (idx, labels[i]) for i, idx in enumerate(selected_atoms) if idx is not None
218
221
  ]
@@ -271,7 +274,7 @@ class AngleDialog(GeometryBaseDialog):
271
274
  self._snapshot_positions = None
272
275
  # Add labels
273
276
  self.add_selection_label(self.atom1_idx, "1")
274
- self.add_selection_label(self.atom2_idx, "2(vertex)")
277
+ self.add_selection_label(self.atom2_idx, "2")
275
278
  # Clear angle input while selection is incomplete
276
279
  try:
277
280
  self.angle_input.blockSignals(True)
@@ -295,23 +298,35 @@ class AngleDialog(GeometryBaseDialog):
295
298
  current_angle = self.calculate_angle()
296
299
  self.angle_label.setText(f"Current angle: {current_angle:.2f}°")
297
300
  self.apply_button.setEnabled(True)
298
- # Update angle input box with current angle
301
+ # Update angle input box and slider
299
302
  try:
300
- self.angle_input.blockSignals(True)
301
- self.angle_input.setText(f"{current_angle:.2f}")
302
- self.angle_input.blockSignals(False)
303
- self.angle_slider.blockSignals(True)
304
- slider_val = int(round(current_angle))
305
- slider_val = max(-180, min(180, slider_val))
306
- self.angle_slider.setValue(slider_val)
307
- self.angle_slider.setEnabled(True)
308
- self.angle_slider.blockSignals(False)
303
+ # Update input box and slider only if not dragging
304
+ if not self._slider_dragging:
305
+ self.angle_input.blockSignals(True)
306
+ self.angle_input.setText(f"{current_angle:.2f}")
307
+ self.angle_input.blockSignals(False)
308
+
309
+ # UPDATE SLIDER: Logic to prevent 'jumping' to positive value
310
+ slider_val = int(round(current_angle))
311
+ current_slider_val = self.angle_slider.value()
312
+
313
+ # If the user is on the negative side of the slider, keep the sign
314
+ if current_slider_val < 0:
315
+ slider_val = -slider_val
316
+
317
+ if current_slider_val != slider_val:
318
+ self.angle_slider.blockSignals(True)
319
+ self.angle_slider.setValue(slider_val)
320
+ self.angle_slider.blockSignals(False)
321
+ self.angle_slider.setEnabled(True)
322
+ else:
323
+ self.angle_slider.setEnabled(True)
309
324
  except (AttributeError, RuntimeError, TypeError):
310
325
  pass
311
326
 
312
327
  # Add labels
313
328
  self.add_selection_label(self.atom1_idx, "1")
314
- self.add_selection_label(self.atom2_idx, "2(vertex)")
329
+ self.add_selection_label(self.atom2_idx, "2")
315
330
  self.add_selection_label(self.atom3_idx, "3")
316
331
 
317
332
  def calculate_angle(self) -> float:
@@ -366,10 +381,14 @@ class AngleDialog(GeometryBaseDialog):
366
381
  """Adjust the bond angle."""
367
382
  conf = self.mol.GetConformer()
368
383
 
369
- # Use snapshot if available (slider dragging) to keep the rotation axis stable
370
- snapshot = self._snapshot_positions
371
- if snapshot is not None:
372
- positions = snapshot.copy()
384
+ # Use baseline positions (fixed for dialog session) to keep the rotation axis stable.
385
+ if (
386
+ hasattr(self, "_baseline_positions")
387
+ and self._baseline_positions is not None
388
+ ):
389
+ positions = self._baseline_positions.copy()
390
+ elif self._snapshot_positions is not None:
391
+ positions = self._snapshot_positions.copy()
373
392
  else:
374
393
  positions = conf.GetPositions()
375
394
 
@@ -379,10 +398,16 @@ class AngleDialog(GeometryBaseDialog):
379
398
  idx_b: int = self.atom2_idx # vertex
380
399
  idx_c: int = self.atom3_idx
381
400
 
401
+ # Calculate baseline angle from the POSITIONS we are working on (important for snapshot stability)
402
+ p_a, p_b, p_c = positions[idx_a], positions[idx_b], positions[idx_c]
403
+ from moleditpy.core.mol_geometry import calc_angle_deg
404
+
405
+ baseline_angle = calc_angle_deg(p_a, p_b, p_c)
406
+
382
407
  if self.both_groups_radio.isChecked():
383
408
  # Both arms rotate equally (half angle each)
384
- current_angle = self.calculate_angle()
385
- half_delta_deg = (new_angle_deg - current_angle) / 2.0
409
+ # Use baseline_angle from snapshot to avoid drift/jumps
410
+ half_delta_deg = (new_angle_deg - baseline_angle) / 2.0
386
411
 
387
412
  group1 = get_connected_group(self.mol, idx_a, exclude=idx_b)
388
413
  group3 = get_connected_group(self.mol, idx_c, exclude=idx_b)
@@ -393,7 +418,7 @@ class AngleDialog(GeometryBaseDialog):
393
418
  idx_c,
394
419
  idx_b,
395
420
  idx_a,
396
- current_angle + half_delta_deg,
421
+ baseline_angle + half_delta_deg,
397
422
  group1,
398
423
  )
399
424
  # Arm 3 rotates to the FINAL angle (relative to the now-moved Arm 1)
@@ -17,7 +17,8 @@ import binascii
17
17
  import copy
18
18
  import logging
19
19
  import os
20
- from typing import Any, Dict, Optional, Tuple, TYPE_CHECKING
20
+ from typing import Any, Dict, Optional, Tuple
21
+
21
22
 
22
23
  import numpy as np
23
24
 
@@ -45,9 +46,6 @@ try:
45
46
  except (AttributeError, RuntimeError, TypeError, ImportError):
46
47
  from moleditpy.core.molecular_data import MolecularData
47
48
 
48
- if TYPE_CHECKING:
49
- from .main_window import MainWindow
50
-
51
49
 
52
50
  # --- Class Definition ---
53
51
  class StateManager:
@@ -634,7 +632,7 @@ class StateManager:
634
632
  method = json_data.get("last_successful_optimization_method", None)
635
633
  if hasattr(self.host, "compute_manager"):
636
634
  self.host.compute_manager.last_successful_optimization_method = method
637
- else: # [REPORT ERROR MISSING ATTRIBUTE]
635
+ else:
638
636
  logging.error(
639
637
  "REPORT ERROR: Missing attribute 'compute_manager' on self.host"
640
638
  )
@@ -808,7 +806,7 @@ class StateManager:
808
806
  "update_atom_id_menu_text",
809
807
  ):
810
808
  self.host.view_3d_manager.update_atom_id_menu_text()
811
- else: # [REPORT ERROR MISSING ATTRIBUTE]
809
+ else:
812
810
  logging.error(
813
811
  "REPORT ERROR: Missing attribute 'update_atom_id_menu_text' on object"
814
812
  )
@@ -817,13 +815,13 @@ class StateManager:
817
815
  "update_atom_id_menu_state",
818
816
  ):
819
817
  self.host.view_3d_manager.update_atom_id_menu_state()
820
- else: # [REPORT ERROR MISSING ATTRIBUTE]
818
+ else:
821
819
  logging.error(
822
820
  "REPORT ERROR: Missing attribute 'update_atom_id_menu_state' on object"
823
821
  )
824
822
  except (RuntimeError, TypeError, AttributeError):
825
823
  pass
826
- else: # [REPORT ERROR MISSING ATTRIBUTE]
824
+ else:
827
825
  logging.error(
828
826
  "REPORT ERROR: Missing attribute 'create_atom_id_mapping' on object"
829
827
  )
@@ -833,7 +831,7 @@ class StateManager:
833
831
  self.host.view_3d_manager.draw_molecule_3d(
834
832
  self.host.view_3d_manager.current_mol
835
833
  )
836
- else: # [REPORT ERROR MISSING ATTRIBUTE]
834
+ else:
837
835
  logging.error(
838
836
  "REPORT ERROR: Missing attribute 'draw_molecule_3d' on object"
839
837
  )
@@ -11,7 +11,7 @@ DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
13
  from __future__ import annotations
14
- import logging # [REPORT ERROR MISSING ATTRIBUTE]
14
+ import logging
15
15
  from typing import Any, List, Optional
16
16
 
17
17
  from PyQt6.QtCore import QPointF, QRectF, Qt
@@ -92,7 +92,7 @@ class AtomItem(QGraphicsItem):
92
92
  if hasattr(scene, "get_setting"):
93
93
  font_size = scene.get_setting("atom_font_size_2d", 20)
94
94
  font_family = scene.get_setting("atom_font_family_2d", FONT_FAMILY)
95
- else: # [REPORT ERROR MISSING ATTRIBUTE]
95
+ else:
96
96
  logging.error(
97
97
  f"REPORT ERROR: Missing attribute 'get_setting' on scene of type {type(scene)}"
98
98
  )
@@ -119,7 +119,7 @@ class AtomItem(QGraphicsItem):
119
119
  if hasattr(scene, "get_setting"):
120
120
  font_size = scene.get_setting("atom_font_size_2d", 20)
121
121
  font_family = scene.get_setting("atom_font_family_2d", FONT_FAMILY)
122
- else: # [REPORT ERROR MISSING ATTRIBUTE]
122
+ else:
123
123
  logging.error(
124
124
  f"REPORT ERROR: Missing attribute 'get_setting' on scene of type {type(scene)}"
125
125
  )
@@ -225,6 +225,77 @@ class AtomItem(QGraphicsItem):
225
225
  # 3. Add final margins for selection highlights, etc.
226
226
  return full_visual_rect.adjusted(-3, -3, 3, 3)
227
227
 
228
+ def get_bg_ellipse_path(self) -> QPainterPath:
229
+ path = QPainterPath()
230
+ if not self.is_visible:
231
+ return path
232
+
233
+ font_size = 20
234
+ font_family = FONT_FAMILY
235
+ scene = self.scene()
236
+ if scene is not None:
237
+ if hasattr(scene, "get_setting"):
238
+ font_size = scene.get_setting("atom_font_size_2d", 20)
239
+ font_family = scene.get_setting("atom_font_family_2d", FONT_FAMILY)
240
+
241
+ font = QFont(font_family, font_size, FONT_WEIGHT_BOLD)
242
+ fm = QFontMetricsF(font)
243
+
244
+ hydrogen_part = ""
245
+ if self.implicit_h_count > 0:
246
+ is_skeletal_carbon = (
247
+ self.symbol == "C"
248
+ and self.charge == 0
249
+ and self.radical == 0
250
+ and len(self.bonds) > 0
251
+ )
252
+ if not is_skeletal_carbon:
253
+ hydrogen_part = "H"
254
+ if self.implicit_h_count > 1:
255
+ subscript_map = str.maketrans("0123456789", "₀₁₂₃₄₅₆₇₈₉")
256
+ hydrogen_part += str(self.implicit_h_count).translate(subscript_map)
257
+
258
+ flip_text = False
259
+ if hydrogen_part and self.bonds:
260
+ my_pos_x = self.pos().x()
261
+ total_dx = 0.0
262
+ for b in self.bonds:
263
+ partner = b.atom2 if b.atom1 is self else b.atom1
264
+ try:
265
+ if partner is None or sip_isdeleted_safe(partner):
266
+ continue
267
+ partner_pos = partner.pos()
268
+ if partner_pos is None:
269
+ continue
270
+ total_dx += partner_pos.x() - my_pos_x
271
+ except (AttributeError, RuntimeError, TypeError, ValueError):
272
+ # Suppress non-critical UI sync errors during atom position retrieval
273
+ continue
274
+ if total_dx > 0:
275
+ flip_text = True
276
+
277
+ if flip_text:
278
+ display_text = hydrogen_part + self.symbol
279
+ else:
280
+ display_text = self.symbol + hydrogen_part
281
+
282
+ text_rect = fm.boundingRect(display_text)
283
+ text_rect.adjust(-2, -2, 2, 2)
284
+ if hydrogen_part:
285
+ symbol_rect = fm.boundingRect(self.symbol)
286
+ if flip_text:
287
+ offset_x = symbol_rect.width() // 2
288
+ text_rect.moveTo(offset_x - text_rect.width(), -text_rect.height() / 2)
289
+ else:
290
+ offset_x = -symbol_rect.width() // 2
291
+ text_rect.moveTo(offset_x, -text_rect.height() / 2)
292
+ else:
293
+ text_rect.moveCenter(QPointF(0, 0))
294
+
295
+ bg_rect = text_rect.adjusted(-5, -8, 5, 8)
296
+ path.addEllipse(bg_rect)
297
+ return path
298
+
228
299
  def shape(self) -> QPainterPath:
229
300
  """Define the shape of the atom item for collision detection."""
230
301
  scene = self.scene()
@@ -360,29 +431,6 @@ class AtomItem(QGraphicsItem):
360
431
  offset_x = -symbol_rect.width() // 2
361
432
  text_rect.moveTo(offset_x, -text_rect.height() // 2)
362
433
 
363
- # 2. Handle background (fill with white or clear if transparent)
364
- if self.scene():
365
- bg_brush = self.scene().backgroundBrush()
366
- bg_rect = text_rect.adjusted(-5, -8, 5, 8)
367
-
368
- if bg_brush.style() == Qt.BrushStyle.NoBrush:
369
- # Use CompositionMode_Clear to erase overlapping bond lines
370
- painter.save()
371
- painter.setCompositionMode(
372
- QPainter.CompositionMode.CompositionMode_Clear
373
- )
374
- painter.setBrush(
375
- QColor(0, 0, 0, 255)
376
- ) # Color doesn't matter (alpha is key)
377
- painter.setPen(Qt.PenStyle.NoPen)
378
- painter.drawEllipse(bg_rect)
379
- painter.restore()
380
- else:
381
- # Fill with background color if it exists
382
- painter.setBrush(bg_brush)
383
- painter.setPen(Qt.PenStyle.NoPen)
384
- painter.drawEllipse(bg_rect)
385
-
386
434
  # 3. Draw the atom symbol itself
387
435
  # Color is already determined above
388
436
  painter.setPen(QPen(color))
@@ -454,7 +502,9 @@ class AtomItem(QGraphicsItem):
454
502
  for bond in self.bonds:
455
503
  if bond.scene():
456
504
  bond.update_position()
457
-
505
+ elif change == QGraphicsItem.GraphicsItemChange.ItemSceneHasChanged:
506
+ if self.scene() is not None:
507
+ self.update_style()
458
508
  return res
459
509
 
460
510
  def hoverEnterEvent(self, event: Any) -> None: