MoleditPy 2.2.5__tar.gz → 2.2.6__tar.gz
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-2.2.5 → moleditpy-2.2.6}/PKG-INFO +1 -1
- {moleditpy-2.2.5 → moleditpy-2.2.6}/pyproject.toml +1 -1
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/MoleditPy.egg-info/PKG-INFO +1 -1
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/align_plane_dialog.py +0 -1
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/alignment_dialog.py +0 -1
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/angle_dialog.py +1 -1
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/bond_length_dialog.py +1 -1
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/constants.py +1 -1
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/constrained_optimization_dialog.py +0 -1
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/custom_interactor_style.py +56 -56
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/custom_qt_interactor.py +43 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/dialog3_d_picking_mixin.py +42 -9
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/dihedral_dialog.py +1 -1
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/main_window_dialog_manager.py +9 -9
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/main_window_edit_actions.py +4 -1
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/main_window_view_3d.py +9 -1
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/move_group_dialog.py +7 -5
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/planarize_dialog.py +0 -1
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/translation_dialog.py +0 -1
- {moleditpy-2.2.5 → moleditpy-2.2.6}/LICENSE +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/README.md +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/setup.cfg +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/MoleditPy.egg-info/SOURCES.txt +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/MoleditPy.egg-info/entry_points.txt +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/MoleditPy.egg-info/requires.txt +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/MoleditPy.egg-info/top_level.txt +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/__init__.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/__main__.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/main.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/__init__.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/about_dialog.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/analysis_window.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/assets/file_icon.ico +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/assets/icon.icns +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/assets/icon.ico +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/assets/icon.png +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/atom_item.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/bond_item.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/calculation_worker.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/color_settings_dialog.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/main_window.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/main_window_app_state.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/main_window_compute.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/main_window_edit_3d.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/main_window_export.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/main_window_main_init.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/main_window_molecular_parsers.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/main_window_project_io.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/main_window_string_importers.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/main_window_ui_manager.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/main_window_view_loaders.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/mirror_dialog.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/molecular_data.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/molecule_scene.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/periodic_table_dialog.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/plugin_interface.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/plugin_manager.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/plugin_manager_window.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/settings_dialog.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/template_preview_item.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/template_preview_view.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/user_template_dialog.py +0 -0
- {moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/zoomable_view.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.6
|
|
4
4
|
Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
|
|
5
5
|
Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.6
|
|
4
4
|
Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
|
|
5
5
|
Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -45,7 +45,6 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog):
|
|
|
45
45
|
plane_names = {'xy': 'XY', 'xz': 'XZ', 'yz': 'YZ'}
|
|
46
46
|
self.setWindowTitle(f"Align to {plane_names[self.plane]} Plane")
|
|
47
47
|
self.setModal(False) # モードレスにしてクリックを阻害しない
|
|
48
|
-
self.setWindowFlags(Qt.WindowType.Window | Qt.WindowType.WindowStaysOnTopHint) # 常に前面表示
|
|
49
48
|
layout = QVBoxLayout(self)
|
|
50
49
|
|
|
51
50
|
# Instructions
|
|
@@ -47,7 +47,6 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
|
47
47
|
axis_names = {'x': 'X-axis', 'y': 'Y-axis', 'z': 'Z-axis'}
|
|
48
48
|
self.setWindowTitle(f"Align to {axis_names[self.axis]}")
|
|
49
49
|
self.setModal(False) # モードレスにしてクリックを阻害しない
|
|
50
|
-
self.setWindowFlags(Qt.WindowType.Window | Qt.WindowType.WindowStaysOnTopHint) # 常に前面表示
|
|
51
50
|
layout = QVBoxLayout(self)
|
|
52
51
|
|
|
53
52
|
# Instructions
|
|
@@ -45,7 +45,7 @@ class AngleDialog(Dialog3DPickingMixin, QDialog):
|
|
|
45
45
|
def init_ui(self):
|
|
46
46
|
self.setWindowTitle("Adjust Angle")
|
|
47
47
|
self.setModal(False) # モードレスにしてクリックを阻害しない
|
|
48
|
-
|
|
48
|
+
# 常に前面表示
|
|
49
49
|
layout = QVBoxLayout(self)
|
|
50
50
|
|
|
51
51
|
# Instructions
|
|
@@ -38,7 +38,7 @@ class BondLengthDialog(Dialog3DPickingMixin, QDialog):
|
|
|
38
38
|
def init_ui(self):
|
|
39
39
|
self.setWindowTitle("Adjust Bond Length")
|
|
40
40
|
self.setModal(False) # モードレスにしてクリックを阻害しない
|
|
41
|
-
|
|
41
|
+
# 常に前面表示
|
|
42
42
|
layout = QVBoxLayout(self)
|
|
43
43
|
|
|
44
44
|
# Instructions
|
{moleditpy-2.2.5 → moleditpy-2.2.6}/src/moleditpy/modules/constrained_optimization_dialog.py
RENAMED
|
@@ -123,7 +123,6 @@ class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
|
|
|
123
123
|
self.setWindowTitle("Constrained Optimization")
|
|
124
124
|
self.setModal(False)
|
|
125
125
|
self.resize(450, 500)
|
|
126
|
-
self.setWindowFlags(Qt.WindowType.Window | Qt.WindowType.WindowStaysOnTopHint)
|
|
127
126
|
layout = QVBoxLayout(self)
|
|
128
127
|
|
|
129
128
|
# 1. 説明
|
|
@@ -42,6 +42,7 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
42
42
|
self._mouse_press_pos = None
|
|
43
43
|
|
|
44
44
|
self.AddObserver("LeftButtonPressEvent", self.on_left_button_down)
|
|
45
|
+
#self.AddObserver("LeftButtonDoubleClickEvent", self.on_left_button_down)
|
|
45
46
|
self.AddObserver("RightButtonPressEvent", self.on_right_button_down)
|
|
46
47
|
self.AddObserver("MouseMoveEvent", self.on_mouse_move)
|
|
47
48
|
self.AddObserver("LeftButtonReleaseEvent", self.on_left_button_up)
|
|
@@ -85,8 +86,12 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
85
86
|
if 0 <= closest_atom_idx < mw.current_mol.GetNumAtoms():
|
|
86
87
|
atom = mw.current_mol.GetAtomWithIdx(int(closest_atom_idx))
|
|
87
88
|
if atom:
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
90
95
|
click_threshold = vdw_radius * 1.5
|
|
91
96
|
|
|
92
97
|
if distances[closest_atom_idx] < click_threshold:
|
|
@@ -148,11 +153,11 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
148
153
|
move_group_dialog.update_display()
|
|
149
154
|
return
|
|
150
155
|
else:
|
|
151
|
-
# 原子以外をクリック
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
+
# 原子以外をクリック
|
|
157
|
+
# 即座に解除せず、マウスイベントを追跡して回転かクリックかを判定する
|
|
158
|
+
self._mouse_press_pos = self.GetInteractor().GetEventPosition()
|
|
159
|
+
self._mouse_moved_during_drag = False
|
|
160
|
+
|
|
156
161
|
# カメラ回転を許可
|
|
157
162
|
super(CustomInteractorStyle, self).OnLeftButtonDown()
|
|
158
163
|
return
|
|
@@ -166,8 +171,9 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
166
171
|
# 測定モードが有効な場合の処理
|
|
167
172
|
if mw.measurement_mode and mw.current_mol:
|
|
168
173
|
click_pos = self.GetInteractor().GetEventPosition()
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
171
177
|
|
|
172
178
|
picker = mw.plotter.picker
|
|
173
179
|
|
|
@@ -185,17 +191,24 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
185
191
|
# クリック閾値チェック
|
|
186
192
|
atom = mw.current_mol.GetAtomWithIdx(int(closest_atom_idx))
|
|
187
193
|
if atom:
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
190
200
|
click_threshold = vdw_radius * 1.5
|
|
191
201
|
|
|
192
202
|
if distances[closest_atom_idx] < click_threshold:
|
|
193
203
|
mw.handle_measurement_atom_selection(int(closest_atom_idx))
|
|
194
204
|
return # 原子選択処理完了、カメラ回転は無効
|
|
195
205
|
|
|
206
|
+
|
|
196
207
|
# 測定モードで原子以外をクリックした場合は計測選択をクリア
|
|
197
|
-
#
|
|
208
|
+
# ただし、回転操作(ドラッグ)の場合はクリアしないため、
|
|
209
|
+
# ここで _mouse_press_pos を記録し、Upイベントで判定する。
|
|
198
210
|
self._is_dragging_atom = False
|
|
211
|
+
self._mouse_press_pos = click_pos
|
|
199
212
|
super().OnLeftButtonDown()
|
|
200
213
|
return
|
|
201
214
|
|
|
@@ -219,8 +232,12 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
219
232
|
# RDKitのMolオブジェクトから原子を安全に取得
|
|
220
233
|
atom = mw.current_mol.GetAtomWithIdx(int(closest_atom_idx))
|
|
221
234
|
if atom:
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
224
241
|
click_threshold = vdw_radius * 1.5
|
|
225
242
|
|
|
226
243
|
if distances[closest_atom_idx] < click_threshold:
|
|
@@ -265,8 +282,12 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
265
282
|
if 0 <= closest_atom_idx < mw.current_mol.GetNumAtoms():
|
|
266
283
|
atom = mw.current_mol.GetAtomWithIdx(int(closest_atom_idx))
|
|
267
284
|
if atom:
|
|
268
|
-
|
|
269
|
-
|
|
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
|
|
270
291
|
click_threshold = vdw_radius * 1.5
|
|
271
292
|
|
|
272
293
|
if distances[closest_atom_idx] < click_threshold:
|
|
@@ -458,37 +479,23 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
458
479
|
move_group_dialog.on_atom_picked(clicked_atom)
|
|
459
480
|
except Exception as e:
|
|
460
481
|
print(f"Error in toggle: {e}")
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
self._is_dragging_atom = False
|
|
473
|
-
self.is_dragging = False
|
|
474
|
-
self._mouse_moved_during_drag = False
|
|
475
|
-
self._mouse_press_pos = None
|
|
476
|
-
|
|
477
|
-
try:
|
|
478
|
-
mw.plotter.setCursor(Qt.CursorShape.ArrowCursor)
|
|
479
|
-
except Exception:
|
|
480
|
-
pass
|
|
481
|
-
return
|
|
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()
|
|
482
493
|
|
|
483
494
|
# 計測モードで、マウスが動いていない場合(つまりクリック)の処理
|
|
495
|
+
# _mouse_press_pos が None でない = 背景をクリックしたことを意味する(Downイベントでそう設定したため)
|
|
484
496
|
if mw.measurement_mode and not self._mouse_moved_during_drag and self._mouse_press_pos is not None:
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
picker.Pick(click_pos[0], click_pos[1], 0, mw.plotter.renderer)
|
|
488
|
-
|
|
489
|
-
# 原子がクリックされていない場合は測定選択をクリア
|
|
490
|
-
if picker.GetActor() is not mw.atom_actor:
|
|
491
|
-
mw.clear_measurement_selection()
|
|
497
|
+
# 背景クリック -> 測定選択をクリア
|
|
498
|
+
mw.clear_measurement_selection()
|
|
492
499
|
|
|
493
500
|
if self._is_dragging_atom:
|
|
494
501
|
# カスタムドラッグの後始末
|
|
@@ -594,7 +601,7 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
594
601
|
# カメラ回転の後始末を親クラスに任せます
|
|
595
602
|
super().OnLeftButtonUp()
|
|
596
603
|
|
|
597
|
-
# 状態をリセット(完全なクリーンアップ)
|
|
604
|
+
# 状態をリセット(完全なクリーンアップ) - すべてのチェックの後に実行
|
|
598
605
|
self._is_dragging_atom = False
|
|
599
606
|
self.is_dragging = False
|
|
600
607
|
self._mouse_press_pos = None
|
|
@@ -602,12 +609,6 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
602
609
|
|
|
603
610
|
# Move Group関連の状態もクリア
|
|
604
611
|
try:
|
|
605
|
-
move_group_dialog = None
|
|
606
|
-
for widget in QApplication.topLevelWidgets():
|
|
607
|
-
if isinstance(widget, MoveGroupDialog) and widget.isVisible():
|
|
608
|
-
move_group_dialog = widget
|
|
609
|
-
break
|
|
610
|
-
|
|
611
612
|
if move_group_dialog:
|
|
612
613
|
move_group_dialog._is_dragging_group_vtk = False
|
|
613
614
|
move_group_dialog._drag_start_pos = None
|
|
@@ -619,12 +620,11 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
619
620
|
except Exception:
|
|
620
621
|
pass
|
|
621
622
|
|
|
622
|
-
# ピックリセットは測定モードで実際に問題が発生した場合のみ行う
|
|
623
|
-
# (通常のドラッグ回転では行わない)
|
|
624
|
-
|
|
625
623
|
# ボタンを離した後のカーソル表示を最新の状態に更新
|
|
626
|
-
|
|
627
|
-
|
|
624
|
+
try:
|
|
625
|
+
mw.plotter.setCursor(Qt.CursorShape.ArrowCursor)
|
|
626
|
+
except Exception:
|
|
627
|
+
pass
|
|
628
628
|
# 2Dビューにフォーカスを戻し、ショートカットキーなどが使えるようにする
|
|
629
629
|
if mw and mw.view_2d:
|
|
630
630
|
mw.view_2d.setFocus()
|
|
@@ -11,6 +11,7 @@ DOI: 10.5281/zenodo.17268532
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
from pyvistaqt import QtInteractor
|
|
14
|
+
import time
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class CustomQtInteractor(QtInteractor):
|
|
@@ -24,6 +25,7 @@ class CustomQtInteractor(QtInteractor):
|
|
|
24
25
|
# the VTK interactor and causing unexpected behaviour in the 3D view.
|
|
25
26
|
self._last_click_time = 0.0
|
|
26
27
|
self._click_count = 0
|
|
28
|
+
self._ignore_next_release = False
|
|
27
29
|
|
|
28
30
|
def wheelEvent(self, event):
|
|
29
31
|
"""
|
|
@@ -40,17 +42,58 @@ class CustomQtInteractor(QtInteractor):
|
|
|
40
42
|
"""
|
|
41
43
|
Qtのマウスリリースイベントをオーバーライドし、
|
|
42
44
|
3Dビューでの全ての操作完了後に2Dビューへフォーカスを戻す。
|
|
45
|
+
また、Ghost Release(対応するPressがないRelease)をフィルタリングする。
|
|
43
46
|
"""
|
|
47
|
+
if self._ignore_next_release:
|
|
48
|
+
self._ignore_next_release = False
|
|
49
|
+
event.accept()
|
|
50
|
+
return
|
|
51
|
+
|
|
44
52
|
super().mouseReleaseEvent(event) # 親クラスのイベントを先に処理
|
|
45
53
|
if self.main_window and hasattr(self.main_window, 'view_2d'):
|
|
46
54
|
self.main_window.view_2d.setFocus()
|
|
47
55
|
|
|
56
|
+
def mousePressEvent(self, event):
|
|
57
|
+
"""
|
|
58
|
+
Custom mouse press handling to track accumulated clicks and filter out
|
|
59
|
+
triple-clicks.
|
|
60
|
+
"""
|
|
61
|
+
current_time = time.time()
|
|
62
|
+
# Reset count if too much time has passed (0.5s is standard double-click time)
|
|
63
|
+
if current_time - self._last_click_time > 0.5:
|
|
64
|
+
self._click_count = 0
|
|
65
|
+
|
|
66
|
+
self._click_count += 1
|
|
67
|
+
self._last_click_time = current_time
|
|
68
|
+
|
|
69
|
+
# If this is the 3rd click (or more), swallow it to prevent
|
|
70
|
+
# the internal state desync that happens with rapid clicking sequences.
|
|
71
|
+
if self._click_count >= 3:
|
|
72
|
+
self._ignore_next_release = True
|
|
73
|
+
event.accept()
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
super().mousePressEvent(event)
|
|
77
|
+
|
|
48
78
|
def mouseDoubleClickEvent(self, event):
|
|
49
79
|
"""Ignore mouse double-clicks on the 3D widget to avoid accidental actions.
|
|
50
80
|
|
|
51
81
|
Swallow the double-click event so it doesn't trigger selection, editing,
|
|
52
82
|
or camera jumps. We intentionally do not call the superclass handler.
|
|
83
|
+
Crucially, we also flag the NEXT release event to be swallowed, preventing
|
|
84
|
+
a "Ghost Release" (Release without Press) from reaching VTK.
|
|
53
85
|
"""
|
|
86
|
+
current_time = time.time()
|
|
87
|
+
self._last_click_time = current_time
|
|
88
|
+
# Set to 2 to ensure the next click counts as 3rd
|
|
89
|
+
if current_time - self._last_click_time < 0.5:
|
|
90
|
+
self._click_count = 2
|
|
91
|
+
else:
|
|
92
|
+
self._click_count = 2 # Force sync
|
|
93
|
+
|
|
94
|
+
# We must ignore the release event that follows this double-click event
|
|
95
|
+
self._ignore_next_release = True
|
|
96
|
+
|
|
54
97
|
try:
|
|
55
98
|
# Accept the event to mark it handled and prevent further processing.
|
|
56
99
|
event.accept()
|
|
@@ -31,6 +31,11 @@ class Dialog3DPickingMixin:
|
|
|
31
31
|
event.type() == QEvent.Type.MouseButtonPress and
|
|
32
32
|
event.button() == Qt.MouseButton.LeftButton):
|
|
33
33
|
|
|
34
|
+
# Start tracking for smart selection (click vs drag)
|
|
35
|
+
self._mouse_press_pos = event.pos()
|
|
36
|
+
self._mouse_moved = False
|
|
37
|
+
|
|
38
|
+
|
|
34
39
|
try:
|
|
35
40
|
# VTKイベント座標を取得(元のロジックと同じ)
|
|
36
41
|
interactor = self.main_window.plotter.interactor
|
|
@@ -48,8 +53,12 @@ class Dialog3DPickingMixin:
|
|
|
48
53
|
# クリック閾値チェック(元のロジックと同じ)
|
|
49
54
|
atom = self.mol.GetAtomWithIdx(int(closest_atom_idx))
|
|
50
55
|
if atom:
|
|
51
|
-
|
|
52
|
-
|
|
56
|
+
try:
|
|
57
|
+
atomic_num = atom.GetAtomicNum()
|
|
58
|
+
vdw_radius = pt.GetRvdw(atomic_num)
|
|
59
|
+
if vdw_radius < 0.1: vdw_radius = 1.5
|
|
60
|
+
except Exception:
|
|
61
|
+
vdw_radius = 1.5
|
|
53
62
|
click_threshold = vdw_radius * 1.5
|
|
54
63
|
|
|
55
64
|
if distances[closest_atom_idx] < click_threshold:
|
|
@@ -63,15 +72,14 @@ class Dialog3DPickingMixin:
|
|
|
63
72
|
except Exception:
|
|
64
73
|
pass
|
|
65
74
|
self.on_atom_picked(int(closest_atom_idx))
|
|
75
|
+
|
|
76
|
+
# We picked an atom, so stop tracking for background click
|
|
77
|
+
self._mouse_press_pos = None
|
|
66
78
|
return True
|
|
67
79
|
|
|
68
|
-
#
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
# We did not actually pick an atom; do NOT consume the event here so
|
|
72
|
-
# the interactor and CustomInteractorStyle can handle camera rotation
|
|
73
|
-
# and other behaviors. Returning False (or calling the base
|
|
74
|
-
# implementation) allows normal processing to continue.
|
|
80
|
+
# 原子以外をクリックした場合
|
|
81
|
+
# 即時には解除せず、回転操作(ドラッグ)を許可する。
|
|
82
|
+
# 実際の解除は MouseButtonRelease イベントで行う。
|
|
75
83
|
return False
|
|
76
84
|
|
|
77
85
|
except Exception as e:
|
|
@@ -80,6 +88,31 @@ class Dialog3DPickingMixin:
|
|
|
80
88
|
# event pipeline continue so the UI remains responsive.
|
|
81
89
|
return False
|
|
82
90
|
|
|
91
|
+
# Add movement tracking for smart selection
|
|
92
|
+
elif (obj == self.main_window.plotter.interactor and
|
|
93
|
+
event.type() == QEvent.Type.MouseMove):
|
|
94
|
+
if hasattr(self, '_mouse_press_pos') and self._mouse_press_pos is not None:
|
|
95
|
+
# Check if moved significantly
|
|
96
|
+
diff = event.pos() - self._mouse_press_pos
|
|
97
|
+
if diff.manhattanLength() > 3:
|
|
98
|
+
self._mouse_moved = True
|
|
99
|
+
|
|
100
|
+
# Add release handling for smart selection
|
|
101
|
+
elif (obj == self.main_window.plotter.interactor and
|
|
102
|
+
event.type() == QEvent.Type.MouseButtonRelease and
|
|
103
|
+
event.button() == Qt.MouseButton.LeftButton):
|
|
104
|
+
|
|
105
|
+
if hasattr(self, '_mouse_press_pos') and self._mouse_press_pos is not None:
|
|
106
|
+
if not getattr(self, '_mouse_moved', False):
|
|
107
|
+
# Pure click (no drag) on background -> Clear selection
|
|
108
|
+
if hasattr(self, 'clear_selection'):
|
|
109
|
+
self.clear_selection()
|
|
110
|
+
|
|
111
|
+
# Reset state
|
|
112
|
+
self._mouse_press_pos = None
|
|
113
|
+
self._mouse_moved = False
|
|
114
|
+
|
|
115
|
+
|
|
83
116
|
return super().eventFilter(obj, event)
|
|
84
117
|
|
|
85
118
|
def enable_picking(self):
|
|
@@ -47,7 +47,7 @@ class DihedralDialog(Dialog3DPickingMixin, QDialog):
|
|
|
47
47
|
def init_ui(self):
|
|
48
48
|
self.setWindowTitle("Adjust Dihedral Angle")
|
|
49
49
|
self.setModal(False) # モードレスにしてクリックを阻害しない
|
|
50
|
-
|
|
50
|
+
# 常に前面表示
|
|
51
51
|
layout = QVBoxLayout(self)
|
|
52
52
|
|
|
53
53
|
# Instructions
|
|
@@ -263,7 +263,7 @@ class MainWindowDialogManager(object):
|
|
|
263
263
|
self.measurement_action.setChecked(False)
|
|
264
264
|
self.toggle_measurement_mode(False)
|
|
265
265
|
|
|
266
|
-
dialog = TranslationDialog(self.current_mol, self)
|
|
266
|
+
dialog = TranslationDialog(self.current_mol, self, parent=self)
|
|
267
267
|
self.active_3d_dialogs.append(dialog) # 参照を保持
|
|
268
268
|
dialog.show() # execではなくshowを使用してモードレス表示
|
|
269
269
|
dialog.accepted.connect(lambda: self.statusBar().showMessage("Translation applied."))
|
|
@@ -279,7 +279,7 @@ class MainWindowDialogManager(object):
|
|
|
279
279
|
self.measurement_action.setChecked(False)
|
|
280
280
|
self.toggle_measurement_mode(False)
|
|
281
281
|
|
|
282
|
-
dialog = MoveGroupDialog(self.current_mol, self)
|
|
282
|
+
dialog = MoveGroupDialog(self.current_mol, self, parent=self)
|
|
283
283
|
self.active_3d_dialogs.append(dialog)
|
|
284
284
|
dialog.show()
|
|
285
285
|
dialog.accepted.connect(lambda: self.statusBar().showMessage("Group transformation applied."))
|
|
@@ -302,7 +302,7 @@ class MainWindowDialogManager(object):
|
|
|
302
302
|
self.measurement_action.setChecked(False)
|
|
303
303
|
self.toggle_measurement_mode(False)
|
|
304
304
|
|
|
305
|
-
dialog = AlignPlaneDialog(self.current_mol, self, plane, preselected_atoms)
|
|
305
|
+
dialog = AlignPlaneDialog(self.current_mol, self, plane, preselected_atoms, parent=self)
|
|
306
306
|
self.active_3d_dialogs.append(dialog) # 参照を保持
|
|
307
307
|
dialog.show() # execではなくshowを使用してモードレス表示
|
|
308
308
|
dialog.accepted.connect(lambda: self.statusBar().showMessage(f"Atoms alignd to {plane.upper()} plane."))
|
|
@@ -325,7 +325,7 @@ class MainWindowDialogManager(object):
|
|
|
325
325
|
self.measurement_action.setChecked(False)
|
|
326
326
|
self.toggle_measurement_mode(False)
|
|
327
327
|
|
|
328
|
-
dialog = PlanarizeDialog(self.current_mol, self, preselected_atoms)
|
|
328
|
+
dialog = PlanarizeDialog(self.current_mol, self, preselected_atoms, parent=self)
|
|
329
329
|
self.active_3d_dialogs.append(dialog)
|
|
330
330
|
dialog.show()
|
|
331
331
|
dialog.accepted.connect(lambda: self.statusBar().showMessage("Selection planarized to best-fit plane."))
|
|
@@ -348,7 +348,7 @@ class MainWindowDialogManager(object):
|
|
|
348
348
|
self.measurement_action.setChecked(False)
|
|
349
349
|
self.toggle_measurement_mode(False)
|
|
350
350
|
|
|
351
|
-
dialog = AlignmentDialog(self.current_mol, self, axis, preselected_atoms)
|
|
351
|
+
dialog = AlignmentDialog(self.current_mol, self, axis, preselected_atoms, parent=self)
|
|
352
352
|
self.active_3d_dialogs.append(dialog) # 参照を保持
|
|
353
353
|
dialog.show() # execではなくshowを使用してモードレス表示
|
|
354
354
|
dialog.accepted.connect(lambda: self.statusBar().showMessage(f"Atoms aligned to {axis.upper()}-axis."))
|
|
@@ -371,7 +371,7 @@ class MainWindowDialogManager(object):
|
|
|
371
371
|
self.measurement_action.setChecked(False)
|
|
372
372
|
self.toggle_measurement_mode(False)
|
|
373
373
|
|
|
374
|
-
dialog = BondLengthDialog(self.current_mol, self, preselected_atoms)
|
|
374
|
+
dialog = BondLengthDialog(self.current_mol, self, preselected_atoms, parent=self)
|
|
375
375
|
self.active_3d_dialogs.append(dialog) # 参照を保持
|
|
376
376
|
dialog.show() # execではなくshowを使用してモードレス表示
|
|
377
377
|
dialog.accepted.connect(lambda: self.statusBar().showMessage("Bond length adjusted."))
|
|
@@ -394,7 +394,7 @@ class MainWindowDialogManager(object):
|
|
|
394
394
|
self.measurement_action.setChecked(False)
|
|
395
395
|
self.toggle_measurement_mode(False)
|
|
396
396
|
|
|
397
|
-
dialog = AngleDialog(self.current_mol, self, preselected_atoms)
|
|
397
|
+
dialog = AngleDialog(self.current_mol, self, preselected_atoms, parent=self)
|
|
398
398
|
self.active_3d_dialogs.append(dialog) # 参照を保持
|
|
399
399
|
dialog.show() # execではなくshowを使用してモードレス表示
|
|
400
400
|
dialog.accepted.connect(lambda: self.statusBar().showMessage("Angle adjusted."))
|
|
@@ -417,7 +417,7 @@ class MainWindowDialogManager(object):
|
|
|
417
417
|
self.measurement_action.setChecked(False)
|
|
418
418
|
self.toggle_measurement_mode(False)
|
|
419
419
|
|
|
420
|
-
dialog = DihedralDialog(self.current_mol, self, preselected_atoms)
|
|
420
|
+
dialog = DihedralDialog(self.current_mol, self, preselected_atoms, parent=self)
|
|
421
421
|
self.active_3d_dialogs.append(dialog) # 参照を保持
|
|
422
422
|
dialog.show() # execではなくshowを使用してモードレス表示
|
|
423
423
|
dialog.accepted.connect(lambda: self.statusBar().showMessage("Dihedral angle adjusted."))
|
|
@@ -453,7 +453,7 @@ class MainWindowDialogManager(object):
|
|
|
453
453
|
self.measurement_action.setChecked(False)
|
|
454
454
|
self.toggle_measurement_mode(False)
|
|
455
455
|
|
|
456
|
-
dialog = ConstrainedOptimizationDialog(self.current_mol, self)
|
|
456
|
+
dialog = ConstrainedOptimizationDialog(self.current_mol, self, parent=self)
|
|
457
457
|
self.active_3d_dialogs.append(dialog) # 参照を保持
|
|
458
458
|
dialog.show() # モードレス表示
|
|
459
459
|
dialog.finished.connect(lambda: self.remove_dialog_from_list(dialog))
|
|
@@ -1211,7 +1211,10 @@ class MainWindowEditActions(object):
|
|
|
1211
1211
|
|
|
1212
1212
|
atom = mol.GetAtomWithIdx(idx)
|
|
1213
1213
|
# GetRvdw() はファンデルワールス半径を返す
|
|
1214
|
-
|
|
1214
|
+
try:
|
|
1215
|
+
vdw_radii.append(pt.GetRvdw(atom.GetAtomicNum()))
|
|
1216
|
+
except RuntimeError:
|
|
1217
|
+
vdw_radii.append(1.5)
|
|
1215
1218
|
|
|
1216
1219
|
positions_np = np.array(positions)
|
|
1217
1220
|
vdw_radii_np = np.array(vdw_radii)
|
|
@@ -219,7 +219,15 @@ class MainWindowView3d(object):
|
|
|
219
219
|
if current_style == 'cpk':
|
|
220
220
|
atom_scale = self.settings.get('cpk_atom_scale', 1.0)
|
|
221
221
|
resolution = self.settings.get('cpk_resolution', 32)
|
|
222
|
-
|
|
222
|
+
# Safe VDW lookup to handle custom elements like 'Bq'
|
|
223
|
+
def get_safe_rvdw(s):
|
|
224
|
+
try:
|
|
225
|
+
r = pt.GetRvdw(pt.GetAtomicNumber(s))
|
|
226
|
+
return r if r > 0.1 else 1.5
|
|
227
|
+
except Exception:
|
|
228
|
+
return 1.5
|
|
229
|
+
|
|
230
|
+
rad = np.array([get_safe_rvdw(s) * atom_scale for s in sym])
|
|
223
231
|
elif current_style == 'wireframe':
|
|
224
232
|
# Wireframeでは原子を描画しないので、この設定は実際には使用されない
|
|
225
233
|
resolution = self.settings.get('wireframe_resolution', 6)
|
|
@@ -42,7 +42,6 @@ class MoveGroupDialog(Dialog3DPickingMixin, QDialog):
|
|
|
42
42
|
def init_ui(self):
|
|
43
43
|
self.setWindowTitle("Move Group")
|
|
44
44
|
self.setModal(False)
|
|
45
|
-
self.setWindowFlags(Qt.WindowType.Window | Qt.WindowType.WindowStaysOnTopHint)
|
|
46
45
|
self.resize(300,400) # ウィンドウサイズを設定
|
|
47
46
|
layout = QVBoxLayout(self)
|
|
48
47
|
|
|
@@ -195,9 +194,12 @@ class MoveGroupDialog(Dialog3DPickingMixin, QDialog):
|
|
|
195
194
|
if 0 <= closest_atom_idx < self.mol.GetNumAtoms():
|
|
196
195
|
atom = self.mol.GetAtomWithIdx(int(closest_atom_idx))
|
|
197
196
|
if atom:
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
197
|
+
try:
|
|
198
|
+
atomic_num = atom.GetAtomicNum()
|
|
199
|
+
vdw_radius = pt.GetRvdw(atomic_num)
|
|
200
|
+
if vdw_radius < 0.1: vdw_radius = 1.5
|
|
201
|
+
except Exception:
|
|
202
|
+
vdw_radius = 1.5
|
|
201
203
|
click_threshold = vdw_radius * 1.5
|
|
202
204
|
|
|
203
205
|
if distances[closest_atom_idx] < click_threshold:
|
|
@@ -349,7 +351,7 @@ class MoveGroupDialog(Dialog3DPickingMixin, QDialog):
|
|
|
349
351
|
return False
|
|
350
352
|
|
|
351
353
|
# その他のイベントは親クラスに渡す
|
|
352
|
-
return
|
|
354
|
+
return super().eventFilter(obj, event)
|
|
353
355
|
|
|
354
356
|
def on_atom_picked(self, atom_idx):
|
|
355
357
|
"""原子がピックされたときに、その原子が属する連結成分全体を選択(複数グループ対応)"""
|
|
@@ -46,7 +46,6 @@ class PlanarizeDialog(Dialog3DPickingMixin, QDialog):
|
|
|
46
46
|
def init_ui(self):
|
|
47
47
|
self.setWindowTitle("Planarize")
|
|
48
48
|
self.setModal(False)
|
|
49
|
-
self.setWindowFlags(Qt.WindowType.Window | Qt.WindowType.WindowStaysOnTopHint)
|
|
50
49
|
layout = QVBoxLayout(self)
|
|
51
50
|
|
|
52
51
|
instruction_label = QLabel("Click atoms in the 3D view to select them for planarization (minimum 3 required).")
|
|
@@ -31,7 +31,6 @@ class TranslationDialog(Dialog3DPickingMixin, QDialog):
|
|
|
31
31
|
def init_ui(self):
|
|
32
32
|
self.setWindowTitle("Translation")
|
|
33
33
|
self.setModal(False) # モードレスにしてクリックを阻害しない
|
|
34
|
-
self.setWindowFlags(Qt.WindowType.Window | Qt.WindowType.WindowStaysOnTopHint) # 常に前面表示
|
|
35
34
|
layout = QVBoxLayout(self)
|
|
36
35
|
|
|
37
36
|
# Instructions
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|