MoleditPy-linux 2.2.4__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.
Files changed (58) hide show
  1. moleditpy_linux/__init__.py +17 -0
  2. moleditpy_linux/__main__.py +29 -0
  3. moleditpy_linux/main.py +37 -0
  4. moleditpy_linux/modules/__init__.py +41 -0
  5. moleditpy_linux/modules/about_dialog.py +104 -0
  6. moleditpy_linux/modules/align_plane_dialog.py +293 -0
  7. moleditpy_linux/modules/alignment_dialog.py +273 -0
  8. moleditpy_linux/modules/analysis_window.py +209 -0
  9. moleditpy_linux/modules/angle_dialog.py +440 -0
  10. moleditpy_linux/modules/assets/icon.icns +0 -0
  11. moleditpy_linux/modules/assets/icon.ico +0 -0
  12. moleditpy_linux/modules/assets/icon.png +0 -0
  13. moleditpy_linux/modules/atom_item.py +348 -0
  14. moleditpy_linux/modules/bond_item.py +406 -0
  15. moleditpy_linux/modules/bond_length_dialog.py +380 -0
  16. moleditpy_linux/modules/calculation_worker.py +766 -0
  17. moleditpy_linux/modules/color_settings_dialog.py +321 -0
  18. moleditpy_linux/modules/constants.py +88 -0
  19. moleditpy_linux/modules/constrained_optimization_dialog.py +679 -0
  20. moleditpy_linux/modules/custom_interactor_style.py +749 -0
  21. moleditpy_linux/modules/custom_qt_interactor.py +59 -0
  22. moleditpy_linux/modules/dialog3_d_picking_mixin.py +108 -0
  23. moleditpy_linux/modules/dihedral_dialog.py +443 -0
  24. moleditpy_linux/modules/main_window.py +842 -0
  25. moleditpy_linux/modules/main_window_app_state.py +780 -0
  26. moleditpy_linux/modules/main_window_compute.py +1242 -0
  27. moleditpy_linux/modules/main_window_dialog_manager.py +460 -0
  28. moleditpy_linux/modules/main_window_edit_3d.py +536 -0
  29. moleditpy_linux/modules/main_window_edit_actions.py +1455 -0
  30. moleditpy_linux/modules/main_window_export.py +806 -0
  31. moleditpy_linux/modules/main_window_main_init.py +2006 -0
  32. moleditpy_linux/modules/main_window_molecular_parsers.py +1044 -0
  33. moleditpy_linux/modules/main_window_project_io.py +434 -0
  34. moleditpy_linux/modules/main_window_string_importers.py +275 -0
  35. moleditpy_linux/modules/main_window_ui_manager.py +606 -0
  36. moleditpy_linux/modules/main_window_view_3d.py +1531 -0
  37. moleditpy_linux/modules/main_window_view_loaders.py +355 -0
  38. moleditpy_linux/modules/mirror_dialog.py +122 -0
  39. moleditpy_linux/modules/molecular_data.py +302 -0
  40. moleditpy_linux/modules/molecule_scene.py +2000 -0
  41. moleditpy_linux/modules/move_group_dialog.py +598 -0
  42. moleditpy_linux/modules/periodic_table_dialog.py +84 -0
  43. moleditpy_linux/modules/planarize_dialog.py +221 -0
  44. moleditpy_linux/modules/plugin_interface.py +195 -0
  45. moleditpy_linux/modules/plugin_manager.py +309 -0
  46. moleditpy_linux/modules/plugin_manager_window.py +221 -0
  47. moleditpy_linux/modules/settings_dialog.py +1149 -0
  48. moleditpy_linux/modules/template_preview_item.py +157 -0
  49. moleditpy_linux/modules/template_preview_view.py +74 -0
  50. moleditpy_linux/modules/translation_dialog.py +365 -0
  51. moleditpy_linux/modules/user_template_dialog.py +692 -0
  52. moleditpy_linux/modules/zoomable_view.py +129 -0
  53. moleditpy_linux-2.2.4.dist-info/METADATA +936 -0
  54. moleditpy_linux-2.2.4.dist-info/RECORD +58 -0
  55. moleditpy_linux-2.2.4.dist-info/WHEEL +5 -0
  56. moleditpy_linux-2.2.4.dist-info/entry_points.txt +2 -0
  57. moleditpy_linux-2.2.4.dist-info/licenses/LICENSE +674 -0
  58. moleditpy_linux-2.2.4.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,365 @@
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
+ self.setWindowFlags(Qt.WindowType.Window | Qt.WindowType.WindowStaysOnTopHint) # 常に前面表示
35
+ layout = QVBoxLayout(self)
36
+
37
+ # Instructions
38
+ 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.")
39
+ instruction_label.setWordWrap(True)
40
+ layout.addWidget(instruction_label)
41
+
42
+ # Selected atoms display
43
+ self.selection_label = QLabel("No atoms selected")
44
+ layout.addWidget(self.selection_label)
45
+
46
+ # Coordinate inputs
47
+ coord_layout = QGridLayout()
48
+ coord_layout.addWidget(QLabel("Target X:"), 0, 0)
49
+ self.x_input = QLineEdit("0.0")
50
+ coord_layout.addWidget(self.x_input, 0, 1)
51
+
52
+ coord_layout.addWidget(QLabel("Target Y:"), 1, 0)
53
+ self.y_input = QLineEdit("0.0")
54
+ coord_layout.addWidget(self.y_input, 1, 1)
55
+
56
+ coord_layout.addWidget(QLabel("Target Z:"), 2, 0)
57
+ self.z_input = QLineEdit("0.0")
58
+ coord_layout.addWidget(self.z_input, 2, 1)
59
+
60
+ layout.addLayout(coord_layout)
61
+
62
+ # Translation target toggle: Entire molecule (default) or Selected atoms only
63
+ self.translate_selected_only_checkbox = QCheckBox("Translate selected atoms only")
64
+ self.translate_selected_only_checkbox.setToolTip(
65
+ "When checked, only the atoms you selected will be moved so their centroid matches the target.\n"
66
+ "When unchecked (default), the entire molecule will be translated so the selected atoms' centroid moves to the target."
67
+ )
68
+ self.translate_selected_only_checkbox.setChecked(False) # default: entire molecule
69
+ layout.addWidget(self.translate_selected_only_checkbox)
70
+
71
+ # Buttons
72
+ button_layout = QHBoxLayout()
73
+ self.clear_button = QPushButton("Clear Selection")
74
+ self.clear_button.clicked.connect(self.clear_selection)
75
+ button_layout.addWidget(self.clear_button)
76
+
77
+ # Select all atoms button
78
+ self.select_all_button = QPushButton("Select All Atoms")
79
+ self.select_all_button.setToolTip("Select all atoms in the molecule for translation")
80
+ self.select_all_button.clicked.connect(self.select_all_atoms)
81
+ button_layout.addWidget(self.select_all_button)
82
+
83
+ button_layout.addStretch()
84
+
85
+ self.apply_button = QPushButton("Apply Translation")
86
+ self.apply_button.clicked.connect(self.apply_translation)
87
+ self.apply_button.setEnabled(False)
88
+ button_layout.addWidget(self.apply_button)
89
+
90
+ close_button = QPushButton("Close")
91
+ close_button.clicked.connect(self.reject)
92
+ button_layout.addWidget(close_button)
93
+
94
+ layout.addLayout(button_layout)
95
+
96
+ # Connect to main window's picker
97
+ self.picker_connection = None
98
+ self.enable_picking()
99
+
100
+ def on_atom_picked(self, atom_idx):
101
+ """原子がピックされたときの処理"""
102
+ if atom_idx in self.selected_atoms:
103
+ self.selected_atoms.remove(atom_idx)
104
+ else:
105
+ self.selected_atoms.add(atom_idx)
106
+ self.show_atom_labels()
107
+ self.update_display()
108
+
109
+ def keyPressEvent(self, event):
110
+ """キーボードイベントを処理"""
111
+ if event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
112
+ if self.apply_button.isEnabled():
113
+ self.apply_translation()
114
+ event.accept()
115
+ else:
116
+ super().keyPressEvent(event)
117
+
118
+ def update_display(self):
119
+ """表示を更新"""
120
+ if not self.selected_atoms:
121
+ self.selection_label.setText("No atoms selected")
122
+ self.apply_button.setEnabled(False)
123
+ else:
124
+ # 分子の有効性チェック
125
+ if not self.mol or self.mol.GetNumConformers() == 0:
126
+ self.selection_label.setText("Error: No valid molecule or conformer")
127
+ self.apply_button.setEnabled(False)
128
+ return
129
+
130
+ try:
131
+ conf = self.mol.GetConformer()
132
+ # 選択原子の重心を計算
133
+ centroid = self.calculate_centroid()
134
+
135
+ # 選択原子の情報を表示
136
+ atom_info = []
137
+ for atom_idx in sorted(self.selected_atoms):
138
+ symbol = self.mol.GetAtomWithIdx(atom_idx).GetSymbol()
139
+ atom_info.append(f"{symbol}({atom_idx})")
140
+
141
+ self.selection_label.setText(
142
+ f"Selected atoms: {', '.join(atom_info)}\n"
143
+ f"Centroid: ({centroid[0]:.2f}, {centroid[1]:.2f}, {centroid[2]:.2f})"
144
+ )
145
+ self.apply_button.setEnabled(True)
146
+ except Exception as e:
147
+ self.selection_label.setText(f"Error accessing atom data: {str(e)}")
148
+ self.apply_button.setEnabled(False)
149
+
150
+ # Update the coordinate input fields when selection changes
151
+ # If there are selected atoms, fill the inputs with the computed centroid (or single atom pos).
152
+ # If no atoms selected, clear or reset to 0.0.
153
+ try:
154
+ if self.selected_atoms:
155
+ # Use the centroid we just computed if available; otherwise compute now.
156
+ try:
157
+ coords = centroid
158
+ except NameError:
159
+ coords = self.calculate_centroid()
160
+
161
+ # Format with reasonable precision
162
+ self.x_input.setText(f"{coords[0]:.4f}")
163
+ self.y_input.setText(f"{coords[1]:.4f}")
164
+ self.z_input.setText(f"{coords[2]:.4f}")
165
+ else:
166
+ # No selection: reset fields to default
167
+ self.x_input.setText("0.0")
168
+ self.y_input.setText("0.0")
169
+ self.z_input.setText("0.0")
170
+ except Exception:
171
+ # Be tolerant: do not crash the UI if inputs cannot be updated
172
+ pass
173
+
174
+ def calculate_centroid(self):
175
+ """選択原子の重心を計算"""
176
+ if not self.selected_atoms:
177
+ return np.array([0.0, 0.0, 0.0])
178
+
179
+ conf = self.mol.GetConformer()
180
+ positions = []
181
+ for atom_idx in self.selected_atoms:
182
+ pos = conf.GetAtomPosition(atom_idx)
183
+ positions.append([pos.x, pos.y, pos.z])
184
+
185
+ return np.mean(positions, axis=0)
186
+
187
+ def apply_translation(self):
188
+ """平行移動を適用"""
189
+ if not self.selected_atoms:
190
+ QMessageBox.warning(self, "Warning", "Please select at least one atom.")
191
+ return
192
+
193
+ # 分子の有効性チェック
194
+ if not self.mol or self.mol.GetNumConformers() == 0:
195
+ QMessageBox.warning(self, "Warning", "No valid molecule or conformer available.")
196
+ return
197
+
198
+ try:
199
+ target_x = float(self.x_input.text())
200
+ target_y = float(self.y_input.text())
201
+ target_z = float(self.z_input.text())
202
+ except ValueError:
203
+ QMessageBox.warning(self, "Warning", "Please enter valid coordinates.")
204
+ return
205
+
206
+ try:
207
+ # 選択原子の重心を計算
208
+ current_centroid = self.calculate_centroid()
209
+ target_pos = np.array([target_x, target_y, target_z])
210
+
211
+ # 移動ベクトルを計算
212
+ translation_vector = target_pos - current_centroid
213
+
214
+ conf = self.mol.GetConformer()
215
+
216
+ if self.translate_selected_only_checkbox.isChecked():
217
+ # Move only the selected atoms: shift selected atoms by translation_vector
218
+ for i in range(self.mol.GetNumAtoms()):
219
+ if i in self.selected_atoms:
220
+ atom_pos = np.array(conf.GetAtomPosition(i))
221
+ new_pos = atom_pos + translation_vector
222
+ conf.SetAtomPosition(i, new_pos.tolist())
223
+ # Update 3d positions for this atom only
224
+ try:
225
+ self.main_window.atom_positions_3d[i] = new_pos
226
+ except Exception:
227
+ pass
228
+ else:
229
+ # leave other atoms unchanged
230
+ continue
231
+ else:
232
+ # Default: translate entire molecule so centroid moves to target
233
+ for i in range(self.mol.GetNumAtoms()):
234
+ atom_pos = np.array(conf.GetAtomPosition(i))
235
+ new_pos = atom_pos + translation_vector
236
+ conf.SetAtomPosition(i, new_pos.tolist())
237
+ self.main_window.atom_positions_3d[i] = new_pos
238
+
239
+
240
+ # 3D表示を更新
241
+ self.main_window.draw_molecule_3d(self.mol)
242
+
243
+ # キラルラベルを更新
244
+ self.main_window.update_chiral_labels()
245
+
246
+ # Apply後に選択解除
247
+ self.clear_selection()
248
+
249
+ # Undo状態を保存
250
+ self.main_window.push_undo_state()
251
+
252
+ except Exception as e:
253
+ QMessageBox.critical(self, "Error", f"Failed to apply translation: {str(e)}")
254
+
255
+ def clear_selection(self):
256
+ """選択をクリア"""
257
+ self.selected_atoms.clear()
258
+ self.clear_atom_labels()
259
+ self.update_display()
260
+
261
+ def select_all_atoms(self):
262
+ """Select all atoms in the current molecule and update labels/UI."""
263
+ try:
264
+ # Prefer RDKit molecule if available
265
+ if hasattr(self, 'mol') and self.mol is not None:
266
+ try:
267
+ n = self.mol.GetNumAtoms()
268
+ # create a set of indices [0..n-1]
269
+ self.selected_atoms = set(range(n))
270
+ except Exception:
271
+ # fallback to main_window data map
272
+ self.selected_atoms = set(self.main_window.data.atoms.keys()) if hasattr(self.main_window, 'data') else set()
273
+ else:
274
+ # fallback to main_window data map
275
+ self.selected_atoms = set(self.main_window.data.atoms.keys()) if hasattr(self.main_window, 'data') else set()
276
+
277
+ # Update labels and display
278
+ self.show_atom_labels()
279
+ self.update_display()
280
+
281
+ except Exception as e:
282
+ QMessageBox.warning(self, "Warning", f"Failed to select all atoms: {e}")
283
+
284
+ def show_atom_labels(self):
285
+ """選択された原子にラベルを表示"""
286
+ # 既存のラベルをクリア
287
+ self.clear_atom_labels()
288
+
289
+ if not hasattr(self, 'selection_labels'):
290
+ self.selection_labels = []
291
+
292
+ if self.selected_atoms:
293
+ positions = []
294
+ labels = []
295
+
296
+ for i, atom_idx in enumerate(sorted(self.selected_atoms)):
297
+ pos = self.main_window.atom_positions_3d[atom_idx]
298
+ positions.append(pos)
299
+ labels.append(f"S{i+1}")
300
+
301
+ # 重心位置も表示
302
+ if len(self.selected_atoms) > 1:
303
+ centroid = self.calculate_centroid()
304
+ positions.append(centroid)
305
+ labels.append("CEN")
306
+
307
+ # ラベルを追加
308
+ if positions:
309
+ label_actor = self.main_window.plotter.add_point_labels(
310
+ positions, labels,
311
+ point_size=20,
312
+ font_size=12,
313
+ text_color='cyan',
314
+ always_visible=True
315
+ )
316
+ # add_point_labelsがリストを返す場合も考慮
317
+ if isinstance(label_actor, list):
318
+ self.selection_labels.extend(label_actor)
319
+ else:
320
+ self.selection_labels.append(label_actor)
321
+
322
+ def clear_atom_labels(self):
323
+ """原子ラベルをクリア"""
324
+ if hasattr(self, 'selection_labels'):
325
+ for label_actor in self.selection_labels:
326
+ try:
327
+ self.main_window.plotter.remove_actor(label_actor)
328
+ except Exception:
329
+ pass
330
+ self.selection_labels = []
331
+ # ラベル消去後に再描画を強制
332
+ try:
333
+ self.main_window.plotter.render()
334
+ except Exception:
335
+ pass
336
+
337
+ def closeEvent(self, event):
338
+ """ダイアログが閉じられる時の処理"""
339
+ self.clear_atom_labels()
340
+ self.disable_picking()
341
+ try:
342
+ self.main_window.draw_molecule_3d(self.mol)
343
+ except Exception:
344
+ pass
345
+ super().closeEvent(event)
346
+
347
+ def reject(self):
348
+ """キャンセル時の処理"""
349
+ self.clear_atom_labels()
350
+ self.disable_picking()
351
+ try:
352
+ self.main_window.draw_molecule_3d(self.mol)
353
+ except Exception:
354
+ pass
355
+ super().reject()
356
+
357
+ def accept(self):
358
+ """OK時の処理"""
359
+ self.clear_atom_labels()
360
+ self.disable_picking()
361
+ try:
362
+ self.main_window.draw_molecule_3d(self.mol)
363
+ except Exception:
364
+ pass
365
+ super().accept()