MoleditPy 1.15.1__py3-none-any.whl → 1.16.0__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/__init__.py +4 -0
- moleditpy/__main__.py +29 -19748
- moleditpy/main.py +37 -0
- moleditpy/modules/__init__.py +36 -0
- moleditpy/modules/about_dialog.py +92 -0
- moleditpy/modules/align_plane_dialog.py +281 -0
- moleditpy/modules/alignment_dialog.py +261 -0
- moleditpy/modules/analysis_window.py +197 -0
- moleditpy/modules/angle_dialog.py +428 -0
- moleditpy/modules/assets/icon.icns +0 -0
- moleditpy/modules/atom_item.py +336 -0
- moleditpy/modules/bond_item.py +303 -0
- moleditpy/modules/bond_length_dialog.py +368 -0
- moleditpy/modules/calculation_worker.py +754 -0
- moleditpy/modules/color_settings_dialog.py +309 -0
- moleditpy/modules/constants.py +76 -0
- moleditpy/modules/constrained_optimization_dialog.py +667 -0
- moleditpy/modules/custom_interactor_style.py +737 -0
- moleditpy/modules/custom_qt_interactor.py +49 -0
- moleditpy/modules/dialog3_d_picking_mixin.py +96 -0
- moleditpy/modules/dihedral_dialog.py +431 -0
- moleditpy/modules/main_window.py +830 -0
- moleditpy/modules/main_window_app_state.py +747 -0
- moleditpy/modules/main_window_compute.py +1203 -0
- moleditpy/modules/main_window_dialog_manager.py +454 -0
- moleditpy/modules/main_window_edit_3d.py +531 -0
- moleditpy/modules/main_window_edit_actions.py +1449 -0
- moleditpy/modules/main_window_export.py +744 -0
- moleditpy/modules/main_window_main_init.py +1641 -0
- moleditpy/modules/main_window_molecular_parsers.py +956 -0
- moleditpy/modules/main_window_project_io.py +429 -0
- moleditpy/modules/main_window_string_importers.py +270 -0
- moleditpy/modules/main_window_ui_manager.py +567 -0
- moleditpy/modules/main_window_view_3d.py +1163 -0
- moleditpy/modules/main_window_view_loaders.py +350 -0
- moleditpy/modules/mirror_dialog.py +110 -0
- moleditpy/modules/molecular_data.py +290 -0
- moleditpy/modules/molecule_scene.py +1895 -0
- moleditpy/modules/move_group_dialog.py +586 -0
- moleditpy/modules/periodic_table_dialog.py +72 -0
- moleditpy/modules/planarize_dialog.py +209 -0
- moleditpy/modules/settings_dialog.py +1034 -0
- moleditpy/modules/template_preview_item.py +148 -0
- moleditpy/modules/template_preview_view.py +62 -0
- moleditpy/modules/translation_dialog.py +353 -0
- moleditpy/modules/user_template_dialog.py +621 -0
- moleditpy/modules/zoomable_view.py +98 -0
- {moleditpy-1.15.1.dist-info → moleditpy-1.16.0.dist-info}/METADATA +1 -1
- moleditpy-1.16.0.dist-info/RECORD +54 -0
- moleditpy-1.15.1.dist-info/RECORD +0 -9
- /moleditpy/{assets → modules/assets}/icon.ico +0 -0
- /moleditpy/{assets → modules/assets}/icon.png +0 -0
- {moleditpy-1.15.1.dist-info → moleditpy-1.16.0.dist-info}/WHEEL +0 -0
- {moleditpy-1.15.1.dist-info → moleditpy-1.16.0.dist-info}/entry_points.txt +0 -0
- {moleditpy-1.15.1.dist-info → moleditpy-1.16.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1163 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
main_window_view_3d.py
|
|
6
|
+
MainWindow (main_window.py) から分離されたモジュール
|
|
7
|
+
機能クラス: MainWindowView3d
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import vtk
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# RDKit imports (explicit to satisfy flake8 and used features)
|
|
16
|
+
from rdkit import Chem
|
|
17
|
+
try:
|
|
18
|
+
pass
|
|
19
|
+
except Exception:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
# PyQt6 Modules
|
|
23
|
+
from PyQt6.QtWidgets import (
|
|
24
|
+
QApplication, QGraphicsView
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from PyQt6.QtGui import (
|
|
28
|
+
QColor, QTransform
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
from PyQt6.QtCore import (
|
|
33
|
+
Qt, QRectF
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
import pyvista as pv
|
|
37
|
+
|
|
38
|
+
# Use centralized Open Babel availability from package-level __init__
|
|
39
|
+
# Use per-package modules availability (local __init__).
|
|
40
|
+
try:
|
|
41
|
+
from . import OBABEL_AVAILABLE
|
|
42
|
+
except Exception:
|
|
43
|
+
from modules import OBABEL_AVAILABLE
|
|
44
|
+
# Only import pybel on demand — `moleditpy` itself doesn't expose `pybel`.
|
|
45
|
+
if OBABEL_AVAILABLE:
|
|
46
|
+
try:
|
|
47
|
+
from openbabel import pybel
|
|
48
|
+
except Exception:
|
|
49
|
+
# If import fails here, disable OBABEL locally; avoid raising
|
|
50
|
+
pybel = None
|
|
51
|
+
OBABEL_AVAILABLE = False
|
|
52
|
+
print("Warning: openbabel.pybel not available. Open Babel fallback and OBabel-based options will be disabled.")
|
|
53
|
+
else:
|
|
54
|
+
pybel = None
|
|
55
|
+
|
|
56
|
+
# Optional SIP helper: on some PyQt6 builds sip.isdeleted is available and
|
|
57
|
+
# allows safely detecting C++ wrapper objects that have been deleted. Import
|
|
58
|
+
# it once at module import time and expose a small, robust wrapper so callers
|
|
59
|
+
# can avoid re-importing sip repeatedly and so we centralize exception
|
|
60
|
+
# handling (this reduces crash risk during teardown and deletion operations).
|
|
61
|
+
try:
|
|
62
|
+
import sip as _sip # type: ignore
|
|
63
|
+
_sip_isdeleted = getattr(_sip, 'isdeleted', None)
|
|
64
|
+
except Exception:
|
|
65
|
+
_sip = None
|
|
66
|
+
_sip_isdeleted = None
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
# package relative imports (preferred when running as `python -m moleditpy`)
|
|
70
|
+
from .constants import CPK_COLORS_PV, DEFAULT_CPK_COLORS, VDW_RADII, pt
|
|
71
|
+
from .template_preview_item import TemplatePreviewItem
|
|
72
|
+
except Exception:
|
|
73
|
+
# Fallback to absolute imports for script-style execution
|
|
74
|
+
from modules.constants import CPK_COLORS_PV, DEFAULT_CPK_COLORS, VDW_RADII, pt
|
|
75
|
+
from modules.template_preview_item import TemplatePreviewItem
|
|
76
|
+
|
|
77
|
+
# --- クラス定義 ---
|
|
78
|
+
class MainWindowView3d(object):
|
|
79
|
+
""" main_window.py から分離された機能クラス """
|
|
80
|
+
|
|
81
|
+
def __init__(self, main_window):
|
|
82
|
+
""" クラスの初期化 """
|
|
83
|
+
self.mw = main_window
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def set_3d_style(self, style_name):
|
|
87
|
+
"""3D表示スタイルを設定し、ビューを更新する"""
|
|
88
|
+
if self.current_3d_style == style_name:
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
# 描画モード変更時に測定モードと3D編集モードをリセット
|
|
92
|
+
if self.measurement_mode:
|
|
93
|
+
self.measurement_action.setChecked(False)
|
|
94
|
+
self.toggle_measurement_mode(False) # 測定モードを無効化
|
|
95
|
+
|
|
96
|
+
if self.is_3d_edit_mode:
|
|
97
|
+
self.edit_3d_action.setChecked(False)
|
|
98
|
+
self.toggle_3d_edit_mode(False) # 3D編集モードを無効化
|
|
99
|
+
|
|
100
|
+
# 3D原子選択をクリア
|
|
101
|
+
self.clear_3d_selection()
|
|
102
|
+
|
|
103
|
+
self.current_3d_style = style_name
|
|
104
|
+
self.statusBar().showMessage(f"3D style set to: {style_name}")
|
|
105
|
+
|
|
106
|
+
# 現在表示中の分子があれば、新しいスタイルで再描画する
|
|
107
|
+
if self.current_mol:
|
|
108
|
+
self.draw_molecule_3d(self.current_mol)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def draw_molecule_3d(self, mol):
|
|
113
|
+
"""3D 分子を描画し、軸アクターの参照をクリアする(軸の再制御は apply_3d_settings に任せる)"""
|
|
114
|
+
|
|
115
|
+
# 測定選択をクリア(分子が変更されたため)
|
|
116
|
+
if hasattr(self, 'measurement_mode'):
|
|
117
|
+
self.clear_measurement_selection()
|
|
118
|
+
|
|
119
|
+
# 色情報追跡のための辞書を初期化
|
|
120
|
+
if not hasattr(self, '_3d_color_map'):
|
|
121
|
+
self._3d_color_map = {}
|
|
122
|
+
self._3d_color_map.clear()
|
|
123
|
+
|
|
124
|
+
# 1. カメラ状態とクリア
|
|
125
|
+
camera_state = self.plotter.camera.copy()
|
|
126
|
+
|
|
127
|
+
# **残留防止のための強制削除**
|
|
128
|
+
if self.axes_actor is not None:
|
|
129
|
+
try:
|
|
130
|
+
self.plotter.remove_actor(self.axes_actor)
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
133
|
+
self.axes_actor = None
|
|
134
|
+
|
|
135
|
+
self.plotter.clear()
|
|
136
|
+
|
|
137
|
+
# 2. 背景色の設定
|
|
138
|
+
self.plotter.set_background(self.settings.get('background_color', '#4f4f4f'))
|
|
139
|
+
|
|
140
|
+
# 3. mol が None または原子数ゼロの場合は、背景と軸のみで終了
|
|
141
|
+
if mol is None or mol.GetNumAtoms() == 0:
|
|
142
|
+
self.atom_actor = None
|
|
143
|
+
self.current_mol = None
|
|
144
|
+
self.plotter.render()
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
# 4. ライティングの設定
|
|
148
|
+
is_lighting_enabled = self.settings.get('lighting_enabled', True)
|
|
149
|
+
|
|
150
|
+
if is_lighting_enabled:
|
|
151
|
+
light = pv.Light(
|
|
152
|
+
position=(1, 1, 2),
|
|
153
|
+
light_type='cameralight',
|
|
154
|
+
intensity=self.settings.get('light_intensity', 1.2)
|
|
155
|
+
)
|
|
156
|
+
self.plotter.add_light(light)
|
|
157
|
+
|
|
158
|
+
# 5. 分子描画ロジック
|
|
159
|
+
conf = mol.GetConformer()
|
|
160
|
+
|
|
161
|
+
self.atom_positions_3d = np.array([list(conf.GetAtomPosition(i)) for i in range(mol.GetNumAtoms())])
|
|
162
|
+
|
|
163
|
+
sym = [a.GetSymbol() for a in mol.GetAtoms()]
|
|
164
|
+
col = np.array([CPK_COLORS_PV.get(s, [0.5, 0.5, 0.5]) for s in sym])
|
|
165
|
+
|
|
166
|
+
# スタイルに応じて原子の半径を設定(設定から読み込み)
|
|
167
|
+
if self.current_3d_style == 'cpk':
|
|
168
|
+
atom_scale = self.settings.get('cpk_atom_scale', 1.0)
|
|
169
|
+
resolution = self.settings.get('cpk_resolution', 32)
|
|
170
|
+
rad = np.array([pt.GetRvdw(pt.GetAtomicNumber(s)) * atom_scale for s in sym])
|
|
171
|
+
elif self.current_3d_style == 'wireframe':
|
|
172
|
+
# Wireframeでは原子を描画しないので、この設定は実際には使用されない
|
|
173
|
+
resolution = self.settings.get('wireframe_resolution', 6)
|
|
174
|
+
rad = np.array([0.01 for s in sym]) # 極小値(使用されない)
|
|
175
|
+
elif self.current_3d_style == 'stick':
|
|
176
|
+
atom_radius = self.settings.get('stick_atom_radius', 0.15)
|
|
177
|
+
resolution = self.settings.get('stick_resolution', 16)
|
|
178
|
+
rad = np.array([atom_radius for s in sym])
|
|
179
|
+
else: # ball_and_stick
|
|
180
|
+
atom_scale = self.settings.get('ball_stick_atom_scale', 1.0)
|
|
181
|
+
resolution = self.settings.get('ball_stick_resolution', 16)
|
|
182
|
+
rad = np.array([VDW_RADII.get(s, 0.4) * atom_scale for s in sym])
|
|
183
|
+
|
|
184
|
+
self.glyph_source = pv.PolyData(self.atom_positions_3d)
|
|
185
|
+
self.glyph_source['colors'] = col
|
|
186
|
+
self.glyph_source['radii'] = rad
|
|
187
|
+
|
|
188
|
+
# メッシュプロパティを共通で定義
|
|
189
|
+
mesh_props = dict(
|
|
190
|
+
smooth_shading=True,
|
|
191
|
+
specular=self.settings.get('specular', 0.2),
|
|
192
|
+
specular_power=self.settings.get('specular_power', 20),
|
|
193
|
+
lighting=is_lighting_enabled,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Wireframeスタイルの場合は原子を描画しない
|
|
197
|
+
if self.current_3d_style != 'wireframe':
|
|
198
|
+
glyphs = self.glyph_source.glyph(scale='radii', geom=pv.Sphere(radius=1.0, theta_resolution=resolution, phi_resolution=resolution), orient=False)
|
|
199
|
+
|
|
200
|
+
if is_lighting_enabled:
|
|
201
|
+
self.atom_actor = self.plotter.add_mesh(glyphs, scalars='colors', rgb=True, **mesh_props)
|
|
202
|
+
else:
|
|
203
|
+
self.atom_actor = self.plotter.add_mesh(
|
|
204
|
+
glyphs, scalars='colors', rgb=True,
|
|
205
|
+
style='surface', show_edges=True, edge_color='grey',
|
|
206
|
+
**mesh_props
|
|
207
|
+
)
|
|
208
|
+
self.atom_actor.GetProperty().SetEdgeOpacity(0.3)
|
|
209
|
+
|
|
210
|
+
# 原子の色情報を記録
|
|
211
|
+
for i, atom_color in enumerate(col):
|
|
212
|
+
atom_rgb = [int(c * 255) for c in atom_color]
|
|
213
|
+
self._3d_color_map[f'atom_{i}'] = atom_rgb
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ボンドの描画(ball_and_stick、wireframe、stickで描画)
|
|
217
|
+
if self.current_3d_style in ['ball_and_stick', 'wireframe', 'stick']:
|
|
218
|
+
# スタイルに応じてボンドの太さと解像度を設定(設定から読み込み)
|
|
219
|
+
if self.current_3d_style == 'wireframe':
|
|
220
|
+
cyl_radius = self.settings.get('wireframe_bond_radius', 0.01)
|
|
221
|
+
bond_resolution = self.settings.get('wireframe_resolution', 6)
|
|
222
|
+
elif self.current_3d_style == 'stick':
|
|
223
|
+
cyl_radius = self.settings.get('stick_bond_radius', 0.15)
|
|
224
|
+
bond_resolution = self.settings.get('stick_resolution', 16)
|
|
225
|
+
else: # ball_and_stick
|
|
226
|
+
cyl_radius = self.settings.get('ball_stick_bond_radius', 0.1)
|
|
227
|
+
bond_resolution = self.settings.get('ball_stick_resolution', 16)
|
|
228
|
+
|
|
229
|
+
bond_counter = 0 # 結合の個別識別用
|
|
230
|
+
|
|
231
|
+
# Ball and Stick用のシリンダーリストを準備(高速化のため)
|
|
232
|
+
if self.current_3d_style == 'ball_and_stick':
|
|
233
|
+
bond_cylinders = []
|
|
234
|
+
# Compute the configured grey/uniform bond color for Ball & Stick
|
|
235
|
+
try:
|
|
236
|
+
bs_hex = self.settings.get('ball_stick_bond_color', '#7F7F7F')
|
|
237
|
+
q = QColor(bs_hex)
|
|
238
|
+
bs_bond_rgb = [q.red(), q.green(), q.blue()]
|
|
239
|
+
except Exception:
|
|
240
|
+
bs_bond_rgb = [127, 127, 127]
|
|
241
|
+
|
|
242
|
+
for bond in mol.GetBonds():
|
|
243
|
+
begin_atom_idx = bond.GetBeginAtomIdx()
|
|
244
|
+
end_atom_idx = bond.GetEndAtomIdx()
|
|
245
|
+
sp = np.array(conf.GetAtomPosition(begin_atom_idx))
|
|
246
|
+
ep = np.array(conf.GetAtomPosition(end_atom_idx))
|
|
247
|
+
bt = bond.GetBondType()
|
|
248
|
+
c = (sp + ep) / 2
|
|
249
|
+
d = ep - sp
|
|
250
|
+
h = np.linalg.norm(d)
|
|
251
|
+
if h == 0: continue
|
|
252
|
+
|
|
253
|
+
# ボンドの色を原子の色から決定(各半分で異なる色)
|
|
254
|
+
begin_color = col[begin_atom_idx]
|
|
255
|
+
end_color = col[end_atom_idx]
|
|
256
|
+
|
|
257
|
+
# 結合の色情報を記録
|
|
258
|
+
begin_color_rgb = [int(c * 255) for c in begin_color]
|
|
259
|
+
end_color_rgb = [int(c * 255) for c in end_color]
|
|
260
|
+
|
|
261
|
+
# UI応答性維持のためイベント処理
|
|
262
|
+
QApplication.processEvents()
|
|
263
|
+
if bt == Chem.rdchem.BondType.SINGLE or bt == Chem.rdchem.BondType.AROMATIC:
|
|
264
|
+
if self.current_3d_style == 'ball_and_stick':
|
|
265
|
+
# Ball and stickは全結合をまとめて処理(高速化)
|
|
266
|
+
cyl = pv.Cylinder(center=c, direction=d, radius=cyl_radius, height=h, resolution=bond_resolution)
|
|
267
|
+
bond_cylinders.append(cyl)
|
|
268
|
+
self._3d_color_map[f'bond_{bond_counter}'] = bs_bond_rgb # グレー (configurable)
|
|
269
|
+
else:
|
|
270
|
+
# その他(stick, wireframe)は中央で色が変わる2つの円柱
|
|
271
|
+
mid_point = (sp + ep) / 2
|
|
272
|
+
|
|
273
|
+
# 前半(開始原子の色)
|
|
274
|
+
cyl1 = pv.Cylinder(center=(sp + mid_point) / 2, direction=d, radius=cyl_radius, height=h/2, resolution=bond_resolution)
|
|
275
|
+
actor1 = self.plotter.add_mesh(cyl1, color=begin_color, **mesh_props)
|
|
276
|
+
self._3d_color_map[f'bond_{bond_counter}_start'] = begin_color_rgb
|
|
277
|
+
|
|
278
|
+
# 後半(終了原子の色)
|
|
279
|
+
cyl2 = pv.Cylinder(center=(mid_point + ep) / 2, direction=d, radius=cyl_radius, height=h/2, resolution=bond_resolution)
|
|
280
|
+
actor2 = self.plotter.add_mesh(cyl2, color=end_color, **mesh_props)
|
|
281
|
+
self._3d_color_map[f'bond_{bond_counter}_end'] = end_color_rgb
|
|
282
|
+
else:
|
|
283
|
+
v1 = d / h
|
|
284
|
+
# モデルごとの半径ファクターを適用
|
|
285
|
+
if self.current_3d_style == 'ball_and_stick':
|
|
286
|
+
double_radius_factor = self.settings.get('ball_stick_double_bond_radius_factor', 0.8)
|
|
287
|
+
triple_radius_factor = self.settings.get('ball_stick_triple_bond_radius_factor', 0.75)
|
|
288
|
+
elif self.current_3d_style == 'wireframe':
|
|
289
|
+
double_radius_factor = self.settings.get('wireframe_double_bond_radius_factor', 1.0)
|
|
290
|
+
triple_radius_factor = self.settings.get('wireframe_triple_bond_radius_factor', 0.75)
|
|
291
|
+
elif self.current_3d_style == 'stick':
|
|
292
|
+
double_radius_factor = self.settings.get('stick_double_bond_radius_factor', 0.60)
|
|
293
|
+
triple_radius_factor = self.settings.get('stick_triple_bond_radius_factor', 0.40)
|
|
294
|
+
else:
|
|
295
|
+
double_radius_factor = 1.0
|
|
296
|
+
triple_radius_factor = 0.75
|
|
297
|
+
r = cyl_radius * 0.8 # fallback, will be overridden below
|
|
298
|
+
# 設定からオフセットファクターを取得(モデルごと)
|
|
299
|
+
if self.current_3d_style == 'ball_and_stick':
|
|
300
|
+
double_offset_factor = self.settings.get('ball_stick_double_bond_offset_factor', 2.0)
|
|
301
|
+
triple_offset_factor = self.settings.get('ball_stick_triple_bond_offset_factor', 2.0)
|
|
302
|
+
elif self.current_3d_style == 'wireframe':
|
|
303
|
+
double_offset_factor = self.settings.get('wireframe_double_bond_offset_factor', 3.0)
|
|
304
|
+
triple_offset_factor = self.settings.get('wireframe_triple_bond_offset_factor', 3.0)
|
|
305
|
+
elif self.current_3d_style == 'stick':
|
|
306
|
+
double_offset_factor = self.settings.get('stick_double_bond_offset_factor', 1.5)
|
|
307
|
+
triple_offset_factor = self.settings.get('stick_triple_bond_offset_factor', 1.0)
|
|
308
|
+
else:
|
|
309
|
+
double_offset_factor = 2.0
|
|
310
|
+
triple_offset_factor = 2.0
|
|
311
|
+
s = cyl_radius * 2.0 # デフォルト値
|
|
312
|
+
|
|
313
|
+
if bt == Chem.rdchem.BondType.DOUBLE:
|
|
314
|
+
r = cyl_radius * double_radius_factor
|
|
315
|
+
# 二重結合の場合、結合している原子の他の結合を考慮してオフセット方向を決定
|
|
316
|
+
off_dir = self._calculate_double_bond_offset(mol, bond, conf)
|
|
317
|
+
# 設定から二重結合のオフセットファクターを適用
|
|
318
|
+
s_double = cyl_radius * double_offset_factor
|
|
319
|
+
c1, c2 = c + off_dir * (s_double / 2), c - off_dir * (s_double / 2)
|
|
320
|
+
|
|
321
|
+
if self.current_3d_style == 'ball_and_stick':
|
|
322
|
+
# Ball and stickは全結合をまとめて処理(高速化)
|
|
323
|
+
cyl1 = pv.Cylinder(center=c1, direction=d, radius=r, height=h, resolution=bond_resolution)
|
|
324
|
+
cyl2 = pv.Cylinder(center=c2, direction=d, radius=r, height=h, resolution=bond_resolution)
|
|
325
|
+
bond_cylinders.extend([cyl1, cyl2])
|
|
326
|
+
self._3d_color_map[f'bond_{bond_counter}_1'] = bs_bond_rgb
|
|
327
|
+
self._3d_color_map[f'bond_{bond_counter}_2'] = bs_bond_rgb
|
|
328
|
+
else:
|
|
329
|
+
# その他(stick, wireframe)は中央で色が変わる
|
|
330
|
+
mid_point = (sp + ep) / 2
|
|
331
|
+
|
|
332
|
+
# 第一の結合線(前半・後半)
|
|
333
|
+
cyl1_1 = pv.Cylinder(center=(sp + mid_point) / 2 + off_dir * (s_double / 2), direction=d, radius=r, height=h/2, resolution=bond_resolution)
|
|
334
|
+
cyl1_2 = pv.Cylinder(center=(mid_point + ep) / 2 + off_dir * (s_double / 2), direction=d, radius=r, height=h/2, resolution=bond_resolution)
|
|
335
|
+
self.plotter.add_mesh(cyl1_1, color=begin_color, **mesh_props)
|
|
336
|
+
self.plotter.add_mesh(cyl1_2, color=end_color, **mesh_props)
|
|
337
|
+
self._3d_color_map[f'bond_{bond_counter}_1_start'] = begin_color_rgb
|
|
338
|
+
self._3d_color_map[f'bond_{bond_counter}_1_end'] = end_color_rgb
|
|
339
|
+
|
|
340
|
+
# 第二の結合線(前半・後半)
|
|
341
|
+
cyl2_1 = pv.Cylinder(center=(sp + mid_point) / 2 - off_dir * (s_double / 2), direction=d, radius=r, height=h/2, resolution=bond_resolution)
|
|
342
|
+
cyl2_2 = pv.Cylinder(center=(mid_point + ep) / 2 - off_dir * (s_double / 2), direction=d, radius=r, height=h/2, resolution=bond_resolution)
|
|
343
|
+
self.plotter.add_mesh(cyl2_1, color=begin_color, **mesh_props)
|
|
344
|
+
self.plotter.add_mesh(cyl2_2, color=end_color, **mesh_props)
|
|
345
|
+
self._3d_color_map[f'bond_{bond_counter}_2_start'] = begin_color_rgb
|
|
346
|
+
self._3d_color_map[f'bond_{bond_counter}_2_end'] = end_color_rgb
|
|
347
|
+
elif bt == Chem.rdchem.BondType.TRIPLE:
|
|
348
|
+
r = cyl_radius * triple_radius_factor
|
|
349
|
+
# 三重結合
|
|
350
|
+
v_arb = np.array([0, 0, 1])
|
|
351
|
+
if np.allclose(np.abs(np.dot(v1, v_arb)), 1.0): v_arb = np.array([0, 1, 0])
|
|
352
|
+
off_dir = np.cross(v1, v_arb)
|
|
353
|
+
off_dir /= np.linalg.norm(off_dir)
|
|
354
|
+
|
|
355
|
+
# 設定から三重結合のオフセットファクターを適用
|
|
356
|
+
s_triple = cyl_radius * triple_offset_factor
|
|
357
|
+
|
|
358
|
+
if self.current_3d_style == 'ball_and_stick':
|
|
359
|
+
# Ball and stickは全結合をまとめて処理(高速化)
|
|
360
|
+
cyl1 = pv.Cylinder(center=c, direction=d, radius=r, height=h, resolution=bond_resolution)
|
|
361
|
+
cyl2 = pv.Cylinder(center=c + off_dir * s_triple, direction=d, radius=r, height=h, resolution=bond_resolution)
|
|
362
|
+
cyl3 = pv.Cylinder(center=c - off_dir * s_triple, direction=d, radius=r, height=h, resolution=bond_resolution)
|
|
363
|
+
bond_cylinders.extend([cyl1, cyl2, cyl3])
|
|
364
|
+
self._3d_color_map[f'bond_{bond_counter}_1'] = bs_bond_rgb
|
|
365
|
+
self._3d_color_map[f'bond_{bond_counter}_2'] = bs_bond_rgb
|
|
366
|
+
self._3d_color_map[f'bond_{bond_counter}_3'] = bs_bond_rgb
|
|
367
|
+
else:
|
|
368
|
+
# その他(stick, wireframe)は中央で色が変わる
|
|
369
|
+
mid_point = (sp + ep) / 2
|
|
370
|
+
|
|
371
|
+
# 中央の結合線(前半・後半)
|
|
372
|
+
cyl1_1 = pv.Cylinder(center=(sp + mid_point) / 2, direction=d, radius=r, height=h/2, resolution=bond_resolution)
|
|
373
|
+
cyl1_2 = pv.Cylinder(center=(mid_point + ep) / 2, direction=d, radius=r, height=h/2, resolution=bond_resolution)
|
|
374
|
+
self.plotter.add_mesh(cyl1_1, color=begin_color, **mesh_props)
|
|
375
|
+
self.plotter.add_mesh(cyl1_2, color=end_color, **mesh_props)
|
|
376
|
+
self._3d_color_map[f'bond_{bond_counter}_1_start'] = begin_color_rgb
|
|
377
|
+
self._3d_color_map[f'bond_{bond_counter}_1_end'] = end_color_rgb
|
|
378
|
+
|
|
379
|
+
# 上側の結合線(前半・後半)
|
|
380
|
+
cyl2_1 = pv.Cylinder(center=(sp + mid_point) / 2 + off_dir * s_triple, direction=d, radius=r, height=h/2, resolution=bond_resolution)
|
|
381
|
+
cyl2_2 = pv.Cylinder(center=(mid_point + ep) / 2 + off_dir * s_triple, direction=d, radius=r, height=h/2, resolution=bond_resolution)
|
|
382
|
+
self.plotter.add_mesh(cyl2_1, color=begin_color, **mesh_props)
|
|
383
|
+
self.plotter.add_mesh(cyl2_2, color=end_color, **mesh_props)
|
|
384
|
+
self._3d_color_map[f'bond_{bond_counter}_2_start'] = begin_color_rgb
|
|
385
|
+
self._3d_color_map[f'bond_{bond_counter}_2_end'] = end_color_rgb
|
|
386
|
+
|
|
387
|
+
# 下側の結合線(前半・後半)
|
|
388
|
+
cyl3_1 = pv.Cylinder(center=(sp + mid_point) / 2 - off_dir * s_triple, direction=d, radius=r, height=h/2, resolution=bond_resolution)
|
|
389
|
+
cyl3_2 = pv.Cylinder(center=(mid_point + ep) / 2 - off_dir * s_triple, direction=d, radius=r, height=h/2, resolution=bond_resolution)
|
|
390
|
+
self.plotter.add_mesh(cyl3_1, color=begin_color, **mesh_props)
|
|
391
|
+
self.plotter.add_mesh(cyl3_2, color=end_color, **mesh_props)
|
|
392
|
+
self._3d_color_map[f'bond_{bond_counter}_3_start'] = begin_color_rgb
|
|
393
|
+
self._3d_color_map[f'bond_{bond_counter}_3_end'] = end_color_rgb
|
|
394
|
+
|
|
395
|
+
bond_counter += 1
|
|
396
|
+
|
|
397
|
+
# Ball and Stick用:全結合をまとめて一括描画(高速化)
|
|
398
|
+
if self.current_3d_style == 'ball_and_stick' and bond_cylinders:
|
|
399
|
+
# 全シリンダーを結合してMultiBlockを作成
|
|
400
|
+
combined_bonds = pv.MultiBlock(bond_cylinders)
|
|
401
|
+
combined_mesh = combined_bonds.combine()
|
|
402
|
+
|
|
403
|
+
# 一括でグレーで描画
|
|
404
|
+
# Use the configured Ball & Stick bond color (hex) for the combined bonds
|
|
405
|
+
try:
|
|
406
|
+
bs_hex = self.settings.get('ball_stick_bond_color', '#7F7F7F')
|
|
407
|
+
q = QColor(bs_hex)
|
|
408
|
+
# Use normalized RGB for pyvista (r,g,b) floats in [0,1]
|
|
409
|
+
bond_color = (q.redF(), q.greenF(), q.blueF())
|
|
410
|
+
bond_actor = self.plotter.add_mesh(combined_mesh, color=bond_color, **mesh_props)
|
|
411
|
+
except Exception:
|
|
412
|
+
bond_actor = self.plotter.add_mesh(combined_mesh, color='grey', **mesh_props)
|
|
413
|
+
|
|
414
|
+
# まとめて色情報を記録
|
|
415
|
+
self._3d_color_map['bonds_combined'] = bs_bond_rgb
|
|
416
|
+
|
|
417
|
+
if getattr(self, 'show_chiral_labels', False):
|
|
418
|
+
try:
|
|
419
|
+
# 3D座標からキラル中心を計算
|
|
420
|
+
chiral_centers = Chem.FindMolChiralCenters(mol, includeUnassigned=True)
|
|
421
|
+
if chiral_centers:
|
|
422
|
+
pts, labels = [], []
|
|
423
|
+
z_off = 0
|
|
424
|
+
for idx, lbl in chiral_centers:
|
|
425
|
+
coord = self.atom_positions_3d[idx].copy(); coord[2] += z_off
|
|
426
|
+
pts.append(coord); labels.append(lbl if lbl is not None else '?')
|
|
427
|
+
try: self.plotter.remove_actor('chiral_labels')
|
|
428
|
+
except Exception: pass
|
|
429
|
+
self.plotter.add_point_labels(np.array(pts), labels, font_size=20, point_size=0, text_color='blue', name='chiral_labels', always_visible=True, tolerance=0.01, show_points=False)
|
|
430
|
+
except Exception as e: self.statusBar().showMessage(f"3D chiral label drawing error: {e}")
|
|
431
|
+
|
|
432
|
+
# E/Zラベルも表示
|
|
433
|
+
if getattr(self, 'show_chiral_labels', False):
|
|
434
|
+
try:
|
|
435
|
+
self.show_ez_labels_3d(mol)
|
|
436
|
+
except Exception as e:
|
|
437
|
+
self.statusBar().showMessage(f"3D E/Z label drawing error: {e}")
|
|
438
|
+
|
|
439
|
+
self.plotter.camera = camera_state
|
|
440
|
+
|
|
441
|
+
# Ensure the underlying VTK camera's parallel/projection flag matches
|
|
442
|
+
# the saved application setting. draw_molecule_3d restores a PyVista
|
|
443
|
+
# camera object which may not propagate the ParallelProjection flag
|
|
444
|
+
# to the VTK renderer camera; enforce it here to guarantee the
|
|
445
|
+
# projection mode selected in settings actually takes effect.
|
|
446
|
+
try:
|
|
447
|
+
proj_mode = self.settings.get('projection_mode', 'Perspective')
|
|
448
|
+
if hasattr(self.plotter, 'renderer') and hasattr(self.plotter.renderer, 'GetActiveCamera'):
|
|
449
|
+
vcam = self.plotter.renderer.GetActiveCamera()
|
|
450
|
+
if vcam:
|
|
451
|
+
if proj_mode == 'Orthographic':
|
|
452
|
+
vcam.SetParallelProjection(True)
|
|
453
|
+
else:
|
|
454
|
+
vcam.SetParallelProjection(False)
|
|
455
|
+
try:
|
|
456
|
+
# Force a render so the change is visible immediately
|
|
457
|
+
self.plotter.render()
|
|
458
|
+
except Exception:
|
|
459
|
+
pass
|
|
460
|
+
except Exception:
|
|
461
|
+
pass
|
|
462
|
+
|
|
463
|
+
# AtomIDまたは他の原子情報が表示されている場合は再表示
|
|
464
|
+
if hasattr(self, 'atom_info_display_mode') and self.atom_info_display_mode is not None:
|
|
465
|
+
self.show_all_atom_info()
|
|
466
|
+
|
|
467
|
+
# メニューテキストと状態を現在の分子の種類に応じて更新
|
|
468
|
+
self.update_atom_id_menu_text()
|
|
469
|
+
self.update_atom_id_menu_state()
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _calculate_double_bond_offset(self, mol, bond, conf):
|
|
474
|
+
"""
|
|
475
|
+
二重結合のオフセット方向を計算する。
|
|
476
|
+
結合している原子の他の結合を考慮して、平面的になるようにする。
|
|
477
|
+
"""
|
|
478
|
+
begin_atom = mol.GetAtomWithIdx(bond.GetBeginAtomIdx())
|
|
479
|
+
end_atom = mol.GetAtomWithIdx(bond.GetEndAtomIdx())
|
|
480
|
+
|
|
481
|
+
begin_pos = np.array(conf.GetAtomPosition(bond.GetBeginAtomIdx()))
|
|
482
|
+
end_pos = np.array(conf.GetAtomPosition(bond.GetEndAtomIdx()))
|
|
483
|
+
|
|
484
|
+
bond_vec = end_pos - begin_pos
|
|
485
|
+
bond_length = np.linalg.norm(bond_vec)
|
|
486
|
+
if bond_length == 0:
|
|
487
|
+
# フォールバック: Z軸基準
|
|
488
|
+
return np.array([0, 0, 1])
|
|
489
|
+
|
|
490
|
+
bond_unit = bond_vec / bond_length
|
|
491
|
+
|
|
492
|
+
# 両端の原子の隣接原子を調べる
|
|
493
|
+
begin_neighbors = []
|
|
494
|
+
end_neighbors = []
|
|
495
|
+
|
|
496
|
+
for neighbor in begin_atom.GetNeighbors():
|
|
497
|
+
if neighbor.GetIdx() != bond.GetEndAtomIdx():
|
|
498
|
+
neighbor_pos = np.array(conf.GetAtomPosition(neighbor.GetIdx()))
|
|
499
|
+
begin_neighbors.append(neighbor_pos)
|
|
500
|
+
|
|
501
|
+
for neighbor in end_atom.GetNeighbors():
|
|
502
|
+
if neighbor.GetIdx() != bond.GetBeginAtomIdx():
|
|
503
|
+
neighbor_pos = np.array(conf.GetAtomPosition(neighbor.GetIdx()))
|
|
504
|
+
end_neighbors.append(neighbor_pos)
|
|
505
|
+
|
|
506
|
+
# 平面の法線ベクトルを計算
|
|
507
|
+
normal_candidates = []
|
|
508
|
+
|
|
509
|
+
# 開始原子の隣接原子から平面を推定
|
|
510
|
+
if len(begin_neighbors) >= 1:
|
|
511
|
+
for neighbor_pos in begin_neighbors:
|
|
512
|
+
vec_to_neighbor = neighbor_pos - begin_pos
|
|
513
|
+
if np.linalg.norm(vec_to_neighbor) > 1e-6:
|
|
514
|
+
# bond_vec と neighbor_vec の外積が平面の法線
|
|
515
|
+
normal = np.cross(bond_vec, vec_to_neighbor)
|
|
516
|
+
norm_length = np.linalg.norm(normal)
|
|
517
|
+
if norm_length > 1e-6:
|
|
518
|
+
normal_candidates.append(normal / norm_length)
|
|
519
|
+
|
|
520
|
+
# 終了原子の隣接原子から平面を推定
|
|
521
|
+
if len(end_neighbors) >= 1:
|
|
522
|
+
for neighbor_pos in end_neighbors:
|
|
523
|
+
vec_to_neighbor = neighbor_pos - end_pos
|
|
524
|
+
if np.linalg.norm(vec_to_neighbor) > 1e-6:
|
|
525
|
+
# bond_vec と neighbor_vec の外積が平面の法線
|
|
526
|
+
normal = np.cross(bond_vec, vec_to_neighbor)
|
|
527
|
+
norm_length = np.linalg.norm(normal)
|
|
528
|
+
if norm_length > 1e-6:
|
|
529
|
+
normal_candidates.append(normal / norm_length)
|
|
530
|
+
|
|
531
|
+
# 複数の法線ベクトルがある場合は平均を取る
|
|
532
|
+
if normal_candidates:
|
|
533
|
+
# 方向を統一するため、最初のベクトルとの内積が正になるように調整
|
|
534
|
+
reference_normal = normal_candidates[0]
|
|
535
|
+
aligned_normals = []
|
|
536
|
+
|
|
537
|
+
for normal in normal_candidates:
|
|
538
|
+
if np.dot(normal, reference_normal) < 0:
|
|
539
|
+
normal = -normal
|
|
540
|
+
aligned_normals.append(normal)
|
|
541
|
+
|
|
542
|
+
avg_normal = np.mean(aligned_normals, axis=0)
|
|
543
|
+
norm_length = np.linalg.norm(avg_normal)
|
|
544
|
+
if norm_length > 1e-6:
|
|
545
|
+
avg_normal /= norm_length
|
|
546
|
+
|
|
547
|
+
# 法線ベクトルと結合ベクトルに垂直な方向を二重結合のオフセット方向とする
|
|
548
|
+
offset_dir = np.cross(bond_unit, avg_normal)
|
|
549
|
+
offset_length = np.linalg.norm(offset_dir)
|
|
550
|
+
if offset_length > 1e-6:
|
|
551
|
+
return offset_dir / offset_length
|
|
552
|
+
|
|
553
|
+
# フォールバック: 結合ベクトルに垂直な任意の方向
|
|
554
|
+
v_arb = np.array([0, 0, 1])
|
|
555
|
+
if np.allclose(np.abs(np.dot(bond_unit, v_arb)), 1.0):
|
|
556
|
+
v_arb = np.array([0, 1, 0])
|
|
557
|
+
|
|
558
|
+
off_dir = np.cross(bond_unit, v_arb)
|
|
559
|
+
off_dir /= np.linalg.norm(off_dir)
|
|
560
|
+
return off_dir
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def show_ez_labels_3d(self, mol):
|
|
565
|
+
"""3DビューでE/Zラベルを表示する(RDKitのステレオ化学判定を使用)"""
|
|
566
|
+
if not mol:
|
|
567
|
+
return
|
|
568
|
+
|
|
569
|
+
try:
|
|
570
|
+
# 既存のE/Zラベルを削除
|
|
571
|
+
self.plotter.remove_actor('ez_labels')
|
|
572
|
+
except:
|
|
573
|
+
pass
|
|
574
|
+
|
|
575
|
+
pts, labels = [], []
|
|
576
|
+
|
|
577
|
+
# 3D座標が存在するかチェック
|
|
578
|
+
if mol.GetNumConformers() == 0:
|
|
579
|
+
return
|
|
580
|
+
|
|
581
|
+
conf = mol.GetConformer()
|
|
582
|
+
|
|
583
|
+
# RDKitに3D座標からステレオ化学を計算させる
|
|
584
|
+
try:
|
|
585
|
+
# 3D座標からステレオ化学を再計算
|
|
586
|
+
Chem.AssignStereochemistry(mol, cleanIt=True, force=True, flagPossibleStereoCenters=True)
|
|
587
|
+
except:
|
|
588
|
+
pass
|
|
589
|
+
|
|
590
|
+
# 二重結合でRDKitが判定したE/Z立体化学を表示
|
|
591
|
+
for bond in mol.GetBonds():
|
|
592
|
+
if bond.GetBondType() == Chem.BondType.DOUBLE:
|
|
593
|
+
stereo = bond.GetStereo()
|
|
594
|
+
if stereo in [Chem.BondStereo.STEREOE, Chem.BondStereo.STEREOZ]:
|
|
595
|
+
# 結合の中心座標を計算
|
|
596
|
+
begin_pos = np.array(conf.GetAtomPosition(bond.GetBeginAtomIdx()))
|
|
597
|
+
end_pos = np.array(conf.GetAtomPosition(bond.GetEndAtomIdx()))
|
|
598
|
+
center_pos = (begin_pos + end_pos) / 2
|
|
599
|
+
|
|
600
|
+
# RDKitの判定結果を使用
|
|
601
|
+
label = 'E' if stereo == Chem.BondStereo.STEREOE else 'Z'
|
|
602
|
+
pts.append(center_pos)
|
|
603
|
+
labels.append(label)
|
|
604
|
+
|
|
605
|
+
if pts and labels:
|
|
606
|
+
self.plotter.add_point_labels(
|
|
607
|
+
np.array(pts),
|
|
608
|
+
labels,
|
|
609
|
+
font_size=18,
|
|
610
|
+
point_size=0,
|
|
611
|
+
text_color='darkgreen', # 暗い緑色
|
|
612
|
+
name='ez_labels',
|
|
613
|
+
always_visible=True,
|
|
614
|
+
tolerance=0.01,
|
|
615
|
+
show_points=False
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def toggle_chiral_labels_display(self, checked):
|
|
622
|
+
"""Viewメニューのアクションに応じてキラルラベル表示を切り替える"""
|
|
623
|
+
self.show_chiral_labels = checked
|
|
624
|
+
|
|
625
|
+
if self.current_mol:
|
|
626
|
+
self.draw_molecule_3d(self.current_mol)
|
|
627
|
+
|
|
628
|
+
if checked:
|
|
629
|
+
self.statusBar().showMessage("Chiral labels: will be (re)computed after Convert→3D.")
|
|
630
|
+
else:
|
|
631
|
+
self.statusBar().showMessage("Chiral labels disabled.")
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def update_chiral_labels(self):
|
|
637
|
+
"""分子のキラル中心を計算し、2Dビューの原子アイテムにR/Sラベルを設定/解除する
|
|
638
|
+
※ 可能なら 3D(self.current_mol)を優先して計算し、なければ 2D から作った RDKit 分子を使う。
|
|
639
|
+
"""
|
|
640
|
+
# まず全てのアイテムからラベルをクリア
|
|
641
|
+
for atom_data in self.data.atoms.values():
|
|
642
|
+
if atom_data.get('item'):
|
|
643
|
+
atom_data['item'].chiral_label = None
|
|
644
|
+
|
|
645
|
+
if not self.show_chiral_labels:
|
|
646
|
+
self.scene.update()
|
|
647
|
+
return
|
|
648
|
+
|
|
649
|
+
# 3D の RDKit Mol(コンフォマーを持つもの)を使う
|
|
650
|
+
mol_for_chirality = None
|
|
651
|
+
if getattr(self, 'current_mol', None) is not None:
|
|
652
|
+
mol_for_chirality = self.current_mol
|
|
653
|
+
else:
|
|
654
|
+
return
|
|
655
|
+
|
|
656
|
+
if mol_for_chirality is None or mol_for_chirality.GetNumAtoms() == 0:
|
|
657
|
+
self.scene.update()
|
|
658
|
+
return
|
|
659
|
+
|
|
660
|
+
try:
|
|
661
|
+
# --- 重要:3D コンフォマーがあるなら、それを使って原子のキラルタグを割り当てる ---
|
|
662
|
+
if mol_for_chirality.GetNumConformers() > 0:
|
|
663
|
+
# confId=0(最初のコンフォマー)を指定して、原子のキラリティータグを3D座標由来で設定
|
|
664
|
+
try:
|
|
665
|
+
Chem.AssignAtomChiralTagsFromStructure(mol_for_chirality, confId=0)
|
|
666
|
+
except Exception:
|
|
667
|
+
# 古い RDKit では関数が無い場合があるので(念のため保護)
|
|
668
|
+
pass
|
|
669
|
+
|
|
670
|
+
# RDKit の通常の stereochemistry 割当(念のため)
|
|
671
|
+
#Chem.AssignStereochemistry(mol_for_chirality, cleanIt=True, force=True, flagPossibleStereoCenters=True)
|
|
672
|
+
|
|
673
|
+
# キラル中心の取得((idx, 'R'/'S'/'?') のリスト)
|
|
674
|
+
chiral_centers = Chem.FindMolChiralCenters(mol_for_chirality, includeUnassigned=True)
|
|
675
|
+
|
|
676
|
+
# RDKit atom index -> エディタ側 atom_id へのマッピング
|
|
677
|
+
rdkit_idx_to_my_id = {}
|
|
678
|
+
for atom in mol_for_chirality.GetAtoms():
|
|
679
|
+
if atom.HasProp("_original_atom_id"):
|
|
680
|
+
rdkit_idx_to_my_id[atom.GetIdx()] = atom.GetIntProp("_original_atom_id")
|
|
681
|
+
|
|
682
|
+
# 見つかったキラル中心を対応する AtomItem に設定
|
|
683
|
+
for idx, label in chiral_centers:
|
|
684
|
+
if idx in rdkit_idx_to_my_id:
|
|
685
|
+
atom_id = rdkit_idx_to_my_id[idx]
|
|
686
|
+
if atom_id in self.data.atoms and self.data.atoms[atom_id].get('item'):
|
|
687
|
+
# 'R' / 'S' / '?'
|
|
688
|
+
self.data.atoms[atom_id]['item'].chiral_label = label
|
|
689
|
+
|
|
690
|
+
except Exception as e:
|
|
691
|
+
self.statusBar().showMessage(f"Update chiral labels error: {e}")
|
|
692
|
+
|
|
693
|
+
# 最後に 2D シーンを再描画
|
|
694
|
+
self.scene.update()
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def toggle_atom_info_display(self, mode):
|
|
699
|
+
"""原子情報表示モードを切り替える"""
|
|
700
|
+
# 現在の表示をクリア
|
|
701
|
+
self.clear_all_atom_info_labels()
|
|
702
|
+
|
|
703
|
+
# 同じモードが選択された場合はOFFにする
|
|
704
|
+
if self.atom_info_display_mode == mode:
|
|
705
|
+
self.atom_info_display_mode = None
|
|
706
|
+
# 全てのアクションのチェックを外す
|
|
707
|
+
self.show_atom_id_action.setChecked(False)
|
|
708
|
+
self.show_rdkit_id_action.setChecked(False)
|
|
709
|
+
self.show_atom_coords_action.setChecked(False)
|
|
710
|
+
self.show_atom_symbol_action.setChecked(False)
|
|
711
|
+
self.statusBar().showMessage("Atom info display disabled.")
|
|
712
|
+
else:
|
|
713
|
+
# 新しいモードを設定
|
|
714
|
+
self.atom_info_display_mode = mode
|
|
715
|
+
# 該当するアクションのみチェック
|
|
716
|
+
self.show_atom_id_action.setChecked(mode == 'id')
|
|
717
|
+
self.show_rdkit_id_action.setChecked(mode == 'rdkit_id')
|
|
718
|
+
self.show_atom_coords_action.setChecked(mode == 'coords')
|
|
719
|
+
self.show_atom_symbol_action.setChecked(mode == 'symbol')
|
|
720
|
+
|
|
721
|
+
mode_names = {'id': 'Atom ID', 'rdkit_id': 'RDKit Index', 'coords': 'Coordinates', 'symbol': 'Element Symbol'}
|
|
722
|
+
self.statusBar().showMessage(f"Displaying: {mode_names[mode]}")
|
|
723
|
+
|
|
724
|
+
# すべての原子に情報を表示
|
|
725
|
+
self.show_all_atom_info()
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def is_xyz_derived_molecule(self):
|
|
730
|
+
"""現在の分子がXYZファイル由来かどうかを判定"""
|
|
731
|
+
if not self.current_mol:
|
|
732
|
+
return False
|
|
733
|
+
try:
|
|
734
|
+
# 最初の原子がxyz_unique_idプロパティを持っているかチェック
|
|
735
|
+
if self.current_mol.GetNumAtoms() > 0:
|
|
736
|
+
return self.current_mol.GetAtomWithIdx(0).HasProp("xyz_unique_id")
|
|
737
|
+
except Exception:
|
|
738
|
+
pass
|
|
739
|
+
return False
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def has_original_atom_ids(self):
|
|
744
|
+
"""現在の分子がOriginal Atom IDsを持っているかどうかを判定"""
|
|
745
|
+
if not self.current_mol:
|
|
746
|
+
return False
|
|
747
|
+
try:
|
|
748
|
+
# いずれかの原子が_original_atom_idプロパティを持っているかチェック
|
|
749
|
+
for atom_idx in range(self.current_mol.GetNumAtoms()):
|
|
750
|
+
atom = self.current_mol.GetAtomWithIdx(atom_idx)
|
|
751
|
+
if atom.HasProp("_original_atom_id"):
|
|
752
|
+
return True
|
|
753
|
+
except Exception:
|
|
754
|
+
pass
|
|
755
|
+
return False
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def update_atom_id_menu_text(self):
|
|
760
|
+
"""原子IDメニューのテキストを現在の分子の種類に応じて更新"""
|
|
761
|
+
if hasattr(self, 'show_atom_id_action'):
|
|
762
|
+
if self.is_xyz_derived_molecule():
|
|
763
|
+
self.show_atom_id_action.setText("Show XYZ Unique ID")
|
|
764
|
+
else:
|
|
765
|
+
self.show_atom_id_action.setText("Show Original ID / Index")
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def update_atom_id_menu_state(self):
|
|
770
|
+
"""原子IDメニューの有効/無効状態を更新"""
|
|
771
|
+
if hasattr(self, 'show_atom_id_action'):
|
|
772
|
+
has_original_ids = self.has_original_atom_ids()
|
|
773
|
+
has_xyz_ids = self.is_xyz_derived_molecule()
|
|
774
|
+
|
|
775
|
+
# Original IDまたはXYZ IDがある場合のみ有効化
|
|
776
|
+
self.show_atom_id_action.setEnabled(has_original_ids or has_xyz_ids)
|
|
777
|
+
|
|
778
|
+
# 現在選択されているモードが無効化される場合は解除
|
|
779
|
+
if not (has_original_ids or has_xyz_ids) and self.atom_info_display_mode == 'id':
|
|
780
|
+
self.atom_info_display_mode = None
|
|
781
|
+
self.show_atom_id_action.setChecked(False)
|
|
782
|
+
self.clear_all_atom_info_labels()
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def show_all_atom_info(self):
|
|
788
|
+
"""すべての原子に情報を表示"""
|
|
789
|
+
if self.atom_info_display_mode is None or not hasattr(self, 'atom_positions_3d') or self.atom_positions_3d is None:
|
|
790
|
+
return
|
|
791
|
+
|
|
792
|
+
# 既存のラベルをクリア
|
|
793
|
+
self.clear_all_atom_info_labels()
|
|
794
|
+
|
|
795
|
+
# ラベルを表示するためにタイプ別に分けてリストを作る
|
|
796
|
+
rdkit_positions = []
|
|
797
|
+
rdkit_texts = []
|
|
798
|
+
id_positions = []
|
|
799
|
+
id_texts = []
|
|
800
|
+
xyz_positions = []
|
|
801
|
+
xyz_texts = []
|
|
802
|
+
other_positions = []
|
|
803
|
+
other_texts = []
|
|
804
|
+
|
|
805
|
+
for atom_idx, pos in enumerate(self.atom_positions_3d):
|
|
806
|
+
# default: skip if no display mode
|
|
807
|
+
if self.atom_info_display_mode is None:
|
|
808
|
+
continue
|
|
809
|
+
|
|
810
|
+
if self.atom_info_display_mode == 'id':
|
|
811
|
+
# Original IDがある場合は優先表示、なければXYZのユニークID、最後にRDKitインデックス
|
|
812
|
+
try:
|
|
813
|
+
if self.current_mol:
|
|
814
|
+
atom = self.current_mol.GetAtomWithIdx(atom_idx)
|
|
815
|
+
if atom.HasProp("_original_atom_id"):
|
|
816
|
+
original_id = atom.GetIntProp("_original_atom_id")
|
|
817
|
+
# プレフィックスを削除して数値だけ表示
|
|
818
|
+
id_positions.append(pos)
|
|
819
|
+
id_texts.append(str(original_id))
|
|
820
|
+
elif atom.HasProp("xyz_unique_id"):
|
|
821
|
+
unique_id = atom.GetIntProp("xyz_unique_id")
|
|
822
|
+
xyz_positions.append(pos)
|
|
823
|
+
xyz_texts.append(str(unique_id))
|
|
824
|
+
else:
|
|
825
|
+
rdkit_positions.append(pos)
|
|
826
|
+
rdkit_texts.append(str(atom_idx))
|
|
827
|
+
else:
|
|
828
|
+
rdkit_positions.append(pos)
|
|
829
|
+
rdkit_texts.append(str(atom_idx))
|
|
830
|
+
except Exception:
|
|
831
|
+
rdkit_positions.append(pos)
|
|
832
|
+
rdkit_texts.append(str(atom_idx))
|
|
833
|
+
|
|
834
|
+
elif self.atom_info_display_mode == 'rdkit_id':
|
|
835
|
+
rdkit_positions.append(pos)
|
|
836
|
+
rdkit_texts.append(str(atom_idx))
|
|
837
|
+
|
|
838
|
+
elif self.atom_info_display_mode == 'coords':
|
|
839
|
+
other_positions.append(pos)
|
|
840
|
+
other_texts.append(f"({pos[0]:.2f},{pos[1]:.2f},{pos[2]:.2f})")
|
|
841
|
+
|
|
842
|
+
elif self.atom_info_display_mode == 'symbol':
|
|
843
|
+
if self.current_mol:
|
|
844
|
+
symbol = self.current_mol.GetAtomWithIdx(atom_idx).GetSymbol()
|
|
845
|
+
other_positions.append(pos)
|
|
846
|
+
other_texts.append(symbol)
|
|
847
|
+
else:
|
|
848
|
+
other_positions.append(pos)
|
|
849
|
+
other_texts.append("?")
|
|
850
|
+
|
|
851
|
+
else:
|
|
852
|
+
continue
|
|
853
|
+
|
|
854
|
+
# 色の定義(暗めの青/緑/赤)
|
|
855
|
+
rdkit_color = '#003366' # 暗めの青
|
|
856
|
+
id_color = '#006400' # 暗めの緑
|
|
857
|
+
xyz_color = '#8B0000' # 暗めの赤
|
|
858
|
+
other_color = 'black'
|
|
859
|
+
|
|
860
|
+
# それぞれのグループごとにラベルを追加し、参照をリストで保持する
|
|
861
|
+
self.current_atom_info_labels = []
|
|
862
|
+
try:
|
|
863
|
+
if rdkit_positions:
|
|
864
|
+
a = self.plotter.add_point_labels(
|
|
865
|
+
np.array(rdkit_positions), rdkit_texts,
|
|
866
|
+
point_size=12, font_size=18, text_color=rdkit_color,
|
|
867
|
+
always_visible=True, tolerance=0.01, show_points=False,
|
|
868
|
+
name='atom_labels_rdkit'
|
|
869
|
+
)
|
|
870
|
+
self.current_atom_info_labels.append(a)
|
|
871
|
+
|
|
872
|
+
if id_positions:
|
|
873
|
+
a = self.plotter.add_point_labels(
|
|
874
|
+
np.array(id_positions), id_texts,
|
|
875
|
+
point_size=12, font_size=18, text_color=id_color,
|
|
876
|
+
always_visible=True, tolerance=0.01, show_points=False,
|
|
877
|
+
name='atom_labels_id'
|
|
878
|
+
)
|
|
879
|
+
self.current_atom_info_labels.append(a)
|
|
880
|
+
|
|
881
|
+
if xyz_positions:
|
|
882
|
+
a = self.plotter.add_point_labels(
|
|
883
|
+
np.array(xyz_positions), xyz_texts,
|
|
884
|
+
point_size=12, font_size=18, text_color=xyz_color,
|
|
885
|
+
always_visible=True, tolerance=0.01, show_points=False,
|
|
886
|
+
name='atom_labels_xyz'
|
|
887
|
+
)
|
|
888
|
+
self.current_atom_info_labels.append(a)
|
|
889
|
+
|
|
890
|
+
if other_positions:
|
|
891
|
+
a = self.plotter.add_point_labels(
|
|
892
|
+
np.array(other_positions), other_texts,
|
|
893
|
+
point_size=12, font_size=18, text_color=other_color,
|
|
894
|
+
always_visible=True, tolerance=0.01, show_points=False,
|
|
895
|
+
name='atom_labels_other'
|
|
896
|
+
)
|
|
897
|
+
self.current_atom_info_labels.append(a)
|
|
898
|
+
except Exception as e:
|
|
899
|
+
print(f"Error adding atom info labels: {e}")
|
|
900
|
+
|
|
901
|
+
# 右上に凡例を表示(既存の凡例は消す)
|
|
902
|
+
try:
|
|
903
|
+
# 古い凡例削除
|
|
904
|
+
if hasattr(self, 'atom_label_legend_names') and self.atom_label_legend_names:
|
|
905
|
+
for nm in self.atom_label_legend_names:
|
|
906
|
+
try:
|
|
907
|
+
self.plotter.remove_actor(nm)
|
|
908
|
+
except:
|
|
909
|
+
pass
|
|
910
|
+
self.atom_label_legend_names = []
|
|
911
|
+
|
|
912
|
+
# 凡例テキストを右上に縦並びで追加(背景なし、太字のみ)
|
|
913
|
+
legend_entries = []
|
|
914
|
+
if rdkit_positions:
|
|
915
|
+
legend_entries.append(('RDKit', rdkit_color, 'legend_rdkit'))
|
|
916
|
+
if id_positions:
|
|
917
|
+
legend_entries.append(('ID', id_color, 'legend_id'))
|
|
918
|
+
if xyz_positions:
|
|
919
|
+
legend_entries.append(('XYZ', xyz_color, 'legend_xyz'))
|
|
920
|
+
# Do not show 'Other' in the legend per UI requirement
|
|
921
|
+
# (other_positions are still labeled in-scene but not listed in the legend)
|
|
922
|
+
|
|
923
|
+
# 左下に凡例ラベルを追加(背景なし、太字のみ)
|
|
924
|
+
# Increase spacing to avoid overlapping when short labels like 'RDKit' and 'ID' appear
|
|
925
|
+
spacing = 30
|
|
926
|
+
for i, (label_text, label_color, label_name) in enumerate(legend_entries):
|
|
927
|
+
# 左下基準でy座標を上げる
|
|
928
|
+
# Add a small horizontal offset for very short adjacent labels so they don't visually collide
|
|
929
|
+
y = 0.0 + i * spacing
|
|
930
|
+
x_offset = 0.0
|
|
931
|
+
# If both RDKit and ID are present, nudge the second entry slightly to the right to avoid overlap
|
|
932
|
+
try:
|
|
933
|
+
if label_text == 'ID' and any(e[0] == 'RDKit' for e in legend_entries):
|
|
934
|
+
x_offset = 0.06
|
|
935
|
+
except Exception:
|
|
936
|
+
x_offset = 0.0
|
|
937
|
+
try:
|
|
938
|
+
actor = self.plotter.add_text(
|
|
939
|
+
label_text,
|
|
940
|
+
position=(0.0 + x_offset, y),
|
|
941
|
+
font_size=12,
|
|
942
|
+
color=label_color,
|
|
943
|
+
name=label_name,
|
|
944
|
+
font='arial'
|
|
945
|
+
)
|
|
946
|
+
self.atom_label_legend_names.append(label_name)
|
|
947
|
+
# 太字のみ設定(背景は設定しない)
|
|
948
|
+
try:
|
|
949
|
+
if hasattr(actor, 'GetTextProperty'):
|
|
950
|
+
tp = actor.GetTextProperty()
|
|
951
|
+
try:
|
|
952
|
+
tp.SetBold(True)
|
|
953
|
+
except Exception:
|
|
954
|
+
pass
|
|
955
|
+
except Exception:
|
|
956
|
+
pass
|
|
957
|
+
except Exception:
|
|
958
|
+
continue
|
|
959
|
+
|
|
960
|
+
except Exception:
|
|
961
|
+
pass
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
def clear_all_atom_info_labels(self):
|
|
966
|
+
"""すべての原子情報ラベルをクリア"""
|
|
967
|
+
# Remove label actors (may be a single actor, a list, or None)
|
|
968
|
+
try:
|
|
969
|
+
if hasattr(self, 'current_atom_info_labels') and self.current_atom_info_labels:
|
|
970
|
+
if isinstance(self.current_atom_info_labels, (list, tuple)):
|
|
971
|
+
for a in list(self.current_atom_info_labels):
|
|
972
|
+
try:
|
|
973
|
+
self.plotter.remove_actor(a)
|
|
974
|
+
except:
|
|
975
|
+
pass
|
|
976
|
+
else:
|
|
977
|
+
try:
|
|
978
|
+
self.plotter.remove_actor(self.current_atom_info_labels)
|
|
979
|
+
except:
|
|
980
|
+
pass
|
|
981
|
+
except Exception:
|
|
982
|
+
pass
|
|
983
|
+
finally:
|
|
984
|
+
self.current_atom_info_labels = None
|
|
985
|
+
|
|
986
|
+
# Remove legend text actors if present
|
|
987
|
+
try:
|
|
988
|
+
if hasattr(self, 'atom_label_legend_names') and self.atom_label_legend_names:
|
|
989
|
+
for nm in list(self.atom_label_legend_names):
|
|
990
|
+
try:
|
|
991
|
+
self.plotter.remove_actor(nm)
|
|
992
|
+
except:
|
|
993
|
+
pass
|
|
994
|
+
except Exception:
|
|
995
|
+
pass
|
|
996
|
+
finally:
|
|
997
|
+
self.atom_label_legend_names = []
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def setup_3d_hover(self):
|
|
1002
|
+
"""3Dビューでの表示を設定(常時表示に変更)"""
|
|
1003
|
+
if self.atom_info_display_mode is not None:
|
|
1004
|
+
self.show_all_atom_info()
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
def zoom_in(self):
|
|
1009
|
+
""" ビューを 20% 拡大する """
|
|
1010
|
+
self.view_2d.scale(1.2, 1.2)
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
def zoom_out(self):
|
|
1015
|
+
""" ビューを 20% 縮小する """
|
|
1016
|
+
self.view_2d.scale(1/1.2, 1/1.2)
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
def reset_zoom(self):
|
|
1021
|
+
""" ビューの拡大率をデフォルト (75%) にリセットする """
|
|
1022
|
+
transform = QTransform()
|
|
1023
|
+
transform.scale(0.75, 0.75)
|
|
1024
|
+
self.view_2d.setTransform(transform)
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
def fit_to_view(self):
|
|
1029
|
+
""" シーン上のすべてのアイテムがビューに収まるように調整する """
|
|
1030
|
+
if not self.scene.items():
|
|
1031
|
+
self.reset_zoom()
|
|
1032
|
+
return
|
|
1033
|
+
|
|
1034
|
+
# 合計の表示矩形(目に見えるアイテムのみ)を計算
|
|
1035
|
+
visible_items_rect = QRectF()
|
|
1036
|
+
for item in self.scene.items():
|
|
1037
|
+
if item.isVisible() and not isinstance(item, TemplatePreviewItem):
|
|
1038
|
+
if visible_items_rect.isEmpty():
|
|
1039
|
+
visible_items_rect = item.sceneBoundingRect()
|
|
1040
|
+
else:
|
|
1041
|
+
visible_items_rect = visible_items_rect.united(item.sceneBoundingRect())
|
|
1042
|
+
|
|
1043
|
+
if visible_items_rect.isEmpty():
|
|
1044
|
+
self.reset_zoom()
|
|
1045
|
+
return
|
|
1046
|
+
|
|
1047
|
+
# 少し余白を持たせる(パディング)
|
|
1048
|
+
padding_factor = 1.10 # 10% の余裕
|
|
1049
|
+
cx = visible_items_rect.center().x()
|
|
1050
|
+
cy = visible_items_rect.center().y()
|
|
1051
|
+
w = visible_items_rect.width() * padding_factor
|
|
1052
|
+
h = visible_items_rect.height() * padding_factor
|
|
1053
|
+
padded = QRectF(cx - w / 2.0, cy - h / 2.0, w, h)
|
|
1054
|
+
|
|
1055
|
+
# フィット時にマウス位置に依存するアンカーが原因でジャンプすることがあるため
|
|
1056
|
+
# 一時的にトランスフォームアンカーをビュー中心にしてから fitInView を呼ぶ
|
|
1057
|
+
try:
|
|
1058
|
+
old_ta = self.view_2d.transformationAnchor()
|
|
1059
|
+
old_ra = self.view_2d.resizeAnchor()
|
|
1060
|
+
except Exception:
|
|
1061
|
+
old_ta = old_ra = None
|
|
1062
|
+
|
|
1063
|
+
try:
|
|
1064
|
+
self.view_2d.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter)
|
|
1065
|
+
self.view_2d.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter)
|
|
1066
|
+
self.view_2d.fitInView(padded, Qt.AspectRatioMode.KeepAspectRatio)
|
|
1067
|
+
finally:
|
|
1068
|
+
# 元のアンカーを復元
|
|
1069
|
+
try:
|
|
1070
|
+
if old_ta is not None:
|
|
1071
|
+
self.view_2d.setTransformationAnchor(old_ta)
|
|
1072
|
+
if old_ra is not None:
|
|
1073
|
+
self.view_2d.setResizeAnchor(old_ra)
|
|
1074
|
+
except Exception:
|
|
1075
|
+
pass
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
def update_cpk_colors_from_settings(self):
|
|
1080
|
+
"""Update global CPK_COLORS and CPK_COLORS_PV from saved settings overrides.
|
|
1081
|
+
|
|
1082
|
+
This modifies the in-memory CPK_COLORS mapping (not persisted until settings are saved).
|
|
1083
|
+
Only keys present in self.settings['cpk_colors'] are changed; other elements keep the defaults.
|
|
1084
|
+
"""
|
|
1085
|
+
try:
|
|
1086
|
+
overrides = self.settings.get('cpk_colors', {}) or {}
|
|
1087
|
+
# Start from a clean copy of the defaults
|
|
1088
|
+
global CPK_COLORS, CPK_COLORS_PV
|
|
1089
|
+
CPK_COLORS = {k: QColor(v) if not isinstance(v, QColor) else QColor(v) for k, v in DEFAULT_CPK_COLORS.items()}
|
|
1090
|
+
for k, hexv in overrides.items():
|
|
1091
|
+
if isinstance(hexv, str) and hexv:
|
|
1092
|
+
CPK_COLORS[k] = QColor(hexv)
|
|
1093
|
+
# Rebuild PV version
|
|
1094
|
+
CPK_COLORS_PV = {k: [c.redF(), c.greenF(), c.blueF()] for k, c in CPK_COLORS.items()}
|
|
1095
|
+
except Exception as e:
|
|
1096
|
+
print(f"Failed to update CPK colors from settings: {e}")
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
def apply_3d_settings(self, redraw=True):
|
|
1102
|
+
# Projection mode
|
|
1103
|
+
proj_mode = self.settings.get('projection_mode', 'Perspective')
|
|
1104
|
+
if hasattr(self.plotter, 'renderer') and hasattr(self.plotter.renderer, 'GetActiveCamera'):
|
|
1105
|
+
cam = self.plotter.renderer.GetActiveCamera()
|
|
1106
|
+
if cam:
|
|
1107
|
+
if proj_mode == 'Orthographic':
|
|
1108
|
+
cam.SetParallelProjection(True)
|
|
1109
|
+
else:
|
|
1110
|
+
cam.SetParallelProjection(False)
|
|
1111
|
+
"""3Dビューの視覚設定を適用する"""
|
|
1112
|
+
if not hasattr(self, 'plotter'):
|
|
1113
|
+
return
|
|
1114
|
+
|
|
1115
|
+
# レンダラーのレイヤー設定を有効化(テキストオーバーレイ用)
|
|
1116
|
+
renderer = self.plotter.renderer
|
|
1117
|
+
if renderer and hasattr(renderer, 'SetNumberOfLayers'):
|
|
1118
|
+
try:
|
|
1119
|
+
renderer.SetNumberOfLayers(2) # レイヤー0:3Dオブジェクト、レイヤー1:2Dオーバーレイ
|
|
1120
|
+
except:
|
|
1121
|
+
pass # PyVistaのバージョンによってはサポートされていない場合がある
|
|
1122
|
+
|
|
1123
|
+
# --- 3D軸ウィジェットの設定 ---
|
|
1124
|
+
show_axes = self.settings.get('show_3d_axes', True)
|
|
1125
|
+
|
|
1126
|
+
# ウィジェットがまだ作成されていない場合は作成する
|
|
1127
|
+
if self.axes_widget is None and hasattr(self.plotter, 'interactor'):
|
|
1128
|
+
axes = vtk.vtkAxesActor()
|
|
1129
|
+
self.axes_widget = vtk.vtkOrientationMarkerWidget()
|
|
1130
|
+
self.axes_widget.SetOrientationMarker(axes)
|
|
1131
|
+
self.axes_widget.SetInteractor(self.plotter.interactor)
|
|
1132
|
+
# 左下隅に設定 (幅・高さ20%)
|
|
1133
|
+
self.axes_widget.SetViewport(0.0, 0.0, 0.2, 0.2)
|
|
1134
|
+
|
|
1135
|
+
# 設定に応じてウィジェットを有効化/無効化
|
|
1136
|
+
if self.axes_widget:
|
|
1137
|
+
if show_axes:
|
|
1138
|
+
self.axes_widget.On()
|
|
1139
|
+
self.axes_widget.SetInteractive(False)
|
|
1140
|
+
else:
|
|
1141
|
+
self.axes_widget.Off()
|
|
1142
|
+
|
|
1143
|
+
if redraw:
|
|
1144
|
+
self.draw_molecule_3d(self.current_mol)
|
|
1145
|
+
|
|
1146
|
+
# 設定変更時にカメラ位置をリセットしない(初回のみリセット)
|
|
1147
|
+
if not getattr(self, '_camera_initialized', False):
|
|
1148
|
+
try:
|
|
1149
|
+
self.plotter.reset_camera()
|
|
1150
|
+
except Exception:
|
|
1151
|
+
pass
|
|
1152
|
+
self._camera_initialized = True
|
|
1153
|
+
|
|
1154
|
+
# 強制的にプロッターを更新
|
|
1155
|
+
try:
|
|
1156
|
+
self.plotter.render()
|
|
1157
|
+
if hasattr(self.plotter, 'update'):
|
|
1158
|
+
self.plotter.update()
|
|
1159
|
+
except Exception:
|
|
1160
|
+
pass
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
|