MoleditPy 1.17.0__py3-none-any.whl → 1.18.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.
Files changed (53) hide show
  1. moleditpy/__init__.py +1 -1
  2. moleditpy/__main__.py +1 -1
  3. moleditpy/main.py +1 -1
  4. moleditpy/modules/__init__.py +1 -1
  5. moleditpy/modules/about_dialog.py +1 -1
  6. moleditpy/modules/align_plane_dialog.py +1 -1
  7. moleditpy/modules/alignment_dialog.py +1 -1
  8. moleditpy/modules/analysis_window.py +1 -1
  9. moleditpy/modules/angle_dialog.py +1 -1
  10. moleditpy/modules/atom_item.py +1 -1
  11. moleditpy/modules/bond_item.py +97 -6
  12. moleditpy/modules/bond_length_dialog.py +1 -1
  13. moleditpy/modules/calculation_worker.py +1 -1
  14. moleditpy/modules/color_settings_dialog.py +1 -1
  15. moleditpy/modules/constants.py +2 -2
  16. moleditpy/modules/constrained_optimization_dialog.py +1 -1
  17. moleditpy/modules/custom_interactor_style.py +1 -1
  18. moleditpy/modules/custom_qt_interactor.py +1 -1
  19. moleditpy/modules/dialog3_d_picking_mixin.py +1 -1
  20. moleditpy/modules/dihedral_dialog.py +1 -1
  21. moleditpy/modules/main_window.py +1 -1
  22. moleditpy/modules/main_window_app_state.py +9 -0
  23. moleditpy/modules/main_window_compute.py +7 -1
  24. moleditpy/modules/main_window_dialog_manager.py +1 -1
  25. moleditpy/modules/main_window_edit_3d.py +1 -1
  26. moleditpy/modules/main_window_edit_actions.py +1 -1
  27. moleditpy/modules/main_window_export.py +1 -1
  28. moleditpy/modules/main_window_main_init.py +1 -1
  29. moleditpy/modules/main_window_molecular_parsers.py +1 -1
  30. moleditpy/modules/main_window_project_io.py +1 -1
  31. moleditpy/modules/main_window_string_importers.py +1 -1
  32. moleditpy/modules/main_window_ui_manager.py +1 -1
  33. moleditpy/modules/main_window_view_3d.py +349 -122
  34. moleditpy/modules/main_window_view_loaders.py +1 -1
  35. moleditpy/modules/mirror_dialog.py +1 -1
  36. moleditpy/modules/molecular_data.py +1 -1
  37. moleditpy/modules/molecule_scene.py +8 -4
  38. moleditpy/modules/move_group_dialog.py +1 -1
  39. moleditpy/modules/periodic_table_dialog.py +1 -1
  40. moleditpy/modules/planarize_dialog.py +1 -1
  41. moleditpy/modules/settings_dialog.py +86 -2
  42. moleditpy/modules/template_preview_item.py +1 -1
  43. moleditpy/modules/template_preview_view.py +1 -1
  44. moleditpy/modules/translation_dialog.py +1 -1
  45. moleditpy/modules/user_template_dialog.py +1 -1
  46. moleditpy/modules/zoomable_view.py +22 -2
  47. {moleditpy-1.17.0.dist-info → moleditpy-1.18.0.dist-info}/METADATA +1 -1
  48. moleditpy-1.18.0.dist-info/RECORD +55 -0
  49. moleditpy-1.17.0.dist-info/RECORD +0 -55
  50. {moleditpy-1.17.0.dist-info → moleditpy-1.18.0.dist-info}/WHEEL +0 -0
  51. {moleditpy-1.17.0.dist-info → moleditpy-1.18.0.dist-info}/entry_points.txt +0 -0
  52. {moleditpy-1.17.0.dist-info → moleditpy-1.18.0.dist-info}/licenses/LICENSE +0 -0
  53. {moleditpy-1.17.0.dist-info → moleditpy-1.18.0.dist-info}/top_level.txt +0 -0
