MoleditPy 3.0.0a3__tar.gz → 3.0.0a5__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 (92) hide show
  1. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/PKG-INFO +1 -1
  2. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/pyproject.toml +1 -1
  3. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/MoleditPy.egg-info/PKG-INFO +1 -1
  4. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/MoleditPy.egg-info/SOURCES.txt +4 -8
  5. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/core/mol_geometry.py +90 -21
  6. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/core/molecular_data.py +21 -13
  7. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/plugins/plugin_interface.py +112 -21
  8. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/plugins/plugin_manager.py +109 -31
  9. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/plugins/plugin_manager_window.py +313 -313
  10. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/about_dialog.py +2 -2
  11. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/align_plane_dialog.py +20 -94
  12. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/alignment_dialog.py +8 -7
  13. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/angle_dialog.py +406 -516
  14. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/app_state.py +172 -283
  15. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/atom_item.py +39 -31
  16. moleditpy-3.0.0a5/src/moleditpy/ui/base_picking_dialog.py +119 -0
  17. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/bond_item.py +69 -89
  18. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/bond_length_dialog.py +356 -456
  19. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/calculation_worker.py +32 -18
  20. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/color_settings_dialog.py +66 -35
  21. moleditpy-3.0.0a5/src/moleditpy/ui/compute_logic.py +625 -0
  22. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/constrained_optimization_dialog.py +26 -26
  23. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/custom_interactor_style.py +83 -101
  24. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/custom_qt_interactor.py +4 -4
  25. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/dialog_3d_picking_mixin.py +39 -34
  26. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/dialog_logic.py +121 -135
  27. moleditpy-3.0.0a5/src/moleditpy/ui/dihedral_dialog.py +393 -0
  28. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/edit_3d_logic.py +47 -48
  29. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/edit_actions_logic.py +331 -170
  30. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/export_logic.py +130 -165
  31. moleditpy-3.0.0a5/src/moleditpy/ui/geometry_base_dialog.py +111 -0
  32. moleditpy-3.0.0a5/src/moleditpy/ui/io_logic.py +835 -0
  33. moleditpy-3.0.0a5/src/moleditpy/ui/main_window.py +97 -0
  34. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/main_window_init.py +507 -521
  35. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/mirror_dialog.py +3 -3
  36. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/molecular_scene_handler.py +63 -42
  37. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/molecule_scene.py +72 -33
  38. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/move_group_dialog.py +123 -259
  39. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/planarize_dialog.py +22 -65
  40. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/settings_dialog.py +33 -66
  41. {moleditpy-3.0.0a3/src/moleditpy/utils → moleditpy-3.0.0a5/src/moleditpy/ui/settings_tabs}/__init__.py +11 -11
  42. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/settings_tabs/settings_2d_tab.py +6 -34
  43. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/settings_tabs/settings_3d_tabs.py +20 -75
  44. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/settings_tabs/settings_other_tab.py +18 -6
  45. moleditpy-3.0.0a5/src/moleditpy/ui/settings_tabs/settings_tab_base.py +63 -0
  46. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/string_importers.py +61 -49
  47. moleditpy-3.0.0a5/src/moleditpy/ui/translation_dialog.py +194 -0
  48. moleditpy-3.0.0a5/src/moleditpy/ui/ui_manager.py +567 -0
  49. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/user_template_dialog.py +30 -33
  50. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/view_3d_logic.py +324 -229
  51. {moleditpy-3.0.0a3/src/moleditpy/plugins → moleditpy-3.0.0a5/src/moleditpy/utils}/__init__.py +11 -11
  52. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/utils/constants.py +1 -1
  53. moleditpy-3.0.0a5/src/moleditpy/utils/default_settings.py +94 -0
  54. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/utils/sip_isdeleted_safe.py +41 -41
  55. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/utils/system_utils.py +71 -71
  56. moleditpy-3.0.0a3/src/moleditpy/modules/__init__.py +0 -122
  57. moleditpy-3.0.0a3/src/moleditpy/ui/compute_engine.py +0 -880
  58. moleditpy-3.0.0a3/src/moleditpy/ui/compute_logic.py +0 -526
  59. moleditpy-3.0.0a3/src/moleditpy/ui/dialog_manager.py +0 -464
  60. moleditpy-3.0.0a3/src/moleditpy/ui/dihedral_dialog.py +0 -545
  61. moleditpy-3.0.0a3/src/moleditpy/ui/edit_3d.py +0 -444
  62. moleditpy-3.0.0a3/src/moleditpy/ui/edit_actions.py +0 -1432
  63. moleditpy-3.0.0a3/src/moleditpy/ui/main_window.py +0 -218
  64. moleditpy-3.0.0a3/src/moleditpy/ui/molecular_parsers.py +0 -587
  65. moleditpy-3.0.0a3/src/moleditpy/ui/project_io.py +0 -386
  66. moleditpy-3.0.0a3/src/moleditpy/ui/settings_tabs/settings_tab_base.py +0 -33
  67. moleditpy-3.0.0a3/src/moleditpy/ui/translation_dialog.py +0 -347
  68. moleditpy-3.0.0a3/src/moleditpy/ui/ui_manager.py +0 -511
  69. moleditpy-3.0.0a3/src/moleditpy/ui/view_loaders.py +0 -274
  70. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/LICENSE +0 -0
  71. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/README.md +0 -0
  72. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/setup.cfg +0 -0
  73. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
  74. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/MoleditPy.egg-info/entry_points.txt +0 -0
  75. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/MoleditPy.egg-info/requires.txt +0 -0
  76. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/MoleditPy.egg-info/top_level.txt +0 -0
  77. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/__init__.py +0 -0
  78. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/__main__.py +0 -0
  79. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/assets/file_icon.ico +0 -0
  80. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/assets/icon.icns +0 -0
  81. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/assets/icon.ico +0 -0
  82. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/assets/icon.png +0 -0
  83. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/core/__init__.py +0 -0
  84. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/main.py +0 -0
  85. {moleditpy-3.0.0a3/src/moleditpy/ui/settings_tabs → moleditpy-3.0.0a5/src/moleditpy/plugins}/__init__.py +0 -0
  86. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/__init__.py +0 -0
  87. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/analysis_window.py +0 -0
  88. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/periodic_table_dialog.py +0 -0
  89. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/sip_isdeleted_safe.py +0 -0
  90. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/template_preview_item.py +0 -0
  91. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/template_preview_view.py +0 -0
  92. {moleditpy-3.0.0a3 → moleditpy-3.0.0a5}/src/moleditpy/ui/zoomable_view.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy
