MoleditPy 3.0.0a3__tar.gz → 3.0.0a4__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.
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/PKG-INFO +1 -1
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/pyproject.toml +1 -1
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/MoleditPy.egg-info/PKG-INFO +1 -1
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/MoleditPy.egg-info/SOURCES.txt +0 -2
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/core/mol_geometry.py +26 -21
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/core/molecular_data.py +20 -13
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/modules/__init__.py +2 -2
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/plugins/plugin_manager.py +32 -31
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/atom_item.py +33 -26
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/bond_item.py +36 -28
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/compute_logic.py +25 -22
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/edit_actions_logic.py +21 -19
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/export_logic.py +27 -24
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/molecular_parsers.py +14 -9
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/molecular_scene_handler.py +20 -9
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/molecule_scene.py +21 -14
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/project_io.py +8 -7
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/string_importers.py +9 -4
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/view_3d_logic.py +56 -36
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/utils/constants.py +1 -1
- moleditpy-3.0.0a3/src/moleditpy/ui/edit_3d.py +0 -444
- moleditpy-3.0.0a3/src/moleditpy/ui/edit_actions.py +0 -1432
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/LICENSE +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/README.md +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/setup.cfg +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/MoleditPy.egg-info/entry_points.txt +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/MoleditPy.egg-info/requires.txt +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/MoleditPy.egg-info/top_level.txt +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/__init__.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/__main__.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/assets/file_icon.ico +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/assets/icon.icns +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/assets/icon.ico +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/assets/icon.png +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/core/__init__.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/main.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/plugins/__init__.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/plugins/plugin_interface.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/plugins/plugin_manager_window.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/__init__.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/about_dialog.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/align_plane_dialog.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/alignment_dialog.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/analysis_window.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/angle_dialog.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/app_state.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/bond_length_dialog.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/calculation_worker.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/color_settings_dialog.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/compute_engine.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/constrained_optimization_dialog.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/custom_interactor_style.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/custom_qt_interactor.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/dialog_3d_picking_mixin.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/dialog_logic.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/dialog_manager.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/dihedral_dialog.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/edit_3d_logic.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/main_window.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/main_window_init.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/mirror_dialog.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/move_group_dialog.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/periodic_table_dialog.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/planarize_dialog.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/settings_dialog.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/settings_tabs/__init__.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/settings_tabs/settings_2d_tab.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/settings_tabs/settings_3d_tabs.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/settings_tabs/settings_other_tab.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/settings_tabs/settings_tab_base.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/sip_isdeleted_safe.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/template_preview_item.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/template_preview_view.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/translation_dialog.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/ui_manager.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/user_template_dialog.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/view_loaders.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/zoomable_view.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/utils/__init__.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/utils/sip_isdeleted_safe.py +0 -0
- {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/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.
|
|
3
|
+
Version: 3.0.0a4
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.0a4
|
|
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
|
|
@@ -49,9 +49,7 @@ src/moleditpy/ui/dialog_3d_picking_mixin.py
|
|
|
49
49
|
src/moleditpy/ui/dialog_logic.py
|
|
50
50
|
src/moleditpy/ui/dialog_manager.py
|
|
51
51
|
src/moleditpy/ui/dihedral_dialog.py
|
|
52
|
-
src/moleditpy/ui/edit_3d.py
|
|
53
52
|
src/moleditpy/ui/edit_3d_logic.py
|
|
54
|
-
src/moleditpy/ui/edit_actions.py
|
|
55
53
|
src/moleditpy/ui/edit_actions_logic.py
|
|
56
54
|
src/moleditpy/ui/export_logic.py
|
|
57
55
|
src/moleditpy/ui/main_window.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
|
-
|
|
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(
|
|
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
|
|
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
|
|
@@ -314,7 +319,7 @@ _VALENCE_LIMITS = {
|
|
|
314
319
|
}
|
|
315
320
|
|
|
316
321
|
|
|
317
|
-
def is_problematic_valence(symbol, bond_count, charge=0):
|
|
322
|
+
def is_problematic_valence(symbol: str, bond_count: Union[int, float], charge: int = 0) -> bool:
|
|
318
323
|
"""Return ``True`` if the atom's total bond order exceeds its
|
|
319
324
|
typical maximum valence.
|
|
320
325
|
|
|
@@ -406,7 +411,7 @@ def inject_ez_stereo_to_mol_block(mol_block, rdkit_mol, bonds_data):
|
|
|
406
411
|
return "\n".join(mol_lines)
|
|
407
412
|
|
|
408
413
|
|
|
409
|
-
def identify_valence_problems(atoms_data, bonds_data):
|
|
414
|
+
def identify_valence_problems(atoms_data: Dict[int, Any], bonds_data: Dict[Tuple[int, int], Any]) -> List[int]:
|
|
410
415
|
"""Identify atoms with problematic valence.
|
|
411
416
|
|
|
412
417
|
Parameters
|
|
@@ -441,7 +446,7 @@ def identify_valence_problems(atoms_data, bonds_data):
|
|
|
441
446
|
return problem_atom_ids
|
|
442
447
|
|
|
443
448
|
|
|
444
|
-
def optimize_2d_coords(mol):
|
|
449
|
+
def optimize_2d_coords(mol: Any) -> Dict[int, Tuple[float, float]]:
|
|
445
450
|
"""Generate 2D coordinates using RDKit and return a map of (x, y) tuples."""
|
|
446
451
|
from rdkit.Chem import AllChem
|
|
447
452
|
|
|
@@ -571,12 +576,12 @@ def resolve_2d_overlaps(
|
|
|
571
576
|
rep_id1, rep_id2 = i1, i2
|
|
572
577
|
break
|
|
573
578
|
|
|
574
|
-
if
|
|
579
|
+
if rep_id1 is None:
|
|
575
580
|
continue
|
|
576
581
|
|
|
577
582
|
frag1 = next((f for f in fragments if rep_id1 in f), None)
|
|
578
583
|
frag2 = next((f for f in fragments if rep_id2 in f), None)
|
|
579
|
-
if
|
|
584
|
+
if frag1 is None or frag2 is None or frag1 == frag2:
|
|
580
585
|
continue
|
|
581
586
|
|
|
582
587
|
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
|
-
|
|
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.
|
|
@@ -227,7 +234,7 @@ class MolecularData:
|
|
|
227
234
|
)
|
|
228
235
|
|
|
229
236
|
# Helper: Pick neighbors prioritizing heavy atoms
|
|
230
|
-
def pick_preferred_neighbor(atom, exclude_idx):
|
|
237
|
+
def pick_preferred_neighbor(atom: Chem.Atom, exclude_idx: int) -> Optional[int]:
|
|
231
238
|
for nbr in atom.GetNeighbors():
|
|
232
239
|
if nbr.GetIdx() == exclude_idx:
|
|
233
240
|
continue
|
|
@@ -305,7 +312,7 @@ class MolecularData:
|
|
|
305
312
|
Chem.AssignStereochemistry(final_mol, cleanIt=False, force=False)
|
|
306
313
|
return final_mol
|
|
307
314
|
|
|
308
|
-
def update_ring_info_2d(self):
|
|
315
|
+
def update_ring_info_2d(self) -> None:
|
|
309
316
|
"""Update is_in_ring and ring_center for all BondItems based on 2D topology."""
|
|
310
317
|
if not self.atoms or not self.bonds:
|
|
311
318
|
return
|
|
@@ -386,7 +393,7 @@ class MolecularData:
|
|
|
386
393
|
# so this is usually correct for 2D drawing.
|
|
387
394
|
bond_item.ring_center = ring_center
|
|
388
395
|
|
|
389
|
-
def to_mol_block(self):
|
|
396
|
+
def to_mol_block(self) -> Optional[str]:
|
|
390
397
|
mol = self.to_rdkit_mol()
|
|
391
398
|
if mol:
|
|
392
399
|
try:
|
|
@@ -449,7 +456,7 @@ class MolecularData:
|
|
|
449
456
|
mol_block += "M END\n"
|
|
450
457
|
return mol_block
|
|
451
458
|
|
|
452
|
-
def to_template_dict(self, name, version="1.0", application_version=""):
|
|
459
|
+
def to_template_dict(self, name: str, version: str = "1.0", application_version: str = "") -> Dict[str, Any]:
|
|
453
460
|
"""Convert current structure to a dictionary for template storage."""
|
|
454
461
|
import datetime
|
|
455
462
|
|
|
@@ -54,8 +54,8 @@ _MAPPINGS = {
|
|
|
54
54
|
"view_2d": "moleditpy.ui.zoomable_view", # Alias for ZoomableView
|
|
55
55
|
# Renamed UI Mixins
|
|
56
56
|
"main_window_dialog_manager": "moleditpy.ui.dialog_manager",
|
|
57
|
-
"
|
|
58
|
-
"
|
|
57
|
+
"main_window_edit_3d_logic": "moleditpy.ui.edit_3d_logic",
|
|
58
|
+
"main_window_edit_actions_logic": "moleditpy.ui.edit_actions_logic",
|
|
59
59
|
"main_window_export": "moleditpy.ui.export_logic",
|
|
60
60
|
"main_window_main_init": "moleditpy.ui.main_window_init",
|
|
61
61
|
"main_window_ui_manager": "moleditpy.ui.ui_manager",
|
|
@@ -10,6 +10,7 @@ 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 ast
|
|
14
15
|
import importlib.util
|
|
15
16
|
import logging
|
|
@@ -17,6 +18,7 @@ import os
|
|
|
17
18
|
import shutil
|
|
18
19
|
import sys
|
|
19
20
|
import zipfile
|
|
21
|
+
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
|
20
22
|
|
|
21
23
|
from PyQt6.QtCore import QUrl
|
|
22
24
|
from PyQt6.QtGui import QDesktopServices
|
|
@@ -28,35 +30,34 @@ except ImportError:
|
|
|
28
30
|
# Fallback if running as script
|
|
29
31
|
from moleditpy.plugins.plugin_interface import PluginContext
|
|
30
32
|
|
|
31
|
-
|
|
32
33
|
class PluginManager:
|
|
33
|
-
def __init__(self, main_window=None):
|
|
34
|
-
self.plugin_dir = os.path.join(os.path.expanduser("~"), ".moleditpy", "plugins")
|
|
35
|
-
self.plugins = [] # List of dicts
|
|
36
|
-
self.main_window = main_window
|
|
34
|
+
def __init__(self, main_window: Any = None) -> None:
|
|
35
|
+
self.plugin_dir: str = os.path.join(os.path.expanduser("~"), ".moleditpy", "plugins")
|
|
36
|
+
self.plugins: List[Dict[str, Any]] = [] # List of dicts
|
|
37
|
+
self.main_window: Any = main_window
|
|
37
38
|
|
|
38
39
|
# Registries for actions
|
|
39
|
-
self.menu_actions
|
|
40
|
-
self.toolbar_actions = []
|
|
41
|
-
self.drop_handlers = []
|
|
42
|
-
|
|
43
|
-
# Extended Registries
|
|
44
|
-
self.export_actions = []
|
|
45
|
-
self.optimization_methods = {}
|
|
46
|
-
self.file_openers
|
|
47
|
-
self.analysis_tools = []
|
|
48
|
-
self.save_handlers = {}
|
|
49
|
-
self.load_handlers = {}
|
|
50
|
-
self.custom_3d_styles
|
|
51
|
-
self.document_reset_handlers = []
|
|
52
|
-
|
|
53
|
-
def get_main_window(self):
|
|
40
|
+
self.menu_actions: List[Dict[str, Any]] = []
|
|
41
|
+
self.toolbar_actions: List[Dict[str, Any]] = []
|
|
42
|
+
self.drop_handlers: List[Dict[str, Any]] = []
|
|
43
|
+
|
|
44
|
+
# Extended Registries
|
|
45
|
+
self.export_actions: List[Dict[str, Any]] = []
|
|
46
|
+
self.optimization_methods: Dict[str, Dict[str, Any]] = {}
|
|
47
|
+
self.file_openers: Dict[str, List[Dict[str, Any]]] = {}
|
|
48
|
+
self.analysis_tools: List[Dict[str, Any]] = []
|
|
49
|
+
self.save_handlers: Dict[str, Callable] = {}
|
|
50
|
+
self.load_handlers: Dict[str, Callable] = {}
|
|
51
|
+
self.custom_3d_styles: Dict[str, Dict[str, Any]] = {}
|
|
52
|
+
self.document_reset_handlers: List[Dict[str, Any]] = []
|
|
53
|
+
|
|
54
|
+
def get_main_window(self) -> Any:
|
|
54
55
|
return self.main_window
|
|
55
56
|
|
|
56
|
-
def set_main_window(self, mw):
|
|
57
|
+
def set_main_window(self, mw: Any) -> None:
|
|
57
58
|
self.main_window = mw
|
|
58
59
|
|
|
59
|
-
def ensure_plugin_dir(self):
|
|
60
|
+
def ensure_plugin_dir(self) -> None:
|
|
60
61
|
"""Creates the plugin directory if it doesn't exist."""
|
|
61
62
|
if not os.path.exists(self.plugin_dir):
|
|
62
63
|
try:
|
|
@@ -64,12 +65,12 @@ class PluginManager:
|
|
|
64
65
|
except OSError as e:
|
|
65
66
|
logging.error(f"Error creating plugin directory: {e}")
|
|
66
67
|
|
|
67
|
-
def open_plugin_folder(self):
|
|
68
|
+
def open_plugin_folder(self) -> None:
|
|
68
69
|
"""Opens the plugin directory in the OS file explorer."""
|
|
69
70
|
self.ensure_plugin_dir()
|
|
70
71
|
QDesktopServices.openUrl(QUrl.fromLocalFile(self.plugin_dir))
|
|
71
72
|
|
|
72
|
-
def install_plugin(self, file_path):
|
|
73
|
+
def install_plugin(self, file_path: str) -> Tuple[bool, str]:
|
|
73
74
|
"""Copies a plugin file to the plugin directory. Supports .py and .zip."""
|
|
74
75
|
self.ensure_plugin_dir()
|
|
75
76
|
try:
|
|
@@ -169,7 +170,7 @@ class PluginManager:
|
|
|
169
170
|
) as e:
|
|
170
171
|
return False, str(e)
|
|
171
172
|
|
|
172
|
-
def discover_plugins(self, parent=None):
|
|
173
|
+
def discover_plugins(self, parent: Any = None) -> List[Dict[str, Any]]:
|
|
173
174
|
"""
|
|
174
175
|
Hybrid discovery:
|
|
175
176
|
- Folders with '__init__.py' -> Treated as single package plugin.
|
|
@@ -233,7 +234,7 @@ class PluginManager:
|
|
|
233
234
|
|
|
234
235
|
return self.plugins
|
|
235
236
|
|
|
236
|
-
def _load_single_plugin(self, filepath, module_name, category):
|
|
237
|
+
def _load_single_plugin(self, filepath: str, module_name: str, category: str) -> None:
|
|
237
238
|
"""Common loading logic for both single-file and package plugins."""
|
|
238
239
|
try:
|
|
239
240
|
# Ensure unique module name by including category path
|
|
@@ -350,7 +351,7 @@ class PluginManager:
|
|
|
350
351
|
# crashing the entire discovery process.
|
|
351
352
|
logging.error(f"Failed to load plugin {module_name}: {e}")
|
|
352
353
|
|
|
353
|
-
def run_plugin(self, module, main_window):
|
|
354
|
+
def run_plugin(self, module: Any, main_window: Any) -> None:
|
|
354
355
|
"""Executes the plugin's run method (Legacy manual trigger)."""
|
|
355
356
|
try:
|
|
356
357
|
module.run(main_window)
|
|
@@ -369,7 +370,7 @@ class PluginManager:
|
|
|
369
370
|
)
|
|
370
371
|
|
|
371
372
|
# --- Registration Callbacks ---
|
|
372
|
-
def register_menu_action(self, plugin_name, path, callback, text, icon, shortcut):
|
|
373
|
+
def register_menu_action(self, plugin_name: str, path: str, callback: Callable, text: str, icon: str, shortcut: str) -> None:
|
|
373
374
|
self.menu_actions.append(
|
|
374
375
|
{
|
|
375
376
|
"plugin": plugin_name,
|
|
@@ -381,7 +382,7 @@ class PluginManager:
|
|
|
381
382
|
}
|
|
382
383
|
)
|
|
383
384
|
|
|
384
|
-
def register_toolbar_action(self, plugin_name, callback, text, icon, tooltip):
|
|
385
|
+
def register_toolbar_action(self, plugin_name: str, callback: Callable, text: str, icon: str, tooltip: str) -> None:
|
|
385
386
|
self.toolbar_actions.append(
|
|
386
387
|
{
|
|
387
388
|
"plugin": plugin_name,
|
|
@@ -392,7 +393,7 @@ class PluginManager:
|
|
|
392
393
|
}
|
|
393
394
|
)
|
|
394
395
|
|
|
395
|
-
def register_drop_handler(self, plugin_name, callback, priority):
|
|
396
|
+
def register_drop_handler(self, plugin_name: str, callback: Callable, priority: int) -> None:
|
|
396
397
|
self.drop_handlers.append(
|
|
397
398
|
{"priority": priority, "plugin": plugin_name, "callback": callback}
|
|
398
399
|
)
|
|
@@ -453,7 +454,7 @@ class PluginManager:
|
|
|
453
454
|
{"plugin": plugin_name, "callback": callback}
|
|
454
455
|
)
|
|
455
456
|
|
|
456
|
-
def invoke_document_reset_handlers(self):
|
|
457
|
+
def invoke_document_reset_handlers(self) -> None:
|
|
457
458
|
"""Call all registered document reset handlers."""
|
|
458
459
|
for handler in self.document_reset_handlers:
|
|
459
460
|
try:
|
|
@@ -10,6 +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
|
|
14
|
+
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
|
15
|
+
|
|
13
16
|
from PyQt6.QtCore import QPointF, QRectF, Qt
|
|
14
17
|
from PyQt6.QtGui import (
|
|
15
18
|
QBrush,
|
|
@@ -38,32 +41,29 @@ except ImportError:
|
|
|
38
41
|
FONT_FAMILY,
|
|
39
42
|
FONT_WEIGHT_BOLD,
|
|
40
43
|
)
|
|
41
|
-
|
|
42
44
|
from PyQt6 import sip
|
|
43
45
|
|
|
44
|
-
|
|
45
|
-
def sip_isdeleted_safe(obj):
|
|
46
|
+
def sip_isdeleted_safe(obj: Any) -> bool:
|
|
46
47
|
try:
|
|
47
48
|
return sip.isdeleted(obj)
|
|
48
|
-
except (AttributeError, TypeError, RuntimeError):
|
|
49
|
+
except (AttributeError, TypeError, ImportError, RuntimeError):
|
|
49
50
|
# If the object does not support sip.isdeleted or is already in a state
|
|
50
51
|
# where the check fails, we assume it's unsafe or "deleted" for our purposes.
|
|
51
52
|
return True
|
|
52
53
|
|
|
53
54
|
|
|
54
55
|
class AtomItem(QGraphicsItem):
|
|
55
|
-
def __init__(self, atom_id, symbol, pos, charge=0, radical=0):
|
|
56
|
+
def __init__(self, atom_id: int, symbol: str, pos: QPointF, charge: int = 0, radical: int = 0) -> None:
|
|
56
57
|
super().__init__()
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
) = atom_id, symbol, charge, radical, [], None
|
|
58
|
+
self.atom_id: int = atom_id
|
|
59
|
+
self.symbol: str = symbol
|
|
60
|
+
self.charge: int = charge
|
|
61
|
+
self.radical: int = radical
|
|
62
|
+
self.bonds: List[Any] = []
|
|
63
|
+
self.chiral_label: Optional[str] = None
|
|
64
|
+
|
|
65
65
|
self.setPos(pos)
|
|
66
|
-
self.implicit_h_count = 0
|
|
66
|
+
self.implicit_h_count: int = 0
|
|
67
67
|
self.setFlags(
|
|
68
68
|
QGraphicsItem.GraphicsItemFlag.ItemIsMovable
|
|
69
69
|
| QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
|
|
@@ -71,13 +71,12 @@ class AtomItem(QGraphicsItem):
|
|
|
71
71
|
self.setZValue(1)
|
|
72
72
|
self.update_style()
|
|
73
73
|
self.setAcceptHoverEvents(True)
|
|
74
|
-
self.hovered = False
|
|
75
|
-
self.
|
|
76
|
-
self.
|
|
77
|
-
self.
|
|
78
|
-
self.font = QFont(FONT_FAMILY, 20, FONT_WEIGHT_BOLD)
|
|
74
|
+
self.hovered: bool = False
|
|
75
|
+
self.has_problem: bool = False
|
|
76
|
+
self.is_visible: bool = True
|
|
77
|
+
self.font: QFont = QFont(FONT_FAMILY, 20, FONT_WEIGHT_BOLD)
|
|
79
78
|
|
|
80
|
-
def update_style(self):
|
|
79
|
+
def update_style(self) -> None:
|
|
81
80
|
if sip_isdeleted_safe(self):
|
|
82
81
|
return
|
|
83
82
|
# Allow updating font preference dynamically
|
|
@@ -100,7 +99,8 @@ class AtomItem(QGraphicsItem):
|
|
|
100
99
|
)
|
|
101
100
|
self.update()
|
|
102
101
|
|
|
103
|
-
def boundingRect(self):
|
|
102
|
+
def boundingRect(self) -> QRectF:
|
|
103
|
+
"""Calculate the bounding rectangle for the atom item."""
|
|
104
104
|
# --- Calculate text position and size using logic matching paint() ---
|
|
105
105
|
# Get dynamic font size and family
|
|
106
106
|
font_size = 20
|
|
@@ -215,7 +215,8 @@ class AtomItem(QGraphicsItem):
|
|
|
215
215
|
# 3. Add final margins for selection highlights, etc.
|
|
216
216
|
return full_visual_rect.adjusted(-3, -3, 3, 3)
|
|
217
217
|
|
|
218
|
-
def shape(self):
|
|
218
|
+
def shape(self) -> QPainterPath:
|
|
219
|
+
"""Define the shape of the atom item for collision detection."""
|
|
219
220
|
scene = self.scene()
|
|
220
221
|
if not scene or not scene.views():
|
|
221
222
|
path = QPainterPath()
|
|
@@ -232,7 +233,13 @@ class AtomItem(QGraphicsItem):
|
|
|
232
233
|
path.addEllipse(QPointF(0, 0), scene_radius, scene_radius)
|
|
233
234
|
return path
|
|
234
235
|
|
|
235
|
-
def paint(
|
|
236
|
+
def paint(
|
|
237
|
+
self,
|
|
238
|
+
painter: QPainter,
|
|
239
|
+
option: Any,
|
|
240
|
+
widget: Optional[QWidget] = None,
|
|
241
|
+
) -> None:
|
|
242
|
+
"""Paint the atom symbol and its associated labels (charge, radical)."""
|
|
236
243
|
# Color logic: check if we should use bond color (uniform) or CPK (element-specific)
|
|
237
244
|
color = CPK_COLORS.get(self.symbol, CPK_COLORS["DEFAULT"])
|
|
238
245
|
# Use bond color if specified in settings
|
|
@@ -428,7 +435,7 @@ class AtomItem(QGraphicsItem):
|
|
|
428
435
|
painter.setPen(pen)
|
|
429
436
|
painter.drawRect(self.boundingRect())
|
|
430
437
|
|
|
431
|
-
def itemChange(self, change, value):
|
|
438
|
+
def itemChange(self, change: QGraphicsItem.GraphicsItemChange, value: Any) -> Any:
|
|
432
439
|
res = super().itemChange(change, value)
|
|
433
440
|
if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
|
434
441
|
if self.flags() & QGraphicsItem.GraphicsItemFlag.ItemIsMovable:
|
|
@@ -438,13 +445,13 @@ class AtomItem(QGraphicsItem):
|
|
|
438
445
|
|
|
439
446
|
return res
|
|
440
447
|
|
|
441
|
-
def hoverEnterEvent(self, event):
|
|
448
|
+
def hoverEnterEvent(self, event: Any) -> None:
|
|
442
449
|
# Enable highlight on hover regardless of scene mode
|
|
443
450
|
self.hovered = True
|
|
444
451
|
self.update()
|
|
445
452
|
super().hoverEnterEvent(event)
|
|
446
453
|
|
|
447
|
-
def hoverLeaveEvent(self, event):
|
|
454
|
+
def hoverLeaveEvent(self, event: Any) -> None:
|
|
448
455
|
if self.hovered:
|
|
449
456
|
self.hovered = False
|
|
450
457
|
self.update()
|