MoleditPy-linux 2.1.1__py3-none-any.whl → 2.2.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.
@@ -120,8 +120,25 @@ class MainWindowView3d(object):
120
120
 
121
121
 
122
122
  def draw_molecule_3d(self, mol):
123
+ """Dispatch to custom style or standard drawing."""
124
+ mw = self.mw if hasattr(self, 'mw') else self
125
+
126
+ if hasattr(mw, 'plugin_manager') and hasattr(mw.plugin_manager, 'custom_3d_styles'):
127
+ if hasattr(self, 'current_3d_style') and self.current_3d_style in mw.plugin_manager.custom_3d_styles:
128
+ handler = mw.plugin_manager.custom_3d_styles[self.current_3d_style]['callback']
129
+ try:
130
+ handler(mw, mol)
131
+ return
132
+ except Exception as e:
133
+ logging.error(f"Error in custom 3d style '{self.current_3d_style}': {e}")
134
+
135
+ self.draw_standard_3d_style(mol)
136
+
137
+ def draw_standard_3d_style(self, mol, style_override=None):
123
138
  """3D 分子を描画し、軸アクターの参照をクリアする(軸の再制御は apply_3d_settings に任せる)"""
124
139
 
140
+ current_style = style_override if style_override else self.current_3d_style
141
+
125
142
  # 測定選択をクリア(分子が変更されたため)
126
143
  if hasattr(self, 'measurement_mode'):
127
144
  self.clear_measurement_selection()
@@ -192,16 +209,26 @@ class MainWindowView3d(object):
192
209
  sym = [a.GetSymbol() for a in mol_to_draw.GetAtoms()]
193
210
  col = np.array([CPK_COLORS_PV.get(s, [0.5, 0.5, 0.5]) for s in sym])
194
211
 
212
+ # Apply plugin color overrides
213
+ if hasattr(self, '_plugin_color_overrides') and self._plugin_color_overrides:
214
+ for atom_idx, hex_color in self._plugin_color_overrides.items():
215
+ if 0 <= atom_idx < len(col):
216
+ try:
217
+ c = QColor(hex_color)
218
+ col[atom_idx] = [c.redF(), c.greenF(), c.blueF()]
219
+ except Exception:
220
+ pass
221
+
195
222
  # スタイルに応じて原子の半径を設定(設定から読み込み)
196
- if self.current_3d_style == 'cpk':
223
+ if current_style == 'cpk':
197
224
  atom_scale = self.settings.get('cpk_atom_scale', 1.0)
198
225
  resolution = self.settings.get('cpk_resolution', 32)
199
226
  rad = np.array([pt.GetRvdw(pt.GetAtomicNumber(s)) * atom_scale for s in sym])
200
- elif self.current_3d_style == 'wireframe':
227
+ elif current_style == 'wireframe':
201
228
  # Wireframeでは原子を描画しないので、この設定は実際には使用されない
202
229
  resolution = self.settings.get('wireframe_resolution', 6)
203
230
  rad = np.array([0.01 for s in sym]) # 極小値(使用されない)
204
- elif self.current_3d_style == 'stick':
231
+ elif current_style == 'stick':
205
232
  atom_radius = self.settings.get('stick_bond_radius', 0.15) # Use bond radius for atoms
206
233
  resolution = self.settings.get('stick_resolution', 16)
207
234
  rad = np.array([atom_radius for s in sym])
@@ -223,9 +250,9 @@ class MainWindowView3d(object):
223
250
  )
224
251
 
225
252
  # Wireframeスタイルの場合は原子を描画しない
226
- if self.current_3d_style != 'wireframe':
253
+ if current_style != 'wireframe':
227
254
  # Stickモードで末端二重結合・三重結合の原子を分裂させるための処理
228
- if self.current_3d_style == 'stick':
255
+ if current_style == 'stick':
229
256
  # 末端原子(次数1)で多重結合を持つものを検出
230
257
  split_atoms = [] # (atom_idx, bond_order, offset_vecs)
231
258
  skip_atoms = set() # スキップする原子のインデックス
@@ -354,12 +381,12 @@ class MainWindowView3d(object):
354
381
 
355
382
 
356
383
  # ボンドの描画(ball_and_stick、wireframe、stickで描画)
357
- if self.current_3d_style in ['ball_and_stick', 'wireframe', 'stick']:
384
+ if current_style in ['ball_and_stick', 'wireframe', 'stick']:
358
385
  # スタイルに応じてボンドの太さと解像度を設定(設定から読み込み)