3
- Version: 3.0.0a3
3
+ Version: 3.0.0a5
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
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "MoleditPy"
7
7
 
8
- version = "3.0.0a3"
8
+ version = "3.0.0a5"
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.0a3
3
+ Version: 3.0.0a5
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
@@ -23,7 +23,6 @@ src/moleditpy/assets/icon.png
23
23
  src/moleditpy/core/__init__.py
24
24
  src/moleditpy/core/mol_geometry.py
25
25
  src/moleditpy/core/molecular_data.py
26
- src/moleditpy/modules/__init__.py
27
26
  src/moleditpy/plugins/__init__.py
28
27
  src/moleditpy/plugins/plugin_interface.py
29
28
  src/moleditpy/plugins/plugin_manager.py
@@ -36,34 +35,31 @@ src/moleditpy/ui/analysis_window.py
36
35
  src/moleditpy/ui/angle_dialog.py
37
36
  src/moleditpy/ui/app_state.py
38
37
  src/moleditpy/ui/atom_item.py
38
+ src/moleditpy/ui/base_picking_dialog.py
39
39
  src/moleditpy/ui/bond_item.py
40
40
  src/moleditpy/ui/bond_length_dialog.py
41
41
  src/moleditpy/ui/calculation_worker.py
42
42
  src/moleditpy/ui/color_settings_dialog.py
43
- src/moleditpy/ui/compute_engine.py
44
43
  src/moleditpy/ui/compute_logic.py
45
44
  src/moleditpy/ui/constrained_optimization_dialog.py
46
45
  src/moleditpy/ui/custom_interactor_style.py
