MoleditPy 2.2.0a2__py3-none-any.whl → 2.2.0a3__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.
Files changed (27) hide show
  1. moleditpy/modules/constants.py +1 -1
  2. moleditpy/modules/main_window_main_init.py +31 -13
  3. moleditpy/modules/plugin_interface.py +1 -10
  4. moleditpy/modules/plugin_manager.py +0 -3
  5. {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.0a3.dist-info}/METADATA +1 -1
  6. {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.0a3.dist-info}/RECORD +10 -27
  7. moleditpy/plugins/Analysis/ms_spectrum_neo.py +0 -919
  8. moleditpy/plugins/File/animated_xyz_giffer.py +0 -583
  9. moleditpy/plugins/File/cube_viewer.py +0 -689
  10. moleditpy/plugins/File/gaussian_fchk_freq_analyzer.py +0 -1148
  11. moleditpy/plugins/File/mapped_cube_viewer.py +0 -552
  12. moleditpy/plugins/File/orca_out_freq_analyzer.py +0 -1226
  13. moleditpy/plugins/File/paste_xyz.py +0 -336
  14. moleditpy/plugins/Input Generator/gaussian_input_generator_neo.py +0 -930
  15. moleditpy/plugins/Input Generator/orca_input_generator_neo.py +0 -1028
  16. moleditpy/plugins/Input Generator/orca_xyz2inp_gui.py +0 -286
  17. moleditpy/plugins/Optimization/all-trans_optimizer.py +0 -65
  18. moleditpy/plugins/Optimization/complex_molecule_untangler.py +0 -268
  19. moleditpy/plugins/Optimization/conf_search.py +0 -224
  20. moleditpy/plugins/Utility/atom_colorizer.py +0 -262
  21. moleditpy/plugins/Utility/console.py +0 -163
  22. moleditpy/plugins/Utility/pubchem_ressolver.py +0 -244
  23. moleditpy/plugins/Utility/vdw_radii_overlay.py +0 -432
  24. {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.0a3.dist-info}/WHEEL +0 -0
  25. {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.0a3.dist-info}/entry_points.txt +0 -0
  26. {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.0a3.dist-info}/licenses/LICENSE +0 -0
  27. {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.0a3.dist-info}/top_level.txt +0 -0
@@ -1,244 +0,0 @@
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
@@ -1,432 +0,0 @@
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