MoleditPy-linux 1.16.1a3__py3-none-any.whl → 1.16.3__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.
@@ -4,7 +4,7 @@ from PyQt6.QtGui import QFont, QColor
4
4
  from rdkit import Chem
5
5
 
6
6
  #Version
7
- VERSION = '1.16.1a3'
7
+ VERSION = '1.16.3'
8
8
 
9
9
  ATOM_RADIUS = 18
10
10
  BOND_OFFSET = 3.5
@@ -1579,6 +1579,9 @@ class MainWindowMainInit(object):
1579
1579
  # Color overrides
1580
1580
  'ball_stick_bond_color': '#7F7F7F',
1581
1581
  'cpk_colors': {}, # symbol->hex overrides
1582
+ # Whether to kekulize aromatic systems for 3D display
1583
+ 'display_kekule_3d': False,
1584
+ 'always_ask_charge': False,
1582
1585
  }
1583
1586
 
1584
1587
  try:
@@ -557,92 +557,173 @@ class MainWindowMolecularParsers(object):
557
557
  # Accept the candidate
558
558
  return candidate_mol
559
559
 
560
- # Silent first attempt
560
+ # Decide whether to silently try charge=0 first, or prompt user first.
561
+ always_ask = bool(self.settings.get('always_ask_charge', False))
562
+
561
563
  try:
562
- final_mol = _process_with_charge(0)
563
- except RuntimeError:
564
- # DetermineBonds explicitly failed for charge=0. In this
565
- # situation, repeatedly prompt the user for charges until
566
- # DetermineBonds succeeds or the user cancels.
567
- while True:
568
- charge_val, ok, skip_flag = prompt_for_charge()
569
- if not ok:
570
- # user cancelled the prompt -> abort
571
- return None
572
- if skip_flag:
573
- # User selected Skip chemistry: attempt distance-based salvage
574
- try:
575
- self.estimate_bonds_from_distances(mol)
576
- except Exception:
577
- pass
578
- salvaged = None
579
- try:
580
- salvaged = mol.GetMol()
581
- except Exception:
582
- salvaged = None
564
+ if not always_ask:
565
+ # Silent first attempt (existing behavior)
566
+ try:
567
+ final_mol = _process_with_charge(0)
568
+ except RuntimeError:
569
+ # DetermineBonds explicitly failed for charge=0. In this
570
+ # situation, repeatedly prompt the user for charges until
571
+ # DetermineBonds succeeds or the user cancels.
572
+ while True:
573
+ charge_val, ok, skip_flag = prompt_for_charge()
574
+ if not ok:
575
+ # user cancelled the prompt -> abort
576
+ return None
577
+ if skip_flag:
578
+ # User selected Skip chemistry: attempt distance-based salvage
579
+ try:
580
+ self.estimate_bonds_from_distances(mol)
581
+ except Exception:
582
+ pass
583
+ salvaged = None
584
+ try:
585
+ salvaged = mol.GetMol()
586
+ except Exception:
587
+ salvaged = None
588
+
589
+ if salvaged is not None:
590
+ try:
591
+ salvaged.SetIntProp("_xyz_skip_checks", 1)
592
+ except Exception:
593
+ try:
594
+ salvaged._xyz_skip_checks = True
595
+ except Exception:
596
+ pass
597
+ final_mol = salvaged
598
+ break
599
+ else:
600
+ # Could not salvage; abort
601
+ try:
602
+ self.statusBar().showMessage("Skip chemistry selected but failed to create salvaged molecule.")
603
+ except Exception:
604
+ pass
605
+ return None
583
606
 
584
- if salvaged is not None:
585
607
  try:
586
- salvaged.SetIntProp("_xyz_skip_checks", 1)
587
- except Exception:
608
+ final_mol = _process_with_charge(charge_val)
609
+ # success -> break out of prompt loop
610
+ break
611
+ except RuntimeError:
612
+ # DetermineBonds still failing for this charge -> loop again
588
613
  try:
589
- salvaged._xyz_skip_checks = True
614
+ self.statusBar().showMessage("DetermineBonds failed for that charge; please try a different total charge or cancel.")
590
615
  except Exception:
591
616
  pass
592
- final_mol = salvaged
593
- break
594
- else:
595
- # Could not salvage; abort
617
+ continue
618
+ except Exception as e_prompt:
619
+ # Some other failure occurred after DetermineBonds or in
620
+ # finalization. If skip_chemistry_checks is enabled we
621
+ # try the salvaged mol once; otherwise prompt again.
622
+ try:
623
+ skip_checks = bool(self.settings.get('skip_chemistry_checks', False))
624
+ except Exception:
625
+ skip_checks = False
626
+
627
+ salvaged = None
628
+ try:
629
+ salvaged = mol.GetMol()
630
+ except Exception:
631
+ salvaged = None
632
+
633
+ if skip_checks and salvaged is not None:
634
+ final_mol = salvaged
635
+ # mark salvaged molecule as produced under skip_checks
636
+ try:
637
+ final_mol.SetIntProp("_xyz_skip_checks", 1)
638
+ except Exception:
639
+ try:
640
+ final_mol._xyz_skip_checks = True
641
+ except Exception:
642
+ pass
643
+ break
644
+ else:
645
+ try:
646
+ self.statusBar().showMessage(f"Retry failed: {e_prompt}")
647
+ except Exception:
648
+ pass
649
+ # Continue prompting
650
+ continue
651
+ else:
652
+ # User has requested to always be asked for charge — prompt before any silent try
653
+ while True:
654
+ charge_val, ok, skip_flag = prompt_for_charge()
655
+ if not ok:
656
+ # user cancelled the prompt -> abort
657
+ return None
658
+ if skip_flag:
659
+ # User selected Skip chemistry: attempt distance-based salvage
596
660
  try:
