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.
- {moleditpy-0.2.1 → moleditpy-0.3.1}/PKG-INFO +1 -1
- {moleditpy-0.2.1 → moleditpy-0.3.1}/pyproject.toml +2 -2
- {moleditpy-0.2.1 → moleditpy-0.3.1}/src/moleditpy/main.py +227 -37
- {moleditpy-0.2.1 → moleditpy-0.3.1}/src/moleditpy.egg-info/PKG-INFO +1 -1
- {moleditpy-0.2.1 → moleditpy-0.3.1}/src/moleditpy.egg-info/SOURCES.txt +1 -1
- {moleditpy-0.2.1 → moleditpy-0.3.1}/README.md +0 -0
- {moleditpy-0.2.1 → moleditpy-0.3.1}/setup.cfg +0 -0
- {moleditpy-0.2.1 → moleditpy-0.3.1}/src/moleditpy/__init__.py +0 -0
- /moleditpy-0.2.1/src/moleditpy/assets/icon.png → /moleditpy-0.3.1/src/moleditpy/assets/icon.ico +0 -0
- {moleditpy-0.2.1 → moleditpy-0.3.1}/src/moleditpy.egg-info/dependency_links.txt +0 -0
- {moleditpy-0.2.1 → moleditpy-0.3.1}/src/moleditpy.egg-info/entry_points.txt +0 -0
- {moleditpy-0.2.1 → moleditpy-0.3.1}/src/moleditpy.egg-info/requires.txt +0 -0
- {moleditpy-0.2.1 → moleditpy-0.3.1}/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.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.
|
|
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
|
|
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('#
|
|
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()
|
|
@@ -86,24 +87,24 @@ def main():
|
|
|
86
87
|
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
|
|
87
88
|
|
|
88
89
|
app = QApplication(sys.argv)
|
|
89
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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))
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/moleditpy-0.2.1/src/moleditpy/assets/icon.png → /moleditpy-0.3.1/src/moleditpy/assets/icon.ico
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|