MoleditPy 1.17.1__py3-none-any.whl → 1.18.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- moleditpy/modules/align_plane_dialog.py +1 -1
- moleditpy/modules/alignment_dialog.py +1 -1
- moleditpy/modules/angle_dialog.py +2 -2
- moleditpy/modules/bond_item.py +96 -5
- moleditpy/modules/bond_length_dialog.py +2 -2
- moleditpy/modules/constants.py +1 -1
- moleditpy/modules/constrained_optimization_dialog.py +2 -2
- moleditpy/modules/dihedral_dialog.py +1 -1
- moleditpy/modules/main_window_app_state.py +1 -1
- moleditpy/modules/main_window_compute.py +6 -0
- moleditpy/modules/main_window_edit_3d.py +7 -7
- moleditpy/modules/main_window_export.py +106 -49
- moleditpy/modules/main_window_main_init.py +3 -3
- moleditpy/modules/main_window_molecular_parsers.py +4 -3
- moleditpy/modules/main_window_project_io.py +2 -2
- moleditpy/modules/main_window_view_3d.py +359 -131
- moleditpy/modules/main_window_view_loaders.py +1 -1
- moleditpy/modules/molecule_scene.py +14 -15
- moleditpy/modules/planarize_dialog.py +1 -1
- moleditpy/modules/settings_dialog.py +86 -20
- moleditpy/modules/user_template_dialog.py +9 -8
- {moleditpy-1.17.1.dist-info → moleditpy-1.18.1.dist-info}/METADATA +1 -2
- {moleditpy-1.17.1.dist-info → moleditpy-1.18.1.dist-info}/RECORD +27 -27
- {moleditpy-1.17.1.dist-info → moleditpy-1.18.1.dist-info}/WHEEL +0 -0
- {moleditpy-1.17.1.dist-info → moleditpy-1.18.1.dist-info}/entry_points.txt +0 -0
- {moleditpy-1.17.1.dist-info → moleditpy-1.18.1.dist-info}/licenses/LICENSE +0 -0
- {moleditpy-1.17.1.dist-info → moleditpy-1.18.1.dist-info}/top_level.txt +0 -0
|
@@ -19,6 +19,7 @@ MainWindow (main_window.py) から分離されたモジュール
|
|
|
19
19
|
|
|
20
20
|
import numpy as np
|
|
21
21
|
import vtk
|
|
22
|
+
import logging
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
# RDKit imports (explicit to satisfy flake8 and used features)
|
|
@@ -58,7 +59,7 @@ if OBABEL_AVAILABLE:
|
|
|
58
59
|
# If import fails here, disable OBABEL locally; avoid raising
|
|
59
60
|
pybel = None
|
|
60
61
|
OBABEL_AVAILABLE = False
|
|
61
|
-
|
|
62
|
+
logging.warning("Warning: openbabel.pybel not available. Open Babel fallback and OBabel-based options will be disabled.")
|
|
62
63
|
else:
|
|
63
64
|
pybel = None
|
|
64
65
|
|
|
@@ -201,7 +202,7 @@ class MainWindowView3d(object):
|
|
|
201
202
|
resolution = self.settings.get('wireframe_resolution', 6)
|
|
202
203
|
rad = np.array([0.01 for s in sym]) # 極小値(使用されない)
|
|
203
204
|
elif self.current_3d_style == 'stick':
|
|
204
|
-
atom_radius = self.settings.get('
|
|
205
|
+
atom_radius = self.settings.get('stick_bond_radius', 0.15) # Use bond radius for atoms
|
|
205
206
|
resolution = self.settings.get('stick_resolution', 16)
|
|
206
207
|
rad = np.array([atom_radius for s in sym])
|
|
207
208
|
else: # ball_and_stick
|
|
@@ -223,7 +224,118 @@ class MainWindowView3d(object):
|
|
|
223
224
|
|
|
224
225
|
# Wireframeスタイルの場合は原子を描画しない
|
|
225
226
|
if self.current_3d_style != 'wireframe':
|
|
226
|
-
|
|
227
|
+
# Stickモードで末端二重結合・三重結合の原子を分裂させるための処理
|
|
228
|
+
if self.current_3d_style == 'stick':
|
|
229
|
+
# 末端原子(次数1)で多重結合を持つものを検出
|
|
230
|
+
split_atoms = [] # (atom_idx, bond_order, offset_vecs)
|
|
231
|
+
skip_atoms = set() # スキップする原子のインデックス
|
|
232
|
+
|
|
233
|
+
for i in range(mol_to_draw.GetNumAtoms()):
|
|
234
|
+
atom = mol_to_draw.GetAtomWithIdx(i)
|
|
235
|
+
if atom.GetDegree() == 1: # 末端原子
|
|
236
|
+
bonds = atom.GetBonds()
|
|
237
|
+
if len(bonds) == 1:
|
|
238
|
+
bond = bonds[0]
|
|
239
|
+
bond_type = bond.GetBondType()
|
|
240
|
+
|
|
241
|
+
if bond_type in [Chem.BondType.DOUBLE, Chem.BondType.TRIPLE]:
|
|
242
|
+
# 多重結合を持つ末端原子を発見
|
|
243
|
+
# 結合のもう一方の原子を取得
|
|
244
|
+
other_idx = bond.GetBeginAtomIdx() if bond.GetEndAtomIdx() == i else bond.GetEndAtomIdx()
|
|
245
|
+
|
|
246
|
+
# 結合ベクトルを計算
|
|
247
|
+
pos_i = np.array(conf.GetAtomPosition(i))
|
|
248
|
+
pos_other = np.array(conf.GetAtomPosition(other_idx))
|
|
249
|
+
bond_vec = pos_i - pos_other
|
|
250
|
+
bond_length = np.linalg.norm(bond_vec)
|
|
251
|
+
|
|
252
|
+
if bond_length > 0:
|
|
253
|
+
bond_unit = bond_vec / bond_length
|
|
254
|
+
|
|
255
|
+
# 二重結合の場合は実際の描画と同じオフセット方向を使用
|
|
256
|
+
if bond_type == Chem.BondType.DOUBLE:
|
|
257
|
+
offset_dir1 = self._calculate_double_bond_offset(mol_to_draw, bond, conf)
|
|
258
|
+
else:
|
|
259
|
+
# 三重結合の場合は結合描画と同じロジック
|
|
260
|
+
v_arb = np.array([0, 0, 1])
|
|
261
|
+
if np.allclose(np.abs(np.dot(bond_unit, v_arb)), 1.0):
|
|
262
|
+
v_arb = np.array([0, 1, 0])
|
|
263
|
+
offset_dir1 = np.cross(bond_unit, v_arb)
|
|
264
|
+
offset_dir1 /= np.linalg.norm(offset_dir1)
|
|
265
|
+
|
|
266
|
+
# 二重/三重結合描画のオフセット値と半径を取得(結合描画と完全に一致させる)
|
|
267
|
+
try:
|
|
268
|
+
cyl_radius = self.settings.get('stick_bond_radius', 0.15)
|
|
269
|
+
if bond_type == Chem.BondType.DOUBLE:
|
|
270
|
+
radius_factor = self.settings.get('stick_double_bond_radius_factor', 0.60)
|
|
271
|
+
offset_factor = self.settings.get('stick_double_bond_offset_factor', 1.5)
|
|
272
|
+
# 二重結合:s_double / 2 を使用
|
|
273
|
+
offset_distance = cyl_radius * offset_factor / 2
|
|
274
|
+
else: # TRIPLE
|
|
275
|
+
radius_factor = self.settings.get('stick_triple_bond_radius_factor', 0.40)
|
|
276
|
+
offset_factor = self.settings.get('stick_triple_bond_offset_factor', 1.0)
|
|
277
|
+
# 三重結合:s_triple をそのまま使用(/ 2 なし)
|
|
278
|
+
offset_distance = cyl_radius * offset_factor
|
|
279
|
+
|
|
280
|
+
# 結合描画と同じ計算
|
|
281
|
+
sphere_radius = cyl_radius * radius_factor
|
|
282
|
+
except Exception:
|
|
283
|
+
sphere_radius = 0.09 # デフォルト値
|
|
284
|
+
offset_distance = 0.15 # デフォルト値
|
|
285
|
+
|
|
286
|
+
if bond_type == Chem.BondType.DOUBLE:
|
|
287
|
+
# 二重結合:2個に分裂
|
|
288
|
+
offset_vecs = [
|
|
289
|
+
offset_dir1 * offset_distance,
|
|
290
|
+
-offset_dir1 * offset_distance
|
|
291
|
+
]
|
|
292
|
+
split_atoms.append((i, 2, offset_vecs))
|
|
293
|
+
else: # TRIPLE
|
|
294
|
+
# 三重結合:3個に分裂(中心 + 両側2つ)
|
|
295
|
+
# 結合描画と同じ配置
|
|
296
|
+
offset_vecs = [
|
|
297
|
+
np.array([0, 0, 0]), # 中心
|
|
298
|
+
offset_dir1 * offset_distance, # +side
|
|
299
|
+
-offset_dir1 * offset_distance # -side
|
|
300
|
+
]
|
|
301
|
+
split_atoms.append((i, 3, offset_vecs))
|
|
302
|
+
|
|
303
|
+
skip_atoms.add(i)
|
|
304
|
+
|
|
305
|
+
# 分裂させる原子がある場合、新しい位置リストを作成
|
|
306
|
+
if split_atoms:
|
|
307
|
+
new_positions = []
|
|
308
|
+
new_colors = []
|
|
309
|
+
new_radii = []
|
|
310
|
+
|
|
311
|
+
# 通常の原子を追加(スキップリスト以外)
|
|
312
|
+
for i in range(len(self.atom_positions_3d)):
|
|
313
|
+
if i not in skip_atoms:
|
|
314
|
+
new_positions.append(self.atom_positions_3d[i])
|
|
315
|
+
new_colors.append(col[i])
|
|
316
|
+
new_radii.append(rad[i])
|
|
317
|
+
|
|
318
|
+
# 分裂した原子を追加
|
|
319
|
+
# 上記で計算されたsphere_radiusを使用(結合描画のradius_factorを適用済み)
|
|
320
|
+
for atom_idx, bond_order, offset_vecs in split_atoms:
|
|
321
|
+
pos = self.atom_positions_3d[atom_idx]
|
|
322
|
+
# この原子の結合から半径を取得(上記ループで計算済み)
|
|
323
|
+
# 簡便のため、最後に計算されたsphere_radiusを使用
|
|
324
|
+
for offset_vec in offset_vecs:
|
|
325
|
+
new_positions.append(pos + offset_vec)
|
|
326
|
+
new_colors.append(col[atom_idx])
|
|
327
|
+
new_radii.append(sphere_radius)
|
|
328
|
+
|
|
329
|
+
# PolyDataを新しい位置で作成
|
|
330
|
+
glyph_source = pv.PolyData(np.array(new_positions))
|
|
331
|
+
glyph_source['colors'] = np.array(new_colors)
|
|
332
|
+
glyph_source['radii'] = np.array(new_radii)
|
|
333
|
+
else:
|
|
334
|
+
glyph_source = self.glyph_source
|
|
335
|
+
else:
|
|
336
|
+
glyph_source = self.glyph_source
|
|
337
|
+
|
|
338
|
+
glyphs = glyph_source.glyph(scale='radii', geom=pv.Sphere(radius=1.0, theta_resolution=resolution, phi_resolution=resolution), orient=False)
|
|
227
339
|
|
|
228
340
|
if is_lighting_enabled:
|
|
229
341
|
self.atom_actor = self.plotter.add_mesh(glyphs, scalars='colors', rgb=True, **mesh_props)
|
|
@@ -254,67 +366,79 @@ class MainWindowView3d(object):
|
|
|
254
366
|
cyl_radius = self.settings.get('ball_stick_bond_radius', 0.1)
|
|
255
367
|
bond_resolution = self.settings.get('ball_stick_resolution', 16)
|
|
256
368
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
# Ball and Stick用のシリンダーリストを準備(高速化のため)
|
|
369
|
+
# Ball and Stick用の共通色
|
|
370
|
+
bs_bond_rgb = [127, 127, 127]
|
|
260
371
|
if self.current_3d_style == 'ball_and_stick':
|
|
261
|
-
bond_cylinders = []
|
|
262
|
-
# Compute the configured grey/uniform bond color for Ball & Stick
|
|
263
372
|
try:
|
|
264
373
|
bs_hex = self.settings.get('ball_stick_bond_color', '#7F7F7F')
|
|
265
374
|
q = QColor(bs_hex)
|
|
266
375
|
bs_bond_rgb = [q.red(), q.green(), q.blue()]
|
|
267
376
|
except Exception:
|
|
268
|
-
|
|
377
|
+
pass
|
|
378
|
+
|
|
379
|
+
# バッチ処理用のリスト
|
|
380
|
+
all_points = []
|
|
381
|
+
all_lines = []
|
|
382
|
+
all_radii = []
|
|
383
|
+
all_colors = [] # Cell data (one per line segment)
|
|
269
384
|
|
|
385
|
+
current_point_idx = 0
|
|
386
|
+
bond_counter = 0
|
|
387
|
+
|
|
270
388
|
for bond in mol_to_draw.GetBonds():
|
|
271
389
|
begin_atom_idx = bond.GetBeginAtomIdx()
|
|
272
390
|
end_atom_idx = bond.GetEndAtomIdx()
|
|
273
391
|
sp = np.array(conf.GetAtomPosition(begin_atom_idx))
|
|
274
392
|
ep = np.array(conf.GetAtomPosition(end_atom_idx))
|
|
275
393
|
bt = bond.GetBondType()
|
|
276
|
-
c = (sp + ep) / 2
|
|
277
394
|
d = ep - sp
|
|
278
395
|
h = np.linalg.norm(d)
|
|
279
396
|
if h == 0: continue
|
|
280
397
|
|
|
281
|
-
#
|
|
398
|
+
# ボンドの色
|
|
282
399
|
begin_color = col[begin_atom_idx]
|
|
283
400
|
end_color = col[end_atom_idx]
|
|
284
|
-
|
|
285
|
-
# 結合の色情報を記録
|
|
286
401
|
begin_color_rgb = [int(c * 255) for c in begin_color]
|
|
287
402
|
end_color_rgb = [int(c * 255) for c in end_color]
|
|
288
403
|
|
|
289
|
-
#
|
|
404
|
+
# セグメント追加用ヘルパー関数
|
|
405
|
+
def add_segment(p1, p2, radius, color_rgb):
|
|
406
|
+
nonlocal current_point_idx
|
|
407
|
+
all_points.append(p1)
|
|
408
|
+
all_points.append(p2)
|
|
409
|
+
all_lines.append([2, current_point_idx, current_point_idx + 1])
|
|
410
|
+
all_radii.append(radius)
|
|
411
|
+
all_radii.append(radius)
|
|
412
|
+
all_colors.append(color_rgb)
|
|
413
|
+
current_point_idx += 2
|
|
414
|
+
|
|
290
415
|
QApplication.processEvents()
|
|
416
|
+
|
|
417
|
+
# Get CPK bond color setting once for all bond types
|
|
418
|
+
use_cpk_bond = self.settings.get('ball_stick_use_cpk_bond_color', False)
|
|
419
|
+
|
|
291
420
|
if bt == Chem.rdchem.BondType.SINGLE or bt == Chem.rdchem.BondType.AROMATIC:
|
|
292
|
-
if self.current_3d_style == 'ball_and_stick':
|
|
293
|
-
#
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
self._3d_color_map[f'bond_{bond_counter}'] = bs_bond_rgb # グレー (configurable)
|
|
421
|
+
if self.current_3d_style == 'ball_and_stick' and not use_cpk_bond:
|
|
422
|
+
# 単一セグメント (Uniform color)
|
|
423
|
+
add_segment(sp, ep, cyl_radius, bs_bond_rgb)
|
|
424
|
+
self._3d_color_map[f'bond_{bond_counter}'] = bs_bond_rgb
|
|
297
425
|
else:
|
|
298
|
-
#
|
|
426
|
+
# 分割セグメント (CPK split colors)
|
|
299
427
|
mid_point = (sp + ep) / 2
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
cyl1 = pv.Cylinder(center=(sp + mid_point) / 2, direction=d, radius=cyl_radius, height=h/2, resolution=bond_resolution)
|
|
303
|
-
actor1 = self.plotter.add_mesh(cyl1, color=begin_color, **mesh_props)
|
|
428
|
+
add_segment(sp, mid_point, cyl_radius, begin_color_rgb)
|
|
429
|
+
add_segment(mid_point, ep, cyl_radius, end_color_rgb)
|
|
304
430
|
self._3d_color_map[f'bond_{bond_counter}_start'] = begin_color_rgb
|
|
305
|
-
|
|
306
|
-
# 後半(終了原子の色)
|
|
307
|
-
cyl2 = pv.Cylinder(center=(mid_point + ep) / 2, direction=d, radius=cyl_radius, height=h/2, resolution=bond_resolution)
|
|
308
|
-
actor2 = self.plotter.add_mesh(cyl2, color=end_color, **mesh_props)
|
|
309
431
|
self._3d_color_map[f'bond_{bond_counter}_end'] = end_color_rgb
|
|
432
|
+
|
|
310
433
|
else:
|
|
434
|
+
# 多重結合のパラメータ計算
|
|
311
435
|
v1 = d / h
|
|
312
436
|
# モデルごとの半径ファクターを適用
|
|
313
437
|
if self.current_3d_style == 'ball_and_stick':
|
|
314
438
|
double_radius_factor = self.settings.get('ball_stick_double_bond_radius_factor', 0.8)
|
|
315
439
|
triple_radius_factor = self.settings.get('ball_stick_triple_bond_radius_factor', 0.75)
|
|
316
440
|
elif self.current_3d_style == 'wireframe':
|
|
317
|
-
double_radius_factor = self.settings.get('wireframe_double_bond_radius_factor',
|
|
441
|
+
double_radius_factor = self.settings.get('wireframe_double_bond_radius_factor', 0.8)
|
|
318
442
|
triple_radius_factor = self.settings.get('wireframe_triple_bond_radius_factor', 0.75)
|
|
319
443
|
elif self.current_3d_style == 'stick':
|
|
320
444
|
double_radius_factor = self.settings.get('stick_double_bond_radius_factor', 0.60)
|
|
@@ -322,7 +446,7 @@ class MainWindowView3d(object):
|
|
|
322
446
|
else:
|
|
323
447
|
double_radius_factor = 1.0
|
|
324
448
|
triple_radius_factor = 0.75
|
|
325
|
-
|
|
449
|
+
|
|
326
450
|
# 設定からオフセットファクターを取得(モデルごと)
|
|
327
451
|
if self.current_3d_style == 'ball_and_stick':
|
|
328
452
|
double_offset_factor = self.settings.get('ball_stick_double_bond_offset_factor', 2.0)
|
|
@@ -336,111 +460,208 @@ class MainWindowView3d(object):
|
|
|
336
460
|
else:
|
|
337
461
|
double_offset_factor = 2.0
|
|
338
462
|
triple_offset_factor = 2.0
|
|
339
|
-
s = cyl_radius * 2.0 # デフォルト値
|
|
340
463
|
|
|
341
464
|
if bt == Chem.rdchem.BondType.DOUBLE:
|
|
342
465
|
r = cyl_radius * double_radius_factor
|
|
343
|
-
# 二重結合の場合、結合している原子の他の結合を考慮してオフセット方向を決定
|
|
344
466
|
off_dir = self._calculate_double_bond_offset(mol_to_draw, bond, conf)
|
|
345
|
-
# 設定から二重結合のオフセットファクターを適用
|
|
346
467
|
s_double = cyl_radius * double_offset_factor
|
|
347
|
-
c1, c2 = c + off_dir * (s_double / 2), c - off_dir * (s_double / 2)
|
|
348
468
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
469
|
+
p1_start = sp + off_dir * (s_double / 2)
|
|
470
|
+
p1_end = ep + off_dir * (s_double / 2)
|
|
471
|
+
p2_start = sp - off_dir * (s_double / 2)
|
|
472
|
+
p2_end = ep - off_dir * (s_double / 2)
|
|
473
|
+
|
|
474
|
+
if self.current_3d_style == 'ball_and_stick' and not use_cpk_bond:
|
|
475
|
+
add_segment(p1_start, p1_end, r, bs_bond_rgb)
|
|
476
|
+
add_segment(p2_start, p2_end, r, bs_bond_rgb)
|
|
354
477
|
self._3d_color_map[f'bond_{bond_counter}_1'] = bs_bond_rgb
|
|
355
478
|
self._3d_color_map[f'bond_{bond_counter}_2'] = bs_bond_rgb
|
|
356
479
|
else:
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
self.plotter.add_mesh(cyl1_1, color=begin_color, **mesh_props)
|
|
364
|
-
self.plotter.add_mesh(cyl1_2, color=end_color, **mesh_props)
|
|
480
|
+
mid1 = (p1_start + p1_end) / 2
|
|
481
|
+
mid2 = (p2_start + p2_end) / 2
|
|
482
|
+
add_segment(p1_start, mid1, r, begin_color_rgb)
|
|
483
|
+
add_segment(mid1, p1_end, r, end_color_rgb)
|
|
484
|
+
add_segment(p2_start, mid2, r, begin_color_rgb)
|
|
485
|
+
add_segment(mid2, p2_end, r, end_color_rgb)
|
|
365
486
|
self._3d_color_map[f'bond_{bond_counter}_1_start'] = begin_color_rgb
|
|
366
487
|
self._3d_color_map[f'bond_{bond_counter}_1_end'] = end_color_rgb
|
|
367
|
-
|
|
368
|
-
# 第二の結合線(前半・後半)
|
|
369
|
-
cyl2_1 = pv.Cylinder(center=(sp + mid_point) / 2 - off_dir * (s_double / 2), direction=d, radius=r, height=h/2, resolution=bond_resolution)
|
|
370
|
-
cyl2_2 = pv.Cylinder(center=(mid_point + ep) / 2 - off_dir * (s_double / 2), direction=d, radius=r, height=h/2, resolution=bond_resolution)
|
|
371
|
-
self.plotter.add_mesh(cyl2_1, color=begin_color, **mesh_props)
|
|
372
|
-
self.plotter.add_mesh(cyl2_2, color=end_color, **mesh_props)
|
|
373
488
|
self._3d_color_map[f'bond_{bond_counter}_2_start'] = begin_color_rgb
|
|
374
489
|
self._3d_color_map[f'bond_{bond_counter}_2_end'] = end_color_rgb
|
|
490
|
+
|
|
375
491
|
elif bt == Chem.rdchem.BondType.TRIPLE:
|
|
376
492
|
r = cyl_radius * triple_radius_factor
|
|
377
|
-
# 三重結合
|
|
378
493
|
v_arb = np.array([0, 0, 1])
|
|
379
494
|
if np.allclose(np.abs(np.dot(v1, v_arb)), 1.0): v_arb = np.array([0, 1, 0])
|
|
380
495
|
off_dir = np.cross(v1, v_arb)
|
|
381
496
|
off_dir /= np.linalg.norm(off_dir)
|
|
382
|
-
|
|
383
|
-
# 設定から三重結合のオフセットファクターを適用
|
|
384
497
|
s_triple = cyl_radius * triple_offset_factor
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
cyl2 = pv.Cylinder(center=c + off_dir * s_triple, direction=d, radius=r, height=h, resolution=bond_resolution)
|
|
390
|
-
cyl3 = pv.Cylinder(center=c - off_dir * s_triple, direction=d, radius=r, height=h, resolution=bond_resolution)
|
|
391
|
-
bond_cylinders.extend([cyl1, cyl2, cyl3])
|
|
498
|
+
|
|
499
|
+
# Center
|
|
500
|
+
if self.current_3d_style == 'ball_and_stick' and not use_cpk_bond:
|
|
501
|
+
add_segment(sp, ep, r, bs_bond_rgb)
|
|
392
502
|
self._3d_color_map[f'bond_{bond_counter}_1'] = bs_bond_rgb
|
|
393
|
-
self._3d_color_map[f'bond_{bond_counter}_2'] = bs_bond_rgb
|
|
394
|
-
self._3d_color_map[f'bond_{bond_counter}_3'] = bs_bond_rgb
|
|
395
503
|
else:
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
# 中央の結合線(前半・後半)
|
|
400
|
-
cyl1_1 = pv.Cylinder(center=(sp + mid_point) / 2, direction=d, radius=r, height=h/2, resolution=bond_resolution)
|
|
401
|
-
cyl1_2 = pv.Cylinder(center=(mid_point + ep) / 2, direction=d, radius=r, height=h/2, resolution=bond_resolution)
|
|
402
|
-
self.plotter.add_mesh(cyl1_1, color=begin_color, **mesh_props)
|
|
403
|
-
self.plotter.add_mesh(cyl1_2, color=end_color, **mesh_props)
|
|
504
|
+
mid = (sp + ep) / 2
|
|
505
|
+
add_segment(sp, mid, r, begin_color_rgb)
|
|
506
|
+
add_segment(mid, ep, r, end_color_rgb)
|
|
404
507
|
self._3d_color_map[f'bond_{bond_counter}_1_start'] = begin_color_rgb
|
|
405
508
|
self._3d_color_map[f'bond_{bond_counter}_1_end'] = end_color_rgb
|
|
509
|
+
|
|
510
|
+
# Sides
|
|
511
|
+
for sign in [1, -1]:
|
|
512
|
+
offset = off_dir * s_triple * sign
|
|
513
|
+
p_start = sp + offset
|
|
514
|
+
p_end = ep + offset
|
|
406
515
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
self.plotter.add_mesh(cyl3_1, color=begin_color, **mesh_props)
|
|
419
|
-
self.plotter.add_mesh(cyl3_2, color=end_color, **mesh_props)
|
|
420
|
-
self._3d_color_map[f'bond_{bond_counter}_3_start'] = begin_color_rgb
|
|
421
|
-
self._3d_color_map[f'bond_{bond_counter}_3_end'] = end_color_rgb
|
|
516
|
+
if self.current_3d_style == 'ball_and_stick' and not use_cpk_bond:
|
|
517
|
+
add_segment(p_start, p_end, r, bs_bond_rgb)
|
|
518
|
+
suffix = '_2' if sign == 1 else '_3'
|
|
519
|
+
self._3d_color_map[f'bond_{bond_counter}{suffix}'] = bs_bond_rgb
|
|
520
|
+
else:
|
|
521
|
+
mid = (p_start + p_end) / 2
|
|
522
|
+
add_segment(p_start, mid, r, begin_color_rgb)
|
|
523
|
+
add_segment(mid, p_end, r, end_color_rgb)
|
|
524
|
+
suffix = '_2' if sign == 1 else '_3'
|
|
525
|
+
self._3d_color_map[f'bond_{bond_counter}{suffix}_start'] = begin_color_rgb
|
|
526
|
+
self._3d_color_map[f'bond_{bond_counter}{suffix}_end'] = end_color_rgb
|
|
422
527
|
|
|
423
528
|
bond_counter += 1
|
|
424
|
-
|
|
425
|
-
#
|
|
426
|
-
if
|
|
427
|
-
#
|
|
428
|
-
|
|
429
|
-
|
|
529
|
+
|
|
530
|
+
# ジオメトリの生成と描画
|
|
531
|
+
if all_points:
|
|
532
|
+
# Create PolyData
|
|
533
|
+
bond_pd = pv.PolyData(np.array(all_points), lines=np.hstack(all_lines))
|
|
534
|
+
# lines needs to be a flat array with padding indicating number of points per cell
|
|
535
|
+
# all_lines is [[2, i, j], [2, k, l], ...], flatten it
|
|
430
536
|
|
|
431
|
-
#
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
bond_actor = self.plotter.add_mesh(combined_mesh, color=bond_color, **mesh_props)
|
|
439
|
-
except Exception:
|
|
440
|
-
bond_actor = self.plotter.add_mesh(combined_mesh, color='grey', **mesh_props)
|
|
537
|
+
# Add data
|
|
538
|
+
bond_pd.point_data['radii'] = np.array(all_radii)
|
|
539
|
+
|
|
540
|
+
# Convert colors to 0-1 range for PyVista if needed, but add_mesh with rgb=True expects uint8 if using direct array?
|
|
541
|
+
# Actually pyvista scalars usually prefer float 0-1 or uint8 0-255.
|
|
542
|
+
# Let's use uint8 0-255 and rgb=True.
|
|
543
|
+
bond_pd.cell_data['colors'] = np.array(all_colors, dtype=np.uint8)
|
|
441
544
|
|
|
442
|
-
#
|
|
443
|
-
|
|
545
|
+
# Tube filter
|
|
546
|
+
# n_sides (resolution) corresponds to theta_resolution in Cylinder
|
|
547
|
+
tube = bond_pd.tube(scalars='radii', absolute=True, radius_factor=1.0, n_sides=bond_resolution, capping=True)
|
|
548
|
+
|
|
549
|
+
# Add to plotter
|
|
550
|
+
self.plotter.add_mesh(tube, scalars='colors', rgb=True, **mesh_props)
|
|
551
|
+
|
|
552
|
+
# Aromatic ring circles display
|
|
553
|
+
if self.settings.get('display_aromatic_circles_3d', False):
|
|
554
|
+
try:
|
|
555
|
+
ring_info = mol_to_draw.GetRingInfo()
|
|
556
|
+
aromatic_rings = []
|
|
557
|
+
|
|
558
|
+
# Find aromatic rings
|
|
559
|
+
for ring in ring_info.AtomRings():
|
|
560
|
+
# Check if all atoms in ring are aromatic
|
|
561
|
+
is_aromatic = all(mol_to_draw.GetAtomWithIdx(idx).GetIsAromatic() for idx in ring)
|
|
562
|
+
if is_aromatic:
|
|
563
|
+
aromatic_rings.append(ring)
|
|
564
|
+
|
|
565
|
+
# Draw circles for aromatic rings
|
|
566
|
+
for ring in aromatic_rings:
|
|
567
|
+
# Get atom positions
|
|
568
|
+
ring_positions = [self.atom_positions_3d[idx] for idx in ring]
|
|
569
|
+
ring_positions_np = np.array(ring_positions)
|
|
570
|
+
|
|
571
|
+
# Calculate ring center
|
|
572
|
+
center = np.mean(ring_positions_np, axis=0)
|
|
573
|
+
|
|
574
|
+
# Calculate ring normal using PCA or cross product
|
|
575
|
+
# Use first 3 atoms to get two vectors
|
|
576
|
+
if len(ring) >= 3:
|
|
577
|
+
v1 = ring_positions_np[1] - ring_positions_np[0]
|
|
578
|
+
v2 = ring_positions_np[2] - ring_positions_np[0]
|
|
579
|
+
normal = np.cross(v1, v2)
|
|
580
|
+
normal_length = np.linalg.norm(normal)
|
|
581
|
+
if normal_length > 0:
|
|
582
|
+
normal = normal / normal_length
|
|
583
|
+
else:
|
|
584
|
+
normal = np.array([0, 0, 1])
|
|
585
|
+
else:
|
|
586
|
+
normal = np.array([0, 0, 1])
|
|
587
|
+
|
|
588
|
+
# Calculate ring radius (average distance from center)
|
|
589
|
+
distances = [np.linalg.norm(pos - center) for pos in ring_positions_np]
|
|
590
|
+
ring_radius = np.mean(distances) * 0.55 # Slightly smaller
|
|
591
|
+
|
|
592
|
+
# Get bond radius from current style settings for torus thickness
|
|
593
|
+
if self.current_3d_style == 'stick':
|
|
594
|
+
bond_radius = self.settings.get('stick_bond_radius', 0.15)
|
|
595
|
+
elif self.current_3d_style == 'ball_and_stick':
|
|
596
|
+
bond_radius = self.settings.get('ball_stick_bond_radius', 0.1)
|
|
597
|
+
elif self.current_3d_style == 'wireframe':
|
|
598
|
+
bond_radius = self.settings.get('wireframe_bond_radius', 0.01)
|
|
599
|
+
else:
|
|
600
|
+
bond_radius = 0.1 # Default
|
|
601
|
+
# Apply user-defined thickness factor (default 0.6)
|
|
602
|
+
thickness_factor = self.settings.get('aromatic_torus_thickness_factor', 0.6)
|
|
603
|
+
tube_radius = bond_radius * thickness_factor
|
|
604
|
+
theta = np.linspace(0, 2.2 * np.pi, 64)
|
|
605
|
+
circle_x = ring_radius * np.cos(theta)
|
|
606
|
+
circle_y = ring_radius * np.sin(theta)
|
|
607
|
+
circle_z = np.zeros_like(theta)
|
|
608
|
+
circle_points = np.c_[circle_x, circle_y, circle_z]
|
|
609
|
+
|
|
610
|
+
# Create line from points
|
|
611
|
+
circle_line = pv.Spline(circle_points, n_points=64).tube(radius=tube_radius, n_sides=16)
|
|
612
|
+
|
|
613
|
+
# Rotate torus to align with ring plane
|
|
614
|
+
# Default torus is in XY plane (normal = [0, 0, 1])
|
|
615
|
+
default_normal = np.array([0, 0, 1])
|
|
616
|
+
|
|
617
|
+
# Calculate rotation axis and angle
|
|
618
|
+
if not np.allclose(normal, default_normal) and not np.allclose(normal, -default_normal):
|
|
619
|
+
axis = np.cross(default_normal, normal)
|
|
620
|
+
axis_length = np.linalg.norm(axis)
|
|
621
|
+
if axis_length > 0:
|
|
622
|
+
axis = axis / axis_length
|
|
623
|
+
angle = np.arccos(np.clip(np.dot(default_normal, normal), -1.0, 1.0))
|
|
624
|
+
angle_deg = np.degrees(angle)
|
|
625
|
+
|
|
626
|
+
# Rotate torus
|
|
627
|
+
circle_line = circle_line.rotate_vector(axis, angle_deg, point=[0, 0, 0])
|
|
628
|
+
|
|
629
|
+
# Translate to ring center
|
|
630
|
+
circle_line = circle_line.translate(center)
|
|
631
|
+
|
|
632
|
+
# Get torus color from bond color settings
|
|
633
|
+
# Calculate most common atom type in ring for CPK color
|
|
634
|
+
from collections import Counter
|
|
635
|
+
atom_symbols = [mol_to_draw.GetAtomWithIdx(idx).GetSymbol() for idx in ring]
|
|
636
|
+
most_common_symbol = Counter(atom_symbols).most_common(1)[0][0] if atom_symbols else None
|
|
637
|
+
|
|
638
|
+
if self.current_3d_style == 'ball_and_stick':
|
|
639
|
+
# Check if using CPK bond colors
|
|
640
|
+
use_cpk = self.settings.get('ball_stick_use_cpk_bond_color', False)
|
|
641
|
+
if use_cpk:
|
|
642
|
+
# Use CPK color of most common atom type in ring
|
|
643
|
+
if most_common_symbol:
|
|
644
|
+
cpk_color = CPK_COLORS_PV.get(most_common_symbol, [0.5, 0.5, 0.5])
|
|
645
|
+
torus_color = cpk_color
|
|
646
|
+
else:
|
|
647
|
+
torus_color = [0.5, 0.5, 0.5]
|
|
648
|
+
else:
|
|
649
|
+
# Use Ball & Stick bond color setting
|
|
650
|
+
bond_hex = self.settings.get('ball_stick_bond_color', '#7F7F7F')
|
|
651
|
+
q = QColor(bond_hex)
|
|
652
|
+
torus_color = [q.red() / 255.0, q.green() / 255.0, q.blue() / 255.0]
|
|
653
|
+
else:
|
|
654
|
+
# For Wireframe and Stick, use CPK color of most common atom
|
|
655
|
+
if most_common_symbol:
|
|
656
|
+
cpk_color = CPK_COLORS_PV.get(most_common_symbol, [0.5, 0.5, 0.5])
|
|
657
|
+
torus_color = cpk_color
|
|
658
|
+
else:
|
|
659
|
+
torus_color = [0.5, 0.5, 0.5]
|
|
660
|
+
|
|
661
|
+
self.plotter.add_mesh(circle_line, color=torus_color, **mesh_props)
|
|
662
|
+
|
|
663
|
+
except Exception as e:
|
|
664
|
+
logging.error(f"Error rendering aromatic circles: {e}")
|
|
444
665
|
|
|
445
666
|
if getattr(self, 'show_chiral_labels', False):
|
|
446
667
|
try:
|
|
@@ -463,7 +684,7 @@ class MainWindowView3d(object):
|
|
|
463
684
|
# If we drew a kekulized molecule use it for E/Z detection so
|
|
464
685
|
# E/Z labels reflect Kekulé rendering; pass mol_to_draw as the
|
|
465
686
|
# molecule to scan for bond stereochemistry.
|
|
466
|
-
self.show_ez_labels_3d(mol
|
|
687
|
+
self.show_ez_labels_3d(mol)
|
|
467
688
|
except Exception as e:
|
|
468
689
|
self.statusBar().showMessage(f"3D E/Z label drawing error: {e}")
|
|
469
690
|
|
|
@@ -592,7 +813,7 @@ class MainWindowView3d(object):
|
|
|
592
813
|
|
|
593
814
|
|
|
594
815
|
|
|
595
|
-
def show_ez_labels_3d(self, mol
|
|
816
|
+
def show_ez_labels_3d(self, mol):
|
|
596
817
|
"""3DビューでE/Zラベルを表示する(RDKitのステレオ化学判定を使用)"""
|
|
597
818
|
if not mol:
|
|
598
819
|
return
|
|
@@ -600,7 +821,7 @@ class MainWindowView3d(object):
|
|
|
600
821
|
try:
|
|
601
822
|
# 既存のE/Zラベルを削除
|
|
602
823
|
self.plotter.remove_actor('ez_labels')
|
|
603
|
-
except:
|
|
824
|
+
except Exception:
|
|
604
825
|
pass
|
|
605
826
|
|
|
606
827
|
pts, labels = [], []
|
|
@@ -611,33 +832,40 @@ class MainWindowView3d(object):
|
|
|
611
832
|
|
|
612
833
|
conf = mol.GetConformer()
|
|
613
834
|
|
|
614
|
-
# RDKit
|
|
835
|
+
# 二重結合でRDKitが判定したE/Z立体化学を表示
|
|
836
|
+
|
|
615
837
|
try:
|
|
616
|
-
# 3D座標からステレオ化学を再計算
|
|
838
|
+
# 3D座標からステレオ化学を再計算 (molに対して行う)
|
|
839
|
+
# これにより、2Dでの描画状態に関わらず、現在の3D座標に基づいたE/Z判定が行われる
|
|
617
840
|
Chem.AssignStereochemistry(mol, cleanIt=True, force=True, flagPossibleStereoCenters=True)
|
|
618
|
-
except:
|
|
841
|
+
except Exception:
|
|
619
842
|
pass
|
|
620
|
-
|
|
621
|
-
# 二重結合でRDKitが判定したE/Z立体化学を表示
|
|
622
|
-
# `scan_mol` is used for stereochemistry detection (bond types); default
|
|
623
|
-
# to the provided molecule if not supplied.
|
|
624
|
-
if scan_mol is None:
|
|
625
|
-
scan_mol = mol
|
|
626
843
|
|
|
627
|
-
for bond in
|
|
844
|
+
for bond in mol.GetBonds():
|
|
628
845
|
if bond.GetBondType() == Chem.BondType.DOUBLE:
|
|
629
|
-
|
|
630
|
-
|
|
846
|
+
new_stereo = bond.GetStereo()
|
|
847
|
+
|
|
848
|
+
if new_stereo in [Chem.BondStereo.STEREOE, Chem.BondStereo.STEREOZ]:
|
|
631
849
|
# 結合の中心座標を計算
|
|
632
|
-
# Use positions from the original molecule's conformer; `bond` may
|
|
633
|
-
# come from `scan_mol` which can be kekulized but position indices
|
|
634
|
-
# correspond to the original `mol`.
|
|
635
850
|
begin_pos = np.array(conf.GetAtomPosition(bond.GetBeginAtomIdx()))
|
|
636
851
|
end_pos = np.array(conf.GetAtomPosition(bond.GetEndAtomIdx()))
|
|
637
852
|
center_pos = (begin_pos + end_pos) / 2
|
|
638
853
|
|
|
639
|
-
#
|
|
640
|
-
label = 'E' if
|
|
854
|
+
# ラベルの決定
|
|
855
|
+
label = 'E' if new_stereo == Chem.BondStereo.STEREOE else 'Z'
|
|
856
|
+
|
|
857
|
+
# 2Dとの不一致チェック
|
|
858
|
+
# main_window_compute.py で保存された2D由来の立体化学プロパティを取得
|
|
859
|
+
try:
|
|
860
|
+
old_stereo = bond.GetIntProp("_original_2d_stereo")
|
|
861
|
+
except KeyError:
|
|
862
|
+
old_stereo = Chem.BondStereo.STEREONONE
|
|
863
|
+
|
|
864
|
+
# 2D側でもE/Zが指定されていて、かつ3Dと異なる場合は「?」にする
|
|
865
|
+
if old_stereo in [Chem.BondStereo.STEREOE, Chem.BondStereo.STEREOZ]:
|
|
866
|
+
if old_stereo != new_stereo:
|
|
867
|
+
label = '?'
|
|
868
|
+
|
|
641
869
|
pts.append(center_pos)
|
|
642
870
|
labels.append(label)
|
|
643
871
|
|
|
@@ -944,7 +1172,7 @@ class MainWindowView3d(object):
|
|
|
944
1172
|
for nm in self.atom_label_legend_names:
|
|
945
1173
|
try:
|
|
946
1174
|
self.plotter.remove_actor(nm)
|
|
947
|
-
except:
|
|
1175
|
+
except Exception:
|
|
948
1176
|
pass
|
|
949
1177
|
self.atom_label_legend_names = []
|
|
950
1178
|
|
|
@@ -1010,12 +1238,12 @@ class MainWindowView3d(object):
|
|
|
1010
1238
|
for a in list(self.current_atom_info_labels):
|
|
1011
1239
|
try:
|
|
1012
1240
|
self.plotter.remove_actor(a)
|
|
1013
|
-
except:
|
|
1241
|
+
except Exception:
|
|
1014
1242
|
pass
|
|
1015
1243
|
else:
|
|
1016
1244
|
try:
|
|
1017
1245
|
self.plotter.remove_actor(self.current_atom_info_labels)
|
|
1018
|
-
except:
|
|
1246
|
+
except Exception:
|
|
1019
1247
|
pass
|
|
1020
1248
|
except Exception:
|
|
1021
1249
|
pass
|
|
@@ -1028,7 +1256,7 @@ class MainWindowView3d(object):
|
|
|
1028
1256
|
for nm in list(self.atom_label_legend_names):
|
|
1029
1257
|
try:
|
|
1030
1258
|
self.plotter.remove_actor(nm)
|
|
1031
|
-
except:
|
|
1259
|
+
except Exception:
|
|
1032
1260
|
pass
|
|
1033
1261
|
except Exception:
|
|
1034
1262
|
pass
|
|
@@ -1174,7 +1402,7 @@ class MainWindowView3d(object):
|
|
|
1174
1402
|
if renderer and hasattr(renderer, 'SetNumberOfLayers'):
|
|
1175
1403
|
try:
|
|
1176
1404
|
renderer.SetNumberOfLayers(2) # レイヤー0:3Dオブジェクト、レイヤー1:2Dオーバーレイ
|
|
1177
|
-
except:
|
|
1405
|
+
except Exception:
|
|
1178
1406
|
pass # PyVistaのバージョンによってはサポートされていない場合がある
|
|
1179
1407
|
|
|
1180
1408
|
# --- 3D軸ウィジェットの設定 ---
|