moleditpy 2.6.0__tar.gz → 2.6.1__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.1}/PKG-INFO +1 -1
- {moleditpy-2.6.0 → moleditpy-2.6.1}/pyproject.toml +1 -1
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/MoleditPy.egg-info/PKG-INFO +1 -1
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/MoleditPy.egg-info/SOURCES.txt +1 -0
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/main.py +1 -1
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/__init__.py +1 -11
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/about_dialog.py +14 -14
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/align_plane_dialog.py +36 -29
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/alignment_dialog.py +44 -41
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/analysis_window.py +30 -24
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/angle_dialog.py +63 -57
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/atom_item.py +47 -39
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/bond_item.py +67 -56
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/bond_length_dialog.py +59 -51
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/calculation_worker.py +104 -101
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/color_settings_dialog.py +13 -6
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/constants.py +3 -3
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/constrained_optimization_dialog.py +91 -84
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/custom_interactor_style.py +73 -79
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/custom_qt_interactor.py +6 -5
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/dialog3_d_picking_mixin.py +18 -19
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/dihedral_dialog.py +72 -65
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window.py +5 -12
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_app_state.py +48 -59
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_compute.py +48 -60
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_dialog_manager.py +70 -75
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_edit_3d.py +64 -74
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_edit_actions.py +101 -102
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_export.py +111 -120
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_main_init.py +190 -184
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_molecular_parsers.py +74 -71
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_project_io.py +68 -73
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_string_importers.py +20 -27
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_ui_manager.py +42 -45
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_view_3d.py +130 -137
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_view_loaders.py +24 -26
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/mirror_dialog.py +37 -26
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/mol_geometry.py +0 -1
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/molecular_data.py +12 -12
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/molecule_scene.py +134 -147
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/move_group_dialog.py +107 -101
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/periodic_table_dialog.py +3 -1
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/planarize_dialog.py +10 -7
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/plugin_interface.py +13 -16
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/plugin_manager.py +54 -55
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/plugin_manager_window.py +42 -33
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/settings_dialog.py +95 -91
- moleditpy-2.6.1/src/moleditpy/modules/sip_isdeleted_safe.py +16 -0
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/template_preview_item.py +18 -27
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/template_preview_view.py +9 -9
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/translation_dialog.py +42 -35
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/user_template_dialog.py +115 -103
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/zoomable_view.py +7 -12
- {moleditpy-2.6.0 → moleditpy-2.6.1}/LICENSE +0 -0
- {moleditpy-2.6.0 → moleditpy-2.6.1}/README.md +0 -0
- {moleditpy-2.6.0 → moleditpy-2.6.1}/setup.cfg +0 -0
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/MoleditPy.egg-info/entry_points.txt +0 -0
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/MoleditPy.egg-info/requires.txt +0 -0
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/MoleditPy.egg-info/top_level.txt +0 -0
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/__init__.py +0 -0
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/__main__.py +0 -0
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/assets/file_icon.ico +0 -0
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/assets/icon.icns +0 -0
- {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/assets/icon.ico +0 -0
- {moleditpy-2.6.0 → moleditpy-2.6.1}/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.1
|
|
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.1
|
|
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,24 +10,14 @@ 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
13
|
try: # pragma: no cover
|
|
19
14
|
import importlib.util
|
|
20
15
|
OBABEL_AVAILABLE = importlib.util.find_spec("openbabel") is not None
|
|
21
16
|
except Exception:
|
|
22
17
|
OBABEL_AVAILABLE = False
|
|
23
18
|
|
|
24
|
-
# Optional SIP helper: on some PyQt6 builds sip.isdeleted is available and
|
|
25
|
-
# allows safely detecting C++ wrapper objects that have been deleted. Import
|
|
26
|
-
# it once at module import time and expose a small, robust wrapper so callers
|
|
27
|
-
# can avoid re-importing sip repeatedly and so we centralize exception
|
|
28
|
-
# handling (this reduces crash risk during teardown and deletion operations).
|
|
29
19
|
try: # pragma: no cover
|
|
30
|
-
import sip as _sip # type: ignore
|
|
20
|
+
from PyQt6 import sip as _sip # type: ignore
|
|
31
21
|
_sip_isdeleted = getattr(_sip, 'isdeleted', None)
|
|
32
22
|
except Exception:
|
|
33
23
|
_sip = None
|
|
@@ -10,12 +10,12 @@ 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:
|
|
@@ -28,14 +28,14 @@ class AboutDialog(QDialog): # pragma: no cover
|
|
|
28
28
|
self.setWindowTitle("About MoleditPy")
|
|
29
29
|
self.setFixedSize(250, 300)
|
|
30
30
|
self.init_ui()
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
def init_ui(self):
|
|
33
33
|
layout = QVBoxLayout(self)
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
# Create a clickable image label
|
|
36
36
|
self.image_label = QLabel()
|
|
37
37
|
self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
# Load the original icon image
|
|
40
40
|
icon_path = os.path.join(os.path.dirname(__file__), '..', 'assets', 'icon.png')
|
|
41
41
|
if os.path.exists(icon_path):
|
|
@@ -50,7 +50,7 @@ class AboutDialog(QDialog): # pragma: no cover
|
|
|
50
50
|
painter.setPen(QPen(Qt.GlobalColor.black, 2))
|
|
51
51
|
painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "MoleditPy")
|
|
52
52
|
painter.end()
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
self.image_label.setPixmap(pixmap)
|
|
55
55
|
try:
|
|
56
56
|
self.image_label.setCursor(QCursor(Qt.CursorShape.ArrowCursor))
|
|
@@ -58,27 +58,27 @@ class AboutDialog(QDialog): # pragma: no cover
|
|
|
58
58
|
pass
|
|
59
59
|
|
|
60
60
|
self.image_label.mousePressEvent = self.image_mouse_press_event
|
|
61
|
-
|
|
61
|
+
|
|
62
62
|
layout.addWidget(self.image_label)
|
|
63
|
-
|
|
63
|
+
|
|
64
64
|
# Add text information
|
|
65
65
|
info_text = f"MoleditPy Ver. {VERSION}\nAuthor: Hiromichi Yokoyama\nLicense: GPL-3.0 license"
|
|
66
66
|
info_label = QLabel(info_text)
|
|
67
67
|
info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
68
68
|
layout.addWidget(info_label)
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
# Add OK button
|
|
71
71
|
ok_button = QPushButton("OK")
|
|
72
72
|
ok_button.setFixedSize(80, 30) # 小さいサイズに固定
|
|
73
73
|
ok_button.clicked.connect(self.accept)
|
|
74
|
-
|
|
74
|
+
|
|
75
75
|
# Center the button
|
|
76
76
|
button_layout = QHBoxLayout()
|
|
77
77
|
button_layout.addStretch()
|
|
78
78
|
button_layout.addWidget(ok_button)
|
|
79
79
|
button_layout.addStretch()
|
|
80
80
|
layout.addLayout(button_layout)
|
|
81
|
-
|
|
81
|
+
|
|
82
82
|
def image_clicked(self, event):
|
|
83
83
|
"""Easter egg: Clear all and load bipyrimidine from SMILES"""
|
|
84
84
|
# Clear the current scene
|
|
@@ -10,11 +10,16 @@ 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
|
|
@@ -29,47 +34,47 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
29
34
|
self.main_window = main_window
|
|
30
35
|
self.plane = plane
|
|
31
36
|
self.selected_atoms = set()
|
|
32
|
-
|
|
37
|
+
|
|
33
38
|
# 事前選択された原子を追加
|
|
34
39
|
if preselected_atoms:
|
|
35
40
|
self.selected_atoms.update(preselected_atoms)
|
|
36
|
-
|
|
41
|
+
|
|
37
42
|
self.init_ui()
|
|
38
|
-
|
|
43
|
+
|
|
39
44
|
# 事前選択された原子にラベルを追加
|
|
40
45
|
if self.selected_atoms:
|
|
41
46
|
self.show_atom_labels()
|
|
42
47
|
self.update_display()
|
|
43
|
-
|
|
48
|
+
|
|
44
49
|
def init_ui(self):
|
|
45
50
|
plane_names = {'xy': 'XY', 'xz': 'XZ', 'yz': 'YZ'}
|
|
46
51
|
self.setWindowTitle(f"Align to {plane_names[self.plane]} Plane")
|
|
47
52
|
self.setModal(False)
|
|
48
53
|
layout = QVBoxLayout(self)
|
|
49
|
-
|
|
54
|
+
|
|
50
55
|
# Instructions
|
|
51
56
|
instruction_label = QLabel(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.")
|
|
52
57
|
instruction_label.setWordWrap(True)
|
|
53
58
|
layout.addWidget(instruction_label)
|
|
54
|
-
|
|
59
|
+
|
|
55
60
|
# Selected atoms display
|
|
56
61
|
self.selection_label = QLabel("No atoms selected")
|
|
57
62
|
layout.addWidget(self.selection_label)
|
|
58
|
-
|
|
63
|
+
|
|
59
64
|
# Buttons
|
|
60
65
|
button_layout = QHBoxLayout()
|
|
61
66
|
self.clear_button = QPushButton("Clear Selection")
|
|
62
67
|
self.clear_button.clicked.connect(self.clear_selection)
|
|
63
68
|
button_layout.addWidget(self.clear_button)
|
|
64
|
-
|
|
69
|
+
|
|
65
70
|
# Select all atoms button
|
|
66
71
|
self.select_all_button = QPushButton("Select All Atoms")
|
|
67
72
|
self.select_all_button.setToolTip("Select all atoms in the molecule for alignment")
|
|
68
73
|
self.select_all_button.clicked.connect(self.select_all_atoms)
|
|
69
74
|
button_layout.addWidget(self.select_all_button)
|
|
70
|
-
|
|
75
|
+
|
|
71
76
|
button_layout.addStretch()
|
|
72
|
-
|
|
77
|
+
|
|
73
78
|
self.apply_button = QPushButton("Apply align")
|
|
74
79
|
self.apply_button.clicked.connect(self.apply_PlaneAlign)
|
|
75
80
|
self.apply_button.setEnabled(False)
|
|
@@ -78,35 +83,35 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
78
83
|
close_button = QPushButton("Close")
|
|
79
84
|
close_button.clicked.connect(self.reject)
|
|
80
85
|
button_layout.addWidget(close_button)
|
|
81
|
-
|
|
86
|
+
|
|
82
87
|
layout.addLayout(button_layout)
|
|
83
|
-
|
|
88
|
+
|
|
84
89
|
# Connect to main window's picker
|
|
85
90
|
self.picker_connection = None
|
|
86
91
|
self.enable_picking()
|
|
87
|
-
|
|
92
|
+
|
|
88
93
|
def enable_picking(self):
|
|
89
94
|
"""3Dビューでの原子選択を有効にする"""
|
|
90
95
|
self.main_window.plotter.interactor.installEventFilter(self)
|
|
91
96
|
self.picking_enabled = True
|
|
92
|
-
|
|
97
|
+
|
|
93
98
|
def disable_picking(self):
|
|
94
99
|
"""3Dビューでの原子選択を無効にする"""
|
|
95
100
|
if hasattr(self, 'picking_enabled') and self.picking_enabled:
|
|
96
101
|
self.main_window.plotter.interactor.removeEventFilter(self)
|
|
97
102
|
self.picking_enabled = False
|
|
98
|
-
|
|
103
|
+
|
|
99
104
|
def on_atom_picked(self, atom_idx):
|
|
100
105
|
"""原子がピックされたときの処理"""
|
|
101
106
|
if atom_idx in self.selected_atoms:
|
|
102
107
|
self.selected_atoms.remove(atom_idx)
|
|
103
108
|
else:
|
|
104
109
|
self.selected_atoms.add(atom_idx)
|
|
105
|
-
|
|
110
|
+
|
|
106
111
|
# 原子ラベルを表示
|
|
107
112
|
self.show_atom_labels()
|
|
108
113
|
self.update_display()
|
|
109
|
-
|
|
114
|
+
|
|
110
115
|
def keyPressEvent(self, event):
|
|
111
116
|
"""キーボードイベントを処理"""
|
|
112
117
|
if event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
|
|
@@ -115,13 +120,13 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
115
120
|
event.accept()
|
|
116
121
|
else:
|
|
117
122
|
super().keyPressEvent(event)
|
|
118
|
-
|
|
123
|
+
|
|
119
124
|
def clear_selection(self):
|
|
120
125
|
"""選択をクリア"""
|
|
121
126
|
self.selected_atoms.clear()
|
|
122
127
|
self.clear_atom_labels()
|
|
123
128
|
self.update_display()
|
|
124
|
-
|
|
129
|
+
|
|
125
130
|
def select_all_atoms(self):
|
|
126
131
|
"""Select all atoms in the current molecule and update labels/UI."""
|
|
127
132
|
try:
|
|
@@ -144,7 +149,7 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
144
149
|
|
|
145
150
|
except Exception as e:
|
|
146
151
|
QMessageBox.warning(self, "Warning", f"Failed to select all atoms: {e}")
|
|
147
|
-
|
|
152
|
+
|
|
148
153
|
def update_display(self):
|
|
149
154
|
"""表示を更新"""
|
|
150
155
|
count = len(self.selected_atoms)
|
|
@@ -157,10 +162,10 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
157
162
|
for i, atom_idx in enumerate(atom_list):
|
|
158
163
|
symbol = self.mol.GetAtomWithIdx(atom_idx).GetSymbol()
|
|
159
164
|
atom_display.append(f"#{i+1}: {symbol}({atom_idx})")
|
|
160
|
-
|
|
165
|
+
|
|
161
166
|
self.selection_label.setText(f"Selected {count} atoms: {', '.join(atom_display)}")
|
|
162
167
|
self.apply_button.setEnabled(count >= 3)
|
|
163
|
-
|
|
168
|
+
|
|
164
169
|
def show_atom_labels(self):
|
|
165
170
|
"""選択された原子にラベルを表示"""
|
|
166
171
|
if self.selected_atoms:
|
|
@@ -169,7 +174,7 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
169
174
|
self.show_atom_labels_for(pairs, color='blue')
|
|
170
175
|
else:
|
|
171
176
|
self.clear_atom_labels()
|
|
172
|
-
|
|
177
|
+
|
|
173
178
|
def apply_PlaneAlign(self):
|
|
174
179
|
"""alignを適用(回転ベース)"""
|
|
175
180
|
if len(self.selected_atoms) < 3:
|
|
@@ -202,6 +207,8 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
202
207
|
target_normal = np.array([0, 1, 0]) # Y軸方向
|
|
203
208
|
elif self.plane == 'yz':
|
|
204
209
|
target_normal = np.array([1, 0, 0]) # X軸方向
|
|
210
|
+
else:
|
|
211
|
+
target_normal = np.array([0, 0, 1]) # Default to Z-axis (XY plane)
|
|
205
212
|
|
|
206
213
|
# 法線ベクトルの向きを調整(内積が正になるように)
|
|
207
214
|
if np.dot(normal_vector, target_normal) < 0:
|
|
@@ -245,19 +252,19 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
245
252
|
|
|
246
253
|
except Exception as e:
|
|
247
254
|
QMessageBox.critical(self, "Error", f"Failed to apply align: {str(e)}")
|
|
248
|
-
|
|
255
|
+
|
|
249
256
|
def closeEvent(self, event):
|
|
250
257
|
"""ダイアログが閉じられる時の処理"""
|
|
251
258
|
self.clear_atom_labels()
|
|
252
259
|
self.disable_picking()
|
|
253
260
|
super().closeEvent(event)
|
|
254
|
-
|
|
261
|
+
|
|
255
262
|
def reject(self):
|
|
256
263
|
"""キャンセル時の処理"""
|
|
257
264
|
self.clear_atom_labels()
|
|
258
265
|
self.disable_picking()
|
|
259
266
|
super().reject()
|
|
260
|
-
|
|
267
|
+
|
|
261
268
|
def accept(self):
|
|
262
269
|
"""OK時の処理"""
|
|
263
270
|
self.clear_atom_labels()
|
|
@@ -10,13 +10,16 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
|
|
|
10
10
|
DOI: 10.5281/zenodo.17268532
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
+
import numpy as np
|
|
13
14
|
from PyQt6.QtWidgets import (
|
|
14
|
-
QDialog,
|
|
15
|
+
QDialog,
|
|
16
|
+
QHBoxLayout,
|
|
17
|
+
QLabel,
|
|
18
|
+
QMessageBox,
|
|
19
|
+
QPushButton,
|
|
20
|
+
QVBoxLayout,
|
|
15
21
|
)
|
|
16
22
|
|
|
17
|
-
from PyQt6.QtWidgets import QMessageBox
|
|
18
|
-
import numpy as np
|
|
19
|
-
|
|
20
23
|
try:
|
|
21
24
|
from .dialog3_d_picking_mixin import Dialog3DPickingMixin
|
|
22
25
|
except Exception:
|
|
@@ -30,72 +33,72 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
30
33
|
self.main_window = main_window
|
|
31
34
|
self.axis = axis
|
|
32
35
|
self.selected_atoms = set()
|
|
33
|
-
|
|
36
|
+
|
|
34
37
|
# 事前選択された原子を追加(最大2個まで)
|
|
35
38
|
if preselected_atoms:
|
|
36
39
|
self.selected_atoms.update(preselected_atoms[:2])
|
|
37
|
-
|
|
40
|
+
|
|
38
41
|
self.init_ui()
|
|
39
|
-
|
|
42
|
+
|
|
40
43
|
# 事前選択された原子にラベルを追加
|
|
41
44
|
if self.selected_atoms:
|
|
42
45
|
for i, atom_idx in enumerate(sorted(self.selected_atoms), 1):
|
|
43
46
|
self.add_selection_label(atom_idx, f"Atom {i}")
|
|
44
47
|
self.update_display()
|
|
45
|
-
|
|
48
|
+
|
|
46
49
|
def init_ui(self):
|
|
47
50
|
axis_names = {'x': 'X-axis', 'y': 'Y-axis', 'z': 'Z-axis'}
|
|
48
51
|
self.setWindowTitle(f"Align to {axis_names[self.axis]}")
|
|
49
52
|
self.setModal(False)
|
|
50
53
|
layout = QVBoxLayout(self)
|
|
51
|
-
|
|
54
|
+
|
|
52
55
|
# Instructions
|
|
53
56
|
instruction_label = QLabel(f"Click atoms in the 3D view to select them for alignment to the {axis_names[self.axis]}. Exactly 2 atoms are required. The first atom will be moved to the origin, and the second atom will be positioned on the {axis_names[self.axis]}.")
|
|
54
57
|
instruction_label.setWordWrap(True)
|
|
55
58
|
layout.addWidget(instruction_label)
|
|
56
|
-
|
|
59
|
+
|
|
57
60
|
# Selected atoms display
|
|
58
61
|
self.selection_label = QLabel("No atoms selected")
|
|
59
62
|
layout.addWidget(self.selection_label)
|
|
60
|
-
|
|
63
|
+
|
|
61
64
|
# Buttons
|
|
62
65
|
button_layout = QHBoxLayout()
|
|
63
66
|
self.clear_button = QPushButton("Clear Selection")
|
|
64
67
|
self.clear_button.clicked.connect(self.clear_selection)
|
|
65
68
|
button_layout.addWidget(self.clear_button)
|
|
66
|
-
|
|
69
|
+
|
|
67
70
|
button_layout.addStretch()
|
|
68
|
-
|
|
71
|
+
|
|
69
72
|
self.apply_button = QPushButton("Apply Alignment")
|
|
70
73
|
self.apply_button.clicked.connect(self.apply_alignment)
|
|
71
74
|
self.apply_button.setEnabled(False)
|
|
72
75
|
button_layout.addWidget(self.apply_button)
|
|
73
|
-
|
|
76
|
+
|
|
74
77
|
close_button = QPushButton("Close")
|
|
75
78
|
close_button.clicked.connect(self.reject)
|
|
76
79
|
button_layout.addWidget(close_button)
|
|
77
|
-
|
|
80
|
+
|
|
78
81
|
layout.addLayout(button_layout)
|
|
79
|
-
|
|
82
|
+
|
|
80
83
|
# Connect to main window's picker
|
|
81
84
|
self.picker_connection = None
|
|
82
85
|
self.enable_picking()
|
|
83
|
-
|
|
86
|
+
|
|
84
87
|
def enable_picking(self):
|
|
85
88
|
"""3Dビューでの原子選択を有効にする"""
|
|
86
89
|
# Dialog3DPickingMixinの機能を使用
|
|
87
90
|
super().enable_picking()
|
|
88
|
-
|
|
91
|
+
|
|
89
92
|
def disable_picking(self):
|
|
90
93
|
"""3Dビューでの原子選択を無効にする"""
|
|
91
94
|
# Dialog3DPickingMixinの機能を使用
|
|
92
95
|
super().disable_picking()
|
|
93
|
-
|
|
96
|
+
|
|
94
97
|
def on_atom_picked(self, atom_idx):
|
|
95
98
|
"""原子がクリックされた時の処理"""
|
|
96
99
|
if self.main_window.current_mol is None:
|
|
97
100
|
return
|
|
98
|
-
|
|
101
|
+
|
|
99
102
|
if atom_idx in self.selected_atoms:
|
|
100
103
|
# 既に選択されている場合は選択解除
|
|
101
104
|
self.selected_atoms.remove(atom_idx)
|
|
@@ -107,9 +110,9 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
107
110
|
# ラベルの順番を示す
|
|
108
111
|
label_text = f"Atom {len(self.selected_atoms)}"
|
|
109
112
|
self.add_selection_label(atom_idx, label_text)
|
|
110
|
-
|
|
113
|
+
|
|
111
114
|
self.update_display()
|
|
112
|
-
|
|
115
|
+
|
|
113
116
|
def update_display(self):
|
|
114
117
|
"""選択状態の表示を更新"""
|
|
115
118
|
if len(self.selected_atoms) == 0:
|
|
@@ -126,13 +129,13 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
126
129
|
atom2 = self.mol.GetAtomWithIdx(selected_list[1])
|
|
127
130
|
self.selection_label.setText(f"Selected 2 atoms: {atom1.GetSymbol()}{selected_list[0]+1}, {atom2.GetSymbol()}{selected_list[1]+1}")
|
|
128
131
|
self.apply_button.setEnabled(True)
|
|
129
|
-
|
|
132
|
+
|
|
130
133
|
def clear_selection(self):
|
|
131
134
|
"""選択をクリア"""
|
|
132
135
|
self.clear_selection_labels()
|
|
133
136
|
self.selected_atoms.clear()
|
|
134
137
|
self.update_display()
|
|
135
|
-
|
|
138
|
+
|
|
136
139
|
def remove_atom_label(self, atom_idx):
|
|
137
140
|
"""特定の原子のラベルを削除"""
|
|
138
141
|
# 簡単化のため、全ラベルをクリアして再描画
|
|
@@ -140,7 +143,7 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
140
143
|
for i, idx in enumerate(sorted(self.selected_atoms), 1):
|
|
141
144
|
if idx != atom_idx:
|
|
142
145
|
self.add_selection_label(idx, f"Atom {i}")
|
|
143
|
-
|
|
146
|
+
|
|
144
147
|
def apply_alignment(self):
|
|
145
148
|
"""アライメントを適用"""
|
|
146
149
|
if len(self.selected_atoms) != 2:
|
|
@@ -174,69 +177,69 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
|
174
177
|
'z': np.array([0.0, 0.0, 1.0])
|
|
175
178
|
}
|
|
176
179
|
target_axis = axis_vectors[self.axis]
|
|
177
|
-
|
|
180
|
+
|
|
178
181
|
# atom2から原点への方向ベクトル
|
|
179
182
|
current_vector = pos2_translated
|
|
180
183
|
current_length = np.linalg.norm(current_vector)
|
|
181
|
-
|
|
184
|
+
|
|
182
185
|
if current_length > 1e-10: # ゼロベクトルでない場合
|
|
183
186
|
current_vector_normalized = current_vector / current_length
|
|
184
|
-
|
|
187
|
+
|
|
185
188
|
# 回転軸と角度を計算
|
|
186
189
|
rotation_axis = np.cross(current_vector_normalized, target_axis)
|
|
187
190
|
rotation_axis_length = np.linalg.norm(rotation_axis)
|
|
188
|
-
|
|
191
|
+
|
|
189
192
|
if rotation_axis_length > 1e-10: # 回転が必要
|
|
190
193
|
rotation_axis = rotation_axis / rotation_axis_length
|
|
191
194
|
cos_angle = np.dot(current_vector_normalized, target_axis)
|
|
192
195
|
cos_angle = np.clip(cos_angle, -1.0, 1.0)
|
|
193
196
|
rotation_angle = np.arccos(cos_angle)
|
|
194
|
-
|
|
197
|
+
|
|
195
198
|
# ロドリゲスの回転公式を使用
|
|
196
199
|
def rodrigues_rotation(v, k, theta):
|
|
197
200
|
cos_theta = np.cos(theta)
|
|
198
201
|
sin_theta = np.sin(theta)
|
|
199
|
-
return (v * cos_theta +
|
|
200
|
-
np.cross(k, v) * sin_theta +
|
|
202
|
+
return (v * cos_theta +
|
|
203
|
+
np.cross(k, v) * sin_theta +
|
|
201
204
|
k * np.dot(k, v) * (1 - cos_theta))
|
|
202
|
-
|
|
205
|
+
|
|
203
206
|
# 全ての原子に回転を適用
|
|
204
207
|
for i in range(self.mol.GetNumAtoms()):
|
|
205
208
|
current_pos = np.array(conf.GetAtomPosition(i))
|
|
206
209
|
rotated_pos = rodrigues_rotation(current_pos, rotation_axis, rotation_angle)
|
|
207
210
|
conf.SetAtomPosition(i, rotated_pos.tolist())
|
|
208
|
-
|
|
211
|
+
|
|
209
212
|
# 3D座標を更新
|
|
210
213
|
self.main_window.atom_positions_3d = np.array([
|
|
211
214
|
list(conf.GetAtomPosition(i)) for i in range(self.mol.GetNumAtoms())
|
|
212
215
|
])
|
|
213
|
-
|
|
216
|
+
|
|
214
217
|
# 3Dビューを更新
|
|
215
218
|
self.main_window.draw_molecule_3d(self.mol)
|
|
216
|
-
|
|
219
|
+
|
|
217
220
|
# キラルラベルを更新
|
|
218
221
|
self.main_window.update_chiral_labels()
|
|
219
222
|
|
|
220
223
|
# Undo状態を保存
|
|
221
224
|
self.main_window.push_undo_state()
|
|
222
|
-
|
|
225
|
+
|
|
223
226
|
QMessageBox.information(self, "Success", f"Alignment to {self.axis.upper()}-axis completed.")
|
|
224
|
-
|
|
227
|
+
|
|
225
228
|
except Exception as e:
|
|
226
229
|
QMessageBox.critical(self, "Error", f"Failed to apply alignment: {str(e)}")
|
|
227
|
-
|
|
230
|
+
|
|
228
231
|
def closeEvent(self, event):
|
|
229
232
|
"""ダイアログが閉じられる時の処理"""
|
|
230
233
|
self.clear_selection_labels()
|
|
231
234
|
self.disable_picking()
|
|
232
235
|
super().closeEvent(event)
|
|
233
|
-
|
|
236
|
+
|
|
234
237
|
def reject(self):
|
|
235
238
|
"""キャンセル時の処理"""
|
|
236
239
|
self.clear_selection_labels()
|
|
237
240
|
self.disable_picking()
|
|
238
241
|
super().reject()
|
|
239
|
-
|
|
242
|
+
|
|
240
243
|
def accept(self):
|
|
241
244
|
"""OK時の処理"""
|
|
242
245
|
self.clear_selection_labels()
|