597
- self.statusBar().showMessage("Skip chemistry selected but failed to create salvaged molecule.")
661
+ self.estimate_bonds_from_distances(mol)
598
662
  except Exception:
599
663
  pass
600
- return None
601
-
602
- try:
603
- final_mol = _process_with_charge(charge_val)
604
- # success -> break out of prompt loop
605
- break
606
- except RuntimeError:
607
- # DetermineBonds still failing for this charge -> loop again
608
- try:
609
- self.statusBar().showMessage("DetermineBonds failed for that charge; please try a different total charge or cancel.")
610
- except Exception:
611
- pass
612
- continue
613
- except Exception as e_prompt:
614
- # Some other failure occurred after DetermineBonds or in
615
- # finalization. If skip_chemistry_checks is enabled we
616
- # try the salvaged mol once; otherwise prompt again.
617
- try:
618
- skip_checks = bool(self.settings.get('skip_chemistry_checks', False))
619
- except Exception:
620
- skip_checks = False
621
-
622
- salvaged = None
623
- try:
624
- salvaged = mol.GetMol()
625
- except Exception:
626
664
  salvaged = None
627
-
628
- if skip_checks and salvaged is not None:
629
- final_mol = salvaged
630
- # mark salvaged molecule as produced under skip_checks
631
665
  try:
632
- final_mol.SetIntProp("_xyz_skip_checks", 1)
666
+ salvaged = mol.GetMol()
633
667
  except Exception:
668
+ salvaged = None
669
+
670
+ if salvaged is not None:
634
671
  try:
635
- final_mol._xyz_skip_checks = True
672
+ salvaged.SetIntProp("_xyz_skip_checks", 1)
673
+ except Exception:
674
+ try:
675
+ salvaged._xyz_skip_checks = True
676
+ except Exception:
677
+ pass
678
+ final_mol = salvaged
679
+ break
680
+ else:
681
+ try:
682
+ self.statusBar().showMessage("Skip chemistry selected but failed to create salvaged molecule.")
636
683
  except Exception:
637
684
  pass
685
+ return None
686
+
687
+ try:
688
+ final_mol = _process_with_charge(charge_val)
689
+ # success -> break out of prompt loop
638
690
  break
639
- else:
691
+ except RuntimeError:
692
+ # DetermineBonds still failing for this charge -> loop again
640
693
  try:
641
- self.statusBar().showMessage(f"Retry failed: {e_prompt}")
694
+ self.statusBar().showMessage("DetermineBonds failed for that charge; please try a different total charge or cancel.")
642
695
  except Exception:
643
696
  pass
644
- # Continue prompting
645
697
  continue
698
+ except Exception as e_prompt:
699
+ try:
700
+ skip_checks = bool(self.settings.get('skip_chemistry_checks', False))
701
+ except Exception:
702
+ skip_checks = False
703
+
704
+ salvaged = None
705
+ try:
706
+ salvaged = mol.GetMol()
707
+ except Exception:
708
+ salvaged = None
709
+
710
+ if skip_checks and salvaged is not None:
711
+ final_mol = salvaged
712
+ try:
713
+ final_mol.SetIntProp("_xyz_skip_checks", 1)
714
+ except Exception:
715
+ try:
716
+ final_mol._xyz_skip_checks = True
717
+ except Exception:
718
+ pass
719
+ break
720
+ else:
721
+ try:
722
+ self.statusBar().showMessage(f"Retry failed: {e_prompt}")
723
+ except Exception:
724
+ pass
725
+ continue
726
+
646
727
  except Exception:
647
728
  # If the silent attempt failed for reasons other than
648
729
  # DetermineBonds failing (e.g., finalization errors), fall
@@ -156,11 +156,30 @@ class MainWindowView3d(object):
156
156
  self.plotter.add_light(light)
157
157
 
158
158
  # 5. 分子描画ロジック
159
+ # Optionally kekulize aromatic systems for 3D visualization.
160
+ mol_to_draw = mol
161
+ if self.settings.get('display_kekule_3d', False):
162
+ try:
163
+ # Operate on a copy to avoid mutating the original molecule
164
+ mol_to_draw = Chem.Mol(mol)
165
+ Chem.Kekulize(mol_to_draw, clearAromaticFlags=True)
166
+ except Exception as e:
167
+ # Kekulize failed; keep original and warn user
168
+ try:
169
+ self.statusBar().showMessage(f"Kekulize failed: {e}")
170
+ except Exception:
171
+ pass
172
+ mol_to_draw = mol
173
+
174
+ # Use the original molecule's conformer (positions) to ensure coordinates
175
+ # are preserved even when we create a kekulized copy for bond types.
159
176
  conf = mol.GetConformer()
160
177
 
