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