moleditpy 1.3.1__tar.gz → 1.4.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-1.3.1 → moleditpy-1.4.0}/PKG-INFO +1 -1
- {moleditpy-1.3.1 → moleditpy-1.4.0}/pyproject.toml +1 -1
- moleditpy-1.4.0/src/moleditpy/assets/icon.ico +0 -0
- moleditpy-1.4.0/src/moleditpy/assets/icon.png +0 -0
- {moleditpy-1.3.1 → moleditpy-1.4.0}/src/moleditpy/main.py +711 -127
- {moleditpy-1.3.1 → moleditpy-1.4.0}/src/moleditpy.egg-info/PKG-INFO +1 -1
- moleditpy-1.3.1/src/moleditpy/assets/icon.ico +0 -0
- moleditpy-1.3.1/src/moleditpy/assets/icon.png +0 -0
- {moleditpy-1.3.1 → moleditpy-1.4.0}/README.md +0 -0
- {moleditpy-1.3.1 → moleditpy-1.4.0}/setup.cfg +0 -0
- {moleditpy-1.3.1 → moleditpy-1.4.0}/src/moleditpy/__init__.py +0 -0
- {moleditpy-1.3.1 → moleditpy-1.4.0}/src/moleditpy.egg-info/SOURCES.txt +0 -0
- {moleditpy-1.3.1 → moleditpy-1.4.0}/src/moleditpy.egg-info/dependency_links.txt +0 -0
- {moleditpy-1.3.1 → moleditpy-1.4.0}/src/moleditpy.egg-info/entry_points.txt +0 -0
- {moleditpy-1.3.1 → moleditpy-1.4.0}/src/moleditpy.egg-info/requires.txt +0 -0
- {moleditpy-1.3.1 → moleditpy-1.4.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: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
|
|
5
5
|
Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/HiroYokoyama/python_molecular_editor
|
|
Binary file
|
|
Binary file
|
|
@@ -11,7 +11,7 @@ DOI 10.5281/zenodo.17268532
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
#Version
|
|
14
|
-
VERSION = '1.
|
|
14
|
+
VERSION = '1.4.0'
|
|
15
15
|
|
|
16
16
|
print("-----------------------------------------------------")
|
|
17
17
|
print("MoleditPy — A Python-based molecular editing software")
|
|
@@ -26,6 +26,7 @@ import io
|
|
|
26
26
|
import os
|
|
27
27
|
import ctypes
|
|
28
28
|
import itertools
|
|
29
|
+
import json
|
|
29
30
|
import vtk
|
|
30
31
|
|
|
31
32
|
from collections import deque
|
|
@@ -35,12 +36,16 @@ from PyQt6.QtWidgets import (
|
|
|
35
36
|
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
|
36
37
|
QPushButton, QSplitter, QGraphicsView, QGraphicsScene, QGraphicsItem,
|
|
37
38
|
QToolBar, QStatusBar, QGraphicsTextItem, QGraphicsLineItem, QDialog, QGridLayout,
|
|
38
|
-
QFileDialog, QSizePolicy, QLabel, QLineEdit, QToolButton, QMenu, QMessageBox, QInputDialog
|
|
39
|
+
QFileDialog, QSizePolicy, QLabel, QLineEdit, QToolButton, QMenu, QMessageBox, QInputDialog,
|
|
40
|
+
QColorDialog, QCheckBox, QSlider, QFormLayout
|
|
39
41
|
)
|
|
42
|
+
|
|
40
43
|
from PyQt6.QtGui import (
|
|
41
44
|
QPen, QBrush, QColor, QPainter, QAction, QActionGroup, QFont, QPolygonF,
|
|
42
|
-
QPainterPath, QPainterPathStroker, QFontMetrics, QFontMetricsF, QKeySequence, QTransform, QCursor, QPixmap, QIcon, QShortcut, QDesktopServices
|
|
45
|
+
QPainterPath, QPainterPathStroker, QFontMetrics, QFontMetricsF, QKeySequence, QTransform, QCursor, QPixmap, QIcon, QShortcut, QDesktopServices, QImage
|
|
43
46
|
)
|
|
47
|
+
|
|
48
|
+
|
|
44
49
|
from PyQt6.QtCore import Qt, QPointF, QRectF, QLineF, QObject, QThread, pyqtSignal, QEvent, QMimeData, QByteArray, QUrl, QTimer
|
|
45
50
|
|
|
46
51
|
from vtkmodules.vtkInteractionStyle import vtkInteractorStyleTrackballCamera
|
|
@@ -363,8 +368,6 @@ class AtomItem(QGraphicsItem):
|
|
|
363
368
|
path.addEllipse(QPointF(0, 0), scene_radius, scene_radius)
|
|
364
369
|
return path
|
|
365
370
|
|
|
366
|
-
# In class AtomItem:
|
|
367
|
-
|
|
368
371
|
def paint(self, painter, option, widget):
|
|
369
372
|
color = CPK_COLORS.get(self.symbol, CPK_COLORS['DEFAULT'])
|
|
370
373
|
if self.is_visible:
|
|
@@ -730,6 +733,9 @@ class MoleculeScene(QGraphicsScene):
|
|
|
730
733
|
self.data_changed_in_event = False
|
|
731
734
|
self.initial_positions_in_event = {item: item.pos() for item in self.items() if isinstance(item, AtomItem)}
|
|
732
735
|
|
|
736
|
+
if not self.window.is_2d_editable:
|
|
737
|
+
return
|
|
738
|
+
|
|
733
739
|
if event.button() == Qt.MouseButton.RightButton:
|
|
734
740
|
item = self.itemAt(event.scenePos(), self.views()[0].transform())
|
|
735
741
|
if not isinstance(item, (AtomItem, BondItem)):
|
|
@@ -794,6 +800,9 @@ class MoleculeScene(QGraphicsScene):
|
|
|
794
800
|
else: super().mousePressEvent(event)
|
|
795
801
|
|
|
796
802
|
def mouseMoveEvent(self, event):
|
|
803
|
+
if not self.window.is_2d_editable:
|
|
804
|
+
return
|
|
805
|
+
|
|
797
806
|
if self.mode.startswith('template'):
|
|
798
807
|
self.update_template_preview(event.scenePos())
|
|
799
808
|
|
|
@@ -830,6 +839,10 @@ class MoleculeScene(QGraphicsScene):
|
|
|
830
839
|
super().mouseMoveEvent(event)
|
|
831
840
|
|
|
832
841
|
def mouseReleaseEvent(self, event):
|
|
842
|
+
|
|
843
|
+
if not self.window.is_2d_editable:
|
|
844
|
+
return
|
|
845
|
+
|
|
833
846
|
end_pos = event.scenePos()
|
|
834
847
|
is_click = self.press_pos and (end_pos - self.press_pos).manhattanLength() < QApplication.startDragDistance()
|
|
835
848
|
|
|
@@ -1409,6 +1422,9 @@ class MoleculeScene(QGraphicsScene):
|
|
|
1409
1422
|
key = event.key()
|
|
1410
1423
|
modifiers = event.modifiers()
|
|
1411
1424
|
|
|
1425
|
+
if not self.window.is_2d_editable:
|
|
1426
|
+
return
|
|
1427
|
+
|
|
1412
1428
|
if key == Qt.Key.Key_4:
|
|
1413
1429
|
# --- 動作1: カーソルが原子/結合上にある場合 (ワンショットでテンプレート配置) ---
|
|
1414
1430
|
if isinstance(item_at_cursor, (AtomItem, BondItem)):
|
|
@@ -1522,32 +1538,78 @@ class MoleculeScene(QGraphicsScene):
|
|
|
1522
1538
|
target_bonds = [it for it in self.selectedItems() if isinstance(it, BondItem)]
|
|
1523
1539
|
|
|
1524
1540
|
if target_bonds:
|
|
1525
|
-
|
|
1541
|
+
any_bond_changed = False
|
|
1526
1542
|
for bond in target_bonds:
|
|
1543
|
+
# 1. 結合の向きを考慮して、データ辞書内の現在のキーを正しく特定する
|
|
1527
1544
|
id1, id2 = bond.atom1.atom_id, bond.atom2.atom_id
|
|
1528
|
-
|
|
1545
|
+
current_key = None
|
|
1546
|
+
if (id1, id2) in self.data.bonds:
|
|
1547
|
+
current_key = (id1, id2)
|
|
1548
|
+
elif (id2, id1) in self.data.bonds:
|
|
1549
|
+
current_key = (id2, id1)
|
|
1550
|
+
|
|
1551
|
+
if not current_key: continue
|
|
1552
|
+
|
|
1553
|
+
# 2. 変更前の状態を保存
|
|
1554
|
+
old_order, old_stereo = bond.order, bond.stereo
|
|
1555
|
+
|
|
1556
|
+
# 3. キー入力に応じてBondItemのプロパティを変更
|
|
1557
|
+
if key == Qt.Key.Key_W:
|
|
1558
|
+
if bond.stereo == 1:
|
|
1559
|
+
bond_data = self.data.bonds.pop(current_key)
|
|
1560
|
+
new_key = (current_key[1], current_key[0])
|
|
1561
|
+
self.data.bonds[new_key] = bond_data
|
|
1562
|
+
bond.atom1, bond.atom2 = bond.atom2, bond.atom1
|
|
1563
|
+
bond.update_position()
|
|
1564
|
+
was_reversed = True
|
|
1565
|
+
else:
|
|
1566
|
+
bond.order = 1; bond.stereo = 1
|
|
1567
|
+
|
|
1568
|
+
elif key == Qt.Key.Key_D:
|
|
1569
|
+
if bond.stereo == 2:
|
|
1570
|
+
bond_data = self.data.bonds.pop(current_key)
|
|
1571
|
+
new_key = (current_key[1], current_key[0])
|
|
1572
|
+
self.data.bonds[new_key] = bond_data
|
|
1573
|
+
bond.atom1, bond.atom2 = bond.atom2, bond.atom1
|
|
1574
|
+
bond.update_position()
|
|
1575
|
+
was_reversed = True
|
|
1576
|
+
else:
|
|
1577
|
+
bond.order = 1; bond.stereo = 2
|
|
1529
1578
|
|
|
1530
|
-
|
|
1531
|
-
bond.order = 1; bond.stereo = 0
|
|
1579
|
+
elif key == Qt.Key.Key_1 and (bond.order != 1 or bond.stereo != 0):
|
|
1580
|
+
bond.order = 1; bond.stereo = 0
|
|
1532
1581
|
elif key == Qt.Key.Key_2 and bond.order != 2:
|
|
1533
1582
|
bond.order = 2; bond.stereo = 0; needs_update = True
|
|
1534
1583
|
elif key == Qt.Key.Key_3 and bond.order != 3:
|
|
1535
1584
|
bond.order = 3; bond.stereo = 0; needs_update = True
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
self.data.bonds
|
|
1543
|
-
|
|
1585
|
+
|
|
1586
|
+
# 4. 実際に変更があった場合のみデータモデルを更新
|
|
1587
|
+
if old_order != bond.order or old_stereo != bond.stereo:
|
|
1588
|
+
any_bond_changed = True
|
|
1589
|
+
|
|
1590
|
+
# 5. 古いキーでデータを辞書から一度削除
|
|
1591
|
+
bond_data = self.data.bonds.pop(current_key)
|
|
1592
|
+
bond_data['order'] = bond.order
|
|
1593
|
+
bond_data['stereo'] = bond.stereo
|
|
1594
|
+
|
|
1595
|
+
# 6. 変更後の種類に応じて新しいキーを決定し、再登録する
|
|
1596
|
+
new_key_id1, new_key_id2 = bond.atom1.atom_id, bond.atom2.atom_id
|
|
1597
|
+
if bond.stereo == 0:
|
|
1598
|
+
if new_key_id1 > new_key_id2:
|
|
1599
|
+
new_key_id1, new_key_id2 = new_key_id2, new_key_id1
|
|
1600
|
+
|
|
1601
|
+
new_key = (new_key_id1, new_key_id2)
|
|
1602
|
+
self.data.bonds[new_key] = bond_data
|
|
1603
|
+
|
|
1544
1604
|
bond.update()
|
|
1545
1605
|
|
|
1546
|
-
if
|
|
1606
|
+
if any_bond_changed:
|
|
1547
1607
|
self.window.push_undo_state()
|
|
1608
|
+
|
|
1609
|
+
if key in [Qt.Key.Key_1, Qt.Key.Key_2, Qt.Key.Key_3, Qt.Key.Key_W, Qt.Key.Key_D]:
|
|
1548
1610
|
event.accept()
|
|
1549
1611
|
return
|
|
1550
|
-
|
|
1612
|
+
|
|
1551
1613
|
# --- 3. Atomに対する操作 (原子の追加 - マージされた機能) ---
|
|
1552
1614
|
if key == Qt.Key.Key_1:
|
|
1553
1615
|
start_atom = None
|
|
@@ -1731,8 +1793,6 @@ class MoleculeScene(QGraphicsScene):
|
|
|
1731
1793
|
# --- どの操作にも当てはまらない場合 ---
|
|
1732
1794
|
super().keyPressEvent(event)
|
|
1733
1795
|
|
|
1734
|
-
|
|
1735
|
-
# In MoleculeScene class
|
|
1736
1796
|
def find_atom_near(self, pos, tol=14.0):
|
|
1737
1797
|
# Create a small search rectangle around the position
|
|
1738
1798
|
search_rect = QRectF(pos.x() - tol, pos.y() - tol, 2 * tol, 2 * tol)
|
|
@@ -1763,7 +1823,7 @@ class ZoomableView(QGraphicsView):
|
|
|
1763
1823
|
self.setDragMode(QGraphicsView.DragMode.NoDrag)
|
|
1764
1824
|
|
|
1765
1825
|
self.main_window = parent
|
|
1766
|
-
self.setAcceptDrops(
|
|
1826
|
+
self.setAcceptDrops(False)
|
|
1767
1827
|
|
|
1768
1828
|
self._is_panning = False
|
|
1769
1829
|
self._pan_start_pos = QPointF()
|
|
@@ -1841,44 +1901,7 @@ class ZoomableView(QGraphicsView):
|
|
|
1841
1901
|
else:
|
|
1842
1902
|
super().mouseReleaseEvent(event)
|
|
1843
1903
|
|
|
1844
|
-
# ZoomableView クラス内 (約1691行目あたりから)
|
|
1845
|
-
|
|
1846
|
-
def dragEnterEvent(self, event):
|
|
1847
|
-
"""ファイルがこのウィジェット上にドラッグされたときに呼び出される"""
|
|
1848
|
-
# ドラッグされたデータにファイルパスが含まれている場合のみ受け入れる
|
|
1849
|
-
if event.mimeData().hasUrls():
|
|
1850
|
-
event.acceptProposedAction()
|
|
1851
|
-
else:
|
|
1852
|
-
event.ignore()
|
|
1853
|
-
|
|
1854
|
-
def dragMoveEvent(self, event):
|
|
1855
|
-
"""
|
|
1856
|
-
ウィジェット上でドラッグが移動したときに呼び出される。
|
|
1857
|
-
"""
|
|
1858
|
-
if event.mimeData().hasUrls():
|
|
1859
|
-
event.acceptProposedAction()
|
|
1860
|
-
else:
|
|
1861
|
-
super().dragMoveEvent(event)
|
|
1862
1904
|
|
|
1863
|
-
def dropEvent(self, event):
|
|
1864
|
-
"""ファイルがこのウィジェット上でドロップされたときに呼び出される"""
|
|
1865
|
-
urls = event.mimeData().urls()
|
|
1866
|
-
if urls and urls[0].isLocalFile():
|
|
1867
|
-
file_path = urls[0].toLocalFile()
|
|
1868
|
-
|
|
1869
|
-
# MainWindowのファイル読み込みメソッドを呼び出す
|
|
1870
|
-
if self.main_window:
|
|
1871
|
-
# 拡張子によって適切な読み込み処理を呼び出す
|
|
1872
|
-
if file_path.lower().endswith('.pmeraw'):
|
|
1873
|
-
self.main_window.load_raw_data(file_path=file_path)
|
|
1874
|
-
elif file_path.lower().endswith(('.mol', '.sdf')):
|
|
1875
|
-
self.main_window.load_mol_file(file_path=file_path)
|
|
1876
|
-
|
|
1877
|
-
event.acceptProposedAction()
|
|
1878
|
-
else:
|
|
1879
|
-
event.ignore()
|
|
1880
|
-
else:
|
|
1881
|
-
event.ignore()
|
|
1882
1905
|
|
|
1883
1906
|
class CalculationWorker(QObject):
|
|
1884
1907
|
status_update = pyqtSignal(str)
|
|
@@ -2123,6 +2146,143 @@ class AnalysisWindow(QDialog):
|
|
|
2123
2146
|
if self.parent() and hasattr(self.parent(), 'statusBar'):
|
|
2124
2147
|
self.parent().statusBar().showMessage(f"Copied '{text}' to clipboard.", 2000)
|
|
2125
2148
|
|
|
2149
|
+
|
|
2150
|
+
class SettingsDialog(QDialog):
|
|
2151
|
+
def __init__(self, current_settings, parent=None):
|
|
2152
|
+
super().__init__(parent)
|
|
2153
|
+
self.setWindowTitle("3D View Settings")
|
|
2154
|
+
|
|
2155
|
+
# デフォルト設定をクラス内で定義
|
|
2156
|
+
self.default_settings = {
|
|
2157
|
+
'background_color': '#919191',
|
|
2158
|
+
'lighting_enabled': True,
|
|
2159
|
+
'specular': 0.20,
|
|
2160
|
+
'specular_power': 20,
|
|
2161
|
+
'light_intensity': 1.0,
|
|
2162
|
+
'show_3d_axes': True,
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
# --- 選択された色を管理する専用のインスタンス変数 ---
|
|
2166
|
+
self.current_bg_color = None
|
|
2167
|
+
|
|
2168
|
+
# --- UI要素の作成 ---
|
|
2169
|
+
layout = QVBoxLayout(self)
|
|
2170
|
+
form_layout = QFormLayout()
|
|
2171
|
+
|
|
2172
|
+
# 1. 背景色
|
|
2173
|
+
self.bg_button = QPushButton()
|
|
2174
|
+
self.bg_button.setToolTip("Click to select a color")
|
|
2175
|
+
self.bg_button.clicked.connect(self.select_color)
|
|
2176
|
+
form_layout.addRow("Background Color:", self.bg_button)
|
|
2177
|
+
|
|
2178
|
+
# 1a. 軸の表示/非表示
|
|
2179
|
+
self.axes_checkbox = QCheckBox()
|
|
2180
|
+
form_layout.addRow("Show 3D Axes:", self.axes_checkbox)
|
|
2181
|
+
|
|
2182
|
+
# 2. ライトの有効/無効
|
|
2183
|
+
self.light_checkbox = QCheckBox()
|
|
2184
|
+
form_layout.addRow("Enable Lighting:", self.light_checkbox)
|
|
2185
|
+
|
|
2186
|
+
# 光の強さスライダーを追加
|
|
2187
|
+
self.intensity_slider = QSlider(Qt.Orientation.Horizontal)
|
|
2188
|
+
self.intensity_slider.setRange(0, 200) # 0.0 ~ 2.0 の範囲
|
|
2189
|
+
form_layout.addRow("Light Intensity:", self.intensity_slider)
|
|
2190
|
+
|
|
2191
|
+
# 3. 光沢 (Specular)
|
|
2192
|
+
self.specular_slider = QSlider(Qt.Orientation.Horizontal)
|
|
2193
|
+
self.specular_slider.setRange(0, 100)
|
|
2194
|
+
form_layout.addRow("Shininess (Specular):", self.specular_slider)
|
|
2195
|
+
|
|
2196
|
+
# 4. 光沢の強さ (Specular Power)
|
|
2197
|
+
self.spec_power_slider = QSlider(Qt.Orientation.Horizontal)
|
|
2198
|
+
self.spec_power_slider.setRange(0, 100)
|
|
2199
|
+
form_layout.addRow("Shininess Power:", self.spec_power_slider)
|
|
2200
|
+
|
|
2201
|
+
# 渡された設定でUIと内部変数を初期化
|
|
2202
|
+
self.update_ui_from_settings(current_settings)
|
|
2203
|
+
|
|
2204
|
+
layout.addLayout(form_layout)
|
|
2205
|
+
|
|
2206
|
+
# --- ボタンの配置 ---
|
|
2207
|
+
buttons = QHBoxLayout()
|
|
2208
|
+
reset_button = QPushButton("Reset to Defaults")
|
|
2209
|
+
reset_button.clicked.connect(self.reset_to_defaults)
|
|
2210
|
+
|
|
2211
|
+
ok_button = QPushButton("OK")
|
|
2212
|
+
cancel_button = QPushButton("Cancel")
|
|
2213
|
+
ok_button.clicked.connect(self.accept)
|
|
2214
|
+
cancel_button.clicked.connect(self.reject)
|
|
2215
|
+
|
|
2216
|
+
buttons.addWidget(reset_button)
|
|
2217
|
+
buttons.addStretch(1)
|
|
2218
|
+
buttons.addWidget(ok_button)
|
|
2219
|
+
buttons.addWidget(cancel_button)
|
|
2220
|
+
layout.addLayout(buttons)
|
|
2221
|
+
|
|
2222
|
+
def reset_to_defaults(self):
|
|
2223
|
+
"""UIをデフォルト設定に戻す"""
|
|
2224
|
+
self.update_ui_from_settings(self.default_settings)
|
|
2225
|
+
|
|
2226
|
+
def update_ui_from_settings(self, settings_dict):
|
|
2227
|
+
self.current_bg_color = settings_dict.get('background_color', self.default_settings['background_color'])
|
|
2228
|
+
self.update_color_button(self.current_bg_color)
|
|
2229
|
+
self.axes_checkbox.setChecked(settings_dict.get('show_3d_axes', self.default_settings['show_3d_axes']))
|
|
2230
|
+
self.light_checkbox.setChecked(settings_dict.get('lighting_enabled', self.default_settings['lighting_enabled']))
|
|
2231
|
+
self.intensity_slider.setValue(int(settings_dict.get('light_intensity', self.default_settings['light_intensity']) * 100))
|
|
2232
|
+
self.specular_slider.setValue(int(settings_dict.get('specular', self.default_settings['specular']) * 100))
|
|
2233
|
+
self.spec_power_slider.setValue(settings_dict.get('specular_power', self.default_settings['specular_power']))
|
|
2234
|
+
|
|
2235
|
+
def select_color(self):
|
|
2236
|
+
"""カラーピッカーを開き、選択された色を内部変数とUIに反映させる"""
|
|
2237
|
+
# 内部変数から現在の色を取得してカラーピッカーを初期化
|
|
2238
|
+
color = QColorDialog.getColor(QColor(self.current_bg_color), self)
|
|
2239
|
+
if color.isValid():
|
|
2240
|
+
# 内部変数を更新
|
|
2241
|
+
self.current_bg_color = color.name()
|
|
2242
|
+
# UIの見た目を更新
|
|
2243
|
+
self.update_color_button(self.current_bg_color)
|
|
2244
|
+
|
|
2245
|
+
def update_color_button(self, color_hex):
|
|
2246
|
+
"""ボタンの背景色と境界線を設定する"""
|
|
2247
|
+
self.bg_button.setStyleSheet(f"background-color: {color_hex}; border: 1px solid #888;")
|
|
2248
|
+
|
|
2249
|
+
def get_settings(self):
|
|
2250
|
+
return {
|
|
2251
|
+
'background_color': self.current_bg_color,
|
|
2252
|
+
'show_3d_axes': self.axes_checkbox.isChecked(),
|
|
2253
|
+
'lighting_enabled': self.light_checkbox.isChecked(),
|
|
2254
|
+
'light_intensity': self.intensity_slider.value() / 100.0,
|
|
2255
|
+
'specular': self.specular_slider.value() / 100.0,
|
|
2256
|
+
'specular_power': self.spec_power_slider.value()
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
|
|
2260
|
+
class CustomQtInteractor(QtInteractor):
|
|
2261
|
+
def __init__(self, parent=None, main_window=None, **kwargs):
|
|
2262
|
+
super().__init__(parent, **kwargs)
|
|
2263
|
+
self.main_window = main_window
|
|
2264
|
+
|
|
2265
|
+
def wheelEvent(self, event):
|
|
2266
|
+
"""
|
|
2267
|
+
マウスホイールイベントをオーバーライドする。
|
|
2268
|
+
"""
|
|
2269
|
+
# 最初に親クラスのイベントを呼び、通常のズーム処理を実行させる
|
|
2270
|
+
super().wheelEvent(event)
|
|
2271
|
+
|
|
2272
|
+
# ズーム処理の完了後、2Dビューにフォーカスを強制的に戻す
|
|
2273
|
+
if self.main_window and hasattr(self.main_window, 'view_2d'):
|
|
2274
|
+
self.main_window.view_2d.setFocus()
|
|
2275
|
+
|
|
2276
|
+
|
|
2277
|
+
def mouseReleaseEvent(self, event):
|
|
2278
|
+
"""
|
|
2279
|
+
Qtのマウスリリースイベントをオーバーライドし、
|
|
2280
|
+
3Dビューでの全ての操作完了後に2Dビューへフォーカスを戻す。
|
|
2281
|
+
"""
|
|
2282
|
+
super().mouseReleaseEvent(event) # 親クラスのイベントを先に処理
|
|
2283
|
+
if self.main_window and hasattr(self.main_window, 'view_2d'):
|
|
2284
|
+
self.main_window.view_2d.setFocus()
|
|
2285
|
+
|
|
2126
2286
|
# --- 3Dインタラクションを管理する専用クラス ---
|
|
2127
2287
|
class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
2128
2288
|
def __init__(self, main_window):
|
|
@@ -2146,7 +2306,8 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
2146
2306
|
is_temp_mode = bool(QApplication.keyboardModifiers() & Qt.KeyboardModifier.AltModifier)
|
|
2147
2307
|
is_edit_active = mw.is_3d_edit_mode or is_temp_mode
|
|
2148
2308
|
|
|
2149
|
-
|
|
2309
|
+
# 3D分子(mw.current_mol)が存在する場合のみ、原子の選択処理を実行
|
|
2310
|
+
if is_edit_active and mw.current_mol:
|
|
2150
2311
|
click_pos = self.GetInteractor().GetEventPosition()
|
|
2151
2312
|
picker = mw.plotter.picker
|
|
2152
2313
|
picker.Pick(click_pos[0], click_pos[1], 0, mw.plotter.renderer)
|
|
@@ -2156,17 +2317,20 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
2156
2317
|
distances = np.linalg.norm(mw.atom_positions_3d - picked_position, axis=1)
|
|
2157
2318
|
closest_atom_idx = np.argmin(distances)
|
|
2158
2319
|
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2320
|
+
# RDKitのMolオブジェクトから原子を安全に取得
|
|
2321
|
+
atom = mw.current_mol.GetAtomWithIdx(int(closest_atom_idx))
|
|
2322
|
+
if atom:
|
|
2323
|
+
atomic_num = atom.GetAtomicNum()
|
|
2324
|
+
vdw_radius = pt.GetRvdw(atomic_num)
|
|
2325
|
+
click_threshold = vdw_radius * 1.5
|
|
2326
|
+
|
|
2327
|
+
if distances[closest_atom_idx] < click_threshold:
|
|
2328
|
+
# 原子を掴むことに成功した場合
|
|
2329
|
+
self._is_dragging_atom = True
|
|
2330
|
+
self.is_dragging = False
|
|
2331
|
+
mw.dragged_atom_info = {'id': int(closest_atom_idx)}
|
|
2332
|
+
mw.plotter.setCursor(Qt.CursorShape.ClosedHandCursor)
|
|
2333
|
+
return # 親クラスのカメラ回転を呼ばない
|
|
2170
2334
|
|
|
2171
2335
|
self._is_dragging_atom = False
|
|
2172
2336
|
super().OnLeftButtonDown()
|
|
@@ -2240,12 +2404,14 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
2240
2404
|
|
|
2241
2405
|
# 状態をリセット
|
|
2242
2406
|
self._is_dragging_atom = False
|
|
2407
|
+
self.is_dragging = False # is_draggingもリセット
|
|
2243
2408
|
|
|
2244
|
-
#
|
|
2245
|
-
# on_mouse_moveを一度呼び出します
|
|
2409
|
+
# ボタンを離した後のカーソル表示を最新の状態に更新
|
|
2246
2410
|
self.on_mouse_move(obj, event)
|
|
2247
2411
|
|
|
2248
|
-
|
|
2412
|
+
# 2Dビューにフォーカスを戻し、ショートカットキーなどが使えるようにする
|
|
2413
|
+
if mw and mw.view_2d:
|
|
2414
|
+
mw.view_2d.setFocus()
|
|
2249
2415
|
|
|
2250
2416
|
class MainWindow(QMainWindow):
|
|
2251
2417
|
|
|
@@ -2253,6 +2419,11 @@ class MainWindow(QMainWindow):
|
|
|
2253
2419
|
def __init__(self, initial_file=None):
|
|
2254
2420
|
super().__init__()
|
|
2255
2421
|
self.setAcceptDrops(True)
|
|
2422
|
+
self.settings_dir = os.path.join(os.path.expanduser('~'), '.moleditpy')
|
|
2423
|
+
self.settings_file = os.path.join(self.settings_dir, 'settings.json')
|
|
2424
|
+
self.settings = {}
|
|
2425
|
+
self.load_settings()
|
|
2426
|
+
self.initial_settings = self.settings.copy()
|
|
2256
2427
|
self.setWindowTitle("MoleditPy -- Python Molecular Editor Ver. " + VERSION); self.setGeometry(100, 100, 1400, 800)
|
|
2257
2428
|
self.data = MolecularData(); self.current_mol = None
|
|
2258
2429
|
self.current_3d_style = 'ball_and_stick'
|
|
@@ -2260,6 +2431,9 @@ class MainWindow(QMainWindow):
|
|
|
2260
2431
|
self.is_3d_edit_mode = False
|
|
2261
2432
|
self.dragged_atom_info = None
|
|
2262
2433
|
self.atom_actor = None
|
|
2434
|
+
self.is_2d_editable = True
|
|
2435
|
+
self.axes_actor = None
|
|
2436
|
+
self.axes_widget = None
|
|
2263
2437
|
self.undo_stack = []
|
|
2264
2438
|
self.redo_stack = []
|
|
2265
2439
|
self.mode_actions = {}
|
|
@@ -2287,6 +2461,8 @@ class MainWindow(QMainWindow):
|
|
|
2287
2461
|
|
|
2288
2462
|
if initial_file:
|
|
2289
2463
|
self.load_raw_data(file_path=initial_file)
|
|
2464
|
+
|
|
2465
|
+
QTimer.singleShot(0, self.apply_initial_settings)
|
|
2290
2466
|
|
|
2291
2467
|
def init_ui(self):
|
|
2292
2468
|
# 1. 現在のスクリプトがあるディレクトリのパスを取得
|
|
@@ -2306,8 +2482,8 @@ class MainWindow(QMainWindow):
|
|
|
2306
2482
|
|
|
2307
2483
|
self.init_menu_bar()
|
|
2308
2484
|
|
|
2309
|
-
splitter=QSplitter(Qt.Orientation.Horizontal)
|
|
2310
|
-
self.setCentralWidget(splitter)
|
|
2485
|
+
self.splitter = QSplitter(Qt.Orientation.Horizontal)
|
|
2486
|
+
self.setCentralWidget(self.splitter)
|
|
2311
2487
|
|
|
2312
2488
|
left_pane=QWidget()
|
|
2313
2489
|
left_pane.setAcceptDrops(True)
|
|
@@ -2337,13 +2513,13 @@ class MainWindow(QMainWindow):
|
|
|
2337
2513
|
left_buttons_layout.addWidget(self.convert_button)
|
|
2338
2514
|
|
|
2339
2515
|
left_layout.addLayout(left_buttons_layout)
|
|
2340
|
-
splitter.addWidget(left_pane)
|
|
2516
|
+
self.splitter.addWidget(left_pane)
|
|
2341
2517
|
|
|
2342
2518
|
# --- 右パネルとボタンレイアウト ---
|
|
2343
2519
|
right_pane = QWidget()
|
|
2344
2520
|
# 1. 右パネル全体は「垂直」レイアウトにする
|
|
2345
2521
|
right_layout = QVBoxLayout(right_pane)
|
|
2346
|
-
self.plotter =
|
|
2522
|
+
self.plotter = CustomQtInteractor(right_pane, main_window=self, lighting='none')
|
|
2347
2523
|
self.plotter.setAcceptDrops(False)
|
|
2348
2524
|
self.plotter.setSizePolicy(
|
|
2349
2525
|
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
|
|
@@ -2383,9 +2559,9 @@ class MainWindow(QMainWindow):
|
|
|
2383
2559
|
|
|
2384
2560
|
# 4. 水平のボタンレイアウトを、全体の垂直レイアウトに追加
|
|
2385
2561
|
right_layout.addLayout(right_buttons_layout)
|
|
2386
|
-
splitter.addWidget(right_pane)
|
|
2562
|
+
self.splitter.addWidget(right_pane)
|
|
2387
2563
|
|
|
2388
|
-
splitter.setSizes([600, 600])
|
|
2564
|
+
self.splitter.setSizes([600, 600])
|
|
2389
2565
|
|
|
2390
2566
|
# ステータスバーを左右に分離するための設定
|
|
2391
2567
|
self.status_bar = self.statusBar()
|
|
@@ -2417,6 +2593,7 @@ class MainWindow(QMainWindow):
|
|
|
2417
2593
|
|
|
2418
2594
|
if mode == 'atom_other':
|
|
2419
2595
|
action.triggered.connect(self.open_periodic_table_dialog)
|
|
2596
|
+
self.other_atom_action = action
|
|
2420
2597
|
else:
|
|
2421
2598
|
action.triggered.connect(lambda c, m=mode: self.set_mode(m))
|
|
2422
2599
|
self.mode_actions[mode] = action
|
|
@@ -2639,6 +2816,12 @@ class MainWindow(QMainWindow):
|
|
|
2639
2816
|
import_inchi_action = QAction("Import InChI...", self)
|
|
2640
2817
|
import_inchi_action.triggered.connect(self.import_inchi_dialog)
|
|
2641
2818
|
file_menu.addAction(import_inchi_action)
|
|
2819
|
+
|
|
2820
|
+
file_menu.addSeparator()
|
|
2821
|
+
load_3d_mol_action = QAction("Load 3D MOL (3D Only)...", self)
|
|
2822
|
+
load_3d_mol_action.triggered.connect(self.load_mol_for_3d_viewing)
|
|
2823
|
+
file_menu.addAction(load_3d_mol_action)
|
|
2824
|
+
|
|
2642
2825
|
file_menu.addSeparator()
|
|
2643
2826
|
save_mol_action = QAction("Save 2D as MOL...", self); save_mol_action.triggered.connect(self.save_as_mol)
|
|
2644
2827
|
file_menu.addAction(save_mol_action)
|
|
@@ -2656,6 +2839,16 @@ class MainWindow(QMainWindow):
|
|
|
2656
2839
|
load_raw_action.setShortcut(QKeySequence.StandardKey.Open)
|
|
2657
2840
|
file_menu.addAction(load_raw_action)
|
|
2658
2841
|
|
|
2842
|
+
file_menu.addSeparator()
|
|
2843
|
+
|
|
2844
|
+
export_2d_png_action = QAction("Export 2D as PNG...", self)
|
|
2845
|
+
export_2d_png_action.triggered.connect(self.export_2d_png)
|
|
2846
|
+
file_menu.addAction(export_2d_png_action)
|
|
2847
|
+
|
|
2848
|
+
export_3d_png_action = QAction("Export 3D as PNG...", self)
|
|
2849
|
+
export_3d_png_action.triggered.connect(self.export_3d_png)
|
|
2850
|
+
file_menu.addAction(export_3d_png_action)
|
|
2851
|
+
|
|
2659
2852
|
file_menu.addSeparator()
|
|
2660
2853
|
quit_action = QAction("Quit", self)
|
|
2661
2854
|
quit_action.triggered.connect(self.close)
|
|
@@ -2735,6 +2928,13 @@ class MainWindow(QMainWindow):
|
|
|
2735
2928
|
|
|
2736
2929
|
view_menu.addSeparator()
|
|
2737
2930
|
|
|
2931
|
+
reset_3d_view_action = QAction("Reset 3D View", self)
|
|
2932
|
+
reset_3d_view_action.triggered.connect(lambda: self.plotter.reset_camera() if hasattr(self, 'plotter') else None)
|
|
2933
|
+
reset_3d_view_action.setShortcut(QKeySequence("Ctrl+R"))
|
|
2934
|
+
view_menu.addAction(reset_3d_view_action)
|
|
2935
|
+
|
|
2936
|
+
view_menu.addSeparator()
|
|
2937
|
+
|
|
2738
2938
|
self.toggle_chiral_action = QAction("Show Chiral Labels", self, checkable=True)
|
|
2739
2939
|
self.toggle_chiral_action.setChecked(self.show_chiral_labels)
|
|
2740
2940
|
self.toggle_chiral_action.triggered.connect(self.toggle_chiral_labels_display)
|
|
@@ -2746,6 +2946,11 @@ class MainWindow(QMainWindow):
|
|
|
2746
2946
|
self.analysis_action.setEnabled(False)
|
|
2747
2947
|
analysis_menu.addAction(self.analysis_action)
|
|
2748
2948
|
|
|
2949
|
+
settings_menu = menu_bar.addMenu("&Settings")
|
|
2950
|
+
view_settings_action = QAction("3D View Settings...", self)
|
|
2951
|
+
view_settings_action.triggered.connect(self.open_settings_dialog)
|
|
2952
|
+
settings_menu.addAction(view_settings_action)
|
|
2953
|
+
|
|
2749
2954
|
help_menu = menu_bar.addMenu("&Help")
|
|
2750
2955
|
about_action = QAction("About", self)
|
|
2751
2956
|
about_action.triggered.connect(lambda: QMessageBox.about(
|
|
@@ -2774,7 +2979,6 @@ class MainWindow(QMainWindow):
|
|
|
2774
2979
|
|
|
2775
2980
|
def set_mode(self, mode_str):
|
|
2776
2981
|
self.scene.mode = mode_str
|
|
2777
|
-
|
|
2778
2982
|
self.view_2d.setMouseTracking(True)
|
|
2779
2983
|
if not mode_str.startswith('template'):
|
|
2780
2984
|
self.scene.template_preview.hide()
|
|
@@ -3020,14 +3224,23 @@ class MainWindow(QMainWindow):
|
|
|
3020
3224
|
self.edit_3d_action.setEnabled(False)
|
|
3021
3225
|
self.statusBar().showMessage("Calculating 3D structure...")
|
|
3022
3226
|
self.plotter.clear()
|
|
3227
|
+
bg_color_hex = self.settings.get('background_color', '#919191')
|
|
3228
|
+
bg_qcolor = QColor(bg_color_hex)
|
|
3229
|
+
|
|
3230
|
+
if bg_qcolor.isValid():
|
|
3231
|
+
luminance = bg_qcolor.toHsl().lightness()
|
|
3232
|
+
text_color = 'black' if luminance > 128 else 'white'
|
|
3233
|
+
else:
|
|
3234
|
+
text_color = 'white'
|
|
3235
|
+
|
|
3023
3236
|
text_actor = self.plotter.add_text(
|
|
3024
3237
|
"Calculating...",
|
|
3025
|
-
position='
|
|
3238
|
+
position='lower_right',
|
|
3026
3239
|
font_size=15,
|
|
3027
|
-
color=
|
|
3240
|
+
color=text_color,
|
|
3028
3241
|
name='calculating_text'
|
|
3029
3242
|
)
|
|
3030
|
-
text_actor.GetTextProperty().SetOpacity(
|
|
3243
|
+
text_actor.GetTextProperty().SetOpacity(1)
|
|
3031
3244
|
self.plotter.render()
|
|
3032
3245
|
self.start_calculation.emit(mol_block)
|
|
3033
3246
|
|
|
@@ -3112,6 +3325,8 @@ class MainWindow(QMainWindow):
|
|
|
3112
3325
|
state['version'] = VERSION
|
|
3113
3326
|
|
|
3114
3327
|
if self.current_mol: state['mol_3d'] = self.current_mol.ToBinary()
|
|
3328
|
+
|
|
3329
|
+
state['is_3d_viewer_mode'] = not self.is_2d_editable
|
|
3115
3330
|
|
|
3116
3331
|
return state
|
|
3117
3332
|
|
|
@@ -3188,6 +3403,12 @@ class MainWindow(QMainWindow):
|
|
|
3188
3403
|
|
|
3189
3404
|
self.update_implicit_hydrogens()
|
|
3190
3405
|
self.update_chiral_labels()
|
|
3406
|
+
|
|
3407
|
+
if loaded_data.get('is_3d_viewer_mode', False):
|
|
3408
|
+
self._enter_3d_viewer_ui_mode()
|
|
3409
|
+
self.statusBar().showMessage("Project loaded in 3D Viewer Mode.")
|
|
3410
|
+
else:
|
|
3411
|
+
self.restore_ui_for_editing()
|
|
3191
3412
|
|
|
3192
3413
|
|
|
3193
3414
|
def push_undo_state(self):
|
|
@@ -3272,10 +3493,13 @@ class MainWindow(QMainWindow):
|
|
|
3272
3493
|
item.setSelected(True)
|
|
3273
3494
|
|
|
3274
3495
|
def clear_all(self):
|
|
3496
|
+
|
|
3497
|
+
self.restore_ui_for_editing()
|
|
3498
|
+
|
|
3275
3499
|
# データが存在しない場合は何もしない
|
|
3276
3500
|
if not self.data.atoms and self.current_mol is None:
|
|
3277
3501
|
return
|
|
3278
|
-
|
|
3502
|
+
|
|
3279
3503
|
self.dragged_atom_info = None
|
|
3280
3504
|
|
|
3281
3505
|
# 2Dエディタをクリアする(Undoスタックにはプッシュしない)
|
|
@@ -3384,7 +3608,8 @@ class MainWindow(QMainWindow):
|
|
|
3384
3608
|
AllChem.AssignStereochemistry(mol, cleanIt=True, force=True)
|
|
3385
3609
|
conf = mol.GetConformer()
|
|
3386
3610
|
AllChem.WedgeMolBonds(mol, conf)
|
|
3387
|
-
|
|
3611
|
+
|
|
3612
|
+
self.restore_ui_for_editing()
|
|
3388
3613
|
self.clear_2d_editor(push_to_undo=False)
|
|
3389
3614
|
self.current_mol = None
|
|
3390
3615
|
self.plotter.clear()
|
|
@@ -3453,7 +3678,8 @@ class MainWindow(QMainWindow):
|
|
|
3453
3678
|
AllChem.AssignStereochemistry(mol, cleanIt=True, force=True)
|
|
3454
3679
|
conf = mol.GetConformer()
|
|
3455
3680
|
AllChem.WedgeMolBonds(mol, conf)
|
|
3456
|
-
|
|
3681
|
+
|
|
3682
|
+
self.restore_ui_for_editing()
|
|
3457
3683
|
self.clear_2d_editor(push_to_undo=False)
|
|
3458
3684
|
self.current_mol = None
|
|
3459
3685
|
self.plotter.clear()
|
|
@@ -3505,9 +3731,12 @@ class MainWindow(QMainWindow):
|
|
|
3505
3731
|
except Exception as e:
|
|
3506
3732
|
self.statusBar().showMessage(f"Error loading from InChI: {e}")
|
|
3507
3733
|
|
|
3508
|
-
def load_mol_file(self):
|
|
3509
|
-
|
|
3510
|
-
|
|
3734
|
+
def load_mol_file(self, file_path=None):
|
|
3735
|
+
if not file_path:
|
|
3736
|
+
options = QFileDialog.Option.DontUseNativeDialog
|
|
3737
|
+
file_path, _ = QFileDialog.getOpenFileName(self, "Import MOL File", "", "Chemical Files (*.mol *.sdf);;All Files (*)", options=options)
|
|
3738
|
+
if not file_path: return
|
|
3739
|
+
|
|
3511
3740
|
if not file_path: return
|
|
3512
3741
|
try:
|
|
3513
3742
|
self.dragged_atom_info = None
|
|
@@ -3516,7 +3745,8 @@ class MainWindow(QMainWindow):
|
|
|
3516
3745
|
if mol is None: raise ValueError("Failed to read molecule from file.")
|
|
3517
3746
|
|
|
3518
3747
|
Chem.Kekulize(mol)
|
|
3519
|
-
|
|
3748
|
+
|
|
3749
|
+
self.restore_ui_for_editing()
|
|
3520
3750
|
self.clear_2d_editor(push_to_undo=False)
|
|
3521
3751
|
self.current_mol = None; self.plotter.clear(); self.analysis_action.setEnabled(False)
|
|
3522
3752
|
|
|
@@ -3573,9 +3803,44 @@ class MainWindow(QMainWindow):
|
|
|
3573
3803
|
self.reset_undo_stack()
|
|
3574
3804
|
QTimer.singleShot(0, self.fit_to_view)
|
|
3575
3805
|
except Exception as e: self.statusBar().showMessage(f"Error loading file: {e}")
|
|
3806
|
+
|
|
3807
|
+
def load_mol_for_3d_viewing(self):
|
|
3808
|
+
options = QFileDialog.Option.DontUseNativeDialog
|
|
3809
|
+
file_path, _ = QFileDialog.getOpenFileName(self, "Load 3D MOL (View Only)", "", "Chemical Files (*.mol *.sdf);;All Files (*)", options=options)
|
|
3810
|
+
if not file_path:
|
|
3811
|
+
return
|
|
3812
|
+
|
|
3813
|
+
try:
|
|
3814
|
+
suppl = Chem.SDMolSupplier(file_path, removeHs=False)
|
|
3815
|
+
mol = next(suppl, None)
|
|
3816
|
+
if mol is None:
|
|
3817
|
+
raise ValueError("Failed to read molecule.")
|
|
3818
|
+
if mol.GetNumConformers() == 0:
|
|
3819
|
+
raise ValueError("MOL file has no 3D coordinates.")
|
|
3820
|
+
|
|
3821
|
+
# 2Dエディタをクリア
|
|
3822
|
+
self.clear_2d_editor(push_to_undo=False)
|
|
3823
|
+
|
|
3824
|
+
# 3D構造をセットして描画
|
|
3825
|
+
self.current_mol = mol
|
|
3826
|
+
self.draw_molecule_3d(self.current_mol)
|
|
3827
|
+
self.plotter.reset_camera()
|
|
3828
|
+
|
|
3829
|
+
# UIを3Dビューアモードに設定
|
|
3830
|
+
self._enter_3d_viewer_ui_mode()
|
|
3831
|
+
|
|
3832
|
+
self.statusBar().showMessage(f"3D Viewer Mode: Loaded {os.path.basename(file_path)}")
|
|
3833
|
+
self.reset_undo_stack()
|
|
3834
|
+
|
|
3835
|
+
except Exception as e:
|
|
3836
|
+
self.statusBar().showMessage(f"Error loading 3D file: {e}", 5000)
|
|
3837
|
+
self.restore_ui_for_editing()
|
|
3838
|
+
|
|
3576
3839
|
|
|
3577
3840
|
def save_raw_data(self):
|
|
3578
|
-
if not self.data.atoms
|
|
3841
|
+
if not self.data.atoms and not self.current_mol:
|
|
3842
|
+
self.statusBar().showMessage("Error: Nothing to save.")
|
|
3843
|
+
return
|
|
3579
3844
|
save_data = self.get_current_state()
|
|
3580
3845
|
options = QFileDialog.Option.DontUseNativeDialog
|
|
3581
3846
|
file_path, _ = QFileDialog.getSaveFileName(self, "Save Project File", "", "Project Files (*.pmeraw);;All Files (*)", options=options)
|
|
@@ -3586,6 +3851,7 @@ class MainWindow(QMainWindow):
|
|
|
3586
3851
|
self.statusBar().showMessage(f"Project saved to {file_path}")
|
|
3587
3852
|
except Exception as e: self.statusBar().showMessage(f"Error saving project file: {e}")
|
|
3588
3853
|
|
|
3854
|
+
|
|
3589
3855
|
def load_raw_data(self, file_path=None):
|
|
3590
3856
|
if not file_path:
|
|
3591
3857
|
options = QFileDialog.Option.DontUseNativeDialog
|
|
@@ -3594,6 +3860,7 @@ class MainWindow(QMainWindow):
|
|
|
3594
3860
|
|
|
3595
3861
|
try:
|
|
3596
3862
|
with open(file_path, 'rb') as f: loaded_data = pickle.load(f)
|
|
3863
|
+
self.restore_ui_for_editing()
|
|
3597
3864
|
self.set_state_from_data(loaded_data)
|
|
3598
3865
|
self.statusBar().showMessage(f"Project loaded from {file_path}")
|
|
3599
3866
|
self.reset_undo_stack()
|
|
@@ -3626,7 +3893,13 @@ class MainWindow(QMainWindow):
|
|
|
3626
3893
|
if not file_path.lower().endswith('.mol'):
|
|
3627
3894
|
file_path += '.mol'
|
|
3628
3895
|
try:
|
|
3629
|
-
|
|
3896
|
+
|
|
3897
|
+
mol_to_save = Chem.Mol(self.current_mol)
|
|
3898
|
+
|
|
3899
|
+
if mol_to_save.HasProp("_2D"):
|
|
3900
|
+
mol_to_save.ClearProp("_2D")
|
|
3901
|
+
|
|
3902
|
+
mol_block = Chem.MolToMolBlock(mol_to_save, includeStereo=True)
|
|
3630
3903
|
lines = mol_block.split('\n')
|
|
3631
3904
|
if len(lines) > 1 and 'RDKit' in lines[1]:
|
|
3632
3905
|
lines[1] = ' MoleditPy Ver. ' + VERSION + ' 3D'
|
|
@@ -3653,6 +3926,133 @@ class MainWindow(QMainWindow):
|
|
|
3653
3926
|
self.statusBar().showMessage(f"Successfully saved to {file_path}")
|
|
3654
3927
|
except Exception as e: self.statusBar().showMessage(f"Error saving file: {e}")
|
|
3655
3928
|
|
|
3929
|
+
def export_2d_png(self):
|
|
3930
|
+
if not self.data.atoms:
|
|
3931
|
+
self.statusBar().showMessage("Nothing to export.", 2000)
|
|
3932
|
+
return
|
|
3933
|
+
|
|
3934
|
+
options = QFileDialog.Option.DontUseNativeDialog
|
|
3935
|
+
filePath, _ = QFileDialog.getSaveFileName(self, "Export 2D as PNG", "", "PNG Files (*.png)", options=options)
|
|
3936
|
+
if not filePath:
|
|
3937
|
+
return
|
|
3938
|
+
|
|
3939
|
+
if not (filePath.lower().endswith(".png")):
|
|
3940
|
+
filePath += ".png"
|
|
3941
|
+
|
|
3942
|
+
reply = QMessageBox.question(self, 'Choose Background',
|
|
3943
|
+
'Do you want a transparent background?\n(Choose "No" for a white background)',
|
|
3944
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel,
|
|
3945
|
+
QMessageBox.StandardButton.No)
|
|
3946
|
+
|
|
3947
|
+
if reply == QMessageBox.StandardButton.Cancel:
|
|
3948
|
+
self.statusBar().showMessage("Export cancelled.", 2000)
|
|
3949
|
+
return
|
|
3950
|
+
|
|
3951
|
+
is_transparent = (reply == QMessageBox.StandardButton.Yes)
|
|
3952
|
+
|
|
3953
|
+
QApplication.processEvents()
|
|
3954
|
+
|
|
3955
|
+
items_to_restore = {}
|
|
3956
|
+
original_background = self.scene.backgroundBrush()
|
|
3957
|
+
|
|
3958
|
+
try:
|
|
3959
|
+
all_items = list(self.scene.items())
|
|
3960
|
+
for item in all_items:
|
|
3961
|
+
is_mol_part = isinstance(item, (AtomItem, BondItem))
|
|
3962
|
+
if not (is_mol_part and item.isVisible()):
|
|
3963
|
+
items_to_restore[item] = item.isVisible()
|
|
3964
|
+
item.hide()
|
|
3965
|
+
|
|
3966
|
+
molecule_bounds = QRectF()
|
|
3967
|
+
for item in self.scene.items():
|
|
3968
|
+
if isinstance(item, (AtomItem, BondItem)) and item.isVisible():
|
|
3969
|
+
molecule_bounds = molecule_bounds.united(item.sceneBoundingRect())
|
|
3970
|
+
|
|
3971
|
+
if molecule_bounds.isEmpty() or not molecule_bounds.isValid():
|
|
3972
|
+
self.statusBar().showMessage("Error: Could not determine molecule bounds for export.", 5000)
|
|
3973
|
+
return
|
|
3974
|
+
|
|
3975
|
+
if is_transparent:
|
|
3976
|
+
self.scene.setBackgroundBrush(QBrush(Qt.BrushStyle.NoBrush))
|
|
3977
|
+
else:
|
|
3978
|
+
self.scene.setBackgroundBrush(QBrush(QColor("#FFFFFF")))
|
|
3979
|
+
|
|
3980
|
+
rect_to_render = molecule_bounds.adjusted(-20, -20, 20, 20)
|
|
3981
|
+
|
|
3982
|
+
w = max(1, int(math.ceil(rect_to_render.width())))
|
|
3983
|
+
h = max(1, int(math.ceil(rect_to_render.height())))
|
|
3984
|
+
|
|
3985
|
+
if w <= 0 or h <= 0:
|
|
3986
|
+
self.statusBar().showMessage("Error: Invalid image size calculated.", 5000)
|
|
3987
|
+
return
|
|
3988
|
+
|
|
3989
|
+
image = QImage(w, h, QImage.Format.Format_ARGB32_Premultiplied)
|
|
3990
|
+
if is_transparent:
|
|
3991
|
+
image.fill(Qt.GlobalColor.transparent)
|
|
3992
|
+
else:
|
|
3993
|
+
image.fill(Qt.GlobalColor.white)
|
|
3994
|
+
|
|
3995
|
+
painter = QPainter()
|
|
3996
|
+
ok = painter.begin(image)
|
|
3997
|
+
if not ok or not painter.isActive():
|
|
3998
|
+
self.statusBar().showMessage("Failed to start QPainter for image rendering.", 5000)
|
|
3999
|
+
return
|
|
4000
|
+
|
|
4001
|
+
try:
|
|
4002
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
4003
|
+
target_rect = QRectF(0, 0, w, h)
|
|
4004
|
+
source_rect = rect_to_render
|
|
4005
|
+
self.scene.render(painter, target_rect, source_rect)
|
|
4006
|
+
finally:
|
|
4007
|
+
painter.end()
|
|
4008
|
+
|
|
4009
|
+
saved = image.save(filePath, "PNG")
|
|
4010
|
+
if saved:
|
|
4011
|
+
self.statusBar().showMessage(f"2D view exported to {filePath}", 3000)
|
|
4012
|
+
else:
|
|
4013
|
+
self.statusBar().showMessage(f"Failed to save image. Check file path or permissions.", 5000)
|
|
4014
|
+
|
|
4015
|
+
except Exception as e:
|
|
4016
|
+
self.statusBar().showMessage(f"An unexpected error occurred during 2D export: {e}", 5000)
|
|
4017
|
+
|
|
4018
|
+
finally:
|
|
4019
|
+
for item, was_visible in items_to_restore.items():
|
|
4020
|
+
item.setVisible(was_visible)
|
|
4021
|
+
self.scene.setBackgroundBrush(original_background)
|
|
4022
|
+
if self.view_2d:
|
|
4023
|
+
self.view_2d.viewport().update()
|
|
4024
|
+
|
|
4025
|
+
def export_3d_png(self):
|
|
4026
|
+
if not self.current_mol:
|
|
4027
|
+
self.statusBar().showMessage("No 3D molecule to export.", 2000)
|
|
4028
|
+
return
|
|
4029
|
+
|
|
4030
|
+
options = QFileDialog.Option.DontUseNativeDialog
|
|
4031
|
+
filePath, _ = QFileDialog.getSaveFileName(self, "Export 3D as PNG", "", "PNG Files (*.png)", options=options)
|
|
4032
|
+
if not filePath:
|
|
4033
|
+
return
|
|
4034
|
+
|
|
4035
|
+
if not (filePath.lower().endswith(".png")):
|
|
4036
|
+
filePath += ".png"
|
|
4037
|
+
|
|
4038
|
+
reply = QMessageBox.question(self, 'Choose Background',
|
|
4039
|
+
'Do you want a transparent background?\n(Choose "No" for current background)',
|
|
4040
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel,
|
|
4041
|
+
QMessageBox.StandardButton.No)
|
|
4042
|
+
|
|
4043
|
+
if reply == QMessageBox.StandardButton.Cancel:
|
|
4044
|
+
self.statusBar().showMessage("Export cancelled.", 2000)
|
|
4045
|
+
return
|
|
4046
|
+
|
|
4047
|
+
is_transparent = (reply == QMessageBox.StandardButton.Yes)
|
|
4048
|
+
|
|
4049
|
+
try:
|
|
4050
|
+
self.plotter.screenshot(filePath, transparent_background=is_transparent)
|
|
4051
|
+
self.statusBar().showMessage(f"3D view exported to {filePath}", 3000)
|
|
4052
|
+
except Exception as e:
|
|
4053
|
+
self.statusBar().showMessage(f"Error exporting 3D PNG: {e}", 3000)
|
|
4054
|
+
|
|
4055
|
+
|
|
3656
4056
|
def open_periodic_table_dialog(self):
|
|
3657
4057
|
dialog=PeriodicTableDialog(self); dialog.element_selected.connect(self.set_atom_from_periodic_table)
|
|
3658
4058
|
checked_action=self.tool_group.checkedAction()
|
|
@@ -3874,32 +4274,80 @@ class MainWindow(QMainWindow):
|
|
|
3874
4274
|
self.statusBar().showMessage("Resolved overlapping groups.", 2000)
|
|
3875
4275
|
|
|
3876
4276
|
|
|
4277
|
+
|
|
3877
4278
|
def draw_molecule_3d(self, mol):
|
|
4279
|
+
"""3D 分子を描画し、軸アクターの参照をクリアする(軸の再制御は apply_3d_settings に任せる)"""
|
|
4280
|
+
|
|
4281
|
+
# 1. カメラ状態とクリア
|
|
3878
4282
|
camera_state = self.plotter.camera.copy()
|
|
4283
|
+
|
|
4284
|
+
# **残留防止のための強制削除**
|
|
4285
|
+
if self.axes_actor is not None:
|
|
4286
|
+
try:
|
|
4287
|
+
self.plotter.remove_actor(self.axes_actor)
|
|
4288
|
+
except Exception:
|
|
4289
|
+
pass
|
|
4290
|
+
self.axes_actor = None
|
|
4291
|
+
|
|
3879
4292
|
self.plotter.clear()
|
|
4293
|
+
|
|
4294
|
+
# 2. 背景色の設定
|
|
4295
|
+
self.plotter.set_background(self.settings.get('background_color', '#4f4f4f'))
|
|
4296
|
+
|
|
4297
|
+
# 3. mol が None または原子数ゼロの場合は、背景と軸のみで終了
|
|
4298
|
+
if mol is None or mol.GetNumAtoms() == 0:
|
|
4299
|
+
self.atom_actor = None
|
|
4300
|
+
self.current_mol = None
|
|
4301
|
+
self.plotter.render()
|
|
4302
|
+
return
|
|
4303
|
+
|
|
4304
|
+
# 4. ライティングの設定
|
|
4305
|
+
is_lighting_enabled = self.settings.get('lighting_enabled', True)
|
|
4306
|
+
|
|
4307
|
+
if is_lighting_enabled:
|
|
4308
|
+
light = pv.Light(
|
|
4309
|
+
position=(1, 1, 2),
|
|
4310
|
+
light_type='cameralight',
|
|
4311
|
+
intensity=self.settings.get('light_intensity', 1.2)
|
|
4312
|
+
)
|
|
4313
|
+
self.plotter.add_light(light)
|
|
4314
|
+
|
|
4315
|
+
# 5. 分子描画ロジック
|
|
3880
4316
|
conf = mol.GetConformer()
|
|
3881
|
-
|
|
3882
|
-
# 3D原子座標をNumPy配列としてクラス変数に保持し、効率的な更新を可能にする
|
|
4317
|
+
|
|
3883
4318
|
self.atom_positions_3d = np.array([list(conf.GetAtomPosition(i)) for i in range(mol.GetNumAtoms())])
|
|
3884
|
-
|
|
4319
|
+
|
|
3885
4320
|
sym = [a.GetSymbol() for a in mol.GetAtoms()]
|
|
3886
4321
|
col = np.array([CPK_COLORS_PV.get(s, [0.5, 0.5, 0.5]) for s in sym])
|
|
3887
|
-
|
|
4322
|
+
|
|
3888
4323
|
if self.current_3d_style == 'cpk':
|
|
3889
4324
|
rad = np.array([pt.GetRvdw(pt.GetAtomicNumber(s)) * 1.0 for s in sym])
|
|
3890
|
-
else:
|
|
4325
|
+
else:
|
|
3891
4326
|
rad = np.array([VDW_RADII.get(s, 0.4) for s in sym])
|
|
3892
4327
|
|
|
3893
|
-
# 原子球(グリフ)の元となるPolyDataをクラス変数として保持
|
|
3894
4328
|
self.glyph_source = pv.PolyData(self.atom_positions_3d)
|
|
3895
4329
|
self.glyph_source['colors'] = col
|
|
3896
4330
|
self.glyph_source['radii'] = rad
|
|
3897
|
-
|
|
3898
|
-
glyphs = self.glyph_source.glyph(scale='radii', geom=pv.Sphere(radius=1.0), orient=False)
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
4331
|
+
|
|
4332
|
+
glyphs = self.glyph_source.glyph(scale='radii', geom=pv.Sphere(radius=1.0, theta_resolution=32, phi_resolution=32), orient=False)
|
|
4333
|
+
|
|
4334
|
+
mesh_props = dict(
|
|
4335
|
+
smooth_shading=True,
|
|
4336
|
+
specular=self.settings.get('specular', 0.2),
|
|
4337
|
+
specular_power=self.settings.get('specular_power', 20),
|
|
4338
|
+
lighting=is_lighting_enabled,
|
|
4339
|
+
)
|
|
4340
|
+
|
|
4341
|
+
if is_lighting_enabled:
|
|
4342
|
+
self.atom_actor = self.plotter.add_mesh(glyphs, scalars='colors', rgb=True, **mesh_props)
|
|
4343
|
+
else:
|
|
4344
|
+
self.atom_actor = self.plotter.add_mesh(
|
|
4345
|
+
glyphs, scalars='colors', rgb=True,
|
|
4346
|
+
style='surface', show_edges=True, edge_color='grey',
|
|
4347
|
+
**mesh_props
|
|
4348
|
+
)
|
|
4349
|
+
self.atom_actor.GetProperty().SetEdgeOpacity(0.3)
|
|
4350
|
+
|
|
3903
4351
|
|
|
3904
4352
|
if self.current_3d_style == 'ball_and_stick':
|
|
3905
4353
|
bond_meshes = []
|
|
@@ -3912,8 +4360,9 @@ class MainWindow(QMainWindow):
|
|
|
3912
4360
|
h = np.linalg.norm(d)
|
|
3913
4361
|
if h == 0: continue
|
|
3914
4362
|
|
|
4363
|
+
cyl_radius = 0.1
|
|
3915
4364
|
if bt == Chem.rdchem.BondType.SINGLE or bt == Chem.rdchem.BondType.AROMATIC:
|
|
3916
|
-
cyl = pv.Cylinder(center=c, direction=d, radius=
|
|
4365
|
+
cyl = pv.Cylinder(center=c, direction=d, radius=cyl_radius, height=h, resolution=16)
|
|
3917
4366
|
bond_meshes.append(cyl)
|
|
3918
4367
|
else:
|
|
3919
4368
|
v1 = d / h
|
|
@@ -3921,20 +4370,19 @@ class MainWindow(QMainWindow):
|
|
|
3921
4370
|
if np.allclose(np.abs(np.dot(v1, v_arb)), 1.0): v_arb = np.array([0, 1, 0])
|
|
3922
4371
|
off_dir = np.cross(v1, v_arb)
|
|
3923
4372
|
off_dir /= np.linalg.norm(off_dir)
|
|
4373
|
+
r, s = cyl_radius * 0.8, cyl_radius * 2.0
|
|
3924
4374
|
if bt == Chem.rdchem.BondType.DOUBLE:
|
|
3925
|
-
r, s = 0.08, 0.20
|
|
3926
4375
|
c1, c2 = c + off_dir * (s / 2), c - off_dir * (s / 2)
|
|
3927
|
-
bond_meshes.append(pv.Cylinder(center=c1, direction=d, radius=r, height=h))
|
|
3928
|
-
bond_meshes.append(pv.Cylinder(center=c2, direction=d, radius=r, height=h))
|
|
4376
|
+
bond_meshes.append(pv.Cylinder(center=c1, direction=d, radius=r, height=h, resolution=16))
|
|
4377
|
+
bond_meshes.append(pv.Cylinder(center=c2, direction=d, radius=r, height=h, resolution=16))
|
|
3929
4378
|
elif bt == Chem.rdchem.BondType.TRIPLE:
|
|
3930
|
-
|
|
3931
|
-
bond_meshes.append(pv.Cylinder(center=c, direction=d, radius=r, height=h))
|
|
3932
|
-
bond_meshes.append(pv.Cylinder(center=c
|
|
3933
|
-
bond_meshes.append(pv.Cylinder(center=c - off_dir * s, direction=d, radius=r, height=h))
|
|
4379
|
+
bond_meshes.append(pv.Cylinder(center=c, direction=d, radius=r, height=h, resolution=16))
|
|
4380
|
+
bond_meshes.append(pv.Cylinder(center=c + off_dir * s, direction=d, radius=r, height=h, resolution=16))
|
|
4381
|
+
bond_meshes.append(pv.Cylinder(center=c - off_dir * s, direction=d, radius=r, height=h, resolution=16))
|
|
3934
4382
|
|
|
3935
4383
|
if bond_meshes:
|
|
3936
4384
|
combined_bonds = pv.merge(bond_meshes)
|
|
3937
|
-
self.plotter.add_mesh(combined_bonds, color=
|
|
4385
|
+
self.plotter.add_mesh(combined_bonds, color='grey', **mesh_props)
|
|
3938
4386
|
|
|
3939
4387
|
if getattr(self, 'show_chiral_labels', False):
|
|
3940
4388
|
try:
|
|
@@ -3949,7 +4397,7 @@ class MainWindow(QMainWindow):
|
|
|
3949
4397
|
except Exception: pass
|
|
3950
4398
|
self.plotter.add_point_labels(np.array(pts), labels, font_size=20, point_size=0, text_color='k', name='chiral_labels', always_visible=True, tolerance=0.01, show_points=False)
|
|
3951
4399
|
except Exception as e: self.statusBar().showMessage(f"3D chiral label drawing error: {e}")
|
|
3952
|
-
|
|
4400
|
+
|
|
3953
4401
|
self.plotter.camera = camera_state
|
|
3954
4402
|
|
|
3955
4403
|
|
|
@@ -4035,6 +4483,8 @@ class MainWindow(QMainWindow):
|
|
|
4035
4483
|
self.statusBar().showMessage("Please generate a 3D structure first to show analysis.")
|
|
4036
4484
|
|
|
4037
4485
|
def closeEvent(self, event):
|
|
4486
|
+
if self.settings != self.initial_settings:
|
|
4487
|
+
self.save_settings()
|
|
4038
4488
|
reply = QMessageBox.question(self, 'Confirm Exit',
|
|
4039
4489
|
"Are you sure you want to exit?",
|
|
4040
4490
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
@@ -4103,36 +4553,170 @@ class MainWindow(QMainWindow):
|
|
|
4103
4553
|
style = CustomInteractorStyle(self)
|
|
4104
4554
|
|
|
4105
4555
|
# 調査の結果、'style' プロパティへの代入が正しい設定方法と判明
|
|
4106
|
-
self.plotter.
|
|
4556
|
+
self.plotter.interactor.SetInteractorStyle(style)
|
|
4557
|
+
self.plotter.interactor.Initialize()
|
|
4107
4558
|
|
|
4108
|
-
# 新しいスタイルを確実に読み込ませるためにインタラクタを初期化
|
|
4109
|
-
self.plotter.iren.initialize()
|
|
4110
|
-
|
|
4111
4559
|
def dragEnterEvent(self, event):
|
|
4112
|
-
"""
|
|
4113
|
-
# ドラッグされたデータにファイルパスが含まれているかチェック
|
|
4560
|
+
"""ウィンドウ全体で .pmeraw ファイルのドラッグのみを受け入れる"""
|
|
4114
4561
|
if event.mimeData().hasUrls():
|
|
4115
|
-
event.
|
|
4116
|
-
|
|
4117
|
-
|
|
4562
|
+
urls = event.mimeData().urls()
|
|
4563
|
+
if urls and urls[0].isLocalFile():
|
|
4564
|
+
file_path = urls[0].toLocalFile()
|
|
4565
|
+
if file_path.lower().endswith(('.pmeraw', '.mol', '.sdf')):
|
|
4566
|
+
event.acceptProposedAction()
|
|
4567
|
+
return
|
|
4568
|
+
event.ignore()
|
|
4118
4569
|
|
|
4119
4570
|
def dropEvent(self, event):
|
|
4120
4571
|
"""ファイルがウィンドウ上でドロップされたときに呼び出される"""
|
|
4121
4572
|
urls = event.mimeData().urls()
|
|
4122
|
-
# ドロップされたのがローカルファイルの場合
|
|
4123
4573
|
if urls and urls[0].isLocalFile():
|
|
4124
4574
|
file_path = urls[0].toLocalFile()
|
|
4125
4575
|
|
|
4126
|
-
#
|
|
4576
|
+
# 拡張子に応じて適切な読み込みメソッドを呼び出す
|
|
4127
4577
|
if file_path.lower().endswith('.pmeraw'):
|
|
4128
|
-
# 既存のファイル読み込み関数を呼び出す
|
|
4129
4578
|
self.load_raw_data(file_path=file_path)
|
|
4130
4579
|
event.acceptProposedAction()
|
|
4580
|
+
elif file_path.lower().endswith(('.mol', '.sdf')):
|
|
4581
|
+
# .mol/.sdfファイルを開くメソッドを呼び出す(メソッドが存在する場合)
|
|
4582
|
+
if hasattr(self, 'load_mol_file'):
|
|
4583
|
+
self.load_mol_file(file_path=file_path)
|
|
4584
|
+
event.acceptProposedAction()
|
|
4585
|
+
else:
|
|
4586
|
+
self.statusBar().showMessage(f"'{file_path.lower().split('.')[-1]}' file import is not implemented.")
|
|
4587
|
+
event.ignore()
|
|
4131
4588
|
else:
|
|
4132
4589
|
event.ignore()
|
|
4133
4590
|
else:
|
|
4134
4591
|
event.ignore()
|
|
4135
4592
|
|
|
4593
|
+
def _enter_3d_viewer_ui_mode(self):
|
|
4594
|
+
"""3DビューアモードのUI状態に設定する"""
|
|
4595
|
+
self.is_2d_editable = False
|
|
4596
|
+
self.cleanup_button.setEnabled(False)
|
|
4597
|
+
self.convert_button.setEnabled(False)
|
|
4598
|
+
for action in self.tool_group.actions():
|
|
4599
|
+
action.setEnabled(False)
|
|
4600
|
+
if hasattr(self, 'other_atom_action'):
|
|
4601
|
+
self.other_atom_action.setEnabled(False)
|
|
4602
|
+
|
|
4603
|
+
self.minimize_2d_panel()
|
|
4604
|
+
|
|
4605
|
+
self.optimize_3d_button.setEnabled(True)
|
|
4606
|
+
self.export_button.setEnabled(True)
|
|
4607
|
+
self.edit_3d_action.setEnabled(True)
|
|
4608
|
+
self.analysis_action.setEnabled(True)
|
|
4609
|
+
|
|
4610
|
+
def restore_ui_for_editing(self):
|
|
4611
|
+
"""Enables all 2D editing UI elements."""
|
|
4612
|
+
self.is_2d_editable = True
|
|
4613
|
+
self.restore_2d_panel()
|
|
4614
|
+
self.cleanup_button.setEnabled(True)
|
|
4615
|
+
self.convert_button.setEnabled(True)
|
|
4616
|
+
|
|
4617
|
+
for action in self.tool_group.actions():
|
|
4618
|
+
action.setEnabled(True)
|
|
4619
|
+
|
|
4620
|
+
if hasattr(self, 'other_atom_action'):
|
|
4621
|
+
self.other_atom_action.setEnabled(True)
|
|
4622
|
+
|
|
4623
|
+
def minimize_2d_panel(self):
|
|
4624
|
+
"""2Dパネルを最小化(非表示に)する"""
|
|
4625
|
+
sizes = self.splitter.sizes()
|
|
4626
|
+
# すでに最小化されていなければ実行
|
|
4627
|
+
if sizes[0] > 0:
|
|
4628
|
+
total_width = sum(sizes)
|
|
4629
|
+
self.splitter.setSizes([0, total_width])
|
|
4630
|
+
|
|
4631
|
+
def restore_2d_panel(self):
|
|
4632
|
+
"""最小化された2Dパネルを元のサイズに戻す"""
|
|
4633
|
+
sizes = self.splitter.sizes()
|
|
4634
|
+
|
|
4635
|
+
# sizesリストが空でないことを確認してからアクセスする
|
|
4636
|
+
if sizes and sizes[0] == 0:
|
|
4637
|
+
self.splitter.setSizes([600, 600])
|
|
4638
|
+
|
|
4639
|
+
|
|
4640
|
+
def apply_initial_settings(self):
|
|
4641
|
+
"""UIの初期化が完了した後に、保存された設定を3Dビューに適用する"""
|
|
4642
|
+
if self.plotter and self.plotter.renderer:
|
|
4643
|
+
bg_color = self.settings.get('background_color', '#919191')
|
|
4644
|
+
self.plotter.set_background(bg_color)
|
|
4645
|
+
self.apply_3d_settings()
|
|
4646
|
+
|
|
4647
|
+
|
|
4648
|
+
def apply_3d_settings(self):
|
|
4649
|
+
"""3Dビューの視覚設定を適用する"""
|
|
4650
|
+
if not hasattr(self, 'plotter'):
|
|
4651
|
+
return
|
|
4652
|
+
|
|
4653
|
+
# --- 3D軸ウィジェットの設定 ---
|
|
4654
|
+
show_axes = self.settings.get('show_3d_axes', True)
|
|
4655
|
+
|
|
4656
|
+
# ウィジェットがまだ作成されていない場合は作成する
|
|
4657
|
+
if self.axes_widget is None and hasattr(self.plotter, 'interactor'):
|
|
4658
|
+
axes = vtk.vtkAxesActor()
|
|
4659
|
+
self.axes_widget = vtk.vtkOrientationMarkerWidget()
|
|
4660
|
+
self.axes_widget.SetOrientationMarker(axes)
|
|
4661
|
+
self.axes_widget.SetInteractor(self.plotter.interactor)
|
|
4662
|
+
# 左下隅に設定 (幅・高さ20%)
|
|
4663
|
+
self.axes_widget.SetViewport(0.0, 0.0, 0.2, 0.2)
|
|
4664
|
+
|
|
4665
|
+
# 設定に応じてウィジェットを有効化/無効化
|
|
4666
|
+
if self.axes_widget:
|
|
4667
|
+
if show_axes:
|
|
4668
|
+
self.axes_widget.On()
|
|
4669
|
+
self.axes_widget.SetInteractive(False)
|
|
4670
|
+
else:
|
|
4671
|
+
self.axes_widget.Off()
|
|
4672
|
+
|
|
4673
|
+
self.draw_molecule_3d(self.current_mol)
|
|
4674
|
+
self.plotter.reset_camera()
|
|
4675
|
+
|
|
4676
|
+
|
|
4677
|
+
|
|
4678
|
+
def open_settings_dialog(self):
|
|
4679
|
+
dialog = SettingsDialog(self.settings, self)
|
|
4680
|
+
if dialog.exec():
|
|
4681
|
+
self.settings = dialog.get_settings()
|
|
4682
|
+
self.save_settings()
|
|
4683
|
+
self.apply_3d_settings()
|
|
4684
|
+
|
|
4685
|
+
def load_settings(self):
|
|
4686
|
+
default_settings = {
|
|
4687
|
+
'background_color': '#919191',
|
|
4688
|
+
'lighting_enabled': True,
|
|
4689
|
+
'specular': 0.2,
|
|
4690
|
+
'specular_power': 20,
|
|
4691
|
+
'light_intensity': 1.0,
|
|
4692
|
+
'show_3d_axes': True,
|
|
4693
|
+
}
|
|
4694
|
+
|
|
4695
|
+
try:
|
|
4696
|
+
if os.path.exists(self.settings_file):
|
|
4697
|
+
with open(self.settings_file, 'r') as f:
|
|
4698
|
+
loaded_settings = json.load(f)
|
|
4699
|
+
|
|
4700
|
+
for key, value in default_settings.items():
|
|
4701
|
+
loaded_settings.setdefault(key, value)
|
|
4702
|
+
self.settings = loaded_settings
|
|
4703
|
+
|
|
4704
|
+
else:
|
|
4705
|
+
self.settings = default_settings
|
|
4706
|
+
|
|
4707
|
+
except Exception:
|
|
4708
|
+
self.settings = default_settings
|
|
4709
|
+
|
|
4710
|
+
def save_settings(self):
|
|
4711
|
+
try:
|
|
4712
|
+
if not os.path.exists(self.settings_dir):
|
|
4713
|
+
os.makedirs(self.settings_dir)
|
|
4714
|
+
with open(self.settings_file, 'w') as f:
|
|
4715
|
+
json.dump(self.settings, f, indent=4)
|
|
4716
|
+
except Exception as e:
|
|
4717
|
+
print(f"Error saving settings: {e}")
|
|
4718
|
+
|
|
4719
|
+
|
|
4136
4720
|
|
|
4137
4721
|
# --- Application Execution ---
|
|
4138
4722
|
if __name__ == '__main__':
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: moleditpy
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
|
|
5
5
|
Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/HiroYokoyama/python_molecular_editor
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|