161
- self.atom_positions_3d = np.array([list(conf.GetAtomPosition(i)) for i in range(mol.GetNumAtoms())])
178
+ # Use the kekulized molecule's atom ordering for color/size decisions
179
+ self.atom_positions_3d = np.array([list(conf.GetAtomPosition(i)) for i in range(mol_to_draw.GetNumAtoms())])
162
180
 
163
- sym = [a.GetSymbol() for a in mol.GetAtoms()]
181
+ # Use the possibly-kekulized molecule for symbol/bond types
182
+ sym = [a.GetSymbol() for a in mol_to_draw.GetAtoms()]
164
183
  col = np.array([CPK_COLORS_PV.get(s, [0.5, 0.5, 0.5]) for s in sym])
165
184
 
166
185
  # スタイルに応じて原子の半径を設定(設定から読み込み)
@@ -239,7 +258,7 @@ class MainWindowView3d(object):
239
258
  except Exception:
240
259
  bs_bond_rgb = [127, 127, 127]
241
260
 
242
- for bond in mol.GetBonds():
261
+ for bond in mol_to_draw.GetBonds():
243
262
  begin_atom_idx = bond.GetBeginAtomIdx()
244
263
  end_atom_idx = bond.GetEndAtomIdx()
245
264
  sp = np.array(conf.GetAtomPosition(begin_atom_idx))
@@ -313,7 +332,7 @@ class MainWindowView3d(object):
313
332
  if bt == Chem.rdchem.BondType.DOUBLE:
314
333
  r = cyl_radius * double_radius_factor
315
334
  # 二重結合の場合、結合している原子の他の結合を考慮してオフセット方向を決定
316
- off_dir = self._calculate_double_bond_offset(mol, bond, conf)
335
+ off_dir = self._calculate_double_bond_offset(mol_to_draw, bond, conf)
317
336
  # 設定から二重結合のオフセットファクターを適用
318
337
  s_double = cyl_radius * double_offset_factor
319
338
  c1, c2 = c + off_dir * (s_double / 2), c - off_dir * (s_double / 2)
@@ -432,7 +451,10 @@ class MainWindowView3d(object):
432
451
  # E/Zラベルも表示
433
452
  if getattr(self, 'show_chiral_labels', False):
434
453
  try:
435
- self.show_ez_labels_3d(mol)
454
+ # If we drew a kekulized molecule use it for E/Z detection so
455
+ # E/Z labels reflect Kekulé rendering; pass mol_to_draw as the
456
+ # molecule to scan for bond stereochemistry.
457
+ self.show_ez_labels_3d(mol, scan_mol=mol_to_draw)
436
458
  except Exception as e:
437
459
  self.statusBar().showMessage(f"3D E/Z label drawing error: {e}")
438
460
 
@@ -561,7 +583,7 @@ class MainWindowView3d(object):
561
583
 
562
584
 
563
585
 
564
- def show_ez_labels_3d(self, mol):
586
+ def show_ez_labels_3d(self, mol, scan_mol=None):
565
587
  """3DビューでE/Zラベルを表示する(RDKitのステレオ化学判定を使用)"""
566
588
  if not mol:
567
589
  return
@@ -588,11 +610,19 @@ class MainWindowView3d(object):
588
610
  pass
589
611
 
590
612
  # 二重結合でRDKitが判定したE/Z立体化学を表示
591
- for bond in mol.GetBonds():
613
+ # `scan_mol` is used for stereochemistry detection (bond types); default
614
+ # to the provided molecule if not supplied.
615
+ if scan_mol is None:
616
+ scan_mol = mol
617
+
618
+ for bond in scan_mol.GetBonds():
592
619
  if bond.GetBondType() == Chem.BondType.DOUBLE:
593
620
  stereo = bond.GetStereo()
594
621
  if stereo in [Chem.BondStereo.STEREOE, Chem.BondStereo.STEREOZ]:
595
622
  # 結合の中心座標を計算
623
+ # Use positions from the original molecule's conformer; `bond` may
624
+ # come from `scan_mol` which can be kekulized but position indices
625
+ # correspond to the original `mol`.
596
626
  begin_pos = np.array(conf.GetAtomPosition(bond.GetBeginAtomIdx()))
597
627
  end_pos = np.array(conf.GetAtomPosition(bond.GetEndAtomIdx()))
598
628
  center_pos = (begin_pos + end_pos) / 2
@@ -1083,15 +1113,33 @@ class MainWindowView3d(object):
1083
1113
  Only keys present in self.settings['cpk_colors'] are changed; other elements keep the defaults.
