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