359
- if self.current_3d_style == 'wireframe':
386
+ if current_style == 'wireframe':
360
387
  cyl_radius = self.settings.get('wireframe_bond_radius', 0.01)
361
388
  bond_resolution = self.settings.get('wireframe_resolution', 6)
362
- elif self.current_3d_style == 'stick':
389
+ elif current_style == 'stick':
363
390
  cyl_radius = self.settings.get('stick_bond_radius', 0.15)
364
391
  bond_resolution = self.settings.get('stick_resolution', 16)
365
392
  else: # ball_and_stick
@@ -368,7 +395,7 @@ class MainWindowView3d(object):
368
395
 
369
396
  # Ball and Stick用の共通色
370
397
  bs_bond_rgb = [127, 127, 127]
371
- if self.current_3d_style == 'ball_and_stick':
398
+ if current_style == 'ball_and_stick':
372
399
  try:
373
400
  bs_hex = self.settings.get('ball_stick_bond_color', '#7F7F7F')
374
401
  q = QColor(bs_hex)
@@ -401,6 +428,28 @@ class MainWindowView3d(object):
401
428
  begin_color_rgb = [int(c * 255) for c in begin_color]
402
429
  end_color_rgb = [int(c * 255) for c in end_color]
403
430
 
431
+ # Check for plugin override
432
+ bond_idx = bond.GetIdx()
433
+ # Override handling: if set, force both ends and uniform color to this value
434
+ if hasattr(self, '_plugin_bond_color_overrides') and bond_idx in self._plugin_bond_color_overrides:
435
+ try:
436
+ # Expecting hex string
437
+ hex_c = self._plugin_bond_color_overrides[bond_idx]
438
+ c_obj = QColor(hex_c)
439
+ ov_rgb = [c_obj.red(), c_obj.green(), c_obj.blue()]
440
+ begin_color_rgb = ov_rgb
441
+ end_color_rgb = ov_rgb
442
+ # Also override uniform color in case style uses it
443
+ # We need to use a local variable for this iteration instead of the global bs_bond_rgb
444
+ # But wait, bs_bond_rgb is defined outside loop.
445
+ # We can define local_bs_bond_rgb
446
+ except Exception:
447
+ pass
448
+
449
+ # Determine effective uniform color for this bond
450
+ local_bs_bond_rgb = begin_color_rgb if (hasattr(self, '_plugin_bond_color_overrides') and bond_idx in self._plugin_bond_color_overrides) else bs_bond_rgb
451
+
452
+
404
453
  # セグメント追加用ヘルパー関数
405
454
  def add_segment(p1, p2, radius, color_rgb):
406
455
  nonlocal current_point_idx
@@ -416,14 +465,20 @@ class MainWindowView3d(object):
416
465
 
417
466
  # Get CPK bond color setting once for all bond types
418
467
  use_cpk_bond = self.settings.get('ball_stick_use_cpk_bond_color', False)
468
+ # If overwritten, treat as if we want to show that color (effectively behave like CPK_Split but with same color, or Uniform).
469
+ # To be robust, if overwritten, we can force "use_cpk_bond" logic but with our same colors?
470
+ # Actually, if overridden, we probably want the whole bond to be that color.
471
+
472
+ is_overridden = hasattr(self, '_plugin_bond_color_overrides') and bond_idx in self._plugin_bond_color_overrides
419
473
 
420
474
  if bt == Chem.rdchem.BondType.SINGLE or bt == Chem.rdchem.BondType.AROMATIC:
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
475
+ if current_style == 'ball_and_stick' and not use_cpk_bond and not is_overridden:
476
+ # 単一セグメント (Uniform color) - Default behavior
477
+ add_segment(sp, ep, cyl_radius, local_bs_bond_rgb)
478
+ self._3d_color_map[f'bond_{bond_counter}'] = local_bs_bond_rgb
425
479
  else:
426
- # 分割セグメント (CPK split colors)
480
+ # 分割セグメント (CPK split colors OR Overridden uniform)
481
+ # If overridden, begin/end are same, so this produces a uniform looking bond split in middle
427
482
  mid_point = (sp + ep) / 2
428
483
  add_segment(sp, mid_point, cyl_radius, begin_color_rgb)
429
484
  add_segment(mid_point, ep, cyl_radius, end_color_rgb)
