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,536 @@
|
|
|
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
|
+
"""
|
|
14
|
+
main_window_edit_3d.py
|
|
15
|
+
MainWindow (main_window.py) から分離されたモジュール
|
|
16
|
+
機能クラス: MainWindowEdit3d
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# RDKit imports (explicit to satisfy flake8 and used features)
|
|
24
|
+
try:
|
|
25
|
+
from . import sip_isdeleted_safe
|
|
26
|
+
except Exception:
|
|
27
|
+
from modules import sip_isdeleted_safe
|
|
28
|
+
|
|
29
|
+
# PyQt6 Modules
|
|
30
|
+
from PyQt6.QtWidgets import (
|
|
31
|
+
QGraphicsTextItem
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
from PyQt6.QtGui import (
|
|
35
|
+
QColor, QFont
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
from PyQt6.QtCore import (
|
|
40
|
+
QPointF
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
import pyvista as pv
|
|
44
|
+
|
|
45
|
+
# Use centralized Open Babel availability from package-level __init__
|
|
46
|
+
# Use per-package modules availability (local __init__).
|
|
47
|
+
try:
|
|
48
|
+
from . import OBABEL_AVAILABLE
|
|
49
|
+
except Exception:
|
|
50
|
+
from modules import OBABEL_AVAILABLE
|
|
51
|
+
# Only import pybel on demand — `moleditpy` itself doesn't expose `pybel`.
|
|
52
|
+
if OBABEL_AVAILABLE:
|
|
53
|
+
try:
|
|
54
|
+
from openbabel import pybel
|
|
55
|
+
except Exception:
|
|
56
|
+
# If import fails here, disable OBABEL locally; avoid raising
|
|
57
|
+
pybel = None
|
|
58
|
+
OBABEL_AVAILABLE = False
|
|
59
|
+
print("Warning: openbabel.pybel not available. Open Babel fallback and OBabel-based options will be disabled.")
|
|
60
|
+
else:
|
|
61
|
+
pybel = None
|
|
62
|
+
|
|
63
|
+
# Optional SIP helper: on some PyQt6 builds sip.isdeleted is available and
|
|
64
|
+
# allows safely detecting C++ wrapper objects that have been deleted. Import
|
|
65
|
+
# it once at module import time and expose a small, robust wrapper so callers
|
|
66
|
+
# can avoid re-importing sip repeatedly and so we centralize exception
|
|
67
|
+
# handling (this reduces crash risk during teardown and deletion operations).
|
|
68
|
+
try:
|
|
69
|
+
import sip as _sip # type: ignore
|
|
70
|
+
_sip_isdeleted = getattr(_sip, 'isdeleted', None)
|
|
71
|
+
except Exception:
|
|
72
|
+
_sip = None
|
|
73
|
+
_sip_isdeleted = None
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
# package relative imports (preferred when running as `python -m moleditpy`)
|
|
77
|
+
from .constants import VDW_RADII
|
|
78
|
+
except Exception:
|
|
79
|
+
# Fallback to absolute imports for script-style execution
|
|
80
|
+
from modules.constants import VDW_RADII
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# --- クラス定義 ---
|
|
84
|
+
class MainWindowEdit3d(object):
|
|
85
|
+
""" main_window.py から分離された機能クラス """
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def toggle_measurement_mode(self, checked):
|
|
89
|
+
"""測定モードのオン/オフを切り替える"""
|
|
90
|
+
if checked:
|
|
91
|
+
# 測定モードをオンにする時は、3D Dragモードを無効化
|
|
92
|
+
if self.is_3d_edit_mode:
|
|
93
|
+
self.edit_3d_action.setChecked(False)
|
|
94
|
+
self.toggle_3d_edit_mode(False)
|
|
95
|
+
|
|
96
|
+
# アクティブな3D編集ダイアログを閉じる
|
|
97
|
+
self.close_all_3d_edit_dialogs()
|
|
98
|
+
|
|
99
|
+
self.measurement_mode = checked
|
|
100
|
+
|
|
101
|
+
if not checked:
|
|
102
|
+
self.clear_measurement_selection()
|
|
103
|
+
|
|
104
|
+
# ボタンのテキストとステータスメッセージを更新
|
|
105
|
+
if checked:
|
|
106
|
+
self.statusBar().showMessage("Measurement mode enabled. Click atoms to measure distances/angles/dihedrals.")
|
|
107
|
+
else:
|
|
108
|
+
self.statusBar().showMessage("Measurement mode disabled.")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def close_all_3d_edit_dialogs(self):
|
|
113
|
+
"""すべてのアクティブな3D編集ダイアログを閉じる"""
|
|
114
|
+
dialogs_to_close = self.active_3d_dialogs.copy()
|
|
115
|
+
for dialog in dialogs_to_close:
|
|
116
|
+
try:
|
|
117
|
+
dialog.close()
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
self.active_3d_dialogs.clear()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def handle_measurement_atom_selection(self, atom_idx):
|
|
125
|
+
"""測定用の原子選択を処理する"""
|
|
126
|
+
# 既に選択されている原子の場合は除外
|
|
127
|
+
if atom_idx in self.selected_atoms_for_measurement:
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
self.selected_atoms_for_measurement.append(atom_idx)
|
|
131
|
+
|
|
132
|
+
'''
|
|
133
|
+
# 4つ以上選択された場合はクリア
|
|
134
|
+
if len(self.selected_atoms_for_measurement) > 4:
|
|
135
|
+
self.clear_measurement_selection()
|
|
136
|
+
self.selected_atoms_for_measurement.append(atom_idx)
|
|
137
|
+
'''
|
|
138
|
+
|
|
139
|
+
# 原子にラベルを追加
|
|
140
|
+
self.add_measurement_label(atom_idx, len(self.selected_atoms_for_measurement))
|
|
141
|
+
|
|
142
|
+
# 測定値を計算して表示
|
|
143
|
+
self.calculate_and_display_measurements()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def add_measurement_label(self, atom_idx, label_number):
|
|
148
|
+
"""原子に数字ラベルを追加する"""
|
|
149
|
+
if not self.current_mol or atom_idx >= self.current_mol.GetNumAtoms():
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
# 測定ラベルリストを更新
|
|
153
|
+
self.measurement_labels.append((atom_idx, str(label_number)))
|
|
154
|
+
|
|
155
|
+
# 3Dビューの測定ラベルを再描画
|
|
156
|
+
self.update_measurement_labels_display()
|
|
157
|
+
|
|
158
|
+
# 2Dビューの測定ラベルも更新
|
|
159
|
+
self.update_2d_measurement_labels()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def update_measurement_labels_display(self):
|
|
164
|
+
"""測定ラベルを3D表示に描画する(原子中心配置)"""
|
|
165
|
+
try:
|
|
166
|
+
# 既存の測定ラベルを削除
|
|
167
|
+
self.plotter.remove_actor('measurement_labels')
|
|
168
|
+
except Exception:
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
if not self.measurement_labels or not self.current_mol:
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
# ラベル位置とテキストを準備
|
|
175
|
+
pts, labels = [], []
|
|
176
|
+
for atom_idx, label_text in self.measurement_labels:
|
|
177
|
+
if atom_idx < len(self.atom_positions_3d):
|
|
178
|
+
coord = self.atom_positions_3d[atom_idx].copy()
|
|
179
|
+
# オフセットを削除して原子中心に配置
|
|
180
|
+
pts.append(coord)
|
|
181
|
+
labels.append(label_text)
|
|
182
|
+
|
|
183
|
+
if pts and labels:
|
|
184
|
+
# PyVistaのpoint_labelsを使用(赤色固定)
|
|
185
|
+
self.plotter.add_point_labels(
|
|
186
|
+
np.array(pts),
|
|
187
|
+
labels,
|
|
188
|
+
font_size=16,
|
|
189
|
+
point_size=0,
|
|
190
|
+
text_color='red', # 測定時は常に赤色
|
|
191
|
+
name='measurement_labels',
|
|
192
|
+
always_visible=True,
|
|
193
|
+
tolerance=0.01,
|
|
194
|
+
show_points=False
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def clear_measurement_selection(self):
|
|
200
|
+
"""測定選択をクリアする"""
|
|
201
|
+
self.selected_atoms_for_measurement.clear()
|
|
202
|
+
|
|
203
|
+
# 3Dビューのラベルを削除
|
|
204
|
+
self.measurement_labels.clear()
|
|
205
|
+
try:
|
|
206
|
+
self.plotter.remove_actor('measurement_labels')
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
# 2Dビューの測定ラベルも削除
|
|
211
|
+
self.clear_2d_measurement_labels()
|
|
212
|
+
|
|
213
|
+
# 測定結果のテキストを削除
|
|
214
|
+
if self.measurement_text_actor:
|
|
215
|
+
try:
|
|
216
|
+
self.plotter.remove_actor(self.measurement_text_actor)
|
|
217
|
+
self.measurement_text_actor = None
|
|
218
|
+
except Exception:
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
self.plotter.render()
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def update_2d_measurement_labels(self):
|
|
226
|
+
"""2Dビューで測定ラベルを更新表示する"""
|
|
227
|
+
# 既存の2D測定ラベルを削除
|
|
228
|
+
self.clear_2d_measurement_labels()
|
|
229
|
+
|
|
230
|
+
# 現在の分子から原子-AtomItemマッピングを作成
|
|
231
|
+
if not self.current_mol or not hasattr(self, 'data') or not self.data.atoms:
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
# RDKit原子インデックスから2D AtomItemへのマッピングを作成
|
|
235
|
+
atom_idx_to_item = {}
|
|
236
|
+
|
|
237
|
+
# シーンからAtomItemを取得してマッピング
|
|
238
|
+
if hasattr(self, 'scene'):
|
|
239
|
+
for item in self.scene.items():
|
|
240
|
+
if hasattr(item, 'atom_id') and hasattr(item, 'symbol'): # AtomItemかチェック
|
|
241
|
+
# 原子IDから対応するRDKit原子インデックスを見つける
|
|
242
|
+
rdkit_idx = self.find_rdkit_atom_index(item)
|
|
243
|
+
if rdkit_idx is not None:
|
|
244
|
+
atom_idx_to_item[rdkit_idx] = item
|
|
245
|
+
|
|
246
|
+
# 測定ラベルを2Dビューに追加
|
|
247
|
+
if not hasattr(self, 'measurement_label_items_2d'):
|
|
248
|
+
self.measurement_label_items_2d = []
|
|
249
|
+
|
|
250
|
+
for atom_idx, label_text in self.measurement_labels:
|
|
251
|
+
if atom_idx in atom_idx_to_item:
|
|
252
|
+
atom_item = atom_idx_to_item[atom_idx]
|
|
253
|
+
self.add_2d_measurement_label(atom_item, label_text)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def add_2d_measurement_label(self, atom_item, label_text):
|
|
258
|
+
"""特定のAtomItemに測定ラベルを追加する"""
|
|
259
|
+
# ラベルアイテムを作成
|
|
260
|
+
label_item = QGraphicsTextItem(label_text)
|
|
261
|
+
label_item.setDefaultTextColor(QColor(255, 0, 0)) # 赤色
|
|
262
|
+
label_item.setFont(QFont("Arial", 12, QFont.Weight.Bold))
|
|
263
|
+
|
|
264
|
+
# Z値を設定して最前面に表示(原子ラベルより上)
|
|
265
|
+
label_item.setZValue(2000) # より高い値で確実に最前面に配置
|
|
266
|
+
|
|
267
|
+
# 原子の右上により近く配置
|
|
268
|
+
atom_pos = atom_item.pos()
|
|
269
|
+
atom_rect = atom_item.boundingRect()
|
|
270
|
+
label_pos = QPointF(
|
|
271
|
+
atom_pos.x() + atom_rect.width() / 4 + 2,
|
|
272
|
+
atom_pos.y() - atom_rect.height() / 4 - 8
|
|
273
|
+
)
|
|
274
|
+
label_item.setPos(label_pos)
|
|
275
|
+
|
|
276
|
+
# シーンに追加
|
|
277
|
+
self.scene.addItem(label_item)
|
|
278
|
+
self.measurement_label_items_2d.append(label_item)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def clear_2d_measurement_labels(self):
|
|
283
|
+
"""2Dビューの測定ラベルを全て削除する"""
|
|
284
|
+
if hasattr(self, 'measurement_label_items_2d'):
|
|
285
|
+
for label_item in self.measurement_label_items_2d:
|
|
286
|
+
try:
|
|
287
|
+
# Avoid touching partially-deleted wrappers
|
|
288
|
+
if sip_isdeleted_safe(label_item):
|
|
289
|
+
continue
|
|
290
|
+
try:
|
|
291
|
+
if label_item.scene():
|
|
292
|
+
self.scene.removeItem(label_item)
|
|
293
|
+
except Exception:
|
|
294
|
+
# Scene access or removal failed; skip
|
|
295
|
+
continue
|
|
296
|
+
except Exception:
|
|
297
|
+
# If sip check itself fails, fall back to best-effort removal
|
|
298
|
+
try:
|
|
299
|
+
if label_item.scene():
|
|
300
|
+
self.scene.removeItem(label_item)
|
|
301
|
+
except Exception:
|
|
302
|
+
continue
|
|
303
|
+
self.measurement_label_items_2d.clear()
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def find_rdkit_atom_index(self, atom_item):
|
|
308
|
+
"""AtomItemから対応するRDKit原子インデックスを見つける"""
|
|
309
|
+
if not self.current_mol or not atom_item:
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
# マッピング辞書を使用(最も確実)
|
|
313
|
+
if hasattr(self, 'atom_id_to_rdkit_idx_map') and atom_item.atom_id in self.atom_id_to_rdkit_idx_map:
|
|
314
|
+
return self.atom_id_to_rdkit_idx_map[atom_item.atom_id]
|
|
315
|
+
|
|
316
|
+
# マッピングが存在しない場合はNone(外部ファイル読み込み時など)
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def calculate_and_display_measurements(self):
|
|
322
|
+
"""選択された原子に基づいて測定値を計算し表示する"""
|
|
323
|
+
num_selected = len(self.selected_atoms_for_measurement)
|
|
324
|
+
if num_selected < 2:
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
measurement_text = []
|
|
328
|
+
|
|
329
|
+
if num_selected >= 2:
|
|
330
|
+
# 距離の計算
|
|
331
|
+
atom1_idx = self.selected_atoms_for_measurement[0]
|
|
332
|
+
atom2_idx = self.selected_atoms_for_measurement[1]
|
|
333
|
+
distance = self.calculate_distance(atom1_idx, atom2_idx)
|
|
334
|
+
measurement_text.append(f"Distance 1-2: {distance:.3f} Å")
|
|
335
|
+
|
|
336
|
+
if num_selected >= 3:
|
|
337
|
+
# 角度の計算
|
|
338
|
+
atom1_idx = self.selected_atoms_for_measurement[0]
|
|
339
|
+
atom2_idx = self.selected_atoms_for_measurement[1]
|
|
340
|
+
atom3_idx = self.selected_atoms_for_measurement[2]
|
|
341
|
+
angle = self.calculate_angle(atom1_idx, atom2_idx, atom3_idx)
|
|
342
|
+
measurement_text.append(f"Angle 1-2-3: {angle:.2f}°")
|
|
343
|
+
|
|
344
|
+
if num_selected >= 4:
|
|
345
|
+
# 二面角の計算
|
|
346
|
+
atom1_idx = self.selected_atoms_for_measurement[0]
|
|
347
|
+
atom2_idx = self.selected_atoms_for_measurement[1]
|
|
348
|
+
atom3_idx = self.selected_atoms_for_measurement[2]
|
|
349
|
+
atom4_idx = self.selected_atoms_for_measurement[3]
|
|
350
|
+
dihedral = self.calculate_dihedral(atom1_idx, atom2_idx, atom3_idx, atom4_idx)
|
|
351
|
+
measurement_text.append(f"Dihedral 1-2-3-4: {dihedral:.2f}°")
|
|
352
|
+
|
|
353
|
+
# 測定結果を3D画面の右上に表示
|
|
354
|
+
self.display_measurement_text(measurement_text)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def calculate_distance(self, atom1_idx, atom2_idx):
|
|
359
|
+
"""2原子間の距離を計算する"""
|
|
360
|
+
pos1 = np.array(self.atom_positions_3d[atom1_idx])
|
|
361
|
+
pos2 = np.array(self.atom_positions_3d[atom2_idx])
|
|
362
|
+
return np.linalg.norm(pos2 - pos1)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def calculate_angle(self, atom1_idx, atom2_idx, atom3_idx):
|
|
367
|
+
"""3原子の角度を計算する(中央が頂点)"""
|
|
368
|
+
pos1 = np.array(self.atom_positions_3d[atom1_idx])
|
|
369
|
+
pos2 = np.array(self.atom_positions_3d[atom2_idx]) # 頂点
|
|
370
|
+
pos3 = np.array(self.atom_positions_3d[atom3_idx])
|
|
371
|
+
|
|
372
|
+
# ベクトルを計算
|
|
373
|
+
vec1 = pos1 - pos2
|
|
374
|
+
vec2 = pos3 - pos2
|
|
375
|
+
|
|
376
|
+
# 角度を計算(ラジアンから度に変換)
|
|
377
|
+
cos_angle = np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))
|
|
378
|
+
# 数値誤差による範囲外の値をクリップ
|
|
379
|
+
cos_angle = np.clip(cos_angle, -1.0, 1.0)
|
|
380
|
+
angle_rad = np.arccos(cos_angle)
|
|
381
|
+
return np.degrees(angle_rad)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def calculate_dihedral(self, atom1_idx, atom2_idx, atom3_idx, atom4_idx):
|
|
386
|
+
"""4原子の二面角を計算する(正しい公式を使用)"""
|
|
387
|
+
pos1 = np.array(self.atom_positions_3d[atom1_idx])
|
|
388
|
+
pos2 = np.array(self.atom_positions_3d[atom2_idx])
|
|
389
|
+
pos3 = np.array(self.atom_positions_3d[atom3_idx])
|
|
390
|
+
pos4 = np.array(self.atom_positions_3d[atom4_idx])
|
|
391
|
+
|
|
392
|
+
# Vectors between consecutive atoms
|
|
393
|
+
v1 = pos2 - pos1 # 1->2
|
|
394
|
+
v2 = pos3 - pos2 # 2->3 (central bond)
|
|
395
|
+
v3 = pos4 - pos3 # 3->4
|
|
396
|
+
|
|
397
|
+
# Normalize the central bond vector
|
|
398
|
+
v2_norm = v2 / np.linalg.norm(v2)
|
|
399
|
+
|
|
400
|
+
# Calculate plane normal vectors
|
|
401
|
+
n1 = np.cross(v1, v2) # Normal to plane 1-2-3
|
|
402
|
+
n2 = np.cross(v2, v3) # Normal to plane 2-3-4
|
|
403
|
+
|
|
404
|
+
# Normalize the normal vectors
|
|
405
|
+
n1_norm = np.linalg.norm(n1)
|
|
406
|
+
n2_norm = np.linalg.norm(n2)
|
|
407
|
+
|
|
408
|
+
if n1_norm == 0 or n2_norm == 0:
|
|
409
|
+
return 0.0 # Atoms are collinear
|
|
410
|
+
|
|
411
|
+
n1 = n1 / n1_norm
|
|
412
|
+
n2 = n2 / n2_norm
|
|
413
|
+
|
|
414
|
+
# Calculate the cosine of the dihedral angle
|
|
415
|
+
cos_angle = np.dot(n1, n2)
|
|
416
|
+
cos_angle = np.clip(cos_angle, -1.0, 1.0)
|
|
417
|
+
|
|
418
|
+
# Calculate the sine for proper sign determination
|
|
419
|
+
sin_angle = np.dot(np.cross(n1, n2), v2_norm)
|
|
420
|
+
|
|
421
|
+
# Calculate the dihedral angle with correct sign
|
|
422
|
+
angle_rad = np.arctan2(sin_angle, cos_angle)
|
|
423
|
+
return np.degrees(angle_rad)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def display_measurement_text(self, measurement_lines):
|
|
428
|
+
"""測定結果のテキストを3D画面の左上に表示する(小さな等幅フォント)"""
|
|
429
|
+
# 既存のテキストを削除
|
|
430
|
+
if self.measurement_text_actor:
|
|
431
|
+
try:
|
|
432
|
+
self.plotter.remove_actor(self.measurement_text_actor)
|
|
433
|
+
except Exception:
|
|
434
|
+
pass
|
|
435
|
+
|
|
436
|
+
if not measurement_lines:
|
|
437
|
+
self.measurement_text_actor = None
|
|
438
|
+
return
|
|
439
|
+
|
|
440
|
+
# テキストを結合
|
|
441
|
+
text = '\n'.join(measurement_lines)
|
|
442
|
+
|
|
443
|
+
# 背景色から適切なテキスト色を決定
|
|
444
|
+
try:
|
|
445
|
+
bg_color_hex = self.settings.get('background_color', '#919191')
|
|
446
|
+
bg_qcolor = QColor(bg_color_hex)
|
|
447
|
+
if bg_qcolor.isValid():
|
|
448
|
+
luminance = bg_qcolor.toHsl().lightness()
|
|
449
|
+
text_color = 'black' if luminance > 128 else 'white'
|
|
450
|
+
else:
|
|
451
|
+
text_color = 'white'
|
|
452
|
+
except Exception:
|
|
453
|
+
text_color = 'white'
|
|
454
|
+
|
|
455
|
+
# 左上に表示(小さな等幅フォント)
|
|
456
|
+
self.measurement_text_actor = self.plotter.add_text(
|
|
457
|
+
text,
|
|
458
|
+
position='upper_left',
|
|
459
|
+
font_size=10, # より小さく
|
|
460
|
+
color=text_color, # 背景に合わせた色
|
|
461
|
+
font='courier', # 等幅フォント
|
|
462
|
+
name='measurement_display'
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
self.plotter.render()
|
|
466
|
+
|
|
467
|
+
# --- 3D Drag functionality ---
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def toggle_atom_selection_3d(self, atom_idx):
|
|
472
|
+
"""3Dビューで原子の選択状態をトグルする"""
|
|
473
|
+
if atom_idx in self.selected_atoms_3d:
|
|
474
|
+
self.selected_atoms_3d.remove(atom_idx)
|
|
475
|
+
else:
|
|
476
|
+
self.selected_atoms_3d.add(atom_idx)
|
|
477
|
+
|
|
478
|
+
# 選択状態のビジュアルフィードバックを更新
|
|
479
|
+
self.update_3d_selection_display()
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def clear_3d_selection(self):
|
|
484
|
+
"""3Dビューでの原子選択をクリア"""
|
|
485
|
+
self.selected_atoms_3d.clear()
|
|
486
|
+
self.update_3d_selection_display()
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def update_3d_selection_display(self):
|
|
491
|
+
"""3Dビューでの選択原子のハイライト表示を更新"""
|
|
492
|
+
try:
|
|
493
|
+
# 既存の選択ハイライトを削除
|
|
494
|
+
self.plotter.remove_actor('selection_highlight')
|
|
495
|
+
except Exception:
|
|
496
|
+
pass
|
|
497
|
+
|
|
498
|
+
if not self.selected_atoms_3d or not self.current_mol:
|
|
499
|
+
self.plotter.render()
|
|
500
|
+
return
|
|
501
|
+
|
|
502
|
+
# 選択された原子のインデックスリストを作成
|
|
503
|
+
selected_indices = list(self.selected_atoms_3d)
|
|
504
|
+
|
|
505
|
+
# 選択された原子の位置を取得
|
|
506
|
+
selected_positions = self.atom_positions_3d[selected_indices]
|
|
507
|
+
|
|
508
|
+
# 原子の半径を少し大きくしてハイライト表示
|
|
509
|
+
selected_radii = np.array([VDW_RADII.get(
|
|
510
|
+
self.current_mol.GetAtomWithIdx(i).GetSymbol(), 0.4) * 1.3
|
|
511
|
+
for i in selected_indices])
|
|
512
|
+
|
|
513
|
+
# ハイライト用のデータセットを作成
|
|
514
|
+
highlight_source = pv.PolyData(selected_positions)
|
|
515
|
+
highlight_source['radii'] = selected_radii
|
|
516
|
+
|
|
517
|
+
# 黄色の半透明球でハイライト
|
|
518
|
+
highlight_glyphs = highlight_source.glyph(
|
|
519
|
+
scale='radii',
|
|
520
|
+
geom=pv.Sphere(radius=1.0, theta_resolution=16, phi_resolution=16),
|
|
521
|
+
orient=False
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
self.plotter.add_mesh(
|
|
525
|
+
highlight_glyphs,
|
|
526
|
+
color='yellow',
|
|
527
|
+
opacity=0.3,
|
|
528
|
+
name='selection_highlight'
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
self.plotter.render()
|
|
532
|
+
|
|
533
|
+
def remove_dialog_from_list(self, dialog):
|
|
534
|
+
"""ダイアログをアクティブリストから削除"""
|
|
535
|
+
if dialog in self.active_3d_dialogs:
|
|
536
|
+
self.active_3d_dialogs.remove(dialog)
|