MoleditPy 1.17.1__tar.gz → 1.18.0__tar.gz
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-1.17.1 → moleditpy-1.18.0}/PKG-INFO +1 -1
- {moleditpy-1.17.1 → moleditpy-1.18.0}/pyproject.toml +1 -1
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/MoleditPy.egg-info/PKG-INFO +1 -1
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/bond_item.py +96 -5
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/constants.py +1 -1
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/main_window_compute.py +6 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/main_window_view_3d.py +348 -121
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/molecule_scene.py +7 -3
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/settings_dialog.py +85 -1
- {moleditpy-1.17.1 → moleditpy-1.18.0}/LICENSE +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/README.md +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/setup.cfg +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/MoleditPy.egg-info/SOURCES.txt +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/MoleditPy.egg-info/entry_points.txt +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/MoleditPy.egg-info/requires.txt +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/MoleditPy.egg-info/top_level.txt +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/__init__.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/__main__.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/main.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/__init__.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/about_dialog.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/align_plane_dialog.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/alignment_dialog.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/analysis_window.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/angle_dialog.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/assets/icon.icns +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/assets/icon.ico +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/assets/icon.png +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/atom_item.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/bond_length_dialog.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/calculation_worker.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/color_settings_dialog.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/constrained_optimization_dialog.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/custom_interactor_style.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/custom_qt_interactor.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/dialog3_d_picking_mixin.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/dihedral_dialog.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/main_window.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/main_window_app_state.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/main_window_dialog_manager.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/main_window_edit_3d.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/main_window_edit_actions.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/main_window_export.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/main_window_main_init.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/main_window_molecular_parsers.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/main_window_project_io.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/main_window_string_importers.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/main_window_ui_manager.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/main_window_view_loaders.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/mirror_dialog.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/molecular_data.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/move_group_dialog.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/periodic_table_dialog.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/planarize_dialog.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/template_preview_item.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/template_preview_view.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/translation_dialog.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/user_template_dialog.py +0 -0
- {moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/zoomable_view.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.18.0
|
|
4
4
|
Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
|
|
5
5
|
Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.18.0
|
|
4
4
|
Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
|
|
5
5
|
Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -216,11 +216,102 @@ class BondItem(QGraphicsItem):
|
|
|
216
216
|
offset = QPointF(v.dx(), v.dy()) * BOND_OFFSET
|
|
217
217
|
|
|
218
218
|
if self.order == 2:
|
|
219
|
-
#
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
219
|
+
# 環構造かどうかを判定し、描画方法を変更
|
|
220
|
+
is_in_ring = False
|
|
221
|
+
ring_center = None
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
# シーンからRDKit分子を取得
|
|
225
|
+
sc = self.scene()
|
|
226
|
+
if sc and hasattr(sc, 'window') and sc.window:
|
|
227
|
+
# 2DデータからRDKit分子を生成
|
|
228
|
+
mol = sc.window.data.to_rdkit_mol(use_2d_stereo=False)
|
|
229
|
+
if mol:
|
|
230
|
+
# この結合に対応するRDKitボンドを探す
|
|
231
|
+
atom1_id = self.atom1.atom_id
|
|
232
|
+
atom2_id = self.atom2.atom_id
|
|
233
|
+
|
|
234
|
+
# RDKitインデックスを取得
|
|
235
|
+
rdkit_idx1 = None
|
|
236
|
+
rdkit_idx2 = None
|
|
237
|
+
for atom in mol.GetAtoms():
|
|
238
|
+
if atom.HasProp("_original_atom_id"):
|
|
239
|
+
orig_id = atom.GetIntProp("_original_atom_id")
|
|
240
|
+
if orig_id == atom1_id:
|
|
241
|
+
rdkit_idx1 = atom.GetIdx()
|
|
242
|
+
elif orig_id == atom2_id:
|
|
243
|
+
rdkit_idx2 = atom.GetIdx()
|
|
244
|
+
|
|
245
|
+
if rdkit_idx1 is not None and rdkit_idx2 is not None:
|
|
246
|
+
bond = mol.GetBondBetweenAtoms(rdkit_idx1, rdkit_idx2)
|
|
247
|
+
if bond and bond.IsInRing():
|
|
248
|
+
is_in_ring = True
|
|
249
|
+
# 環の中心を計算(この結合を含む最小環)
|
|
250
|
+
from rdkit import Chem
|
|
251
|
+
ring_info = mol.GetRingInfo()
|
|
252
|
+
for ring in ring_info.AtomRings():
|
|
253
|
+
if rdkit_idx1 in ring and rdkit_idx2 in ring:
|
|
254
|
+
# 環の原子位置の平均を計算
|
|
255
|
+
ring_positions = []
|
|
256
|
+
for atom_idx in ring:
|
|
257
|
+
# 対応するエディタ側の原子を探す
|
|
258
|
+
rdkit_atom = mol.GetAtomWithIdx(atom_idx)
|
|
259
|
+
if rdkit_atom.HasProp("_original_atom_id"):
|
|
260
|
+
editor_atom_id = rdkit_atom.GetIntProp("_original_atom_id")
|
|
261
|
+
if editor_atom_id in sc.window.data.atoms:
|
|
262
|
+
atom_item = sc.window.data.atoms[editor_atom_id]['item']
|
|
263
|
+
if atom_item:
|
|
264
|
+
ring_positions.append(atom_item.pos())
|
|
265
|
+
|
|
266
|
+
if ring_positions:
|
|
267
|
+
# 環の中心を計算
|
|
268
|
+
center_x = sum(p.x() for p in ring_positions) / len(ring_positions)
|
|
269
|
+
center_y = sum(p.y() for p in ring_positions) / len(ring_positions)
|
|
270
|
+
ring_center = QPointF(center_x, center_y)
|
|
271
|
+
break
|
|
272
|
+
except Exception as e:
|
|
273
|
+
# エラーが発生した場合は通常の描画にフォールバック
|
|
274
|
+
is_in_ring = False
|
|
275
|
+
|
|
276
|
+
v = line.unitVector().normalVector()
|
|
277
|
+
offset = QPointF(v.dx(), v.dy()) * BOND_OFFSET
|
|
278
|
+
|
|
279
|
+
if is_in_ring and ring_center:
|
|
280
|
+
# 環構造: 1本の中心線(単結合位置) + 1本の短い内側線
|
|
281
|
+
# 結合の中心から環の中心への方向を計算
|
|
282
|
+
bond_center = line.center()
|
|
283
|
+
|
|
284
|
+
# ローカル座標系での環中心方向
|
|
285
|
+
local_ring_center = self.mapFromScene(ring_center)
|
|
286
|
+
local_bond_center = line.center()
|
|
287
|
+
inward_vec = local_ring_center - local_bond_center
|
|
288
|
+
|
|
289
|
+
# offsetとinward_vecの内積で内側を判定
|
|
290
|
+
if QPointF.dotProduct(offset, inward_vec) > 0:
|
|
291
|
+
# offsetが内側方向(2倍のオフセット)
|
|
292
|
+
inner_offset = offset * 2
|
|
293
|
+
else:
|
|
294
|
+
# -offsetが内側方向(2倍のオフセット)
|
|
295
|
+
inner_offset = -offset * 2
|
|
296
|
+
|
|
297
|
+
# 中心線を描画(単結合と同じ位置)
|
|
298
|
+
painter.drawLine(line)
|
|
299
|
+
|
|
300
|
+
# 内側の短い線を描画(80%の長さ)
|
|
301
|
+
inner_line = line.translated(inner_offset)
|
|
302
|
+
shorten_factor = 0.8
|
|
303
|
+
p1 = inner_line.p1()
|
|
304
|
+
p2 = inner_line.p2()
|
|
305
|
+
center = QPointF((p1.x() + p2.x()) / 2, (p1.y() + p2.y()) / 2)
|
|
306
|
+
shortened_p1 = center + (p1 - center) * shorten_factor
|
|
307
|
+
shortened_p2 = center + (p2 - center) * shorten_factor
|
|
308
|
+
painter.drawLine(QLineF(shortened_p1, shortened_p2))
|
|
309
|
+
else:
|
|
310
|
+
# 非環構造: 従来の2本の平行線
|
|
311
|
+
line1 = line.translated(offset)
|
|
312
|
+
line2 = line.translated(-offset)
|
|
313
|
+
painter.drawLine(line1)
|
|
314
|
+
painter.drawLine(line2)
|
|
224
315
|
|
|
225
316
|
# E/Z ラベルの描画処理
|
|
226
317
|
if self.stereo in [3, 4]:
|
|
@@ -958,6 +958,12 @@ class MainWindowCompute(object):
|
|
|
958
958
|
try:
|
|
959
959
|
if mol.GetNumConformers() > 0:
|
|
960
960
|
# 初回変換では、2Dで設定したwedge/dashボンドの立体情報を保持
|
|
961
|
+
|
|
962
|
+
# 3D立体化学計算で上書きされる前に、2D由来の立体化学情報をプロパティとして保存
|
|
963
|
+
for bond in mol.GetBonds():
|
|
964
|
+
if bond.GetBondType() == Chem.BondType.DOUBLE:
|
|
965
|
+
bond.SetIntProp("_original_2d_stereo", bond.GetStereo())
|
|
966
|
+
|
|
961
967
|
# 立体化学の割り当てを行うが、既存の2D立体情報を尊重
|
|
962
968
|
Chem.AssignStereochemistry(mol, cleanIt=False, force=True)
|
|
963
969
|
|
|
@@ -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
|
|
|
@@ -1712,7 +1712,11 @@ class MoleculeScene(QGraphicsScene):
|
|
|
1712
1712
|
return
|
|
1713
1713
|
|
|
1714
1714
|
# --- 3. Atomに対する操作 (原子の追加 - マージされた機能) ---
|
|
1715
|
-
if key
|
|
1715
|
+
if key in [Qt.Key.Key_1, Qt.Key.Key_2, Qt.Key.Key_3]:
|
|
1716
|
+
target_order = 1
|
|
1717
|
+
if key == Qt.Key.Key_2: target_order = 2
|
|
1718
|
+
elif key == Qt.Key.Key_3: target_order = 3
|
|
1719
|
+
|
|
1716
1720
|
start_atom = None
|
|
1717
1721
|
if isinstance(item_at_cursor, AtomItem):
|
|
1718
1722
|
start_atom = item_at_cursor
|
|
@@ -1802,12 +1806,12 @@ class MoleculeScene(QGraphicsScene):
|
|
|
1802
1806
|
|
|
1803
1807
|
if near_atom and near_atom is not start_atom:
|
|
1804
1808
|
# 近くに既存原子があれば結合
|
|
1805
|
-
self.create_bond(start_atom, near_atom)
|
|
1809
|
+
self.create_bond(start_atom, near_atom, bond_order=target_order, bond_stereo=0)
|
|
1806
1810
|
else:
|
|
1807
1811
|
# 新規原子を作成し結合
|
|
1808
1812
|
new_atom_id = self.create_atom('C', target_pos)
|
|
1809
1813
|
new_atom_item = self.data.atoms[new_atom_id]['item']
|
|
1810
|
-
self.create_bond(start_atom, new_atom_item)
|
|
1814
|
+
self.create_bond(start_atom, new_atom_item, bond_order=target_order, bond_stereo=0)
|
|
1811
1815
|
|
|
1812
1816
|
self.clearSelection()
|
|
1813
1817
|
self.window.push_undo_state()
|
|
@@ -69,6 +69,9 @@ class SettingsDialog(QDialog):
|
|
|
69
69
|
'stick_triple_bond_offset_factor': 1.0,
|
|
70
70
|
'stick_double_bond_radius_factor': 0.6,
|
|
71
71
|
'stick_triple_bond_radius_factor': 0.4,
|
|
72
|
+
'aromatic_torus_thickness_factor': 0.6,
|
|
73
|
+
# Whether to draw an aromatic circle inside rings in 3D
|
|
74
|
+
'display_aromatic_circles_3d': False,
|
|
72
75
|
# If True, attempts to be permissive when RDKit raises chemical/sanitization errors
|
|
73
76
|
# during file import (useful for viewing malformed XYZ/MOL files). When enabled,
|
|
74
77
|
# element symbol recognition will be coerced where possible and Chem.SanitizeMol
|
|
@@ -86,6 +89,7 @@ class SettingsDialog(QDialog):
|
|
|
86
89
|
# If True, RDKit will attempt to kekulize aromatic systems for 3D display
|
|
87
90
|
# (shows alternating single/double bonds rather than aromatic circles)
|
|
88
91
|
'display_kekule_3d': False,
|
|
92
|
+
'ball_stick_use_cpk_bond_color': False,
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
# --- 選択された色を管理する専用のインスタンス変数 ---
|
|
@@ -118,6 +122,13 @@ class SettingsDialog(QDialog):
|
|
|
118
122
|
|
|
119
123
|
# 渡された設定でUIと内部変数を初期化
|
|
120
124
|
self.update_ui_from_settings(current_settings)
|
|
125
|
+
|
|
126
|
+
# Initialize aromatic circle checkbox and torus thickness from settings
|
|
127
|
+
self.aromatic_circle_checkbox.setChecked(current_settings.get('display_aromatic_circles_3d', self.default_settings.get('display_aromatic_circles_3d', False)))
|
|
128
|
+
# Thickness factor is stored as a multiplier (e.g., 1.0), slider uses integer 0-300 representing 0.1x-3.0x
|
|
129
|
+
thickness_factor = current_settings.get('aromatic_torus_thickness_factor', self.default_settings.get('aromatic_torus_thickness_factor', 1.0))
|
|
130
|
+
self.aromatic_torus_thickness_slider.setValue(int(thickness_factor * 100))
|
|
131
|
+
self.aromatic_torus_thickness_label.setText(f"{thickness_factor:.1f}")
|
|
121
132
|
|
|
122
133
|
# --- ボタンの配置 ---
|
|
123
134
|
buttons = QHBoxLayout()
|
|
@@ -243,11 +254,42 @@ class SettingsDialog(QDialog):
|
|
|
243
254
|
except Exception:
|
|
244
255
|
pass
|
|
245
256
|
|
|
257
|
+
# Add separator after Kekule bonds option
|
|
258
|
+
separator = QFrame()
|
|
259
|
+
separator.setFrameShape(QFrame.Shape.HLine)
|
|
260
|
+
separator.setFrameShadow(QFrame.Shadow.Sunken)
|
|
261
|
+
self.other_form_layout.addRow(separator)
|
|
262
|
+
|
|
246
263
|
# Place the Kekulé option after the always-ask-charge option
|
|
247
264
|
try:
|
|
248
265
|
self.other_form_layout.addRow("Display Kekulé bonds in 3D:", self.kekule_3d_checkbox)
|
|
249
266
|
except Exception:
|
|
250
267
|
pass
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# Aromatic ring circle display option
|
|
271
|
+
self.aromatic_circle_checkbox = QCheckBox()
|
|
272
|
+
self.aromatic_circle_checkbox.setToolTip("When enabled, aromatic rings will be displayed with a circle inside the ring in 3D view.")
|
|
273
|
+
try:
|
|
274
|
+
self.other_form_layout.addRow("Display aromatic rings as circles in 3D:", self.aromatic_circle_checkbox)
|
|
275
|
+
except Exception:
|
|
276
|
+
pass
|
|
277
|
+
|
|
278
|
+
# Aromatic torus thickness factor
|
|
279
|
+
self.aromatic_torus_thickness_slider = QSlider(Qt.Orientation.Horizontal)
|
|
280
|
+
self.aromatic_torus_thickness_slider.setRange(10, 300) # 0.1x to 3.0x
|
|
281
|
+
self.aromatic_torus_thickness_slider.setValue(100) # Default 1.0x
|
|
282
|
+
self.aromatic_torus_thickness_label = QLabel("1.0")
|
|
283
|
+
self.aromatic_torus_thickness_slider.valueChanged.connect(
|
|
284
|
+
lambda v: self.aromatic_torus_thickness_label.setText(f"{v/100:.1f}")
|
|
285
|
+
)
|
|
286
|
+
thickness_layout = QHBoxLayout()
|
|
287
|
+
thickness_layout.addWidget(self.aromatic_torus_thickness_slider)
|
|
288
|
+
thickness_layout.addWidget(self.aromatic_torus_thickness_label)
|
|
289
|
+
try:
|
|
290
|
+
self.other_form_layout.addRow("Aromatic torus thickness (× bond radius):", thickness_layout)
|
|
291
|
+
except Exception:
|
|
292
|
+
pass
|
|
251
293
|
|
|
252
294
|
# Add Other tab to the tab widget
|
|
253
295
|
self.tab_widget.addTab(self.other_widget, "Other")
|
|
@@ -378,6 +420,12 @@ class SettingsDialog(QDialog):
|
|
|
378
420
|
resolution_layout.addWidget(self.bs_resolution_label)
|
|
379
421
|
form_layout.addRow("Resolution (Quality):", resolution_layout)
|
|
380
422
|
|
|
423
|
+
# --- 区切り線(水平ライン) ---
|
|
424
|
+
line = QFrame()
|
|
425
|
+
line.setFrameShape(QFrame.Shape.HLine)
|
|
426
|
+
line.setFrameShadow(QFrame.Shadow.Sunken)
|
|
427
|
+
form_layout.addRow(line)
|
|
428
|
+
|
|
381
429
|
# --- Ball & Stick bond color ---
|
|
382
430
|
self.bs_bond_color_button = QPushButton()
|
|
383
431
|
self.bs_bond_color_button.setFixedSize(36, 24)
|
|
@@ -385,6 +433,11 @@ class SettingsDialog(QDialog):
|
|
|
385
433
|
self.bs_bond_color_button.setToolTip("Choose the uniform bond color for Ball & Stick model (3D)")
|
|
386
434
|
form_layout.addRow("Ball & Stick bond color:", self.bs_bond_color_button)
|
|
387
435
|
|
|
436
|
+
# Use CPK colors for bonds option
|
|
437
|
+
self.bs_use_cpk_bond_checkbox = QCheckBox()
|
|
438
|
+
self.bs_use_cpk_bond_checkbox.setToolTip("If checked, bonds will be colored using the atom colors (split bonds). If unchecked, a uniform color is used.")
|
|
439
|
+
form_layout.addRow("Use CPK colors for bonds:", self.bs_use_cpk_bond_checkbox)
|
|
440
|
+
|
|
388
441
|
self.tab_widget.addTab(ball_stick_widget, "Ball & Stick")
|
|
389
442
|
|
|
390
443
|
def create_cpk_tab(self):
|
|
@@ -619,6 +672,8 @@ class SettingsDialog(QDialog):
|
|
|
619
672
|
'skip_chemistry_checks': self.default_settings.get('skip_chemistry_checks', False),
|
|
620
673
|
'display_kekule_3d': self.default_settings.get('display_kekule_3d', False),
|
|
621
674
|
'always_ask_charge': self.default_settings.get('always_ask_charge', False),
|
|
675
|
+
'display_aromatic_circles_3d': self.default_settings.get('display_aromatic_circles_3d', False),
|
|
676
|
+
'aromatic_torus_thickness_factor': self.default_settings.get('aromatic_torus_thickness_factor', 1.0),
|
|
622
677
|
},
|
|
623
678
|
"Ball & Stick": {
|
|
624
679
|
'ball_stick_atom_scale': self.default_settings['ball_stick_atom_scale'],
|
|
@@ -627,11 +682,14 @@ class SettingsDialog(QDialog):
|
|
|
627
682
|
'ball_stick_double_bond_offset_factor': self.default_settings.get('ball_stick_double_bond_offset_factor', 2.0),
|
|
628
683
|
'ball_stick_triple_bond_offset_factor': self.default_settings.get('ball_stick_triple_bond_offset_factor', 2.0),
|
|
629
684
|
'ball_stick_double_bond_radius_factor': self.default_settings.get('ball_stick_double_bond_radius_factor', 0.8),
|
|
630
|
-
'ball_stick_triple_bond_radius_factor': self.default_settings.get('ball_stick_triple_bond_radius_factor', 0.75)
|
|
685
|
+
'ball_stick_triple_bond_radius_factor': self.default_settings.get('ball_stick_triple_bond_radius_factor', 0.75),
|
|
686
|
+
'ball_stick_use_cpk_bond_color': self.default_settings['ball_stick_use_cpk_bond_color'],
|
|
687
|
+
'ball_stick_bond_color': self.default_settings.get('ball_stick_bond_color', '#7F7F7F')
|
|
631
688
|
},
|
|
632
689
|
"CPK (Space-filling)": {
|
|
633
690
|
'cpk_atom_scale': self.default_settings['cpk_atom_scale'],
|
|
634
691
|
'cpk_resolution': self.default_settings['cpk_resolution'],
|
|
692
|
+
'cpk_colors': {}
|
|
635
693
|
},
|
|
636
694
|
"Wireframe": {
|
|
637
695
|
'wireframe_bond_radius': self.default_settings['wireframe_bond_radius'],
|
|
@@ -665,6 +723,7 @@ class SettingsDialog(QDialog):
|
|
|
665
723
|
|
|
666
724
|
# UIを更新
|
|
667
725
|
self.update_ui_from_settings(updated_settings)
|
|
726
|
+
# CPK tab: do not change parent/settings immediately; let Apply/OK persist any changes
|
|
668
727
|
|
|
669
728
|
# ユーザーへのフィードバック
|
|
670
729
|
QMessageBox.information(self, "Reset Complete", f"Settings for '{tab_name}' tab have been reset to defaults.")
|
|
@@ -801,6 +860,17 @@ class SettingsDialog(QDialog):
|
|
|
801
860
|
'stick_triple_bond_offset_factor': self.stick_triple_offset_slider.value() / 100.0,
|
|
802
861
|
'stick_double_bond_radius_factor': self.stick_double_radius_slider.value() / 100.0,
|
|
803
862
|
'stick_triple_bond_radius_factor': self.stick_triple_radius_slider.value() / 100.0,
|
|
863
|
+
# Projection mode
|
|
864
|
+
'projection_mode': self.projection_combo.currentText(),
|
|
865
|
+
# Kekule / aromatic / torus settings
|
|
866
|
+
'display_kekule_3d': self.kekule_3d_checkbox.isChecked(),
|
|
867
|
+
'display_aromatic_circles_3d': self.aromatic_circle_checkbox.isChecked(),
|
|
868
|
+
'aromatic_torus_thickness_factor': self.aromatic_torus_thickness_slider.value() / 100.0,
|
|
869
|
+
'skip_chemistry_checks': self.skip_chem_checks_checkbox.isChecked(),
|
|
870
|
+
'always_ask_charge': self.always_ask_charge_checkbox.isChecked(),
|
|
871
|
+
# Ball & Stick bond color and use-cpk option
|
|
872
|
+
'ball_stick_bond_color': getattr(self, 'bs_bond_color', self.default_settings.get('ball_stick_bond_color')),
|
|
873
|
+
'ball_stick_use_cpk_bond_color': self.bs_use_cpk_bond_checkbox.isChecked(),
|
|
804
874
|
}
|
|
805
875
|
|
|
806
876
|
def reset_to_defaults(self):
|
|
@@ -854,6 +924,9 @@ class SettingsDialog(QDialog):
|
|
|
854
924
|
except Exception:
|
|
855
925
|
pass
|
|
856
926
|
|
|
927
|
+
# Ball & Stick CPK bond color option
|
|
928
|
+
self.bs_use_cpk_bond_checkbox.setChecked(settings_dict.get('ball_stick_use_cpk_bond_color', self.default_settings['ball_stick_use_cpk_bond_color']))
|
|
929
|
+
|
|
857
930
|
# CPK設定
|
|
858
931
|
cpk_atom_scale = int(settings_dict.get('cpk_atom_scale', self.default_settings['cpk_atom_scale']) * 100)
|
|
859
932
|
self.cpk_atom_scale_slider.setValue(cpk_atom_scale)
|
|
@@ -944,6 +1017,14 @@ class SettingsDialog(QDialog):
|
|
|
944
1017
|
self.kekule_3d_checkbox.setChecked(settings_dict.get('display_kekule_3d', self.default_settings.get('display_kekule_3d', False)))
|
|
945
1018
|
# always ask for charge on XYZ imports
|
|
946
1019
|
self.always_ask_charge_checkbox.setChecked(settings_dict.get('always_ask_charge', self.default_settings.get('always_ask_charge', False)))
|
|
1020
|
+
# Aromatic ring circle display and torus thickness factor
|
|
1021
|
+
self.aromatic_circle_checkbox.setChecked(settings_dict.get('display_aromatic_circles_3d', self.default_settings.get('display_aromatic_circles_3d', False)))
|
|
1022
|
+
thickness_factor = float(settings_dict.get('aromatic_torus_thickness_factor', self.default_settings.get('aromatic_torus_thickness_factor', 1.0)))
|
|
1023
|
+
try:
|
|
1024
|
+
self.aromatic_torus_thickness_slider.setValue(int(thickness_factor * 100))
|
|
1025
|
+
self.aromatic_torus_thickness_label.setText(f"{thickness_factor:.1f}")
|
|
1026
|
+
except Exception:
|
|
1027
|
+
pass
|
|
947
1028
|
|
|
948
1029
|
def select_color(self):
|
|
949
1030
|
"""カラーピッカーを開き、選択された色を内部変数とUIに反映させる"""
|
|
@@ -996,10 +1077,13 @@ class SettingsDialog(QDialog):
|
|
|
996
1077
|
'stick_double_bond_radius_factor': self.stick_double_radius_slider.value() / 100.0,
|
|
997
1078
|
'stick_triple_bond_radius_factor': self.stick_triple_radius_slider.value() / 100.0,
|
|
998
1079
|
'display_kekule_3d': self.kekule_3d_checkbox.isChecked(),
|
|
1080
|
+
'display_aromatic_circles_3d': self.aromatic_circle_checkbox.isChecked(),
|
|
1081
|
+
'aromatic_torus_thickness_factor': self.aromatic_torus_thickness_slider.value() / 100.0,
|
|
999
1082
|
'skip_chemistry_checks': self.skip_chem_checks_checkbox.isChecked(),
|
|
1000
1083
|
'always_ask_charge': self.always_ask_charge_checkbox.isChecked(),
|
|
1001
1084
|
# Ball & Stick bond color (3D grey/uniform color)
|
|
1002
1085
|
'ball_stick_bond_color': getattr(self, 'bs_bond_color', self.default_settings.get('ball_stick_bond_color', '#7F7F7F')),
|
|
1086
|
+
'ball_stick_use_cpk_bond_color': self.bs_use_cpk_bond_checkbox.isChecked(),
|
|
1003
1087
|
}
|
|
1004
1088
|
|
|
1005
1089
|
def pick_bs_bond_color(self):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/constrained_optimization_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy-1.17.1 → moleditpy-1.18.0}/src/moleditpy/modules/main_window_molecular_parsers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|