MoleditPy-linux 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.
Files changed (27) hide show
  1. moleditpy_linux/modules/align_plane_dialog.py +1 -1
  2. moleditpy_linux/modules/alignment_dialog.py +1 -1
  3. moleditpy_linux/modules/angle_dialog.py +2 -2
  4. moleditpy_linux/modules/bond_item.py +96 -5
  5. moleditpy_linux/modules/bond_length_dialog.py +2 -2
  6. moleditpy_linux/modules/constants.py +1 -1
  7. moleditpy_linux/modules/constrained_optimization_dialog.py +2 -2
  8. moleditpy_linux/modules/dihedral_dialog.py +1 -1
  9. moleditpy_linux/modules/main_window_app_state.py +1 -1
  10. moleditpy_linux/modules/main_window_compute.py +6 -0
  11. moleditpy_linux/modules/main_window_edit_3d.py +7 -7
  12. moleditpy_linux/modules/main_window_export.py +106 -49
  13. moleditpy_linux/modules/main_window_main_init.py +3 -3
  14. moleditpy_linux/modules/main_window_molecular_parsers.py +4 -3
  15. moleditpy_linux/modules/main_window_project_io.py +2 -2
  16. moleditpy_linux/modules/main_window_view_3d.py +359 -131
  17. moleditpy_linux/modules/main_window_view_loaders.py +1 -1
  18. moleditpy_linux/modules/molecule_scene.py +14 -15
  19. moleditpy_linux/modules/planarize_dialog.py +1 -1
  20. moleditpy_linux/modules/settings_dialog.py +86 -20
  21. moleditpy_linux/modules/user_template_dialog.py +9 -8
  22. {moleditpy_linux-1.17.1.dist-info → moleditpy_linux-1.18.1.dist-info}/METADATA +1 -2
  23. {moleditpy_linux-1.17.1.dist-info → moleditpy_linux-1.18.1.dist-info}/RECORD +27 -27
  24. {moleditpy_linux-1.17.1.dist-info → moleditpy_linux-1.18.1.dist-info}/WHEEL +0 -0
  25. {moleditpy_linux-1.17.1.dist-info → moleditpy_linux-1.18.1.dist-info}/entry_points.txt +0 -0
  26. {moleditpy_linux-1.17.1.dist-info → moleditpy_linux-1.18.1.dist-info}/licenses/LICENSE +0 -0
  27. {moleditpy_linux-1.17.1.dist-info → moleditpy_linux-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
- print("Warning: openbabel.pybel not available. Open Babel fallback and OBabel-based options will be disabled.")
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('stick_atom_radius', 0.15)
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
- glyphs = self.glyph_source.glyph(scale='radii', geom=pv.Sphere(radius=1.0, theta_resolution=resolution, phi_resolution=resolution), orient=False)
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
- bond_counter = 0 # 結合の個別識別用
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
- bs_bond_rgb = [127, 127, 127]
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
- # UI応答性維持のためイベント処理
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
- # Ball and stickは全結合をまとめて処理(高速化)
294
- cyl = pv.Cylinder(center=c, direction=d, radius=cyl_radius, height=h, resolution=bond_resolution)
295
- bond_cylinders.append(cyl)
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
- # その他(stick, wireframe)は中央で色が変わる2つの円柱
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', 1.0)
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
- r = cyl_radius * 0.8 # fallback, will be overridden below
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
- if self.current_3d_style == 'ball_and_stick':
350
- # Ball and stickは全結合をまとめて処理(高速化)
351
- cyl1 = pv.Cylinder(center=c1, direction=d, radius=r, height=h, resolution=bond_resolution)
352
- cyl2 = pv.Cylinder(center=c2, direction=d, radius=r, height=h, resolution=bond_resolution)
353
- bond_cylinders.extend([cyl1, cyl2])
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
- # その他(stick, wireframe)は中央で色が変わる
358
- mid_point = (sp + ep) / 2
359
-
360
- # 第一の結合線(前半・後半)
361
- cyl1_1 = pv.Cylinder(center=(sp + mid_point) / 2 + off_dir * (s_double / 2), direction=d, radius=r, height=h/2, resolution=bond_resolution)
362
- cyl1_2 = pv.Cylinder(center=(mid_point + ep) / 2 + off_dir * (s_double / 2), direction=d, radius=r, height=h/2, resolution=bond_resolution)
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
- if self.current_3d_style == 'ball_and_stick':
387
- # Ball and stickは全結合をまとめて処理(高速化)
388
- cyl1 = pv.Cylinder(center=c, direction=d, radius=r, height=h, resolution=bond_resolution)
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
- # その他(stick, wireframe)は中央で色が変わる
397
- mid_point = (sp + ep) / 2
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
- cyl2_1 = pv.Cylinder(center=(sp + mid_point) / 2 + off_dir * s_triple, direction=d, radius=r, height=h/2, resolution=bond_resolution)
409
- cyl2_2 = pv.Cylinder(center=(mid_point + ep) / 2 + off_dir * s_triple, direction=d, radius=r, height=h/2, resolution=bond_resolution)
410
- self.plotter.add_mesh(cyl2_1, color=begin_color, **mesh_props)
411
- self.plotter.add_mesh(cyl2_2, color=end_color, **mesh_props)
412
- self._3d_color_map[f'bond_{bond_counter}_2_start'] = begin_color_rgb
413
- self._3d_color_map[f'bond_{bond_counter}_2_end'] = end_color_rgb
414
-
415
- # 下側の結合線(前半・後半)
416
- cyl3_1 = pv.Cylinder(center=(sp + mid_point) / 2 - off_dir * s_triple, direction=d, radius=r, height=h/2, resolution=bond_resolution)
417
- cyl3_2 = pv.Cylinder(center=(mid_point + ep) / 2 - off_dir * s_triple, direction=d, radius=r, height=h/2, resolution=bond_resolution)
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
- # Ball and Stick用:全結合をまとめて一括描画(高速化)
426
- if self.current_3d_style == 'ball_and_stick' and bond_cylinders:
427
- # 全シリンダーを結合してMultiBlockを作成
428
- combined_bonds = pv.MultiBlock(bond_cylinders)
429
- combined_mesh = combined_bonds.combine()
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
- # Use the configured Ball & Stick bond color (hex) for the combined bonds
433
- try:
434
- bs_hex = self.settings.get('ball_stick_bond_color', '#7F7F7F')
435
- q = QColor(bs_hex)
436
- # Use normalized RGB for pyvista (r,g,b) floats in [0,1]
437
- bond_color = (q.redF(), q.greenF(), q.blueF())
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
- self._3d_color_map['bonds_combined'] = bs_bond_rgb
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, scan_mol=mol_to_draw)
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, scan_mol=None):
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に3D座標からステレオ化学を計算させる
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 scan_mol.GetBonds():
844
+ for bond in mol.GetBonds():
628
845
  if bond.GetBondType() == Chem.BondType.DOUBLE:
629
- stereo = bond.GetStereo()
630
- if stereo in [Chem.BondStereo.STEREOE, Chem.BondStereo.STEREOZ]:
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
- # RDKitの判定結果を使用
640
- label = 'E' if stereo == Chem.BondStereo.STEREOE else 'Z'
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軸ウィジェットの設定 ---