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