moleditpy 0.2.0__tar.gz → 0.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moleditpy
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: A program to edit molecular structures using Python.
5
5
  Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/HiroYokoyama/python_molecular_editor
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "moleditpy"
7
7
 
8
- version = "0.2.0"
8
+ version = "0.3.0"
9
9
 
10
10
  authors = [
11
11
  { name="HiroYokoyama", email="titech.yoko.hiro@gmail.com" },
@@ -43,3 +43,6 @@ Issues = "https://github.com/HiroYokoyama/python_molecular_editor/issues"
43
43
 
44
44
  [project.scripts]
45
45
  moleditpy = "moleditpy.main:main"
46
+
47
+ [tool.setuptools.package-data]
48
+ "moleditpy" = ["assets/icon.png"]
@@ -51,8 +51,8 @@ DEFAULT_BOND_LENGTH = 75 # テンプレートで使用する標準結合長
51
51
  CLIPBOARD_MIME_TYPE = "application/x-moleditpy-fragment"
52
52
 
53
53
  CPK_COLORS = {
54
- 'H': QColor('#FFFFFF'), 'C': QColor('#222222'), 'N': QColor('#3377FF'), 'O': QColor('#FF3333'), 'F': QColor('#33FF33'),
55
- 'Cl': QColor('#33FF33'), 'Br': QColor('#A52A2A'), 'I': QColor('#9400D3'), 'S': QColor('#FFC000'), 'P': QColor('#FFA500'),
54
+ 'H': QColor('#FFFFFF'), 'C': QColor('#222222'), 'N': QColor('#3377FF'), 'O': QColor('#FF3333'), 'F': QColor('#99E6E6'),
55
+ 'Cl': QColor('#33FF33'), 'Br': QColor('#A52A2A'), 'I': QColor('#9400D3'), 'S': QColor('#FFC000'), 'P': QColor('#FF8000'),
56
56
  'Si': QColor('#DAA520'), 'B': QColor('#FA8072'), 'He': QColor('#D9FFFF'), 'Ne': QColor('#B3E3F5'), 'Ar': QColor('#80D1E3'),
57
57
  'Kr': QColor('#5CACC8'), 'Xe': QColor('#429EB0'), 'Rn': QColor('#298FA2'), 'Li': QColor('#CC80FF'), 'Na': QColor('#AB5CF2'),
58
58
  'K': QColor('#8F44D7'), 'Rb': QColor('#702EBC'), 'Cs': QColor('#561B9E'), 'Fr': QColor('#421384'), 'Be': QColor('#C2FF00'),
@@ -70,7 +70,8 @@ CPK_COLORS = {
70
70
  'Bi': QColor('#9E4FB5'), 'Po': QColor('#AB5C00'), 'At': QColor('#754F45'), 'Ac': QColor('#70ABFA'), 'Th': QColor('#00BAFF'),
71
71
  'Pa': QColor('#00A1FF'), 'U': QColor('#008FFF'), 'Np': QColor('#0080FF'), 'Pu': QColor('#006BFF'), 'Am': QColor('#545CF2'),
72
72
  'Cm': QColor('#785CE3'), 'Bk': QColor('#8A4FE3'), 'Cf': QColor('#A136D4'), 'Es': QColor('#B31FD4'), 'Fm': QColor('#B31FBA'),
73
- 'Md': QColor('#B30DA6'), 'No': QColor('#BD0D87'), 'Lr': QColor('#C70066'), 'DEFAULT': QColor('#FF1493') # Pink fallback
73
+ 'Md': QColor('#B30DA6'), 'No': QColor('#BD0D87'), 'Lr': QColor('#C70066'), 'Al': QColor('#B3A68F'), 'Y': QColor('#99FFFF'),
74
+ 'Zr': QColor('#7EE7E7'), 'Nb': QColor('#68CFCE'), 'Mo': QColor('#52B7B7'), 'DEFAULT': QColor('#FF1493') # Pink fallback
74
75
  }
75
76
  CPK_COLORS_PV = {
76
77
  k: [c.redF(), c.greenF(), c.blueF()] for k, c in CPK_COLORS.items()
@@ -91,19 +92,18 @@ def main():
91
92
  sys.exit(app.exec())
92
93
 
93
94
 
94
- # --- Data Model ---
95
95
  # --- Data Model ---
96
96
  class MolecularData:
97
97
  def __init__(self):
98
98
  self.atoms = {}
99
99
  self.bonds = {}
100
100
  self._next_atom_id = 0
101
- self.adjacency_list = {} # <--- 追加: 隣接リストを初期化
101
+ self.adjacency_list = {}
102
102
 
103
103
  def add_atom(self, symbol, pos, charge=0, radical=0):
104
104
  atom_id = self._next_atom_id
105
105
  self.atoms[atom_id] = {'symbol': symbol, 'pos': pos, 'item': None, 'charge': charge, 'radical': radical}
106
- self.adjacency_list[atom_id] = [] # <--- 追加: 新しい原子のエントリを作成
106
+ self.adjacency_list[atom_id] = []
107
107
  self._next_atom_id += 1
108
108
  return atom_id
109
109
 
@@ -124,14 +124,17 @@ class MolecularData:
124
124
  self.bonds[(id1, id2)] = bond_data
125
125
  return (id1, id2), 'created'
126
126
 
127
+
127
128
  def remove_atom(self, atom_id):
128
129
  if atom_id in self.atoms:
129
- # この原子に接続していた他の原子のリストからも、この原子IDを削除
130
+ # Safely get neighbors before deleting the atom's own entry
131
+ neighbors = self.adjacency_list.get(atom_id, [])
132
+ for neighbor_id in neighbors:
133
+ if neighbor_id in self.adjacency_list and atom_id in self.adjacency_list[neighbor_id]:
134
+ self.adjacency_list[neighbor_id].remove(atom_id)
135
+
136
+ # Now, safely delete the atom's own entry from the adjacency list
130
137
  if atom_id in self.adjacency_list:
131
- for neighbor_id in self.adjacency_list[atom_id]:
132
- if neighbor_id in self.adjacency_list and atom_id in self.adjacency_list[neighbor_id]:
133
- self.adjacency_list[neighbor_id].remove(atom_id)
134
- # この原子自身のエントリを削除
135
138
  del self.adjacency_list[atom_id]
136
139
 
137
140
  del self.atoms[atom_id]
@@ -180,8 +183,6 @@ class MolecularData:
180
183
  mol_block += "M END\n"
181
184
  return mol_block
182
185
 
183
- # main.py の MolecularDataクラス内 (約193行目あたり)
184
- # 既存の to_rdkit_mol メソッドを、以下のコードで完全に置き換えてください。
185
186
 
186
187
  def to_rdkit_mol(self):
187
188
  if not self.atoms: return None
@@ -212,7 +213,6 @@ class MolecularData:
212
213
  else:
213
214
  order = Chem.BondType.SINGLE
214
215
 
215
- # 結合を追加し、そのインデックスを直接取得する、より確実な方法に変更しました。
216
216
  bond_idx = mol.AddBond(idx1, idx2, order)
217
217
  bond = mol.GetBondWithIdx(bond_idx - 1)
218
218
 
@@ -229,7 +229,6 @@ class MolecularData:
229
229
  pass
230
230
  return mol
231
231
 
232
- # --- Custom 2D Graphics Items ---
233
232
  class AtomItem(QGraphicsItem):
234
233
  def __init__(self, atom_id, symbol, pos, charge=0, radical=0):
235
234
  super().__init__()
@@ -249,7 +248,6 @@ class AtomItem(QGraphicsItem):
249
248
  path.addEllipse(QRectF(-hit_r, -hit_r, hit_r * 2.0, hit_r * 2.0))
250
249
  return path
251
250
 
252
- # AtomItem クラス内
253
251
  def paint(self, painter, option, widget):
254
252
  color = CPK_COLORS.get(self.symbol, CPK_COLORS['DEFAULT'])
255
253
  if self.is_visible:
@@ -266,9 +264,13 @@ class AtomItem(QGraphicsItem):
266
264
  painter.setBrush(bg_brush)
267
265
  painter.setPen(Qt.PenStyle.NoPen)
268
266
  painter.drawRect(bg_rect)
269
-
267
+
270
268
  # 3. 原子記号自体を描画
271
- painter.setPen(QPen(color)) # <-- 修正:常に元素カラーで描画
269
+ # 水素だけ黒文字、それ以外は CPK カラー
270
+ if self.symbol == 'H':
271
+ painter.setPen(QPen(Qt.GlobalColor.black))
272
+ else:
273
+ painter.setPen(QPen(color))
272
274
  painter.drawText(text_rect, Qt.AlignmentFlag.AlignCenter, self.symbol)
273
275
 
274
276
 
@@ -473,7 +475,9 @@ class MoleculeScene(QGraphicsScene):
473
475
  if isinstance(item, AtomItem):
474
476
  self.start_atom = item
475
477
  if self.mode != 'select':
476
- self.temp_line=QGraphicsLineItem(QLineF(self.start_atom.pos(),self.press_pos)); self.temp_line.setPen(QPen(Qt.GlobalColor.red,2,Qt.PenStyle.DotLine)); self.addItem(self.temp_line)
478
+ self.temp_line = QGraphicsLineItem(QLineF(self.start_atom.pos(), self.press_pos))
479
+ self.temp_line.setPen(QPen(Qt.GlobalColor.red, 2, Qt.PenStyle.DotLine))
480
+ self.addItem(self.temp_line)
477
481
  else: super().mousePressEvent(event)
478
482
  elif item is None and self.mode.startswith('atom'):
479
483
  self.start_pos = self.press_pos
@@ -1308,10 +1312,16 @@ class MoleculeScene(QGraphicsScene):
1308
1312
  super().keyPressEvent(event)
1309
1313
 
1310
1314
 
1315
+ # In MoleculeScene class
1311
1316
  def find_atom_near(self, pos, tol=14.0):
1312
- for it in self.items():
1317
+ # Create a small search rectangle around the position
1318
+ search_rect = QRectF(pos.x() - tol, pos.y() - tol, 2 * tol, 2 * tol)
1319
+ nearby_items = self.items(search_rect)
1320
+
1321
+ for it in nearby_items:
1313
1322
  if isinstance(it, AtomItem):
1314
- if math.hypot(it.pos().x() - pos.x(), it.pos().y() - pos.y()) <= tol:
1323
+ # Check the precise distance only for candidate items
1324
+ if QLineF(it.pos(), pos).length() <= tol:
1315
1325
  return it
1316
1326
  return None
1317
1327
 
@@ -1382,10 +1392,10 @@ class PeriodicTableDialog(QDialog):
1382
1392
  ('Mt',7,9), ('Ds',7,10), ('Rg',7,11), ('Cn',7,12), ('Nh',7,13), ('Fl',7,14), ('Mc',7,15), ('Lv',7,16),
1383
1393
  ('Ts',7,17), ('Og',7,18),
1384
1394
  # Lanthanides (placed on a separate row)
1385
- ('Ce',8,4), ('Pr',8,5), ('Nd',8,6), ('Pm',8,7), ('Sm',8,8), ('Eu',8,9), ('Gd',8,10), ('Tb',8,11),
1395
+ ('La',8,3), ('Ce',8,4), ('Pr',8,5), ('Nd',8,6), ('Pm',8,7), ('Sm',8,8), ('Eu',8,9), ('Gd',8,10), ('Tb',8,11),
1386
1396
  ('Dy',8,12), ('Ho',8,13), ('Er',8,14), ('Tm',8,15), ('Yb',8,16), ('Lu',8,17),
1387
1397
  # Actinides (separate row)
1388
- ('Th',9,4), ('Pa',9,5), ('U',9,6), ('Np',9,7), ('Pu',9,8), ('Am',9,9), ('Cm',9,10), ('Bk',9,11),
1398
+ ('Ac',9,3), ('Th',9,4), ('Pa',9,5), ('U',9,6), ('Np',9,7), ('Pu',9,8), ('Am',9,9), ('Cm',9,10), ('Bk',9,11),
1389
1399
  ('Cf',9,12), ('Es',9,13), ('Fm',9,14), ('Md',9,15), ('No',9,16), ('Lr',9,17),
1390
1400
  ]
1391
1401
  for symbol, row, col in elements:
@@ -1578,6 +1588,14 @@ class MainWindow(QMainWindow):
1578
1588
  splitter.setStretchFactor(0, 1)
1579
1589
  splitter.setStretchFactor(1, 1)
1580
1590
 
1591
+ # ステータスバーを左右に分離するための設定
1592
+ self.status_bar = self.statusBar()
1593
+ self.formula_label = QLabel("") # 右側に表示するラベルを作成
1594
+ # 右端に余白を追加して見栄えを調整
1595
+ self.formula_label.setStyleSheet("padding-right: 8px;")
1596
+ # ラベルを右側に常時表示ウィジェットとして追加
1597
+ self.status_bar.addPermanentWidget(self.formula_label)
1598
+
1581
1599
  #self.view_2d.fitInView(self.scene.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio)
1582
1600
 
1583
1601
  toolbar = QToolBar("Main Toolbar")
@@ -1877,6 +1895,12 @@ class MainWindow(QMainWindow):
1877
1895
  clear_all_action.setShortcut(QKeySequence("Ctrl+Shift+C"))
1878
1896
  clear_all_action.triggered.connect(self.clear_all); edit_menu.addAction(clear_all_action)
1879
1897
 
1898
+ view_menu = menu_bar.addMenu("&View")
1899
+ self.toggle_chiral_action = QAction("Show Chiral Labels", self, checkable=True)
1900
+ self.toggle_chiral_action.setChecked(self.show_chiral_labels)
1901
+ self.toggle_chiral_action.triggered.connect(self.toggle_chiral_labels_display)
1902
+ view_menu.addAction(self.toggle_chiral_action)
1903
+
1880
1904
  analysis_menu = menu_bar.addMenu("&Analysis")
1881
1905
  self.analysis_action = QAction("Show Analysis...", self)
1882
1906
  self.analysis_action.triggered.connect(self.open_analysis_window)
@@ -1903,6 +1927,16 @@ class MainWindow(QMainWindow):
1903
1927
  self.view_2d.setMouseTracking(False)
1904
1928
  self.scene.template_preview.hide()
1905
1929
 
1930
+ # カーソル形状の設定
1931
+ if mode_str == 'select':
1932
+ self.view_2d.setCursor(Qt.CursorShape.ArrowCursor)
1933
+ elif mode_str.startswith(('atom', 'bond', 'template')):
1934
+ self.view_2d.setCursor(Qt.CursorShape.CrossCursor)
1935
+ elif mode_str.startswith(('charge', 'radical')):
1936
+ self.view_2d.setCursor(Qt.CursorShape.PointingHandCursor)
1937
+ else:
1938
+ self.view_2d.setCursor(Qt.CursorShape.ArrowCursor)
1939
+
1906
1940
  if mode_str.startswith('atom'):
1907
1941
  self.scene.current_atom_symbol = mode_str.split('_')[1]
1908
1942
  self.statusBar().showMessage(f"Mode: Draw Atom ({self.scene.current_atom_symbol})")
@@ -1932,10 +1966,8 @@ class MainWindow(QMainWindow):
1932
1966
  else: # Select mode
1933
1967
  self.statusBar().showMessage("Mode: Select")
1934
1968
  self.view_2d.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
1935
- # ★追加: selectモードに切り替わったら結合様式をデフォルトにリセット★
1936
1969
  self.scene.bond_order = 1
1937
1970
  self.scene.bond_stereo = 0
1938
- # ★追加 終わり★
1939
1971
 
1940
1972
  def set_mode_and_update_toolbar(self, mode_str):
1941
1973
  self.set_mode(mode_str)
@@ -2090,10 +2122,26 @@ class MainWindow(QMainWindow):
2090
2122
  self.view_2d.setFocus()
2091
2123
  return
2092
2124
 
2125
+ problems = Chem.DetectChemistryProblems(mol)
2126
+ if problems:
2127
+ self.statusBar().showMessage(f"Error: {len(problems)} chemistry problem(s) found.")
2128
+ self.scene.clearSelection()
2129
+
2130
+ # 問題のある原子をハイライト
2131
+ for prob in problems:
2132
+ atom_idx = prob.GetAtomIdx()
2133
+ rdkit_atom = mol.GetAtomWithIdx(atom_idx)
2134
+ original_id = rdkit_atom.GetIntProp("_original_atom_id")
2135
+ if original_id in self.data.atoms:
2136
+ item = self.data.atoms[original_id]['item']
2137
+ item.setSelected(True)
2138
+ self.view_2d.setFocus()
2139
+ return
2140
+
2093
2141
  try:
2094
2142
  Chem.SanitizeMol(mol)
2095
2143
  except Exception:
2096
- self.statusBar().showMessage("Error: Invalid chemical structure (sanitization failed).")
2144
+ self.statusBar().showMessage("Error: Invalid chemical structure.")
2097
2145
  self.view_2d.setFocus()
2098
2146
  return
2099
2147
 
@@ -2110,13 +2158,20 @@ class MainWindow(QMainWindow):
2110
2158
  self.view_2d.setFocus()
2111
2159
 
2112
2160
  def on_calculation_finished(self, mol):
2113
- self.current_mol=mol
2161
+ self.current_mol = mol
2114
2162
  self.draw_molecule_3d(mol)
2163
+ # ここで最適化済みの current_mol を用いて R/S を再解析して表示を更新
2164
+ try:
2165
+ self.update_chiral_labels()
2166
+ except Exception:
2167
+ # 念のためエラーを握り潰して UI を壊さない
2168
+ pass
2169
+
2115
2170
  self.statusBar().showMessage("3D conversion successful.")
2116
2171
  self.convert_button.setEnabled(True)
2117
2172
  self.analysis_action.setEnabled(True)
2118
2173
  self.push_undo_state()
2119
- self.view_2d.setFocus()
2174
+ self.view_2d.setFocus()
2120
2175
 
2121
2176
  def on_calculation_error(self, error_message):
2122
2177
  self.statusBar().showMessage(f"Error: {error_message}")
@@ -2196,6 +2251,8 @@ class MainWindow(QMainWindow):
2196
2251
  self.current_mol = None; self.analysis_action.setEnabled(False)
2197
2252
  else:
2198
2253
  self.current_mol = None; self.plotter.clear(); self.analysis_action.setEnabled(False)
2254
+
2255
+ self.update_chiral_labels()
2199
2256
 
2200
2257
 
2201
2258
  def push_undo_state(self):
@@ -2222,8 +2279,10 @@ class MainWindow(QMainWindow):
2222
2279
  self.undo_stack.append(state)
2223
2280
  self.redo_stack.clear()
2224
2281
 
2282
+ self.update_realtime_info()
2283
+ self.update_chiral_labels()
2225
2284
  self.update_undo_redo_actions()
2226
-
2285
+
2227
2286
  def reset_undo_stack(self):
2228
2287
  self.undo_stack.clear()
2229
2288
  self.redo_stack.clear()
@@ -2235,6 +2294,8 @@ class MainWindow(QMainWindow):
2235
2294
  state = self.undo_stack[-1]
2236
2295
  self.set_state_from_data(state)
2237
2296
  self.update_undo_redo_actions()
2297
+ self.update_realtime_info()
2298
+ self.update_chiral_labels()
2238
2299
  self.view_2d.setFocus()
2239
2300
 
2240
2301
  def redo(self):
@@ -2243,12 +2304,30 @@ class MainWindow(QMainWindow):
2243
2304
  self.undo_stack.append(state)
2244
2305
  self.set_state_from_data(state)
2245
2306
  self.update_undo_redo_actions()
2307
+ self.update_realtime_info()
2246
2308
  self.view_2d.setFocus()
2247
2309
 
2248
2310
  def update_undo_redo_actions(self):
2249
2311
  self.undo_action.setEnabled(len(self.undo_stack) > 1)
2250
2312
  self.redo_action.setEnabled(len(self.redo_stack) > 0)
2251
2313
 
2314
+ def update_realtime_info(self):
2315
+ """ステータスバーの右側に現在の分子情報を表示する"""
2316
+ if not self.data.atoms:
2317
+ self.formula_label.setText("") # 原子がなければ右側のラベルをクリア
2318
+ return
2319
+
2320
+ try:
2321
+ mol = self.data.to_rdkit_mol()
2322
+ if mol:
2323
+ mol_formula = rdMolDescriptors.CalcMolFormula(mol)
2324
+ num_atoms = mol.GetNumAtoms()
2325
+ # 右側のラベルのテキストを更新
2326
+ self.formula_label.setText(f"Formula: {mol_formula} | Atoms: {num_atoms}")
2327
+ except Exception:
2328
+ # 計算に失敗してもアプリは継続
2329
+ self.formula_label.setText("Invalid structure")
2330
+
2252
2331
  def select_all(self):
2253
2332
  for item in self.scene.items():
2254
2333
  if isinstance(item, (AtomItem, BondItem)):
@@ -2683,13 +2762,118 @@ class MainWindow(QMainWindow):
2683
2762
  self.plotter.add_mesh(cc, color=color, smooth_shading=True, show_edges=True, edge_color=edge_color, edge_opacity=0.3)
2684
2763
  self.plotter.add_mesh(c1, color=color, smooth_shading=True, show_edges=True, edge_color=edge_color, edge_opacity=0.3)
2685
2764
  self.plotter.add_mesh(c2, color=color, smooth_shading=True, show_edges=True, edge_color=edge_color, edge_opacity=0.3)
2686
-
2765
+
2766
+ # chiral labels: 3D コンフォマーを持つ場合はここで R/S を表示
2767
+ if getattr(self, 'show_chiral_labels', False):
2768
+ try:
2769
+ # RDKit 側でキラル中心を取得(3D コンフォマーがあればそれを使う)
2770
+ chiral_centers = Chem.FindMolChiralCenters(mol, includeUnassigned=True)
2771
+ if chiral_centers:
2772
+ # ラベル描画用の点配列と文字列リストを作成
2773
+ # pos は numpy array (N,3) なので、index で取れば座標が得られる
2774
+ pts = []
2775
+ labels = []
2776
+ # 少しラベルが球と重ならないよう z 方向にオフセット(全体のサイズに合わせて調整)
2777
+ z_off = 0
2778
+ for idx, lbl in chiral_centers:
2779
+ coord = pos[idx].copy()
2780
+ coord[2] += z_off
2781
+ pts.append(coord)
2782
+ labels.append(lbl if lbl is not None else '?')
2783
+
2784
+ # 既存のラベルを削除し(存在すれば)、再描画する
2785
+ try:
2786
+ # actor 名 'chiral_labels' をキーとして remove を試みる(無ければ ignore)
2787
+ self.plotter.remove_actor('chiral_labels')
2788
+ except Exception:
2789
+ pass
2790
+
2791
+ # PyVista のラベル描画(font_size 等はお好みで調整)
2792
+ self.plotter.add_point_labels(
2793
+ np.array(pts),
2794
+ labels,
2795
+ font_size=20,
2796
+ point_size=0,
2797
+ text_color='k',
2798
+ name='chiral_labels',
2799
+ always_visible=True, # ← ここを追加(ラベルの可視性フィルタをスキップ)
2800
+ tolerance=0.01, # ← 必要なら少し大きめにして消えにくくする(デフォルトは 0.001)
2801
+ show_points=False # ← ポイント自体の小さなドット表示が不要ならオフに(任意)
2802
+ )
2803
+ except Exception as e:
2804
+ self.statusBar().showMessage(f"3D chiral label drawing error: {e}")
2805
+
2687
2806
  self.plotter.reset_camera()
2688
2807
 
2808
+
2689
2809
  def toggle_chiral_labels_display(self, checked):
2690
2810
  """Viewメニューのアクションに応じてキラルラベル表示を切り替える"""
2691
2811
  self.show_chiral_labels = checked
2692
2812
 
2813
+ self.update_chiral_labels()
2814
+
2815
+
2816
+ def update_chiral_labels(self):
2817
+ """分子のキラル中心を計算し、2Dビューの原子アイテムにR/Sラベルを設定/解除する
2818
+ ※ 可能なら 3D(self.current_mol)を優先して計算し、なければ 2D から作った RDKit 分子を使う。
2819
+ """
2820
+ # まず全てのアイテムからラベルをクリア
2821
+ for atom_data in self.data.atoms.values():
2822
+ if atom_data.get('item'):
2823
+ atom_data['item'].chiral_label = None
2824
+
2825
+ if not self.show_chiral_labels:
2826
+ self.scene.update()
2827
+ return
2828
+
2829
+ # 3D の RDKit Mol(コンフォマーを持つもの)を使う
2830
+ mol_for_chirality = None
2831
+ if getattr(self, 'current_mol', None) is not None:
2832
+ mol_for_chirality = self.current_mol
2833
+ else:
2834
+ return
2835
+
2836
+ if mol_for_chirality is None or mol_for_chirality.GetNumAtoms() == 0:
2837
+ self.scene.update()
2838
+ return
2839
+
2840
+ try:
2841
+ # --- 重要:3D コンフォマーがあるなら、それを使って原子のキラルタグを割り当てる ---
2842
+ if mol_for_chirality.GetNumConformers() > 0:
2843
+ # confId=0(最初のコンフォマー)を指定して、原子のキラリティータグを3D座標由来で設定
2844
+ try:
2845
+ Chem.AssignAtomChiralTagsFromStructure(mol_for_chirality, confId=0)
2846
+ except Exception:
2847
+ # 古い RDKit では関数が無い場合があるので(念のため保護)
2848
+ pass
2849
+
2850
+ # RDKit の通常の stereochemistry 割当(念のため)
2851
+ Chem.AssignStereochemistry(mol_for_chirality, cleanIt=True, force=True, flagPossibleStereoCenters=True)
2852
+
2853
+ # キラル中心の取得((idx, 'R'/'S'/'?') のリスト)
2854
+ chiral_centers = Chem.FindMolChiralCenters(mol_for_chirality, includeUnassigned=True)
2855
+
2856
+ # RDKit atom index -> エディタ側 atom_id へのマッピング
2857
+ rdkit_idx_to_my_id = {}
2858
+ for atom in mol_for_chirality.GetAtoms():
2859
+ if atom.HasProp("_original_atom_id"):
2860
+ rdkit_idx_to_my_id[atom.GetIdx()] = atom.GetIntProp("_original_atom_id")
2861
+
2862
+ # 見つかったキラル中心を対応する AtomItem に設定
2863
+ for idx, label in chiral_centers:
2864
+ if idx in rdkit_idx_to_my_id:
2865
+ atom_id = rdkit_idx_to_my_id[idx]
2866
+ if atom_id in self.data.atoms and self.data.atoms[atom_id].get('item'):
2867
+ # 'R' / 'S' / '?'
2868
+ self.data.atoms[atom_id]['item'].chiral_label = label
2869
+
2870
+ except Exception as e:
2871
+ self.statusBar().showMessage(f"Update chiral labels error: {e}")
2872
+
2873
+ # 最後に 2D シーンを再描画
2874
+ self.scene.update()
2875
+
2876
+
2693
2877
  def open_analysis_window(self):
2694
2878
  if self.current_mol:
2695
2879
  dialog = AnalysisWindow(self.current_mol, self)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moleditpy
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: A program to edit molecular structures using Python.
5
5
  Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/HiroYokoyama/python_molecular_editor
@@ -7,4 +7,5 @@ src/moleditpy.egg-info/SOURCES.txt
7
7
  src/moleditpy.egg-info/dependency_links.txt
8
8
  src/moleditpy.egg-info/entry_points.txt
9
9
  src/moleditpy.egg-info/requires.txt
10
- src/moleditpy.egg-info/top_level.txt
10
+ src/moleditpy.egg-info/top_level.txt
11
+ src/moleditpy/assets/icon.png
File without changes
File without changes