47
46
  src/moleditpy/ui/custom_qt_interactor.py
48
47
  src/moleditpy/ui/dialog_3d_picking_mixin.py
49
48
  src/moleditpy/ui/dialog_logic.py
50
- src/moleditpy/ui/dialog_manager.py
51
49
  src/moleditpy/ui/dihedral_dialog.py
52
- src/moleditpy/ui/edit_3d.py
53
50
  src/moleditpy/ui/edit_3d_logic.py
54
- src/moleditpy/ui/edit_actions.py
55
51
  src/moleditpy/ui/edit_actions_logic.py
56
52
  src/moleditpy/ui/export_logic.py
53
+ src/moleditpy/ui/geometry_base_dialog.py
54
+ src/moleditpy/ui/io_logic.py
57
55
  src/moleditpy/ui/main_window.py
58
56
  src/moleditpy/ui/main_window_init.py
59
57
  src/moleditpy/ui/mirror_dialog.py
60
- src/moleditpy/ui/molecular_parsers.py
61
58
  src/moleditpy/ui/molecular_scene_handler.py
62
59
  src/moleditpy/ui/molecule_scene.py
63
60
  src/moleditpy/ui/move_group_dialog.py
64
61
  src/moleditpy/ui/periodic_table_dialog.py
65
62
  src/moleditpy/ui/planarize_dialog.py
66
- src/moleditpy/ui/project_io.py
67
63
  src/moleditpy/ui/settings_dialog.py
68
64
  src/moleditpy/ui/sip_isdeleted_safe.py
69
65
  src/moleditpy/ui/string_importers.py
@@ -73,7 +69,6 @@ src/moleditpy/ui/translation_dialog.py
73
69
  src/moleditpy/ui/ui_manager.py
74
70
  src/moleditpy/ui/user_template_dialog.py
75
71
  src/moleditpy/ui/view_3d_logic.py
76
- src/moleditpy/ui/view_loaders.py
77
72
  src/moleditpy/ui/zoomable_view.py
78
73
  src/moleditpy/ui/settings_tabs/__init__.py
79
74
  src/moleditpy/ui/settings_tabs/settings_2d_tab.py
@@ -82,5 +77,6 @@ src/moleditpy/ui/settings_tabs/settings_other_tab.py
82
77
  src/moleditpy/ui/settings_tabs/settings_tab_base.py
83
78
  src/moleditpy/utils/__init__.py
84
79
  src/moleditpy/utils/constants.py
80
+ src/moleditpy/utils/default_settings.py
85
81
  src/moleditpy/utils/sip_isdeleted_safe.py
86
82
  src/moleditpy/utils/system_utils.py
@@ -10,23 +10,19 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
10
  DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
- """Pure-logic molecular-geometry helpers.
14
-
15
- This module is intentionally free of any GUI imports so that it can
16
- be unit-tested in isolation and shared across dialogs and main-window
17
- submodules without circular dependencies.
18
- """
19
-
20
- import numpy as np
13
+ from __future__ import annotations
21
14
  import math
15
+ import logging
22
16
  from collections import deque
17
+ from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union
18
+
19
+ import numpy as np
23
20
 
24
21
  # ------------------------------------------------------------------
25
22
  # Primitive geometry helpers
26
23
  # ------------------------------------------------------------------
27
24
 
28
-
29
- def calc_distance(pos1, pos2) -> float:
25
+ def calc_distance(pos1: Union[np.ndarray, Tuple[float, float, float], List[float]], pos2: Union[np.ndarray, Tuple[float, float, float], List[float]]) -> float:
30
26
  """Return the Euclidean distance between two 3-D positions.
31
27
 
32
28
  Parameters
@@ -44,7 +40,11 @@ def calc_distance(pos1, pos2) -> float:
44
40
  )
45
41
 
46
42
 
47
- def calc_angle_deg(pos1, pos2_vertex, pos3) -> float:
43
+ def calc_angle_deg(
44
+ pos1: Union[np.ndarray, Tuple[float, float, float], List[float]],
45
+ pos2_vertex: Union[np.ndarray, Tuple[float, float, float], List[float]],
46
+ pos3: Union[np.ndarray, Tuple[float, float, float], List[float]],
47
+ ) -> float:
48
48
  """Return the angle pos1–pos2_vertex–pos3 in degrees.
