moleditpy 2.6.0__tar.gz → 2.6.2__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.6.0 → moleditpy-2.6.2}/PKG-INFO +1 -1
- {moleditpy-2.6.0 → moleditpy-2.6.2}/pyproject.toml +1 -1
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/MoleditPy.egg-info/PKG-INFO +1 -1
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/MoleditPy.egg-info/SOURCES.txt +1 -0
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/__init__.py +0 -2
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/__main__.py +1 -3
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/main.py +4 -4
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/__init__.py +8 -16
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/about_dialog.py +29 -21
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/align_plane_dialog.py +83 -50
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/alignment_dialog.py +77 -58
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/analysis_window.py +50 -34
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/angle_dialog.py +126 -82
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/atom_item.py +163 -104
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/bond_item.py +217 -129
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/bond_length_dialog.py +97 -68
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/calculation_worker.py +367 -213
- moleditpy-2.6.2/src/moleditpy/modules/color_settings_dialog.py +524 -0
- moleditpy-2.6.2/src/moleditpy/modules/constants.py +170 -0
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/constrained_optimization_dialog.py +231 -133
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/custom_interactor_style.py +295 -183
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/custom_qt_interactor.py +11 -10
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/dialog3_d_picking_mixin.py +64 -49
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/dihedral_dialog.py +161 -95
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window.py +50 -35
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window_app_state.py +303 -199
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window_compute.py +549 -389
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window_dialog_manager.py +215 -142
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window_edit_3d.py +126 -118
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window_edit_actions.py +405 -299
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window_export.py +373 -264
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window_main_init.py +947 -687
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window_molecular_parsers.py +405 -252
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window_project_io.py +168 -140
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window_string_importers.py +71 -55
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window_ui_manager.py +162 -139
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window_view_3d.py +715 -419
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window_view_loaders.py +96 -71
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/mirror_dialog.py +47 -30
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/mol_geometry.py +10 -9
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/molecular_data.py +85 -53
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/molecule_scene.py +790 -523
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/move_group_dialog.py +260 -183
- moleditpy-2.6.2/src/moleditpy/modules/periodic_table_dialog.py +196 -0
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/planarize_dialog.py +57 -23
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/plugin_interface.py +64 -33
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/plugin_manager.py +239 -194
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/plugin_manager_window.py +131 -84
- moleditpy-2.6.2/src/moleditpy/modules/settings_dialog.py +1861 -0
- moleditpy-2.6.2/src/moleditpy/modules/sip_isdeleted_safe.py +17 -0
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/template_preview_item.py +50 -37
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/template_preview_view.py +29 -14
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/translation_dialog.py +97 -61
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/user_template_dialog.py +312 -227
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/zoomable_view.py +24 -26
- moleditpy-2.6.0/src/moleditpy/modules/color_settings_dialog.py +0 -318
- moleditpy-2.6.0/src/moleditpy/modules/constants.py +0 -88
- moleditpy-2.6.0/src/moleditpy/modules/periodic_table_dialog.py +0 -84
- moleditpy-2.6.0/src/moleditpy/modules/settings_dialog.py +0 -1402
- {moleditpy-2.6.0 → moleditpy-2.6.2}/LICENSE +0 -0
- {moleditpy-2.6.0 → moleditpy-2.6.2}/README.md +0 -0
- {moleditpy-2.6.0 → moleditpy-2.6.2}/setup.cfg +0 -0
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/MoleditPy.egg-info/entry_points.txt +0 -0
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/MoleditPy.egg-info/requires.txt +0 -0
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/MoleditPy.egg-info/top_level.txt +0 -0
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/assets/file_icon.ico +0 -0
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/assets/icon.icns +0 -0
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/assets/icon.ico +0 -0
- {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/assets/icon.png +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 2.6.
|
|
3
|
+
Version: 2.6.2
|
|
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: 2.6.
|
|
3
|
+
Version: 2.6.2
|
|
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
|
|
@@ -62,6 +62,7 @@ src/moleditpy/modules/plugin_interface.py
|
|
|
62
62
|
src/moleditpy/modules/plugin_manager.py
|
|
63
63
|
src/moleditpy/modules/plugin_manager_window.py
|
|
64
64
|
src/moleditpy/modules/settings_dialog.py
|
|
65
|
+
src/moleditpy/modules/sip_isdeleted_safe.py
|
|
65
66
|
src/moleditpy/modules/template_preview_item.py
|
|
66
67
|
src/moleditpy/modules/template_preview_view.py
|
|
67
68
|
src/moleditpy/modules/translation_dialog.py
|
|
@@ -10,7 +10,6 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
|
|
|
10
10
|
DOI: 10.5281/zenodo.17268532
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
|
|
14
13
|
print("-----------------------------------------------------")
|
|
15
14
|
print("MoleditPy — A Python-based molecular editing software")
|
|
16
15
|
print("-----------------------------------------------------\n")
|
|
@@ -21,6 +20,5 @@ except Exception:
|
|
|
21
20
|
from main import main
|
|
22
21
|
|
|
23
22
|
# --- Application Execution ---
|
|
24
|
-
if __name__ ==
|
|
23
|
+
if __name__ == "__main__":
|
|
25
24
|
main()
|
|
26
|
-
|
|
@@ -10,9 +10,8 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
|
|
|
10
10
|
DOI: 10.5281/zenodo.17268532
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
import sys
|
|
15
13
|
import ctypes
|
|
14
|
+
import sys
|
|
16
15
|
|
|
17
16
|
from PyQt6.QtWidgets import QApplication
|
|
18
17
|
|
|
@@ -21,10 +20,11 @@ try:
|
|
|
21
20
|
except Exception:
|
|
22
21
|
from modules.main_window import MainWindow
|
|
23
22
|
|
|
23
|
+
|
|
24
24
|
def main():
|
|
25
25
|
# --- Windows タスクバーアイコンのための追加処理 ---
|
|
26
|
-
if sys.platform ==
|
|
27
|
-
myappid =
|
|
26
|
+
if sys.platform == "win32":
|
|
27
|
+
myappid = "hyoko.moleditpy.1.0" # アプリケーション固有のID(任意)
|
|
28
28
|
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
|
|
29
29
|
|
|
30
30
|
app = QApplication(sys.argv)
|
|
@@ -10,29 +10,22 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
|
|
|
10
10
|
DOI: 10.5281/zenodo.17268532
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
# Open Babel Python binding (optional; required for fallback)
|
|
15
|
-
# Do not import `pybel` at module import time. Only expose the presence
|
|
16
|
-
# of Open Babel via `OBABEL_AVAILABLE`. Modules should import `pybel`
|
|
17
|
-
# lazily if and when they need it.
|
|
18
|
-
try: # pragma: no cover
|
|
13
|
+
try:
|
|
19
14
|
import importlib.util
|
|
15
|
+
|
|
20
16
|
OBABEL_AVAILABLE = importlib.util.find_spec("openbabel") is not None
|
|
21
17
|
except Exception:
|
|
22
18
|
OBABEL_AVAILABLE = False
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
# handling (this reduces crash risk during teardown and deletion operations).
|
|
29
|
-
try: # pragma: no cover
|
|
30
|
-
import sip as _sip # type: ignore
|
|
31
|
-
_sip_isdeleted = getattr(_sip, 'isdeleted', None)
|
|
20
|
+
try:
|
|
21
|
+
from PyQt6 import sip as _sip # type: ignore
|
|
22
|
+
|
|
23
|
+
_sip_isdeleted = getattr(_sip, "isdeleted", None)
|
|
32
24
|
except Exception:
|
|
33
25
|
_sip = None
|
|
34
26
|
_sip_isdeleted = None
|
|
35
27
|
|
|
28
|
+
|
|
36
29
|
def sip_isdeleted_safe(obj) -> bool:
|
|
37
30
|
"""Return True if sip reports the given wrapper object as deleted.
|
|
38
31
|
|
|
@@ -40,10 +33,9 @@ def sip_isdeleted_safe(obj) -> bool:
|
|
|
40
33
|
occurs while checking, it returns False (i.e. not deleted) so that the
|
|
41
34
|
caller can continue other lightweight guards (like checking scene()).
|
|
42
35
|
"""
|
|
43
|
-
try:
|
|
36
|
+
try:
|
|
44
37
|
if _sip_isdeleted is None:
|
|
45
38
|
return False
|
|
46
39
|
return bool(_sip_isdeleted(obj))
|
|
47
40
|
except Exception:
|
|
48
41
|
return False
|
|
49
|
-
|
|
@@ -10,38 +10,44 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
|
|
|
10
10
|
DOI: 10.5281/zenodo.17268532
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
from PyQt6.QtWidgets import (
|
|
14
|
-
QDialog, QVBoxLayout, QLabel, QPushButton, QHBoxLayout
|
|
15
|
-
)
|
|
16
|
-
from PyQt6.QtGui import QPixmap, QPainter, QPen, QCursor
|
|
17
|
-
from PyQt6.QtCore import Qt
|
|
18
13
|
import os
|
|
14
|
+
|
|
15
|
+
from PyQt6.QtCore import Qt
|
|
16
|
+
from PyQt6.QtGui import QCursor, QPainter, QPen, QPixmap
|
|
17
|
+
from PyQt6.QtWidgets import QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout
|
|
18
|
+
|
|
19
19
|
try:
|
|
20
20
|
from .constants import VERSION
|
|
21
21
|
except Exception:
|
|
22
22
|
from modules.constants import VERSION
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
|
|
25
|
+
class AboutDialog(QDialog): # pragma: no cover
|
|
25
26
|
def __init__(self, main_window, parent=None):
|
|
26
27
|
super().__init__(parent)
|
|
27
28
|
self.main_window = main_window
|
|
28
29
|
self.setWindowTitle("About MoleditPy")
|
|
29
30
|
self.setFixedSize(250, 300)
|
|
30
31
|
self.init_ui()
|
|
31
|
-
|
|
32
|
+
|
|
32
33
|
def init_ui(self):
|
|
33
34
|
layout = QVBoxLayout(self)
|
|
34
|
-
|
|
35
|
+
|
|
35
36
|
# Create a clickable image label
|
|
36
37
|
self.image_label = QLabel()
|
|
37
38
|
self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
38
|
-
|
|
39
|
+
|
|
39
40
|
# Load the original icon image
|
|
40
|
-
icon_path = os.path.join(os.path.dirname(__file__),
|
|
41
|
+
icon_path = os.path.join(os.path.dirname(__file__), "..", "assets", "icon.png")
|
|
41
42
|
if os.path.exists(icon_path):
|
|
42
43
|
original_pixmap = QPixmap(icon_path)
|
|
43
44
|
# Scale to 2x size (160x160)
|
|
44
|
-
pixmap = original_pixmap.scaled(
|
|
45
|
+
pixmap = original_pixmap.scaled(
|
|
46
|
+
160,
|
|
47
|
+
160,
|
|
48
|
+
Qt.AspectRatioMode.KeepAspectRatio,
|
|
49
|
+
Qt.TransformationMode.SmoothTransformation,
|
|
50
|
+
)
|
|
45
51
|
else:
|
|
46
52
|
# Fallback: create a simple placeholder if icon.png not found
|
|
47
53
|
pixmap = QPixmap(160, 160)
|
|
@@ -50,35 +56,36 @@ class AboutDialog(QDialog): # pragma: no cover
|
|
|
50
56
|
painter.setPen(QPen(Qt.GlobalColor.black, 2))
|
|
51
57
|
painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "MoleditPy")
|
|
52
58
|
painter.end()
|
|
53
|
-
|
|
59
|
+
|
|
54
60
|
self.image_label.setPixmap(pixmap)
|
|
55
61
|
try:
|
|
56
62
|
self.image_label.setCursor(QCursor(Qt.CursorShape.ArrowCursor))
|
|
57
|
-
except Exception:
|
|
58
|
-
|
|
63
|
+
except Exception: # pragma: no cover
|
|
64
|
+
import traceback
|
|
65
|
+
traceback.print_exc()
|
|
59
66
|
|
|
60
67
|
self.image_label.mousePressEvent = self.image_mouse_press_event
|
|
61
|
-
|
|
68
|
+
|
|
62
69
|
layout.addWidget(self.image_label)
|
|
63
|
-
|
|
70
|
+
|
|
64
71
|
# Add text information
|
|
65
72
|
info_text = f"MoleditPy Ver. {VERSION}\nAuthor: Hiromichi Yokoyama\nLicense: GPL-3.0 license"
|
|
66
73
|
info_label = QLabel(info_text)
|
|
67
74
|
info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
68
75
|
layout.addWidget(info_label)
|
|
69
|
-
|
|
76
|
+
|
|
70
77
|
# Add OK button
|
|
71
78
|
ok_button = QPushButton("OK")
|
|
72
79
|
ok_button.setFixedSize(80, 30) # 小さいサイズに固定
|
|
73
80
|
ok_button.clicked.connect(self.accept)
|
|
74
|
-
|
|
81
|
+
|
|
75
82
|
# Center the button
|
|
76
83
|
button_layout = QHBoxLayout()
|
|
77
84
|
button_layout.addStretch()
|
|
78
85
|
button_layout.addWidget(ok_button)
|
|
79
86
|
button_layout.addStretch()
|
|
80
87
|
layout.addLayout(button_layout)
|
|
81
|
-
|
|
88
|
+
|
|
82
89
|
def image_clicked(self, event):
|
|
83
90
|
"""Easter egg: Clear all and load bipyrimidine from SMILES"""
|
|
84
91
|
# Clear the current scene
|
|
@@ -100,5 +107,6 @@ class AboutDialog(QDialog): # pragma: no cover
|
|
|
100
107
|
except Exception:
|
|
101
108
|
try:
|
|
102
109
|
event.ignore()
|
|
103
|
-
except Exception:
|
|
104
|
-
|
|
110
|
+
except Exception: # pragma: no cover
|
|
111
|
+
import traceback
|
|
112
|
+
traceback.print_exc()
|
|
@@ -10,18 +10,24 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
|
|
|
10
10
|
DOI: 10.5281/zenodo.17268532
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
+
import numpy as np
|
|
14
|
+
from PyQt6.QtCore import Qt
|
|
13
15
|
from PyQt6.QtWidgets import (
|
|
14
|
-
QDialog,
|
|
16
|
+
QDialog,
|
|
17
|
+
QHBoxLayout,
|
|
18
|
+
QLabel,
|
|
19
|
+
QMessageBox,
|
|
20
|
+
QPushButton,
|
|
21
|
+
QVBoxLayout,
|
|
15
22
|
)
|
|
16
|
-
from PyQt6.QtCore import Qt
|
|
17
|
-
import numpy as np
|
|
18
23
|
|
|
19
24
|
try:
|
|
20
25
|
from .dialog3_d_picking_mixin import Dialog3DPickingMixin
|
|
21
26
|
except Exception:
|
|
22
27
|
from modules.dialog3_d_picking_mixin import Dialog3DPickingMixin
|
|
23
28
|
|
|
24
|
-
|
|
29
|
+
|
|
30
|
+
class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
25
31
|
def __init__(self, mol, main_window, plane, preselected_atoms=None, parent=None):
|
|
26
32
|
QDialog.__init__(self, parent)
|
|
27
33
|
Dialog3DPickingMixin.__init__(self)
|
|
@@ -29,47 +35,51 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
29
35
|
self.main_window = main_window
|
|
30
36
|
self.plane = plane
|
|
31
37
|
self.selected_atoms = set()
|
|
32
|
-
|
|
38
|
+
|
|
33
39
|
# 事前選択された原子を追加
|
|
34
40
|
if preselected_atoms:
|
|
35
41
|
self.selected_atoms.update(preselected_atoms)
|
|
36
|
-
|
|
42
|
+
|
|
37
43
|
self.init_ui()
|
|
38
|
-
|
|
44
|
+
|
|
39
45
|
# 事前選択された原子にラベルを追加
|
|
40
46
|
if self.selected_atoms:
|
|
41
47
|
self.show_atom_labels()
|
|
42
48
|
self.update_display()
|
|
43
|
-
|
|
49
|
+
|
|
44
50
|
def init_ui(self):
|
|
45
|
-
plane_names = {
|
|
51
|
+
plane_names = {"xy": "XY", "xz": "XZ", "yz": "YZ"}
|
|
46
52
|
self.setWindowTitle(f"Align to {plane_names[self.plane]} Plane")
|
|
47
53
|
self.setModal(False)
|
|
48
54
|
layout = QVBoxLayout(self)
|
|
49
|
-
|
|
55
|
+
|
|
50
56
|
# Instructions
|
|
51
|
-
instruction_label = QLabel(
|
|
57
|
+
instruction_label = QLabel(
|
|
58
|
+
f"Click atoms in the 3D view to select them for align to the {plane_names[self.plane]} plane. At least 3 atoms are required."
|
|
59
|
+
)
|
|
52
60
|
instruction_label.setWordWrap(True)
|
|
53
61
|
layout.addWidget(instruction_label)
|
|
54
|
-
|
|
62
|
+
|
|
55
63
|
# Selected atoms display
|
|
56
64
|
self.selection_label = QLabel("No atoms selected")
|
|
57
65
|
layout.addWidget(self.selection_label)
|
|
58
|
-
|
|
66
|
+
|
|
59
67
|
# Buttons
|
|
60
68
|
button_layout = QHBoxLayout()
|
|
61
69
|
self.clear_button = QPushButton("Clear Selection")
|
|
62
70
|
self.clear_button.clicked.connect(self.clear_selection)
|
|
63
71
|
button_layout.addWidget(self.clear_button)
|
|
64
|
-
|
|
72
|
+
|
|
65
73
|
# Select all atoms button
|
|
66
74
|
self.select_all_button = QPushButton("Select All Atoms")
|
|
67
|
-
self.select_all_button.setToolTip(
|
|
75
|
+
self.select_all_button.setToolTip(
|
|
76
|
+
"Select all atoms in the molecule for alignment"
|
|
77
|
+
)
|
|
68
78
|
self.select_all_button.clicked.connect(self.select_all_atoms)
|
|
69
79
|
button_layout.addWidget(self.select_all_button)
|
|
70
|
-
|
|
80
|
+
|
|
71
81
|
button_layout.addStretch()
|
|
72
|
-
|
|
82
|
+
|
|
73
83
|
self.apply_button = QPushButton("Apply align")
|
|
74
84
|
self.apply_button.clicked.connect(self.apply_PlaneAlign)
|
|
75
85
|
self.apply_button.setEnabled(False)
|
|
@@ -78,35 +88,35 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
78
88
|
close_button = QPushButton("Close")
|
|
79
89
|
close_button.clicked.connect(self.reject)
|
|
80
90
|
button_layout.addWidget(close_button)
|
|
81
|
-
|
|
91
|
+
|
|
82
92
|
layout.addLayout(button_layout)
|
|
83
|
-
|
|
93
|
+
|
|
84
94
|
# Connect to main window's picker
|
|
85
95
|
self.picker_connection = None
|
|
86
96
|
self.enable_picking()
|
|
87
|
-
|
|
97
|
+
|
|
88
98
|
def enable_picking(self):
|
|
89
99
|
"""3Dビューでの原子選択を有効にする"""
|
|
90
100
|
self.main_window.plotter.interactor.installEventFilter(self)
|
|
91
101
|
self.picking_enabled = True
|
|
92
|
-
|
|
102
|
+
|
|
93
103
|
def disable_picking(self):
|
|
94
104
|
"""3Dビューでの原子選択を無効にする"""
|
|
95
|
-
if hasattr(self,
|
|
105
|
+
if hasattr(self, "picking_enabled") and self.picking_enabled:
|
|
96
106
|
self.main_window.plotter.interactor.removeEventFilter(self)
|
|
97
107
|
self.picking_enabled = False
|
|
98
|
-
|
|
108
|
+
|
|
99
109
|
def on_atom_picked(self, atom_idx):
|
|
100
110
|
"""原子がピックされたときの処理"""
|
|
101
111
|
if atom_idx in self.selected_atoms:
|
|
102
112
|
self.selected_atoms.remove(atom_idx)
|
|
103
113
|
else:
|
|
104
114
|
self.selected_atoms.add(atom_idx)
|
|
105
|
-
|
|
115
|
+
|
|
106
116
|
# 原子ラベルを表示
|
|
107
117
|
self.show_atom_labels()
|
|
108
118
|
self.update_display()
|
|
109
|
-
|
|
119
|
+
|
|
110
120
|
def keyPressEvent(self, event):
|
|
111
121
|
"""キーボードイベントを処理"""
|
|
112
122
|
if event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
|
|
@@ -115,28 +125,36 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
115
125
|
event.accept()
|
|
116
126
|
else:
|
|
117
127
|
super().keyPressEvent(event)
|
|
118
|
-
|
|
128
|
+
|
|
119
129
|
def clear_selection(self):
|
|
120
130
|
"""選択をクリア"""
|
|
121
131
|
self.selected_atoms.clear()
|
|
122
132
|
self.clear_atom_labels()
|
|
123
133
|
self.update_display()
|
|
124
|
-
|
|
134
|
+
|
|
125
135
|
def select_all_atoms(self):
|
|
126
136
|
"""Select all atoms in the current molecule and update labels/UI."""
|
|
127
137
|
try:
|
|
128
138
|
# Prefer RDKit molecule if available
|
|
129
|
-
if hasattr(self,
|
|
139
|
+
if hasattr(self, "mol") and self.mol is not None:
|
|
130
140
|
try:
|
|
131
141
|
n = self.mol.GetNumAtoms()
|
|
132
142
|
# create a set of indices [0..n-1]
|
|
133
143
|
self.selected_atoms = set(range(n))
|
|
134
144
|
except Exception:
|
|
135
145
|
# fallback to main_window data map
|
|
136
|
-
self.selected_atoms =
|
|
146
|
+
self.selected_atoms = (
|
|
147
|
+
set(self.main_window.data.atoms.keys())
|
|
148
|
+
if hasattr(self.main_window, "data")
|
|
149
|
+
else set()
|
|
150
|
+
)
|
|
137
151
|
else:
|
|
138
152
|
# fallback to main_window data map
|
|
139
|
-
self.selected_atoms =
|
|
153
|
+
self.selected_atoms = (
|
|
154
|
+
set(self.main_window.data.atoms.keys())
|
|
155
|
+
if hasattr(self.main_window, "data")
|
|
156
|
+
else set()
|
|
157
|
+
)
|
|
140
158
|
|
|
141
159
|
# Update labels and display
|
|
142
160
|
self.show_atom_labels()
|
|
@@ -144,42 +162,49 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
144
162
|
|
|
145
163
|
except Exception as e:
|
|
146
164
|
QMessageBox.warning(self, "Warning", f"Failed to select all atoms: {e}")
|
|
147
|
-
|
|
165
|
+
|
|
148
166
|
def update_display(self):
|
|
149
167
|
"""表示を更新"""
|
|
150
168
|
count = len(self.selected_atoms)
|
|
151
169
|
if count == 0:
|
|
152
|
-
self.selection_label.setText(
|
|
170
|
+
self.selection_label.setText(
|
|
171
|
+
"Click atoms to select for align (minimum 3 required)"
|
|
172
|
+
)
|
|
153
173
|
self.apply_button.setEnabled(False)
|
|
154
174
|
else:
|
|
155
175
|
atom_list = sorted(self.selected_atoms)
|
|
156
176
|
atom_display = []
|
|
157
177
|
for i, atom_idx in enumerate(atom_list):
|
|
158
178
|
symbol = self.mol.GetAtomWithIdx(atom_idx).GetSymbol()
|
|
159
|
-
atom_display.append(f"#{i+1}: {symbol}({atom_idx})")
|
|
160
|
-
|
|
161
|
-
self.selection_label.setText(
|
|
179
|
+
atom_display.append(f"#{i + 1}: {symbol}({atom_idx})")
|
|
180
|
+
|
|
181
|
+
self.selection_label.setText(
|
|
182
|
+
f"Selected {count} atoms: {', '.join(atom_display)}"
|
|
183
|
+
)
|
|
162
184
|
self.apply_button.setEnabled(count >= 3)
|
|
163
|
-
|
|
185
|
+
|
|
164
186
|
def show_atom_labels(self):
|
|
165
187
|
"""選択された原子にラベルを表示"""
|
|
166
188
|
if self.selected_atoms:
|
|
167
189
|
sorted_atoms = sorted(self.selected_atoms)
|
|
168
|
-
pairs = [(idx, f"#{i+1}") for i, idx in enumerate(sorted_atoms)]
|
|
169
|
-
self.show_atom_labels_for(pairs, color=
|
|
190
|
+
pairs = [(idx, f"#{i + 1}") for i, idx in enumerate(sorted_atoms)]
|
|
191
|
+
self.show_atom_labels_for(pairs, color="blue")
|
|
170
192
|
else:
|
|
171
193
|
self.clear_atom_labels()
|
|
172
|
-
|
|
194
|
+
|
|
173
195
|
def apply_PlaneAlign(self):
|
|
174
196
|
"""alignを適用(回転ベース)"""
|
|
175
197
|
if len(self.selected_atoms) < 3:
|
|
176
|
-
QMessageBox.warning(
|
|
198
|
+
QMessageBox.warning(
|
|
199
|
+
self, "Warning", "Please select at least 3 atoms for align."
|
|
200
|
+
)
|
|
177
201
|
return
|
|
178
202
|
try:
|
|
179
|
-
|
|
180
203
|
# 選択された原子の位置を取得
|
|
181
204
|
selected_indices = list(self.selected_atoms)
|
|
182
|
-
selected_positions = self.main_window.atom_positions_3d[
|
|
205
|
+
selected_positions = self.main_window.atom_positions_3d[
|
|
206
|
+
selected_indices
|
|
207
|
+
].copy()
|
|
183
208
|
|
|
184
209
|
# 重心を計算
|
|
185
210
|
centroid = np.mean(selected_positions, axis=0)
|
|
@@ -196,12 +221,14 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
196
221
|
normal_vector = eigenvectors[:, 0] # 最小固有値に対応する固有ベクトル
|
|
197
222
|
|
|
198
223
|
# 目標の平面の法線ベクトルを定義
|
|
199
|
-
if self.plane ==
|
|
224
|
+
if self.plane == "xy":
|
|
200
225
|
target_normal = np.array([0, 0, 1]) # Z軸方向
|
|
201
|
-
elif self.plane ==
|
|
226
|
+
elif self.plane == "xz":
|
|
202
227
|
target_normal = np.array([0, 1, 0]) # Y軸方向
|
|
203
|
-
elif self.plane ==
|
|
228
|
+
elif self.plane == "yz":
|
|
204
229
|
target_normal = np.array([1, 0, 0]) # X軸方向
|
|
230
|
+
else:
|
|
231
|
+
target_normal = np.array([0, 0, 1]) # Default to Z-axis (XY plane)
|
|
205
232
|
|
|
206
233
|
# 法線ベクトルの向きを調整(内積が正になるように)
|
|
207
234
|
if np.dot(normal_vector, target_normal) < 0:
|
|
@@ -221,7 +248,11 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
221
248
|
def rodrigues_rotation(v, axis, angle):
|
|
222
249
|
cos_a = np.cos(angle)
|
|
223
250
|
sin_a = np.sin(angle)
|
|
224
|
-
return
|
|
251
|
+
return (
|
|
252
|
+
v * cos_a
|
|
253
|
+
+ np.cross(axis, v) * sin_a
|
|
254
|
+
+ axis * np.dot(axis, v) * (1 - cos_a)
|
|
255
|
+
)
|
|
225
256
|
|
|
226
257
|
# 分子全体を回転させる
|
|
227
258
|
conf = self.mol.GetConformer()
|
|
@@ -229,7 +260,9 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
229
260
|
current_pos = np.array(conf.GetAtomPosition(i))
|
|
230
261
|
# 重心基準で回転
|
|
231
262
|
centered_pos = current_pos - centroid
|
|
232
|
-
rotated_pos = rodrigues_rotation(
|
|
263
|
+
rotated_pos = rodrigues_rotation(
|
|
264
|
+
centered_pos, rotation_axis, rotation_angle
|
|
265
|
+
)
|
|
233
266
|
new_pos = rotated_pos + centroid
|
|
234
267
|
conf.SetAtomPosition(i, new_pos.tolist())
|
|
235
268
|
self.main_window.atom_positions_3d[i] = new_pos
|
|
@@ -245,19 +278,19 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
245
278
|
|
|
246
279
|
except Exception as e:
|
|
247
280
|
QMessageBox.critical(self, "Error", f"Failed to apply align: {str(e)}")
|
|
248
|
-
|
|
281
|
+
|
|
249
282
|
def closeEvent(self, event):
|
|
250
283
|
"""ダイアログが閉じられる時の処理"""
|
|
251
284
|
self.clear_atom_labels()
|
|
252
285
|
self.disable_picking()
|
|
253
286
|
super().closeEvent(event)
|
|
254
|
-
|
|
287
|
+
|
|
255
288
|
def reject(self):
|
|
256
289
|
"""キャンセル時の処理"""
|
|
257
290
|
self.clear_atom_labels()
|
|
258
291
|
self.disable_picking()
|
|
259
292
|
super().reject()
|
|
260
|
-
|
|
293
|
+
|
|
261
294
|
def accept(self):
|
|
262
295
|
"""OK時の処理"""
|
|
263
296
|
self.clear_atom_labels()
|