@@ -434,13 +489,13 @@ class MainWindowView3d(object):
434
489
  # 多重結合のパラメータ計算
435
490
  v1 = d / h
436
491
  # モデルごとの半径ファクターを適用
437
- if self.current_3d_style == 'ball_and_stick':
492
+ if current_style == 'ball_and_stick':
438
493
  double_radius_factor = self.settings.get('ball_stick_double_bond_radius_factor', 0.8)
439
494
  triple_radius_factor = self.settings.get('ball_stick_triple_bond_radius_factor', 0.75)
440
- elif self.current_3d_style == 'wireframe':
495
+ elif current_style == 'wireframe':
441
496
  double_radius_factor = self.settings.get('wireframe_double_bond_radius_factor', 0.8)
442
497
  triple_radius_factor = self.settings.get('wireframe_triple_bond_radius_factor', 0.75)
443
- elif self.current_3d_style == 'stick':
498
+ elif current_style == 'stick':
444
499
  double_radius_factor = self.settings.get('stick_double_bond_radius_factor', 0.60)
445
500
  triple_radius_factor = self.settings.get('stick_triple_bond_radius_factor', 0.40)
446
501
  else:
@@ -448,13 +503,13 @@ class MainWindowView3d(object):
448
503
  triple_radius_factor = 0.75
449
504
 
450
505
  # 設定からオフセットファクターを取得(モデルごと)
451
- if self.current_3d_style == 'ball_and_stick':
506
+ if current_style == 'ball_and_stick':
452
507
  double_offset_factor = self.settings.get('ball_stick_double_bond_offset_factor', 2.0)
453
508
  triple_offset_factor = self.settings.get('ball_stick_triple_bond_offset_factor', 2.0)
454
- elif self.current_3d_style == 'wireframe':
509
+ elif current_style == 'wireframe':
455
510
  double_offset_factor = self.settings.get('wireframe_double_bond_offset_factor', 3.0)
456
511
  triple_offset_factor = self.settings.get('wireframe_triple_bond_offset_factor', 3.0)
457
- elif self.current_3d_style == 'stick':
512
+ elif current_style == 'stick':
458
513
  double_offset_factor = self.settings.get('stick_double_bond_offset_factor', 1.5)
459
514
  triple_offset_factor = self.settings.get('stick_triple_bond_offset_factor', 1.0)
460
515
  else:
@@ -471,11 +526,11 @@ class MainWindowView3d(object):
471
526
  p2_start = sp - off_dir * (s_double / 2)
472
527
  p2_end = ep - off_dir * (s_double / 2)
473
528
 
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)
477
- self._3d_color_map[f'bond_{bond_counter}_1'] = bs_bond_rgb
478
- self._3d_color_map[f'bond_{bond_counter}_2'] = bs_bond_rgb
529
+ if current_style == 'ball_and_stick' and not use_cpk_bond and not is_overridden:
530
+ add_segment(p1_start, p1_end, r, local_bs_bond_rgb)
531
+ add_segment(p2_start, p2_end, r, local_bs_bond_rgb)
532
+ self._3d_color_map[f'bond_{bond_counter}_1'] = local_bs_bond_rgb
533
+ self._3d_color_map[f'bond_{bond_counter}_2'] = local_bs_bond_rgb
479
534
  else:
480
535
  mid1 = (p1_start + p1_end) / 2
481
536
  mid2 = (p2_start + p2_end) / 2
@@ -497,9 +552,9 @@ class MainWindowView3d(object):
497
552
  s_triple = cyl_radius * triple_offset_factor
498
553
 
499
554
  # Center
500
- if self.current_3d_style == 'ball_and_stick' and not use_cpk_bond:
501
- add_segment(sp, ep, r, bs_bond_rgb)
502
- self._3d_color_map[f'bond_{bond_counter}_1'] = bs_bond_rgb
555
+ if current_style == 'ball_and_stick' and not use_cpk_bond and not is_overridden:
556
+ add_segment(sp, ep, r, local_bs_bond_rgb)
557
+ self._3d_color_map[f'bond_{bond_counter}_1'] = local_bs_bond_rgb
503
558
  else:
504
559
  mid = (sp + ep) / 2
505
560
  add_segment(sp, mid, r, begin_color_rgb)
@@ -513,10 +568,10 @@ class MainWindowView3d(object):
513
568
  p_start = sp + offset