@@ -7,7 +7,7 @@ MoleditPy — A Python-based molecular editing software
7
7
  Author: Hiromichi Yokoyama
8
8
  License: GPL-3.0 license
9
9
  Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
- DOI 10.5281/zenodo.17268532
10
+ DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
13
  """
@@ -223,7 +223,118 @@ class MainWindowView3d(object):
223
223
 
224
224
  # Wireframeスタイルの場合は原子を描画しない
225
225
  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)
226
+ # Stickモードで末端二重結合・三重結合の原子を分裂させるための処理
227
+ if self.current_3d_style == 'stick':
228
+ # 末端原子(次数1)で多重結合を持つものを検出
229
+ split_atoms = [] # (atom_idx, bond_order, offset_vecs)
230
+ skip_atoms = set() # スキップする原子のインデックス
231
+
232
+ for i in range(mol_to_draw.GetNumAtoms()):
233
+ atom = mol_to_draw.GetAtomWithIdx(i)
234
+ if atom.GetDegree() == 1: # 末端原子
235
+ bonds = atom.GetBonds()
236
+ if len(bonds) == 1:
237
+ bond = bonds[0]
238
+ bond_type = bond.GetBondType()
239
+
240
+ if bond_type in [Chem.BondType.DOUBLE, Chem.BondType.TRIPLE]:
241
+ # 多重結合を持つ末端原子を発見
242
+ # 結合のもう一方の原子を取得
243
+ other_idx = bond.GetBeginAtomIdx() if bond.GetEndAtomIdx() == i else bond.GetEndAtomIdx()
244
+
245
+ # 結合ベクトルを計算
246
+ pos_i = np.array(conf.GetAtomPosition(i))
247
+ pos_other = np.array(conf.GetAtomPosition(other_idx))
248
+ bond_vec = pos_i - pos_other
249
+ bond_length = np.linalg.norm(bond_vec)
250
+
251
+ if bond_length > 0:
252
+ bond_unit = bond_vec / bond_length
253
+
254
+ # 二重結合の場合は実際の描画と同じオフセット方向を使用
255
+ if bond_type == Chem.BondType.DOUBLE:
256
+ offset_dir1 = self._calculate_double_bond_offset(mol_to_draw, bond, conf)
257
+ else:
258
+ # 三重結合の場合は結合描画と同じロジック
259
+ v_arb = np.array([0, 0, 1])
260
+ if np.allclose(np.abs(np.dot(bond_unit, v_arb)), 1.0):
261
+ v_arb = np.array([0, 1, 0])
262
+ offset_dir1 = np.cross(bond_unit, v_arb)
263
+ offset_dir1 /= np.linalg.norm(offset_dir1)
264
+
265
+ # 二重/三重結合描画のオフセット値と半径を取得(結合描画と完全に一致させる)
266
+ try:
267
+ cyl_radius = self.settings.get('stick_bond_radius', 0.15)
268
+ if bond_type == Chem.BondType.DOUBLE:
269
+ radius_factor = self.settings.get('stick_double_bond_radius_factor', 0.60)
270
+ offset_factor = self.settings.get('stick_double_bond_offset_factor', 1.5)
271
+ # 二重結合:s_double / 2 を使用
272
+ offset_distance = cyl_radius * offset_factor / 2
273
+ else: # TRIPLE
274
+ radius_factor = self.settings.get('stick_triple_bond_radius_factor', 0.40)
275
+ offset_factor = self.settings.get('stick_triple_bond_offset_factor', 1.0)
276
+ # 三重結合:s_triple をそのまま使用(/ 2 なし)
277
+ offset_distance = cyl_radius * offset_factor
278
+
279
+ # 結合描画と同じ計算
280
+ sphere_radius = cyl_radius * radius_factor
281
+ except:
282
+ sphere_radius = 0.09 # デフォルト値
283
+ offset_distance = 0.15 # デフォルト値
284
+
285
+ if bond_type == Chem.BondType.DOUBLE:
286
+ # 二重結合:2個に分裂
287
+ offset_vecs = [
288
+ offset_dir1 * offset_distance,
289
+ -offset_dir1 * offset_distance
290
+ ]
291
+ split_atoms.append((i, 2, offset_vecs))
292
+ else: # TRIPLE
293
+ # 三重結合:3個に分裂(中心 + 両側2つ)
294
+ # 結合描画と同じ配置
295
+ offset_vecs = [
296
+ np.array([0, 0, 0]), # 中心
297
+ offset_dir1 * offset_distance, # +side
298
+ -offset_dir1 * offset_distance # -side
299
+ ]
300
+ split_atoms.append((i, 3, offset_vecs))
301
+
302
+ skip_atoms.add(i)
303
+
304
+ # 分裂させる原子がある場合、新しい位置リストを作成
305
+ if split_atoms:
306
+ new_positions = []
307
+ new_colors = []
308
+ new_radii = []
309
+
310
+ # 通常の原子を追加(スキップリスト以外)
311
+ for i in range(len(self.atom_positions_3d)):
312
+ if i not in skip_atoms:
313
+ new_positions.append(self.atom_positions_3d[i])
314
+ new_colors.append(col[i])
315
+ new_radii.append(rad[i])
316
+
317
+ # 分裂した原子を追加
318
+ # 上記で計算されたsphere_radiusを使用(結合描画のradius_factorを適用済み)
319
+ for atom_idx, bond_order, offset_vecs in split_atoms:
320
+ pos = self.atom_positions_3d[atom_idx]
321
+ # この原子の結合から半径を取得(上記ループで計算済み)
322
+ # 簡便のため、最後に計算されたsphere_radiusを使用
323
+ for offset_vec in offset_vecs:
324
+ new_positions.append(pos + offset_vec)
325
+ new_colors.append(col[atom_idx])
326
+ new_radii.append(sphere_radius)
327
+
328
+ # PolyDataを新しい位置で作成
329
+ glyph_source = pv.PolyData(np.array(new_positions))
330
+ glyph_source['colors'] = np.array(new_colors)
331
+ glyph_source['radii'] = np.array(new_radii)
332
+ else:
333
+ glyph_source = self.glyph_source
334
+ else:
335
+ glyph_source = self.glyph_source
336
+
337
+ glyphs = glyph_source.glyph(scale='radii', geom=pv.Sphere(radius=1.0, theta_resolution=resolution, phi_resolution=resolution), orient=False)
227
338
 
228
339
  if is_lighting_enabled:
229
340
  self.atom_actor = self.plotter.add_mesh(glyphs, scalars='colors', rgb=True, **mesh_props)
@@ -254,60 +365,72 @@ class MainWindowView3d(object):
254
365
  cyl_radius = self.settings.get('ball_stick_bond_radius', 0.1)
255
366
  bond_resolution = self.settings.get('ball_stick_resolution', 16)
256
367
 
257
- bond_counter = 0 # 結合の個別識別用
258
-
259
- # Ball and Stick用のシリンダーリストを準備(高速化のため)
368
+ # Ball and Stick用の共通色
369
+ bs_bond_rgb = [127, 127, 127]
260
370
  if self.current_3d_style == 'ball_and_stick':
261
- bond_cylinders = []
262
- # Compute the configured grey/uniform bond color for Ball & Stick
263
371
  try:
264
372
  bs_hex = self.settings.get('ball_stick_bond_color', '#7F7F7F')
265
373
  q = QColor(bs_hex)
266
374
  bs_bond_rgb = [q.red(), q.green(), q.blue()]
267
375
  except Exception:
268
- bs_bond_rgb = [127, 127, 127]
376
+ pass
377
+
378
+ # バッチ処理用のリスト
379
+ all_points = []
380
+ all_lines = []
381
+ all_radii = []
382
+ all_colors = [] # Cell data (one per line segment)
269
383
 
384
+ current_point_idx = 0
385
+ bond_counter = 0
386
+
270
387
  for bond in mol_to_draw.GetBonds():
271
388
  begin_atom_idx = bond.GetBeginAtomIdx()
272
389
  end_atom_idx = bond.GetEndAtomIdx()
273
390
  sp = np.array(conf.GetAtomPosition(begin_atom_idx))
274
391
  ep = np.array(conf.GetAtomPosition(end_atom_idx))
275
392
  bt = bond.GetBondType()
276
- c = (sp + ep) / 2
277
393
  d = ep - sp
278
394
  h = np.linalg.norm(d)
279
395
  if h == 0: continue
280
396
 
281
- # ボンドの色を原子の色から決定(各半分で異なる色)
397
+ # ボンドの色
282
398
  begin_color = col[begin_atom_idx]
283
399
  end_color = col[end_atom_idx]
284
-
285
- # 結合の色情報を記録
286
400
  begin_color_rgb = [int(c * 255) for c in begin_color]
287
401
  end_color_rgb = [int(c * 255) for c in end_color]
288
402
 
289
- # UI応答性維持のためイベント処理
403
+ # セグメント追加用ヘルパー関数
404
+ def add_segment(p1, p2, radius, color_rgb):
405
+ nonlocal current_point_idx
406
+ all_points.append(p1)
407
+ all_points.append(p2)
408
+ all_lines.append([2, current_point_idx, current_point_idx + 1])
409
+ all_radii.append(radius)
410
+ all_radii.append(radius)
411
+ all_colors.append(color_rgb)
412
+ current_point_idx += 2
413
+
290
414
  QApplication.processEvents()
415
+
416
+ # Get CPK bond color setting once for all bond types
417
+ use_cpk_bond = self.settings.get('ball_stick_use_cpk_bond_color', False)
418
+
291
419
  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)
420
+ if self.current_3d_style == 'ball_and_stick' and not use_cpk_bond:
421
+ # 単一セグメント (Uniform color)
422
+ add_segment(sp, ep, cyl_radius, bs_bond_rgb)
423
+ self._3d_color_map[f'bond_{bond_counter}'] = bs_bond_rgb
297
424
  else:
298
- # その他(stick, wireframe)は中央で色が変わる2つの円柱
425
+ # 分割セグメント (CPK split colors)
299
426
  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)
427
+ add_segment(sp, mid_point, cyl_radius, begin_color_rgb)
428
+ add_segment(mid_point, ep, cyl_radius, end_color_rgb)
304
429
  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
430
  self._3d_color_map[f'bond_{bond_counter}_end'] = end_color_rgb
431
+
310
432
  else:
433
+ # 多重結合のパラメータ計算
311
434
  v1 = d / h
312
435
  # モデルごとの半径ファクターを適用
313
436
  if self.current_3d_style == 'ball_and_stick':
@@ -322,7 +445,7 @@ class MainWindowView3d(object):
322
445
  else:
323
446
  double_radius_factor = 1.0
324
447
  triple_radius_factor = 0.75
325
- r = cyl_radius * 0.8 # fallback, will be overridden below
448
+
326
449
  # 設定からオフセットファクターを取得(モデルごと)
327
450
  if self.current_3d_style == 'ball_and_stick':
328
451
  double_offset_factor = self.settings.get('ball_stick_double_bond_offset_factor', 2.0)
@@ -336,111 +459,208 @@ class MainWindowView3d(object):
336
459
  else:
337
460
  double_offset_factor = 2.0
338
461
  triple_offset_factor = 2.0
339
- s = cyl_radius * 2.0 # デフォルト値
340
462
 
341
463
  if bt == Chem.rdchem.BondType.DOUBLE:
342
464
  r = cyl_radius * double_radius_factor
343
- # 二重結合の場合、結合している原子の他の結合を考慮してオフセット方向を決定
344
465
  off_dir = self._calculate_double_bond_offset(mol_to_draw, bond, conf)
345
- # 設定から二重結合のオフセットファクターを適用
346
466
  s_double = cyl_radius * double_offset_factor
347
- c1, c2 = c + off_dir * (s_double / 2), c - off_dir * (s_double / 2)
348
467
 
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])
468
+ p1_start = sp + off_dir * (s_double / 2)
469
+ p1_end = ep + off_dir * (s_double / 2)
470
+ p2_start = sp - off_dir * (s_double / 2)
471
+ p2_end = ep - off_dir * (s_double / 2)
472
+
473
+ if self.current_3d_style == 'ball_and_stick' and not use_cpk_bond:
474
+ add_segment(p1_start, p1_end, r, bs_bond_rgb)
475
+ add_segment(p2_start, p2_end, r, bs_bond_rgb)
354
476
  self._3d_color_map[f'bond_{bond_counter}_1'] = bs_bond_rgb
355
477
  self._3d_color_map[f'bond_{bond_counter}_2'] = bs_bond_rgb
356
478
  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)
479
+ mid1 = (p1_start + p1_end) / 2
480
+ mid2 = (p2_start + p2_end) / 2
481
+ add_segment(p1_start, mid1, r, begin_color_rgb)
482
+ add_segment(mid1, p1_end, r, end_color_rgb)
483
+ add_segment(p2_start, mid2, r, begin_color_rgb)
484
+ add_segment(mid2, p2_end, r, end_color_rgb)
365
485
  self._3d_color_map[f'bond_{bond_counter}_1_start'] = begin_color_rgb
366
486
  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
487
  self._3d_color_map[f'bond_{bond_counter}_2_start'] = begin_color_rgb
374
488
  self._3d_color_map[f'bond_{bond_counter}_2_end'] = end_color_rgb
489
+
375
490
  elif bt == Chem.rdchem.BondType.TRIPLE:
376
491
  r = cyl_radius * triple_radius_factor
377
- # 三重結合
378
492
  v_arb = np.array([0, 0, 1])
379
493
  if np.allclose(np.abs(np.dot(v1, v_arb)), 1.0): v_arb = np.array([0, 1, 0])
380
494
  off_dir = np.cross(v1, v_arb)
381
495
  off_dir /= np.linalg.norm(off_dir)
382
-
383
- # 設定から三重結合のオフセットファクターを適用
384
496
  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])
497
+
498
+ # Center
499
+ if self.current_3d_style == 'ball_and_stick' and not use_cpk_bond:
500
+ add_segment(sp, ep, r, bs_bond_rgb)
392
501
  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
502
  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)
503
+ mid = (sp + ep) / 2
504
+ add_segment(sp, mid, r, begin_color_rgb)
505
+ add_segment(mid, ep, r, end_color_rgb)
404
506
  self._3d_color_map[f'bond_{bond_counter}_1_start'] = begin_color_rgb
405
507
  self._3d_color_map[f'bond_{bond_counter}_1_end'] = end_color_rgb
508
+
509
+ # Sides
510
+ for sign in [1, -1]:
511
+ offset = off_dir * s_triple * sign
512
+ p_start = sp + offset
513
+ p_end = ep + offset
406
514
 
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
515
+ if self.current_3d_style == 'ball_and_stick' and not use_cpk_bond:
516
+ add_segment(p_start, p_end, r, bs_bond_rgb)
517
+ suffix = '_2' if sign == 1 else '_3'
518
+ self._3d_color_map[f'bond_{bond_counter}{suffix}'] = bs_bond_rgb
519
+ else:
520
+ mid = (p_start + p_end) / 2
521
+ add_segment(p_start, mid, r, begin_color_rgb)
522
+ add_segment(mid, p_end, r, end_color_rgb)
523
+ suffix = '_2' if sign == 1 else '_3'
524
+ self._3d_color_map[f'bond_{bond_counter}{suffix}_start'] = begin_color_rgb
525
+ self._3d_color_map[f'bond_{bond_counter}{suffix}_end'] = end_color_rgb
422
526
 
423
527
  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()
528
+
529
+ # ジオメトリの生成と描画
530
+ if all_points:
531
+ # Create PolyData
532
+ bond_pd = pv.PolyData(np.array(all_points), lines=np.hstack(all_lines))
533
+ # lines needs to be a flat array with padding indicating number of points per cell
534
+ # all_lines is [[2, i, j], [2, k, l], ...], flatten it
430
535
 
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)
536
+ # Add data
537
+ bond_pd.point_data['radii'] = np.array(all_radii)
538
+
539
+ # Convert colors to 0-1 range for PyVista if needed, but add_mesh with rgb=True expects uint8 if using direct array?
540
+ # Actually pyvista scalars usually prefer float 0-1 or uint8 0-255.
541
+ # Let's use uint8 0-255 and rgb=True.
542
+ bond_pd.cell_data['colors'] = np.array(all_colors, dtype=np.uint8)
543
+
544
+ # Tube filter
545
+ # n_sides (resolution) corresponds to theta_resolution in Cylinder
546
+ tube = bond_pd.tube(scalars='radii', absolute=True, radius_factor=1.0, n_sides=bond_resolution, capping=True)
547
+
548
+ # Add to plotter
549
+ self.plotter.add_mesh(tube, scalars='colors', rgb=True, **mesh_props)
550
+
551
+ # Aromatic ring circles display
552
+ if self.settings.get('display_aromatic_circles_3d', False):
553
+ try:
554
+ ring_info = mol_to_draw.GetRingInfo()
555
+ aromatic_rings = []
556
+
557
+ # Find aromatic rings
558
+ for ring in ring_info.AtomRings():
559
+ # Check if all atoms in ring are aromatic
560
+ is_aromatic = all(mol_to_draw.GetAtomWithIdx(idx).GetIsAromatic() for idx in ring)
561
+ if is_aromatic:
562
+ aromatic_rings.append(ring)
441
563
 
442
- # まとめて色情報を記録
443
- self._3d_color_map['bonds_combined'] = bs_bond_rgb
564
+ # Draw circles for aromatic rings
565
+ for ring in aromatic_rings:
566
+ # Get atom positions
567
+ ring_positions = [self.atom_positions_3d[idx] for idx in ring]
568
+ ring_positions_np = np.array(ring_positions)
569
+
570
+ # Calculate ring center
571
+ center = np.mean(ring_positions_np, axis=0)
572
+
573
+ # Calculate ring normal using PCA or cross product
574
+ # Use first 3 atoms to get two vectors
575
+ if len(ring) >= 3:
576
+ v1 = ring_positions_np[1] - ring_positions_np[0]
577
+ v2 = ring_positions_np[2] - ring_positions_np[0]
578
+ normal = np.cross(v1, v2)
579
+ normal_length = np.linalg.norm(normal)
580
+ if normal_length > 0:
581
+ normal = normal / normal_length
582
+ else:
583
+ normal = np.array([0, 0, 1])
584
+ else:
585
+ normal = np.array([0, 0, 1])
586
+
587
+ # Calculate ring radius (average distance from center)
588
+ distances = [np.linalg.norm(pos - center) for pos in ring_positions_np]
589
+ ring_radius = np.mean(distances) * 0.55 # Slightly smaller
590
+
591
+ # Get bond radius from current style settings for torus thickness
592
+ if self.current_3d_style == 'stick':
593
+ bond_radius = self.settings.get('stick_bond_radius', 0.15)
594
+ elif self.current_3d_style == 'ball_and_stick':
595
+ bond_radius = self.settings.get('ball_stick_bond_radius', 0.1)
596
+ elif self.current_3d_style == 'wireframe':
597
+ bond_radius = self.settings.get('wireframe_bond_radius', 0.01)
598
+ else:
599
+ bond_radius = 0.1 # Default
600
+ # Apply user-defined thickness factor (default 0.6)
601
+ thickness_factor = self.settings.get('aromatic_torus_thickness_factor', 0.6)
602
+ tube_radius = bond_radius * thickness_factor
603
+ theta = np.linspace(0, 2.2 * np.pi, 64)
604
+ circle_x = ring_radius * np.cos(theta)
605
+ circle_y = ring_radius * np.sin(theta)
606
+ circle_z = np.zeros_like(theta)
607
+ circle_points = np.c_[circle_x, circle_y, circle_z]
608
+
609
+ # Create line from points
610
+ circle_line = pv.Spline(circle_points, n_points=64).tube(radius=tube_radius, n_sides=16)
611
+
612
+ # Rotate torus to align with ring plane
613
+ # Default torus is in XY plane (normal = [0, 0, 1])
614
+ default_normal = np.array([0, 0, 1])
615
+
616
+ # Calculate rotation axis and angle
617
+ if not np.allclose(normal, default_normal) and not np.allclose(normal, -default_normal):
618
+ axis = np.cross(default_normal, normal)
619
+ axis_length = np.linalg.norm(axis)
620
+ if axis_length > 0:
621
+ axis = axis / axis_length
622
+ angle = np.arccos(np.clip(np.dot(default_normal, normal), -1.0, 1.0))
623
+ angle_deg = np.degrees(angle)
624
+
625
+ # Rotate torus
626
+ circle_line = circle_line.rotate_vector(axis, angle_deg, point=[0, 0, 0])
627
+
628
+ # Translate to ring center
629
+ circle_line = circle_line.translate(center)
630
+
631
+ # Get torus color from bond color settings
632
+ # Calculate most common atom type in ring for CPK color
633
+ from collections import Counter
634
+ atom_symbols = [mol_to_draw.GetAtomWithIdx(idx).GetSymbol() for idx in ring]
635
+ most_common_symbol = Counter(atom_symbols).most_common(1)[0][0] if atom_symbols else None
636
+
637
+ if self.current_3d_style == 'ball_and_stick':
638
+ # Check if using CPK bond colors
639
+ use_cpk = self.settings.get('ball_stick_use_cpk_bond_color', False)
640
+ if use_cpk:
641
+ # Use CPK color of most common atom type in ring
642
+ if most_common_symbol:
643
+ cpk_color = CPK_COLORS_PV.get(most_common_symbol, [0.5, 0.5, 0.5])
644
+ torus_color = cpk_color
645
+ else:
646
+ torus_color = [0.5, 0.5, 0.5]
647
+ else:
648
+ # Use Ball & Stick bond color setting
649
+ bond_hex = self.settings.get('ball_stick_bond_color', '#7F7F7F')
650
+ q = QColor(bond_hex)
651
+ torus_color = [q.red() / 255.0, q.green() / 255.0, q.blue() / 255.0]
652
+ else:
653
+ # For Wireframe and Stick, use CPK color of most common atom
654
+ if most_common_symbol:
655
+ cpk_color = CPK_COLORS_PV.get(most_common_symbol, [0.5, 0.5, 0.5])
656
+ torus_color = cpk_color
657
+ else:
658
+ torus_color = [0.5, 0.5, 0.5]
659
+
660
+ self.plotter.add_mesh(circle_line, color=torus_color, **mesh_props)
661
+
662
+ except Exception as e:
663
+ print(f"Error rendering aromatic circles: {e}")
444
664
 
445
665
  if getattr(self, 'show_chiral_labels', False):
446
666
  try:
@@ -463,7 +683,7 @@ class MainWindowView3d(object):
463
683
  # If we drew a kekulized molecule use it for E/Z detection so
464
684
  # E/Z labels reflect Kekulé rendering; pass mol_to_draw as the
465
685
  # molecule to scan for bond stereochemistry.
466
- self.show_ez_labels_3d(mol, scan_mol=mol_to_draw)
686
+ self.show_ez_labels_3d(mol)
467
687
  except Exception as e:
468
688
  self.statusBar().showMessage(f"3D E/Z label drawing error: {e}")
469
689
 
@@ -592,7 +812,7 @@ class MainWindowView3d(object):
592
812
 
593
813
 
594
814
 
595
- def show_ez_labels_3d(self, mol, scan_mol=None):
815
+ def show_ez_labels_3d(self, mol):
596
816
  """3DビューでE/Zラベルを表示する(RDKitのステレオ化学判定を使用)"""
597
817
  if not mol:
598
818
  return
@@ -611,33 +831,40 @@ class MainWindowView3d(object):
611
831
 
612
832
  conf = mol.GetConformer()
613
833
 
614
- # RDKitに3D座標からステレオ化学を計算させる
834
+ # 二重結合でRDKitが判定したE/Z立体化学を表示
835
+
615
836
  try:
616
- # 3D座標からステレオ化学を再計算
837
+ # 3D座標からステレオ化学を再計算 (molに対して行う)
838
+ # これにより、2Dでの描画状態に関わらず、現在の3D座標に基づいたE/Z判定が行われる
617
839
  Chem.AssignStereochemistry(mol, cleanIt=True, force=True, flagPossibleStereoCenters=True)
618
840
  except:
619
841
  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
842
 
627
- for bond in scan_mol.GetBonds():
843
+ for bond in mol.GetBonds():
628
844
  if bond.GetBondType() == Chem.BondType.DOUBLE:
629
- stereo = bond.GetStereo()
630
- if stereo in [Chem.BondStereo.STEREOE, Chem.BondStereo.STEREOZ]:
845
+ new_stereo = bond.GetStereo()
846
+
847
+ if new_stereo in [Chem.BondStereo.STEREOE, Chem.BondStereo.STEREOZ]:
631
848
  # 結合の中心座標を計算
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
849
  begin_pos = np.array(conf.GetAtomPosition(bond.GetBeginAtomIdx()))
636
850
  end_pos = np.array(conf.GetAtomPosition(bond.GetEndAtomIdx()))
637
851
  center_pos = (begin_pos + end_pos) / 2
638
852
 
639
- # RDKitの判定結果を使用
640
- label = 'E' if stereo == Chem.BondStereo.STEREOE else 'Z'
853
+ # ラベルの決定
854
+ label = 'E' if new_stereo == Chem.BondStereo.STEREOE else 'Z'
855
+
856
+ # 2Dとの不一致チェック
857
+ # main_window_compute.py で保存された2D由来の立体化学プロパティを取得
858
+ try:
859
+ old_stereo = bond.GetIntProp("_original_2d_stereo")
860
+ except KeyError:
861
+ old_stereo = Chem.BondStereo.STEREONONE
862
+
863
+ # 2D側でもE/Zが指定されていて、かつ3Dと異なる場合は「?」にする
864
+ if old_stereo in [Chem.BondStereo.STEREOE, Chem.BondStereo.STEREOZ]:
865
+ if old_stereo != new_stereo:
866
+ label = '?'
867
+
641
868
  pts.append(center_pos)
642
869
  labels.append(label)
643
870
 
@@ -7,7 +7,7 @@ MoleditPy — A Python-based molecular editing software
7
7
  Author: Hiromichi Yokoyama
8
8
  License: GPL-3.0 license
9
9
  Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
- DOI 10.5281/zenodo.17268532
10
+ DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
13
  """
@@ -7,7 +7,7 @@ MoleditPy — A Python-based molecular editing software
7
7
  Author: Hiromichi Yokoyama
8
8
  License: GPL-3.0 license
9
9
  Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
- DOI 10.5281/zenodo.17268532
10
+ DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
13
  from PyQt6.QtWidgets import (
@@ -7,7 +7,7 @@ MoleditPy — A Python-based molecular editing software
7
7
  Author: Hiromichi Yokoyama
8
8
  License: GPL-3.0 license
9
9
  Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
- DOI 10.5281/zenodo.17268532
10
+ DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
13
  from rdkit import Chem