MoleditPy 2.5.1__tar.gz → 2.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {moleditpy-2.5.1 → moleditpy-2.6.0}/PKG-INFO +1 -1
- {moleditpy-2.5.1 → moleditpy-2.6.0}/pyproject.toml +2 -2
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/MoleditPy.egg-info/PKG-INFO +1 -1
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/MoleditPy.egg-info/SOURCES.txt +6 -5
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/__main__.py +0 -3
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/main.py +0 -3
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/__init__.py +3 -3
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/about_dialog.py +2 -2
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/align_plane_dialog.py +6 -33
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/alignment_dialog.py +3 -31
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/angle_dialog.py +10 -87
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/atom_item.py +2 -2
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/bond_item.py +2 -6
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/bond_length_dialog.py +10 -87
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/calculation_worker.py +5 -36
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/color_settings_dialog.py +6 -9
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/constants.py +1 -1
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/constrained_optimization_dialog.py +3 -14
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/custom_interactor_style.py +1 -36
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/custom_qt_interactor.py +0 -6
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/dialog3_d_picking_mixin.py +69 -5
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/dihedral_dialog.py +18 -102
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window.py +2 -67
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window_app_state.py +4 -50
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window_compute.py +24 -86
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window_dialog_manager.py +3 -63
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window_edit_3d.py +7 -98
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window_edit_actions.py +6 -97
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window_export.py +22 -65
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window_main_init.py +35 -113
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window_molecular_parsers.py +34 -96
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window_project_io.py +34 -87
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window_string_importers.py +0 -35
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window_ui_manager.py +0 -70
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window_view_3d.py +0 -87
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/main_window_view_loaders.py +0 -33
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/mirror_dialog.py +2 -2
- moleditpy-2.6.0/src/moleditpy/modules/mol_geometry.py +165 -0
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/molecule_scene.py +22 -48
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/move_group_dialog.py +10 -12
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/periodic_table_dialog.py +1 -1
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/planarize_dialog.py +7 -25
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/plugin_interface.py +0 -16
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/plugin_manager_window.py +5 -13
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/settings_dialog.py +8 -159
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/template_preview_item.py +0 -1
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/translation_dialog.py +4 -13
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/user_template_dialog.py +2 -2
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/zoomable_view.py +0 -1
- {moleditpy-2.5.1 → moleditpy-2.6.0}/LICENSE +0 -0
- {moleditpy-2.5.1 → moleditpy-2.6.0}/README.md +0 -0
- {moleditpy-2.5.1 → moleditpy-2.6.0}/setup.cfg +0 -0
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/MoleditPy.egg-info/entry_points.txt +0 -0
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/MoleditPy.egg-info/requires.txt +0 -0
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/MoleditPy.egg-info/top_level.txt +0 -0
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/__init__.py +0 -0
- {moleditpy-2.5.1/src/moleditpy/modules → moleditpy-2.6.0/src/moleditpy}/assets/file_icon.ico +0 -0
- {moleditpy-2.5.1/src/moleditpy/modules → moleditpy-2.6.0/src/moleditpy}/assets/icon.icns +0 -0
- {moleditpy-2.5.1/src/moleditpy/modules → moleditpy-2.6.0/src/moleditpy}/assets/icon.ico +0 -0
- {moleditpy-2.5.1/src/moleditpy/modules → moleditpy-2.6.0/src/moleditpy}/assets/icon.png +0 -0
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/analysis_window.py +0 -0
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/molecular_data.py +0 -0
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/plugin_manager.py +0 -0
- {moleditpy-2.5.1 → moleditpy-2.6.0}/src/moleditpy/modules/template_preview_view.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.0
|
|
4
4
|
Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
|
|
5
5
|
Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "MoleditPy"
|
|
7
7
|
|
|
8
|
-
version = "2.
|
|
8
|
+
version = "2.6.0"
|
|
9
9
|
|
|
10
10
|
license = {file = "LICENSE"}
|
|
11
11
|
|
|
@@ -48,4 +48,4 @@ Issues = "https://github.com/HiroYokoyama/python_molecular_editor/issues"
|
|
|
48
48
|
moleditpy = "moleditpy.__main__:main"
|
|
49
49
|
|
|
50
50
|
[tool.setuptools.package-data]
|
|
51
|
-
"moleditpy" = ["
|
|
51
|
+
"moleditpy" = ["assets/*"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.0
|
|
4
4
|
Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
|
|
5
5
|
Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -16,6 +16,10 @@ src/moleditpy.egg-info/dependency_links.txt
|
|
|
16
16
|
src/moleditpy.egg-info/entry_points.txt
|
|
17
17
|
src/moleditpy.egg-info/requires.txt
|
|
18
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
|
|
19
23
|
src/moleditpy/modules/__init__.py
|
|
20
24
|
src/moleditpy/modules/about_dialog.py
|
|
21
25
|
src/moleditpy/modules/align_plane_dialog.py
|
|
@@ -48,6 +52,7 @@ src/moleditpy/modules/main_window_ui_manager.py
|
|
|
48
52
|
src/moleditpy/modules/main_window_view_3d.py
|
|
49
53
|
src/moleditpy/modules/main_window_view_loaders.py
|
|
50
54
|
src/moleditpy/modules/mirror_dialog.py
|
|
55
|
+
src/moleditpy/modules/mol_geometry.py
|
|
51
56
|
src/moleditpy/modules/molecular_data.py
|
|
52
57
|
src/moleditpy/modules/molecule_scene.py
|
|
53
58
|
src/moleditpy/modules/move_group_dialog.py
|
|
@@ -61,8 +66,4 @@ src/moleditpy/modules/template_preview_item.py
|
|
|
61
66
|
src/moleditpy/modules/template_preview_view.py
|
|
62
67
|
src/moleditpy/modules/translation_dialog.py
|
|
63
68
|
src/moleditpy/modules/user_template_dialog.py
|
|
64
|
-
src/moleditpy/modules/zoomable_view.py
|
|
65
|
-
src/moleditpy/modules/assets/file_icon.ico
|
|
66
|
-
src/moleditpy/modules/assets/icon.icns
|
|
67
|
-
src/moleditpy/modules/assets/icon.ico
|
|
68
|
-
src/moleditpy/modules/assets/icon.png
|
|
69
|
+
src/moleditpy/modules/zoomable_view.py
|
|
@@ -16,11 +16,8 @@ print("MoleditPy — A Python-based molecular editing software")
|
|
|
16
16
|
print("-----------------------------------------------------\n")
|
|
17
17
|
|
|
18
18
|
try:
|
|
19
|
-
# Preferred when running as a package: python -m moleditpy
|
|
20
19
|
from .main import main
|
|
21
20
|
except Exception:
|
|
22
|
-
# Fallback when running the file directly: python __main__.py
|
|
23
|
-
# This will import the top-level `main` module in the same folder.
|
|
24
21
|
from main import main
|
|
25
22
|
|
|
26
23
|
# --- Application Execution ---
|
|
@@ -17,11 +17,8 @@ import ctypes
|
|
|
17
17
|
from PyQt6.QtWidgets import QApplication
|
|
18
18
|
|
|
19
19
|
try:
|
|
20
|
-
# When executed as part of the package (python -m moleditpy)
|
|
21
20
|
from .modules.main_window import MainWindow
|
|
22
21
|
except Exception:
|
|
23
|
-
# When executed as a standalone script (python main.py) the package-relative
|
|
24
|
-
# import won't work; fall back to absolute import that works with sys.path
|
|
25
22
|
from modules.main_window import MainWindow
|
|
26
23
|
|
|
27
24
|
def main():
|
|
@@ -15,7 +15,7 @@ DOI: 10.5281/zenodo.17268532
|
|
|
15
15
|
# Do not import `pybel` at module import time. Only expose the presence
|
|
16
16
|
# of Open Babel via `OBABEL_AVAILABLE`. Modules should import `pybel`
|
|
17
17
|
# lazily if and when they need it.
|
|
18
|
-
try:
|
|
18
|
+
try: # pragma: no cover
|
|
19
19
|
import importlib.util
|
|
20
20
|
OBABEL_AVAILABLE = importlib.util.find_spec("openbabel") is not None
|
|
21
21
|
except Exception:
|
|
@@ -26,7 +26,7 @@ except Exception:
|
|
|
26
26
|
# it once at module import time and expose a small, robust wrapper so callers
|
|
27
27
|
# can avoid re-importing sip repeatedly and so we centralize exception
|
|
28
28
|
# handling (this reduces crash risk during teardown and deletion operations).
|
|
29
|
-
try:
|
|
29
|
+
try: # pragma: no cover
|
|
30
30
|
import sip as _sip # type: ignore
|
|
31
31
|
_sip_isdeleted = getattr(_sip, 'isdeleted', None)
|
|
32
32
|
except Exception:
|
|
@@ -40,7 +40,7 @@ def sip_isdeleted_safe(obj) -> bool:
|
|
|
40
40
|
occurs while checking, it returns False (i.e. not deleted) so that the
|
|
41
41
|
caller can continue other lightweight guards (like checking scene()).
|
|
42
42
|
"""
|
|
43
|
-
try:
|
|
43
|
+
try: # pragma: no cover
|
|
44
44
|
if _sip_isdeleted is None:
|
|
45
45
|
return False
|
|
46
46
|
return bool(_sip_isdeleted(obj))
|
|
@@ -21,7 +21,7 @@ try:
|
|
|
21
21
|
except Exception:
|
|
22
22
|
from modules.constants import VERSION
|
|
23
23
|
|
|
24
|
-
class AboutDialog(QDialog):
|
|
24
|
+
class AboutDialog(QDialog): # pragma: no cover
|
|
25
25
|
def __init__(self, main_window, parent=None):
|
|
26
26
|
super().__init__(parent)
|
|
27
27
|
self.main_window = main_window
|
|
@@ -37,7 +37,7 @@ class AboutDialog(QDialog):
|
|
|
37
37
|
self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
38
38
|
|
|
39
39
|
# Load the original icon image
|
|
40
|
-
icon_path = os.path.join(os.path.dirname(__file__), 'assets', 'icon.png')
|
|
40
|
+
icon_path = os.path.join(os.path.dirname(__file__), '..', 'assets', 'icon.png')
|
|
41
41
|
if os.path.exists(icon_path):
|
|
42
42
|
original_pixmap = QPixmap(icon_path)
|
|
43
43
|
# Scale to 2x size (160x160)
|
|
@@ -21,7 +21,7 @@ try:
|
|
|
21
21
|
except Exception:
|
|
22
22
|
from modules.dialog3_d_picking_mixin import Dialog3DPickingMixin
|
|
23
23
|
|
|
24
|
-
class AlignPlaneDialog(Dialog3DPickingMixin, QDialog):
|
|
24
|
+
class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
25
25
|
def __init__(self, mol, main_window, plane, preselected_atoms=None, parent=None):
|
|
26
26
|
QDialog.__init__(self, parent)
|
|
27
27
|
Dialog3DPickingMixin.__init__(self)
|
|
@@ -44,7 +44,7 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog):
|
|
|
44
44
|
def init_ui(self):
|
|
45
45
|
plane_names = {'xy': 'XY', 'xz': 'XZ', 'yz': 'YZ'}
|
|
46
46
|
self.setWindowTitle(f"Align to {plane_names[self.plane]} Plane")
|
|
47
|
-
self.setModal(False)
|
|
47
|
+
self.setModal(False)
|
|
48
48
|
layout = QVBoxLayout(self)
|
|
49
49
|
|
|
50
50
|
# Instructions
|
|
@@ -163,39 +163,12 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog):
|
|
|
163
163
|
|
|
164
164
|
def show_atom_labels(self):
|
|
165
165
|
"""選択された原子にラベルを表示"""
|
|
166
|
-
# 既存のラベルをクリア
|
|
167
|
-
self.clear_atom_labels()
|
|
168
|
-
|
|
169
|
-
# 新しいラベルを表示
|
|
170
|
-
if not hasattr(self, 'selection_labels'):
|
|
171
|
-
self.selection_labels = []
|
|
172
|
-
|
|
173
166
|
if self.selected_atoms:
|
|
174
167
|
sorted_atoms = sorted(self.selected_atoms)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
# ラベルを追加
|
|
181
|
-
label_actor = self.main_window.plotter.add_point_labels(
|
|
182
|
-
[pos], [label_text],
|
|
183
|
-
point_size=20,
|
|
184
|
-
font_size=12,
|
|
185
|
-
text_color='blue',
|
|
186
|
-
always_visible=True
|
|
187
|
-
)
|
|
188
|
-
self.selection_labels.append(label_actor)
|
|
189
|
-
|
|
190
|
-
def clear_atom_labels(self):
|
|
191
|
-
"""原子ラベルをクリア"""
|
|
192
|
-
if hasattr(self, 'selection_labels'):
|
|
193
|
-
for label_actor in self.selection_labels:
|
|
194
|
-
try:
|
|
195
|
-
self.main_window.plotter.remove_actor(label_actor)
|
|
196
|
-
except Exception:
|
|
197
|
-
pass
|
|
198
|
-
self.selection_labels = []
|
|
168
|
+
pairs = [(idx, f"#{i+1}") for i, idx in enumerate(sorted_atoms)]
|
|
169
|
+
self.show_atom_labels_for(pairs, color='blue')
|
|
170
|
+
else:
|
|
171
|
+
self.clear_atom_labels()
|
|
199
172
|
|
|
200
173
|
def apply_PlaneAlign(self):
|
|
201
174
|
"""alignを適用(回転ベース)"""
|
|
@@ -13,7 +13,7 @@ DOI: 10.5281/zenodo.17268532
|
|
|
13
13
|
from PyQt6.QtWidgets import (
|
|
14
14
|
QDialog, QVBoxLayout, QLabel, QHBoxLayout, QPushButton
|
|
15
15
|
)
|
|
16
|
-
|
|
16
|
+
|
|
17
17
|
from PyQt6.QtWidgets import QMessageBox
|
|
18
18
|
import numpy as np
|
|
19
19
|
|
|
@@ -22,7 +22,7 @@ try:
|
|
|
22
22
|
except Exception:
|
|
23
23
|
from modules.dialog3_d_picking_mixin import Dialog3DPickingMixin
|
|
24
24
|
|
|
25
|
-
class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
25
|
+
class AlignmentDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
26
26
|
def __init__(self, mol, main_window, axis, preselected_atoms=None, parent=None):
|
|
27
27
|
QDialog.__init__(self, parent)
|
|
28
28
|
Dialog3DPickingMixin.__init__(self)
|
|
@@ -46,7 +46,7 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
|
46
46
|
def init_ui(self):
|
|
47
47
|
axis_names = {'x': 'X-axis', 'y': 'Y-axis', 'z': 'Z-axis'}
|
|
48
48
|
self.setWindowTitle(f"Align to {axis_names[self.axis]}")
|
|
49
|
-
self.setModal(False)
|
|
49
|
+
self.setModal(False)
|
|
50
50
|
layout = QVBoxLayout(self)
|
|
51
51
|
|
|
52
52
|
# Instructions
|
|
@@ -133,24 +133,6 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
|
133
133
|
self.selected_atoms.clear()
|
|
134
134
|
self.update_display()
|
|
135
135
|
|
|
136
|
-
def add_selection_label(self, atom_idx, label_text):
|
|
137
|
-
"""選択された原子にラベルを追加"""
|
|
138
|
-
if not hasattr(self, 'selection_labels'):
|
|
139
|
-
self.selection_labels = []
|
|
140
|
-
|
|
141
|
-
# 原子の位置を取得
|
|
142
|
-
pos = self.main_window.atom_positions_3d[atom_idx]
|
|
143
|
-
|
|
144
|
-
# ラベルを追加
|
|
145
|
-
label_actor = self.main_window.plotter.add_point_labels(
|
|
146
|
-
[pos], [label_text],
|
|
147
|
-
point_size=20,
|
|
148
|
-
font_size=12,
|
|
149
|
-
text_color='yellow',
|
|
150
|
-
always_visible=True
|
|
151
|
-
)
|
|
152
|
-
self.selection_labels.append(label_actor)
|
|
153
|
-
|
|
154
136
|
def remove_atom_label(self, atom_idx):
|
|
155
137
|
"""特定の原子のラベルを削除"""
|
|
156
138
|
# 簡単化のため、全ラベルをクリアして再描画
|
|
@@ -159,16 +141,6 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
|
159
141
|
if idx != atom_idx:
|
|
160
142
|
self.add_selection_label(idx, f"Atom {i}")
|
|
161
143
|
|
|
162
|
-
def clear_selection_labels(self):
|
|
163
|
-
"""選択ラベルをクリア"""
|
|
164
|
-
if hasattr(self, 'selection_labels'):
|
|
165
|
-
for label_actor in self.selection_labels:
|
|
166
|
-
try:
|
|
167
|
-
self.main_window.plotter.remove_actor(label_actor)
|
|
168
|
-
except Exception:
|
|
169
|
-
pass
|
|
170
|
-
self.selection_labels = []
|
|
171
|
-
|
|
172
144
|
def apply_alignment(self):
|
|
173
145
|
"""アライメントを適用"""
|
|
174
146
|
if len(self.selected_atoms) != 2:
|
|
@@ -16,15 +16,17 @@ from PyQt6.QtWidgets import (
|
|
|
16
16
|
|
|
17
17
|
try:
|
|
18
18
|
from .dialog3_d_picking_mixin import Dialog3DPickingMixin
|
|
19
|
+
from .mol_geometry import get_connected_group
|
|
19
20
|
except Exception:
|
|
20
21
|
from modules.dialog3_d_picking_mixin import Dialog3DPickingMixin
|
|
22
|
+
from modules.mol_geometry import get_connected_group
|
|
21
23
|
|
|
22
24
|
from PyQt6.QtCore import Qt
|
|
23
25
|
from PyQt6.QtWidgets import QMessageBox
|
|
24
26
|
import numpy as np
|
|
25
27
|
|
|
26
28
|
|
|
27
|
-
class AngleDialog(Dialog3DPickingMixin, QDialog):
|
|
29
|
+
class AngleDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
28
30
|
def __init__(self, mol, main_window, preselected_atoms=None, parent=None):
|
|
29
31
|
QDialog.__init__(self, parent)
|
|
30
32
|
Dialog3DPickingMixin.__init__(self)
|
|
@@ -44,8 +46,7 @@ class AngleDialog(Dialog3DPickingMixin, QDialog):
|
|
|
44
46
|
|
|
45
47
|
def init_ui(self):
|
|
46
48
|
self.setWindowTitle("Adjust Angle")
|
|
47
|
-
self.setModal(False)
|
|
48
|
-
# 常に前面表示
|
|
49
|
+
self.setModal(False)
|
|
49
50
|
layout = QVBoxLayout(self)
|
|
50
51
|
|
|
51
52
|
# Instructions
|
|
@@ -170,69 +171,10 @@ class AngleDialog(Dialog3DPickingMixin, QDialog):
|
|
|
170
171
|
|
|
171
172
|
def show_atom_labels(self):
|
|
172
173
|
"""選択された原子にラベルを表示"""
|
|
173
|
-
# 既存のラベルをクリア
|
|
174
|
-
self.clear_atom_labels()
|
|
175
|
-
|
|
176
|
-
# 新しいラベルを表示
|
|
177
|
-
if not hasattr(self, 'selection_labels'):
|
|
178
|
-
self.selection_labels = []
|
|
179
|
-
|
|
180
174
|
selected_atoms = [self.atom1_idx, self.atom2_idx, self.atom3_idx]
|
|
181
175
|
labels = ["1st", "2nd (vertex)", "3rd"]
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
for i, atom_idx in enumerate(selected_atoms):
|
|
185
|
-
if atom_idx is not None:
|
|
186
|
-
pos = self.main_window.atom_positions_3d[atom_idx]
|
|
187
|
-
label_text = f"{labels[i]}"
|
|
188
|
-
|
|
189
|
-
# ラベルを追加
|
|
190
|
-
label_actor = self.main_window.plotter.add_point_labels(
|
|
191
|
-
[pos], [label_text],
|
|
192
|
-
point_size=20,
|
|
193
|
-
font_size=12,
|
|
194
|
-
text_color=colors[i],
|
|
195
|
-
always_visible=True
|
|
196
|
-
)
|
|
197
|
-
self.selection_labels.append(label_actor)
|
|
198
|
-
|
|
199
|
-
def clear_atom_labels(self):
|
|
200
|
-
"""原子ラベルをクリア"""
|
|
201
|
-
if hasattr(self, 'selection_labels'):
|
|
202
|
-
for label_actor in self.selection_labels:
|
|
203
|
-
try:
|
|
204
|
-
self.main_window.plotter.remove_actor(label_actor)
|
|
205
|
-
except Exception:
|
|
206
|
-
pass
|
|
207
|
-
self.selection_labels = []
|
|
208
|
-
|
|
209
|
-
def clear_selection_labels(self):
|
|
210
|
-
"""選択ラベルをクリア"""
|
|
211
|
-
if hasattr(self, 'selection_labels'):
|
|
212
|
-
for label_actor in self.selection_labels:
|
|
213
|
-
try:
|
|
214
|
-
self.main_window.plotter.remove_actor(label_actor)
|
|
215
|
-
except Exception:
|
|
216
|
-
pass
|
|
217
|
-
self.selection_labels = []
|
|
218
|
-
|
|
219
|
-
def add_selection_label(self, atom_idx, label_text):
|
|
220
|
-
"""選択された原子にラベルを追加"""
|
|
221
|
-
if not hasattr(self, 'selection_labels'):
|
|
222
|
-
self.selection_labels = []
|
|
223
|
-
|
|
224
|
-
# 原子の位置を取得
|
|
225
|
-
pos = self.main_window.atom_positions_3d[atom_idx]
|
|
226
|
-
|
|
227
|
-
# ラベルを追加
|
|
228
|
-
label_actor = self.main_window.plotter.add_point_labels(
|
|
229
|
-
[pos], [label_text],
|
|
230
|
-
point_size=20,
|
|
231
|
-
font_size=12,
|
|
232
|
-
text_color='yellow',
|
|
233
|
-
always_visible=True
|
|
234
|
-
)
|
|
235
|
-
self.selection_labels.append(label_actor)
|
|
176
|
+
pairs = [(idx, labels[i]) for i, idx in enumerate(selected_atoms) if idx is not None]
|
|
177
|
+
self.show_atom_labels_for(pairs)
|
|
236
178
|
|
|
237
179
|
def update_display(self):
|
|
238
180
|
"""表示を更新"""
|
|
@@ -373,8 +315,8 @@ class AngleDialog(Dialog3DPickingMixin, QDialog):
|
|
|
373
315
|
half_rotation = total_rotation_angle / 2
|
|
374
316
|
|
|
375
317
|
# Get both connected groups
|
|
376
|
-
group1_atoms =
|
|
377
|
-
group3_atoms =
|
|
318
|
+
group1_atoms = get_connected_group(self.mol, self.atom1_idx, exclude=self.atom2_idx)
|
|
319
|
+
group3_atoms = get_connected_group(self.mol, self.atom3_idx, exclude=self.atom2_idx)
|
|
378
320
|
|
|
379
321
|
# Rotate group 1 by -half_rotation
|
|
380
322
|
for atom_idx in group1_atoms:
|
|
@@ -402,7 +344,7 @@ class AngleDialog(Dialog3DPickingMixin, QDialog):
|
|
|
402
344
|
self.main_window.atom_positions_3d[self.atom3_idx] = new_pos3
|
|
403
345
|
else:
|
|
404
346
|
# Rotate the connected group around atom2 (vertex) - default behavior
|
|
405
|
-
atoms_to_move =
|
|
347
|
+
atoms_to_move = get_connected_group(self.mol, self.atom3_idx, exclude=self.atom2_idx)
|
|
406
348
|
|
|
407
349
|
for atom_idx in atoms_to_move:
|
|
408
350
|
current_pos = np.array(conf.GetAtomPosition(atom_idx))
|
|
@@ -418,23 +360,4 @@ class AngleDialog(Dialog3DPickingMixin, QDialog):
|
|
|
418
360
|
# Update the 3D view
|
|
419
361
|
self.main_window.draw_molecule_3d(self.mol)
|
|
420
362
|
|
|
421
|
-
|
|
422
|
-
"""指定された原子から連結されているグループを取得"""
|
|
423
|
-
visited = set()
|
|
424
|
-
to_visit = [start_atom]
|
|
425
|
-
|
|
426
|
-
while to_visit:
|
|
427
|
-
current = to_visit.pop()
|
|
428
|
-
if current in visited or current == exclude:
|
|
429
|
-
continue
|
|
430
|
-
|
|
431
|
-
visited.add(current)
|
|
432
|
-
|
|
433
|
-
# Get neighboring atoms
|
|
434
|
-
atom = self.mol.GetAtomWithIdx(current)
|
|
435
|
-
for bond in atom.GetBonds():
|
|
436
|
-
other_idx = bond.GetOtherAtomIdx(current)
|
|
437
|
-
if other_idx not in visited and other_idx != exclude:
|
|
438
|
-
to_visit.append(other_idx)
|
|
439
|
-
|
|
440
|
-
return visited
|
|
363
|
+
|
|
@@ -23,13 +23,13 @@ from PyQt6.QtCore import (
|
|
|
23
23
|
try:
|
|
24
24
|
from .constants import (
|
|
25
25
|
ATOM_RADIUS, DESIRED_ATOM_PIXEL_RADIUS,
|
|
26
|
-
FONT_FAMILY,
|
|
26
|
+
FONT_FAMILY, FONT_WEIGHT_BOLD,
|
|
27
27
|
CPK_COLORS,
|
|
28
28
|
)
|
|
29
29
|
except Exception:
|
|
30
30
|
from modules.constants import (
|
|
31
31
|
ATOM_RADIUS, DESIRED_ATOM_PIXEL_RADIUS,
|
|
32
|
-
FONT_FAMILY,
|
|
32
|
+
FONT_FAMILY, FONT_WEIGHT_BOLD,
|
|
33
33
|
CPK_COLORS,
|
|
34
34
|
)
|
|
35
35
|
|
|
@@ -24,13 +24,13 @@ from PyQt6.QtCore import (
|
|
|
24
24
|
try:
|
|
25
25
|
from .constants import (
|
|
26
26
|
EZ_LABEL_BOX_SIZE, EZ_LABEL_TEXT_OUTLINE, EZ_LABEL_MARGIN,
|
|
27
|
-
|
|
27
|
+
FONT_FAMILY, FONT_WEIGHT_BOLD,
|
|
28
28
|
HOVER_PEN_WIDTH, DESIRED_BOND_PIXEL_WIDTH,
|
|
29
29
|
)
|
|
30
30
|
except Exception:
|
|
31
31
|
from modules.constants import (
|
|
32
32
|
EZ_LABEL_BOX_SIZE, EZ_LABEL_TEXT_OUTLINE, EZ_LABEL_MARGIN,
|
|
33
|
-
|
|
33
|
+
FONT_FAMILY, FONT_WEIGHT_BOLD,
|
|
34
34
|
HOVER_PEN_WIDTH, DESIRED_BOND_PIXEL_WIDTH,
|
|
35
35
|
)
|
|
36
36
|
|
|
@@ -206,9 +206,6 @@ class BondItem(QGraphicsItem):
|
|
|
206
206
|
except Exception:
|
|
207
207
|
return None
|
|
208
208
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
209
|
def paint(self, painter, option, widget):
|
|
213
210
|
if self.atom1 is None or self.atom2 is None:
|
|
214
211
|
return
|
|
@@ -341,7 +338,6 @@ class BondItem(QGraphicsItem):
|
|
|
341
338
|
if bond and bond.IsInRing():
|
|
342
339
|
is_in_ring = True
|
|
343
340
|
# 環の中心を計算(この結合を含む最小環)
|
|
344
|
-
from rdkit import Chem
|
|
345
341
|
ring_info = mol.GetRingInfo()
|
|
346
342
|
for ring in ring_info.AtomRings():
|
|
347
343
|
if rdkit_idx1 in ring and rdkit_idx2 in ring:
|
|
@@ -15,11 +15,12 @@ from PyQt6.QtWidgets import (
|
|
|
15
15
|
)
|
|
16
16
|
|
|
17
17
|
from .dialog3_d_picking_mixin import Dialog3DPickingMixin
|
|
18
|
+
from .mol_geometry import get_connected_group
|
|
18
19
|
|
|
19
20
|
from PyQt6.QtCore import Qt
|
|
20
21
|
import numpy as np
|
|
21
22
|
|
|
22
|
-
class BondLengthDialog(Dialog3DPickingMixin, QDialog):
|
|
23
|
+
class BondLengthDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
23
24
|
def __init__(self, mol, main_window, preselected_atoms=None, parent=None):
|
|
24
25
|
QDialog.__init__(self, parent)
|
|
25
26
|
Dialog3DPickingMixin.__init__(self)
|
|
@@ -37,8 +38,8 @@ class BondLengthDialog(Dialog3DPickingMixin, QDialog):
|
|
|
37
38
|
|
|
38
39
|
def init_ui(self):
|
|
39
40
|
self.setWindowTitle("Adjust Bond Length")
|
|
40
|
-
self.setModal(False)
|
|
41
|
-
|
|
41
|
+
self.setModal(False)
|
|
42
|
+
|
|
42
43
|
layout = QVBoxLayout(self)
|
|
43
44
|
|
|
44
45
|
# Instructions
|
|
@@ -158,69 +159,10 @@ class BondLengthDialog(Dialog3DPickingMixin, QDialog):
|
|
|
158
159
|
|
|
159
160
|
def show_atom_labels(self):
|
|
160
161
|
"""選択された原子にラベルを表示"""
|
|
161
|
-
# 既存のラベルをクリア
|
|
162
|
-
self.clear_atom_labels()
|
|
163
|
-
|
|
164
|
-
# 新しいラベルを表示
|
|
165
|
-
if not hasattr(self, 'selection_labels'):
|
|
166
|
-
self.selection_labels = []
|
|
167
|
-
|
|
168
162
|
selected_atoms = [self.atom1_idx, self.atom2_idx]
|
|
169
163
|
labels = ["1st", "2nd"]
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
for i, atom_idx in enumerate(selected_atoms):
|
|
173
|
-
if atom_idx is not None:
|
|
174
|
-
pos = self.main_window.atom_positions_3d[atom_idx]
|
|
175
|
-
label_text = f"{labels[i]}"
|
|
176
|
-
|
|
177
|
-
# ラベルを追加
|
|
178
|
-
label_actor = self.main_window.plotter.add_point_labels(
|
|
179
|
-
[pos], [label_text],
|
|
180
|
-
point_size=20,
|
|
181
|
-
font_size=12,
|
|
182
|
-
text_color=colors[i],
|
|
183
|
-
always_visible=True
|
|
184
|
-
)
|
|
185
|
-
self.selection_labels.append(label_actor)
|
|
186
|
-
|
|
187
|
-
def clear_atom_labels(self):
|
|
188
|
-
"""原子ラベルをクリア"""
|
|
189
|
-
if hasattr(self, 'selection_labels'):
|
|
190
|
-
for label_actor in self.selection_labels:
|
|
191
|
-
try:
|
|
192
|
-
self.main_window.plotter.remove_actor(label_actor)
|
|
193
|
-
except Exception:
|
|
194
|
-
pass
|
|
195
|
-
self.selection_labels = []
|
|
196
|
-
|
|
197
|
-
def clear_selection_labels(self):
|
|
198
|
-
"""選択ラベルをクリア"""
|
|
199
|
-
if hasattr(self, 'selection_labels'):
|
|
200
|
-
for label_actor in self.selection_labels:
|
|
201
|
-
try:
|
|
202
|
-
self.main_window.plotter.remove_actor(label_actor)
|
|
203
|
-
except Exception:
|
|
204
|
-
pass
|
|
205
|
-
self.selection_labels = []
|
|
206
|
-
|
|
207
|
-
def add_selection_label(self, atom_idx, label_text):
|
|
208
|
-
"""選択された原子にラベルを追加"""
|
|
209
|
-
if not hasattr(self, 'selection_labels'):
|
|
210
|
-
self.selection_labels = []
|
|
211
|
-
|
|
212
|
-
# 原子の位置を取得
|
|
213
|
-
pos = self.main_window.atom_positions_3d[atom_idx]
|
|
214
|
-
|
|
215
|
-
# ラベルを追加
|
|
216
|
-
label_actor = self.main_window.plotter.add_point_labels(
|
|
217
|
-
[pos], [label_text],
|
|
218
|
-
point_size=20,
|
|
219
|
-
font_size=12,
|
|
220
|
-
text_color='yellow',
|
|
221
|
-
always_visible=True
|
|
222
|
-
)
|
|
223
|
-
self.selection_labels.append(label_actor)
|
|
164
|
+
pairs = [(idx, labels[i]) for i, idx in enumerate(selected_atoms) if idx is not None]
|
|
165
|
+
self.show_atom_labels_for(pairs)
|
|
224
166
|
|
|
225
167
|
def update_display(self):
|
|
226
168
|
"""表示を更新"""
|
|
@@ -317,8 +259,8 @@ class BondLengthDialog(Dialog3DPickingMixin, QDialog):
|
|
|
317
259
|
new_pos2 = bond_center + direction * half_distance
|
|
318
260
|
|
|
319
261
|
# Get both connected groups
|
|
320
|
-
group1_atoms =
|
|
321
|
-
group2_atoms =
|
|
262
|
+
group1_atoms = get_connected_group(self.mol, self.atom1_idx, exclude=self.atom2_idx)
|
|
263
|
+
group2_atoms = get_connected_group(self.mol, self.atom2_idx, exclude=self.atom1_idx)
|
|
322
264
|
|
|
323
265
|
# Calculate displacements
|
|
324
266
|
displacement1 = new_pos1 - pos1
|
|
@@ -346,7 +288,7 @@ class BondLengthDialog(Dialog3DPickingMixin, QDialog):
|
|
|
346
288
|
else:
|
|
347
289
|
# Move the connected group (default behavior)
|
|
348
290
|
new_pos2 = pos1 + direction * new_distance
|
|
349
|
-
atoms_to_move =
|
|
291
|
+
atoms_to_move = get_connected_group(self.mol, self.atom2_idx, exclude=self.atom1_idx)
|
|
350
292
|
displacement = new_pos2 - pos2
|
|
351
293
|
|
|
352
294
|
for atom_idx in atoms_to_move:
|
|
@@ -358,23 +300,4 @@ class BondLengthDialog(Dialog3DPickingMixin, QDialog):
|
|
|
358
300
|
# Update the 3D view
|
|
359
301
|
self.main_window.draw_molecule_3d(self.mol)
|
|
360
302
|
|
|
361
|
-
|
|
362
|
-
"""指定された原子から連結されているグループを取得"""
|
|
363
|
-
visited = set()
|
|
364
|
-
to_visit = [start_atom]
|
|
365
|
-
|
|
366
|
-
while to_visit:
|
|
367
|
-
current = to_visit.pop()
|
|
368
|
-
if current in visited or current == exclude:
|
|
369
|
-
continue
|
|
370
|
-
|
|
371
|
-
visited.add(current)
|
|
372
|
-
|
|
373
|
-
# Get neighboring atoms
|
|
374
|
-
atom = self.mol.GetAtomWithIdx(current)
|
|
375
|
-
for bond in atom.GetBonds():
|
|
376
|
-
other_idx = bond.GetOtherAtomIdx(current)
|
|
377
|
-
if other_idx not in visited and other_idx != exclude:
|
|
378
|
-
to_visit.append(other_idx)
|
|
379
|
-
|
|
380
|
-
return visited
|
|
303
|
+
|