514
569
  p_end = ep + offset
515
570
 
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)
571
+ if current_style == 'ball_and_stick' and not use_cpk_bond and not is_overridden:
572
+ add_segment(p_start, p_end, r, local_bs_bond_rgb)
518
573
  suffix = '_2' if sign == 1 else '_3'
519
- self._3d_color_map[f'bond_{bond_counter}{suffix}'] = bs_bond_rgb
574
+ self._3d_color_map[f'bond_{bond_counter}{suffix}'] = local_bs_bond_rgb
520
575
  else:
521
576
  mid = (p_start + p_end) / 2
522
577
  add_segment(p_start, mid, r, begin_color_rgb)
@@ -590,11 +645,11 @@ class MainWindowView3d(object):
590
645
  ring_radius = np.mean(distances) * 0.55 # Slightly smaller
591
646
 
592
647
  # Get bond radius from current style settings for torus thickness
593
- if self.current_3d_style == 'stick':
648
+ if current_style == 'stick':
594
649
  bond_radius = self.settings.get('stick_bond_radius', 0.15)
595
- elif self.current_3d_style == 'ball_and_stick':
650
+ elif current_style == 'ball_and_stick':
596
651
  bond_radius = self.settings.get('ball_stick_bond_radius', 0.1)
597
- elif self.current_3d_style == 'wireframe':
652
+ elif current_style == 'wireframe':
598
653
  bond_radius = self.settings.get('wireframe_bond_radius', 0.01)
599
654
  else:
600
655
  bond_radius = 0.1 # Default
@@ -635,7 +690,7 @@ class MainWindowView3d(object):
635
690
  atom_symbols = [mol_to_draw.GetAtomWithIdx(idx).GetSymbol() for idx in ring]
636
691
  most_common_symbol = Counter(atom_symbols).most_common(1)[0][0] if atom_symbols else None
637
692
 
638
- if self.current_3d_style == 'ball_and_stick':
693
+ if current_style == 'ball_and_stick':
639
694
  # Check if using CPK bond colors
640
695
  use_cpk = self.settings.get('ball_stick_use_cpk_bond_color', False)
641
696
  if use_cpk:
@@ -1446,3 +1501,35 @@ class MainWindowView3d(object):
1446
1501
 
1447
1502
 
1448
1503
 
1504
+
1505
+ def update_bond_color_override(self, bond_idx, hex_color):
1506
+ """Plugin API helper to override bond color."""
1507
+ if not hasattr(self, '_plugin_bond_color_overrides'):
1508
+ self._plugin_bond_color_overrides = {}
1509
+
1510
+ if hex_color is None:
1511
+ if bond_idx in self._plugin_bond_color_overrides:
1512
+ del self._plugin_bond_color_overrides[bond_idx]
1513
+ else:
1514
+ self._plugin_bond_color_overrides[bond_idx] = hex_color
1515
+
1516
+ if self.current_mol:
1517
+ self.draw_molecule_3d(self.current_mol)
1518
+
1519
+ def update_atom_color_override(self, atom_index, color_hex):
1520
+ """Plugin helper to update specific atom color override."""
1521
+ if not hasattr(self, '_plugin_color_overrides'):
1522
+ self._plugin_color_overrides = {}
1523
+
1524
+ if color_hex is None:
1525
+ if atom_index in self._plugin_color_overrides:
1526
+ del self._plugin_color_overrides[atom_index]
1527
+ else:
1528
+ self._plugin_color_overrides[atom_index] = color_hex
1529
+
1530
+ if self.current_mol:
1531
+ self.draw_molecule_3d(self.current_mol)
1532
+
1533
+
1534
+
1535
+
@@ -1495,24 +1495,16 @@ class MoleculeScene(QGraphicsScene):
1495
1495
  'atoms_data': atoms,
1496
1496
  'attachment_atom': attachment_atom,
1497
1497
  }
1498
- # 既存のプレビューアイテムを一旦クリア
1498
+ # 既存のプレビューアイテムを一旦クリア (レガシーな線画描画の消去)
1499
1499
  for item in list(self.items()):
1500
1500
  if isinstance(item, QGraphicsLineItem) and getattr(item, '_is_template_preview', False):
1501
1501
  self.removeItem(item)
1502
1502
 