49
49
 
50
50
  The angle is measured at *pos2_vertex* and is always in [0, 180].
@@ -78,7 +78,7 @@ def calc_angle_deg(pos1, pos2_vertex, pos3) -> float:
78
78
  # ------------------------------------------------------------------
79
79
 
80
80
 
81
- def get_connected_group(mol, start_atom, exclude=None):
81
+ def get_connected_group(mol: Any, start_atom: int, exclude: Optional[int] = None) -> Set[int]:
82
82
  """Return the set of atom indices reachable from *start_atom*
83
83
  without passing through *exclude*.
84
84
 
@@ -124,7 +124,7 @@ def get_connected_group(mol, start_atom, exclude=None):
124
124
  # ------------------------------------------------------------------
125
125
 
126
126
 
127
- def rodrigues_rotate(v, axis, angle):
127
+ def rodrigues_rotate(v: np.ndarray, axis: np.ndarray, angle: float) -> np.ndarray:
128
128
  """Rotate vector *v* around a unit *axis* by *angle* radians.
129
129
 
130
130
  Implements Rodrigues' rotation formula:
@@ -151,8 +151,13 @@ def rodrigues_rotate(v, axis, angle):
151
151
 
152
152
 
153
153
  def adjust_bond_angle(
154
- positions, idx_a, idx_b, idx_c, target_angle_deg, atom_indices_to_move
155
- ):
154
+ positions: np.ndarray,
155
+ idx_a: int,
156
+ idx_b: int,
157
+ idx_c: int,
158
+ target_angle_deg: float,
159
+ atom_indices_to_move: Iterable[int],
160
+ ) -> float:
156
161
  """Adjust the A–B–C bond angle to *target_angle_deg* using a
157
162
  difference-based rotation.
158
163
 
@@ -244,7 +249,7 @@ def adjust_bond_angle(
244
249
  # ------------------------------------------------------------------
245
250
 
246
251
 
247
- def calculate_dihedral(positions, i1, i2, i3, i4):
252
+ def calculate_dihedral(positions: Any, i1: int, i2: int, i3: int, i4: int) -> float:
248
253
  """Compute the dihedral angle defined by four atom indices.
249
254
 
250
255
  Parameters
@@ -295,6 +300,70 @@ def calculate_dihedral(positions, i1, i2, i3, i4):
295
300
  return float(np.degrees(angle_rad))
296
301
 
297
302
 
303
+ def adjust_dihedral(
304
+ positions: np.ndarray,
305
+ i1: int,
306
+ i2: int,
307
+ i3: int,
308
+ i4: int,
309
+ target_dihedral_deg: float,
310
+ atom_indices_to_move: Iterable[int],
311
+ ) -> float:
312
+ """Adjust the dihedral angle defined by i1-i2-i3-i4 to target_dihedral_deg.
313
+ The rotation is performed around the i2-i3 bond axis.
314
+
315
+ Parameters
316
+ ----------
317
+ positions : ndarray, shape (N, 3)
318
+ Atom coordinates. **Modified in-place.**
319
+ i1, i2, i3, i4 : int
320
+ Atom indices defining the dihedral.
321
+ target_dihedral_deg : float
322
+ Target dihedral angle in degrees.
323
+ atom_indices_to_move : iterable of int
324
+ Indices of atoms to be rotated.
325
+
326
+ Returns
327
+ -------
328
+ float
329
+ Applied rotation in radians.
330
+ """
331
+ # Current dihedral
332
+ current_dihedral = calculate_dihedral(positions, i1, i2, i3, i4)
333
+
334
+ # Rotation angle needed
335
+ delta_deg = target_dihedral_deg - current_dihedral
336
+
337
+ # Shortest rotation path
338
+ if delta_deg > 180:
339
+ delta_deg -= 360
340
+ elif delta_deg < -180:
341
+ delta_deg += 360
342
+
343
+ delta_rad = np.radians(delta_deg)
344
+
345
+ if abs(delta_rad) < 1e-9:
346
+ return 0.0
347
+
348
+ # Rotation axis (i2 -> i3)
349
+ pos2 = positions[i2]
350
+ pos3 = positions[i3]
351
+ axis = pos3 - pos2
352
+ axis_norm = np.linalg.norm(axis)
353
+
354
+ if axis_norm < 1e-12:
355
+ return 0.0
356
+
357
+ axis_unit = axis / axis_norm
358
+
359
+ # Rotate each movable atom
360
+ for idx in atom_indices_to_move:
361
+ rel = positions[idx] - pos2
362
+ positions[idx] = pos2 + rodrigues_rotate(rel, axis_unit, delta_rad)
363
+
364
+ return float(delta_rad)
365
+
366
+
298
367
  # ------------------------------------------------------------------
