MoleditPy-linux 2.4.1__py3-none-any.whl
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_linux/__init__.py +17 -0
- moleditpy_linux/__main__.py +29 -0
- moleditpy_linux/main.py +37 -0
- moleditpy_linux/modules/__init__.py +41 -0
- moleditpy_linux/modules/about_dialog.py +104 -0
- moleditpy_linux/modules/align_plane_dialog.py +292 -0
- moleditpy_linux/modules/alignment_dialog.py +272 -0
- moleditpy_linux/modules/analysis_window.py +209 -0
- moleditpy_linux/modules/angle_dialog.py +440 -0
- moleditpy_linux/modules/assets/file_icon.ico +0 -0
- moleditpy_linux/modules/assets/icon.icns +0 -0
- moleditpy_linux/modules/assets/icon.ico +0 -0
- moleditpy_linux/modules/assets/icon.png +0 -0
- moleditpy_linux/modules/atom_item.py +395 -0
- moleditpy_linux/modules/bond_item.py +464 -0
- moleditpy_linux/modules/bond_length_dialog.py +380 -0
- moleditpy_linux/modules/calculation_worker.py +766 -0
- moleditpy_linux/modules/color_settings_dialog.py +321 -0
- moleditpy_linux/modules/constants.py +88 -0
- moleditpy_linux/modules/constrained_optimization_dialog.py +678 -0
- moleditpy_linux/modules/custom_interactor_style.py +749 -0
- moleditpy_linux/modules/custom_qt_interactor.py +102 -0
- moleditpy_linux/modules/dialog3_d_picking_mixin.py +141 -0
- moleditpy_linux/modules/dihedral_dialog.py +443 -0
- moleditpy_linux/modules/main_window.py +850 -0
- moleditpy_linux/modules/main_window_app_state.py +787 -0
- moleditpy_linux/modules/main_window_compute.py +1242 -0
- moleditpy_linux/modules/main_window_dialog_manager.py +460 -0
- moleditpy_linux/modules/main_window_edit_3d.py +536 -0
- moleditpy_linux/modules/main_window_edit_actions.py +1565 -0
- moleditpy_linux/modules/main_window_export.py +917 -0
- moleditpy_linux/modules/main_window_main_init.py +2100 -0
- moleditpy_linux/modules/main_window_molecular_parsers.py +1044 -0
- moleditpy_linux/modules/main_window_project_io.py +434 -0
- moleditpy_linux/modules/main_window_string_importers.py +275 -0
- moleditpy_linux/modules/main_window_ui_manager.py +602 -0
- moleditpy_linux/modules/main_window_view_3d.py +1539 -0
- moleditpy_linux/modules/main_window_view_loaders.py +355 -0
- moleditpy_linux/modules/mirror_dialog.py +122 -0
- moleditpy_linux/modules/molecular_data.py +302 -0
- moleditpy_linux/modules/molecule_scene.py +2000 -0
- moleditpy_linux/modules/move_group_dialog.py +600 -0
- moleditpy_linux/modules/periodic_table_dialog.py +84 -0
- moleditpy_linux/modules/planarize_dialog.py +220 -0
- moleditpy_linux/modules/plugin_interface.py +215 -0
- moleditpy_linux/modules/plugin_manager.py +473 -0
- moleditpy_linux/modules/plugin_manager_window.py +274 -0
- moleditpy_linux/modules/settings_dialog.py +1503 -0
- moleditpy_linux/modules/template_preview_item.py +157 -0
- moleditpy_linux/modules/template_preview_view.py +74 -0
- moleditpy_linux/modules/translation_dialog.py +364 -0
- moleditpy_linux/modules/user_template_dialog.py +692 -0
- moleditpy_linux/modules/zoomable_view.py +129 -0
- moleditpy_linux-2.4.1.dist-info/METADATA +954 -0
- moleditpy_linux-2.4.1.dist-info/RECORD +59 -0
- moleditpy_linux-2.4.1.dist-info/WHEEL +5 -0
- moleditpy_linux-2.4.1.dist-info/entry_points.txt +2 -0
- moleditpy_linux-2.4.1.dist-info/licenses/LICENSE +674 -0
- moleditpy_linux-2.4.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
MoleditPy — A Python-based molecular editing software
|
|
6
|
+
|
|
7
|
+
Author: Hiromichi Yokoyama
|
|
8
|
+
License: GPL-3.0 license
|
|
9
|
+
Repo: https://github.com/HiroYokoyama/python_molecular_editor
|
|
10
|
+
DOI: 10.5281/zenodo.17268532
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
"""Top-level package for moleditpy.
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
MoleditPy — A Python-based molecular editing software
|
|
6
|
+
|
|
7
|
+
Author: Hiromichi Yokoyama
|
|
8
|
+
License: GPL-3.0 license
|
|
9
|
+
Repo: https://github.com/HiroYokoyama/python_molecular_editor
|
|
10
|
+
DOI: 10.5281/zenodo.17268532
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
print("-----------------------------------------------------")
|
|
15
|
+
print("MoleditPy — A Python-based molecular editing software")
|
|
16
|
+
print("-----------------------------------------------------\n")
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
# Preferred when running as a package: python -m moleditpy
|
|
20
|
+
from .main import main
|
|
21
|
+
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
|
+
from main import main
|
|
25
|
+
|
|
26
|
+
# --- Application Execution ---
|
|
27
|
+
if __name__ == '__main__':
|
|
28
|
+
main()
|
|
29
|
+
|
moleditpy_linux/main.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
MoleditPy — A Python-based molecular editing software
|
|
6
|
+
|
|
7
|
+
Author: Hiromichi Yokoyama
|
|
8
|
+
License: GPL-3.0 license
|
|
9
|
+
Repo: https://github.com/HiroYokoyama/python_molecular_editor
|
|
10
|
+
DOI: 10.5281/zenodo.17268532
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
import ctypes
|
|
16
|
+
|
|
17
|
+
from PyQt6.QtWidgets import QApplication
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
# When executed as part of the package (python -m moleditpy)
|
|
21
|
+
from .modules.main_window import MainWindow
|
|
22
|
+
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
|
+
from modules.main_window import MainWindow
|
|
26
|
+
|
|
27
|
+
def main():
|
|
28
|
+
# --- Windows タスクバーアイコンのための追加処理 ---
|
|
29
|
+
if sys.platform == 'win32':
|
|
30
|
+
myappid = 'hyoko.moleditpy.1.0' # アプリケーション固有のID(任意)
|
|
31
|
+
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
|
|
32
|
+
|
|
33
|
+
app = QApplication(sys.argv)
|
|
34
|
+
file_path = sys.argv[1] if len(sys.argv) > 1 else None
|
|
35
|
+
window = MainWindow(initial_file=file_path)
|
|
36
|
+
window.show()
|
|
37
|
+
sys.exit(app.exec())
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
MoleditPy — A Python-based molecular editing software
|
|
6
|
+
|
|
7
|
+
Author: Hiromichi Yokoyama
|
|
8
|
+
License: GPL-3.0 license
|
|
9
|
+
Repo: https://github.com/HiroYokoyama/python_molecular_editor
|
|
10
|
+
DOI: 10.5281/zenodo.17268532
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# Open Babel is disabled for Linux version
|
|
14
|
+
OBABEL_AVAILABLE = False
|
|
15
|
+
|
|
16
|
+
# Optional SIP helper: on some PyQt6 builds sip.isdeleted is available and
|
|
17
|
+
# allows safely detecting C++ wrapper objects that have been deleted. Import
|
|
18
|
+
# it once at module import time and expose a small, robust wrapper so callers
|
|
19
|
+
# can avoid re-importing sip repeatedly and so we centralize exception
|
|
20
|
+
# handling (this reduces crash risk during teardown and deletion operations).
|
|
21
|
+
try:
|
|
22
|
+
import sip as _sip # type: ignore
|
|
23
|
+
_sip_isdeleted = getattr(_sip, 'isdeleted', None)
|
|
24
|
+
except Exception:
|
|
25
|
+
_sip = None
|
|
26
|
+
_sip_isdeleted = None
|
|
27
|
+
|
|
28
|
+
def sip_isdeleted_safe(obj) -> bool:
|
|
29
|
+
"""Return True if sip reports the given wrapper object as deleted.
|
|
30
|
+
|
|
31
|
+
This function is conservative: if SIP isn't available or any error
|
|
32
|
+
occurs while checking, it returns False (i.e. not deleted) so that the
|
|
33
|
+
caller can continue other lightweight guards (like checking scene()).
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
if _sip_isdeleted is None:
|
|
37
|
+
return False
|
|
38
|
+
return bool(_sip_isdeleted(obj))
|
|
39
|
+
except Exception:
|
|
40
|
+
return False
|
|
41
|
+
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
MoleditPy — A Python-based molecular editing software
|
|
6
|
+
|
|
7
|
+
Author: Hiromichi Yokoyama
|
|
8
|
+
License: GPL-3.0 license
|
|
9
|
+
Repo: https://github.com/HiroYokoyama/python_molecular_editor
|
|
10
|
+
DOI: 10.5281/zenodo.17268532
|
|
11
|
+
"""
|
|
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
|
+
import os
|
|
19
|
+
try:
|
|
20
|
+
from .constants import VERSION
|
|
21
|
+
except Exception:
|
|
22
|
+
from modules.constants import VERSION
|
|
23
|
+
|
|
24
|
+
class AboutDialog(QDialog):
|
|
25
|
+
def __init__(self, main_window, parent=None):
|
|
26
|
+
super().__init__(parent)
|
|
27
|
+
self.main_window = main_window
|
|
28
|
+
self.setWindowTitle("About MoleditPy")
|
|
29
|
+
self.setFixedSize(250, 300)
|
|
30
|
+
self.init_ui()
|
|
31
|
+
|
|
32
|
+
def init_ui(self):
|
|
33
|
+
layout = QVBoxLayout(self)
|
|
34
|
+
|
|
35
|
+
# Create a clickable image label
|
|
36
|
+
self.image_label = QLabel()
|
|
37
|
+
self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
38
|
+
|
|
39
|
+
# Load the original icon image
|
|
40
|
+
icon_path = os.path.join(os.path.dirname(__file__), 'assets', 'icon.png')
|
|
41
|
+
if os.path.exists(icon_path):
|
|
42
|
+
original_pixmap = QPixmap(icon_path)
|
|
43
|
+
# Scale to 2x size (160x160)
|
|
44
|
+
pixmap = original_pixmap.scaled(160, 160, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
|
|
45
|
+
else:
|
|
46
|
+
# Fallback: create a simple placeholder if icon.png not found
|
|
47
|
+
pixmap = QPixmap(160, 160)
|
|
48
|
+
pixmap.fill(Qt.GlobalColor.lightGray)
|
|
49
|
+
painter = QPainter(pixmap)
|
|
50
|
+
painter.setPen(QPen(Qt.GlobalColor.black, 2))
|
|
51
|
+
painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "MoleditPy")
|
|
52
|
+
painter.end()
|
|
53
|
+
|
|
54
|
+
self.image_label.setPixmap(pixmap)
|
|
55
|
+
try:
|
|
56
|
+
self.image_label.setCursor(QCursor(Qt.CursorShape.ArrowCursor))
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
self.image_label.mousePressEvent = self.image_mouse_press_event
|
|
61
|
+
|
|
62
|
+
layout.addWidget(self.image_label)
|
|
63
|
+
|
|
64
|
+
# Add text information
|
|
65
|
+
info_text = f"MoleditPy for Linux Ver. {VERSION}\nAuthor: Hiromichi Yokoyama\nLicense: GPL-3.0 license"
|
|
66
|
+
info_label = QLabel(info_text)
|
|
67
|
+
info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
68
|
+
layout.addWidget(info_label)
|
|
69
|
+
|
|
70
|
+
# Add OK button
|
|
71
|
+
ok_button = QPushButton("OK")
|
|
72
|
+
ok_button.setFixedSize(80, 30) # 小さいサイズに固定
|
|
73
|
+
ok_button.clicked.connect(self.accept)
|
|
74
|
+
|
|
75
|
+
# Center the button
|
|
76
|
+
button_layout = QHBoxLayout()
|
|
77
|
+
button_layout.addStretch()
|
|
78
|
+
button_layout.addWidget(ok_button)
|
|
79
|
+
button_layout.addStretch()
|
|
80
|
+
layout.addLayout(button_layout)
|
|
81
|
+
|
|
82
|
+
def image_clicked(self, event):
|
|
83
|
+
"""Easter egg: Clear all and load bipyrimidine from SMILES"""
|
|
84
|
+
# Clear the current scene
|
|
85
|
+
self.main_window.clear_all()
|
|
86
|
+
|
|
87
|
+
bipyrimidine_smiles = "C1=CN=C(N=C1)C2=NC=CC=N2"
|
|
88
|
+
self.main_window.load_from_smiles(bipyrimidine_smiles)
|
|
89
|
+
|
|
90
|
+
# Close the dialog
|
|
91
|
+
self.accept()
|
|
92
|
+
|
|
93
|
+
def image_mouse_press_event(self, event):
|
|
94
|
+
"""Handle mouse press on the image: trigger easter egg only for right-click."""
|
|
95
|
+
try:
|
|
96
|
+
if event.button() == Qt.MouseButton.RightButton:
|
|
97
|
+
self.image_clicked(event)
|
|
98
|
+
else:
|
|
99
|
+
event.ignore()
|
|
100
|
+
except Exception:
|
|
101
|
+
try:
|
|
102
|
+
event.ignore()
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
MoleditPy — A Python-based molecular editing software
|
|
6
|
+
|
|
7
|
+
Author: Hiromichi Yokoyama
|
|
8
|
+
License: GPL-3.0 license
|
|
9
|
+
Repo: https://github.com/HiroYokoyama/python_molecular_editor
|
|
10
|
+
DOI: 10.5281/zenodo.17268532
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from PyQt6.QtWidgets import (
|
|
14
|
+
QDialog, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QMessageBox
|
|
15
|
+
)
|
|
16
|
+
from PyQt6.QtCore import Qt
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from .dialog3_d_picking_mixin import Dialog3DPickingMixin
|
|
21
|
+
except Exception:
|
|
22
|
+
from modules.dialog3_d_picking_mixin import Dialog3DPickingMixin
|
|
23
|
+
|
|
24
|
+
class AlignPlaneDialog(Dialog3DPickingMixin, QDialog):
|
|
25
|
+
def __init__(self, mol, main_window, plane, preselected_atoms=None, parent=None):
|
|
26
|
+
QDialog.__init__(self, parent)
|
|
27
|
+
Dialog3DPickingMixin.__init__(self)
|
|
28
|
+
self.mol = mol
|
|
29
|
+
self.main_window = main_window
|
|
30
|
+
self.plane = plane
|
|
31
|
+
self.selected_atoms = set()
|
|
32
|
+
|
|
33
|
+
# 事前選択された原子を追加
|
|
34
|
+
if preselected_atoms:
|
|
35
|
+
self.selected_atoms.update(preselected_atoms)
|
|
36
|
+
|
|
37
|
+
self.init_ui()
|
|
38
|
+
|
|
39
|
+
# 事前選択された原子にラベルを追加
|
|
40
|
+
if self.selected_atoms:
|
|
41
|
+
self.show_atom_labels()
|
|
42
|
+
self.update_display()
|
|
43
|
+
|
|
44
|
+
def init_ui(self):
|
|
45
|
+
plane_names = {'xy': 'XY', 'xz': 'XZ', 'yz': 'YZ'}
|
|
46
|
+
self.setWindowTitle(f"Align to {plane_names[self.plane]} Plane")
|
|
47
|
+
self.setModal(False) # モードレスにしてクリックを阻害しない
|
|
48
|
+
layout = QVBoxLayout(self)
|
|
49
|
+
|
|
50
|
+
# Instructions
|
|
51
|
+
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
|
+
instruction_label.setWordWrap(True)
|
|
53
|
+
layout.addWidget(instruction_label)
|
|
54
|
+
|
|
55
|
+
# Selected atoms display
|
|
56
|
+
self.selection_label = QLabel("No atoms selected")
|
|
57
|
+
layout.addWidget(self.selection_label)
|
|
58
|
+
|
|
59
|
+
# Buttons
|
|
60
|
+
button_layout = QHBoxLayout()
|
|
61
|
+
self.clear_button = QPushButton("Clear Selection")
|
|
62
|
+
self.clear_button.clicked.connect(self.clear_selection)
|
|
63
|
+
button_layout.addWidget(self.clear_button)
|
|
64
|
+
|
|
65
|
+
# Select all atoms button
|
|
66
|
+
self.select_all_button = QPushButton("Select All Atoms")
|
|
67
|
+
self.select_all_button.setToolTip("Select all atoms in the molecule for alignment")
|
|
68
|
+
self.select_all_button.clicked.connect(self.select_all_atoms)
|
|
69
|
+
button_layout.addWidget(self.select_all_button)
|
|
70
|
+
|
|
71
|
+
button_layout.addStretch()
|
|
72
|
+
|
|
73
|
+
self.apply_button = QPushButton("Apply align")
|
|
74
|
+
self.apply_button.clicked.connect(self.apply_PlaneAlign)
|
|
75
|
+
self.apply_button.setEnabled(False)
|
|
76
|
+
button_layout.addWidget(self.apply_button)
|
|
77
|
+
|
|
78
|
+
close_button = QPushButton("Close")
|
|
79
|
+
close_button.clicked.connect(self.reject)
|
|
80
|
+
button_layout.addWidget(close_button)
|
|
81
|
+
|
|
82
|
+
layout.addLayout(button_layout)
|
|
83
|
+
|
|
84
|
+
# Connect to main window's picker
|
|
85
|
+
self.picker_connection = None
|
|
86
|
+
self.enable_picking()
|
|
87
|
+
|
|
88
|
+
def enable_picking(self):
|
|
89
|
+
"""3Dビューでの原子選択を有効にする"""
|
|
90
|
+
self.main_window.plotter.interactor.installEventFilter(self)
|
|
91
|
+
self.picking_enabled = True
|
|
92
|
+
|
|
93
|
+
def disable_picking(self):
|
|
94
|
+
"""3Dビューでの原子選択を無効にする"""
|
|
95
|
+
if hasattr(self, 'picking_enabled') and self.picking_enabled:
|
|
96
|
+
self.main_window.plotter.interactor.removeEventFilter(self)
|
|
97
|
+
self.picking_enabled = False
|
|
98
|
+
|
|
99
|
+
def on_atom_picked(self, atom_idx):
|
|
100
|
+
"""原子がピックされたときの処理"""
|
|
101
|
+
if atom_idx in self.selected_atoms:
|
|
102
|
+
self.selected_atoms.remove(atom_idx)
|
|
103
|
+
else:
|
|
104
|
+
self.selected_atoms.add(atom_idx)
|
|
105
|
+
|
|
106
|
+
# 原子ラベルを表示
|
|
107
|
+
self.show_atom_labels()
|
|
108
|
+
self.update_display()
|
|
109
|
+
|
|
110
|
+
def keyPressEvent(self, event):
|
|
111
|
+
"""キーボードイベントを処理"""
|
|
112
|
+
if event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
|
|
113
|
+
if self.apply_button.isEnabled():
|
|
114
|
+
self.apply_PlaneAlign()
|
|
115
|
+
event.accept()
|
|
116
|
+
else:
|
|
117
|
+
super().keyPressEvent(event)
|
|
118
|
+
|
|
119
|
+
def clear_selection(self):
|
|
120
|
+
"""選択をクリア"""
|
|
121
|
+
self.selected_atoms.clear()
|
|
122
|
+
self.clear_atom_labels()
|
|
123
|
+
self.update_display()
|
|
124
|
+
|
|
125
|
+
def select_all_atoms(self):
|
|
126
|
+
"""Select all atoms in the current molecule and update labels/UI."""
|
|
127
|
+
try:
|
|
128
|
+
# Prefer RDKit molecule if available
|
|
129
|
+
if hasattr(self, 'mol') and self.mol is not None:
|
|
130
|
+
try:
|
|
131
|
+
n = self.mol.GetNumAtoms()
|
|
132
|
+
# create a set of indices [0..n-1]
|
|
133
|
+
self.selected_atoms = set(range(n))
|
|
134
|
+
except Exception:
|
|
135
|
+
# fallback to main_window data map
|
|
136
|
+
self.selected_atoms = set(self.main_window.data.atoms.keys()) if hasattr(self.main_window, 'data') else set()
|
|
137
|
+
else:
|
|
138
|
+
# fallback to main_window data map
|
|
139
|
+
self.selected_atoms = set(self.main_window.data.atoms.keys()) if hasattr(self.main_window, 'data') else set()
|
|
140
|
+
|
|
141
|
+
# Update labels and display
|
|
142
|
+
self.show_atom_labels()
|
|
143
|
+
self.update_display()
|
|
144
|
+
|
|
145
|
+
except Exception as e:
|
|
146
|
+
QMessageBox.warning(self, "Warning", f"Failed to select all atoms: {e}")
|
|
147
|
+
|
|
148
|
+
def update_display(self):
|
|
149
|
+
"""表示を更新"""
|
|
150
|
+
count = len(self.selected_atoms)
|
|
151
|
+
if count == 0:
|
|
152
|
+
self.selection_label.setText("Click atoms to select for align (minimum 3 required)")
|
|
153
|
+
self.apply_button.setEnabled(False)
|
|
154
|
+
else:
|
|
155
|
+
atom_list = sorted(self.selected_atoms)
|
|
156
|
+
atom_display = []
|
|
157
|
+
for i, atom_idx in enumerate(atom_list):
|
|
158
|
+
symbol = self.mol.GetAtomWithIdx(atom_idx).GetSymbol()
|
|
159
|
+
atom_display.append(f"#{i+1}: {symbol}({atom_idx})")
|
|
160
|
+
|
|
161
|
+
self.selection_label.setText(f"Selected {count} atoms: {', '.join(atom_display)}")
|
|
162
|
+
self.apply_button.setEnabled(count >= 3)
|
|
163
|
+
|
|
164
|
+
def show_atom_labels(self):
|
|
165
|
+
"""選択された原子にラベルを表示"""
|
|
166
|
+
# 既存のラベルをクリア
|
|
167
|
+
self.clear_atom_labels()
|
|
168
|
+
|
|
169
|
+
# 新しいラベルを表示
|
|
170
|
+
if not hasattr(self, 'selection_labels'):
|
|
171
|
+
self.selection_labels = []
|
|
172
|
+
|
|
173
|
+
if self.selected_atoms:
|
|
174
|
+
sorted_atoms = sorted(self.selected_atoms)
|
|
175
|
+
|
|
176
|
+
for i, atom_idx in enumerate(sorted_atoms):
|
|
177
|
+
pos = self.main_window.atom_positions_3d[atom_idx]
|
|
178
|
+
label_text = f"#{i+1}"
|
|
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 = []
|
|
199
|
+
|
|
200
|
+
def apply_PlaneAlign(self):
|
|
201
|
+
"""alignを適用(回転ベース)"""
|
|
202
|
+
if len(self.selected_atoms) < 3:
|
|
203
|
+
QMessageBox.warning(self, "Warning", "Please select at least 3 atoms for align.")
|
|
204
|
+
return
|
|
205
|
+
try:
|
|
206
|
+
|
|
207
|
+
# 選択された原子の位置を取得
|
|
208
|
+
selected_indices = list(self.selected_atoms)
|
|
209
|
+
selected_positions = self.main_window.atom_positions_3d[selected_indices].copy()
|
|
210
|
+
|
|
211
|
+
# 重心を計算
|
|
212
|
+
centroid = np.mean(selected_positions, axis=0)
|
|
213
|
+
|
|
214
|
+
# 重心を原点に移動
|
|
215
|
+
centered_positions = selected_positions - centroid
|
|
216
|
+
|
|
217
|
+
# 主成分分析で最適な平面を見つける
|
|
218
|
+
# 選択された原子の座標の共分散行列を計算
|
|
219
|
+
cov_matrix = np.cov(centered_positions.T)
|
|
220
|
+
eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)
|
|
221
|
+
|
|
222
|
+
# 固有値が最も小さい固有ベクトルが平面の法線方向
|
|
223
|
+
normal_vector = eigenvectors[:, 0] # 最小固有値に対応する固有ベクトル
|
|
224
|
+
|
|
225
|
+
# 目標の平面の法線ベクトルを定義
|
|
226
|
+
if self.plane == 'xy':
|
|
227
|
+
target_normal = np.array([0, 0, 1]) # Z軸方向
|
|
228
|
+
elif self.plane == 'xz':
|
|
229
|
+
target_normal = np.array([0, 1, 0]) # Y軸方向
|
|
230
|
+
elif self.plane == 'yz':
|
|
231
|
+
target_normal = np.array([1, 0, 0]) # X軸方向
|
|
232
|
+
|
|
233
|
+
# 法線ベクトルの向きを調整(内積が正になるように)
|
|
234
|
+
if np.dot(normal_vector, target_normal) < 0:
|
|
235
|
+
normal_vector = -normal_vector
|
|
236
|
+
|
|
237
|
+
# 回転軸と回転角度を計算
|
|
238
|
+
rotation_axis = np.cross(normal_vector, target_normal)
|
|
239
|
+
rotation_axis_norm = np.linalg.norm(rotation_axis)
|
|
240
|
+
|
|
241
|
+
if rotation_axis_norm > 1e-10: # 回転が必要な場合
|
|
242
|
+
rotation_axis = rotation_axis / rotation_axis_norm
|
|
243
|
+
cos_angle = np.dot(normal_vector, target_normal)
|
|
244
|
+
cos_angle = np.clip(cos_angle, -1.0, 1.0)
|
|
245
|
+
rotation_angle = np.arccos(cos_angle)
|
|
246
|
+
|
|
247
|
+
# Rodrigues回転公式を使用して全分子を回転
|
|
248
|
+
def rodrigues_rotation(v, axis, angle):
|
|
249
|
+
cos_a = np.cos(angle)
|
|
250
|
+
sin_a = np.sin(angle)
|
|
251
|
+
return v * cos_a + np.cross(axis, v) * sin_a + axis * np.dot(axis, v) * (1 - cos_a)
|
|
252
|
+
|
|
253
|
+
# 分子全体を回転させる
|
|
254
|
+
conf = self.mol.GetConformer()
|
|
255
|
+
for i in range(self.mol.GetNumAtoms()):
|
|
256
|
+
current_pos = np.array(conf.GetAtomPosition(i))
|
|
257
|
+
# 重心基準で回転
|
|
258
|
+
centered_pos = current_pos - centroid
|
|
259
|
+
rotated_pos = rodrigues_rotation(centered_pos, rotation_axis, rotation_angle)
|
|
260
|
+
new_pos = rotated_pos + centroid
|
|
261
|
+
conf.SetAtomPosition(i, new_pos.tolist())
|
|
262
|
+
self.main_window.atom_positions_3d[i] = new_pos
|
|
263
|
+
|
|
264
|
+
# 3D表示を更新
|
|
265
|
+
self.main_window.draw_molecule_3d(self.mol)
|
|
266
|
+
|
|
267
|
+
# キラルラベルを更新
|
|
268
|
+
self.main_window.update_chiral_labels()
|
|
269
|
+
|
|
270
|
+
# Undo状態を保存
|
|
271
|
+
self.main_window.push_undo_state()
|
|
272
|
+
|
|
273
|
+
except Exception as e:
|
|
274
|
+
QMessageBox.critical(self, "Error", f"Failed to apply align: {str(e)}")
|
|
275
|
+
|
|
276
|
+
def closeEvent(self, event):
|
|
277
|
+
"""ダイアログが閉じられる時の処理"""
|
|
278
|
+
self.clear_atom_labels()
|
|
279
|
+
self.disable_picking()
|
|
280
|
+
super().closeEvent(event)
|
|
281
|
+
|
|
282
|
+
def reject(self):
|
|
283
|
+
"""キャンセル時の処理"""
|
|
284
|
+
self.clear_atom_labels()
|
|
285
|
+
self.disable_picking()
|
|
286
|
+
super().reject()
|
|
287
|
+
|
|
288
|
+
def accept(self):
|
|
289
|
+
"""OK時の処理"""
|
|
290
|
+
self.clear_atom_labels()
|
|
291
|
+
self.disable_picking()
|
|
292
|
+
super().accept()
|