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.
Files changed (59) 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 +292 -0
  7. moleditpy_linux/modules/alignment_dialog.py +272 -0
  8. moleditpy_linux/modules/analysis_window.py +209 -0
  9. moleditpy_linux/modules/angle_dialog.py +440 -0
  10. moleditpy_linux/modules/assets/file_icon.ico +0 -0
  11. moleditpy_linux/modules/assets/icon.icns +0 -0
  12. moleditpy_linux/modules/assets/icon.ico +0 -0
  13. moleditpy_linux/modules/assets/icon.png +0 -0
  14. moleditpy_linux/modules/atom_item.py +395 -0
  15. moleditpy_linux/modules/bond_item.py +464 -0
  16. moleditpy_linux/modules/bond_length_dialog.py +380 -0
  17. moleditpy_linux/modules/calculation_worker.py +766 -0
  18. moleditpy_linux/modules/color_settings_dialog.py +321 -0
  19. moleditpy_linux/modules/constants.py +88 -0
  20. moleditpy_linux/modules/constrained_optimization_dialog.py +678 -0
  21. moleditpy_linux/modules/custom_interactor_style.py +749 -0
  22. moleditpy_linux/modules/custom_qt_interactor.py +102 -0
  23. moleditpy_linux/modules/dialog3_d_picking_mixin.py +141 -0
  24. moleditpy_linux/modules/dihedral_dialog.py +443 -0
  25. moleditpy_linux/modules/main_window.py +850 -0
  26. moleditpy_linux/modules/main_window_app_state.py +787 -0
  27. moleditpy_linux/modules/main_window_compute.py +1242 -0
  28. moleditpy_linux/modules/main_window_dialog_manager.py +460 -0
  29. moleditpy_linux/modules/main_window_edit_3d.py +536 -0
  30. moleditpy_linux/modules/main_window_edit_actions.py +1565 -0
  31. moleditpy_linux/modules/main_window_export.py +917 -0
  32. moleditpy_linux/modules/main_window_main_init.py +2100 -0
  33. moleditpy_linux/modules/main_window_molecular_parsers.py +1044 -0
  34. moleditpy_linux/modules/main_window_project_io.py +434 -0
  35. moleditpy_linux/modules/main_window_string_importers.py +275 -0
  36. moleditpy_linux/modules/main_window_ui_manager.py +602 -0
  37. moleditpy_linux/modules/main_window_view_3d.py +1539 -0
  38. moleditpy_linux/modules/main_window_view_loaders.py +355 -0
  39. moleditpy_linux/modules/mirror_dialog.py +122 -0
  40. moleditpy_linux/modules/molecular_data.py +302 -0
  41. moleditpy_linux/modules/molecule_scene.py +2000 -0
  42. moleditpy_linux/modules/move_group_dialog.py +600 -0
  43. moleditpy_linux/modules/periodic_table_dialog.py +84 -0
  44. moleditpy_linux/modules/planarize_dialog.py +220 -0
  45. moleditpy_linux/modules/plugin_interface.py +215 -0
  46. moleditpy_linux/modules/plugin_manager.py +473 -0
  47. moleditpy_linux/modules/plugin_manager_window.py +274 -0
  48. moleditpy_linux/modules/settings_dialog.py +1503 -0
  49. moleditpy_linux/modules/template_preview_item.py +157 -0
  50. moleditpy_linux/modules/template_preview_view.py +74 -0
  51. moleditpy_linux/modules/translation_dialog.py +364 -0
  52. moleditpy_linux/modules/user_template_dialog.py +692 -0
  53. moleditpy_linux/modules/zoomable_view.py +129 -0
  54. moleditpy_linux-2.4.1.dist-info/METADATA +954 -0
  55. moleditpy_linux-2.4.1.dist-info/RECORD +59 -0
  56. moleditpy_linux-2.4.1.dist-info/WHEEL +5 -0
  57. moleditpy_linux-2.4.1.dist-info/entry_points.txt +2 -0
  58. moleditpy_linux-2.4.1.dist-info/licenses/LICENSE +674 -0
  59. 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()