MoleditPy 2.2.0a0__py3-none-any.whl → 2.2.0a2__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/modules/constants.py +1 -1
- moleditpy/modules/main_window_main_init.py +73 -103
- moleditpy/modules/main_window_ui_manager.py +21 -2
- moleditpy/modules/plugin_manager.py +10 -0
- moleditpy/plugins/Analysis/ms_spectrum_neo.py +919 -0
- moleditpy/plugins/File/animated_xyz_giffer.py +583 -0
- moleditpy/plugins/File/cube_viewer.py +689 -0
- moleditpy/plugins/File/gaussian_fchk_freq_analyzer.py +1148 -0
- moleditpy/plugins/File/mapped_cube_viewer.py +552 -0
- moleditpy/plugins/File/orca_out_freq_analyzer.py +1226 -0
- moleditpy/plugins/File/paste_xyz.py +336 -0
- moleditpy/plugins/Input Generator/gaussian_input_generator_neo.py +930 -0
- moleditpy/plugins/Input Generator/orca_input_generator_neo.py +1028 -0
- moleditpy/plugins/Input Generator/orca_xyz2inp_gui.py +286 -0
- moleditpy/plugins/Optimization/all-trans_optimizer.py +65 -0
- moleditpy/plugins/Optimization/complex_molecule_untangler.py +268 -0
- moleditpy/plugins/Optimization/conf_search.py +224 -0
- moleditpy/plugins/Utility/atom_colorizer.py +262 -0
- moleditpy/plugins/Utility/console.py +163 -0
- moleditpy/plugins/Utility/pubchem_ressolver.py +244 -0
- moleditpy/plugins/Utility/vdw_radii_overlay.py +432 -0
- {moleditpy-2.2.0a0.dist-info → moleditpy-2.2.0a2.dist-info}/METADATA +1 -1
- {moleditpy-2.2.0a0.dist-info → moleditpy-2.2.0a2.dist-info}/RECORD +27 -10
- {moleditpy-2.2.0a0.dist-info → moleditpy-2.2.0a2.dist-info}/WHEEL +0 -0
- {moleditpy-2.2.0a0.dist-info → moleditpy-2.2.0a2.dist-info}/entry_points.txt +0 -0
- {moleditpy-2.2.0a0.dist-info → moleditpy-2.2.0a2.dist-info}/licenses/LICENSE +0 -0
- {moleditpy-2.2.0a0.dist-info → moleditpy-2.2.0a2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import requests # API通信に必要 (pip install requests)
|
|
3
|
+
from PyQt6.QtWidgets import (
|
|
4
|
+
QDialog, QVBoxLayout, QHBoxLayout, QTableWidget,
|
|
5
|
+
QTableWidgetItem, QPushButton, QMessageBox, QLabel, QHeaderView,
|
|
6
|
+
QAbstractItemView, QApplication, QLineEdit, QComboBox
|
|
7
|
+
)
|
|
8
|
+
from PyQt6.QtCore import Qt, QPointF
|
|
9
|
+
from rdkit import Chem
|
|
10
|
+
from rdkit.Chem import AllChem
|
|
11
|
+
|
|
12
|
+
__version__="2025.12.25"
|
|
13
|
+
__author__="HiroYokoyama"
|
|
14
|
+
PLUGIN_NAME = "PubChem Name Resolver"
|
|
15
|
+
|
|
16
|
+
class MoleculeResolverDialog(QDialog):
|
|
17
|
+
def __init__(self, main_window, parent=None):
|
|
18
|
+
super().__init__(parent)
|
|
19
|
+
self.main_window = main_window
|
|
20
|
+
self.setWindowTitle("PubChem Name Resolver")
|
|
21
|
+
self.resize(500, 600)
|
|
22
|
+
|
|
23
|
+
# 取得した候補データのリスト
|
|
24
|
+
# 各要素は辞書: {'name': str, 'smiles': str, 'formula': str}
|
|
25
|
+
self.candidates_data = []
|
|
26
|
+
|
|
27
|
+
# 生成されたRDKit分子オブジェクト(一時保存)
|
|
28
|
+
self.generated_mol = None
|
|
29
|
+
|
|
30
|
+
self.init_ui()
|
|
31
|
+
|
|
32
|
+
def init_ui(self):
|
|
33
|
+
layout = QVBoxLayout(self)
|
|
34
|
+
|
|
35
|
+
# --- 入力エリア ---
|
|
36
|
+
input_layout = QHBoxLayout()
|
|
37
|
+
|
|
38
|
+
self.combo_type = QComboBox()
|
|
39
|
+
self.combo_type.addItems(["Auto (Name/CAS)", "SMILES"])
|
|
40
|
+
input_layout.addWidget(self.combo_type)
|
|
41
|
+
|
|
42
|
+
self.line_input = QLineEdit()
|
|
43
|
+
self.line_input.setPlaceholderText("Enter Name or SMILES...")
|
|
44
|
+
self.line_input.returnPressed.connect(self.run_search) # Enterキーで検索
|
|
45
|
+
input_layout.addWidget(self.line_input)
|
|
46
|
+
|
|
47
|
+
self.btn_search = QPushButton("Search Online")
|
|
48
|
+
self.btn_search.clicked.connect(self.run_search)
|
|
49
|
+
input_layout.addWidget(self.btn_search)
|
|
50
|
+
|
|
51
|
+
layout.addLayout(input_layout)
|
|
52
|
+
|
|
53
|
+
# --- 説明ラベル ---
|
|
54
|
+
self.lbl_info = QLabel("Enter a chemical identifier and click Search.")
|
|
55
|
+
layout.addWidget(self.lbl_info)
|
|
56
|
+
|
|
57
|
+
# --- 結果表示用テーブル ---
|
|
58
|
+
self.table = QTableWidget()
|
|
59
|
+
self.table.setColumnCount(3)
|
|
60
|
+
self.table.setHorizontalHeaderLabels(["Name/Synonym", "Formula", "SMILES"])
|
|
61
|
+
# ヘッダー調整
|
|
62
|
+
header = self.table.horizontalHeader()
|
|
63
|
+
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
|
64
|
+
header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
|
|
65
|
+
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
|
66
|
+
|
|
67
|
+
self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
|
68
|
+
self.table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
|
69
|
+
# 選択変更時にロードボタンを有効化するなどの処理を入れるならここ
|
|
70
|
+
layout.addWidget(self.table)
|
|
71
|
+
|
|
72
|
+
# --- ボタンエリア ---
|
|
73
|
+
btn_layout = QHBoxLayout()
|
|
74
|
+
|
|
75
|
+
self.btn_load = QPushButton("Load to 2D Editor")
|
|
76
|
+
self.btn_load.clicked.connect(self.load_molecule)
|
|
77
|
+
|
|
78
|
+
self.btn_close = QPushButton("Close")
|
|
79
|
+
self.btn_close.clicked.connect(self.close)
|
|
80
|
+
|
|
81
|
+
btn_layout.addWidget(self.btn_load)
|
|
82
|
+
btn_layout.addWidget(self.btn_close)
|
|
83
|
+
layout.addLayout(btn_layout)
|
|
84
|
+
|
|
85
|
+
def run_search(self):
|
|
86
|
+
query = self.line_input.text().strip()
|
|
87
|
+
if not query:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
self.lbl_info.setText("Searching PubChem... please wait.")
|
|
91
|
+
self.btn_search.setEnabled(False)
|
|
92
|
+
self.table.setRowCount(0)
|
|
93
|
+
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
|
|
94
|
+
QApplication.processEvents()
|
|
95
|
+
|
|
96
|
+
results = []
|
|
97
|
+
error_msg = None
|
|
98
|
+
network_error = False
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
search_type = self.combo_type.currentText()
|
|
102
|
+
|
|
103
|
+
if search_type == "SMILES":
|
|
104
|
+
# SMILESの場合は直接リストに追加(検証含む)
|
|
105
|
+
mol = Chem.MolFromSmiles(query)
|
|
106
|
+
if mol:
|
|
107
|
+
results.append({
|
|
108
|
+
'name': 'User Input SMILES',
|
|
109
|
+
'smiles': query,
|
|
110
|
+
'formula': Chem.rdMolDescriptors.CalcMolFormula(mol)
|
|
111
|
+
})
|
|
112
|
+
else:
|
|
113
|
+
raise ValueError("Invalid SMILES string.")
|
|
114
|
+
|
|
115
|
+
else: # Auto (PubChem API)
|
|
116
|
+
# PUG REST APIを使用して検索
|
|
117
|
+
# プロパティとしてSMILESと分子式を取得
|
|
118
|
+
# CanonicalSMILESも取得してフォールバックに使用
|
|
119
|
+
url = f"https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name/{query}/property/IsomericSMILES,CanonicalSMILES,MolecularFormula,Title/JSON"
|
|
120
|
+
|
|
121
|
+
response = requests.get(url, timeout=10)
|
|
122
|
+
|
|
123
|
+
if response.status_code == 200:
|
|
124
|
+
data = response.json()
|
|
125
|
+
props = data.get('PropertyTable', {}).get('Properties', [])
|
|
126
|
+
for p in props:
|
|
127
|
+
# SMILESの取得(Isomericを優先、なければCanonical、それでもなければキー検索)
|
|
128
|
+
smiles = p.get('IsomericSMILES')
|
|
129
|
+
if not smiles:
|
|
130
|
+
smiles = p.get('CanonicalSMILES')
|
|
131
|
+
|
|
132
|
+
# まだ取得できていない場合、キー名に'SMILES'が含まれるものを探す
|
|
133
|
+
if not smiles:
|
|
134
|
+
for k in p.keys():
|
|
135
|
+
if 'SMILES' in k:
|
|
136
|
+
smiles = p[k]
|
|
137
|
+
if smiles:
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
if not smiles:
|
|
141
|
+
smiles = "" # 見つからない場合
|
|
142
|
+
|
|
143
|
+
results.append({
|
|
144
|
+
'name': p.get('Title', query),
|
|
145
|
+
'smiles': smiles,
|
|
146
|
+
'formula': p.get('MolecularFormula', '')
|
|
147
|
+
})
|
|
148
|
+
else:
|
|
149
|
+
# found nothing or error status
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
except requests.exceptions.RequestException:
|
|
153
|
+
network_error = True
|
|
154
|
+
except Exception as e:
|
|
155
|
+
error_msg = str(e)
|
|
156
|
+
finally:
|
|
157
|
+
QApplication.restoreOverrideCursor()
|
|
158
|
+
self.btn_search.setEnabled(True)
|
|
159
|
+
|
|
160
|
+
# UI updates after cursor restore
|
|
161
|
+
if network_error:
|
|
162
|
+
QMessageBox.critical(self, PLUGIN_NAME, "Network error. Please check your internet connection.")
|
|
163
|
+
self.lbl_info.setText("Network error.")
|
|
164
|
+
elif error_msg:
|
|
165
|
+
QMessageBox.critical(self, PLUGIN_NAME, f"Error: {error_msg}")
|
|
166
|
+
self.lbl_info.setText("Error occurred.")
|
|
167
|
+
elif not results and 'response' in locals() and response.status_code != 200:
|
|
168
|
+
self.lbl_info.setText("Not found in PubChem search.")
|
|
169
|
+
else:
|
|
170
|
+
# データを保持
|
|
171
|
+
self.candidates_data = results
|
|
172
|
+
self.update_table()
|
|
173
|
+
|
|
174
|
+
if results:
|
|
175
|
+
self.lbl_info.setText(f"Found {len(results)} candidates. Select one and click Load to 2D Editor.")
|
|
176
|
+
else:
|
|
177
|
+
self.lbl_info.setText("No results found.")
|
|
178
|
+
|
|
179
|
+
def update_table(self):
|
|
180
|
+
self.table.setRowCount(0)
|
|
181
|
+
|
|
182
|
+
for i, data in enumerate(self.candidates_data):
|
|
183
|
+
row_idx = self.table.rowCount()
|
|
184
|
+
self.table.insertRow(row_idx)
|
|
185
|
+
|
|
186
|
+
self.table.setItem(row_idx, 0, QTableWidgetItem(str(data['name'])))
|
|
187
|
+
self.table.setItem(row_idx, 1, QTableWidgetItem(str(data['formula'])))
|
|
188
|
+
self.table.setItem(row_idx, 2, QTableWidgetItem(str(data['smiles'])))
|
|
189
|
+
|
|
190
|
+
def load_molecule(self):
|
|
191
|
+
"""選択された行のSMILESから2D構造を生成し、メインウィンドウのエディタに入れる"""
|
|
192
|
+
selected_items = self.table.selectedItems()
|
|
193
|
+
if not selected_items:
|
|
194
|
+
QMessageBox.warning(self, PLUGIN_NAME, "Please select a molecule from the list.")
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
row = selected_items[0].row()
|
|
198
|
+
smiles = self.candidates_data[row]['smiles']
|
|
199
|
+
name = self.candidates_data[row]['name']
|
|
200
|
+
|
|
201
|
+
if not smiles:
|
|
202
|
+
QMessageBox.warning(self, PLUGIN_NAME, "No SMILES data available for this entry.")
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
self.lbl_info.setText("Loading into 2D Editor...")
|
|
206
|
+
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
|
|
207
|
+
QApplication.processEvents()
|
|
208
|
+
|
|
209
|
+
success = False
|
|
210
|
+
error_msg = None
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
# メインウィンドウのSMILES読み込み機能を使用
|
|
214
|
+
if hasattr(self.main_window, "load_from_smiles"):
|
|
215
|
+
self.main_window.load_from_smiles(smiles)
|
|
216
|
+
success = True
|
|
217
|
+
else:
|
|
218
|
+
error_msg = "Main window does not support 'load_from_smiles'."
|
|
219
|
+
|
|
220
|
+
except Exception as e:
|
|
221
|
+
error_msg = str(e)
|
|
222
|
+
|
|
223
|
+
finally:
|
|
224
|
+
QApplication.restoreOverrideCursor()
|
|
225
|
+
|
|
226
|
+
if success:
|
|
227
|
+
self.lbl_info.setText(f"Loaded: {name}")
|
|
228
|
+
QMessageBox.information(self, PLUGIN_NAME, f"Successfully loaded: {name}")
|
|
229
|
+
self.accept() # ダイアログを閉じる
|
|
230
|
+
elif error_msg:
|
|
231
|
+
QMessageBox.critical(self, PLUGIN_NAME, f"Error: {error_msg}")
|
|
232
|
+
self.lbl_info.setText("Load failed.")
|
|
233
|
+
|
|
234
|
+
def run(mw):
|
|
235
|
+
if hasattr(mw, "_molecule_resolver_dialog") and mw._molecule_resolver_dialog.isVisible():
|
|
236
|
+
mw._molecule_resolver_dialog.raise_()
|
|
237
|
+
mw._molecule_resolver_dialog.activateWindow()
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
dialog = MoleculeResolverDialog(mw, parent=mw)
|
|
241
|
+
mw._molecule_resolver_dialog = dialog
|
|
242
|
+
dialog.show()
|
|
243
|
+
|
|
244
|
+
# initialize removed as it only registered the menu action
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
import traceback
|
|
5
|
+
import pyvista as pv
|
|
6
|
+
import numpy as np
|
|
7
|
+
import functools
|
|
8
|
+
import types
|
|
9
|
+
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QLabel,
|
|
10
|
+
QSlider, QHBoxLayout, QPushButton, QDoubleSpinBox)
|
|
11
|
+
from PyQt6.QtGui import QAction, QColor
|
|
12
|
+
from PyQt6.QtCore import Qt, QTimer
|
|
13
|
+
|
|
14
|
+
# Try to import VDW radii from constants, fallback if needed
|
|
15
|
+
try:
|
|
16
|
+
from moleditpy.modules.constants import pt, CPK_COLORS_PV
|
|
17
|
+
except ImportError:
|
|
18
|
+
try:
|
|
19
|
+
from modules.constants import pt, CPK_COLORS_PV
|
|
20
|
+
except ImportError:
|
|
21
|
+
try:
|
|
22
|
+
from rdkit import Chem
|
|
23
|
+
pt = Chem.GetPeriodicTable()
|
|
24
|
+
except Exception:
|
|
25
|
+
pt = None
|
|
26
|
+
CPK_COLORS_PV = {} # Last resort fallback
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Plugin Metadata
|
|
30
|
+
__version__="2025.12.17"
|
|
31
|
+
__author__="HiroYokoyama"
|
|
32
|
+
PLUGIN_NAME = "VDW Radii Overlay"
|
|
33
|
+
SETTINGS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "vdw_radii_overlay.json")
|
|
34
|
+
|
|
35
|
+
# Global State
|
|
36
|
+
_config_window = None
|
|
37
|
+
_vdw_settings = {
|
|
38
|
+
"occupancy": 0.3, # Opacity (0.0 - 1.0)
|
|
39
|
+
"resolution": 0.125 # Voxel spacing in Angstroms
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
def load_settings():
|
|
43
|
+
global _vdw_settings
|
|
44
|
+
try:
|
|
45
|
+
if os.path.exists(SETTINGS_FILE):
|
|
46
|
+
with open(SETTINGS_FILE, 'r') as f:
|
|
47
|
+
saved = json.load(f)
|
|
48
|
+
if "occupancy" in saved:
|
|
49
|
+
_vdw_settings["occupancy"] = float(saved["occupancy"])
|
|
50
|
+
if "resolution" in saved:
|
|
51
|
+
_vdw_settings["resolution"] = float(saved["resolution"])
|
|
52
|
+
except Exception as e:
|
|
53
|
+
print(f"Error loading VDW settings: {e}")
|
|
54
|
+
|
|
55
|
+
def save_settings():
|
|
56
|
+
try:
|
|
57
|
+
with open(SETTINGS_FILE, 'w') as f:
|
|
58
|
+
json.dump(_vdw_settings, f, indent=4)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
print(f"Error saving VDW settings: {e}")
|
|
61
|
+
|
|
62
|
+
class VDWConfigWindow(QDialog):
|
|
63
|
+
def __init__(self, main_window):
|
|
64
|
+
super().__init__(parent=main_window)
|
|
65
|
+
self.mw = main_window
|
|
66
|
+
self.setWindowTitle("VDW Overlay Settings")
|
|
67
|
+
self.setModal(False)
|
|
68
|
+
self.resize(350, 150)
|
|
69
|
+
self.init_ui()
|
|
70
|
+
|
|
71
|
+
def init_ui(self):
|
|
72
|
+
layout = QVBoxLayout()
|
|
73
|
+
|
|
74
|
+
# Occupancy Slider
|
|
75
|
+
occ_layout = QHBoxLayout()
|
|
76
|
+
occ_layout.addWidget(QLabel("Occupancy:"))
|
|
77
|
+
self.slider_occ = QSlider(Qt.Orientation.Horizontal)
|
|
78
|
+
self.slider_occ.setRange(0, 100)
|
|
79
|
+
current_occ = _vdw_settings.get("occupancy", 0.3)
|
|
80
|
+
self.slider_occ.setValue(int(current_occ * 100))
|
|
81
|
+
self.slider_occ.valueChanged.connect(self.on_occupancy_slider_changed)
|
|
82
|
+
occ_layout.addWidget(self.slider_occ)
|
|
83
|
+
|
|
84
|
+
self.spin_occ = QDoubleSpinBox()
|
|
85
|
+
self.spin_occ.setRange(0.0, 1.0)
|
|
86
|
+
self.spin_occ.setSingleStep(0.05)
|
|
87
|
+
self.spin_occ.setValue(current_occ)
|
|
88
|
+
self.spin_occ.valueChanged.connect(self.on_occupancy_spin_changed)
|
|
89
|
+
occ_layout.addWidget(self.spin_occ)
|
|
90
|
+
|
|
91
|
+
layout.addLayout(occ_layout)
|
|
92
|
+
|
|
93
|
+
# Resolution Slider
|
|
94
|
+
res_layout = QHBoxLayout()
|
|
95
|
+
res_layout.addWidget(QLabel("Resolution (Å):"))
|
|
96
|
+
self.slider_res = QSlider(Qt.Orientation.Horizontal)
|
|
97
|
+
self.slider_res.setRange(5, 50) # 0.05 to 0.50
|
|
98
|
+
current_res = _vdw_settings.get("resolution", 0.125)
|
|
99
|
+
self.slider_res.setValue(int(current_res * 100))
|
|
100
|
+
self.slider_res.valueChanged.connect(self.on_resolution_slider_changed)
|
|
101
|
+
res_layout.addWidget(self.slider_res)
|
|
102
|
+
|
|
103
|
+
self.spin_res = QDoubleSpinBox()
|
|
104
|
+
self.spin_res.setRange(0.05, 0.50)
|
|
105
|
+
self.spin_res.setSingleStep(0.005)
|
|
106
|
+
self.spin_res.setDecimals(3)
|
|
107
|
+
self.spin_res.setValue(current_res)
|
|
108
|
+
self.spin_res.valueChanged.connect(self.on_resolution_spin_changed)
|
|
109
|
+
res_layout.addWidget(self.spin_res)
|
|
110
|
+
|
|
111
|
+
layout.addLayout(res_layout)
|
|
112
|
+
|
|
113
|
+
# Reset Button
|
|
114
|
+
btn_reset = QPushButton("Reset to Defaults")
|
|
115
|
+
btn_reset.clicked.connect(self.reset_defaults)
|
|
116
|
+
layout.addWidget(btn_reset)
|
|
117
|
+
|
|
118
|
+
# Close Button
|
|
119
|
+
btn_close = QPushButton("Close")
|
|
120
|
+
btn_close.clicked.connect(self.close)
|
|
121
|
+
layout.addWidget(btn_close)
|
|
122
|
+
|
|
123
|
+
self.setLayout(layout)
|
|
124
|
+
|
|
125
|
+
def on_occupancy_slider_changed(self, value):
|
|
126
|
+
val_float = value / 100.0
|
|
127
|
+
self.spin_occ.blockSignals(True)
|
|
128
|
+
self.spin_occ.setValue(val_float)
|
|
129
|
+
self.spin_occ.blockSignals(False)
|
|
130
|
+
self._update_occupancy(val_float)
|
|
131
|
+
|
|
132
|
+
def on_occupancy_spin_changed(self, value):
|
|
133
|
+
val_int = int(value * 100)
|
|
134
|
+
self.slider_occ.blockSignals(True)
|
|
135
|
+
self.slider_occ.setValue(val_int)
|
|
136
|
+
self.slider_occ.blockSignals(False)
|
|
137
|
+
self._update_occupancy(value)
|
|
138
|
+
|
|
139
|
+
def _update_occupancy(self, value):
|
|
140
|
+
_vdw_settings["occupancy"] = value
|
|
141
|
+
save_settings()
|
|
142
|
+
self.update_view()
|
|
143
|
+
|
|
144
|
+
def on_resolution_slider_changed(self, value):
|
|
145
|
+
val_float = value / 100.0
|
|
146
|
+
self.spin_res.blockSignals(True)
|
|
147
|
+
self.spin_res.setValue(val_float)
|
|
148
|
+
self.spin_res.blockSignals(False)
|
|
149
|
+
self._update_resolution(val_float)
|
|
150
|
+
|
|
151
|
+
def on_resolution_spin_changed(self, value):
|
|
152
|
+
val_int = int(value * 100)
|
|
153
|
+
self.slider_res.blockSignals(True)
|
|
154
|
+
self.slider_res.setValue(val_int)
|
|
155
|
+
self.slider_res.blockSignals(False)
|
|
156
|
+
self._update_resolution(value)
|
|
157
|
+
|
|
158
|
+
def _update_resolution(self, value):
|
|
159
|
+
_vdw_settings["resolution"] = value
|
|
160
|
+
save_settings()
|
|
161
|
+
self.update_view()
|
|
162
|
+
|
|
163
|
+
def reset_defaults(self):
|
|
164
|
+
# Default values
|
|
165
|
+
def_occ = 0.3
|
|
166
|
+
def_res = 0.125
|
|
167
|
+
|
|
168
|
+
# Block signals to prevent redundant updates/saves during setting
|
|
169
|
+
self.slider_occ.blockSignals(True)
|
|
170
|
+
self.spin_occ.blockSignals(True)
|
|
171
|
+
self.slider_res.blockSignals(True)
|
|
172
|
+
self.spin_res.blockSignals(True)
|
|
173
|
+
|
|
174
|
+
# Set values
|
|
175
|
+
self.slider_occ.setValue(int(def_occ * 100))
|
|
176
|
+
self.spin_occ.setValue(def_occ)
|
|
177
|
+
self.slider_res.setValue(int(def_res * 100))
|
|
178
|
+
self.spin_res.setValue(def_res)
|
|
179
|
+
|
|
180
|
+
# Unblock
|
|
181
|
+
self.slider_occ.blockSignals(False)
|
|
182
|
+
self.spin_occ.blockSignals(False)
|
|
183
|
+
self.slider_res.blockSignals(False)
|
|
184
|
+
self.spin_res.blockSignals(False)
|
|
185
|
+
|
|
186
|
+
# Update settings and view once
|
|
187
|
+
_vdw_settings["occupancy"] = def_occ
|
|
188
|
+
_vdw_settings["resolution"] = def_res
|
|
189
|
+
save_settings()
|
|
190
|
+
self.update_view()
|
|
191
|
+
|
|
192
|
+
def refresh_ui_values(self):
|
|
193
|
+
"""Update UI elements from global settings."""
|
|
194
|
+
occ = _vdw_settings.get("occupancy", 0.3)
|
|
195
|
+
res = _vdw_settings.get("resolution", 0.125)
|
|
196
|
+
|
|
197
|
+
self.slider_occ.blockSignals(True)
|
|
198
|
+
self.spin_occ.blockSignals(True)
|
|
199
|
+
self.slider_res.blockSignals(True)
|
|
200
|
+
self.spin_res.blockSignals(True)
|
|
201
|
+
|
|
202
|
+
self.slider_occ.setValue(int(occ * 100))
|
|
203
|
+
self.spin_occ.setValue(occ)
|
|
204
|
+
self.slider_res.setValue(int(res * 100))
|
|
205
|
+
self.spin_res.setValue(res)
|
|
206
|
+
|
|
207
|
+
self.slider_occ.blockSignals(False)
|
|
208
|
+
self.spin_occ.blockSignals(False)
|
|
209
|
+
self.slider_res.blockSignals(False)
|
|
210
|
+
self.spin_res.blockSignals(False)
|
|
211
|
+
|
|
212
|
+
def update_view(self):
|
|
213
|
+
# Trigger redraw if we are in the correct mode
|
|
214
|
+
if hasattr(self.mw, 'current_3d_style') and self.mw.current_3d_style == "vdw_overlay":
|
|
215
|
+
if getattr(self.mw, 'current_mol', None):
|
|
216
|
+
# Trigger redraw
|
|
217
|
+
if hasattr(self.mw, 'draw_molecule_3d'):
|
|
218
|
+
self.mw.draw_molecule_3d(self.mw.current_mol)
|
|
219
|
+
|
|
220
|
+
def draw_vdw_overlay(mw, mol):
|
|
221
|
+
"""
|
|
222
|
+
Callback for drawing the VDW overlay style.
|
|
223
|
+
Registered via context.register_3d_style.
|
|
224
|
+
"""
|
|
225
|
+
# 1. Draw standard Ball & Stick
|
|
226
|
+
# Attempt to locate the standard draw method
|
|
227
|
+
draw_std = None
|
|
228
|
+
if hasattr(mw, 'main_window_view_3d') and hasattr(mw.main_window_view_3d, 'draw_standard_3d_style'):
|
|
229
|
+
draw_std = mw.main_window_view_3d.draw_standard_3d_style
|
|
230
|
+
elif hasattr(mw, 'view3d') and hasattr(mw.view3d, 'draw_standard_3d_style'):
|
|
231
|
+
draw_std = mw.view3d.draw_standard_3d_style
|
|
232
|
+
|
|
233
|
+
if draw_std:
|
|
234
|
+
draw_std(mol, style_override='ball_and_stick')
|
|
235
|
+
else:
|
|
236
|
+
print("VDW Plugin Error: could not find draw_standard_3d_style")
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
# 2. Draw VDW Surface Overlay
|
|
240
|
+
if mol and mol.GetNumAtoms() > 0:
|
|
241
|
+
try:
|
|
242
|
+
positions = []
|
|
243
|
+
radii = []
|
|
244
|
+
atom_colors = []
|
|
245
|
+
|
|
246
|
+
# Use custom colors if available (API-based or legacy)
|
|
247
|
+
custom_map = getattr(mw, '_plugin_color_overrides', {})
|
|
248
|
+
if not custom_map:
|
|
249
|
+
custom_map = getattr(mw, 'custom_atom_colors', {})
|
|
250
|
+
|
|
251
|
+
if mol.GetNumConformers() > 0:
|
|
252
|
+
conf = mol.GetConformer()
|
|
253
|
+
for i in range(mol.GetNumAtoms()):
|
|
254
|
+
atom = mol.GetAtomWithIdx(i)
|
|
255
|
+
pos = conf.GetAtomPosition(i)
|
|
256
|
+
positions.append([pos.x, pos.y, pos.z])
|
|
257
|
+
|
|
258
|
+
sym = atom.GetSymbol()
|
|
259
|
+
# GetRvdw returns standard VDW radius.
|
|
260
|
+
r = 1.5 # Default
|
|
261
|
+
if pt:
|
|
262
|
+
r = pt.GetRvdw(atom.GetAtomicNum())
|
|
263
|
+
radii.append(r)
|
|
264
|
+
|
|
265
|
+
# Color handling
|
|
266
|
+
if i in custom_map:
|
|
267
|
+
val = custom_map[i]
|
|
268
|
+
# Handling new API (Hex string) vs Legacy (List/Tuple)
|
|
269
|
+
if isinstance(val, str) and val.startswith('#'):
|
|
270
|
+
# Convert Hex to RGB [0-1]
|
|
271
|
+
qc = QColor(val)
|
|
272
|
+
c = [qc.redF(), qc.greenF(), qc.blueF()]
|
|
273
|
+
else:
|
|
274
|
+
# Assume legacy list/tuple
|
|
275
|
+
c = val
|
|
276
|
+
# Normalize 0-255 to 0-1 if needed
|
|
277
|
+
if any(x > 1.0 for x in c):
|
|
278
|
+
c = [x/255.0 for x in c]
|
|
279
|
+
else:
|
|
280
|
+
c = CPK_COLORS_PV.get(sym, [0.8, 0.8, 0.8]) # Default grey if missing
|
|
281
|
+
atom_colors.append(c)
|
|
282
|
+
|
|
283
|
+
if positions:
|
|
284
|
+
positions = np.array(positions)
|
|
285
|
+
radii = np.array(radii)
|
|
286
|
+
atom_colors = np.array(atom_colors)
|
|
287
|
+
|
|
288
|
+
# --- Generate Merged Surface (SDF) ---
|
|
289
|
+
|
|
290
|
+
# 1. Define Grid Bounds
|
|
291
|
+
padding = radii.max() + 1.0
|
|
292
|
+
min_bounds = positions.min(axis=0) - padding
|
|
293
|
+
max_bounds = positions.max(axis=0) + padding
|
|
294
|
+
|
|
295
|
+
# Resolution (voxel size in Angstroms)
|
|
296
|
+
res_val = _vdw_settings.get("resolution", 0.125)
|
|
297
|
+
# Clamp to safe limits just in case
|
|
298
|
+
if res_val < 0.01: res_val = 0.01
|
|
299
|
+
spacing = (res_val, res_val, res_val)
|
|
300
|
+
|
|
301
|
+
dims = np.ceil((max_bounds - min_bounds) / spacing).astype(int)
|
|
302
|
+
|
|
303
|
+
grid = pv.ImageData()
|
|
304
|
+
grid.dimensions = dims
|
|
305
|
+
grid.origin = min_bounds
|
|
306
|
+
grid.spacing = spacing
|
|
307
|
+
|
|
308
|
+
grid_points = grid.points
|
|
309
|
+
n_points = grid_points.shape[0]
|
|
310
|
+
values = np.empty(n_points)
|
|
311
|
+
|
|
312
|
+
# Process in chunks of 100k points
|
|
313
|
+
chunk_size = 100000
|
|
314
|
+
for start_idx in range(0, n_points, chunk_size):
|
|
315
|
+
end_idx = min(start_idx + chunk_size, n_points)
|
|
316
|
+
chunk_pts = grid_points[start_idx:end_idx]
|
|
317
|
+
|
|
318
|
+
d = np.linalg.norm(chunk_pts[:, np.newaxis, :] - positions[np.newaxis, :, :], axis=2)
|
|
319
|
+
d_surface = d - radii[np.newaxis, :]
|
|
320
|
+
values[start_idx:end_idx] = d_surface.min(axis=1)
|
|
321
|
+
|
|
322
|
+
grid.point_data["values"] = values
|
|
323
|
+
|
|
324
|
+
# 3. Contour at iso-value 0 to get the surface
|
|
325
|
+
mesh = grid.contour([0], scalars="values")
|
|
326
|
+
|
|
327
|
+
# 4. Map Colors to Surface Vertices
|
|
328
|
+
mesh_points = mesh.points
|
|
329
|
+
if mesh_points.shape[0] > 0:
|
|
330
|
+
n_mesh_pts = mesh_points.shape[0]
|
|
331
|
+
mesh_colors = np.zeros((n_mesh_pts, 3))
|
|
332
|
+
|
|
333
|
+
chunk_size = 50000
|
|
334
|
+
for start_idx in range(0, n_mesh_pts, chunk_size):
|
|
335
|
+
end_idx = min(start_idx + chunk_size, n_mesh_pts)
|
|
336
|
+
chunk_pts = mesh_points[start_idx:end_idx]
|
|
337
|
+
|
|
338
|
+
d_center = np.linalg.norm(chunk_pts[:, np.newaxis, :] - positions[np.newaxis, :, :], axis=2)
|
|
339
|
+
d_surface = d_center - radii[np.newaxis, :]
|
|
340
|
+
nearest_atom_indices = np.argmin(d_surface, axis=1)
|
|
341
|
+
mesh_colors[start_idx:end_idx] = atom_colors[nearest_atom_indices]
|
|
342
|
+
|
|
343
|
+
mesh.point_data["AtomColors"] = mesh_colors
|
|
344
|
+
|
|
345
|
+
opacity = _vdw_settings.get("occupancy", 0.3)
|
|
346
|
+
|
|
347
|
+
# Assume mw.plotter is available
|
|
348
|
+
if hasattr(mw, 'plotter'):
|
|
349
|
+
mw.plotter.add_mesh(
|
|
350
|
+
mesh,
|
|
351
|
+
scalars="AtomColors",
|
|
352
|
+
rgb=True,
|
|
353
|
+
opacity=opacity,
|
|
354
|
+
smooth_shading=True,
|
|
355
|
+
specular=0.2,
|
|
356
|
+
name="vdw_overlay_mesh"
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
except Exception as e:
|
|
360
|
+
print(f"VDW Overlay Error: {e}")
|
|
361
|
+
traceback.print_exc()
|
|
362
|
+
|
|
363
|
+
def run(mw):
|
|
364
|
+
global _config_window
|
|
365
|
+
load_settings()
|
|
366
|
+
|
|
367
|
+
if _config_window is None:
|
|
368
|
+
_config_window = VDWConfigWindow(mw)
|
|
369
|
+
_config_window.finished.connect(lambda: _cleanup_config())
|
|
370
|
+
|
|
371
|
+
# Ensure UI reflects the loaded settings (important if window was already open or reused)
|
|
372
|
+
_config_window.refresh_ui_values()
|
|
373
|
+
|
|
374
|
+
_config_window.show()
|
|
375
|
+
_config_window.raise_()
|
|
376
|
+
_config_window.activateWindow()
|
|
377
|
+
|
|
378
|
+
def initialize(context):
|
|
379
|
+
"""
|
|
380
|
+
New Plugin System Entry Point
|
|
381
|
+
"""
|
|
382
|
+
mw = context.get_main_window()
|
|
383
|
+
load_settings()
|
|
384
|
+
|
|
385
|
+
# Register 3D Style
|
|
386
|
+
if hasattr(context, 'register_3d_style'):
|
|
387
|
+
context.register_3d_style("vdw_overlay", draw_vdw_overlay)
|
|
388
|
+
else:
|
|
389
|
+
print("Error: PluginContext does not support register_3d_style")
|
|
390
|
+
|
|
391
|
+
# 1. Register Configuration Menu - REMOVED in favor of run()
|
|
392
|
+
# (Managed by run() function now)
|
|
393
|
+
|
|
394
|
+
# 2. Inject into Style Menu (Legacy behavior preserved via mw access)
|
|
395
|
+
def add_menu_item():
|
|
396
|
+
# Locate the "3D Style" tool button on the main toolbar
|
|
397
|
+
style_button = getattr(mw, 'style_button', None)
|
|
398
|
+
|
|
399
|
+
if style_button and style_button.menu():
|
|
400
|
+
style_menu = style_button.menu()
|
|
401
|
+
|
|
402
|
+
# Check if already added
|
|
403
|
+
exists = False
|
|
404
|
+
for a in style_menu.actions():
|
|
405
|
+
if a.text() == "VDW Overlay":
|
|
406
|
+
exists = True
|
|
407
|
+
break
|
|
408
|
+
|
|
409
|
+
if not exists:
|
|
410
|
+
# Add to action group for mutual exclusion
|
|
411
|
+
existing_actions = style_menu.actions()
|
|
412
|
+
group = None
|
|
413
|
+
if existing_actions:
|
|
414
|
+
group = existing_actions[0].actionGroup()
|
|
415
|
+
|
|
416
|
+
action = QAction("VDW Overlay", mw)
|
|
417
|
+
action.setCheckable(True)
|
|
418
|
+
|
|
419
|
+
if group:
|
|
420
|
+
group.addAction(action)
|
|
421
|
+
|
|
422
|
+
# Connect to set_3d_style
|
|
423
|
+
action.triggered.connect(lambda: mw.set_3d_style("vdw_overlay"))
|
|
424
|
+
|
|
425
|
+
style_menu.addAction(action)
|
|
426
|
+
|
|
427
|
+
# Run after 2000ms to ensure UI is ready
|
|
428
|
+
QTimer.singleShot(2000, add_menu_item)
|
|
429
|
+
|
|
430
|
+
def _cleanup_config():
|
|
431
|
+
global _config_window
|
|
432
|
+
_config_window = None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.0a2
|
|
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
|