moleditpy 3.0.0a2__tar.gz → 3.0.0a3__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 (86) hide show
  1. {moleditpy-3.0.0a2 → moleditpy-3.0.0a3}/PKG-INFO +3 -2
  2. {moleditpy-3.0.0a2 → moleditpy-3.0.0a3}/README.md +2 -1
  3. {moleditpy-3.0.0a2 → moleditpy-3.0.0a3}/pyproject.toml +1 -1
  4. {moleditpy-3.0.0a2 → moleditpy-3.0.0a3}/src/MoleditPy.egg-info/PKG-INFO +3 -2
  5. moleditpy-3.0.0a3/src/MoleditPy.egg-info/SOURCES.txt +86 -0
  6. moleditpy-3.0.0a3/src/moleditpy/__init__.py +20 -0
  7. moleditpy-3.0.0a3/src/moleditpy/core/__init__.py +0 -0
  8. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/core}/mol_geometry.py +147 -0
  9. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/core}/molecular_data.py +119 -55
  10. {moleditpy-3.0.0a2 → moleditpy-3.0.0a3}/src/moleditpy/main.py +23 -4
  11. moleditpy-3.0.0a3/src/moleditpy/modules/__init__.py +122 -0
  12. {moleditpy-3.0.0a2/src/moleditpy → moleditpy-3.0.0a3/src/moleditpy/plugins}/__init__.py +11 -15
  13. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/plugins}/plugin_manager.py +573 -578
  14. moleditpy-3.0.0a3/src/moleditpy/ui/__init__.py +25 -0
  15. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/ui}/about_dialog.py +4 -3
  16. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/ui}/align_plane_dialog.py +306 -306
  17. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/ui}/alignment_dialog.py +1 -1
  18. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/ui}/analysis_window.py +226 -226
  19. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/ui}/angle_dialog.py +5 -4
  20. moleditpy-3.0.0a2/src/moleditpy/modules/main_window_app_state.py → moleditpy-3.0.0a3/src/moleditpy/ui/app_state.py +22 -27
  21. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/ui}/atom_item.py +4 -2
  22. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/ui}/bond_item.py +535 -528
  23. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/ui}/bond_length_dialog.py +3 -2
  24. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/ui}/calculation_worker.py +970 -970
  25. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/ui}/color_settings_dialog.py +404 -404
  26. moleditpy-3.0.0a2/src/moleditpy/modules/main_window_compute.py → moleditpy-3.0.0a3/src/moleditpy/ui/compute_engine.py +4 -11
  27. moleditpy-3.0.0a3/src/moleditpy/ui/compute_logic.py +526 -0
  28. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/ui}/constrained_optimization_dialog.py +2 -1
  29. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/ui}/custom_interactor_style.py +34 -24
  30. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/ui}/custom_qt_interactor.py +97 -97
  31. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/ui}/dialog_3d_picking_mixin.py +210 -210
  32. moleditpy-3.0.0a3/src/moleditpy/ui/dialog_logic.py +438 -0
  33. moleditpy-3.0.0a2/src/moleditpy/modules/main_window_dialog_manager.py → moleditpy-3.0.0a3/src/moleditpy/ui/dialog_manager.py +17 -21
  34. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/ui}/dihedral_dialog.py +5 -4
  35. moleditpy-3.0.0a2/src/moleditpy/modules/main_window_edit_3d.py → moleditpy-3.0.0a3/src/moleditpy/ui/edit_3d.py +2 -10
  36. moleditpy-3.0.0a3/src/moleditpy/ui/edit_3d_logic.py +477 -0
  37. moleditpy-3.0.0a2/src/moleditpy/modules/main_window_edit_actions.py → moleditpy-3.0.0a3/src/moleditpy/ui/edit_actions.py +5 -11
  38. moleditpy-3.0.0a3/src/moleditpy/ui/edit_actions_logic.py +1330 -0
  39. moleditpy-3.0.0a2/src/moleditpy/modules/main_window_export.py → moleditpy-3.0.0a3/src/moleditpy/ui/export_logic.py +12 -10
  40. moleditpy-3.0.0a3/src/moleditpy/ui/main_window.py +218 -0
  41. moleditpy-3.0.0a2/src/moleditpy/modules/main_window_main_init.py → moleditpy-3.0.0a3/src/moleditpy/ui/main_window_init.py +58 -72
  42. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/ui}/mirror_dialog.py +140 -140
  43. moleditpy-3.0.0a2/src/moleditpy/modules/main_window_molecular_parsers.py → moleditpy-3.0.0a3/src/moleditpy/ui/molecular_parsers.py +587 -594
  44. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/ui}/molecular_scene_handler.py +10 -6
  45. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/ui}/molecule_scene.py +12 -10
  46. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/ui}/move_group_dialog.py +4 -6
  47. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/ui}/periodic_table_dialog.py +196 -196
  48. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/ui}/planarize_dialog.py +239 -242
  49. moleditpy-3.0.0a2/src/moleditpy/modules/main_window_project_io.py → moleditpy-3.0.0a3/src/moleditpy/ui/project_io.py +1 -8
  50. moleditpy-3.0.0a3/src/moleditpy/ui/settings_dialog.py +229 -0
  51. moleditpy-3.0.0a3/src/moleditpy/ui/settings_tabs/__init__.py +11 -0
  52. moleditpy-3.0.0a3/src/moleditpy/ui/settings_tabs/settings_2d_tab.py +249 -0
  53. moleditpy-3.0.0a3/src/moleditpy/ui/settings_tabs/settings_3d_tabs.py +321 -0
  54. moleditpy-3.0.0a3/src/moleditpy/ui/settings_tabs/settings_other_tab.py +99 -0
  55. moleditpy-3.0.0a3/src/moleditpy/ui/settings_tabs/settings_tab_base.py +33 -0
  56. moleditpy-3.0.0a2/src/moleditpy/modules/__init__.py → moleditpy-3.0.0a3/src/moleditpy/ui/sip_isdeleted_safe.py +35 -41
  57. moleditpy-3.0.0a2/src/moleditpy/modules/main_window_string_importers.py → moleditpy-3.0.0a3/src/moleditpy/ui/string_importers.py +242 -248
  58. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/ui}/template_preview_item.py +2 -2
  59. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/ui}/template_preview_view.py +85 -85
  60. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/ui}/translation_dialog.py +347 -347
  61. moleditpy-3.0.0a2/src/moleditpy/modules/main_window_ui_manager.py → moleditpy-3.0.0a3/src/moleditpy/ui/ui_manager.py +511 -518
  62. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/ui}/user_template_dialog.py +712 -712
  63. moleditpy-3.0.0a2/src/moleditpy/modules/main_window_view_3d.py → moleditpy-3.0.0a3/src/moleditpy/ui/view_3d_logic.py +45 -45
  64. moleditpy-3.0.0a2/src/moleditpy/modules/main_window_view_loaders.py → moleditpy-3.0.0a3/src/moleditpy/ui/view_loaders.py +274 -282
  65. moleditpy-3.0.0a3/src/moleditpy/utils/__init__.py +11 -0
  66. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/utils}/constants.py +1 -1
  67. moleditpy-3.0.0a3/src/moleditpy/utils/sip_isdeleted_safe.py +41 -0
  68. moleditpy-3.0.0a2/src/MoleditPy.egg-info/SOURCES.txt +0 -72
  69. moleditpy-3.0.0a2/src/moleditpy/modules/main_window.py +0 -107
  70. moleditpy-3.0.0a2/src/moleditpy/modules/settings_dialog.py +0 -1705
  71. moleditpy-3.0.0a2/src/moleditpy/modules/sip_isdeleted_safe.py +0 -17
  72. {moleditpy-3.0.0a2 → moleditpy-3.0.0a3}/LICENSE +0 -0
  73. {moleditpy-3.0.0a2 → moleditpy-3.0.0a3}/setup.cfg +0 -0
  74. {moleditpy-3.0.0a2 → moleditpy-3.0.0a3}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
  75. {moleditpy-3.0.0a2 → moleditpy-3.0.0a3}/src/MoleditPy.egg-info/entry_points.txt +0 -0
  76. {moleditpy-3.0.0a2 → moleditpy-3.0.0a3}/src/MoleditPy.egg-info/requires.txt +0 -0
  77. {moleditpy-3.0.0a2 → moleditpy-3.0.0a3}/src/MoleditPy.egg-info/top_level.txt +0 -0
  78. {moleditpy-3.0.0a2 → moleditpy-3.0.0a3}/src/moleditpy/__main__.py +0 -0
  79. {moleditpy-3.0.0a2 → moleditpy-3.0.0a3}/src/moleditpy/assets/file_icon.ico +0 -0
  80. {moleditpy-3.0.0a2 → moleditpy-3.0.0a3}/src/moleditpy/assets/icon.icns +0 -0
  81. {moleditpy-3.0.0a2 → moleditpy-3.0.0a3}/src/moleditpy/assets/icon.ico +0 -0
  82. {moleditpy-3.0.0a2 → moleditpy-3.0.0a3}/src/moleditpy/assets/icon.png +0 -0
  83. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/plugins}/plugin_interface.py +0 -0
  84. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/plugins}/plugin_manager_window.py +0 -0
  85. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/src/moleditpy/ui}/zoomable_view.py +0 -0
  86. {moleditpy-3.0.0a2/src/moleditpy/modules → moleditpy-3.0.0a3/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.0.0a2
3
+ Version: 3.0.0a3
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
@@ -711,7 +711,7 @@ Dynamic: license-file
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
713
  ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage-80%25-green)
