moleditpy 0.2.1__tar.gz → 0.3.1__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.1
3
+ Version: 0.3.1
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.1"
8
+ version = "0.3.1"
9
9
 
10
10
  authors = [
11
11
  { name="HiroYokoyama", email="titech.yoko.hiro@gmail.com" },
@@ -45,4 +45,4 @@ Issues = "https://github.com/HiroYokoyama/python_molecular_editor/issues"
45
45
  moleditpy = "moleditpy.main:main"
46
46
 
47
47
  [tool.setuptools.package-data]
48
- "moleditpy" = ["assets/icon.png"]
48
+ "moleditpy" = ["assets/*"]
@@ -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()
@@ -86,24 +87,24 @@ def main():
86
87
  ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
87
88
 
88
89
  app = QApplication(sys.argv)
89
- window = MainWindow()
90
+ file_path = sys.argv[1] if len(sys.argv) > 1 else None
91
+ window = MainWindow(initial_file=file_path)
90
92
  window.show()
91
93
  sys.exit(app.exec())
92
94
 
93
95
 
94
- # --- Data Model ---
95
96
  # --- Data Model ---
96
97
  class MolecularData:
97
98
  def __init__(self):
98
99
  self.atoms = {}
99
100
  self.bonds = {}
100
101
  self._next_atom_id = 0
101
- self.adjacency_list = {} # <--- 追加: 隣接リストを初期化
102
+ self.adjacency_list = {}
102
103
 
103
104
  def add_atom(self, symbol, pos, charge=0, radical=0):
104
105
  atom_id = self._next_atom_id
105
106
  self.atoms[atom_id] = {'symbol': symbol, 'pos': pos, 'item': None, 'charge': charge, 'radical': radical}
106
- self.adjacency_list[atom_id] = [] # <--- 追加: 新しい原子のエントリを作成
107
+ self.adjacency_list[atom_id] = []
107
108
  self._next_atom_id += 1
108
109
  return atom_id
109
110
 
@@ -124,14 +125,17 @@ class MolecularData:
124
125
  self.bonds[(id1, id2)] = bond_data
125
126
  return (id1, id2), 'created'
126
127
 
128
+
127
129
  def remove_atom(self, atom_id):
128
130
  if atom_id in self.atoms:
129
- # この原子に接続していた他の原子のリストからも、この原子IDを削除
131
+ # Safely get neighbors before deleting the atom's own entry
132
+ neighbors = self.adjacency_list.get(atom_id, [])
133
+ for neighbor_id in neighbors:
134
+ if neighbor_id in self.adjacency_list and atom_id in self.adjacency_list[neighbor_id]:
135
+ self.adjacency_list[neighbor_id].remove(atom_id)
136
+
137
+ # Now, safely delete the atom's own entry from the adjacency list
130
138
  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
139
  del self.adjacency_list[atom_id]
136
140
 
137
141
  del self.atoms[atom_id]
@@ -180,8 +184,6 @@ class MolecularData:
180
184
  mol_block += "M END\n"
181
185
  return mol_block
182
186
 
183
- # main.py の MolecularDataクラス内 (約193行目あたり)
184
- # 既存の to_rdkit_mol メソッドを、以下のコードで完全に置き換えてください。
185
187
 
186
188
  def to_rdkit_mol(self):
187
189
  if not self.atoms: return None
@@ -212,7 +214,6 @@ class MolecularData:
212
214
  else:
213
215
  order = Chem.BondType.SINGLE
214
216
 
215
- # 結合を追加し、そのインデックスを直接取得する、より確実な方法に変更しました。
216
217
  bond_idx = mol.AddBond(idx1, idx2, order)
217
218
  bond = mol.GetBondWithIdx(bond_idx - 1)
218
219
 
@@ -229,7 +230,6 @@ class MolecularData:
229
230
  pass
230
231
  return mol
231
232
 
232
- # --- Custom 2D Graphics Items ---
233
233
  class AtomItem(QGraphicsItem):
234
234
  def __init__(self, atom_id, symbol, pos, charge=0, radical=0):
235
235
  super().__init__()
@@ -249,7 +249,6 @@ class AtomItem(QGraphicsItem):
249
249
  path.addEllipse(QRectF(-hit_r, -hit_r, hit_r * 2.0, hit_r * 2.0))
250
250
  return path
251
251
 
252
- # AtomItem クラス内
253
252
  def paint(self, painter, option, widget):
254
253
  color = CPK_COLORS.get(self.symbol, CPK_COLORS['DEFAULT'])
255
254
  if self.is_visible:
@@ -266,9 +265,13 @@ class AtomItem(QGraphicsItem):
266
265
  painter.setBrush(bg_brush)
267
266
  painter.setPen(Qt.PenStyle.NoPen)
268
267
  painter.drawRect(bg_rect)
269
-
268
+
270
269
  # 3. 原子記号自体を描画
271
- painter.setPen(QPen(color)) # <-- 修正:常に元素カラーで描画
270
+ # 水素だけ黒文字、それ以外は CPK カラー
271
+ if self.symbol == 'H':
272
+ painter.setPen(QPen(Qt.GlobalColor.black))
273
+ else:
274
+ painter.setPen(QPen(color))
272
275
  painter.drawText(text_rect, Qt.AlignmentFlag.AlignCenter, self.symbol)
273
276
 
274
277
 
@@ -473,7 +476,9 @@ class MoleculeScene(QGraphicsScene):
473
476
  if isinstance(item, AtomItem):
474
477
  self.start_atom = item
475
478
  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)