1503
- # Draw preview lines only using calculated points (do not access self.data.atoms)
1504
- for bond_info in bonds_info:
1505
- if isinstance(bond_info, (list, tuple)) and len(bond_info) >= 2:
1506
- i, j = bond_info[0], bond_info[1]
1507
- order = bond_info[2] if len(bond_info) > 2 else 1
1508
- # stereo = bond_info[3] if len(bond_info) > 3 else 0
1509
- if i < len(points) and j < len(points):
1510
- line = QGraphicsLineItem(QLineF(points[i], points[j]))
1511
- pen = QPen(Qt.black, 2 if order == 2 else 1)
1512
- line.setPen(pen)
1513
- line._is_template_preview = True # フラグで区別
1514
- self.addItem(line)
1515
- # Never access self.data.atoms here for preview-only atoms
1503
+ # TemplatePreviewItemを使用して高機能なプレビューを描画
1504
+ self.template_preview.set_user_template_geometry(points, bonds_info, atoms)
1505
+ self.template_preview.show()
1506
+ if self.views():
1507
+ self.views()[0].viewport().update()
1516
1508
 
1517
1509
  def leaveEvent(self, event):
1518
1510
  self.template_preview.hide(); super().leaveEvent(event)
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ MoleditPy — A Python-based molecular editing software
6
+
7
+ Author: Hiromichi Yokoyama
8
+ License: GPL-3.0 license
9
+ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
+ DOI: 10.5281/zenodo.17268532
11
+ """
12
+
13
+ from typing import Callable, Optional, Any
14
+
15
+ class PluginContext:
16
+ """
17
+ PluginContext provides a safe interface for plugins to interact with the application.
18
+ It is passed to the `initialize(context)` function of the plugin.
19
+ """
20
+ def __init__(self, manager, plugin_name: str):
21
+ self._manager = manager
22
+ self._plugin_name = plugin_name
23
+
24
+ def add_menu_action(self, path: str, callback: Callable, text: Optional[str] = None, icon: Optional[str] = None, shortcut: Optional[str] = None):
25
+ """
26
+ Register a menu action.
27
+
28
+ Args:
29
+ path: Menu path, e.g., "File/Import", "Edit", or "MyPlugin" (top level).
30
+ callback: Function to call when triggered.
31
+ text: Label for the action (defaults to last part of path if None).
32
+ icon: Path to icon or icon name (optional).
33
+ shortcut: Keyboard shortcut (optional).
34
+ """
35
+ self._manager.register_menu_action(self._plugin_name, path, callback, text, icon, shortcut)
36
+
37
+ def add_toolbar_action(self, callback: Callable, text: str, icon: Optional[str] = None, tooltip: Optional[str] = None):
38
+ """
39
+ Register a toolbar action.
40
+ """
41
+ self._manager.register_toolbar_action(self._plugin_name, callback, text, icon, tooltip)
42
+
43
+ def register_drop_handler(self, callback: Callable[[str], bool], priority: int = 0):
44
+ """
45
+ Register a handler for file drops.
46
+
47
+ Args:
48
+ callback: Function taking (file_path) -> bool. Returns True if handled.
49
+ priority: Higher priority handlers are tried first.
50
+ """
51
+ self._manager.register_drop_handler(self._plugin_name, callback, priority)
52
+
53
+
54
+
55
+ def get_3d_controller(self) -> 'Plugin3DController':
56
+ """
57
+ Returns a controller to manipulate the 3D scene (e.g. colors).
58
+ """
59
+ return Plugin3DController(self._manager.get_main_window())
60
+
61
+ def get_main_window(self) -> Any:
62
+ """
63
+ Returns the raw MainWindow instance.
64
+ Use with caution; prefer specific methods when available.
65
+ """
66
+ return self._manager.get_main_window()
67
+
68
+ def add_export_action(self, label: str, callback: Callable):
69
+ """
70
+ Register a custom export action.
71
+
72
+ Args:
73
+ label: Text to display in the Export menu (e.g., "Export as MyFormat...").
74
+ callback: Function to call when triggered.
75
+ """
76
+ self._manager.register_export_action(self._plugin_name, label, callback)
77
+
78
+ def register_optimization_method(self, method_name: str, callback: Callable[[Any], bool]):
79
+ """
80
+ Register a custom 3D optimization method.
81
+
82
+ Args:
83
+ method_name: Name of the method to display in 3D Optimization menu.
84
+ callback: Function taking (rdkit_mol) -> bool (success).
85
+ Modifies the molecule in-place.
86
+ """
87
+ self._manager.register_optimization_method(self._plugin_name, method_name, callback)
88
+
89
+ def register_file_opener(self, extension: str, callback: Callable[[str], None]):
90
+ """
91
+ Register a handler for opening a specific file extension.
92
+
93
+ Args:
94
+ extension: File extension including dot, e.g. ".xyz".
95
+ callback: Function taking (file_path) -> None.
96
+ Should load the file into the main window.
97
+ """
98
+ self._manager.register_file_opener(self._plugin_name, extension, callback)
99
+
100
+ def register_file_opener(self, extension: str, callback: Callable[[str], None]):
101
+ """
102
+ Register a handler for opening a specific file extension.
103
+
104
+ Args:
105
+ extension: File extension including dot, e.g. ".xyz".
106
+ callback: Function taking (file_path) -> None.
107
+ Should load the file into the main window.
108
+ """
109
+ self._manager.register_file_opener(self._plugin_name, extension, callback)
110
+
111
+ def add_analysis_tool(self, label: str, callback: Callable):
112
+ """
113
+ Register a tool in the Analysis menu.
114
+
115
+ Args:
116
+ label: Text to display in the menu.
117
+ callback: Function to contact when triggered.
118
+ """
119
+ self._manager.register_analysis_tool(self._plugin_name, label, callback)
120
+
121
+ def register_save_handler(self, callback: Callable[[], dict]):
122
+ """
123
+ Register a callback to save state into the project file.
124
+
125
+ Args:
126
+ callback: Function returning a dict of serializable data.
127
+ """
128
+ self._manager.register_save_handler(self._plugin_name, callback)
129
+
130
+ def register_load_handler(self, callback: Callable[[dict], None]):
131
+ """
132
+ Register a callback to restore state from the project file.
133
+
134
+ Args:
135
+ callback: Function receiving the dict of saved data.
136
+ """
137
+ def register_load_handler(self, callback: Callable[[dict], None]):
138
+ """
139
+ Register a callback to restore state from the project file.
140
+
141
+ Args:
142
+ callback: Function receiving the dict of saved data.
143
+ """
144
+ self._manager.register_load_handler(self._plugin_name, callback)
145
+
146
+ def register_3d_context_menu(self, callback: Callable, label: str):
147
+ """Deprecated: This method does nothing. Kept for backward compatibility."""
148
+ print(f"Warning: Plugin '{self._plugin_name}' uses deprecated 'register_3d_context_menu'. This API has been removed.")
149
+
150
+ def register_3d_style(self, style_name: str, callback: Callable[[Any, Any], None]):
151
+ """
152
+ Register a custom 3D rendering style.
153
+
154
+ Args:
155
+ style_name: Name of the style (must be unique).
156
+ callback: Function taking (main_window, mol) -> None.
157
+ Should fully handle drawing the molecule in the 3D view.
158
+ """
159
+ self._manager.register_3d_style(self._plugin_name, style_name, callback)
160
+
161
+
162
+
163
+
164
+
165
+
166
+ class Plugin3DController:
167
+ """Helper to manipulate the 3D scene."""
168
+ def __init__(self, main_window):
169
+ self._mw = main_window
170
+
171
+ def set_atom_color(self, atom_index: int, color_hex: str):
172
+ """
173
+ Set the color of a specific atom in the 3D view.
174
+ Args:
175
+ atom_index: RDKit atom index.
176
+ color_hex: Hex string e.g., "#FF0000".
177
+ """
178
+ # This will need to hook into the actual 3D view logic
179
+ if hasattr(self._mw, 'main_window_view_3d'):
180
+ # Logic to update color map and trigger redraw
181
+ # For now we can assume we might need to expose a method in view_3d
182
+ self._mw.main_window_view_3d.update_atom_color_override(atom_index, color_hex)
183
+ self._mw.plotter.render()
184
+
185
+ def set_bond_color(self, bond_index: int, color_hex: str):
186
+ """
187
+ Set the color of a specific bond in the 3D view.
188
+
189
+ Args:
190
+ bond_index: RDKit bond index.
191
+ color_hex: Hex string e.g., "#00FF00".
192
+ """
193
+ if hasattr(self._mw, 'main_window_view_3d'):
194
+ self._mw.main_window_view_3d.update_bond_color_override(bond_index, color_hex)
195
+ self._mw.plotter.render()