714
- ![Overall Coverage](https://img.shields.io/badge/coverage-63.83%25-green)
714
+ ![Overall Coverage](https://img.shields.io/badge/coverage-64%25-green)
715
715
  ![GUI Status](https://img.shields.io/badge/GUI-Manually_Verified-blue)
716
716
  ![Pylint Score](https://img.shields.io/badge/pylint-8.93%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)
@@ -834,6 +834,7 @@ moleditpy
834
834
  * **GUI and 2D Drawing (PyQt6):** The editor is built on a `QGraphicsScene`, where custom `AtomItem` and `BondItem` objects are interactively manipulated. The Undo/Redo feature is implemented by serializing the application state.
835
835
  * **Chemical Calculations (RDKit / Open Babel):** RDKit is used to generate molecule objects from 2D data, perform 3D coordinate generation, and calculate properties. Open Babel serves as a fallback for 3D conversion. All heavy computations are run on a separate `QThread` to keep the GUI responsive.
836
836
  * **3D Visualization (PyVista / pyvistaqt):** 3D rendering is achieved by generating PyVista meshes (spheres and cylinders) from RDKit conformer coordinates. A custom `vtkInteractorStyle` enables direct drag-and-drop editing of atoms in the 3D view.
837
+ * **Modular Architecture:** The codebase is organized into dedicated packages for `core` logic, `ui` components, and `utils`. The main application logic is decomposed into reusable mixins, ensuring long-term maintainability and easier verification.
837
838
 
838
839
  ## License
839
840
 
@@ -7,7 +7,7 @@
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
9
  ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage-80%25-green)
10
- ![Overall Coverage](https://img.shields.io/badge/coverage-63.83%25-green)
10
+ ![Overall Coverage](https://img.shields.io/badge/coverage-64%25-green)
11
11
  ![GUI Status](https://img.shields.io/badge/GUI-Manually_Verified-blue)
12
12
  ![Pylint Score](https://img.shields.io/badge/pylint-8.93%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)
@@ -130,6 +130,7 @@ moleditpy
130
130
  * **GUI and 2D Drawing (PyQt6):** The editor is built on a `QGraphicsScene`, where custom `AtomItem` and `BondItem` objects are interactively manipulated. The Undo/Redo feature is implemented by serializing the application state.
131
131
  * **Chemical Calculations (RDKit / Open Babel):** RDKit is used to generate molecule objects from 2D data, perform 3D coordinate generation, and calculate properties. Open Babel serves as a fallback for 3D conversion. All heavy computations are run on a separate `QThread` to keep the GUI responsive.
132
132
  * **3D Visualization (PyVista / pyvistaqt):** 3D rendering is achieved by generating PyVista meshes (spheres and cylinders) from RDKit conformer coordinates. A custom `vtkInteractorStyle` enables direct drag-and-drop editing of atoms in the 3D view.
133
+ * **Modular Architecture:** The codebase is organized into dedicated packages for `core` logic, `ui` components, and `utils`. The main application logic is decomposed into reusable mixins, ensuring long-term maintainability and easier verification.
133
134
 
134
135
  ## License
135
136
 
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "MoleditPy"
7
7
 
8
- version = "3.0.0a2"
8
+ version = "3.0.0a3"
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.0.0a2
3
+ Version: 3.0.0a3
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
@@ -711,7 +711,7 @@ Dynamic: license-file
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
713
  ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage-80%25-green)
714
- ![Overall Coverage](https://img.shields.io/badge/coverage-63.83%25-green)
714
+ ![Overall Coverage](https://img.shields.io/badge/coverage-64%25-green)
715
715
  ![GUI Status](https://img.shields.io/badge/GUI-Manually_Verified-blue)
716
716
  ![Pylint Score](https://img.shields.io/badge/pylint-8.93%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)
@@ -834,6 +834,7 @@ moleditpy
834
834
  * **GUI and 2D Drawing (PyQt6):** The editor is built on a `QGraphicsScene`, where custom `AtomItem` and `BondItem` objects are interactively manipulated. The Undo/Redo feature is implemented by serializing the application state.
835
835
  * **Chemical Calculations (RDKit / Open Babel):** RDKit is used to generate molecule objects from 2D data, perform 3D coordinate generation, and calculate properties. Open Babel serves as a fallback for 3D conversion. All heavy computations are run on a separate `QThread` to keep the GUI responsive.
836
836
  * **3D Visualization (PyVista / pyvistaqt):** 3D rendering is achieved by generating PyVista meshes (spheres and cylinders) from RDKit conformer coordinates. A custom `vtkInteractorStyle` enables direct drag-and-drop editing of atoms in the 3D view.
837
+ * **Modular Architecture:** The codebase is organized into dedicated packages for `core` logic, `ui` components, and `utils`. The main application logic is decomposed into reusable mixins, ensuring long-term maintainability and easier verification.
837
838
 
838
839
  ## License
839
840
 
@@ -0,0 +1,86 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/MoleditPy.egg-info/PKG-INFO
5
+ src/MoleditPy.egg-info/SOURCES.txt
6
+ src/MoleditPy.egg-info/dependency_links.txt
7
+ src/MoleditPy.egg-info/entry_points.txt
8
+ src/MoleditPy.egg-info/requires.txt
9
+ src/MoleditPy.egg-info/top_level.txt
10
+ src/moleditpy/__init__.py
11
+ src/moleditpy/__main__.py
12
+ src/moleditpy/main.py
13
+ src/moleditpy.egg-info/PKG-INFO
14
+ src/moleditpy.egg-info/SOURCES.txt
15
+ src/moleditpy.egg-info/dependency_links.txt
16
+ src/moleditpy.egg-info/entry_points.txt
17
+ src/moleditpy.egg-info/requires.txt
18
+ src/moleditpy.egg-info/top_level.txt
19
+ src/moleditpy/assets/file_icon.ico
20
+ src/moleditpy/assets/icon.icns
21
+ src/moleditpy/assets/icon.ico
22
+ src/moleditpy/assets/icon.png
23
+ src/moleditpy/core/__init__.py
24
+ src/moleditpy/core/mol_geometry.py
25
+ src/moleditpy/core/molecular_data.py
26
+ src/moleditpy/modules/__init__.py
27
+ src/moleditpy/plugins/__init__.py
28
+ src/moleditpy/plugins/plugin_interface.py
29
+ src/moleditpy/plugins/plugin_manager.py
30
+ src/moleditpy/plugins/plugin_manager_window.py
31
+ src/moleditpy/ui/__init__.py
32
+ src/moleditpy/ui/about_dialog.py
33
+ src/moleditpy/ui/align_plane_dialog.py
34
+ src/moleditpy/ui/alignment_dialog.py
35
+ src/moleditpy/ui/analysis_window.py
36
+ src/moleditpy/ui/angle_dialog.py
37
+ src/moleditpy/ui/app_state.py
38
+ src/moleditpy/ui/atom_item.py
39
+ src/moleditpy/ui/bond_item.py
40
+ src/moleditpy/ui/bond_length_dialog.py
41
+ src/moleditpy/ui/calculation_worker.py
42
+ src/moleditpy/ui/color_settings_dialog.py
43
+ src/moleditpy/ui/compute_engine.py
44
+ src/moleditpy/ui/compute_logic.py
45
+ src/moleditpy/ui/constrained_optimization_dialog.py
46
+ src/moleditpy/ui/custom_interactor_style.py
47
+ src/moleditpy/ui/custom_qt_interactor.py
48
+ src/moleditpy/ui/dialog_3d_picking_mixin.py
49
+ src/moleditpy/ui/dialog_logic.py
50
+ src/moleditpy/ui/dialog_manager.py
51
+ src/moleditpy/ui/dihedral_dialog.py
52
+ src/moleditpy/ui/edit_3d.py
53
+ src/moleditpy/ui/edit_3d_logic.py
54
+ src/moleditpy/ui/edit_actions.py
55
+ src/moleditpy/ui/edit_actions_logic.py
56
+ src/moleditpy/ui/export_logic.py
57
+ src/moleditpy/ui/main_window.py
58
+ src/moleditpy/ui/main_window_init.py
59
+ src/moleditpy/ui/mirror_dialog.py
60
+ src/moleditpy/ui/molecular_parsers.py
61
+ src/moleditpy/ui/molecular_scene_handler.py
62
+ src/moleditpy/ui/molecule_scene.py
63
+ src/moleditpy/ui/move_group_dialog.py
64
+ src/moleditpy/ui/periodic_table_dialog.py
65
+ src/moleditpy/ui/planarize_dialog.py
66
+ src/moleditpy/ui/project_io.py
67
+ src/moleditpy/ui/settings_dialog.py
68
+ src/moleditpy/ui/sip_isdeleted_safe.py
69
+ src/moleditpy/ui/string_importers.py
70
+ src/moleditpy/ui/template_preview_item.py
71
+ src/moleditpy/ui/template_preview_view.py
72
+ src/moleditpy/ui/translation_dialog.py
73
+ src/moleditpy/ui/ui_manager.py
74
+ src/moleditpy/ui/user_template_dialog.py
75
+ src/moleditpy/ui/view_3d_logic.py
76
+ src/moleditpy/ui/view_loaders.py
77
+ src/moleditpy/ui/zoomable_view.py
78
+ src/moleditpy/ui/settings_tabs/__init__.py
79
+ src/moleditpy/ui/settings_tabs/settings_2d_tab.py
80
+ src/moleditpy/ui/settings_tabs/settings_3d_tabs.py
81
+ src/moleditpy/ui/settings_tabs/settings_other_tab.py
82
+ src/moleditpy/ui/settings_tabs/settings_tab_base.py
83
+ src/moleditpy/utils/__init__.py
84
+ src/moleditpy/utils/constants.py
85
+ src/moleditpy/utils/sip_isdeleted_safe.py
86
+ src/moleditpy/utils/system_utils.py
@@ -0,0 +1,20 @@
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
+ """Top-level package for moleditpy."""
14
+
15
+ import importlib.util
16
+
17
+ try:
18
+ OBABEL_AVAILABLE = importlib.util.find_spec("openbabel") is not None
19
+ except ImportError:
20
+ OBABEL_AVAILABLE = False
File without changes
@@ -18,6 +18,8 @@ submodules without circular dependencies.
18
18
  """
19
19
 
20
20
  import numpy as np
21
+ import math
22
+ from collections import deque
21
23
 
22
24
  # ------------------------------------------------------------------
23
25
  # Primitive geometry helpers
@@ -437,3 +439,148 @@ def identify_valence_problems(atoms_data, bonds_data):
437
439
  problem_atom_ids.append(atom_id)
438
440
 
439
441
  return problem_atom_ids
442
+
443
+
444
+ def optimize_2d_coords(mol):
445
+ """Generate 2D coordinates using RDKit and return a map of (x, y) tuples."""
446
+ from rdkit.Chem import AllChem
447
+
448
+ AllChem.Compute2DCoords(mol)
449
+ conf = mol.GetConformer()
450
+ new_positions = {}
451
+ for rdkit_atom in mol.GetAtoms():
452
+ if rdkit_atom.HasProp("_original_atom_id"):
453
+ original_id = rdkit_atom.GetIntProp("_original_atom_id")
454
+ pos = conf.GetAtomPosition(rdkit_atom.GetIdx())
455
+ new_positions[original_id] = (pos.x, pos.y)
456
+ return new_positions
457
+
458
+
459
+ def calculate_best_fit_plane_projection(centered_positions, normal, centroid):
460
+ """Project centered points orthogonally onto the plane defined by normal and centroid."""
461
+ projections = centered_positions - np.outer(
462
+ np.dot(centered_positions, normal), normal
463
+ )
464
+ return projections + centroid
465
+
466
+
467
+ def rotate_2d_points(points_map, center_x, center_y, angle_degrees):
468
+ """Rotate 2D points (atom_id -> (x, y)) around a center."""
469
+ rad = math.radians(angle_degrees)
470
+ cos_a = math.cos(rad)
471
+ sin_a = math.sin(rad)
472
+ new_positions = {}
473
+ for atom_id, (x, y) in points_map.items():
474
+ dx = x - center_x
475
+ dy = y - center_y
476
+ new_dx = dx * cos_a - dy * sin_a
477
+ new_dy = dx * sin_a + dy * cos_a
478
+ new_positions[atom_id] = (center_x + new_dx, center_y + new_dy)
479
+ return new_positions
480
+
481
+
482
+ def resolve_2d_overlaps(
483
+ atom_ids,
484
+ positions_map,
485
+ adjacency_list,
486
+ overlap_threshold=0.5,
487
+ move_distance=20,
488
+ has_bond_check_func=None,
489
+ ):
490
+ """Detect and resolve overlapping atom groups in 2D.
491
+
492
+ Returns list of (atom_ids_set, translation_vector_tuple).
493
+ """
494
+ overlapping_pairs = []
495
+ ids_list = list(atom_ids)
496
+ for i in range(len(ids_list)):
497
+ for j in range(i + 1, len(ids_list)):
498
+ id1 = ids_list[i]
499
+ id2 = ids_list[j]
500
+
501
+ # Skip directly bonded pairs
502
+ if has_bond_check_func and has_bond_check_func(id1, id2):
503
+ continue
504
+
505
+ p1 = positions_map[id1]
506
+ p2 = positions_map[id2]
507
+ dist = math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)
508
+ if dist < overlap_threshold:
509
+ overlapping_pairs.append((id1, id2))
510
+
511
+ if not overlapping_pairs:
512
+ return []
513
+
514
+ # Union-Find for overlap groups
515
+ parent = {aid: aid for aid in atom_ids}
516
+
517
+ def find_set(aid):
518
+ if parent[aid] == aid:
519
+ return aid
520
+ parent[aid] = find_set(parent[aid])
521
+ return parent[aid]
522
+
523
+ def unite_sets(aid1, aid2):
524
+ root1 = find_set(aid1)
525
+ root2 = find_set(aid2)
526
+ if root1 != root2:
527
+ parent[root2] = root1
528
+
529
+ for id1, id2 in overlapping_pairs:
530
+ unite_sets(id1, id2)
531
+
532
+ groups_by_root = {}
533
+ for aid in atom_ids:
534
+ root = find_set(aid)
535
+ groups_by_root.setdefault(root, []).append(aid)
536
+
537
+ move_operations = []
538
+ processed_roots = set()
539
+
540
+ for root_id, group_ids in groups_by_root.items():
541
+ if root_id in processed_roots or len(group_ids) < 2:
542
+ continue
543
+ processed_roots.add(root_id)
544
+
545
+ # Split into fragments via BFS
546
+ fragments = []
547
+ visited = set()
548
+ group_set = set(group_ids)
549
+ for aid in group_ids:
550
+ if aid not in visited:
551
+ frag = set()
552
+ q = deque([aid])
553
+ visited.add(aid)
554
+ frag.add(aid)
555
+ while q:
556
+ curr = q.popleft()
557
+ for neighbor in adjacency_list.get(curr, []):
558
+ if neighbor in group_set and neighbor not in visited:
559
+ visited.add(neighbor)
560
+ frag.add(neighbor)
561
+ q.append(neighbor)
562
+ fragments.append(frag)
563
+
564
+ if len(fragments) < 2:
565
+ continue
566
+
567
+ # Find representative pair
568
+ rep_id1, rep_id2 = None, None
569
+ for i1, i2 in overlapping_pairs:
570
+ if find_set(i1) == root_id:
571
+ rep_id1, rep_id2 = i1, i2
572
+ break
573
+
574
+ if not rep_id1:
575
+ continue
576
+
577
+ frag1 = next((f for f in fragments if rep_id1 in f), None)
578
+ frag2 = next((f for f in fragments if rep_id2 in f), None)
579
+ if not frag1 or not frag2 or frag1 == frag2:
580
+ continue
581
+
582
+ ids_to_move = frag1 if rep_id1 > rep_id2 else frag2
583
+ # Move vector (-move_distance, move_distance)
584
+ move_operations.append((ids_to_move, (-move_distance, move_distance)))
585
+
586
+ return move_operations
@@ -1,4 +1,3 @@
1
- import logging
2
1
  #!/usr/bin/env python3
3
2
  # -*- coding: utf-8 -*-
4
3
 
@@ -11,13 +10,23 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
11
10
  DOI: 10.5281/zenodo.17268532
12
11
  """
13
12
 
13
+ import logging
14
14
  from rdkit import Chem
15
- from PyQt6.QtCore import QPointF
16
15
 
17
16
  try:
18
- from .constants import ANGSTROM_PER_PIXEL
17
+ from ..utils.constants import ANGSTROM_PER_PIXEL
19
18
  except ImportError:
20
- from modules.constants import ANGSTROM_PER_PIXEL
19
+ from moleditpy.utils.constants import ANGSTROM_PER_PIXEL
20
+
21
+
22
+ class PointTuple(tuple):
23
+ """Backward-compatible tuple that allows .x() and .y() access like QPointF."""
24
+
25
+ def x(self):
26
+ return self[0]
27
+
28
+ def y(self):
29
+ return self[1]
21
30
 
22
31
 
23
32
  class MolecularData:
@@ -29,9 +38,15 @@ class MolecularData:
29
38
 
30
39
  def add_atom(self, symbol, pos, charge=0, radical=0):
31
40
  atom_id = self._next_atom_id
41
+ # Internalize position as raw floats to decouple from UI types (QPointF)
42
+ if hasattr(pos, "x") and hasattr(pos, "y"):
43
+ raw_pos = PointTuple((float(pos.x()), float(pos.y())))
44
+ else:
45
+ raw_pos = PointTuple((float(pos[0]), float(pos[1])))
46
+
32
47
  self.atoms[atom_id] = {
33
48
  "symbol": symbol,
34
- "pos": pos,
49
+ "pos": raw_pos,
35
50
  "item": None,
36
51
  "charge": charge,
37
52
  "radical": radical,
@@ -40,6 +55,16 @@ class MolecularData:
40
55
  self._next_atom_id += 1
41
56
  return atom_id
42
57
 
58
+ def set_atom_pos(self, atom_id, pos):
59
+ """Update atom position using raw floats or QPointF."""
60
+ if atom_id in self.atoms:
61
+ if hasattr(pos, "x") and hasattr(pos, "y"):
62
+ self.atoms[atom_id]["pos"] = PointTuple(
63
+ (float(pos.x()), float(pos.y()))
64
+ )
65
+ else:
66
+ self.atoms[atom_id]["pos"] = PointTuple((float(pos[0]), float(pos[1])))
67
+
43
68
  def add_bond(self, id1, id2, order=1, stereo=0):
44
69
  # For stereo bonds, do not sort because ID order determines direction.
45
70
  # For non-stereo bonds, sort to normalize the key.
@@ -68,16 +93,11 @@ class MolecularData:
68
93
  # Safely get neighbors before deleting the atom's own entry
69
94
  neighbors = self.adjacency_list.get(atom_id, [])
70
95
  for neighbor_id in neighbors:
71
- try:
72
- if (
73
- neighbor_id in self.adjacency_list
74
- and atom_id in self.adjacency_list[neighbor_id]
75
- ):
76
- self.adjacency_list[neighbor_id].remove(atom_id)
77
- except (ValueError, KeyError, TypeError) as e:
78
- logging.debug(
79
- f"Suppressed exception: {e}"
80
- ) # Ignore adjacency list inconsistencies during atom removal
96
+ if (
97
+ neighbor_id in self.adjacency_list
98
+ and atom_id in self.adjacency_list[neighbor_id]
99
+ ):
100
+ self.adjacency_list[neighbor_id].remove(atom_id)
81
101
 
82
102
  # Now, safely delete the atom's own entry from the adjacency list
83
103
  if atom_id in self.adjacency_list:
@@ -87,14 +107,9 @@ class MolecularData:
87
107
  del self.atoms[atom_id]
88
108
 
89
109
  # Remove bonds involving this atom
90
- try:
91
- bonds_to_remove = [key for key in self.bonds if atom_id in key]
92
- for key in bonds_to_remove:
93
- del self.bonds[key]
94
- except (RuntimeError, KeyError) as e:
95
- logging.debug(
96
- f"Suppressed exception: {e}"
97
- ) # Ignore mutation issues during batch bond removal
110
+ bonds_to_remove = [key for key in self.bonds if atom_id in key]
111
+ for key in bonds_to_remove:
112
+ self.bonds.pop(key, None)
98
113
 
99
114
  def remove_bond(self, id1, id2):
100
115
  # Look for directional stereo bonds (forward/reverse) and normalized non-stereo bond keys.
@@ -105,16 +120,11 @@ class MolecularData:
105
120
  key_to_remove = (id2, id1)
106
121
 
107
122
  if key_to_remove:
108
- try:
109
- if id1 in self.adjacency_list and id2 in self.adjacency_list[id1]:
110
- self.adjacency_list[id1].remove(id2)
111
- if id2 in self.adjacency_list and id1 in self.adjacency_list[id2]:
112
- self.adjacency_list[id2].remove(id1)
113
- del self.bonds[key_to_remove]
114
- except (ValueError, KeyError) as e:
115
- logging.debug(
116
- f"Suppressed exception: {e}"
117
- ) # Ignore if bond already removed or inconsistent
123
+ if id1 in self.adjacency_list and id2 in self.adjacency_list[id1]:
124
+ self.adjacency_list[id1].remove(id2)
125
+ if id2 in self.adjacency_list and id1 in self.adjacency_list[id2]:
126
+ self.adjacency_list[id2].remove(id1)
127
+ self.bonds.pop(key_to_remove, None)
118
128
 
119
129
  def to_rdkit_mol(self, use_2d_stereo=True):
120
130
  """
@@ -125,7 +135,7 @@ class MolecularData:
125
135
  return None
126
136
  mol = Chem.RWMol()
127
137
 
128
- # --- Step 1: atoms ---
138
+ # atoms
129
139
  atom_id_to_idx_map = {}
130
140
  for atom_id, data in self.atoms.items():
131
141
  try:
@@ -140,7 +150,7 @@ class MolecularData:
140
150
  idx = mol.AddAtom(atom)
141
151
  atom_id_to_idx_map[atom_id] = idx
142
152
 
143
- # --- Step 2: save bonds & stereo info (label info is kept here) ---
153
+ # save bonds & stereo info (label info is kept here)
144
154
  bond_stereo_info = {} # bond_idx -> {'type': int, 'atom_ids': (id1,id2), 'bond_data': bond_data}
145
155
  for (id1, id2), bond_data in self.bonds.items():
146
156
  if id1 not in atom_id_to_idx_map or id2 not in atom_id_to_idx_map:
@@ -165,7 +175,7 @@ class MolecularData:
165
175
  "bond_data": bond_data,
166
176
  }
167
177
 
168
- # --- Step 3: sanitize ---
178
+ # sanitize
169
179
  final_mol = mol.GetMol()
170
180
  try:
171
181
  Chem.SanitizeMol(final_mol)
@@ -173,7 +183,7 @@ class MolecularData:
173
183
  # Sanitization failure: return None to trigger manual MOL block fallback
174
184
  return None
175
185
 
176
- # --- Step 4: add 2D conformer ---
186
+ # add 2D conformer
177
187
  # Convert from scene pixels to angstroms when creating RDKit conformer.
178
188
  conf = Chem.Conformer(final_mol.GetNumAtoms())
179
189
  conf.Set3D(False)
@@ -182,14 +192,19 @@ class MolecularData:
182
192
  idx = atom_id_to_idx_map[atom_id]
183
193
  pos = data.get("pos")
184
194
  if pos:
185
- ax = pos.x() * ANGSTROM_PER_PIXEL
186
- ay = (
187
- -pos.y() * ANGSTROM_PER_PIXEL
188
- ) # Invert Y-coordinate (screen coordinates -> chemical coordinates)
195
+ # pos may be a tuple (x, y) or a QPointF (from old state deserialization)
196
+ if hasattr(pos, "x") and hasattr(pos, "y"):
197
+ ax = pos.x() * ANGSTROM_PER_PIXEL
198
+ ay = -pos.y() * ANGSTROM_PER_PIXEL
199
+ elif isinstance(pos, (list, tuple)) and len(pos) >= 2:
200
+ ax = pos[0] * ANGSTROM_PER_PIXEL
201
+ ay = -pos[1] * ANGSTROM_PER_PIXEL
202
+ else:
203
+ continue
189
204
  conf.SetAtomPosition(idx, (ax, ay, 0.0))
190
205
  final_mol.AddConformer(conf)
191
206
 
192
- # --- Step 5: Stereochemistry setting prioritizing E/Z labels ---
207
+ # Stereochemistry setting prioritizing E/Z labels ---
193
208
  # First, record bonds with E/Z labels
194
209
  ez_labeled_bonds = set()
195
210
  for bond_idx, info in bond_stereo_info.items():
@@ -223,7 +238,7 @@ class MolecularData:
223
238
  return nbr.GetIdx()
224
239
  return None
225
240
 
226
- # --- Step 6: Overwrite based on labels (E/Z has highest priority) ---
241
+ # Overwrite based on labels (E/Z has highest priority) ---
227
242
  for bond_idx, info in bond_stereo_info.items():
228
243
  stereo_type = info["type"]
229
244
  bond = final_mol.GetBondWithIdx(bond_idx)
@@ -280,7 +295,7 @@ class MolecularData:
280
295
  if b2 is not None:
281
296
  b2.SetBondDir(Chem.BondDir.NONE)
282
297
 
283
- # Step 7: Finalization (cache update + stereochemistry reassignment)
298
+ # Finalization (cache update + stereochemistry reassignment)
284
299
  final_mol.UpdatePropertyCache(strict=False)
285
300
 
286
301
  # During 3D conversion (use_2d_stereo=False), apply force=True if E/Z labels exist
@@ -347,18 +362,17 @@ class MolecularData:
347
362
  positions = []
348
363
  for aidx in a_ring:
349
364
  item = rdkit_idx_to_item.get(aidx)
350
- if item:
351
- try:
352
- positions.append(item.pos())
353
- except (AttributeError, RuntimeError) as e:
354
- logging.debug(f"Suppressed exception: {e}")
365
+ if item and hasattr(item, "pos"):
366
+ pos = item.pos()
367
+ if pos is not None:
368
+ positions.append(pos)
355
369
 
356
370
  if not positions:
357
371
  continue
358
372
 
359
373
  center_x = sum(p.x() for p in positions) / len(positions)
360
374
  center_y = sum(p.y() for p in positions) / len(positions)
361
- ring_center = QPointF(center_x, center_y)
375
+ ring_center = (center_x, center_y) # Use tuple (x, y) instead of QPointF
362
376
 
363
377
  # Update all bonds in this ring
364
378
  for bidx in b_ring:
@@ -378,9 +392,9 @@ class MolecularData:
378
392
  try:
379
393
  return Chem.MolToMolBlock(mol, includeStereo=True)
380
394
  except (RuntimeError, ValueError, TypeError) as e:
381
- logging.debug(
382
- f"Suppressed exception: {e}"
383
- ) # Suppress errors during RDKit MolBlock generation
395
+ logging.warning(
396
+ f"RDKit MolBlock generation failed: {e}"
397
+ ) # Fallback to manual MolBlock generation if RDKit fails
384
398
 
385
399
  if not self.atoms:
386
400
  return None
@@ -390,8 +404,17 @@ class MolecularData:
390
404
  mol_block += f"{num_atoms:3d}{num_bonds:3d} 0 0 0 0 0 0 0 0999 V2000\n"
391
405
  for old_id, atom in self.atoms.items():
392
406
  # Convert scene pixel coordinates to angstroms when emitting MOL block
393
- x_px = atom["item"].pos().x()
394
- y_px = -atom["item"].pos().y()
407
+ pos = atom.get("pos")
408
+ if not pos:
409
+ continue
410
+
411
+ if hasattr(pos, "x") and hasattr(pos, "y"):
412
+ x_px, y_px = pos.x(), -pos.y()
413
+ elif isinstance(pos, (list, tuple)) and len(pos) >= 2:
414
+ x_px, y_px = pos[0], -pos[1]
415
+ else:
416
+ continue
417
+
395
418
  x, y = x_px * ANGSTROM_PER_PIXEL, y_px * ANGSTROM_PER_PIXEL
396
419
  z, symbol = 0.0, atom["symbol"]
397
420
  charge = atom.get("charge", 0)
@@ -425,3 +448,44 @@ class MolecularData:
425
448
 
426
449
  mol_block += "M END\n"
427
450
  return mol_block
451
+
452
+ def to_template_dict(self, name, version="1.0", application_version=""):
453
+ """Convert current structure to a dictionary for template storage."""
454
+ import datetime
455
+
456
+ atoms_data = []
457
+ for atom_id, atom_info in self.atoms.items():
458
+ pos = atom_info["pos"]
459
+ # pos is guaranteed to be a tuple (x, y) due to internal caching in add_atom/set_atom_pos
460
+ atoms_data.append(
461
+ {
462
+ "id": atom_id,
463
+ "symbol": atom_info["symbol"],
464
+ "x": pos[0],
465
+ "y": pos[1],
466
+ "charge": atom_info.get("charge", 0),
467
+ "radical": atom_info.get("radical", 0),
468
+ }
469
+ )
470
+
471
+ bonds_data = []
472
+ for (id1, id2), bond_info in self.bonds.items():
473
+ bonds_data.append(
474
+ {
475
+ "atom1": id1,
476
+ "atom2": id2,
477
+ "order": bond_info["order"],
478
+ "stereo": bond_info.get("stereo", 0),
479
+ }
480
+ )
481
+
482
+ return {
483
+ "format": "PME Template",
484
+ "version": version,
485
+ "application": "MoleditPy",
486
+ "application_version": application_version,
487
+ "name": name,
488
+ "created": str(datetime.datetime.now()),
489
+ "atoms": atoms_data,
490
+ "bonds": bonds_data,
491
+ }