479
+ self.temp_line = QGraphicsLineItem(QLineF(self.start_atom.pos(), self.press_pos))
480
+ self.temp_line.setPen(QPen(Qt.GlobalColor.red, 2, Qt.PenStyle.DotLine))
481
+ self.addItem(self.temp_line)
477
482
  else: super().mousePressEvent(event)
478
483
  elif item is None and self.mode.startswith('atom'):
479
484
  self.start_pos = self.press_pos
@@ -1308,10 +1313,16 @@ class MoleculeScene(QGraphicsScene):
1308
1313
  super().keyPressEvent(event)
1309
1314
 
1310
1315
 
1316
+ # In MoleculeScene class
1311
1317
  def find_atom_near(self, pos, tol=14.0):
1312
- for it in self.items():
1318
+ # Create a small search rectangle around the position
1319
+ search_rect = QRectF(pos.x() - tol, pos.y() - tol, 2 * tol, 2 * tol)
1320
+ nearby_items = self.items(search_rect)
1321
+
1322
+ for it in nearby_items:
1313
1323
  if isinstance(it, AtomItem):
1314
- if math.hypot(it.pos().x() - pos.x(), it.pos().y() - pos.y()) <= tol:
1324
+ # Check the precise distance only for candidate items
1325
+ if QLineF(it.pos(), pos).length() <= tol:
1315
1326
  return it
1316
1327
  return None
1317
1328
 
