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