MoleditPy-linux 2.4.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 (59) hide show
  1. moleditpy_linux/__init__.py +17 -0
  2. moleditpy_linux/__main__.py +29 -0
  3. moleditpy_linux/main.py +37 -0
  4. moleditpy_linux/modules/__init__.py +41 -0
  5. moleditpy_linux/modules/about_dialog.py +104 -0
  6. moleditpy_linux/modules/align_plane_dialog.py +292 -0
  7. moleditpy_linux/modules/alignment_dialog.py +272 -0
  8. moleditpy_linux/modules/analysis_window.py +209 -0
  9. moleditpy_linux/modules/angle_dialog.py +440 -0
  10. moleditpy_linux/modules/assets/file_icon.ico +0 -0
  11. moleditpy_linux/modules/assets/icon.icns +0 -0
  12. moleditpy_linux/modules/assets/icon.ico +0 -0
  13. moleditpy_linux/modules/assets/icon.png +0 -0
  14. moleditpy_linux/modules/atom_item.py +395 -0
  15. moleditpy_linux/modules/bond_item.py +464 -0
  16. moleditpy_linux/modules/bond_length_dialog.py +380 -0
  17. moleditpy_linux/modules/calculation_worker.py +766 -0
  18. moleditpy_linux/modules/color_settings_dialog.py +321 -0
  19. moleditpy_linux/modules/constants.py +88 -0
  20. moleditpy_linux/modules/constrained_optimization_dialog.py +678 -0
  21. moleditpy_linux/modules/custom_interactor_style.py +749 -0
  22. moleditpy_linux/modules/custom_qt_interactor.py +102 -0
  23. moleditpy_linux/modules/dialog3_d_picking_mixin.py +141 -0
  24. moleditpy_linux/modules/dihedral_dialog.py +443 -0
  25. moleditpy_linux/modules/main_window.py +850 -0
  26. moleditpy_linux/modules/main_window_app_state.py +787 -0
  27. moleditpy_linux/modules/main_window_compute.py +1242 -0
  28. moleditpy_linux/modules/main_window_dialog_manager.py +460 -0
  29. moleditpy_linux/modules/main_window_edit_3d.py +536 -0
  30. moleditpy_linux/modules/main_window_edit_actions.py +1565 -0
  31. moleditpy_linux/modules/main_window_export.py +917 -0
  32. moleditpy_linux/modules/main_window_main_init.py +2100 -0
  33. moleditpy_linux/modules/main_window_molecular_parsers.py +1044 -0
  34. moleditpy_linux/modules/main_window_project_io.py +434 -0
  35. moleditpy_linux/modules/main_window_string_importers.py +275 -0
  36. moleditpy_linux/modules/main_window_ui_manager.py +602 -0
  37. moleditpy_linux/modules/main_window_view_3d.py +1539 -0
  38. moleditpy_linux/modules/main_window_view_loaders.py +355 -0
  39. moleditpy_linux/modules/mirror_dialog.py +122 -0
  40. moleditpy_linux/modules/molecular_data.py +302 -0
  41. moleditpy_linux/modules/molecule_scene.py +2000 -0
  42. moleditpy_linux/modules/move_group_dialog.py +600 -0
  43. moleditpy_linux/modules/periodic_table_dialog.py +84 -0
  44. moleditpy_linux/modules/planarize_dialog.py +220 -0
  45. moleditpy_linux/modules/plugin_interface.py +215 -0
  46. moleditpy_linux/modules/plugin_manager.py +473 -0
  47. moleditpy_linux/modules/plugin_manager_window.py +274 -0
  48. moleditpy_linux/modules/settings_dialog.py +1503 -0
  49. moleditpy_linux/modules/template_preview_item.py +157 -0
  50. moleditpy_linux/modules/template_preview_view.py +74 -0
  51. moleditpy_linux/modules/translation_dialog.py +364 -0
  52. moleditpy_linux/modules/user_template_dialog.py +692 -0
  53. moleditpy_linux/modules/zoomable_view.py +129 -0
  54. moleditpy_linux-2.4.1.dist-info/METADATA +954 -0
  55. moleditpy_linux-2.4.1.dist-info/RECORD +59 -0
  56. moleditpy_linux-2.4.1.dist-info/WHEEL +5 -0
  57. moleditpy_linux-2.4.1.dist-info/entry_points.txt +2 -0
  58. moleditpy_linux-2.4.1.dist-info/licenses/LICENSE +674 -0
  59. moleditpy_linux-2.4.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,536 @@
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
+ """
14
+ main_window_edit_3d.py
15
+ MainWindow (main_window.py) から分離されたモジュール
16
+ 機能クラス: MainWindowEdit3d
17
+ """
18
+
19
+
20
+ import numpy as np
21
+
22
+
23
+ # RDKit imports (explicit to satisfy flake8 and used features)
24
+ try:
25
+ from . import sip_isdeleted_safe
26
+ except Exception:
27
+ from modules import sip_isdeleted_safe
28
+
29
+ # PyQt6 Modules
30
+ from PyQt6.QtWidgets import (
31
+ QGraphicsTextItem
32
+ )
33
+
34
+ from PyQt6.QtGui import (
35
+ QColor, QFont
36
+ )
37
+
38
+
39
+ from PyQt6.QtCore import (
40
+ QPointF
41
+ )
42
+
43
+ import pyvista as pv
44
+
45
+ # Use centralized Open Babel availability from package-level __init__
46
+ # Use per-package modules availability (local __init__).
47
+ try:
48
+ from . import OBABEL_AVAILABLE
49
+ except Exception:
50
+ from modules import OBABEL_AVAILABLE
51
+ # Only import pybel on demand — `moleditpy` itself doesn't expose `pybel`.
52
+ if OBABEL_AVAILABLE:
53
+ try:
54
+ from openbabel import pybel
55
+ except Exception:
56
+ # If import fails here, disable OBABEL locally; avoid raising
57
+ pybel = None
58
+ OBABEL_AVAILABLE = False
59
+ print("Warning: openbabel.pybel not available. Open Babel fallback and OBabel-based options will be disabled.")
60
+ else:
61
+ pybel = None
62
+
63
+ # Optional SIP helper: on some PyQt6 builds sip.isdeleted is available and
64
+ # allows safely detecting C++ wrapper objects that have been deleted. Import
65
+ # it once at module import time and expose a small, robust wrapper so callers
66
+ # can avoid re-importing sip repeatedly and so we centralize exception
67
+ # handling (this reduces crash risk during teardown and deletion operations).
68
+ try:
69
+ import sip as _sip # type: ignore
70
+ _sip_isdeleted = getattr(_sip, 'isdeleted', None)
71
+ except Exception:
72
+ _sip = None
73
+ _sip_isdeleted = None
74
+
75
+ try:
76
+ # package relative imports (preferred when running as `python -m moleditpy`)
77
+ from .constants import VDW_RADII
78
+ except Exception:
79
+ # Fallback to absolute imports for script-style execution
80
+ from modules.constants import VDW_RADII
81
+
82
+
83
+ # --- クラス定義 ---
84
+ class MainWindowEdit3d(object):
85
+ """ main_window.py から分離された機能クラス """
86
+
87
+
88
+ def toggle_measurement_mode(self, checked):
89
+ """測定モードのオン/オフを切り替える"""
90
+ if checked:
91
+ # 測定モードをオンにする時は、3D Dragモードを無効化
92
+ if self.is_3d_edit_mode:
93
+ self.edit_3d_action.setChecked(False)
94
+ self.toggle_3d_edit_mode(False)
95
+
96
+ # アクティブな3D編集ダイアログを閉じる
97
+ self.close_all_3d_edit_dialogs()
98
+
99
+ self.measurement_mode = checked
100
+
101
+ if not checked:
102
+ self.clear_measurement_selection()
103
+
104
+ # ボタンのテキストとステータスメッセージを更新
105
+ if checked:
106
+ self.statusBar().showMessage("Measurement mode enabled. Click atoms to measure distances/angles/dihedrals.")
107
+ else:
108
+ self.statusBar().showMessage("Measurement mode disabled.")
109
+
110
+
111
+
112
+ def close_all_3d_edit_dialogs(self):
113
+ """すべてのアクティブな3D編集ダイアログを閉じる"""
114
+ dialogs_to_close = self.active_3d_dialogs.copy()
115
+ for dialog in dialogs_to_close:
116
+ try:
117
+ dialog.close()
118
+ except Exception:
119
+ pass
120
+ self.active_3d_dialogs.clear()
121
+
122
+
123
+
124
+ def handle_measurement_atom_selection(self, atom_idx):
125
+ """測定用の原子選択を処理する"""
126
+ # 既に選択されている原子の場合は除外
127
+ if atom_idx in self.selected_atoms_for_measurement:
128
+ return
129
+
130
+ self.selected_atoms_for_measurement.append(atom_idx)
131
+
132
+ '''
133
+ # 4つ以上選択された場合はクリア
134
+ if len(self.selected_atoms_for_measurement) > 4:
135
+ self.clear_measurement_selection()
136
+ self.selected_atoms_for_measurement.append(atom_idx)
137
+ '''
138
+
139
+ # 原子にラベルを追加
140
+ self.add_measurement_label(atom_idx, len(self.selected_atoms_for_measurement))
141
+
142
+ # 測定値を計算して表示
143
+ self.calculate_and_display_measurements()
144
+
145
+
146
+
147
+ def add_measurement_label(self, atom_idx, label_number):
148
+ """原子に数字ラベルを追加する"""
149
+ if not self.current_mol or atom_idx >= self.current_mol.GetNumAtoms():
150
+ return
151
+
152
+ # 測定ラベルリストを更新
153
+ self.measurement_labels.append((atom_idx, str(label_number)))
154
+
155
+ # 3Dビューの測定ラベルを再描画
156
+ self.update_measurement_labels_display()
157
+
158
+ # 2Dビューの測定ラベルも更新
159
+ self.update_2d_measurement_labels()
160
+
161
+
162
+
163
+ def update_measurement_labels_display(self):
164
+ """測定ラベルを3D表示に描画する(原子中心配置)"""
165
+ try:
166
+ # 既存の測定ラベルを削除
167
+ self.plotter.remove_actor('measurement_labels')
168
+ except Exception:
169
+ pass
170
+
171
+ if not self.measurement_labels or not self.current_mol:
172
+ return
173
+
174
+ # ラベル位置とテキストを準備
175
+ pts, labels = [], []
176
+ for atom_idx, label_text in self.measurement_labels:
177
+ if atom_idx < len(self.atom_positions_3d):
178
+ coord = self.atom_positions_3d[atom_idx].copy()
179
+ # オフセットを削除して原子中心に配置
180
+ pts.append(coord)
181
+ labels.append(label_text)
182
+
183
+ if pts and labels:
184
+ # PyVistaのpoint_labelsを使用(赤色固定)
185
+ self.plotter.add_point_labels(
186
+ np.array(pts),
187
+ labels,
188
+ font_size=16,
189
+ point_size=0,
190
+ text_color='red', # 測定時は常に赤色
191
+ name='measurement_labels',
192
+ always_visible=True,
193
+ tolerance=0.01,
194
+ show_points=False
195
+ )
196
+
197
+
198
+
199
+ def clear_measurement_selection(self):
200
+ """測定選択をクリアする"""
201
+ self.selected_atoms_for_measurement.clear()
202
+
203
+ # 3Dビューのラベルを削除
204
+ self.measurement_labels.clear()
205
+ try:
206
+ self.plotter.remove_actor('measurement_labels')
207
+ except Exception:
208
+ pass
209
+
210
+ # 2Dビューの測定ラベルも削除
211
+ self.clear_2d_measurement_labels()
212
+
213
+ # 測定結果のテキストを削除
214
+ if self.measurement_text_actor:
215
+ try:
216
+ self.plotter.remove_actor(self.measurement_text_actor)
217
+ self.measurement_text_actor = None
218
+ except Exception:
219
+ pass
220
+
221
+ self.plotter.render()
222
+
223
+
224
+
225
+ def update_2d_measurement_labels(self):
226
+ """2Dビューで測定ラベルを更新表示する"""
227
+ # 既存の2D測定ラベルを削除
228
+ self.clear_2d_measurement_labels()
229
+
230
+ # 現在の分子から原子-AtomItemマッピングを作成
231
+ if not self.current_mol or not hasattr(self, 'data') or not self.data.atoms:
232
+ return
233
+
234
+ # RDKit原子インデックスから2D AtomItemへのマッピングを作成
235
+ atom_idx_to_item = {}
236
+
237
+ # シーンからAtomItemを取得してマッピング
238
+ if hasattr(self, 'scene'):
239
+ for item in self.scene.items():
240
+ if hasattr(item, 'atom_id') and hasattr(item, 'symbol'): # AtomItemかチェック
241
+ # 原子IDから対応するRDKit原子インデックスを見つける
242
+ rdkit_idx = self.find_rdkit_atom_index(item)
243
+ if rdkit_idx is not None:
244
+ atom_idx_to_item[rdkit_idx] = item
245
+
246
+ # 測定ラベルを2Dビューに追加
247
+ if not hasattr(self, 'measurement_label_items_2d'):
248
+ self.measurement_label_items_2d = []
249
+
250
+ for atom_idx, label_text in self.measurement_labels:
251
+ if atom_idx in atom_idx_to_item:
252
+ atom_item = atom_idx_to_item[atom_idx]
253
+ self.add_2d_measurement_label(atom_item, label_text)
254
+
255
+
256
+
257
+ def add_2d_measurement_label(self, atom_item, label_text):
258
+ """特定のAtomItemに測定ラベルを追加する"""
259
+ # ラベルアイテムを作成
260
+ label_item = QGraphicsTextItem(label_text)
261
+ label_item.setDefaultTextColor(QColor(255, 0, 0)) # 赤色
262
+ label_item.setFont(QFont("Arial", 12, QFont.Weight.Bold))
263
+
264
+ # Z値を設定して最前面に表示(原子ラベルより上)
265
+ label_item.setZValue(2000) # より高い値で確実に最前面に配置
266
+
267
+ # 原子の右上により近く配置
268
+ atom_pos = atom_item.pos()
269
+ atom_rect = atom_item.boundingRect()
270
+ label_pos = QPointF(
271
+ atom_pos.x() + atom_rect.width() / 4 + 2,
272
+ atom_pos.y() - atom_rect.height() / 4 - 8
273
+ )
274
+ label_item.setPos(label_pos)
275
+
276
+ # シーンに追加
277
+ self.scene.addItem(label_item)
278
+ self.measurement_label_items_2d.append(label_item)
279
+
280
+
281
+
282
+ def clear_2d_measurement_labels(self):
283
+ """2Dビューの測定ラベルを全て削除する"""
284
+ if hasattr(self, 'measurement_label_items_2d'):
285
+ for label_item in self.measurement_label_items_2d:
286
+ try:
287
+ # Avoid touching partially-deleted wrappers
288
+ if sip_isdeleted_safe(label_item):
289
+ continue
290
+ try:
291
+ if label_item.scene():
292
+ self.scene.removeItem(label_item)
293
+ except Exception:
294
+ # Scene access or removal failed; skip
295
+ continue
296
+ except Exception:
297
+ # If sip check itself fails, fall back to best-effort removal
298
+ try:
299
+ if label_item.scene():
300
+ self.scene.removeItem(label_item)
301
+ except Exception:
302
+ continue
303
+ self.measurement_label_items_2d.clear()
304
+
305
+
306
+
307
+ def find_rdkit_atom_index(self, atom_item):
308
+ """AtomItemから対応するRDKit原子インデックスを見つける"""
309
+ if not self.current_mol or not atom_item:
310
+ return None
311
+
312
+ # マッピング辞書を使用(最も確実)
313
+ if hasattr(self, 'atom_id_to_rdkit_idx_map') and atom_item.atom_id in self.atom_id_to_rdkit_idx_map:
314
+ return self.atom_id_to_rdkit_idx_map[atom_item.atom_id]
315
+
316
+ # マッピングが存在しない場合はNone(外部ファイル読み込み時など)
317
+ return None
318
+
319
+
320
+
321
+ def calculate_and_display_measurements(self):
322
+ """選択された原子に基づいて測定値を計算し表示する"""
323
+ num_selected = len(self.selected_atoms_for_measurement)
324
+ if num_selected < 2:
325
+ return
326
+
327
+ measurement_text = []
328
+
329
+ if num_selected >= 2:
330
+ # 距離の計算
331
+ atom1_idx = self.selected_atoms_for_measurement[0]
332
+ atom2_idx = self.selected_atoms_for_measurement[1]
333
+ distance = self.calculate_distance(atom1_idx, atom2_idx)
334
+ measurement_text.append(f"Distance 1-2: {distance:.3f} Å")
335
+
336
+ if num_selected >= 3:
337
+ # 角度の計算
338
+ atom1_idx = self.selected_atoms_for_measurement[0]
339
+ atom2_idx = self.selected_atoms_for_measurement[1]
340
+ atom3_idx = self.selected_atoms_for_measurement[2]
341
+ angle = self.calculate_angle(atom1_idx, atom2_idx, atom3_idx)
342
+ measurement_text.append(f"Angle 1-2-3: {angle:.2f}°")
343
+
344
+ if num_selected >= 4:
345
+ # 二面角の計算
346
+ atom1_idx = self.selected_atoms_for_measurement[0]
347
+ atom2_idx = self.selected_atoms_for_measurement[1]
348
+ atom3_idx = self.selected_atoms_for_measurement[2]
349
+ atom4_idx = self.selected_atoms_for_measurement[3]
350
+ dihedral = self.calculate_dihedral(atom1_idx, atom2_idx, atom3_idx, atom4_idx)
351
+ measurement_text.append(f"Dihedral 1-2-3-4: {dihedral:.2f}°")
352
+
353
+ # 測定結果を3D画面の右上に表示
354
+ self.display_measurement_text(measurement_text)
355
+
356
+
357
+
358
+ def calculate_distance(self, atom1_idx, atom2_idx):
359
+ """2原子間の距離を計算する"""
360
+ pos1 = np.array(self.atom_positions_3d[atom1_idx])
361
+ pos2 = np.array(self.atom_positions_3d[atom2_idx])
362
+ return np.linalg.norm(pos2 - pos1)
363
+
364
+
365
+
366
+ def calculate_angle(self, atom1_idx, atom2_idx, atom3_idx):
367
+ """3原子の角度を計算する(中央が頂点)"""
368
+ pos1 = np.array(self.atom_positions_3d[atom1_idx])
369
+ pos2 = np.array(self.atom_positions_3d[atom2_idx]) # 頂点
370
+ pos3 = np.array(self.atom_positions_3d[atom3_idx])
371
+
372
+ # ベクトルを計算
373
+ vec1 = pos1 - pos2
374
+ vec2 = pos3 - pos2
375
+
376
+ # 角度を計算(ラジアンから度に変換)
377
+ cos_angle = np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))
378
+ # 数値誤差による範囲外の値をクリップ
379
+ cos_angle = np.clip(cos_angle, -1.0, 1.0)
380
+ angle_rad = np.arccos(cos_angle)
381
+ return np.degrees(angle_rad)
382
+
383
+
384
+
385
+ def calculate_dihedral(self, atom1_idx, atom2_idx, atom3_idx, atom4_idx):
386
+ """4原子の二面角を計算する(正しい公式を使用)"""
387
+ pos1 = np.array(self.atom_positions_3d[atom1_idx])
388
+ pos2 = np.array(self.atom_positions_3d[atom2_idx])
389
+ pos3 = np.array(self.atom_positions_3d[atom3_idx])
390
+ pos4 = np.array(self.atom_positions_3d[atom4_idx])
391
+
392
+ # Vectors between consecutive atoms
393
+ v1 = pos2 - pos1 # 1->2
394
+ v2 = pos3 - pos2 # 2->3 (central bond)
395
+ v3 = pos4 - pos3 # 3->4
396
+
397
+ # Normalize the central bond vector
398
+ v2_norm = v2 / np.linalg.norm(v2)
399
+
400
+ # Calculate plane normal vectors
401
+ n1 = np.cross(v1, v2) # Normal to plane 1-2-3
402
+ n2 = np.cross(v2, v3) # Normal to plane 2-3-4
403
+
404
+ # Normalize the normal vectors
405
+ n1_norm = np.linalg.norm(n1)
406
+ n2_norm = np.linalg.norm(n2)
407
+
408
+ if n1_norm == 0 or n2_norm == 0:
409
+ return 0.0 # Atoms are collinear
410
+
411
+ n1 = n1 / n1_norm
412
+ n2 = n2 / n2_norm
413
+
414
+ # Calculate the cosine of the dihedral angle
415
+ cos_angle = np.dot(n1, n2)
416
+ cos_angle = np.clip(cos_angle, -1.0, 1.0)
417
+
418
+ # Calculate the sine for proper sign determination
419
+ sin_angle = np.dot(np.cross(n1, n2), v2_norm)
420
+
421
+ # Calculate the dihedral angle with correct sign
422
+ angle_rad = np.arctan2(sin_angle, cos_angle)
423
+ return np.degrees(angle_rad)
424
+
425
+
426
+
427
+ def display_measurement_text(self, measurement_lines):
428
+ """測定結果のテキストを3D画面の左上に表示する(小さな等幅フォント)"""
429
+ # 既存のテキストを削除
430
+ if self.measurement_text_actor:
431
+ try:
432
+ self.plotter.remove_actor(self.measurement_text_actor)
433
+ except Exception:
434
+ pass
435
+
436
+ if not measurement_lines:
437
+ self.measurement_text_actor = None
438
+ return
439
+
440
+ # テキストを結合
441
+ text = '\n'.join(measurement_lines)
442
+
443
+ # 背景色から適切なテキスト色を決定
444
+ try:
445
+ bg_color_hex = self.settings.get('background_color', '#919191')
446
+ bg_qcolor = QColor(bg_color_hex)
447
+ if bg_qcolor.isValid():
448
+ luminance = bg_qcolor.toHsl().lightness()
449
+ text_color = 'black' if luminance > 128 else 'white'
450
+ else:
451
+ text_color = 'white'
452
+ except Exception:
453
+ text_color = 'white'
454
+
455
+ # 左上に表示(小さな等幅フォント)
456
+ self.measurement_text_actor = self.plotter.add_text(
457
+ text,
458
+ position='upper_left',
459
+ font_size=10, # より小さく
460
+ color=text_color, # 背景に合わせた色
461
+ font='courier', # 等幅フォント
462
+ name='measurement_display'
463
+ )
464
+
465
+ self.plotter.render()
466
+
467
+ # --- 3D Drag functionality ---
468
+
469
+
470
+
471
+ def toggle_atom_selection_3d(self, atom_idx):
472
+ """3Dビューで原子の選択状態をトグルする"""
473
+ if atom_idx in self.selected_atoms_3d:
474
+ self.selected_atoms_3d.remove(atom_idx)
475
+ else:
476
+ self.selected_atoms_3d.add(atom_idx)
477
+
478
+ # 選択状態のビジュアルフィードバックを更新
479
+ self.update_3d_selection_display()
480
+
481
+
482
+
483
+ def clear_3d_selection(self):
484
+ """3Dビューでの原子選択をクリア"""
485
+ self.selected_atoms_3d.clear()
486
+ self.update_3d_selection_display()
487
+
488
+
489
+
490
+ def update_3d_selection_display(self):
491
+ """3Dビューでの選択原子のハイライト表示を更新"""
492
+ try:
493
+ # 既存の選択ハイライトを削除
494
+ self.plotter.remove_actor('selection_highlight')
495
+ except Exception:
496
+ pass
497
+
498
+ if not self.selected_atoms_3d or not self.current_mol:
499
+ self.plotter.render()
500
+ return
501
+
502
+ # 選択された原子のインデックスリストを作成
503
+ selected_indices = list(self.selected_atoms_3d)
504
+
505
+ # 選択された原子の位置を取得
506
+ selected_positions = self.atom_positions_3d[selected_indices]
507
+
508
+ # 原子の半径を少し大きくしてハイライト表示
509
+ selected_radii = np.array([VDW_RADII.get(
510
+ self.current_mol.GetAtomWithIdx(i).GetSymbol(), 0.4) * 1.3
511
+ for i in selected_indices])
512
+
513
+ # ハイライト用のデータセットを作成
514
+ highlight_source = pv.PolyData(selected_positions)
515
+ highlight_source['radii'] = selected_radii
516
+
517
+ # 黄色の半透明球でハイライト
518
+ highlight_glyphs = highlight_source.glyph(
519
+ scale='radii',
520
+ geom=pv.Sphere(radius=1.0, theta_resolution=16, phi_resolution=16),
521
+ orient=False
522
+ )
523
+
524
+ self.plotter.add_mesh(
525
+ highlight_glyphs,
526
+ color='yellow',
527
+ opacity=0.3,
528
+ name='selection_highlight'
529
+ )
530
+
531
+ self.plotter.render()
532
+
533
+ def remove_dialog_from_list(self, dialog):
534
+ """ダイアログをアクティブリストから削除"""
535
+ if dialog in self.active_3d_dialogs:
536
+ self.active_3d_dialogs.remove(dialog)