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.
- {moleditpy-0.2.0 → moleditpy-0.3.0}/PKG-INFO +1 -1
- {moleditpy-0.2.0 → moleditpy-0.3.0}/pyproject.toml +4 -1
- moleditpy-0.3.0/src/moleditpy/assets/icon.png +0 -0
- {moleditpy-0.2.0 → moleditpy-0.3.0}/src/moleditpy/main.py +214 -30
- {moleditpy-0.2.0 → moleditpy-0.3.0}/src/moleditpy.egg-info/PKG-INFO +1 -1
- {moleditpy-0.2.0 → moleditpy-0.3.0}/src/moleditpy.egg-info/SOURCES.txt +2 -1
- {moleditpy-0.2.0 → moleditpy-0.3.0}/README.md +0 -0
- {moleditpy-0.2.0 → moleditpy-0.3.0}/setup.cfg +0 -0
- {moleditpy-0.2.0 → moleditpy-0.3.0}/src/moleditpy/__init__.py +0 -0
- {moleditpy-0.2.0 → moleditpy-0.3.0}/src/moleditpy.egg-info/dependency_links.txt +0 -0
- {moleditpy-0.2.0 → moleditpy-0.3.0}/src/moleditpy.egg-info/entry_points.txt +0 -0
- {moleditpy-0.2.0 → moleditpy-0.3.0}/src/moleditpy.egg-info/requires.txt +0 -0
- {moleditpy-0.2.0 → moleditpy-0.3.0}/src/moleditpy.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: moleditpy
|
|
3
|
-
Version: 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.
|
|
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"]
|
|
Binary file
|
|
@@ -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('#
|
|
55
|
-
'Cl': QColor('#33FF33'), 'Br': QColor('#A52A2A'), 'I': QColor('#9400D3'), 'S': QColor('#FFC000'), 'P': QColor('#
|
|
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'), '
|
|
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
|
-
#
|
|
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
|
-
|
|
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))
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|