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,586 @@
1
+ from PyQt6.QtWidgets import (
2
+ QDialog, QVBoxLayout, QLabel, QGridLayout, QHBoxLayout, QPushButton, QLineEdit, QMessageBox
3
+ )
4
+ from PyQt6.QtCore import Qt, QEvent
5
+ import numpy as np
6
+ import pyvista as pv
7
+ from rdkit import Chem
8
+ try:
9
+ from .constants import VDW_RADII
10
+ except Exception:
11
+ from modules.constants import VDW_RADII
12
+
13
+ try:
14
+ from .dialog3_d_picking_mixin import Dialog3DPickingMixin
15
+ except Exception:
16
+ from modules.dialog3_d_picking_mixin import Dialog3DPickingMixin
17
+
18
+ class MoveGroupDialog(Dialog3DPickingMixin, QDialog):
19
+ """結合している分子グループを選択して並行移動・回転するダイアログ"""
20
+
21
+ def __init__(self, mol, main_window, parent=None):
22
+ QDialog.__init__(self, parent)
23
+ Dialog3DPickingMixin.__init__(self)
24
+ self.mol = mol
25
+ self.main_window = main_window
26
+ self.selected_atoms = set()
27
+ self.group_atoms = set() # 選択原子に結合している全原子
28
+ self.init_ui()
29
+
30
+ def init_ui(self):
31
+ self.setWindowTitle("Move Group")
32
+ self.setModal(False)
33
+ self.setWindowFlags(Qt.WindowType.Window | Qt.WindowType.WindowStaysOnTopHint)
34
+ self.resize(300,400) # ウィンドウサイズを設定
35
+ layout = QVBoxLayout(self)
36
+
37
+ # ドラッグ状態管理
38
+ self.is_dragging_group = False
39
+ self.drag_start_pos = None
40
+ self.mouse_moved_during_drag = False # ドラッグ中にマウスが動いたかを追跡
41
+
42
+ # Instructions
43
+ instruction_label = QLabel("Click an atom in the 3D view to select its connected molecule group.\n"
44
+ "Left-drag: Move the group\n"
45
+ "Right-drag: Rotate the group around its center")
46
+ instruction_label.setWordWrap(True)
47
+ layout.addWidget(instruction_label)
48
+
49
+ # Selected group display
50
+ self.selection_label = QLabel("No group selected")
51
+ layout.addWidget(self.selection_label)
52
+
53
+ # Translation controls
54
+ trans_group = QLabel("Translation (Å):")
55
+ trans_group.setStyleSheet("font-weight: bold;")
56
+ layout.addWidget(trans_group)
57
+
58
+ trans_layout = QGridLayout()
59
+ self.x_trans_input = QLineEdit("0.0")
60
+ self.y_trans_input = QLineEdit("0.0")
61
+ self.z_trans_input = QLineEdit("0.0")
62
+
63
+ # Enterキーでapply_translationを実行
64
+ self.x_trans_input.returnPressed.connect(self.apply_translation)
65
+ self.y_trans_input.returnPressed.connect(self.apply_translation)
66
+ self.z_trans_input.returnPressed.connect(self.apply_translation)
67
+
68
+ trans_layout.addWidget(QLabel("X:"), 0, 0)
69
+ trans_layout.addWidget(self.x_trans_input, 0, 1)
70
+ trans_layout.addWidget(QLabel("Y:"), 1, 0)
71
+ trans_layout.addWidget(self.y_trans_input, 1, 1)
72
+ trans_layout.addWidget(QLabel("Z:"), 2, 0)
73
+ trans_layout.addWidget(self.z_trans_input, 2, 1)
74
+
75
+ trans_button_layout = QHBoxLayout()
76
+ reset_trans_button = QPushButton("Reset")
77
+ reset_trans_button.clicked.connect(self.reset_translation_inputs)
78
+ trans_button_layout.addWidget(reset_trans_button)
79
+
80
+ apply_trans_button = QPushButton("Apply Translation")
81
+ apply_trans_button.clicked.connect(self.apply_translation)
82
+ trans_button_layout.addWidget(apply_trans_button)
83
+
84
+ trans_layout.addLayout(trans_button_layout, 3, 0, 1, 2)
85
+
86
+ layout.addLayout(trans_layout)
87
+
88
+ layout.addSpacing(10)
89
+
90
+ # Rotation controls
91
+ rot_group = QLabel("Rotation (degrees):")
92
+ rot_group.setStyleSheet("font-weight: bold;")
93
+ layout.addWidget(rot_group)
94
+
95
+ rot_layout = QGridLayout()
96
+ self.x_rot_input = QLineEdit("0.0")
97
+ self.y_rot_input = QLineEdit("0.0")
98
+ self.z_rot_input = QLineEdit("0.0")
99
+
100
+ # Enterキーでapply_rotationを実行
101
+ self.x_rot_input.returnPressed.connect(self.apply_rotation)
102
+ self.y_rot_input.returnPressed.connect(self.apply_rotation)
103
+ self.z_rot_input.returnPressed.connect(self.apply_rotation)
104
+
105
+ rot_layout.addWidget(QLabel("Around X:"), 0, 0)
106
+ rot_layout.addWidget(self.x_rot_input, 0, 1)
107
+ rot_layout.addWidget(QLabel("Around Y:"), 1, 0)
108
+ rot_layout.addWidget(self.y_rot_input, 1, 1)
109
+ rot_layout.addWidget(QLabel("Around Z:"), 2, 0)
110
+ rot_layout.addWidget(self.z_rot_input, 2, 1)
111
+
112
+ rot_button_layout = QHBoxLayout()
113
+ reset_rot_button = QPushButton("Reset")
114
+ reset_rot_button.clicked.connect(self.reset_rotation_inputs)
115
+ rot_button_layout.addWidget(reset_rot_button)
116
+
117
+ apply_rot_button = QPushButton("Apply Rotation")
118
+ apply_rot_button.clicked.connect(self.apply_rotation)
119
+ rot_button_layout.addWidget(apply_rot_button)
120
+
121
+ rot_layout.addLayout(rot_button_layout, 3, 0, 1, 2)
122
+
123
+ layout.addLayout(rot_layout)
124
+
125
+ # Buttons
126
+ button_layout = QHBoxLayout()
127
+ self.clear_button = QPushButton("Clear Selection")
128
+ self.clear_button.clicked.connect(self.clear_selection)
129
+ button_layout.addWidget(self.clear_button)
130
+
131
+ button_layout.addStretch()
132
+
133
+ close_button = QPushButton("Close")
134
+ close_button.clicked.connect(self.reject)
135
+ button_layout.addWidget(close_button)
136
+
137
+ layout.addLayout(button_layout)
138
+
139
+ # Enable picking to handle atom selection
140
+ self.enable_picking()
141
+
142
+ def eventFilter(self, obj, event):
143
+ """3Dビューでのマウスイベント処理 - グループが選択されている場合はCustomInteractorStyleに任せる"""
144
+ if obj == self.main_window.plotter.interactor:
145
+ # ダブルクリック/トリプルクリックで状態が混乱するのを防ぐ
146
+ if event.type() == QEvent.Type.MouseButtonDblClick:
147
+ # ダブルクリックは無視し、状態をリセット
148
+ self.is_dragging_group = False
149
+ self.drag_start_pos = None
150
+ self.mouse_moved_during_drag = False
151
+ self.potential_drag = False
152
+ if hasattr(self, 'clicked_atom_for_toggle'):
153
+ delattr(self, 'clicked_atom_for_toggle')
154
+ return False
155
+
156
+ if event.type() == QEvent.Type.MouseButtonPress and event.button() == Qt.MouseButton.LeftButton:
157
+ # 前回の状態をクリーンアップ(トリプルクリック対策)
158
+ self.is_dragging_group = False
159
+ self.potential_drag = False
160
+ if hasattr(self, 'clicked_atom_for_toggle'):
161
+ delattr(self, 'clicked_atom_for_toggle')
162
+ # グループが既に選択されている場合は、CustomInteractorStyleに処理を任せる
163
+ if self.group_atoms:
164
+ return False
165
+
166
+ # マウスプレス時の処理
167
+ # マウスプレス時の処理
168
+ try:
169
+ interactor = self.main_window.plotter.interactor
170
+ click_pos = interactor.GetEventPosition()
171
+
172
+ # まずピッキングしてどの原子がクリックされたか確認
173
+ picker = self.main_window.plotter.picker
174
+ picker.Pick(click_pos[0], click_pos[1], 0, self.main_window.plotter.renderer)
175
+
176
+ clicked_atom_idx = None
177
+ if picker.GetActor() is self.main_window.atom_actor:
178
+ picked_position = np.array(picker.GetPickPosition())
179
+ distances = np.linalg.norm(self.main_window.atom_positions_3d - picked_position, axis=1)
180
+ closest_atom_idx = np.argmin(distances)
181
+
182
+ # 閾値チェック
183
+ if 0 <= closest_atom_idx < self.mol.GetNumAtoms():
184
+ atom = self.mol.GetAtomWithIdx(int(closest_atom_idx))
185
+ if atom:
186
+ atomic_num = atom.GetAtomicNum()
187
+ pt = Chem.GetPeriodicTable()
188
+ vdw_radius = pt.GetRvdw(atomic_num)
189
+ click_threshold = vdw_radius * 1.5
190
+
191
+ if distances[closest_atom_idx] < click_threshold:
192
+ clicked_atom_idx = int(closest_atom_idx)
193
+
194
+
195
+ # クリックされた原子の処理
196
+ if clicked_atom_idx is not None:
197
+ if self.group_atoms and clicked_atom_idx in self.group_atoms:
198
+ # 既存のグループ内の原子 - ドラッグ準備(まだドラッグとは確定しない)
199
+ self.is_dragging_group = False # まだドラッグ中ではない
200
+ self.drag_start_pos = click_pos
201
+ self.drag_atom_idx = clicked_atom_idx
202
+ self.mouse_moved_during_drag = False
203
+ self.potential_drag = True # ドラッグの可能性がある
204
+ self.clicked_atom_for_toggle = clicked_atom_idx # トグル用に保存
205
+ # イベントを消費せず、カメラ操作を許可(閾値超えたらドラッグ開始)
206
+ return False
207
+ else:
208
+ # グループ外の原子 - 新しいグループを選択
209
+ # 親クラス(Mixin)のon_atom_pickedを手動で呼ぶ
210
+ self.on_atom_picked(clicked_atom_idx)
211
+ return True
212
+ else:
213
+ # 原子以外をクリック
214
+ # グループがあっても通常のカメラ操作を許可
215
+ return False
216
+
217
+ except Exception as e:
218
+ print(f"Error in mouse press: {e}")
219
+ return False
220
+
221
+ elif event.type() == QEvent.Type.MouseMove:
222
+ # マウス移動時の処理
223
+ if getattr(self, 'potential_drag', False) and self.drag_start_pos and not self.is_dragging_group:
224
+ # potential_drag状態:閾値チェック
225
+ try:
226
+ interactor = self.main_window.plotter.interactor
227
+ current_pos = interactor.GetEventPosition()
228
+ dx = current_pos[0] - self.drag_start_pos[0]
229
+ dy = current_pos[1] - self.drag_start_pos[1]
230
+
231
+ # 閾値を超えたらドラッグ開始
232
+ drag_threshold = 5 # ピクセル
233
+ if abs(dx) > drag_threshold or abs(dy) > drag_threshold:
234
+ # ドラッグ開始を確定
235
+ self.is_dragging_group = True
236
+ self.potential_drag = False
237
+ try:
238
+ self.main_window.plotter.setCursor(Qt.CursorShape.ClosedHandCursor)
239
+ except Exception:
240
+ pass
241
+ except Exception:
242
+ pass
243
+
244
+ # 閾値以下の場合はカメラ操作を許可
245
+ if not self.is_dragging_group:
246
+ return False
247
+
248
+ if self.is_dragging_group and self.drag_start_pos:
249
+ # ドラッグモード中 - 移動距離を記録するのみ(リアルタイム更新なし)
250
+ try:
251
+ interactor = self.main_window.plotter.interactor
252
+ current_pos = interactor.GetEventPosition()
253
+
254
+ dx = current_pos[0] - self.drag_start_pos[0]
255
+ dy = current_pos[1] - self.drag_start_pos[1]
256
+
257
+ if abs(dx) > 2 or abs(dy) > 2:
258
+ self.mouse_moved_during_drag = True
259
+ except Exception:
260
+ pass
261
+
262
+ # ドラッグ中はイベントを消費してカメラ回転を防ぐ
263
+ return True
264
+
265
+ # ホバー処理(ドラッグ中でない場合)
266
+ if self.group_atoms:
267
+ try:
268
+ interactor = self.main_window.plotter.interactor
269
+ current_pos = interactor.GetEventPosition()
270
+ picker = self.main_window.plotter.picker
271
+ picker.Pick(current_pos[0], current_pos[1], 0, self.main_window.plotter.renderer)
272
+
273
+ if picker.GetActor() is self.main_window.atom_actor:
274
+ picked_position = np.array(picker.GetPickPosition())
275
+ distances = np.linalg.norm(self.main_window.atom_positions_3d - picked_position, axis=1)
276
+ closest_atom_idx = np.argmin(distances)
277
+
278
+ if closest_atom_idx in self.group_atoms:
279
+ self.main_window.plotter.setCursor(Qt.CursorShape.OpenHandCursor)
280
+ else:
281
+ self.main_window.plotter.setCursor(Qt.CursorShape.ArrowCursor)
282
+ else:
283
+ self.main_window.plotter.setCursor(Qt.CursorShape.ArrowCursor)
284
+ except Exception:
285
+ pass
286
+
287
+ # ドラッグ中でない場合はカメラ回転を許可
288
+ return False
289
+
290
+ elif event.type() == QEvent.Type.MouseButtonRelease and event.button() == Qt.MouseButton.LeftButton:
291
+ # マウスリリース時の処理
292
+ if getattr(self, 'potential_drag', False) or (self.is_dragging_group and self.drag_start_pos):
293
+ try:
294
+ if self.is_dragging_group and self.mouse_moved_during_drag:
295
+ # ドラッグが実行された - CustomInteractorStyleに任せる(何もしない)
296
+ pass
297
+ else:
298
+ # マウスが閾値以下の移動 = 単なるクリック
299
+ # グループ内の原子をクリックした場合は選択/解除をトグル
300
+ if hasattr(self, 'clicked_atom_for_toggle'):
301
+ clicked_atom = self.clicked_atom_for_toggle
302
+ delattr(self, 'clicked_atom_for_toggle')
303
+ # ドラッグ状態をリセットしてからトグル処理
304
+ self.is_dragging_group = False
305
+ self.drag_start_pos = None
306
+ self.mouse_moved_during_drag = False
307
+ self.potential_drag = False
308
+ if hasattr(self, 'last_drag_positions'):
309
+ delattr(self, 'last_drag_positions')
310
+ # トグル処理を実行
311
+ self.on_atom_picked(clicked_atom)
312
+ try:
313
+ self.main_window.plotter.setCursor(Qt.CursorShape.ArrowCursor)
314
+ except Exception:
315
+ pass
316
+ return True
317
+
318
+ except Exception:
319
+ pass
320
+ finally:
321
+ # ドラッグ状態をリセット
322
+ self.is_dragging_group = False
323
+ self.drag_start_pos = None
324
+ self.mouse_moved_during_drag = False
325
+ self.potential_drag = False
326
+ # 保存していた位置情報をクリア
327
+ if hasattr(self, 'last_drag_positions'):
328
+ delattr(self, 'last_drag_positions')
329
+ try:
330
+ self.main_window.plotter.setCursor(Qt.CursorShape.ArrowCursor)
331
+ except Exception:
332
+ pass
333
+
334
+ return True # イベントを消費
335
+
336
+ # ドラッグ中でない場合は通常のリリース処理
337
+ return False
338
+
339
+ # その他のイベントは親クラスに渡す
340
+ return False
341
+
342
+ def on_atom_picked(self, atom_idx):
343
+ """原子がピックされたときに、その原子が属する連結成分全体を選択(複数グループ対応)"""
344
+ # ドラッグ中は選択を変更しない(ただしリリース時のトグルは許可)
345
+ if getattr(self, 'is_dragging_group', False):
346
+ return
347
+
348
+ # BFS/DFSで連結成分を探索
349
+ visited = set()
350
+ queue = [atom_idx]
351
+ visited.add(atom_idx)
352
+
353
+ while queue:
354
+ current_idx = queue.pop(0)
355
+ for bond_idx in range(self.mol.GetNumBonds()):
356
+ bond = self.mol.GetBondWithIdx(bond_idx)
357
+ begin_idx = bond.GetBeginAtomIdx()
358
+ end_idx = bond.GetEndAtomIdx()
359
+
360
+ if begin_idx == current_idx and end_idx not in visited:
361
+ visited.add(end_idx)
362
+ queue.append(end_idx)
363
+ elif end_idx == current_idx and begin_idx not in visited:
364
+ visited.add(begin_idx)
365
+ queue.append(begin_idx)
366
+
367
+ # 新しいグループとして追加または解除
368
+ if visited.issubset(self.group_atoms):
369
+ # すでに選択されている - 解除
370
+ self.group_atoms -= visited
371
+ else:
372
+ # 新しいグループを追加
373
+ self.group_atoms |= visited
374
+
375
+ self.selected_atoms.add(atom_idx)
376
+ self.show_atom_labels()
377
+ self.update_display()
378
+
379
+ def update_display(self):
380
+ if not self.group_atoms:
381
+ self.selection_label.setText("No group selected")
382
+ else:
383
+ atom_info = []
384
+ for atom_idx in sorted(self.group_atoms):
385
+ symbol = self.mol.GetAtomWithIdx(atom_idx).GetSymbol()
386
+ atom_info.append(f"{symbol}({atom_idx})")
387
+
388
+ self.selection_label.setText(f"Selected group: {len(self.group_atoms)} atoms - {', '.join(atom_info[:5])}{' ...' if len(atom_info) > 5 else ''}")
389
+
390
+ def show_atom_labels(self):
391
+ """選択されたグループの原子をハイライト表示(Ctrlクリックと同じスタイル)"""
392
+ self.clear_atom_labels()
393
+
394
+ if not self.group_atoms:
395
+ return
396
+
397
+ # 選択された原子のインデックスリストを作成
398
+ selected_indices = list(self.group_atoms)
399
+
400
+ # 選択された原子の位置を取得
401
+ selected_positions = self.main_window.atom_positions_3d[selected_indices]
402
+
403
+ # 原子の半径を少し大きくしてハイライト表示
404
+ selected_radii = np.array([VDW_RADII.get(
405
+ self.mol.GetAtomWithIdx(i).GetSymbol(), 0.4) * 1.3
406
+ for i in selected_indices])
407
+
408
+ # ハイライト用のデータセットを作成
409
+ highlight_source = pv.PolyData(selected_positions)
410
+ highlight_source['radii'] = selected_radii
411
+
412
+ # 黄色の半透明球でハイライト
413
+ highlight_glyphs = highlight_source.glyph(
414
+ scale='radii',
415
+ geom=pv.Sphere(radius=1.0, theta_resolution=16, phi_resolution=16),
416
+ orient=False
417
+ )
418
+
419
+ # ハイライトアクターを追加して保存(ピッキング不可に設定)
420
+ self.highlight_actor = self.main_window.plotter.add_mesh(
421
+ highlight_glyphs,
422
+ color='yellow',
423
+ opacity=0.3,
424
+ name='move_group_highlight',
425
+ pickable=False # ピッキングを無効化
426
+ )
427
+
428
+ self.main_window.plotter.render()
429
+
430
+ def clear_atom_labels(self):
431
+ """原子ハイライトをクリア"""
432
+ try:
433
+ self.main_window.plotter.remove_actor('move_group_highlight')
434
+ except Exception:
435
+ pass
436
+
437
+ if hasattr(self, 'highlight_actor'):
438
+ try:
439
+ self.main_window.plotter.remove_actor(self.highlight_actor)
440
+ except Exception:
441
+ pass
442
+ self.highlight_actor = None
443
+
444
+ try:
445
+ self.main_window.plotter.render()
446
+ except Exception:
447
+ pass
448
+
449
+ def reset_translation_inputs(self):
450
+ """Translation入力フィールドをリセット"""
451
+ self.x_trans_input.setText("0.0")
452
+ self.y_trans_input.setText("0.0")
453
+ self.z_trans_input.setText("0.0")
454
+
455
+ def apply_translation(self):
456
+ """選択したグループを並行移動"""
457
+ if not self.group_atoms:
458
+ QMessageBox.warning(self, "Warning", "Please select a group first.")
459
+ return
460
+
461
+ try:
462
+ dx = float(self.x_trans_input.text())
463
+ dy = float(self.y_trans_input.text())
464
+ dz = float(self.z_trans_input.text())
465
+ except ValueError:
466
+ QMessageBox.warning(self, "Warning", "Please enter valid translation values.")
467
+ return
468
+
469
+ translation_vector = np.array([dx, dy, dz])
470
+
471
+ conf = self.mol.GetConformer()
472
+ for atom_idx in self.group_atoms:
473
+ atom_pos = np.array(conf.GetAtomPosition(atom_idx))
474
+ new_pos = atom_pos + translation_vector
475
+ conf.SetAtomPosition(atom_idx, new_pos.tolist())
476
+ self.main_window.atom_positions_3d[atom_idx] = new_pos
477
+
478
+ self.main_window.draw_molecule_3d(self.mol)
479
+ self.main_window.update_chiral_labels()
480
+ self.show_atom_labels() # ラベルを再描画
481
+ self.main_window.push_undo_state()
482
+
483
+ def reset_rotation_inputs(self):
484
+ """Rotation入力フィールドをリセット"""
485
+ self.x_rot_input.setText("0.0")
486
+ self.y_rot_input.setText("0.0")
487
+ self.z_rot_input.setText("0.0")
488
+
489
+ def apply_rotation(self):
490
+ """選択したグループを回転"""
491
+ if not self.group_atoms:
492
+ QMessageBox.warning(self, "Warning", "Please select a group first.")
493
+ return
494
+
495
+ try:
496
+ rx = float(self.x_rot_input.text())
497
+ ry = float(self.y_rot_input.text())
498
+ rz = float(self.z_rot_input.text())
499
+ except ValueError:
500
+ QMessageBox.warning(self, "Warning", "Please enter valid rotation values.")
501
+ return
502
+
503
+ # 度をラジアンに変換
504
+ rx_rad = np.radians(rx)
505
+ ry_rad = np.radians(ry)
506
+ rz_rad = np.radians(rz)
507
+
508
+ # グループの重心を計算
509
+ conf = self.mol.GetConformer()
510
+ positions = []
511
+ for atom_idx in self.group_atoms:
512
+ pos = conf.GetAtomPosition(atom_idx)
513
+ positions.append([pos.x, pos.y, pos.z])
514
+ centroid = np.mean(positions, axis=0)
515
+
516
+ # 回転行列を作成
517
+ # X軸周り
518
+ Rx = np.array([
519
+ [1, 0, 0],
520
+ [0, np.cos(rx_rad), -np.sin(rx_rad)],
521
+ [0, np.sin(rx_rad), np.cos(rx_rad)]
522
+ ])
523
+ # Y軸周り
524
+ Ry = np.array([
525
+ [np.cos(ry_rad), 0, np.sin(ry_rad)],
526
+ [0, 1, 0],
527
+ [-np.sin(ry_rad), 0, np.cos(ry_rad)]
528
+ ])
529
+ # Z軸周り
530
+ Rz = np.array([
531
+ [np.cos(rz_rad), -np.sin(rz_rad), 0],
532
+ [np.sin(rz_rad), np.cos(rz_rad), 0],
533
+ [0, 0, 1]
534
+ ])
535
+
536
+ # 合成回転行列 (Z * Y * X)
537
+ R = Rz @ Ry @ Rx
538
+
539
+ # 各原子を回転
540
+ for atom_idx in self.group_atoms:
541
+ atom_pos = np.array(conf.GetAtomPosition(atom_idx))
542
+ # 重心を原点に移動
543
+ centered_pos = atom_pos - centroid
544
+ # 回転
545
+ rotated_pos = R @ centered_pos
546
+ # 重心を元に戻す
547
+ new_pos = rotated_pos + centroid
548
+ conf.SetAtomPosition(atom_idx, new_pos.tolist())
549
+ self.main_window.atom_positions_3d[atom_idx] = new_pos
550
+
551
+ self.main_window.draw_molecule_3d(self.mol)
552
+ self.main_window.update_chiral_labels()
553
+ self.show_atom_labels() # ラベルを再描画
554
+ self.main_window.push_undo_state()
555
+
556
+ def clear_selection(self):
557
+ """選択をクリア"""
558
+ self.selected_atoms.clear()
559
+ self.group_atoms.clear()
560
+ self.clear_atom_labels()
561
+ self.update_display()
562
+ # ドラッグ関連のフラグもリセット
563
+ self.is_dragging_group = False
564
+ self.drag_start_pos = None
565
+ if hasattr(self, 'last_drag_positions'):
566
+ delattr(self, 'last_drag_positions')
567
+
568
+ def closeEvent(self, event):
569
+ """ダイアログが閉じられる時の処理"""
570
+ self.clear_atom_labels()
571
+ self.disable_picking()
572
+ try:
573
+ self.main_window.draw_molecule_3d(self.mol)
574
+ except Exception:
575
+ pass
576
+ super().closeEvent(event)
577
+
578
+ def reject(self):
579
+ """キャンセル時の処理"""
580
+ self.clear_atom_labels()
581
+ self.disable_picking()
582
+ try:
583
+ self.main_window.draw_molecule_3d(self.mol)
584
+ except Exception:
585
+ pass
586
+ super().reject()
@@ -0,0 +1,72 @@
1
+ from PyQt6.QtWidgets import QDialog, QGridLayout, QPushButton
2
+ from PyQt6.QtGui import QColor
3
+ try:
4
+ from .constants import CPK_COLORS
5
+ except Exception:
6
+ from modules.constants import CPK_COLORS
7
+
8
+ from PyQt6.QtCore import pyqtSignal
9
+
10
+ class PeriodicTableDialog(QDialog):
11
+ element_selected = pyqtSignal(str)
12
+ def __init__(self, parent=None):
13
+ super().__init__(parent)
14
+ self.setWindowTitle("Select an Element")
15
+ layout = QGridLayout(self)
16
+ self.setLayout(layout)
17
+
18
+ elements = [
19
+ ('H',1,1), ('He',1,18),
20
+ ('Li',2,1), ('Be',2,2), ('B',2,13), ('C',2,14), ('N',2,15), ('O',2,16), ('F',2,17), ('Ne',2,18),
21
+ ('Na',3,1), ('Mg',3,2), ('Al',3,13), ('Si',3,14), ('P',3,15), ('S',3,16), ('Cl',3,17), ('Ar',3,18),
22
+ ('K',4,1), ('Ca',4,2), ('Sc',4,3), ('Ti',4,4), ('V',4,5), ('Cr',4,6), ('Mn',4,7), ('Fe',4,8),
23
+ ('Co',4,9), ('Ni',4,10), ('Cu',4,11), ('Zn',4,12), ('Ga',4,13), ('Ge',4,14), ('As',4,15), ('Se',4,16),
24
+ ('Br',4,17), ('Kr',4,18),
25
+ ('Rb',5,1), ('Sr',5,2), ('Y',5,3), ('Zr',5,4), ('Nb',5,5), ('Mo',5,6), ('Tc',5,7), ('Ru',5,8),
26
+ ('Rh',5,9), ('Pd',5,10), ('Ag',5,11), ('Cd',5,12), ('In',5,13), ('Sn',5,14), ('Sb',5,15), ('Te',5,16),
27
+ ('I',5,17), ('Xe',5,18),
28
+ ('Cs',6,1), ('Ba',6,2), ('Hf',6,4), ('Ta',6,5), ('W',6,6), ('Re',6,7), ('Os',6,8),
29
+ ('Ir',6,9), ('Pt',6,10), ('Au',6,11), ('Hg',6,12), ('Tl',6,13), ('Pb',6,14), ('Bi',6,15), ('Po',6,16),
30
+ ('At',6,17), ('Rn',6,18),
31
+ ('Fr',7,1), ('Ra',7,2), ('Rf',7,4), ('Db',7,5), ('Sg',7,6), ('Bh',7,7), ('Hs',7,8),
32
+ ('Mt',7,9), ('Ds',7,10), ('Rg',7,11), ('Cn',7,12), ('Nh',7,13), ('Fl',7,14), ('Mc',7,15), ('Lv',7,16),
33
+ ('Ts',7,17), ('Og',7,18),
34
+ # Lanthanides (placed on a separate row)
35
+ ('La',8,3), ('Ce',8,4), ('Pr',8,5), ('Nd',8,6), ('Pm',8,7), ('Sm',8,8), ('Eu',8,9), ('Gd',8,10), ('Tb',8,11),
36
+ ('Dy',8,12), ('Ho',8,13), ('Er',8,14), ('Tm',8,15), ('Yb',8,16), ('Lu',8,17),
37
+ # Actinides (separate row)
38
+ ('Ac',9,3), ('Th',9,4), ('Pa',9,5), ('U',9,6), ('Np',9,7), ('Pu',9,8), ('Am',9,9), ('Cm',9,10), ('Bk',9,11),
39
+ ('Cf',9,12), ('Es',9,13), ('Fm',9,14), ('Md',9,15), ('No',9,16), ('Lr',9,17),
40
+ ]
41
+ for symbol, row, col in elements:
42
+ b = QPushButton(symbol)
43
+ b.setFixedSize(40,40)
44
+
45
+ # Prefer saved user override (from parent.settings), otherwise use CPK_COLORS
46
+ try:
47
+ overrides = parent.settings.get('cpk_colors', {}) if parent and hasattr(parent, 'settings') else {}
48
+ override = overrides.get(symbol)
49
+ except Exception:
50
+ override = None
51
+ q_color = QColor(override) if override else CPK_COLORS.get(symbol, CPK_COLORS['DEFAULT'])
52
+
53
+ # 背景色の輝度を計算して、文字色を黒か白に決定
54
+ # 輝度 = (R*299 + G*587 + B*114) / 1000
55
+ brightness = (q_color.red() * 299 + q_color.green() * 587 + q_color.blue() * 114) / 1000
56
+ text_color = "white" if brightness < 128 else "black"
57
+
58
+ # ボタンのスタイルシートを設定
59
+ b.setStyleSheet(
60
+ f"background-color: {q_color.name()};"
61
+ f"color: {text_color};"
62
+ "border: 1px solid #555;"
63
+ "font-weight: bold;"
64
+ )
65
+
66
+ b.clicked.connect(self.on_button_clicked)
67
+ layout.addWidget(b, row, col)
68
+
69
+ def on_button_clicked(self):
70
+ b=self.sender()
71
+ self.element_selected.emit(b.text())
72
+ self.accept()