1084
1114
  """
1085
1115
  try:
1116
+ # Overridden CPK settings are stored in self.settings['cpk_colors'].
1117
+ # To ensure that 2D modules (e.g., atom_item.py) which imported the
1118
+ # `CPK_COLORS` mapping from `modules.constants` at import time see
1119
+ # updates, mutate the mapping in-place on the constants module
1120
+ # instead of rebinding a new local variable here.
1086
1121
  overrides = self.settings.get('cpk_colors', {}) or {}
1087
- # Start from a clean copy of the defaults
1088
- global CPK_COLORS, CPK_COLORS_PV
1089
- CPK_COLORS = {k: QColor(v) if not isinstance(v, QColor) else QColor(v) for k, v in DEFAULT_CPK_COLORS.items()}
1122
+
1123
+ # Import the constants module so we can update mappings directly
1124
+ try:
1125
+ from . import constants as constants_mod
1126
+ except Exception:
1127
+ import modules.constants as constants_mod
1128
+
1129
+ # Reset constants.CPK_COLORS to defaults but keep the same dict
1130
+ constants_mod.CPK_COLORS.clear()
1131
+ for k, v in DEFAULT_CPK_COLORS.items():
1132
+ constants_mod.CPK_COLORS[k] = QColor(v) if not isinstance(v, QColor) else v
1133
+
1134
+ # Apply overrides from settings
1090
1135
  for k, hexv in overrides.items():
1091
1136
  if isinstance(hexv, str) and hexv:
1092
- CPK_COLORS[k] = QColor(hexv)
1093
- # Rebuild PV version
1094
- CPK_COLORS_PV = {k: [c.redF(), c.greenF(), c.blueF()] for k, c in CPK_COLORS.items()}
1137
+ constants_mod.CPK_COLORS[k] = QColor(hexv)
1138
+
1139
+ # Rebuild the PV representation in-place too
1140
+ constants_mod.CPK_COLORS_PV.clear()
1141
+ for k, c in constants_mod.CPK_COLORS.items():
1142
+ constants_mod.CPK_COLORS_PV[k] = [c.redF(), c.greenF(), c.blueF()]
1095
1143
  except Exception as e:
1096
1144
  print(f"Failed to update CPK colors from settings: {e}")
1097
1145
 
@@ -727,61 +727,130 @@ class MoleculeScene(QGraphicsScene):
727
727
 
728
728
  # --- フューズされた辺の数による条件分岐 ---
729
729
  if len(existing_orders) >= 2:
730
- # 2辺以上フューズ: 単純に既存の辺の次数とテンプレートの辺の次数が一致するものを最優先する
731
- # (この場合、新しい環を交互配置にするのは難しく、単に既存の構造を壊さないことを優先)
730
+
732
731
  for rot in range(num_points):
733
- current_score = sum(100 for k, exist_order in existing_orders.items()
734
- if orig_orders[(k + rot) % num_points] == exist_order)
732
+ match_double_count = 0
733
+ match_bonus = 0
734
+ mismatch_penalty = 0
735
+
736
+ # 【新規追加】接続部(Legs)の安全性チェック
737
+ # フューズ領域の両隣(テンプレート側)が「単結合(1)」であることを強く推奨する
738
+ # これにより、既存構造との接続点での原子価オーバー(手が5本になる)を防ぐ
739
+ safe_connection_score = 0
740
+
741
+ # フューズ領域の開始と終了を探す(インデックス集合から判定)
742
+ fused_indices = sorted(list(existing_orders.keys()))
743
+ # 連続領域と仮定して、端のインデックスを取得
744
+ # (0と5がつながっている環状のケースも考慮すべきだが、簡易的に最小/最大で判定し、
745
+ # もし飛び地なら不整合ペナルティで自然と落ちる)
746
+
747
+ # 簡易的な隣接チェック:
748
+ # フューズに使われる辺集合に含まれない「その隣」の辺を見る
749
+ for k in existing_orders:
750
+ # 左隣を見る
751
+ prev_idx = (k - 1 + rot) % num_points
752
+ # 右隣を見る
753
+ next_idx = (k + 1 + rot) % num_points
754
+
755
+ # もし隣がフューズ領域外(=接続部)なら、その次数をチェック
756
+ # 注意: existing_ordersのキーは「配置位置(k)」
757
+ # rotはテンプレートのズレ。
758
+ # テンプレート上の該当エッジの次数は orig_orders[(neighbor_k + rot)] ではなく
759
+ # orig_orders[neighbor_template_index]
760
+
761
+ # 正確なロジック:
762
+ # 今、配置位置 k にテンプレートの bond (k+rot) が来ている。
763
+ # 配置位置 k の「隣のボンド」ではなく、
764
+ # 「テンプレート上で」そのボンドの両隣にあるボンドが、今回のフューズに使われていないか確認する。
765
+ pass
766
+
767
+ # --- シンプルな実装: 全ての非フューズ辺(外周になる辺)をチェック ---
768
+ # 「フューズに使われていない辺」が単結合か二重結合かで加点
769
+ # ピレンの場合(3辺フューズ)、残り3辺が外周。
770
+ # ベンゼン(D-S-D-S-D-S)において、D-S-Dでフューズすると、残りはS-D-S。
771
+ # 接合部(Legs)にあたるのは、残りのS-D-Sの両端のS。これが重要。
772
+
773
+ # テンプレートの結合次数配列
774
+ current_template_orders = [orig_orders[(i + rot) % num_points] for i in range(num_points)]
775
+
776
+ # フューズ領域の両端を特定するために、
777
+ # 「フューズしているk」に対応するテンプレート側のインデックスを集める
778
+ used_template_indices = set((k + rot) % num_points for k in existing_orders)
779
+
780
+ # テンプレート上で「使われている領域」の両隣(接続部)が「1(単結合)」なら超高得点
781
+ for t_idx in used_template_indices:
782
+ # そのボンドのテンプレート上の左隣
783
+ adj_l = (t_idx - 1) % num_points
784
+ # そのボンドのテンプレート上の右隣
785
+ adj_r = (t_idx + 1) % num_points
786
+
787
+ # もし隣が「使われていない」なら、それは接続部である
788
+ if adj_l not in used_template_indices:
789
+ if orig_orders[adj_l] == 1: safe_connection_score += 5000
790
+
791
+ if adj_r not in used_template_indices:
792
+ if orig_orders[adj_r] == 1: safe_connection_score += 5000
793
+
794
+ # 既存のスコア計算
795
+ for k, exist_order in existing_orders.items():
796
+ template_ord = orig_orders[(k + rot) % num_points]
797
+ if template_ord == exist_order:
798
+ match_bonus += 100
799
+ if exist_order == 2: match_double_count += 1
800
+ else:
801
+ # 不一致でも、Legsが安全なら許容したいのでペナルティは控えめに、
802
+ # または safe_connection_score が圧倒的に勝つようにする
803
+ mismatch_penalty += 50
804
+
805
+ # 最終スコア: 接続部の安全性を最優先
806
+ current_score = safe_connection_score + (match_double_count * 1000) + match_bonus - mismatch_penalty
807
+
735
808
  if current_score > max_score:
736
809
  max_score = current_score
737
810
  best_rot = rot
738
811
 
739
812
  elif len(existing_orders) == 1:
740
- # 1辺フューズ: 既存の辺を維持しつつ、その両隣で「反転一致」を達成し、新しい環を交互配置にする
741
-
742
- # フューズされた辺のインデックスと次数を取得
813
+ # 1辺フューズ
743
814
  k_fuse = next(iter(existing_orders.keys()))
744
815
  exist_order = existing_orders[k_fuse]
745
816
 
746
- # 目標: フューズされた辺の両隣(k-1とk+1)に来るテンプレートの次数が、既存の辺の次数と逆であること
747
- # k_adj_1 -> (k_fuse - 1) % 6
748
- # k_adj_2 -> (k_fuse + 1) % 6
749
-
750
817
  for rot in range(num_points):
751
818
  current_score = 0
752
819
  rotated_template_order = orig_orders[(k_fuse + rot) % num_points]
753
820
 
754
- # 1. まず、フューズされた辺自体が次数を反転させられる位置にあるかチェック(必須ではないが、回転を絞る)
821
+ # 1. 接合部の次数マッチング
822
+
823
+ # パターンA: 交互配置(既存と逆)
755
824
  if (exist_order == 1 and rotated_template_order == 2) or \
756
825
  (exist_order == 2 and rotated_template_order == 1):
757
- current_score += 100 # 大幅ボーナス: 理想的な回転
826
+ current_score += 100
758
827
 
759
- # 2. 次に、両隣の辺の次数をチェック(交互配置維持の主目的)
760
- # 既存辺の両隣は、新規に作成されるため、テンプレートの次数でボンドが作成されます。
761
- # ここで、テンプレートの次数が既存辺の次数と逆になる回転を選ぶ必要があります。
762
-
763
- # テンプレートの辺は、回転後のk_fuseの両隣(m_adj1, m_adj2)
828
+ # 【追加変更点2】二重結合の重ね合わせ(共役維持)
829
+ # 既存が二重結合で、テンプレートも二重結合なら、ここで1つ消費される
830
+ elif (exist_order == 2 and rotated_template_order == 2):
831
+ current_score += 100
832
+
833
+ # 2. 両隣の辺の次数チェック(交互配置の維持を確認)
764
834
  m_adj1 = (k_fuse - 1 + rot) % num_points
765
835
  m_adj2 = (k_fuse + 1 + rot) % num_points
766
-
767
836
  neighbor_order_1 = orig_orders[m_adj1]
768
837
  neighbor_order_2 = orig_orders[m_adj2]
769
838
 
770
- # 既存が単結合(1)の場合、両隣は二重結合(2)であってほしい
771
839
  if exist_order == 1:
840
+ # 接合部が単なら、隣は二重であってほしい
772
841
  if neighbor_order_1 == 2: current_score += 50
773
842
  if neighbor_order_2 == 2: current_score += 50
774
843
 
775
- # 既存が二重結合(2)の場合、両隣は単結合(1)であってほしい
776
844
  elif exist_order == 2:
845
+ # 接合部が二重なら、隣は単であってほしい
777
846
  if neighbor_order_1 == 1: current_score += 50
778
847
  if neighbor_order_2 == 1: current_score += 50
779
848
 
780
- # 3. タイブレーク: その他の既存結合(フューズ辺ではない)との次数一致度も加味
849
+ # 3. タイブレーク(他の接触しない辺との整合性など)
781
850
  for k, e_order in existing_orders.items():
782
851
  if k != k_fuse:
783
852
  r_t_order = orig_orders[(k + rot) % num_points]
784
- if r_t_order == e_order: current_score += 10 # 既存構造維持のボーナス
853
+ if r_t_order == e_order: current_score += 10
785
854
 
786
855
  if current_score > max_score:
787
856
  max_score = current_score
@@ -62,11 +62,18 @@ class SettingsDialog(QDialog):
62
62
  # element symbol recognition will be coerced where possible and Chem.SanitizeMol
63
63
  # failures will be ignored so the 3D viewer can still display the structure.
64
64
  'skip_chemistry_checks': False,
65
+ # When True, always prompt the user for molecular charge on XYZ import
66
+ # instead of silently trying charge=0 first. Default True to disable
67
+ # the silent 'charge=0' test.
68
+ 'always_ask_charge': False,
65
69
  # 3D conversion/optimization defaults
66
70
  '3d_conversion_mode': 'fallback',
67
71
  'optimization_method': 'MMFF_RDKIT',
68
72
  'ball_stick_bond_color': '#7F7F7F',
69
73
  'cpk_colors': {},
74
+ # If True, RDKit will attempt to kekulize aromatic systems for 3D display
75
+ # (shows alternating single/double bonds rather than aromatic circles)
76
+ 'display_kekule_3d': False,
70
77
  }
71
78
 
72
79
  # --- 選択された色を管理する専用のインスタンス変数 ---
@@ -197,7 +204,7 @@ class SettingsDialog(QDialog):
197
204
 
198
205
  # 化学チェックスキップオプション(otherタブに移動)
199
206
  self.skip_chem_checks_checkbox = QCheckBox()
200
- self.skip_chem_checks_checkbox.setToolTip("When enabled, file import will try to ignore chemical/sanitization errors and allow viewing malformed files.")
207
+ self.skip_chem_checks_checkbox.setToolTip("When enabled, XYZ file import will try to ignore chemical/sanitization errors and allow viewing malformed files.")
201
208
  # Immediately persist change to settings when user toggles the checkbox
202
209
  try:
203
210
  self.skip_chem_checks_checkbox.stateChanged.connect(lambda s: self._on_skip_chem_checks_changed(s))
@@ -206,7 +213,27 @@ class SettingsDialog(QDialog):
206
213
 
207
214
  # Add the checkbox to the other tab's form
208
215
  try:
209
- self.other_form_layout.addRow("Skip chemistry checks on import xyz file:", self.skip_chem_checks_checkbox)
216
+ self.other_form_layout.addRow("Skip chemistry checks on import XYZ file:", self.skip_chem_checks_checkbox)
217
+ except Exception:
218
+ pass
219
+
220
+ # 3D Kekule display option (under Other) will be added below the
221
+ # 'Always ask molecular charge on import' option so ordering is clear
222
+ # in the UI.
223
+ self.kekule_3d_checkbox = QCheckBox()
224
+ self.kekule_3d_checkbox.setToolTip("When enabled, aromatic bonds will be kekulized in the 3D view (show alternating single/double bonds).")
225
+ # Don't persist kekule state immediately; Apply/OK should commit setting.
226
+ # Always ask charge on XYZ import (skip silent charge=0 test)
227
+ self.always_ask_charge_checkbox = QCheckBox()
228
+ self.always_ask_charge_checkbox.setToolTip("Prompt for overall molecular charge when importing XYZ files instead of silently trying charge=0 first.")
229
+ try:
230
+ self.other_form_layout.addRow("Always ask molecular charge on import XYZ file:", self.always_ask_charge_checkbox)
231
+ except Exception:
232
+ pass
233
+
234
+ # Place the Kekulé option after the always-ask-charge option
235
+ try:
236
+ self.other_form_layout.addRow("Display Kekulé bonds in 3D:", self.kekule_3d_checkbox)
210
237
  except Exception:
211
238
  pass
212
239
 
@@ -578,6 +605,8 @@ class SettingsDialog(QDialog):
578
605
  "Other": {
579
606
  # other options
580
607
  'skip_chemistry_checks': self.default_settings.get('skip_chemistry_checks', False),
608
+ 'display_kekule_3d': self.default_settings.get('display_kekule_3d', False),
609
+ 'always_ask_charge': self.default_settings.get('always_ask_charge', False),
581
610
  },
582
611
  "Ball & Stick": {
583
612
  'ball_stick_atom_scale': self.default_settings['ball_stick_atom_scale'],
@@ -899,6 +928,10 @@ class SettingsDialog(QDialog):
899
928
  self.projection_combo.setCurrentIndex(idx if idx != -1 else 0)
900
929
  # skip chemistry checks
901
930
  self.skip_chem_checks_checkbox.setChecked(settings_dict.get('skip_chemistry_checks', self.default_settings.get('skip_chemistry_checks', False)))
931
+ # kekule setting
932
+ self.kekule_3d_checkbox.setChecked(settings_dict.get('display_kekule_3d', self.default_settings.get('display_kekule_3d', False)))
933
+ # always ask for charge on XYZ imports
934
+ self.always_ask_charge_checkbox.setChecked(settings_dict.get('always_ask_charge', self.default_settings.get('always_ask_charge', False)))
902
935
 
903
936
  def select_color(self):
904
937
  """カラーピッカーを開き、選択された色を内部変数とUIに反映させる"""
@@ -950,7 +983,9 @@ class SettingsDialog(QDialog):
950
983
  'stick_triple_bond_offset_factor': self.stick_triple_offset_slider.value() / 100.0,
951
984
  'stick_double_bond_radius_factor': self.stick_double_radius_slider.value() / 100.0,
952
985
  'stick_triple_bond_radius_factor': self.stick_triple_radius_slider.value() / 100.0,
986
+ 'display_kekule_3d': self.kekule_3d_checkbox.isChecked(),
953
987
  'skip_chemistry_checks': self.skip_chem_checks_checkbox.isChecked(),
988
+ 'always_ask_charge': self.always_ask_charge_checkbox.isChecked(),
954
989
  # Ball & Stick bond color (3D grey/uniform color)
955
990
  'ball_stick_bond_color': getattr(self, 'bs_bond_color', self.default_settings.get('ball_stick_bond_color', '#7F7F7F')),
956
991
  }
@@ -1027,6 +1062,8 @@ class SettingsDialog(QDialog):
1027
1062
  except Exception:
1028
1063
  pass
1029
1064
 
1065
+ # Note: Kekule display is applied only when user clicks Apply/OK.
1066
+
1030
1067
  def accept(self):
1031
1068
  """ダイアログの設定を適用してから閉じる"""
1032
1069
  # apply_settingsを呼び出して設定を適用
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy-linux
3
- Version: 1.16.1a3
3
+ Version: 1.16.3
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
  Project-URL: Homepage, https://github.com/HiroYokoyama/python_molecular_editor
@@ -109,7 +109,14 @@ For detailed instructions, please refer to the project [Wiki](https://github.com
109
109
  moleditpy-installer
110
110
  ```
111
111
 
112
- -----
112
+
113
+ To enable Open Babel features (e.g., alternative 3D structure generation), please install the package manually.
114
+ **Note:** Open Babel is licensed under **GPL v2**. Installing this package combines GPL code with this software.
115
+
116
+
117
+ ```bash
118
+ pip install "moleditpy[openbabel]"
119
+ ```
113
120
 
114
121
  #### Running the Application
115
122
 
@@ -225,6 +232,13 @@ This project is licensed under the **Apache-2.0 License**. See the `LICENSE` fil
225
232
  moleditpy-installer
226
233
  ```
227
234
 
235
+ Open Babel 機能(代替的な3D構造生成など)を有効にする場合は、以下のコマンドで個別にインストールしてください。
236
+ **注意:** Open Babel は **GPL v2** ライセンスです。このパッケージをインストールすると、GPLの条項が適用される可能性があります。
237
+
238
+ ```bash
239
+ pip install "moleditpy[openbabel]"
240
+ ```
241
+
228
242
  #### アプリケーションの起動
229
243
 
230
244
  ```bash
@@ -12,7 +12,7 @@ moleditpy_linux/modules/bond_item.py,sha256=zjQHa4vb8xhS9B7cYPRM0nak-f7lr5NQ1uAj
12
12
  moleditpy_linux/modules/bond_length_dialog.py,sha256=xlx-bU3tVeLfShdVRw6_Geo5Gl9mztlIfTdT9tJ6WMA,14579
13
13
  moleditpy_linux/modules/calculation_worker.py,sha256=detE48BW08a2tvmKjMgz8zCShgARzsRH-ABmWrPcqZA,42055
14
14
  moleditpy_linux/modules/color_settings_dialog.py,sha256=h4AOKU8dCTenecI8zOM9GfnmKDm7jfe5C4Fa23Budvs,15205
15
- moleditpy_linux/modules/constants.py,sha256=WjilRvqiXFWNenqWLdGAfr7iTPEvSp_nojw97fcgrso,4436
15
+ moleditpy_linux/modules/constants.py,sha256=BvPXTjQLBc-sCxWxhcbYctejy9Hfe7tCs9KGwL04tRw,4434
16
16
  moleditpy_linux/modules/constrained_optimization_dialog.py,sha256=MlWnPze0JJvnqmHx9n3qZWG_h-2kZymT0PQ6lALbCro,29861
17
17
  moleditpy_linux/modules/custom_interactor_style.py,sha256=K_uGM6FezY0kZ3zPqoR6f0nowG40ytt-L4UCAbPlwGM,38184
18
18
  moleditpy_linux/modules/custom_qt_interactor.py,sha256=6mzaVb3Mhp-4nryG5AraEvPPgBJpotrzVYwrpCAKmVo,2186
@@ -25,20 +25,20 @@ moleditpy_linux/modules/main_window_dialog_manager.py,sha256=5WU6mFABB0aI4XCywP-
25
25
  moleditpy_linux/modules/main_window_edit_3d.py,sha256=FStBWVeDVAM2MoO-JCTjPM-G7iT8QZUHxsb0dS4MEAI,19553
26
26
  moleditpy_linux/modules/main_window_edit_actions.py,sha256=8tR0rYfgWYgdKTxBP4snzpxhiD2DExSKyf4jzSWb6sE,64598
27
27
  moleditpy_linux/modules/main_window_export.py,sha256=f_Z4qVYKBTe06lGTFqjd3deluUdkQvHhZYa81h7UpBM,34465
28
- moleditpy_linux/modules/main_window_main_init.py,sha256=xAstCr__601hkbb1IpqpQKUFTSS6quYe66sBOgqJjkc,75228
29
- moleditpy_linux/modules/main_window_molecular_parsers.py,sha256=8JAIgr1axzmJqX_Ue-Adkl8e_8B2Th9yutQbau8EEWQ,43401
28
+ moleditpy_linux/modules/main_window_main_init.py,sha256=lXJ_Zl9xGwa3P7NbFlnCttEG9Ta9vQgov7iNOEVY9NE,75377
29
+ moleditpy_linux/modules/main_window_molecular_parsers.py,sha256=tyzVb-TqyjCiEUacwEJJi7zVokjwColezrRlRxfmzks,48174
30
30
  moleditpy_linux/modules/main_window_project_io.py,sha256=2ArkW23L4ahQIiktCCXlNsJphU0awO5YzJGihIJsn1c,17021
31
31
  moleditpy_linux/modules/main_window_string_importers.py,sha256=yrZblvPG840qnqVEJf__XVfNnWl_r3vt68Abfs2aYDQ,10674
32
32
  moleditpy_linux/modules/main_window_ui_manager.py,sha256=0jdTZGv5JRtDlDniblPKzLPXdfUBZ3qh12s6pav4ihI,22038
33
- moleditpy_linux/modules/main_window_view_3d.py,sha256=aU6fI-ZYUV7qOQmucsF5WuafGYyvb4P2xj0oIgsnDaU,55443
33
+ moleditpy_linux/modules/main_window_view_3d.py,sha256=DgEnR4IlOVjMn45JdILEU7krUU6d5AcWHJDkhXFpJKM,57990
34
34
  moleditpy_linux/modules/main_window_view_loaders.py,sha256=WuzLCYC22eaDFIvUvRtXgULZb-n4B04gcdgSKqTgWGA,14234
35
35
  moleditpy_linux/modules/mirror_dialog.py,sha256=wYlnqrxAZfsADB5Gvabe-MoX3j0_NjfmWPyf3GCYj9U,4427
36
36
  moleditpy_linux/modules/molecular_data.py,sha256=OCdiRIDXgnqYCKmf56x6XfbOJYTEQjY-MtBfnYZtTWY,12981
37
- moleditpy_linux/modules/molecule_scene.py,sha256=F7W7HLctfqbtM0gI76fcyEar6Q5t_rWoy1cAPUTYrMg,90222
37
+ moleditpy_linux/modules/molecule_scene.py,sha256=ULUYAq4Hmz1e1V8E4xxplw00L_NTq5XmAxJn9NUoCoU,94612
38
38
  moleditpy_linux/modules/move_group_dialog.py,sha256=MVVdy0R-HIHcsCWD2yBVDWoDN4NFXPkOc72dq9laBP4,26905
39
39
  moleditpy_linux/modules/periodic_table_dialog.py,sha256=slh1X-6YidaQGzQamrKJ6aetIMKTQLRlBfaAH6B6qfw,3737
40
40
  moleditpy_linux/modules/planarize_dialog.py,sha256=u8IZGUEIXnVrBOXBqJefQpFqz3wiU6oLXp4gBNcC7Iw,8402
41
- moleditpy_linux/modules/settings_dialog.py,sha256=ylofChBPAvUH-wkn_UGNqinYkKoEQKULis8i80Wzh9Y,58264
41
+ moleditpy_linux/modules/settings_dialog.py,sha256=7pVPc4zg6l4FXb1b_2gr0aYXufomFDJqa0yUtS54dgA,60658
42
42
  moleditpy_linux/modules/template_preview_item.py,sha256=KDuLEZpPSMm9ZB0z5ms8LZyHbFKvszemG0XnR5vi0qg,6404
43
43
  moleditpy_linux/modules/template_preview_view.py,sha256=AXUaFJR0E1yX9dBY9IDbxYNsBTcRFZWnDwpS_pRZXhs,3081
44
44
  moleditpy_linux/modules/translation_dialog.py,sha256=gIG_mz4wc4y4ZNq02Ql33ek6B-DrOf1pdWF3FsViCT4,14394
@@ -47,8 +47,8 @@ moleditpy_linux/modules/zoomable_view.py,sha256=ZgAmmWXIKtx7AhMjs6H6PCyvb_kpYuan
47
47
  moleditpy_linux/modules/assets/icon.icns,sha256=wD5R6-Vw7K662tVKhu2E1ImN0oUuyAP4youesEQsn9c,139863
48
48
  moleditpy_linux/modules/assets/icon.ico,sha256=RfgFcx7-dHY_2STdsOQCQziY5SNhDr3gPnjO6jzEDPI,147975
49
49
  moleditpy_linux/modules/assets/icon.png,sha256=kCFN1WacYIdy0GN6SFEbNA00ef39pCczBnFdkkBI8Bs,147110
50
- moleditpy_linux-1.16.1a3.dist-info/METADATA,sha256=HNzP5MU9NSJUXdXDMkEME57oFZytrmYtKyeRs_A2jD0,17437
51
- moleditpy_linux-1.16.1a3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
52
- moleditpy_linux-1.16.1a3.dist-info/entry_points.txt,sha256=-OzipSi__yVwlimNtu3eiRP5t5UMg55Cs0udyhXYiyw,60
53
- moleditpy_linux-1.16.1a3.dist-info/top_level.txt,sha256=qyqe-hDYL6CXyin9E5Me5rVl3PG84VqiOjf9bQvfJLs,16
54
- moleditpy_linux-1.16.1a3.dist-info/RECORD,,
50
+ moleditpy_linux-1.16.3.dist-info/METADATA,sha256=_WJNvQMRyhYhY6TkEutL1mCAO57RK__4WiwsMl0sNe0,18099
51
+ moleditpy_linux-1.16.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
52
+ moleditpy_linux-1.16.3.dist-info/entry_points.txt,sha256=-OzipSi__yVwlimNtu3eiRP5t5UMg55Cs0udyhXYiyw,60
53
+ moleditpy_linux-1.16.3.dist-info/top_level.txt,sha256=qyqe-hDYL6CXyin9E5Me5rVl3PG84VqiOjf9bQvfJLs,16
54
+ moleditpy_linux-1.16.3.dist-info/RECORD,,