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.
- moleditpy/__init__.py +1 -1
- moleditpy/__main__.py +1 -1
- moleditpy/main.py +1 -1
- moleditpy/modules/__init__.py +1 -1
- moleditpy/modules/about_dialog.py +1 -1
- moleditpy/modules/align_plane_dialog.py +1 -1
- moleditpy/modules/alignment_dialog.py +1 -1
- moleditpy/modules/analysis_window.py +1 -1
- moleditpy/modules/angle_dialog.py +1 -1
- moleditpy/modules/atom_item.py +1 -1
- moleditpy/modules/bond_item.py +97 -6
- moleditpy/modules/bond_length_dialog.py +1 -1
- moleditpy/modules/calculation_worker.py +1 -1
- moleditpy/modules/color_settings_dialog.py +1 -1
- moleditpy/modules/constants.py +2 -2
- moleditpy/modules/constrained_optimization_dialog.py +1 -1
- moleditpy/modules/custom_interactor_style.py +1 -1
- moleditpy/modules/custom_qt_interactor.py +1 -1
- moleditpy/modules/dialog3_d_picking_mixin.py +1 -1
- moleditpy/modules/dihedral_dialog.py +1 -1
- moleditpy/modules/main_window.py +1 -1
- moleditpy/modules/main_window_app_state.py +9 -0
- moleditpy/modules/main_window_compute.py +7 -1
- moleditpy/modules/main_window_dialog_manager.py +1 -1
- moleditpy/modules/main_window_edit_3d.py +1 -1
- moleditpy/modules/main_window_edit_actions.py +1 -1
- moleditpy/modules/main_window_export.py +1 -1
- moleditpy/modules/main_window_main_init.py +1 -1
- moleditpy/modules/main_window_molecular_parsers.py +1 -1
- moleditpy/modules/main_window_project_io.py +1 -1
- moleditpy/modules/main_window_string_importers.py +1 -1
- moleditpy/modules/main_window_ui_manager.py +1 -1
- moleditpy/modules/main_window_view_3d.py +349 -122
- moleditpy/modules/main_window_view_loaders.py +1 -1
- moleditpy/modules/mirror_dialog.py +1 -1
- moleditpy/modules/molecular_data.py +1 -1
- moleditpy/modules/molecule_scene.py +8 -4
- moleditpy/modules/move_group_dialog.py +1 -1
- moleditpy/modules/periodic_table_dialog.py +1 -1
- moleditpy/modules/planarize_dialog.py +1 -1
- moleditpy/modules/settings_dialog.py +86 -2
- moleditpy/modules/template_preview_item.py +1 -1
- moleditpy/modules/template_preview_view.py +1 -1
- moleditpy/modules/translation_dialog.py +1 -1
- moleditpy/modules/user_template_dialog.py +1 -1
- moleditpy/modules/zoomable_view.py +22 -2
- {moleditpy-1.17.0.dist-info → moleditpy-1.18.0.dist-info}/METADATA +1 -1
- moleditpy-1.18.0.dist-info/RECORD +55 -0
- moleditpy-1.17.0.dist-info/RECORD +0 -55
- {moleditpy-1.17.0.dist-info → moleditpy-1.18.0.dist-info}/WHEEL +0 -0
- {moleditpy-1.17.0.dist-info → moleditpy-1.18.0.dist-info}/entry_points.txt +0 -0
- {moleditpy-1.17.0.dist-info → moleditpy-1.18.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
self.plotter.add_mesh(cyl1_1, color=begin_color, **mesh_props)
|
|
364
|
-
self.plotter.add_mesh(cyl1_2, color=end_color, **mesh_props)
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
cyl2 = pv.Cylinder(center=c + off_dir * s_triple, direction=d, radius=r, height=h, resolution=bond_resolution)
|
|
390
|
-
cyl3 = pv.Cylinder(center=c - off_dir * s_triple, direction=d, radius=r, height=h, resolution=bond_resolution)
|
|
391
|
-
bond_cylinders.extend([cyl1, cyl2, cyl3])
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
# 中央の結合線(前半・後半)
|
|
400
|
-
cyl1_1 = pv.Cylinder(center=(sp + mid_point) / 2, direction=d, radius=r, height=h/2, resolution=bond_resolution)
|
|
401
|
-
cyl1_2 = pv.Cylinder(center=(mid_point + ep) / 2, direction=d, radius=r, height=h/2, resolution=bond_resolution)
|
|
402
|
-
self.plotter.add_mesh(cyl1_1, color=begin_color, **mesh_props)
|
|
403
|
-
self.plotter.add_mesh(cyl1_2, color=end_color, **mesh_props)
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
self.plotter.add_mesh(cyl3_1, color=begin_color, **mesh_props)
|
|
419
|
-
self.plotter.add_mesh(cyl3_2, color=end_color, **mesh_props)
|
|
420
|
-
self._3d_color_map[f'bond_{bond_counter}_3_start'] = begin_color_rgb
|
|
421
|
-
self._3d_color_map[f'bond_{bond_counter}_3_end'] = end_color_rgb
|
|
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
|
-
#
|
|
426
|
-
if
|
|
427
|
-
#
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
843
|
+
for bond in mol.GetBonds():
|
|
628
844
|
if bond.GetBondType() == Chem.BondType.DOUBLE:
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
#
|
|
640
|
-
label = 'E' if
|
|
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
|
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
|