299
368
  # Valence sanity check
300
369
  # ------------------------------------------------------------------
@@ -314,7 +383,7 @@ _VALENCE_LIMITS = {
314
383
  }
315
384
 
316
385
 
317
- def is_problematic_valence(symbol, bond_count, charge=0):
386
+ def is_problematic_valence(symbol: str, bond_count: Union[int, float], charge: int = 0) -> bool:
318
387
  """Return ``True`` if the atom's total bond order exceeds its
319
388
  typical maximum valence.
320
389
 
@@ -406,7 +475,7 @@ def inject_ez_stereo_to_mol_block(mol_block, rdkit_mol, bonds_data):
406
475
  return "\n".join(mol_lines)
407
476
 
408
477
 
409
- def identify_valence_problems(atoms_data, bonds_data):
478
+ def identify_valence_problems(atoms_data: Dict[int, Any], bonds_data: Dict[Tuple[int, int], Any]) -> List[int]:
410
479
  """Identify atoms with problematic valence.
411
480
 
412
481
  Parameters
@@ -441,7 +510,7 @@ def identify_valence_problems(atoms_data, bonds_data):
441
510
  return problem_atom_ids
442
511
 
443
512
 
444
- def optimize_2d_coords(mol):
513
+ def optimize_2d_coords(mol: Any) -> Dict[int, Tuple[float, float]]:
445
514
  """Generate 2D coordinates using RDKit and return a map of (x, y) tuples."""
446
515
  from rdkit.Chem import AllChem
447
516
 
