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,737 @@
1
+ import numpy as np
2
+
3
+ from vtkmodules.vtkInteractionStyle import vtkInteractorStyleTrackballCamera
4
+
5
+ from PyQt6.QtWidgets import QApplication
6
+
7
+ from PyQt6.QtCore import (
8
+ Qt
9
+ )
10
+
11
+ try:
12
+ from .constants import pt
13
+ except Exception:
14
+ from modules.constants import pt
15
+ try:
16
+ from .move_group_dialog import MoveGroupDialog
17
+ except Exception:
18
+ from modules.move_group_dialog import MoveGroupDialog
19
+
20
+ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
21
+ def __init__(self, main_window):
22
+ super().__init__()
23
+ self.main_window = main_window
24
+ # カスタム状態を管理するフラグを一つに絞ります
25
+ self._is_dragging_atom = False
26
+ # undoスタックのためのフラグ
27
+ self.is_dragging = False
28
+ # 回転操作を検出するためのフラグ
29
+ self._mouse_moved_during_drag = False
30
+ self._mouse_press_pos = None
31
+
32
+ self.AddObserver("LeftButtonPressEvent", self.on_left_button_down)
33
+ self.AddObserver("RightButtonPressEvent", self.on_right_button_down)
34
+ self.AddObserver("MouseMoveEvent", self.on_mouse_move)
35
+ self.AddObserver("LeftButtonReleaseEvent", self.on_left_button_up)
36
+ self.AddObserver("RightButtonReleaseEvent", self.on_right_button_up)
37
+
38
+ def on_left_button_down(self, obj, event):
39
+ """
40
+ クリック時の処理を振り分けます。
41
+ 原子を掴めた場合のみカスタム動作に入り、それ以外は親クラス(カメラ回転)に任せます。
42
+ """
43
+ mw = self.main_window
44
+
45
+ # 前回のドラッグ状態をクリア(トリプルクリック/ダブルクリック対策)
46
+ self._is_dragging_atom = False
47
+ self.is_dragging = False
48
+ self._mouse_moved_during_drag = False
49
+ self._mouse_press_pos = None
50
+
51
+ # Move Groupダイアログが開いている場合の処理
52
+ move_group_dialog = None
53
+ try:
54
+ for widget in QApplication.topLevelWidgets():
55
+ if isinstance(widget, MoveGroupDialog) and widget.isVisible():
56
+ move_group_dialog = widget
57
+ break
58
+ except Exception:
59
+ pass
60
+
61
+ if move_group_dialog and move_group_dialog.group_atoms:
62
+ # グループが選択されている場合、グループドラッグ処理
63
+ click_pos = self.GetInteractor().GetEventPosition()
64
+ picker = mw.plotter.picker
65
+ picker.Pick(click_pos[0], click_pos[1], 0, mw.plotter.renderer)
66
+
67
+ clicked_atom_idx = None
68
+ if picker.GetActor() is mw.atom_actor:
69
+ picked_position = np.array(picker.GetPickPosition())
70
+ distances = np.linalg.norm(mw.atom_positions_3d - picked_position, axis=1)
71
+ closest_atom_idx = np.argmin(distances)
72
+
73
+ if 0 <= closest_atom_idx < mw.current_mol.GetNumAtoms():
74
+ atom = mw.current_mol.GetAtomWithIdx(int(closest_atom_idx))
75
+ if atom:
76
+ atomic_num = atom.GetAtomicNum()
77
+ vdw_radius = pt.GetRvdw(atomic_num)
78
+ click_threshold = vdw_radius * 1.5
79
+
80
+ if distances[closest_atom_idx] < click_threshold:
81
+ clicked_atom_idx = int(closest_atom_idx)
82
+
83
+ # グループ内の原子がクリックされた場合
84
+ if clicked_atom_idx is not None:
85
+ if clicked_atom_idx in move_group_dialog.group_atoms:
86
+ # 既存グループ内の原子 - ドラッグ準備
87
+ move_group_dialog._is_dragging_group_vtk = True
88
+ move_group_dialog._drag_atom_idx = clicked_atom_idx
89
+ move_group_dialog._drag_start_pos = click_pos
90
+ move_group_dialog._mouse_moved = False
91
+ # 初期位置を保存
92
+ move_group_dialog._initial_positions = {}
93
+ conf = mw.current_mol.GetConformer()
94
+ for atom_idx in move_group_dialog.group_atoms:
95
+ pos = conf.GetAtomPosition(atom_idx)
96
+ move_group_dialog._initial_positions[atom_idx] = np.array([pos.x, pos.y, pos.z])
97
+ mw.plotter.setCursor(Qt.CursorShape.ClosedHandCursor)
98
+ return # カメラ回転を無効化
99
+ else:
100
+ # グループ外の原子をクリック - BFS/DFSで連結成分を探索
101
+ visited = set()
102
+ queue = [clicked_atom_idx]
103
+ visited.add(clicked_atom_idx)
104
+
105
+ while queue:
106
+ current_idx = queue.pop(0)
107
+ for bond_idx in range(mw.current_mol.GetNumBonds()):
108
+ bond = mw.current_mol.GetBondWithIdx(bond_idx)
109
+ begin_idx = bond.GetBeginAtomIdx()
110
+ end_idx = bond.GetEndAtomIdx()
111
+
112
+ if begin_idx == current_idx and end_idx not in visited:
113
+ visited.add(end_idx)
114
+ queue.append(end_idx)
115
+ elif end_idx == current_idx and begin_idx not in visited:
116
+ visited.add(begin_idx)
117
+ queue.append(begin_idx)
118
+
119
+ # Ctrlキーが押されている場合のみ複数グループ選択
120
+ is_ctrl_pressed = bool(QApplication.keyboardModifiers() & Qt.KeyboardModifier.ControlModifier)
121
+
122
+ if is_ctrl_pressed:
123
+ # Ctrl + クリック: 追加または解除
124
+ if visited.issubset(move_group_dialog.group_atoms):
125
+ # すでに選択されている - 解除
126
+ move_group_dialog.group_atoms -= visited
127
+ else:
128
+ # 新しいグループを追加
129
+ move_group_dialog.group_atoms |= visited
130
+ else:
131
+ # 通常のクリック: 既存の選択を置き換え
132
+ move_group_dialog.group_atoms = visited.copy()
133
+
134
+ move_group_dialog.selected_atoms.add(clicked_atom_idx)
135
+ move_group_dialog.show_atom_labels()
136
+ move_group_dialog.update_display()
137
+ return
138
+ else:
139
+ # 原子以外をクリック - 全選択を解除
140
+ move_group_dialog.group_atoms.clear()
141
+ move_group_dialog.selected_atoms.clear()
142
+ move_group_dialog.clear_atom_labels()
143
+ move_group_dialog.update_display()
144
+ # カメラ回転を許可
145
+ super(CustomInteractorStyle, self).OnLeftButtonDown()
146
+ return
147
+
148
+ is_temp_mode = bool(QApplication.keyboardModifiers() & Qt.KeyboardModifier.AltModifier)
149
+ is_edit_active = mw.is_3d_edit_mode or is_temp_mode
150
+
151
+ # Ctrl+クリックで原子選択(3D編集用)
152
+ is_ctrl_click = bool(QApplication.keyboardModifiers() & Qt.KeyboardModifier.ControlModifier)
153
+
154
+ # 測定モードが有効な場合の処理
155
+ if mw.measurement_mode and mw.current_mol:
156
+ click_pos = self.GetInteractor().GetEventPosition()
157
+ self._mouse_press_pos = click_pos # マウスプレス位置を記録
158
+ self._mouse_moved_during_drag = False # 移動フラグをリセット
159
+
160
+ picker = mw.plotter.picker
161
+
162
+ # 通常のピック処理を実行
163
+ picker.Pick(click_pos[0], click_pos[1], 0, mw.plotter.renderer)
164
+
165
+ # 原子がクリックされた場合のみ特別処理
166
+ if picker.GetActor() is mw.atom_actor:
167
+ picked_position = np.array(picker.GetPickPosition())
168
+ distances = np.linalg.norm(mw.atom_positions_3d - picked_position, axis=1)
169
+ closest_atom_idx = np.argmin(distances)
170
+
171
+ # 範囲チェックを追加
172
+ if 0 <= closest_atom_idx < mw.current_mol.GetNumAtoms():
173
+ # クリック閾値チェック
174
+ atom = mw.current_mol.GetAtomWithIdx(int(closest_atom_idx))
175
+ if atom:
176
+ atomic_num = atom.GetAtomicNum()
177
+ vdw_radius = pt.GetRvdw(atomic_num)
178
+ click_threshold = vdw_radius * 1.5
179
+
180
+ if distances[closest_atom_idx] < click_threshold:
181
+ mw.handle_measurement_atom_selection(int(closest_atom_idx))
182
+ return # 原子選択処理完了、カメラ回転は無効
183
+
184
+ # 測定モードで原子以外をクリックした場合は計測選択をクリア
185
+ # ただし、これは通常のカメラ回転も許可する
186
+ self._is_dragging_atom = False
187
+ super().OnLeftButtonDown()
188
+ return
189
+
190
+ # Ctrl+クリックの原子選択機能は無効化(Move Group機能で代替)
191
+ # if is_ctrl_click and mw.current_mol:
192
+ # ... (無効化)
193
+
194
+ # 3D分子(mw.current_mol)が存在する場合のみ、原子の選択処理を実行
195
+ if is_edit_active and mw.current_mol:
196
+ click_pos = self.GetInteractor().GetEventPosition()
197
+ picker = mw.plotter.picker
198
+ picker.Pick(click_pos[0], click_pos[1], 0, mw.plotter.renderer)
199
+
200
+ if picker.GetActor() is mw.atom_actor:
201
+ picked_position = np.array(picker.GetPickPosition())
202
+ distances = np.linalg.norm(mw.atom_positions_3d - picked_position, axis=1)
203
+ closest_atom_idx = np.argmin(distances)
204
+
205
+ # 範囲チェックを追加
206
+ if 0 <= closest_atom_idx < mw.current_mol.GetNumAtoms():
207
+ # RDKitのMolオブジェクトから原子を安全に取得
208
+ atom = mw.current_mol.GetAtomWithIdx(int(closest_atom_idx))
209
+ if atom:
210
+ atomic_num = atom.GetAtomicNum()
211
+ vdw_radius = pt.GetRvdw(atomic_num)
212
+ click_threshold = vdw_radius * 1.5
213
+
214
+ if distances[closest_atom_idx] < click_threshold:
215
+ # 原子を掴むことに成功した場合
216
+ self._is_dragging_atom = True
217
+ self.is_dragging = False
218
+ mw.dragged_atom_info = {'id': int(closest_atom_idx)}
219
+ mw.plotter.setCursor(Qt.CursorShape.ClosedHandCursor)
220
+ return # 親クラスのカメラ回転を呼ばない
221
+
222
+ self._is_dragging_atom = False
223
+ super().OnLeftButtonDown()
224
+
225
+ def on_right_button_down(self, obj, event):
226
+ """
227
+ 右クリック時の処理。Move Groupダイアログが開いている場合はグループ回転を開始。
228
+ """
229
+ mw = self.main_window
230
+
231
+ # Move Groupダイアログが開いているか確認
232
+ move_group_dialog = None
233
+ try:
234
+ for widget in QApplication.topLevelWidgets():
235
+ if isinstance(widget, MoveGroupDialog) and widget.isVisible():
236
+ move_group_dialog = widget
237
+ break
238
+ except Exception:
239
+ pass
240
+
241
+ if move_group_dialog and move_group_dialog.group_atoms:
242
+ # グループが選択されている場合、回転ドラッグを開始
243
+ click_pos = self.GetInteractor().GetEventPosition()
244
+ picker = mw.plotter.picker
245
+ picker.Pick(click_pos[0], click_pos[1], 0, mw.plotter.renderer)
246
+
247
+ clicked_atom_idx = None
248
+ if picker.GetActor() is mw.atom_actor:
249
+ picked_position = np.array(picker.GetPickPosition())
250
+ distances = np.linalg.norm(mw.atom_positions_3d - picked_position, axis=1)
251
+ closest_atom_idx = np.argmin(distances)
252
+
253
+ if 0 <= closest_atom_idx < mw.current_mol.GetNumAtoms():
254
+ atom = mw.current_mol.GetAtomWithIdx(int(closest_atom_idx))
255
+ if atom:
256
+ atomic_num = atom.GetAtomicNum()
257
+ vdw_radius = pt.GetRvdw(atomic_num)
258
+ click_threshold = vdw_radius * 1.5
259
+
260
+ if distances[closest_atom_idx] < click_threshold:
261
+ clicked_atom_idx = int(closest_atom_idx)
262
+
263
+ # グループ内の原子がクリックされた場合、回転ドラッグを開始
264
+ if clicked_atom_idx is not None and clicked_atom_idx in move_group_dialog.group_atoms:
265
+ move_group_dialog._is_rotating_group_vtk = True
266
+ move_group_dialog._rotation_start_pos = click_pos
267
+ move_group_dialog._rotation_mouse_moved = False
268
+ move_group_dialog._rotation_atom_idx = clicked_atom_idx # 掴んだ原子を記録
269
+
270
+ # 初期位置と重心を保存
271
+ move_group_dialog._initial_positions = {}
272
+ conf = mw.current_mol.GetConformer()
273
+ centroid = np.zeros(3)
274
+ for atom_idx in move_group_dialog.group_atoms:
275
+ pos = conf.GetAtomPosition(atom_idx)
276
+ pos_array = np.array([pos.x, pos.y, pos.z])
277
+ move_group_dialog._initial_positions[atom_idx] = pos_array
278
+ centroid += pos_array
279
+ centroid /= len(move_group_dialog.group_atoms)
280
+ move_group_dialog._group_centroid = centroid
281
+
282
+ mw.plotter.setCursor(Qt.CursorShape.ClosedHandCursor)
283
+ return # カメラ回転を無効化
284
+
285
+ # 通常の右クリック処理
286
+ super().OnRightButtonDown()
287
+
288
+ def on_mouse_move(self, obj, event):
289
+ """
290
+ マウス移動時の処理。原子ドラッグ中か、それ以外(カメラ回転+ホバー)かをハンドリングします。
291
+ """
292
+ mw = self.main_window
293
+
294
+ # Move Groupダイアログのドラッグ処理
295
+ move_group_dialog = None
296
+ try:
297
+ for widget in QApplication.topLevelWidgets():
298
+ if isinstance(widget, MoveGroupDialog) and widget.isVisible():
299
+ move_group_dialog = widget
300
+ break
301
+ except Exception:
302
+ pass
303
+
304
+ if move_group_dialog and getattr(move_group_dialog, '_is_dragging_group_vtk', False):
305
+ # グループをドラッグ中 - 移動距離を記録するのみ
306
+ interactor = self.GetInteractor()
307
+ current_pos = interactor.GetEventPosition()
308
+
309
+ dx = current_pos[0] - move_group_dialog._drag_start_pos[0]
310
+ dy = current_pos[1] - move_group_dialog._drag_start_pos[1]
311
+
312
+ if abs(dx) > 2 or abs(dy) > 2:
313
+ move_group_dialog._mouse_moved = True
314
+
315
+ return # カメラ回転を無効化
316
+
317
+ # グループ回転中の処理
318
+ if move_group_dialog and getattr(move_group_dialog, '_is_rotating_group_vtk', False):
319
+ interactor = self.GetInteractor()
320
+ current_pos = interactor.GetEventPosition()
321
+
322
+ dx = current_pos[0] - move_group_dialog._rotation_start_pos[0]
323
+ dy = current_pos[1] - move_group_dialog._rotation_start_pos[1]
324
+
325
+ if abs(dx) > 2 or abs(dy) > 2:
326
+ move_group_dialog._rotation_mouse_moved = True
327
+
328
+ return # カメラ回転を無効化
329
+
330
+ interactor = self.GetInteractor()
331
+
332
+ # マウス移動があったことを記録
333
+ if self._mouse_press_pos is not None:
334
+ current_pos = interactor.GetEventPosition()
335
+ if abs(current_pos[0] - self._mouse_press_pos[0]) > 3 or abs(current_pos[1] - self._mouse_press_pos[1]) > 3:
336
+ self._mouse_moved_during_drag = True
337
+
338
+ if self._is_dragging_atom and mw.dragged_atom_info is not None:
339
+ # カスタムの原子ドラッグ処理
340
+ self.is_dragging = True
341
+ atom_id = mw.dragged_atom_info['id']
342
+ # We intentionally do NOT update visible coordinates or the
343
+ # authoritative atom position during mouse-move while dragging.
344
+ # The UX requirement here is that atoms need not visibly move
345
+ # while the mouse is being dragged. Compute and apply the final
346
+ # world-coordinate only once on mouse release (on_left_button_up).
347
+ # Keep minimal state: mark that a drag occurred (is_dragging)
348
+ # and allow the release handler to compute the final position.
349
+ # This avoids duplicate updates and simplifies event ordering.
350
+ else:
351
+ # カメラ回転処理を親クラスに任せます
352
+ super().OnMouseMove()
353
+
354
+ # その後、カーソルの表示を更新します
355
+ is_edit_active = mw.is_3d_edit_mode or interactor.GetAltKey()
356
+ if is_edit_active:
357
+ # 編集がアクティブな場合のみ、原子のホバーチェックを行う
358
+ atom_under_cursor = False
359
+ click_pos = interactor.GetEventPosition()
360
+ picker = mw.plotter.picker
361
+ picker.Pick(click_pos[0], click_pos[1], 0, mw.plotter.renderer)
362
+ if picker.GetActor() is mw.atom_actor:
363
+ atom_under_cursor = True
364
+
365
+ if atom_under_cursor:
366
+ mw.plotter.setCursor(Qt.CursorShape.OpenHandCursor)
367
+ else:
368
+ mw.plotter.setCursor(Qt.CursorShape.ArrowCursor)
369
+ else:
370
+ mw.plotter.setCursor(Qt.CursorShape.ArrowCursor)
371
+
372
+ def on_left_button_up(self, obj, event):
373
+ """
374
+ クリック終了時の処理。状態をリセットします。
375
+ """
376
+ mw = self.main_window
377
+
378
+ # Move Groupダイアログのドラッグ終了処理
379
+ move_group_dialog = None
380
+ try:
381
+ for widget in QApplication.topLevelWidgets():
382
+ if isinstance(widget, MoveGroupDialog) and widget.isVisible():
383
+ move_group_dialog = widget
384
+ break
385
+ except Exception:
386
+ pass
387
+
388
+ # ダブルクリック/トリプルクリックで状態が混乱するのを防ぐ(Move Group用)
389
+ if move_group_dialog:
390
+ if getattr(move_group_dialog, '_is_dragging_group_vtk', False) and not getattr(move_group_dialog, '_mouse_moved', False):
391
+ # ドラッグしていない状態で複数クリックされた場合は状態をリセット
392
+ move_group_dialog._is_dragging_group_vtk = False
393
+ move_group_dialog._drag_start_pos = None
394
+ move_group_dialog._mouse_moved = False
395
+ if hasattr(move_group_dialog, '_initial_positions'):
396
+ delattr(move_group_dialog, '_initial_positions')
397
+
398
+ if move_group_dialog and getattr(move_group_dialog, '_is_dragging_group_vtk', False):
399
+ if getattr(move_group_dialog, '_mouse_moved', False):
400
+ # ドラッグが実行された - リリース時に座標を更新
401
+ try:
402
+ interactor = self.GetInteractor()
403
+ renderer = mw.plotter.renderer
404
+ current_pos = interactor.GetEventPosition()
405
+ conf = mw.current_mol.GetConformer()
406
+
407
+ # ドラッグ原子の初期位置
408
+ drag_atom_initial_pos = move_group_dialog._initial_positions[move_group_dialog._drag_atom_idx]
409
+
410
+ # スクリーン座標からワールド座標への変換
411
+ renderer.SetWorldPoint(drag_atom_initial_pos[0], drag_atom_initial_pos[1], drag_atom_initial_pos[2], 1.0)
412
+ renderer.WorldToDisplay()
413
+ display_coords = renderer.GetDisplayPoint()
414
+
415
+ new_display_pos = (current_pos[0], current_pos[1], display_coords[2])
416
+ renderer.SetDisplayPoint(new_display_pos[0], new_display_pos[1], new_display_pos[2])
417
+ renderer.DisplayToWorld()
418
+ new_world_coords = renderer.GetWorldPoint()
419
+
420
+ # 移動ベクトル
421
+ translation_vector = np.array([
422
+ new_world_coords[0] - drag_atom_initial_pos[0],
423
+ new_world_coords[1] - drag_atom_initial_pos[1],
424
+ new_world_coords[2] - drag_atom_initial_pos[2]
425
+ ])
426
+
427
+ # グループ全体を移動
428
+ for atom_idx in move_group_dialog.group_atoms:
429
+ initial_pos = move_group_dialog._initial_positions[atom_idx]
430
+ new_pos = initial_pos + translation_vector
431
+ conf.SetAtomPosition(atom_idx, new_pos.tolist())
432
+ mw.atom_positions_3d[atom_idx] = new_pos
433
+
434
+ # 3D表示を更新
435
+ mw.draw_molecule_3d(mw.current_mol)
436
+ mw.update_chiral_labels()
437
+ move_group_dialog.show_atom_labels()
438
+ mw.push_undo_state()
439
+ except Exception as e:
440
+ print(f"Error finalizing group drag: {e}")
441
+ else:
442
+ # ドラッグがなかった = クリックのみ → トグル処理
443
+ if hasattr(move_group_dialog, '_drag_atom_idx'):
444
+ clicked_atom = move_group_dialog._drag_atom_idx
445
+ try:
446
+ move_group_dialog.on_atom_picked(clicked_atom)
447
+ except Exception as e:
448
+ print(f"Error in toggle: {e}")
449
+
450
+ # 状態をリセット(完全なクリーンアップ)
451
+ move_group_dialog._is_dragging_group_vtk = False
452
+ move_group_dialog._drag_start_pos = None
453
+ move_group_dialog._mouse_moved = False
454
+ if hasattr(move_group_dialog, '_initial_positions'):
455
+ delattr(move_group_dialog, '_initial_positions')
456
+ if hasattr(move_group_dialog, '_drag_atom_idx'):
457
+ delattr(move_group_dialog, '_drag_atom_idx')
458
+
459
+ # CustomInteractorStyleの状態もクリア
460
+ self._is_dragging_atom = False
461
+ self.is_dragging = False
462
+ self._mouse_moved_during_drag = False
463
+ self._mouse_press_pos = None
464
+
465
+ try:
466
+ mw.plotter.setCursor(Qt.CursorShape.ArrowCursor)
467
+ except Exception:
468
+ pass
469
+ return
470
+
471
+ # 計測モードで、マウスが動いていない場合(つまりクリック)の処理
472
+ if mw.measurement_mode and not self._mouse_moved_during_drag and self._mouse_press_pos is not None:
473
+ click_pos = self.GetInteractor().GetEventPosition()
474
+ picker = mw.plotter.picker
475
+ picker.Pick(click_pos[0], click_pos[1], 0, mw.plotter.renderer)
476
+
477
+ # 原子がクリックされていない場合は測定選択をクリア
478
+ if picker.GetActor() is not mw.atom_actor:
479
+ mw.clear_measurement_selection()
480
+
481
+ if self._is_dragging_atom:
482
+ # カスタムドラッグの後始末
483
+ if self.is_dragging:
484
+ if mw.current_mol and mw.current_mol.GetNumConformers() > 0:
485
+ try:
486
+ # Before applying conformer updates, compute the final
487
+ # world coordinates for the dragged atom based on the
488
+ # release pointer position. During the drag we did not
489
+ # update mw.atom_positions_3d (to keep the visuals
490
+ # static). Now compute the final position for the
491
+ # dragged atom and store it into mw.atom_positions_3d
492
+ # so the conformer update loop below will pick it up.
493
+ atom_id = None
494
+ try:
495
+ atom_id = mw.dragged_atom_info.get('id') if mw.dragged_atom_info else None
496
+ except Exception:
497
+ atom_id = None
498
+
499
+ if atom_id is not None:
500
+ try:
501
+ interactor = self.GetInteractor()
502
+ renderer = mw.plotter.renderer
503
+ current_display_pos = interactor.GetEventPosition()
504
+ conf = mw.current_mol.GetConformer()
505
+ # Use the atom's current 3D position to obtain a
506
+ # display-space depth (z) value, then replace the
507
+ # x/y with the pointer position to project back to
508
+ # world coordinates at that depth.
509
+ pos_3d = conf.GetAtomPosition(atom_id)
510
+ renderer.SetWorldPoint(pos_3d.x, pos_3d.y, pos_3d.z, 1.0)
511
+ renderer.WorldToDisplay()
512
+ display_coords = renderer.GetDisplayPoint()
513
+ new_display_pos = (current_display_pos[0], current_display_pos[1], display_coords[2])
514
+ renderer.SetDisplayPoint(new_display_pos[0], new_display_pos[1], new_display_pos[2])
515
+ renderer.DisplayToWorld()
516
+ new_world_coords_tuple = renderer.GetWorldPoint()
517
+ new_world_coords = list(new_world_coords_tuple)[:3]
518
+ # Ensure the container supports assignment
519
+ try:
520
+ mw.atom_positions_3d[atom_id] = new_world_coords
521
+ except Exception:
522
+ # If atom_positions_3d is immutable or shaped
523
+ # differently, attempt a safe conversion.
524
+ try:
525
+ ap = list(mw.atom_positions_3d)
526
+ ap[atom_id] = new_world_coords
527
+ mw.atom_positions_3d = ap
528
+ except Exception:
529
+ pass
530
+ except Exception:
531
+ # If final-position computation fails, continue
532
+ # and apply whatever state is available.
533
+ pass
534
+
535
+ # Apply the (now updated) positions to the RDKit conformer
536
+ # exactly once. This ensures the conformer is
537
+ # authoritative and avoids double-moves.
538
+ conf = mw.current_mol.GetConformer()
539
+ for i in range(mw.current_mol.GetNumAtoms()):
540
+ try:
541
+ pos = mw.atom_positions_3d[i]
542
+ conf.SetAtomPosition(i, pos.tolist())
543
+ except Exception:
544
+ # Skip individual failures but continue applying
545
+ # other atom positions.
546
+ pass
547
+ except Exception:
548
+ # If applying positions fails, continue to redraw from
549
+ # whatever authoritative state is available.
550
+ pass
551
+
552
+ # Redraw once and push undo state
553
+ try:
554
+ mw.draw_molecule_3d(mw.current_mol)
555
+ except Exception:
556
+ pass
557
+ mw.push_undo_state()
558
+ mw.dragged_atom_info = None
559
+ # Refresh overlays and labels that depend on atom_positions_3d. Do
560
+ # not overwrite mw.atom_positions_3d here — it already reflects the
561
+ # positions the user dragged to. Only update dependent displays.
562
+ try:
563
+ mw.update_3d_selection_display()
564
+ except Exception:
565
+ pass
566
+ try:
567
+ mw.update_measurement_labels_display()
568
+ except Exception:
569
+ pass
570
+ try:
571
+ mw.update_2d_measurement_labels()
572
+ except Exception:
573
+ pass
574
+ try:
575
+ mw.show_all_atom_info()
576
+ except Exception:
577
+ pass
578
+ except Exception:
579
+ # Do not allow a failure here to interrupt release flow
580
+ pass
581
+ else:
582
+ # カメラ回転の後始末を親クラスに任せます
583
+ super().OnLeftButtonUp()
584
+
585
+ # 状態をリセット(完全なクリーンアップ)
586
+ self._is_dragging_atom = False
587
+ self.is_dragging = False
588
+ self._mouse_press_pos = None
589
+ self._mouse_moved_during_drag = False
590
+
591
+ # Move Group関連の状態もクリア
592
+ try:
593
+ move_group_dialog = None
594
+ for widget in QApplication.topLevelWidgets():
595
+ if isinstance(widget, MoveGroupDialog) and widget.isVisible():
596
+ move_group_dialog = widget
597
+ break
598
+
599
+ if move_group_dialog:
600
+ move_group_dialog._is_dragging_group_vtk = False
601
+ move_group_dialog._drag_start_pos = None
602
+ move_group_dialog._mouse_moved = False
603
+ if hasattr(move_group_dialog, '_initial_positions'):
604
+ delattr(move_group_dialog, '_initial_positions')
605
+ if hasattr(move_group_dialog, '_drag_atom_idx'):
606
+ delattr(move_group_dialog, '_drag_atom_idx')
607
+ except Exception:
608
+ pass
609
+
610
+ # ピックリセットは測定モードで実際に問題が発生した場合のみ行う
611
+ # (通常のドラッグ回転では行わない)
612
+
613
+ # ボタンを離した後のカーソル表示を最新の状態に更新
614
+ self.on_mouse_move(obj, event)
615
+
616
+ # 2Dビューにフォーカスを戻し、ショートカットキーなどが使えるようにする
617
+ if mw and mw.view_2d:
618
+ mw.view_2d.setFocus()
619
+
620
+ def on_right_button_up(self, obj, event):
621
+ """
622
+ 右クリック終了時の処理。グループ回転を確定。
623
+ """
624
+ mw = self.main_window
625
+
626
+ # Move Groupダイアログの回転終了処理
627
+ move_group_dialog = None
628
+ try:
629
+ for widget in QApplication.topLevelWidgets():
630
+ if isinstance(widget, MoveGroupDialog) and widget.isVisible():
631
+ move_group_dialog = widget
632
+ break
633
+ except Exception:
634
+ pass
635
+
636
+ if move_group_dialog and getattr(move_group_dialog, '_is_rotating_group_vtk', False):
637
+ # 回転モードで右クリックリリース - 選択を保持
638
+ if getattr(move_group_dialog, '_rotation_mouse_moved', False):
639
+ # 回転が実行された - リリース時に回転を適用
640
+ try:
641
+ interactor = self.GetInteractor()
642
+ renderer = mw.plotter.renderer
643
+ current_pos = interactor.GetEventPosition()
644
+ conf = mw.current_mol.GetConformer()
645
+ centroid = move_group_dialog._group_centroid
646
+
647
+ # 掴んだ原子の初期位置
648
+ if not hasattr(move_group_dialog, '_rotation_atom_idx'):
649
+ # 最初に掴んだ原子のインデックスを保存
650
+ move_group_dialog._rotation_atom_idx = next(iter(move_group_dialog.group_atoms))
651
+
652
+ grabbed_atom_idx = move_group_dialog._rotation_atom_idx
653
+ grabbed_initial_pos = move_group_dialog._initial_positions[grabbed_atom_idx]
654
+
655
+ # 開始位置のスクリーン座標を取得
656
+ renderer.SetWorldPoint(grabbed_initial_pos[0], grabbed_initial_pos[1], grabbed_initial_pos[2], 1.0)
657
+ renderer.WorldToDisplay()
658
+ start_display = renderer.GetDisplayPoint()
659
+
660
+ # 現在のマウス位置をワールド座標に変換(同じ深度で)
661
+ renderer.SetDisplayPoint(current_pos[0], current_pos[1], start_display[2])
662
+ renderer.DisplayToWorld()
663
+ target_world = renderer.GetWorldPoint()
664
+ target_pos = np.array([target_world[0], target_world[1], target_world[2]])
665
+
666
+ # 重心から見た、掴んだ原子の初期ベクトルと目標ベクトル
667
+ v1 = grabbed_initial_pos - centroid
668
+ v2 = target_pos - centroid
669
+
670
+ # ベクトルを正規化
671
+ v1_norm = np.linalg.norm(v1)
672
+ v2_norm = np.linalg.norm(v2)
673
+
674
+ if v1_norm > 1e-6 and v2_norm > 1e-6:
675
+ v1_normalized = v1 / v1_norm
676
+ v2_normalized = v2 / v2_norm
677
+
678
+ # 回転軸(外積)
679
+ rotation_axis = np.cross(v1_normalized, v2_normalized)
680
+ axis_norm = np.linalg.norm(rotation_axis)
681
+
682
+ if axis_norm > 1e-6:
683
+ rotation_axis = rotation_axis / axis_norm
684
+
685
+ # 回転角(内積)
686
+ cos_angle = np.clip(np.dot(v1_normalized, v2_normalized), -1.0, 1.0)
687
+ angle = np.arccos(cos_angle)
688
+
689
+ # Rodriguesの回転公式で回転行列を作成
690
+ K = np.array([
691
+ [0, -rotation_axis[2], rotation_axis[1]],
692
+ [rotation_axis[2], 0, -rotation_axis[0]],
693
+ [-rotation_axis[1], rotation_axis[0], 0]
694
+ ])
695
+
696
+ rot_matrix = np.eye(3) + np.sin(angle) * K + (1 - np.cos(angle)) * (K @ K)
697
+
698
+ # グループ全体を重心周りに回転
699
+ for atom_idx in move_group_dialog.group_atoms:
700
+ initial_pos = move_group_dialog._initial_positions[atom_idx]
701
+ # 重心からの相対座標
702
+ relative_pos = initial_pos - centroid
703
+ # 回転を適用
704
+ rotated_pos = rot_matrix @ relative_pos
705
+ # 絶対座標に戻す
706
+ new_pos = rotated_pos + centroid
707
+
708
+ conf.SetAtomPosition(atom_idx, new_pos.tolist())
709
+ mw.atom_positions_3d[atom_idx] = new_pos
710
+
711
+ # 3D表示を更新
712
+ mw.draw_molecule_3d(mw.current_mol)
713
+ mw.update_chiral_labels()
714
+ move_group_dialog.show_atom_labels()
715
+ mw.push_undo_state()
716
+ except Exception as e:
717
+ print(f"Error finalizing group rotation: {e}")
718
+
719
+ # 状態をリセット
720
+ move_group_dialog._is_rotating_group_vtk = False
721
+ move_group_dialog._rotation_start_pos = None
722
+ move_group_dialog._rotation_mouse_moved = False
723
+ if hasattr(move_group_dialog, '_initial_positions'):
724
+ delattr(move_group_dialog, '_initial_positions')
725
+ if hasattr(move_group_dialog, '_group_centroid'):
726
+ delattr(move_group_dialog, '_group_centroid')
727
+ if hasattr(move_group_dialog, '_rotation_atom_idx'):
728
+ delattr(move_group_dialog, '_rotation_atom_idx')
729
+
730
+ try:
731
+ mw.plotter.setCursor(Qt.CursorShape.ArrowCursor)
732
+ except Exception:
733
+ pass
734
+ return
735
+
736
+ # 通常の右クリックリリース処理
737
+ super().OnRightButtonUp()