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,157 @@
|
|
|
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
|
+
QGraphicsItem
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from PyQt6.QtGui import (
|
|
18
|
+
QPen, QBrush, QColor, QFont, QPolygonF
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
from PyQt6.QtCore import (
|
|
23
|
+
Qt, QPointF, QRectF, QLineF
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
from .constants import CPK_COLORS
|
|
28
|
+
except Exception:
|
|
29
|
+
from modules.constants import CPK_COLORS
|
|
30
|
+
|
|
31
|
+
class TemplatePreviewItem(QGraphicsItem):
|
|
32
|
+
def __init__(self):
|
|
33
|
+
super().__init__()
|
|
34
|
+
self.setZValue(2)
|
|
35
|
+
self.pen = QPen(QColor(80, 80, 80, 180), 2)
|
|
36
|
+
self.polygon = QPolygonF()
|
|
37
|
+
self.is_aromatic = False
|
|
38
|
+
self.user_template_points = []
|
|
39
|
+
self.user_template_bonds = []
|
|
40
|
+
self.user_template_atoms = []
|
|
41
|
+
self.is_user_template = False
|
|
42
|
+
|
|
43
|
+
def set_geometry(self, points, is_aromatic=False):
|
|
44
|
+
self.prepareGeometryChange()
|
|
45
|
+
self.polygon = QPolygonF(points)
|
|
46
|
+
self.is_aromatic = is_aromatic
|
|
47
|
+
self.is_user_template = False
|
|
48
|
+
self.update()
|
|
49
|
+
|
|
50
|
+
def set_user_template_geometry(self, points, bonds_info, atoms_data):
|
|
51
|
+
self.prepareGeometryChange()
|
|
52
|
+
self.user_template_points = points
|
|
53
|
+
self.user_template_bonds = bonds_info
|
|
54
|
+
self.user_template_atoms = atoms_data
|
|
55
|
+
self.is_user_template = True
|
|
56
|
+
self.is_aromatic = False
|
|
57
|
+
self.polygon = QPolygonF()
|
|
58
|
+
self.update()
|
|
59
|
+
|
|
60
|
+
def boundingRect(self):
|
|
61
|
+
if self.is_user_template and self.user_template_points:
|
|
62
|
+
# Calculate bounding rect for user template
|
|
63
|
+
min_x = min(p.x() for p in self.user_template_points)
|
|
64
|
+
max_x = max(p.x() for p in self.user_template_points)
|
|
65
|
+
min_y = min(p.y() for p in self.user_template_points)
|
|
66
|
+
max_y = max(p.y() for p in self.user_template_points)
|
|
67
|
+
return QRectF(min_x - 20, min_y - 20, max_x - min_x + 40, max_y - min_y + 40)
|
|
68
|
+
return self.polygon.boundingRect().adjusted(-5, -5, 5, 5)
|
|
69
|
+
|
|
70
|
+
def paint(self, painter, option, widget):
|
|
71
|
+
if self.is_user_template:
|
|
72
|
+
self.paint_user_template(painter)
|
|
73
|
+
else:
|
|
74
|
+
self.paint_regular_template(painter)
|
|
75
|
+
|
|
76
|
+
def paint_regular_template(self, painter):
|
|
77
|
+
painter.setPen(self.pen)
|
|
78
|
+
painter.setBrush(Qt.BrushStyle.NoBrush)
|
|
79
|
+
if not self.polygon.isEmpty():
|
|
80
|
+
painter.drawPolygon(self.polygon)
|
|
81
|
+
if self.is_aromatic:
|
|
82
|
+
center = self.polygon.boundingRect().center()
|
|
83
|
+
radius = QLineF(center, self.polygon.first()).length() * 0.6
|
|
84
|
+
painter.drawEllipse(center, radius, radius)
|
|
85
|
+
|
|
86
|
+
def paint_user_template(self, painter):
|
|
87
|
+
if not self.user_template_points:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
# Draw bonds first with better visibility
|
|
91
|
+
# Draw bonds first with better visibility
|
|
92
|
+
# Use gray (ghost) color for template preview to distinguish from real bonds
|
|
93
|
+
bond_pen = QPen(QColor(80, 80, 80, 180), 2.5)
|
|
94
|
+
painter.setPen(bond_pen)
|
|
95
|
+
|
|
96
|
+
for bond_info in self.user_template_bonds:
|
|
97
|
+
if len(bond_info) >= 3:
|
|
98
|
+
atom1_idx, atom2_idx, order = bond_info[:3]
|
|
99
|
+
else:
|
|
100
|
+
atom1_idx, atom2_idx = bond_info[:2]
|
|
101
|
+
order = 1
|
|
102
|
+
|
|
103
|
+
if atom1_idx < len(self.user_template_points) and atom2_idx < len(self.user_template_points):
|
|
104
|
+
pos1 = self.user_template_points[atom1_idx]
|
|
105
|
+
pos2 = self.user_template_points[atom2_idx]
|
|
106
|
+
|
|
107
|
+
if order == 2:
|
|
108
|
+
# Double bond - draw two parallel lines
|
|
109
|
+
line = QLineF(pos1, pos2)
|
|
110
|
+
normal = line.normalVector()
|
|
111
|
+
normal.setLength(4)
|
|
112
|
+
|
|
113
|
+
line1 = QLineF(pos1 + normal.p2() - normal.p1(), pos2 + normal.p2() - normal.p1())
|
|
114
|
+
line2 = QLineF(pos1 - normal.p2() + normal.p1(), pos2 - normal.p2() + normal.p1())
|
|
115
|
+
|
|
116
|
+
painter.drawLine(line1)
|
|
117
|
+
painter.drawLine(line2)
|
|
118
|
+
elif order == 3:
|
|
119
|
+
# Triple bond - draw three parallel lines
|
|
120
|
+
line = QLineF(pos1, pos2)
|
|
121
|
+
normal = line.normalVector()
|
|
122
|
+
normal.setLength(6)
|
|
123
|
+
|
|
124
|
+
painter.drawLine(line)
|
|
125
|
+
line1 = QLineF(pos1 + normal.p2() - normal.p1(), pos2 + normal.p2() - normal.p1())
|
|
126
|
+
line2 = QLineF(pos1 - normal.p2() + normal.p1(), pos2 - normal.p2() + normal.p1())
|
|
127
|
+
|
|
128
|
+
painter.drawLine(line1)
|
|
129
|
+
painter.drawLine(line2)
|
|
130
|
+
else:
|
|
131
|
+
# Single bond
|
|
132
|
+
painter.drawLine(QLineF(pos1, pos2))
|
|
133
|
+
|
|
134
|
+
# Draw atoms - white ellipse background to hide bonds, then CPK colored text
|
|
135
|
+
for i, pos in enumerate(self.user_template_points):
|
|
136
|
+
if i < len(self.user_template_atoms):
|
|
137
|
+
atom_data = self.user_template_atoms[i]
|
|
138
|
+
symbol = atom_data.get('symbol', 'C')
|
|
139
|
+
|
|
140
|
+
# Draw all non-carbon atoms including hydrogen with white background ellipse + CPK colored text
|
|
141
|
+
if symbol != 'C':
|
|
142
|
+
# Get CPK color for text
|
|
143
|
+
color = CPK_COLORS.get(symbol, CPK_COLORS.get('DEFAULT', QColor('#FF1493')))
|
|
144
|
+
|
|
145
|
+
# Draw white background ellipse to hide bonds
|
|
146
|
+
painter.setPen(QPen(Qt.GlobalColor.white, 0)) # No border
|
|
147
|
+
painter.setBrush(QBrush(Qt.GlobalColor.white))
|
|
148
|
+
painter.drawEllipse(int(pos.x() - 12), int(pos.y() - 8), 24, 16)
|
|
149
|
+
|
|
150
|
+
# Draw CPK colored text on top
|
|
151
|
+
painter.setPen(QPen(color))
|
|
152
|
+
font = QFont("Arial", 12, QFont.Weight.Bold) # Larger font
|
|
153
|
+
painter.setFont(font)
|
|
154
|
+
metrics = painter.fontMetrics()
|
|
155
|
+
text_rect = metrics.boundingRect(symbol)
|
|
156
|
+
text_pos = QPointF(pos.x() - text_rect.width()/2, pos.y() + text_rect.height()/3)
|
|
157
|
+
painter.drawText(text_pos, symbol)
|
|
@@ -0,0 +1,74 @@
|
|
|
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 QGraphicsView
|
|
14
|
+
|
|
15
|
+
from PyQt6.QtCore import Qt, QTimer
|
|
16
|
+
|
|
17
|
+
class TemplatePreviewView(QGraphicsView):
|
|
18
|
+
"""テンプレートプレビュー用のカスタムビュークラス"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, scene):
|
|
21
|
+
super().__init__(scene)
|
|
22
|
+
self.original_scene_rect = None
|
|
23
|
+
self.template_data = None # Store template data for dynamic redrawing
|
|
24
|
+
self.parent_dialog = None # Reference to parent dialog for redraw access
|
|
25
|
+
|
|
26
|
+
def set_template_data(self, template_data, parent_dialog):
|
|
27
|
+
"""テンプレートデータと親ダイアログの参照を設定"""
|
|
28
|
+
self.template_data = template_data
|
|
29
|
+
self.parent_dialog = parent_dialog
|
|
30
|
+
|
|
31
|
+
def resizeEvent(self, event):
|
|
32
|
+
"""リサイズイベントを処理してプレビューを再フィット"""
|
|
33
|
+
super().resizeEvent(event)
|
|
34
|
+
if self.original_scene_rect and not self.original_scene_rect.isEmpty():
|
|
35
|
+
# Delay the fitInView call to ensure proper widget sizing
|
|
36
|
+
QTimer.singleShot(10, self.refit_view)
|
|
37
|
+
|
|
38
|
+
def refit_view(self):
|
|
39
|
+
"""ビューを再フィット"""
|
|
40
|
+
try:
|
|
41
|
+
if self.original_scene_rect and not self.original_scene_rect.isEmpty():
|
|
42
|
+
self.fitInView(self.original_scene_rect, Qt.AspectRatioMode.KeepAspectRatio)
|
|
43
|
+
except Exception as e:
|
|
44
|
+
print(f"Warning: Failed to refit template preview: {e}")
|
|
45
|
+
|
|
46
|
+
def showEvent(self, event):
|
|
47
|
+
"""表示イベントを処理"""
|
|
48
|
+
super().showEvent(event)
|
|
49
|
+
# Ensure proper fitting when widget becomes visible
|
|
50
|
+
if self.original_scene_rect:
|
|
51
|
+
QTimer.singleShot(50, self.refit_view)
|
|
52
|
+
|
|
53
|
+
def redraw_with_current_size(self):
|
|
54
|
+
"""現在のサイズに合わせてテンプレートを再描画"""
|
|
55
|
+
if self.template_data and self.parent_dialog:
|
|
56
|
+
try:
|
|
57
|
+
# Clear current scene
|
|
58
|
+
self.scene().clear()
|
|
59
|
+
|
|
60
|
+
# Redraw with current view size for proper fit-based scaling
|
|
61
|
+
view_size = (self.width(), self.height())
|
|
62
|
+
self.parent_dialog.draw_template_preview(self.scene(), self.template_data, view_size)
|
|
63
|
+
|
|
64
|
+
# Refit the view
|
|
65
|
+
bounding_rect = self.scene().itemsBoundingRect()
|
|
66
|
+
if not bounding_rect.isEmpty() and bounding_rect.width() > 0 and bounding_rect.height() > 0:
|
|
67
|
+
content_size = max(bounding_rect.width(), bounding_rect.height())
|
|
68
|
+
padding = max(20, content_size * 0.2)
|
|
69
|
+
padded_rect = bounding_rect.adjusted(-padding, -padding, padding, padding)
|
|
70
|
+
self.scene().setSceneRect(padded_rect)
|
|
71
|
+
self.original_scene_rect = padded_rect
|
|
72
|
+
QTimer.singleShot(10, lambda: self.fitInView(padded_rect, Qt.AspectRatioMode.KeepAspectRatio))
|
|
73
|
+
except Exception as e:
|
|
74
|
+
print(f"Warning: Failed to redraw template preview: {e}")
|
|
@@ -0,0 +1,364 @@
|
|
|
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, QGridLayout, QLineEdit, QCheckBox, QPushButton, QHBoxLayout
|
|
15
|
+
)
|
|
16
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
17
|
+
from PyQt6.QtCore import Qt
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
from .dialog3_d_picking_mixin import Dialog3DPickingMixin
|
|
21
|
+
|
|
22
|
+
class TranslationDialog(Dialog3DPickingMixin, QDialog):
|
|
23
|
+
def __init__(self, mol, main_window, parent=None):
|
|
24
|
+
QDialog.__init__(self, parent)
|
|
25
|
+
Dialog3DPickingMixin.__init__(self)
|
|
26
|
+
self.mol = mol
|
|
27
|
+
self.main_window = main_window
|
|
28
|
+
self.selected_atoms = set() # 複数原子選択用
|
|
29
|
+
self.init_ui()
|
|
30
|
+
|
|
31
|
+
def init_ui(self):
|
|
32
|
+
self.setWindowTitle("Translation")
|
|
33
|
+
self.setModal(False) # モードレスにしてクリックを阻害しない
|
|
34
|
+
layout = QVBoxLayout(self)
|
|
35
|
+
|
|
36
|
+
# Instructions
|
|
37
|
+
instruction_label = QLabel("Click atoms in the 3D view to select them. The centroid of selected atoms will be moved to the target coordinates, translating the entire molecule.")
|
|
38
|
+
instruction_label.setWordWrap(True)
|
|
39
|
+
layout.addWidget(instruction_label)
|
|
40
|
+
|
|
41
|
+
# Selected atoms display
|
|
42
|
+
self.selection_label = QLabel("No atoms selected")
|
|
43
|
+
layout.addWidget(self.selection_label)
|
|
44
|
+
|
|
45
|
+
# Coordinate inputs
|
|
46
|
+
coord_layout = QGridLayout()
|
|
47
|
+
coord_layout.addWidget(QLabel("Target X:"), 0, 0)
|
|
48
|
+
self.x_input = QLineEdit("0.0")
|
|
49
|
+
coord_layout.addWidget(self.x_input, 0, 1)
|
|
50
|
+
|
|
51
|
+
coord_layout.addWidget(QLabel("Target Y:"), 1, 0)
|
|
52
|
+
self.y_input = QLineEdit("0.0")
|
|
53
|
+
coord_layout.addWidget(self.y_input, 1, 1)
|
|
54
|
+
|
|
55
|
+
coord_layout.addWidget(QLabel("Target Z:"), 2, 0)
|
|
56
|
+
self.z_input = QLineEdit("0.0")
|
|
57
|
+
coord_layout.addWidget(self.z_input, 2, 1)
|
|
58
|
+
|
|
59
|
+
layout.addLayout(coord_layout)
|
|
60
|
+
|
|
61
|
+
# Translation target toggle: Entire molecule (default) or Selected atoms only
|
|
62
|
+
self.translate_selected_only_checkbox = QCheckBox("Translate selected atoms only")
|
|
63
|
+
self.translate_selected_only_checkbox.setToolTip(
|
|
64
|
+
"When checked, only the atoms you selected will be moved so their centroid matches the target.\n"
|
|
65
|
+
"When unchecked (default), the entire molecule will be translated so the selected atoms' centroid moves to the target."
|
|
66
|
+
)
|
|
67
|
+
self.translate_selected_only_checkbox.setChecked(False) # default: entire molecule
|
|
68
|
+
layout.addWidget(self.translate_selected_only_checkbox)
|
|
69
|
+
|
|
70
|
+
# Buttons
|
|
71
|
+
button_layout = QHBoxLayout()
|
|
72
|
+
self.clear_button = QPushButton("Clear Selection")
|
|
73
|
+
self.clear_button.clicked.connect(self.clear_selection)
|
|
74
|
+
button_layout.addWidget(self.clear_button)
|
|
75
|
+
|
|
76
|
+
# Select all atoms button
|
|
77
|
+
self.select_all_button = QPushButton("Select All Atoms")
|
|
78
|
+
self.select_all_button.setToolTip("Select all atoms in the molecule for translation")
|
|
79
|
+
self.select_all_button.clicked.connect(self.select_all_atoms)
|
|
80
|
+
button_layout.addWidget(self.select_all_button)
|
|
81
|
+
|
|
82
|
+
button_layout.addStretch()
|
|
83
|
+
|
|
84
|
+
self.apply_button = QPushButton("Apply Translation")
|
|
85
|
+
self.apply_button.clicked.connect(self.apply_translation)
|
|
86
|
+
self.apply_button.setEnabled(False)
|
|
87
|
+
button_layout.addWidget(self.apply_button)
|
|
88
|
+
|
|
89
|
+
close_button = QPushButton("Close")
|
|
90
|
+
close_button.clicked.connect(self.reject)
|
|
91
|
+
button_layout.addWidget(close_button)
|
|
92
|
+
|
|
93
|
+
layout.addLayout(button_layout)
|
|
94
|
+
|
|
95
|
+
# Connect to main window's picker
|
|
96
|
+
self.picker_connection = None
|
|
97
|
+
self.enable_picking()
|
|
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
|
+
self.show_atom_labels()
|
|
106
|
+
self.update_display()
|
|
107
|
+
|
|
108
|
+
def keyPressEvent(self, event):
|
|
109
|
+
"""キーボードイベントを処理"""
|
|
110
|
+
if event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
|
|
111
|
+
if self.apply_button.isEnabled():
|
|
112
|
+
self.apply_translation()
|
|
113
|
+
event.accept()
|
|
114
|
+
else:
|
|
115
|
+
super().keyPressEvent(event)
|
|
116
|
+
|
|
117
|
+
def update_display(self):
|
|
118
|
+
"""表示を更新"""
|
|
119
|
+
if not self.selected_atoms:
|
|
120
|
+
self.selection_label.setText("No atoms selected")
|
|
121
|
+
self.apply_button.setEnabled(False)
|
|
122
|
+
else:
|
|
123
|
+
# 分子の有効性チェック
|
|
124
|
+
if not self.mol or self.mol.GetNumConformers() == 0:
|
|
125
|
+
self.selection_label.setText("Error: No valid molecule or conformer")
|
|
126
|
+
self.apply_button.setEnabled(False)
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
conf = self.mol.GetConformer()
|
|
131
|
+
# 選択原子の重心を計算
|
|
132
|
+
centroid = self.calculate_centroid()
|
|
133
|
+
|
|
134
|
+
# 選択原子の情報を表示
|
|
135
|
+
atom_info = []
|
|
136
|
+
for atom_idx in sorted(self.selected_atoms):
|
|
137
|
+
symbol = self.mol.GetAtomWithIdx(atom_idx).GetSymbol()
|
|
138
|
+
atom_info.append(f"{symbol}({atom_idx})")
|
|
139
|
+
|
|
140
|
+
self.selection_label.setText(
|
|
141
|
+
f"Selected atoms: {', '.join(atom_info)}\n"
|
|
142
|
+
f"Centroid: ({centroid[0]:.2f}, {centroid[1]:.2f}, {centroid[2]:.2f})"
|
|
143
|
+
)
|
|
144
|
+
self.apply_button.setEnabled(True)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
self.selection_label.setText(f"Error accessing atom data: {str(e)}")
|
|
147
|
+
self.apply_button.setEnabled(False)
|
|
148
|
+
|
|
149
|
+
# Update the coordinate input fields when selection changes
|
|
150
|
+
# If there are selected atoms, fill the inputs with the computed centroid (or single atom pos).
|
|
151
|
+
# If no atoms selected, clear or reset to 0.0.
|
|
152
|
+
try:
|
|
153
|
+
if self.selected_atoms:
|
|
154
|
+
# Use the centroid we just computed if available; otherwise compute now.
|
|
155
|
+
try:
|
|
156
|
+
coords = centroid
|
|
157
|
+
except NameError:
|
|
158
|
+
coords = self.calculate_centroid()
|
|
159
|
+
|
|
160
|
+
# Format with reasonable precision
|
|
161
|
+
self.x_input.setText(f"{coords[0]:.4f}")
|
|
162
|
+
self.y_input.setText(f"{coords[1]:.4f}")
|
|
163
|
+
self.z_input.setText(f"{coords[2]:.4f}")
|
|
164
|
+
else:
|
|
165
|
+
# No selection: reset fields to default
|
|
166
|
+
self.x_input.setText("0.0")
|
|
167
|
+
self.y_input.setText("0.0")
|
|
168
|
+
self.z_input.setText("0.0")
|
|
169
|
+
except Exception:
|
|
170
|
+
# Be tolerant: do not crash the UI if inputs cannot be updated
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
def calculate_centroid(self):
|
|
174
|
+
"""選択原子の重心を計算"""
|
|
175
|
+
if not self.selected_atoms:
|
|
176
|
+
return np.array([0.0, 0.0, 0.0])
|
|
177
|
+
|
|
178
|
+
conf = self.mol.GetConformer()
|
|
179
|
+
positions = []
|
|
180
|
+
for atom_idx in self.selected_atoms:
|
|
181
|
+
pos = conf.GetAtomPosition(atom_idx)
|
|
182
|
+
positions.append([pos.x, pos.y, pos.z])
|
|
183
|
+
|
|
184
|
+
return np.mean(positions, axis=0)
|
|
185
|
+
|
|
186
|
+
def apply_translation(self):
|
|
187
|
+
"""平行移動を適用"""
|
|
188
|
+
if not self.selected_atoms:
|
|
189
|
+
QMessageBox.warning(self, "Warning", "Please select at least one atom.")
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
# 分子の有効性チェック
|
|
193
|
+
if not self.mol or self.mol.GetNumConformers() == 0:
|
|
194
|
+
QMessageBox.warning(self, "Warning", "No valid molecule or conformer available.")
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
target_x = float(self.x_input.text())
|
|
199
|
+
target_y = float(self.y_input.text())
|
|
200
|
+
target_z = float(self.z_input.text())
|
|
201
|
+
except ValueError:
|
|
202
|
+
QMessageBox.warning(self, "Warning", "Please enter valid coordinates.")
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
# 選択原子の重心を計算
|
|
207
|
+
current_centroid = self.calculate_centroid()
|
|
208
|
+
target_pos = np.array([target_x, target_y, target_z])
|
|
209
|
+
|
|
210
|
+
# 移動ベクトルを計算
|
|
211
|
+
translation_vector = target_pos - current_centroid
|
|
212
|
+
|
|
213
|
+
conf = self.mol.GetConformer()
|
|
214
|
+
|
|
215
|
+
if self.translate_selected_only_checkbox.isChecked():
|
|
216
|
+
# Move only the selected atoms: shift selected atoms by translation_vector
|
|
217
|
+
for i in range(self.mol.GetNumAtoms()):
|
|
218
|
+
if i in self.selected_atoms:
|
|
219
|
+
atom_pos = np.array(conf.GetAtomPosition(i))
|
|
220
|
+
new_pos = atom_pos + translation_vector
|
|
221
|
+
conf.SetAtomPosition(i, new_pos.tolist())
|
|
222
|
+
# Update 3d positions for this atom only
|
|
223
|
+
try:
|
|
224
|
+
self.main_window.atom_positions_3d[i] = new_pos
|
|
225
|
+
except Exception:
|
|
226
|
+
pass
|
|
227
|
+
else:
|
|
228
|
+
# leave other atoms unchanged
|
|
229
|
+
continue
|
|
230
|
+
else:
|
|
231
|
+
# Default: translate entire molecule so centroid moves to target
|
|
232
|
+
for i in range(self.mol.GetNumAtoms()):
|
|
233
|
+
atom_pos = np.array(conf.GetAtomPosition(i))
|
|
234
|
+
new_pos = atom_pos + translation_vector
|
|
235
|
+
conf.SetAtomPosition(i, new_pos.tolist())
|
|
236
|
+
self.main_window.atom_positions_3d[i] = new_pos
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# 3D表示を更新
|
|
240
|
+
self.main_window.draw_molecule_3d(self.mol)
|
|
241
|
+
|
|
242
|
+
# キラルラベルを更新
|
|
243
|
+
self.main_window.update_chiral_labels()
|
|
244
|
+
|
|
245
|
+
# Apply後に選択解除
|
|
246
|
+
self.clear_selection()
|
|
247
|
+
|
|
248
|
+
# Undo状態を保存
|
|
249
|
+
self.main_window.push_undo_state()
|
|
250
|
+
|
|
251
|
+
except Exception as e:
|
|
252
|
+
QMessageBox.critical(self, "Error", f"Failed to apply translation: {str(e)}")
|
|
253
|
+
|
|
254
|
+
def clear_selection(self):
|
|
255
|
+
"""選択をクリア"""
|
|
256
|
+
self.selected_atoms.clear()
|
|
257
|
+
self.clear_atom_labels()
|
|
258
|
+
self.update_display()
|
|
259
|
+
|
|
260
|
+
def select_all_atoms(self):
|
|
261
|
+
"""Select all atoms in the current molecule and update labels/UI."""
|
|
262
|
+
try:
|
|
263
|
+
# Prefer RDKit molecule if available
|
|
264
|
+
if hasattr(self, 'mol') and self.mol is not None:
|
|
265
|
+
try:
|
|
266
|
+
n = self.mol.GetNumAtoms()
|
|
267
|
+
# create a set of indices [0..n-1]
|
|
268
|
+
self.selected_atoms = set(range(n))
|
|
269
|
+
except Exception:
|
|
270
|
+
# fallback to main_window data map
|
|
271
|
+
self.selected_atoms = set(self.main_window.data.atoms.keys()) if hasattr(self.main_window, 'data') else set()
|
|
272
|
+
else:
|
|
273
|
+
# fallback to main_window data map
|
|
274
|
+
self.selected_atoms = set(self.main_window.data.atoms.keys()) if hasattr(self.main_window, 'data') else set()
|
|
275
|
+
|
|
276
|
+
# Update labels and display
|
|
277
|
+
self.show_atom_labels()
|
|
278
|
+
self.update_display()
|
|
279
|
+
|
|
280
|
+
except Exception as e:
|
|
281
|
+
QMessageBox.warning(self, "Warning", f"Failed to select all atoms: {e}")
|
|
282
|
+
|
|
283
|
+
def show_atom_labels(self):
|
|
284
|
+
"""選択された原子にラベルを表示"""
|
|
285
|
+
# 既存のラベルをクリア
|
|
286
|
+
self.clear_atom_labels()
|
|
287
|
+
|
|
288
|
+
if not hasattr(self, 'selection_labels'):
|
|
289
|
+
self.selection_labels = []
|
|
290
|
+
|
|
291
|
+
if self.selected_atoms:
|
|
292
|
+
positions = []
|
|
293
|
+
labels = []
|
|
294
|
+
|
|
295
|
+
for i, atom_idx in enumerate(sorted(self.selected_atoms)):
|
|
296
|
+
pos = self.main_window.atom_positions_3d[atom_idx]
|
|
297
|
+
positions.append(pos)
|
|
298
|
+
labels.append(f"S{i+1}")
|
|
299
|
+
|
|
300
|
+
# 重心位置も表示
|
|
301
|
+
if len(self.selected_atoms) > 1:
|
|
302
|
+
centroid = self.calculate_centroid()
|
|
303
|
+
positions.append(centroid)
|
|
304
|
+
labels.append("CEN")
|
|
305
|
+
|
|
306
|
+
# ラベルを追加
|
|
307
|
+
if positions:
|
|
308
|
+
label_actor = self.main_window.plotter.add_point_labels(
|
|
309
|
+
positions, labels,
|
|
310
|
+
point_size=20,
|
|
311
|
+
font_size=12,
|
|
312
|
+
text_color='cyan',
|
|
313
|
+
always_visible=True
|
|
314
|
+
)
|
|
315
|
+
# add_point_labelsがリストを返す場合も考慮
|
|
316
|
+
if isinstance(label_actor, list):
|
|
317
|
+
self.selection_labels.extend(label_actor)
|
|
318
|
+
else:
|
|
319
|
+
self.selection_labels.append(label_actor)
|
|
320
|
+
|
|
321
|
+
def clear_atom_labels(self):
|
|
322
|
+
"""原子ラベルをクリア"""
|
|
323
|
+
if hasattr(self, 'selection_labels'):
|
|
324
|
+
for label_actor in self.selection_labels:
|
|
325
|
+
try:
|
|
326
|
+
self.main_window.plotter.remove_actor(label_actor)
|
|
327
|
+
except Exception:
|
|
328
|
+
pass
|
|
329
|
+
self.selection_labels = []
|
|
330
|
+
# ラベル消去後に再描画を強制
|
|
331
|
+
try:
|
|
332
|
+
self.main_window.plotter.render()
|
|
333
|
+
except Exception:
|
|
334
|
+
pass
|
|
335
|
+
|
|
336
|
+
def closeEvent(self, event):
|
|
337
|
+
"""ダイアログが閉じられる時の処理"""
|
|
338
|
+
self.clear_atom_labels()
|
|
339
|
+
self.disable_picking()
|
|
340
|
+
try:
|
|
341
|
+
self.main_window.draw_molecule_3d(self.mol)
|
|
342
|
+
except Exception:
|
|
343
|
+
pass
|
|
344
|
+
super().closeEvent(event)
|
|
345
|
+
|
|
346
|
+
def reject(self):
|
|
347
|
+
"""キャンセル時の処理"""
|
|
348
|
+
self.clear_atom_labels()
|
|
349
|
+
self.disable_picking()
|
|
350
|
+
try:
|
|
351
|
+
self.main_window.draw_molecule_3d(self.mol)
|
|
352
|
+
except Exception:
|
|
353
|
+
pass
|
|
354
|
+
super().reject()
|
|
355
|
+
|
|
356
|
+
def accept(self):
|
|
357
|
+
"""OK時の処理"""
|
|
358
|
+
self.clear_atom_labels()
|
|
359
|
+
self.disable_picking()
|
|
360
|
+
try:
|
|
361
|
+
self.main_window.draw_molecule_3d(self.mol)
|
|
362
|
+
except Exception:
|
|
363
|
+
pass
|
|
364
|
+
super().accept()
|