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