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.
- moleditpy_linux/__init__.py +17 -0
- moleditpy_linux/__main__.py +29 -0
- moleditpy_linux/main.py +37 -0
- moleditpy_linux/modules/__init__.py +41 -0
- moleditpy_linux/modules/about_dialog.py +104 -0
- moleditpy_linux/modules/align_plane_dialog.py +292 -0
- moleditpy_linux/modules/alignment_dialog.py +272 -0
- moleditpy_linux/modules/analysis_window.py +209 -0
- moleditpy_linux/modules/angle_dialog.py +440 -0
- moleditpy_linux/modules/assets/file_icon.ico +0 -0
- moleditpy_linux/modules/assets/icon.icns +0 -0
- moleditpy_linux/modules/assets/icon.ico +0 -0
- moleditpy_linux/modules/assets/icon.png +0 -0
- moleditpy_linux/modules/atom_item.py +395 -0
- moleditpy_linux/modules/bond_item.py +464 -0
- moleditpy_linux/modules/bond_length_dialog.py +380 -0
- moleditpy_linux/modules/calculation_worker.py +766 -0
- moleditpy_linux/modules/color_settings_dialog.py +321 -0
- moleditpy_linux/modules/constants.py +88 -0
- moleditpy_linux/modules/constrained_optimization_dialog.py +678 -0
- moleditpy_linux/modules/custom_interactor_style.py +749 -0
- moleditpy_linux/modules/custom_qt_interactor.py +102 -0
- moleditpy_linux/modules/dialog3_d_picking_mixin.py +141 -0
- moleditpy_linux/modules/dihedral_dialog.py +443 -0
- moleditpy_linux/modules/main_window.py +850 -0
- moleditpy_linux/modules/main_window_app_state.py +787 -0
- moleditpy_linux/modules/main_window_compute.py +1242 -0
- moleditpy_linux/modules/main_window_dialog_manager.py +460 -0
- moleditpy_linux/modules/main_window_edit_3d.py +536 -0
- moleditpy_linux/modules/main_window_edit_actions.py +1565 -0
- moleditpy_linux/modules/main_window_export.py +917 -0
- moleditpy_linux/modules/main_window_main_init.py +2100 -0
- moleditpy_linux/modules/main_window_molecular_parsers.py +1044 -0
- moleditpy_linux/modules/main_window_project_io.py +434 -0
- moleditpy_linux/modules/main_window_string_importers.py +275 -0
- moleditpy_linux/modules/main_window_ui_manager.py +602 -0
- moleditpy_linux/modules/main_window_view_3d.py +1539 -0
- moleditpy_linux/modules/main_window_view_loaders.py +355 -0
- moleditpy_linux/modules/mirror_dialog.py +122 -0
- moleditpy_linux/modules/molecular_data.py +302 -0
- moleditpy_linux/modules/molecule_scene.py +2000 -0
- moleditpy_linux/modules/move_group_dialog.py +600 -0
- moleditpy_linux/modules/periodic_table_dialog.py +84 -0
- moleditpy_linux/modules/planarize_dialog.py +220 -0
- moleditpy_linux/modules/plugin_interface.py +215 -0
- moleditpy_linux/modules/plugin_manager.py +473 -0
- moleditpy_linux/modules/plugin_manager_window.py +274 -0
- moleditpy_linux/modules/settings_dialog.py +1503 -0
- moleditpy_linux/modules/template_preview_item.py +157 -0
- moleditpy_linux/modules/template_preview_view.py +74 -0
- moleditpy_linux/modules/translation_dialog.py +364 -0
- moleditpy_linux/modules/user_template_dialog.py +692 -0
- moleditpy_linux/modules/zoomable_view.py +129 -0
- moleditpy_linux-2.4.1.dist-info/METADATA +954 -0
- moleditpy_linux-2.4.1.dist-info/RECORD +59 -0
- moleditpy_linux-2.4.1.dist-info/WHEEL +5 -0
- moleditpy_linux-2.4.1.dist-info/entry_points.txt +2 -0
- moleditpy_linux-2.4.1.dist-info/licenses/LICENSE +674 -0
- 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()
|