@@ -1382,10 +1393,10 @@ class PeriodicTableDialog(QDialog):
1382
1393
  ('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
1394
  ('Ts',7,17), ('Og',7,18),
1384
1395
  # 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),
1396
+ ('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
1397
  ('Dy',8,12), ('Ho',8,13), ('Er',8,14), ('Tm',8,15), ('Yb',8,16), ('Lu',8,17),
1387
1398
  # 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),
1399
+ ('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
1400
  ('Cf',9,12), ('Es',9,13), ('Fm',9,14), ('Md',9,15), ('No',9,16), ('Lr',9,17),
1390
1401
  ]
1391
1402
  for symbol, row, col in elements:
@@ -1501,7 +1512,7 @@ class AnalysisWindow(QDialog):
1501
1512
  class MainWindow(QMainWindow):
1502
1513
 
1503
1514
  start_calculation = pyqtSignal(str)
1504
- def __init__(self):
1515
+ def __init__(self, initial_file=None):
1505
1516
  super().__init__()
1506
1517
  self.setWindowTitle("moleditpy -- Python Molecular Editor by HY"); self.setGeometry(100, 100, 1400, 800)
1507
1518
  self.data = MolecularData(); self.current_mol = None
@@ -1517,12 +1528,15 @@ class MainWindow(QMainWindow):
1517
1528
  QApplication.clipboard().dataChanged.connect(self.update_edit_menu_actions)
1518
1529
  self.update_edit_menu_actions()
1519
1530
 
1531
+ if initial_file:
1532
+ self.load_raw_data(file_path=initial_file)
1533
+
1520
1534
  def init_ui(self):
1521
1535
  # 1. 現在のスクリプトがあるディレクトリのパスを取得
1522
1536
  script_dir = os.path.dirname(os.path.abspath(__file__))
1523
1537
 
1524
1538
  # 2. 'assets'フォルダ内のアイコンファイルへのフルパスを構築
1525
- icon_path = os.path.join(script_dir, 'assets', 'icon.png')
1539
+ icon_path = os.path.join(script_dir, 'assets', 'icon.ico')
1526
1540
 
1527
1541
  # 3. ファイルパスから直接QIconオブジェクトを作成
1528
1542
  if os.path.exists(icon_path): # ファイルが存在するか確認
@@ -1578,6 +1592,14 @@ class MainWindow(QMainWindow):
1578
1592
  splitter.setStretchFactor(0, 1)
1579
1593
  splitter.setStretchFactor(1, 1)
1580
1594
 
1595
+ # ステータスバーを左右に分離するための設定
1596
+ self.status_bar = self.statusBar()
1597
+ self.formula_label = QLabel("") # 右側に表示するラベルを作成
1598
+ # 右端に余白を追加して見栄えを調整
1599
+ self.formula_label.setStyleSheet("padding-right: 8px;")
1600
+ # ラベルを右側に常時表示ウィジェットとして追加
1601
+ self.status_bar.addPermanentWidget(self.formula_label)
1602
+
1581
1603
  #self.view_2d.fitInView(self.scene.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio)
1582
1604
 
1583
1605
  toolbar = QToolBar("Main Toolbar")
@@ -1877,6 +1899,12 @@ class MainWindow(QMainWindow):
1877
1899
  clear_all_action.setShortcut(QKeySequence("Ctrl+Shift+C"))
1878
1900
  clear_all_action.triggered.connect(self.clear_all); edit_menu.addAction(clear_all_action)
1879
1901
 
1902
+ view_menu = menu_bar.addMenu("&View")
1903
+ self.toggle_chiral_action = QAction("Show Chiral Labels", self, checkable=True)
1904
+ self.toggle_chiral_action.setChecked(self.show_chiral_labels)
1905
+ self.toggle_chiral_action.triggered.connect(self.toggle_chiral_labels_display)
1906
+ view_menu.addAction(self.toggle_chiral_action)
1907
+
1880
1908
  analysis_menu = menu_bar.addMenu("&Analysis")
1881
1909
  self.analysis_action = QAction("Show Analysis...", self)
1882
1910
  self.analysis_action.triggered.connect(self.open_analysis_window)
@@ -1903,6 +1931,16 @@ class MainWindow(QMainWindow):
1903
1931
  self.view_2d.setMouseTracking(False)
1904
1932
  self.scene.template_preview.hide()
1905
1933
 
1934
+ # カーソル形状の設定
1935
+ if mode_str == 'select':
1936
+ self.view_2d.setCursor(Qt.CursorShape.ArrowCursor)
1937
+ elif mode_str.startswith(('atom', 'bond', 'template')):
1938
+ self.view_2d.setCursor(Qt.CursorShape.CrossCursor)
1939
+ elif mode_str.startswith(('charge', 'radical')):
1940
+ self.view_2d.setCursor(Qt.CursorShape.PointingHandCursor)
1941
+ else:
1942
+ self.view_2d.setCursor(Qt.CursorShape.ArrowCursor)
1943
+
1906
1944
  if mode_str.startswith('atom'):
1907
1945
  self.scene.current_atom_symbol = mode_str.split('_')[1]
1908
1946
  self.statusBar().showMessage(f"Mode: Draw Atom ({self.scene.current_atom_symbol})")
@@ -1932,10 +1970,8 @@ class MainWindow(QMainWindow):
1932
1970
  else: # Select mode
1933
1971
  self.statusBar().showMessage("Mode: Select")
1934
1972
  self.view_2d.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
1935
- # ★追加: selectモードに切り替わったら結合様式をデフォルトにリセット★
1936
1973
  self.scene.bond_order = 1
1937
1974
  self.scene.bond_stereo = 0
1938
- # ★追加 終わり★
1939
1975
 
1940
1976
  def set_mode_and_update_toolbar(self, mode_str):
1941
1977
  self.set_mode(mode_str)
@@ -2090,10 +2126,26 @@ class MainWindow(QMainWindow):
2090
2126
  self.view_2d.setFocus()
2091
2127
  return
2092
2128
 
2129
+ problems = Chem.DetectChemistryProblems(mol)
2130
+ if problems:
2131
+ self.statusBar().showMessage(f"Error: {len(problems)} chemistry problem(s) found.")
2132
+ self.scene.clearSelection()
2133
+
2134
+ # 問題のある原子をハイライト
2135
+ for prob in problems:
2136
+ atom_idx = prob.GetAtomIdx()
2137
+ rdkit_atom = mol.GetAtomWithIdx(atom_idx)
2138
+ original_id = rdkit_atom.GetIntProp("_original_atom_id")
2139
+ if original_id in self.data.atoms:
2140
+ item = self.data.atoms[original_id]['item']
2141
+ item.setSelected(True)
2142
+ self.view_2d.setFocus()
2143
+ return
2144
+
2093
2145
  try:
2094
2146
  Chem.SanitizeMol(mol)
2095
2147
  except Exception:
2096
- self.statusBar().showMessage("Error: Invalid chemical structure (sanitization failed).")
2148
+ self.statusBar().showMessage("Error: Invalid chemical structure.")
2097
2149
  self.view_2d.setFocus()
2098
2150
  return
2099
2151
 
@@ -2110,13 +2162,20 @@ class MainWindow(QMainWindow):
2110
2162
  self.view_2d.setFocus()
2111
2163
 
2112
2164
  def on_calculation_finished(self, mol):
2113
- self.current_mol=mol
2165
+ self.current_mol = mol
2114
2166
  self.draw_molecule_3d(mol)
2167
+ # ここで最適化済みの current_mol を用いて R/S を再解析して表示を更新
2168
+ try:
2169
+ self.update_chiral_labels()
2170
+ except Exception:
2171
+ # 念のためエラーを握り潰して UI を壊さない
2172
+ pass
2173
+
2115
2174
  self.statusBar().showMessage("3D conversion successful.")
2116
2175
  self.convert_button.setEnabled(True)
2117
2176
  self.analysis_action.setEnabled(True)
2118
2177
  self.push_undo_state()
2119
- self.view_2d.setFocus()
2178
+ self.view_2d.setFocus()
2120
2179
 
2121
2180
  def on_calculation_error(self, error_message):
2122
2181
  self.statusBar().showMessage(f"Error: {error_message}")
@@ -2196,6 +2255,8 @@ class MainWindow(QMainWindow):
2196
2255
  self.current_mol = None; self.analysis_action.setEnabled(False)
2197
2256
  else:
2198
2257
  self.current_mol = None; self.plotter.clear(); self.analysis_action.setEnabled(False)
2258
+
2259
+ self.update_chiral_labels()
2199
2260
 
2200
2261
 
2201
2262
  def push_undo_state(self):
@@ -2222,8 +2283,10 @@ class MainWindow(QMainWindow):
2222
2283
  self.undo_stack.append(state)
2223
2284
  self.redo_stack.clear()
2224
2285
 
2286
+ self.update_realtime_info()
2287
+ self.update_chiral_labels()
2225
2288
  self.update_undo_redo_actions()
2226
-
2289
+
2227
2290
  def reset_undo_stack(self):
2228
2291
  self.undo_stack.clear()
2229
2292
  self.redo_stack.clear()
@@ -2235,6 +2298,8 @@ class MainWindow(QMainWindow):
2235
2298
  state = self.undo_stack[-1]
2236
2299
  self.set_state_from_data(state)
2237
2300
  self.update_undo_redo_actions()
2301
+ self.update_realtime_info()
2302
+ self.update_chiral_labels()
2238
2303
  self.view_2d.setFocus()
2239
2304
 
2240
2305
  def redo(self):
@@ -2243,12 +2308,30 @@ class MainWindow(QMainWindow):
2243
2308
  self.undo_stack.append(state)
2244
2309
  self.set_state_from_data(state)
2245
2310
  self.update_undo_redo_actions()
2311
+ self.update_realtime_info()
2246
2312
  self.view_2d.setFocus()
2247
2313
 
2248
2314
  def update_undo_redo_actions(self):
2249
2315
  self.undo_action.setEnabled(len(self.undo_stack) > 1)
2250
2316
  self.redo_action.setEnabled(len(self.redo_stack) > 0)
2251
2317
 
2318
+ def update_realtime_info(self):
2319
+ """ステータスバーの右側に現在の分子情報を表示する"""
2320
+ if not self.data.atoms:
2321
+ self.formula_label.setText("") # 原子がなければ右側のラベルをクリア
2322
+ return
2323
+
2324
+ try:
2325
+ mol = self.data.to_rdkit_mol()
2326
+ if mol:
2327
+ mol_formula = rdMolDescriptors.CalcMolFormula(mol)
2328
+ num_atoms = mol.GetNumAtoms()
2329
+ # 右側のラベルのテキストを更新
2330
+ self.formula_label.setText(f"Formula: {mol_formula} | Atoms: {num_atoms}")
2331
+ except Exception:
2332
+ # 計算に失敗してもアプリは継続
2333
+ self.formula_label.setText("Invalid structure")
2334
+
2252
2335
  def select_all(self):
2253
2336
  for item in self.scene.items():
2254
2337
  if isinstance(item, (AtomItem, BondItem)):
@@ -2346,10 +2429,12 @@ class MainWindow(QMainWindow):
2346
2429
  self.statusBar().showMessage(f"Project saved to {file_path}")
2347
2430
  except Exception as e: self.statusBar().showMessage(f"Error saving project file: {e}")
2348
2431
 
2349
- def load_raw_data(self):
2350
- options = QFileDialog.Option.DontUseNativeDialog
2351
- file_path, _ = QFileDialog.getOpenFileName(self, "Open Project File", "", "Project Files (*.pmeraw);;All Files (*)", options=options)
2352
- if not file_path: return
2432
+ def load_raw_data(self, file_path=None):
2433
+ if not file_path:
2434
+ options = QFileDialog.Option.DontUseNativeDialog
2435
+ file_path, _ = QFileDialog.getOpenFileName(self, "Open Project File", "", "Project Files (*.pmeraw);;All Files (*)", options=options)
2436
+ if not file_path: return
2437
+
2353
2438
  try:
2354
2439
  with open(file_path, 'rb') as f: loaded_data = pickle.load(f)
2355
2440
  self.set_state_from_data(loaded_data)
@@ -2683,13 +2768,118 @@ class MainWindow(QMainWindow):
2683
2768
  self.plotter.add_mesh(cc, color=color, smooth_shading=True, show_edges=True, edge_color=edge_color, edge_opacity=0.3)
2684
2769
  self.plotter.add_mesh(c1, color=color, smooth_shading=True, show_edges=True, edge_color=edge_color, edge_opacity=0.3)
2685
2770
  self.plotter.add_mesh(c2, color=color, smooth_shading=True, show_edges=True, edge_color=edge_color, edge_opacity=0.3)
2686
-
2771
+
2772
+ # chiral labels: 3D コンフォマーを持つ場合はここで R/S を表示
2773
+ if getattr(self, 'show_chiral_labels', False):
2774
+ try:
2775
+ # RDKit 側でキラル中心を取得(3D コンフォマーがあればそれを使う)
2776
+ chiral_centers = Chem.FindMolChiralCenters(mol, includeUnassigned=True)
2777
+ if chiral_centers:
2778
+ # ラベル描画用の点配列と文字列リストを作成
2779
+ # pos は numpy array (N,3) なので、index で取れば座標が得られる
2780
+ pts = []
2781
+ labels = []
2782
+ # 少しラベルが球と重ならないよう z 方向にオフセット(全体のサイズに合わせて調整)
2783
+ z_off = 0
2784
+ for idx, lbl in chiral_centers:
2785
+ coord = pos[idx].copy()
2786
+ coord[2] += z_off
2787
+ pts.append(coord)
2788
+ labels.append(lbl if lbl is not None else '?')
2789
+
2790
+ # 既存のラベルを削除し(存在すれば)、再描画する
2791
+ try:
2792
+ # actor 名 'chiral_labels' をキーとして remove を試みる(無ければ ignore)
2793
+ self.plotter.remove_actor('chiral_labels')
2794
+ except Exception:
2795
+ pass
2796
+
2797
+ # PyVista のラベル描画(font_size 等はお好みで調整)
2798
+ self.plotter.add_point_labels(
2799
+ np.array(pts),
2800
+ labels,
2801
+ font_size=20,
2802
+ point_size=0,
2803
+ text_color='k',
2804
+ name='chiral_labels',
2805
+ always_visible=True, # ← ここを追加(ラベルの可視性フィルタをスキップ)
2806
+ tolerance=0.01, # ← 必要なら少し大きめにして消えにくくする(デフォルトは 0.001)
2807
+ show_points=False # ← ポイント自体の小さなドット表示が不要ならオフに(任意)
2808
+ )
2809
+ except Exception as e:
2810
+ self.statusBar().showMessage(f"3D chiral label drawing error: {e}")
2811
+
2687
2812
  self.plotter.reset_camera()
2688
2813
 
2814
+
2689
2815
  def toggle_chiral_labels_display(self, checked):
2690
2816
  """Viewメニューのアクションに応じてキラルラベル表示を切り替える"""
2691
2817
  self.show_chiral_labels = checked
2692
2818
 
2819
+ self.update_chiral_labels()
2820
+
2821
+
2822
+ def update_chiral_labels(self):
2823
+ """分子のキラル中心を計算し、2Dビューの原子アイテムにR/Sラベルを設定/解除する
2824
+ ※ 可能なら 3D(self.current_mol)を優先して計算し、なければ 2D から作った RDKit 分子を使う。
2825
+ """
2826
+ # まず全てのアイテムからラベルをクリア
2827
+ for atom_data in self.data.atoms.values():
2828
+ if atom_data.get('item'):
2829
+ atom_data['item'].chiral_label = None
2830
+
2831
+ if not self.show_chiral_labels:
2832
+ self.scene.update()
2833
+ return
2834
+
2835
+ # 3D の RDKit Mol(コンフォマーを持つもの)を使う
2836
+ mol_for_chirality = None
2837
+ if getattr(self, 'current_mol', None) is not None:
2838
+ mol_for_chirality = self.current_mol
2839
+ else:
2840
+ return
2841
+
2842
+ if mol_for_chirality is None or mol_for_chirality.GetNumAtoms() == 0:
2843
+ self.scene.update()
2844
+ return
2845
+
2846
+ try:
2847
+ # --- 重要:3D コンフォマーがあるなら、それを使って原子のキラルタグを割り当てる ---
2848
+ if mol_for_chirality.GetNumConformers() > 0:
2849
+ # confId=0(最初のコンフォマー)を指定して、原子のキラリティータグを3D座標由来で設定
2850
+ try:
2851
+ Chem.AssignAtomChiralTagsFromStructure(mol_for_chirality, confId=0)
2852
+ except Exception:
2853
+ # 古い RDKit では関数が無い場合があるので(念のため保護)
2854
+ pass
2855
+
2856
+ # RDKit の通常の stereochemistry 割当(念のため)
2857
+ Chem.AssignStereochemistry(mol_for_chirality, cleanIt=True, force=True, flagPossibleStereoCenters=True)
2858
+
2859
+ # キラル中心の取得((idx, 'R'/'S'/'?') のリスト)
2860
+ chiral_centers = Chem.FindMolChiralCenters(mol_for_chirality, includeUnassigned=True)
2861
+
2862
+ # RDKit atom index -> エディタ側 atom_id へのマッピング
2863
+ rdkit_idx_to_my_id = {}
2864
+ for atom in mol_for_chirality.GetAtoms():
2865
+ if atom.HasProp("_original_atom_id"):
2866
+ rdkit_idx_to_my_id[atom.GetIdx()] = atom.GetIntProp("_original_atom_id")
2867
+
2868
+ # 見つかったキラル中心を対応する AtomItem に設定
2869
+ for idx, label in chiral_centers:
2870
+ if idx in rdkit_idx_to_my_id:
2871
+ atom_id = rdkit_idx_to_my_id[idx]
2872
+ if atom_id in self.data.atoms and self.data.atoms[atom_id].get('item'):
2873
+ # 'R' / 'S' / '?'
2874
+ self.data.atoms[atom_id]['item'].chiral_label = label
2875
+
2876
+ except Exception as e:
2877
+ self.statusBar().showMessage(f"Update chiral labels error: {e}")
2878
+
2879
+ # 最後に 2D シーンを再描画
2880
+ self.scene.update()
2881
+
2882
+
2693
2883
  def open_analysis_window(self):
2694
2884
  if self.current_mol:
2695
2885
  dialog = AnalysisWindow(self.current_mol, self)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moleditpy
3
- Version: 0.2.1
3
+ Version: 0.3.1
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
@@ -8,4 +8,4 @@ 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
10
  src/moleditpy.egg-info/top_level.txt
11
- src/moleditpy/assets/icon.png
11
+ src/moleditpy/assets/icon.ico
File without changes
File without changes