@@ -571,12 +640,12 @@ def resolve_2d_overlaps(
571
640
  rep_id1, rep_id2 = i1, i2
572
641
  break
573
642
 
574
- if not rep_id1:
643
+ if rep_id1 is None:
575
644
  continue
576
645
 
577
646
  frag1 = next((f for f in fragments if rep_id1 in f), None)
578
647
  frag2 = next((f for f in fragments if rep_id2 in f), None)
579
- if not frag1 or not frag2 or frag1 == frag2:
648
+ if frag1 is None or frag2 is None or frag1 == frag2:
580
649
  continue
581
650
 
582
651
  ids_to_move = frag1 if rep_id1 > rep_id2 else frag2
@@ -10,7 +10,9 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
10
  DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
+ from __future__ import annotations
13
14
  import logging
15
+ from typing import Any, Dict, List, Optional, Set, Tuple, Union
14
16
  from rdkit import Chem
15
17
 
16
18
  try:
@@ -22,21 +24,26 @@ except ImportError:
22
24
  class PointTuple(tuple):
23
25
  """Backward-compatible tuple that allows .x() and .y() access like QPointF."""
24
26
 
25
- def x(self):
27
+ def x(self) -> float:
26
28
  return self[0]
27
29
 
28
- def y(self):
30
+ def y(self) -> float:
29
31
  return self[1]
30
32
 
31
33
 
32
34
  class MolecularData:
33
- def __init__(self):
35
+ atoms: Dict[int, Dict[str, Any]]
36
+ bonds: Dict[Tuple[int, int], Dict[str, Any]]
37
+ adjacency_list: Dict[int, List[int]]
38
+ _next_atom_id: int
39
+
40
+ def __init__(self) -> None:
34
41
  self.atoms = {}
35
42
  self.bonds = {}
36
43
  self._next_atom_id = 0
37
44
  self.adjacency_list = {}
38
45
 
39
- def add_atom(self, symbol, pos, charge=0, radical=0):
46
+ def add_atom(self, symbol: str, pos: Union[Any, Tuple[float, float]], charge: int = 0, radical: int = 0) -> int:
40
47
  atom_id = self._next_atom_id
41
48
  # Internalize position as raw floats to decouple from UI types (QPointF)
42
49
  if hasattr(pos, "x") and hasattr(pos, "y"):
@@ -55,7 +62,7 @@ class MolecularData:
55
62
  self._next_atom_id += 1
56
63
  return atom_id
57
64
 
58
- def set_atom_pos(self, atom_id, pos):
65
+ def set_atom_pos(self, atom_id: int, pos: Union[Any, Tuple[float, float]]) -> None:
59
66
  """Update atom position using raw floats or QPointF."""
60
67
  if atom_id in self.atoms:
61
68
  if hasattr(pos, "x") and hasattr(pos, "y"):
@@ -65,7 +72,7 @@ class MolecularData:
65
72
  else:
66
73
  self.atoms[atom_id]["pos"] = PointTuple((float(pos[0]), float(pos[1])))
67
74
 
68
- def add_bond(self, id1, id2, order=1, stereo=0):
75
+ def add_bond(self, id1: int, id2: int, order: Union[int, float] = 1, stereo: int = 0) -> Tuple[Tuple[int, int], str]:
69
76
  # For stereo bonds, do not sort because ID order determines direction.
70
77
  # For non-stereo bonds, sort to normalize the key.
71
78
  if stereo == 0:
@@ -88,7 +95,7 @@ class MolecularData:
88
95
  self.bonds[(id1, id2)] = bond_data
89
96
  return (id1, id2), "created"
90
97
 
91
- def remove_atom(self, atom_id):
98
+ def remove_atom(self, atom_id: int) -> None:
92
99
  if atom_id in self.atoms:
93
100
  # Safely get neighbors before deleting the atom's own entry
94
101
  neighbors = self.adjacency_list.get(atom_id, [])
@@ -111,7 +118,7 @@ class MolecularData:
111
118
  for key in bonds_to_remove:
112
119
  self.bonds.pop(key, None)
113
120
 
114
- def remove_bond(self, id1, id2):
121
+ def remove_bond(self, id1: int, id2: int) -> None:
115
122
  # Look for directional stereo bonds (forward/reverse) and normalized non-stereo bond keys.
116
123
  key_to_remove = None
117
124
  if (id1, id2) in self.bonds:
@@ -126,7 +133,7 @@ class MolecularData:
126
133
  self.adjacency_list[id2].remove(id1)
127
134
  self.bonds.pop(key_to_remove, None)
128
135
 
129
- def to_rdkit_mol(self, use_2d_stereo=True):
136
+ def to_rdkit_mol(self, use_2d_stereo: bool = True) -> Optional[Chem.Mol]:
130
137
  """
131
138
  use_2d_stereo: True estimates E/Z from 2D coordinates (as before). False prioritizes E/Z labels.
132
139
  Call with use_2d_stereo=False for 3D conversion.
@@ -147,6 +154,7 @@ class MolecularData:
147
154
  atom.SetFormalCharge(data.get("charge", 0))
148
155
  atom.SetNumRadicalElectrons(data.get("radical", 0))
149
156
  atom.SetIntProp("_original_atom_id", atom_id)
157
+ atom.SetNoImplicit(False) # Allow RDKit to perceive implicit valence
150
158
  idx = mol.AddAtom(atom)
151
159
  atom_id_to_idx_map[atom_id] = idx
152
160
 
@@ -227,7 +235,7 @@ class MolecularData:
227
235
  )
228
236
 
229
237
  # Helper: Pick neighbors prioritizing heavy atoms
230
- def pick_preferred_neighbor(atom, exclude_idx):
238
+ def pick_preferred_neighbor(atom: Chem.Atom, exclude_idx: int) -> Optional[int]:
231
239
  for nbr in atom.GetNeighbors():
232
240
  if nbr.GetIdx() == exclude_idx:
233
241
  continue
@@ -305,7 +313,7 @@ class MolecularData:
305
313
  Chem.AssignStereochemistry(final_mol, cleanIt=False, force=False)
306
314
  return final_mol
307
315
 
308
- def update_ring_info_2d(self):
316
+ def update_ring_info_2d(self) -> None:
309
317
  """Update is_in_ring and ring_center for all BondItems based on 2D topology."""
310
318
  if not self.atoms or not self.bonds:
311
319
  return
@@ -386,7 +394,7 @@ class MolecularData:
386
394
  # so this is usually correct for 2D drawing.
387
395
  bond_item.ring_center = ring_center
388
396
 
389
- def to_mol_block(self):
397
+ def to_mol_block(self) -> Optional[str]:
390
398
  mol = self.to_rdkit_mol()
391
399
  if mol:
392
400
  try:
@@ -449,7 +457,7 @@ class MolecularData:
449
457
  mol_block += "M END\n"
450
458
  return mol_block
451
459
 
452
- def to_template_dict(self, name, version="1.0", application_version=""):
460
+ def to_template_dict(self, name: str, version: str = "1.0", application_version: str = "") -> Dict[str, Any]:
453
461
  """Convert current structure to a dictionary for template storage."""
454
462
  import datetime
455
463
 
@@ -10,7 +10,7 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
10
  DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
- from typing import Any, Callable, Optional
13
+ from typing import Any, Callable, List, Optional, Union
14
14
 
15
15
 
16
16
  class PluginContext:
@@ -75,6 +75,53 @@ class PluginContext:
75
75
  """
76
76
  return Plugin3DController(self._manager.get_main_window())
77
77
 
78
+ def show_status_message(self, message: str, timeout: int = 3000) -> None:
79
+ """
80
+ Display a message in the application status bar.
81
+ """
82
+ self._manager.show_status_message(message, timeout)
83
+
84
+ def push_undo_checkpoint(self) -> None:
85
+ """
86
+ Create an undo checkpoint for the current state.
87
+ Call this AFTER making modifications to the molecule to ensure the
88
+ new state is saved to the undo history.
89
+ """
90
+ self._manager.push_undo_checkpoint()
91
+
92
+ def refresh_3d_view(self) -> None:
93
+ """
94
+ Force a refresh (re-render) of the 3D scene.
95
+ """
96
+ self._manager.refresh_3d_view()
97
+
98
+ def reset_3d_camera(self) -> None:
99
+ """
100
+ Resets the 3D camera to fit the current molecule.
101
+ """
102
+ self._manager.reset_3d_camera()
103
+
104
+ def get_selected_atom_indices(self) -> List[int]:
105
+ """
106
+ Returns a list of RDKit atom indices currently selected in the 2D or 3D view.
107
+ Note: RDKit indices are returned, which map to the current_mol.
108
+ """
109
+ return self._manager.get_selected_atom_indices()
110
+
111
+ def register_window(self, window_id: str, window: Any) -> None:
112
+ """
113
+ Register a custom plugin window/dialog with the application.
114
+ This allows the application to manage the window lifecycle.
115
+ Windows are namespaced by the plugin name automatically.
116
+ """
117
+ self._manager.register_window(self._plugin_name, window_id, window)
118
+
119
+ def get_window(self, window_id: str) -> Optional[Any]:
120
+ """
121
+ Retrieve a previously registered window by its ID.
122
+ """
123
+ return self._manager.get_window(self._plugin_name, window_id)
124
+
78
125
  def get_main_window(self) -> Any:
79
126
  """
80
127
  Returns the raw MainWindow instance.
@@ -83,22 +130,45 @@ class PluginContext:
83
130
  return self._manager.get_main_window()
84
131
 
85
132
  @property
86
- def current_molecule(self) -> Any:
133
+ def current_mol(self) -> Any:
87
134
  """
88
- Get or set the current molecule (RDKit Mol object).
135
+ Get or set the current molecule (RDKit Mol object). Shortcut for current_molecule.
89
136
  """
90
- mw = self._manager.get_main_window()
137
+ mw = self.get_main_window()
138
+ return mw.current_mol if mw else None
139
+
140
+ @current_mol.setter
141
+ def current_mol(self, mol: Any):
142
+ mw = self.get_main_window()
91
143
  if mw:
92
- return mw.current_mol
93
- return None
144
+ mw.current_mol = mol
145
+ if hasattr(mw.view_3d_manager, "draw_molecule_3d"):
146
+ mw.view_3d_manager.draw_molecule_3d(mol)
147
+
148
+ @property
149
+ def current_molecule(self) -> Any:
150
+ """Alias for current_mol for backward compatibility."""
151
+ return self.current_mol
94
152
 
95
153
  @current_molecule.setter
96
154
  def current_molecule(self, mol: Any):
97
- mw = self._manager.get_main_window()
98
- if mw:
99
- mw.current_mol = mol
100
- if hasattr(mw, "draw_molecule_3d"):
101
- mw.draw_molecule_3d(mol)
155
+ self.current_mol = mol
156
+
157
+ @property
158
+ def plotter(self) -> Any:
159
+ """
160
+ Returns the PyVista plotter from the MainWindow.
161
+ """
162
+ mw = self.get_main_window()
163
+ return mw.plotter if mw else None
164
+
165
+ @property
166
+ def scene(self) -> Any:
167
+ """
168
+ Returns the 2D MoleculeScene from the MainWindow.
169
+ """
170
+ mw = self.get_main_window()
171
+ return mw.scene if mw else None
102
172
 
103
173
  def add_export_action(self, label: str, callback: Callable):
104
174
  """
@@ -202,6 +272,10 @@ class Plugin3DController:
202
272
  def __init__(self, main_window):
203
273
  self._mw = main_window
204
274
 
275
+ def _get_v3d(self):
276
+ """Helper to get the 3D manager."""
277
+ return getattr(self._mw, "view_3d_manager", None)
278
+
205
279
  def set_atom_color(self, atom_index: int, color_hex: str):
206
280
  """
207
281
  Set the color of a specific atom in the 3D view.
@@ -209,11 +283,11 @@ class Plugin3DController:
209
283
  atom_index: RDKit atom index.
210
284
  color_hex: Hex string e.g., "#FF0000".
211
285
  """
212
- if hasattr(self._mw, "main_window_view_3d"):
213
- self._mw.main_window_view_3d.update_atom_color_override(
214
- atom_index, color_hex
215
- )
216
- self._mw.plotter.render()
286
+ v3d = self._get_v3d()
287
+ if v3d:
288
+ v3d.update_atom_color_override(atom_index, color_hex)
289
+ if hasattr(self._mw, "plotter") and self._mw.plotter:
290
+ self._mw.plotter.render()
217
291
 
218
292
  def set_bond_color(self, bond_index: int, color_hex: str):
219
293
  """
@@ -223,8 +297,25 @@ class Plugin3DController:
223
297
  bond_index: RDKit bond index.
224
298
  color_hex: Hex string e.g., "#00FF00".
225
299
  """
226
- if hasattr(self._mw, "main_window_view_3d"):
227
- self._mw.main_window_view_3d.update_bond_color_override(
228
- bond_index, color_hex
229
- )
230
- self._mw.plotter.render()
300
+ v3d = self._get_v3d()
301
+ if v3d:
302
+ v3d.update_bond_color_override(bond_index, color_hex)
303
+ if hasattr(self._mw, "plotter") and self._mw.plotter:
304
+ self._mw.plotter.render()
305
+
306
+ def set_bond_color_by_atoms(self, atom_idx1: int, atom_idx2: int, color_hex: str):
307
+ """
308
+ Set the color of the bond between two atoms.
309
+
310
+ Args:
311
+ atom_idx1: First RDKit atom index.
312
+ atom_idx2: Second RDKit atom index.
313
+ color_hex: Hex string e.g., "#00FF00".
314
+ """
315
+ mol = getattr(self._mw, "current_mol", None)
316
+ if not mol:
317
+ return
318
+
319
+ bond = mol.GetBondBetweenAtoms(atom_idx1, atom_idx2)
320
+ if bond:
321
+ self.set_bond_color(bond.GetIdx(), color_hex)