moleditpy 1.3.2__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.2 → moleditpy-1.4.0}/PKG-INFO +1 -1
- {moleditpy-1.3.2 → 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.2 → moleditpy-1.4.0}/src/moleditpy/main.py +645 -110
- {moleditpy-1.3.2 → moleditpy-1.4.0}/src/moleditpy.egg-info/PKG-INFO +1 -1
- moleditpy-1.3.2/src/moleditpy/assets/icon.ico +0 -0
- moleditpy-1.3.2/src/moleditpy/assets/icon.png +0 -0
- {moleditpy-1.3.2 → moleditpy-1.4.0}/README.md +0 -0
- {moleditpy-1.3.2 → moleditpy-1.4.0}/setup.cfg +0 -0
- {moleditpy-1.3.2 → moleditpy-1.4.0}/src/moleditpy/__init__.py +0 -0
- {moleditpy-1.3.2 → moleditpy-1.4.0}/src/moleditpy.egg-info/SOURCES.txt +0 -0
- {moleditpy-1.3.2 → moleditpy-1.4.0}/src/moleditpy.egg-info/dependency_links.txt +0 -0
- {moleditpy-1.3.2 → moleditpy-1.4.0}/src/moleditpy.egg-info/entry_points.txt +0 -0
- {moleditpy-1.3.2 → moleditpy-1.4.0}/src/moleditpy.egg-info/requires.txt +0 -0
- {moleditpy-1.3.2 → 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)):
|
|
@@ -1777,8 +1793,6 @@ class MoleculeScene(QGraphicsScene):
|
|
|
1777
1793
|
# --- どの操作にも当てはまらない場合 ---
|
|
1778
1794
|
super().keyPressEvent(event)
|
|
1779
1795
|
|
|
1780
|
-
|
|
1781
|
-
# In MoleculeScene class
|
|
1782
1796
|
def find_atom_near(self, pos, tol=14.0):
|
|
1783
1797
|
# Create a small search rectangle around the position
|
|
1784
1798
|
search_rect = QRectF(pos.x() - tol, pos.y() - tol, 2 * tol, 2 * tol)
|
|
@@ -1809,7 +1823,7 @@ class ZoomableView(QGraphicsView):
|
|
|
1809
1823
|
self.setDragMode(QGraphicsView.DragMode.NoDrag)
|
|
1810
1824
|
|
|
1811
1825
|
self.main_window = parent
|
|
1812
|
-
self.setAcceptDrops(
|
|
1826
|
+
self.setAcceptDrops(False)
|
|
1813
1827
|
|
|
1814
1828
|
self._is_panning = False
|
|
1815
1829
|
self._pan_start_pos = QPointF()
|
|
@@ -1887,44 +1901,7 @@ class ZoomableView(QGraphicsView):
|
|
|
1887
1901
|
else:
|
|
1888
1902
|
super().mouseReleaseEvent(event)
|
|
1889
1903
|
|
|
1890
|
-
# ZoomableView クラス内 (約1691行目あたりから)
|
|
1891
|
-
|
|
1892
|
-
def dragEnterEvent(self, event):
|
|
1893
|
-
"""ファイルがこのウィジェット上にドラッグされたときに呼び出される"""
|
|
1894
|
-
# ドラッグされたデータにファイルパスが含まれている場合のみ受け入れる
|
|
1895
|
-
if event.mimeData().hasUrls():
|
|
1896
|
-
event.acceptProposedAction()
|
|
1897
|
-
else:
|
|
1898
|
-
event.ignore()
|
|
1899
|
-
|
|
1900
|
-
def dragMoveEvent(self, event):
|
|
1901
|
-
"""
|
|
1902
|
-
ウィジェット上でドラッグが移動したときに呼び出される。
|
|
1903
|
-
"""
|
|
1904
|
-
if event.mimeData().hasUrls():
|
|
1905
|
-
event.acceptProposedAction()
|
|
1906
|
-
else:
|
|
1907
|
-
super().dragMoveEvent(event)
|
|
1908
1904
|
|
|
1909
|
-
def dropEvent(self, event):
|
|
1910
|
-
"""ファイルがこのウィジェット上でドロップされたときに呼び出される"""
|
|
1911
|
-
urls = event.mimeData().urls()
|
|
1912
|
-
if urls and urls[0].isLocalFile():
|
|
1913
|
-
file_path = urls[0].toLocalFile()
|
|
1914
|
-
|
|
1915
|
-
# MainWindowのファイル読み込みメソッドを呼び出す
|
|
1916
|
-
if self.main_window:
|
|
1917
|
-
# 拡張子によって適切な読み込み処理を呼び出す
|
|
1918
|
-
if file_path.lower().endswith('.pmeraw'):
|
|
1919
|
-
self.main_window.load_raw_data(file_path=file_path)
|
|
1920
|
-
elif file_path.lower().endswith(('.mol', '.sdf')):
|
|
1921
|
-
self.main_window.load_mol_file(file_path=file_path)
|
|
1922
|
-
|
|
1923
|
-
event.acceptProposedAction()
|
|
1924
|
-
else:
|
|
1925
|
-
event.ignore()
|
|
1926
|
-
else:
|
|
1927
|
-
event.ignore()
|
|
1928
1905
|
|
|
1929
1906
|
class CalculationWorker(QObject):
|
|
1930
1907
|
status_update = pyqtSignal(str)
|
|
@@ -2169,6 +2146,143 @@ class AnalysisWindow(QDialog):
|
|
|
2169
2146
|
if self.parent() and hasattr(self.parent(), 'statusBar'):
|
|
2170
2147
|
self.parent().statusBar().showMessage(f"Copied '{text}' to clipboard.", 2000)
|
|
2171
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
|
+
|
|
2172
2286
|
# --- 3Dインタラクションを管理する専用クラス ---
|
|
2173
2287
|
class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
2174
2288
|
def __init__(self, main_window):
|
|
@@ -2192,7 +2306,8 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
2192
2306
|
is_temp_mode = bool(QApplication.keyboardModifiers() & Qt.KeyboardModifier.AltModifier)
|
|
2193
2307
|
is_edit_active = mw.is_3d_edit_mode or is_temp_mode
|
|
2194
2308
|
|
|
2195
|
-
|
|
2309
|
+
# 3D分子(mw.current_mol)が存在する場合のみ、原子の選択処理を実行
|
|
2310
|
+
if is_edit_active and mw.current_mol:
|
|
2196
2311
|
click_pos = self.GetInteractor().GetEventPosition()
|
|
2197
2312
|
picker = mw.plotter.picker
|
|
2198
2313
|
picker.Pick(click_pos[0], click_pos[1], 0, mw.plotter.renderer)
|
|
@@ -2202,17 +2317,20 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
2202
2317
|
distances = np.linalg.norm(mw.atom_positions_3d - picked_position, axis=1)
|
|
2203
2318
|
closest_atom_idx = np.argmin(distances)
|
|
2204
2319
|
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
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 # 親クラスのカメラ回転を呼ばない
|
|
2216
2334
|
|
|
2217
2335
|
self._is_dragging_atom = False
|
|
2218
2336
|
super().OnLeftButtonDown()
|
|
@@ -2286,12 +2404,14 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
2286
2404
|
|
|
2287
2405
|
# 状態をリセット
|
|
2288
2406
|
self._is_dragging_atom = False
|
|
2407
|
+
self.is_dragging = False # is_draggingもリセット
|
|
2289
2408
|
|
|
2290
|
-
#
|
|
2291
|
-
# on_mouse_moveを一度呼び出します
|
|
2409
|
+
# ボタンを離した後のカーソル表示を最新の状態に更新
|
|
2292
2410
|
self.on_mouse_move(obj, event)
|
|
2293
2411
|
|
|
2294
|
-
|
|
2412
|
+
# 2Dビューにフォーカスを戻し、ショートカットキーなどが使えるようにする
|
|
2413
|
+
if mw and mw.view_2d:
|
|
2414
|
+
mw.view_2d.setFocus()
|
|
2295
2415
|
|
|
2296
2416
|
class MainWindow(QMainWindow):
|
|
2297
2417
|
|
|
@@ -2299,6 +2419,11 @@ class MainWindow(QMainWindow):
|
|
|
2299
2419
|
def __init__(self, initial_file=None):
|
|
2300
2420
|
super().__init__()
|
|
2301
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()
|
|
2302
2427
|
self.setWindowTitle("MoleditPy -- Python Molecular Editor Ver. " + VERSION); self.setGeometry(100, 100, 1400, 800)
|
|
2303
2428
|
self.data = MolecularData(); self.current_mol = None
|
|
2304
2429
|
self.current_3d_style = 'ball_and_stick'
|
|
@@ -2306,6 +2431,9 @@ class MainWindow(QMainWindow):
|
|
|
2306
2431
|
self.is_3d_edit_mode = False
|
|
2307
2432
|
self.dragged_atom_info = None
|
|
2308
2433
|
self.atom_actor = None
|
|
2434
|
+
self.is_2d_editable = True
|
|
2435
|
+
self.axes_actor = None
|
|
2436
|
+
self.axes_widget = None
|
|
2309
2437
|
self.undo_stack = []
|
|
2310
2438
|
self.redo_stack = []
|
|
2311
2439
|
self.mode_actions = {}
|
|
@@ -2333,6 +2461,8 @@ class MainWindow(QMainWindow):
|
|
|
2333
2461
|
|
|
2334
2462
|
if initial_file:
|
|
2335
2463
|
self.load_raw_data(file_path=initial_file)
|
|
2464
|
+
|
|
2465
|
+
QTimer.singleShot(0, self.apply_initial_settings)
|
|
2336
2466
|
|
|
2337
2467
|
def init_ui(self):
|
|
2338
2468
|
# 1. 現在のスクリプトがあるディレクトリのパスを取得
|
|
@@ -2352,8 +2482,8 @@ class MainWindow(QMainWindow):
|
|
|
2352
2482
|
|
|
2353
2483
|
self.init_menu_bar()
|
|
2354
2484
|
|
|
2355
|
-
splitter=QSplitter(Qt.Orientation.Horizontal)
|
|
2356
|
-
self.setCentralWidget(splitter)
|
|
2485
|
+
self.splitter = QSplitter(Qt.Orientation.Horizontal)
|
|
2486
|
+
self.setCentralWidget(self.splitter)
|
|
2357
2487
|
|
|
2358
2488
|
left_pane=QWidget()
|
|
2359
2489
|
left_pane.setAcceptDrops(True)
|
|
@@ -2383,13 +2513,13 @@ class MainWindow(QMainWindow):
|
|
|
2383
2513
|
left_buttons_layout.addWidget(self.convert_button)
|
|
2384
2514
|
|
|
2385
2515
|
left_layout.addLayout(left_buttons_layout)
|
|
2386
|
-
splitter.addWidget(left_pane)
|
|
2516
|
+
self.splitter.addWidget(left_pane)
|
|
2387
2517
|
|
|
2388
2518
|
# --- 右パネルとボタンレイアウト ---
|
|
2389
2519
|
right_pane = QWidget()
|
|
2390
2520
|
# 1. 右パネル全体は「垂直」レイアウトにする
|
|
2391
2521
|
right_layout = QVBoxLayout(right_pane)
|
|
2392
|
-
self.plotter =
|
|
2522
|
+
self.plotter = CustomQtInteractor(right_pane, main_window=self, lighting='none')
|
|
2393
2523
|
self.plotter.setAcceptDrops(False)
|
|
2394
2524
|
self.plotter.setSizePolicy(
|
|
2395
2525
|
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
|
|
@@ -2429,9 +2559,9 @@ class MainWindow(QMainWindow):
|
|
|
2429
2559
|
|
|
2430
2560
|
# 4. 水平のボタンレイアウトを、全体の垂直レイアウトに追加
|
|
2431
2561
|
right_layout.addLayout(right_buttons_layout)
|
|
2432
|
-
splitter.addWidget(right_pane)
|
|
2562
|
+
self.splitter.addWidget(right_pane)
|
|
2433
2563
|
|
|
2434
|
-
splitter.setSizes([600, 600])
|
|
2564
|
+
self.splitter.setSizes([600, 600])
|
|
2435
2565
|
|
|
2436
2566
|
# ステータスバーを左右に分離するための設定
|
|
2437
2567
|
self.status_bar = self.statusBar()
|
|
@@ -2463,6 +2593,7 @@ class MainWindow(QMainWindow):
|
|
|
2463
2593
|
|
|
2464
2594
|
if mode == 'atom_other':
|
|
2465
2595
|
action.triggered.connect(self.open_periodic_table_dialog)
|
|
2596
|
+
self.other_atom_action = action
|
|
2466
2597
|
else:
|
|
2467
2598
|
action.triggered.connect(lambda c, m=mode: self.set_mode(m))
|
|
2468
2599
|
self.mode_actions[mode] = action
|
|
@@ -2685,6 +2816,12 @@ class MainWindow(QMainWindow):
|
|
|
2685
2816
|
import_inchi_action = QAction("Import InChI...", self)
|
|
2686
2817
|
import_inchi_action.triggered.connect(self.import_inchi_dialog)
|
|
2687
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
|
+
|
|
2688
2825
|
file_menu.addSeparator()
|
|
2689
2826
|
save_mol_action = QAction("Save 2D as MOL...", self); save_mol_action.triggered.connect(self.save_as_mol)
|
|
2690
2827
|
file_menu.addAction(save_mol_action)
|
|
@@ -2702,6 +2839,16 @@ class MainWindow(QMainWindow):
|
|
|
2702
2839
|
load_raw_action.setShortcut(QKeySequence.StandardKey.Open)
|
|
2703
2840
|
file_menu.addAction(load_raw_action)
|
|
2704
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
|
+
|
|
2705
2852
|
file_menu.addSeparator()
|
|
2706
2853
|
quit_action = QAction("Quit", self)
|
|
2707
2854
|
quit_action.triggered.connect(self.close)
|
|
@@ -2781,6 +2928,13 @@ class MainWindow(QMainWindow):
|
|
|
2781
2928
|
|
|
2782
2929
|
view_menu.addSeparator()
|
|
2783
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
|
+
|
|
2784
2938
|
self.toggle_chiral_action = QAction("Show Chiral Labels", self, checkable=True)
|
|
2785
2939
|
self.toggle_chiral_action.setChecked(self.show_chiral_labels)
|
|
2786
2940
|
self.toggle_chiral_action.triggered.connect(self.toggle_chiral_labels_display)
|
|
@@ -2792,6 +2946,11 @@ class MainWindow(QMainWindow):
|
|
|
2792
2946
|
self.analysis_action.setEnabled(False)
|
|
2793
2947
|
analysis_menu.addAction(self.analysis_action)
|
|
2794
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
|
+
|
|
2795
2954
|
help_menu = menu_bar.addMenu("&Help")
|
|
2796
2955
|
about_action = QAction("About", self)
|
|
2797
2956
|
about_action.triggered.connect(lambda: QMessageBox.about(
|
|
@@ -2820,7 +2979,6 @@ class MainWindow(QMainWindow):
|
|
|
2820
2979
|
|
|
2821
2980
|
def set_mode(self, mode_str):
|
|
2822
2981
|
self.scene.mode = mode_str
|
|
2823
|
-
|
|
2824
2982
|
self.view_2d.setMouseTracking(True)
|
|
2825
2983
|
if not mode_str.startswith('template'):
|
|
2826
2984
|
self.scene.template_preview.hide()
|
|
@@ -3066,14 +3224,23 @@ class MainWindow(QMainWindow):
|
|
|
3066
3224
|
self.edit_3d_action.setEnabled(False)
|
|
3067
3225
|
self.statusBar().showMessage("Calculating 3D structure...")
|
|
3068
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
|
+
|
|
3069
3236
|
text_actor = self.plotter.add_text(
|
|
3070
3237
|
"Calculating...",
|
|
3071
|
-
position='
|
|
3238
|
+
position='lower_right',
|
|
3072
3239
|
font_size=15,
|
|
3073
|
-
color=
|
|
3240
|
+
color=text_color,
|
|
3074
3241
|
name='calculating_text'
|
|
3075
3242
|
)
|
|
3076
|
-
text_actor.GetTextProperty().SetOpacity(
|
|
3243
|
+
text_actor.GetTextProperty().SetOpacity(1)
|
|
3077
3244
|
self.plotter.render()
|
|
3078
3245
|
self.start_calculation.emit(mol_block)
|
|
3079
3246
|
|
|
@@ -3158,6 +3325,8 @@ class MainWindow(QMainWindow):
|
|
|
3158
3325
|
state['version'] = VERSION
|
|
3159
3326
|
|
|
3160
3327
|
if self.current_mol: state['mol_3d'] = self.current_mol.ToBinary()
|
|
3328
|
+
|
|
3329
|
+
state['is_3d_viewer_mode'] = not self.is_2d_editable
|
|
3161
3330
|
|
|
3162
3331
|
return state
|
|
3163
3332
|
|
|
@@ -3234,6 +3403,12 @@ class MainWindow(QMainWindow):
|
|
|
3234
3403
|
|
|
3235
3404
|
self.update_implicit_hydrogens()
|
|
3236
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()
|
|
3237
3412
|
|
|
3238
3413
|
|
|
3239
3414
|
def push_undo_state(self):
|
|
@@ -3318,10 +3493,13 @@ class MainWindow(QMainWindow):
|
|
|
3318
3493
|
item.setSelected(True)
|
|
3319
3494
|
|
|
3320
3495
|
def clear_all(self):
|
|
3496
|
+
|
|
3497
|
+
self.restore_ui_for_editing()
|
|
3498
|
+
|
|
3321
3499
|
# データが存在しない場合は何もしない
|
|
3322
3500
|
if not self.data.atoms and self.current_mol is None:
|
|
3323
3501
|
return
|
|
3324
|
-
|
|
3502
|
+
|
|
3325
3503
|
self.dragged_atom_info = None
|
|
3326
3504
|
|
|
3327
3505
|
# 2Dエディタをクリアする(Undoスタックにはプッシュしない)
|
|
@@ -3430,7 +3608,8 @@ class MainWindow(QMainWindow):
|
|
|
3430
3608
|
AllChem.AssignStereochemistry(mol, cleanIt=True, force=True)
|
|
3431
3609
|
conf = mol.GetConformer()
|
|
3432
3610
|
AllChem.WedgeMolBonds(mol, conf)
|
|
3433
|
-
|
|
3611
|
+
|
|
3612
|
+
self.restore_ui_for_editing()
|
|
3434
3613
|
self.clear_2d_editor(push_to_undo=False)
|
|
3435
3614
|
self.current_mol = None
|
|
3436
3615
|
self.plotter.clear()
|
|
@@ -3499,7 +3678,8 @@ class MainWindow(QMainWindow):
|
|
|
3499
3678
|
AllChem.AssignStereochemistry(mol, cleanIt=True, force=True)
|
|
3500
3679
|
conf = mol.GetConformer()
|
|
3501
3680
|
AllChem.WedgeMolBonds(mol, conf)
|
|
3502
|
-
|
|
3681
|
+
|
|
3682
|
+
self.restore_ui_for_editing()
|
|
3503
3683
|
self.clear_2d_editor(push_to_undo=False)
|
|
3504
3684
|
self.current_mol = None
|
|
3505
3685
|
self.plotter.clear()
|
|
@@ -3565,7 +3745,8 @@ class MainWindow(QMainWindow):
|
|
|
3565
3745
|
if mol is None: raise ValueError("Failed to read molecule from file.")
|
|
3566
3746
|
|
|
3567
3747
|
Chem.Kekulize(mol)
|
|
3568
|
-
|
|
3748
|
+
|
|
3749
|
+
self.restore_ui_for_editing()
|
|
3569
3750
|
self.clear_2d_editor(push_to_undo=False)
|
|
3570
3751
|
self.current_mol = None; self.plotter.clear(); self.analysis_action.setEnabled(False)
|
|
3571
3752
|
|
|
@@ -3622,9 +3803,44 @@ class MainWindow(QMainWindow):
|
|
|
3622
3803
|
self.reset_undo_stack()
|
|
3623
3804
|
QTimer.singleShot(0, self.fit_to_view)
|
|
3624
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
|
+
|
|
3625
3839
|
|
|
3626
3840
|
def save_raw_data(self):
|
|
3627
|
-
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
|
|
3628
3844
|
save_data = self.get_current_state()
|
|
3629
3845
|
options = QFileDialog.Option.DontUseNativeDialog
|
|
3630
3846
|
file_path, _ = QFileDialog.getSaveFileName(self, "Save Project File", "", "Project Files (*.pmeraw);;All Files (*)", options=options)
|
|
@@ -3635,6 +3851,7 @@ class MainWindow(QMainWindow):
|
|
|
3635
3851
|
self.statusBar().showMessage(f"Project saved to {file_path}")
|
|
3636
3852
|
except Exception as e: self.statusBar().showMessage(f"Error saving project file: {e}")
|
|
3637
3853
|
|
|
3854
|
+
|
|
3638
3855
|
def load_raw_data(self, file_path=None):
|
|
3639
3856
|
if not file_path:
|
|
3640
3857
|
options = QFileDialog.Option.DontUseNativeDialog
|
|
@@ -3643,6 +3860,7 @@ class MainWindow(QMainWindow):
|
|
|
3643
3860
|
|
|
3644
3861
|
try:
|
|
3645
3862
|
with open(file_path, 'rb') as f: loaded_data = pickle.load(f)
|
|
3863
|
+
self.restore_ui_for_editing()
|
|
3646
3864
|
self.set_state_from_data(loaded_data)
|
|
3647
3865
|
self.statusBar().showMessage(f"Project loaded from {file_path}")
|
|
3648
3866
|
self.reset_undo_stack()
|
|
@@ -3675,7 +3893,13 @@ class MainWindow(QMainWindow):
|
|
|
3675
3893
|
if not file_path.lower().endswith('.mol'):
|
|
3676
3894
|
file_path += '.mol'
|
|
3677
3895
|
try:
|
|
3678
|
-
|
|
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)
|
|
3679
3903
|
lines = mol_block.split('\n')
|
|
3680
3904
|
if len(lines) > 1 and 'RDKit' in lines[1]:
|
|
3681
3905
|
lines[1] = ' MoleditPy Ver. ' + VERSION + ' 3D'
|
|
@@ -3702,6 +3926,133 @@ class MainWindow(QMainWindow):
|
|
|
3702
3926
|
self.statusBar().showMessage(f"Successfully saved to {file_path}")
|
|
3703
3927
|
except Exception as e: self.statusBar().showMessage(f"Error saving file: {e}")
|
|
3704
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
|
+
|
|
3705
4056
|
def open_periodic_table_dialog(self):
|
|
3706
4057
|
dialog=PeriodicTableDialog(self); dialog.element_selected.connect(self.set_atom_from_periodic_table)
|
|
3707
4058
|
checked_action=self.tool_group.checkedAction()
|
|
@@ -3923,32 +4274,80 @@ class MainWindow(QMainWindow):
|
|
|
3923
4274
|
self.statusBar().showMessage("Resolved overlapping groups.", 2000)
|
|
3924
4275
|
|
|
3925
4276
|
|
|
4277
|
+
|
|
3926
4278
|
def draw_molecule_3d(self, mol):
|
|
4279
|
+
"""3D 分子を描画し、軸アクターの参照をクリアする(軸の再制御は apply_3d_settings に任せる)"""
|
|
4280
|
+
|
|
4281
|
+
# 1. カメラ状態とクリア
|
|
3927
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
|
+
|
|
3928
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. 分子描画ロジック
|
|
3929
4316
|
conf = mol.GetConformer()
|
|
3930
|
-
|
|
3931
|
-
# 3D原子座標をNumPy配列としてクラス変数に保持し、効率的な更新を可能にする
|
|
4317
|
+
|
|
3932
4318
|
self.atom_positions_3d = np.array([list(conf.GetAtomPosition(i)) for i in range(mol.GetNumAtoms())])
|
|
3933
|
-
|
|
4319
|
+
|
|
3934
4320
|
sym = [a.GetSymbol() for a in mol.GetAtoms()]
|
|
3935
4321
|
col = np.array([CPK_COLORS_PV.get(s, [0.5, 0.5, 0.5]) for s in sym])
|
|
3936
|
-
|
|
4322
|
+
|
|
3937
4323
|
if self.current_3d_style == 'cpk':
|
|
3938
4324
|
rad = np.array([pt.GetRvdw(pt.GetAtomicNumber(s)) * 1.0 for s in sym])
|
|
3939
|
-
else:
|
|
4325
|
+
else:
|
|
3940
4326
|
rad = np.array([VDW_RADII.get(s, 0.4) for s in sym])
|
|
3941
4327
|
|
|
3942
|
-
# 原子球(グリフ)の元となるPolyDataをクラス変数として保持
|
|
3943
4328
|
self.glyph_source = pv.PolyData(self.atom_positions_3d)
|
|
3944
4329
|
self.glyph_source['colors'] = col
|
|
3945
4330
|
self.glyph_source['radii'] = rad
|
|
3946
|
-
|
|
3947
|
-
glyphs = self.glyph_source.glyph(scale='radii', geom=pv.Sphere(radius=1.0), orient=False)
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
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
|
+
|
|
3952
4351
|
|
|
3953
4352
|
if self.current_3d_style == 'ball_and_stick':
|
|
3954
4353
|
bond_meshes = []
|
|
@@ -3961,8 +4360,9 @@ class MainWindow(QMainWindow):
|
|
|
3961
4360
|
h = np.linalg.norm(d)
|
|
3962
4361
|
if h == 0: continue
|
|
3963
4362
|
|
|
4363
|
+
cyl_radius = 0.1
|
|
3964
4364
|
if bt == Chem.rdchem.BondType.SINGLE or bt == Chem.rdchem.BondType.AROMATIC:
|
|
3965
|
-
cyl = pv.Cylinder(center=c, direction=d, radius=
|
|
4365
|
+
cyl = pv.Cylinder(center=c, direction=d, radius=cyl_radius, height=h, resolution=16)
|
|
3966
4366
|
bond_meshes.append(cyl)
|
|
3967
4367
|
else:
|
|
3968
4368
|
v1 = d / h
|
|
@@ -3970,20 +4370,19 @@ class MainWindow(QMainWindow):
|
|
|
3970
4370
|
if np.allclose(np.abs(np.dot(v1, v_arb)), 1.0): v_arb = np.array([0, 1, 0])
|
|
3971
4371
|
off_dir = np.cross(v1, v_arb)
|
|
3972
4372
|
off_dir /= np.linalg.norm(off_dir)
|
|
4373
|
+
r, s = cyl_radius * 0.8, cyl_radius * 2.0
|
|
3973
4374
|
if bt == Chem.rdchem.BondType.DOUBLE:
|
|
3974
|
-
r, s = 0.08, 0.20
|
|
3975
4375
|
c1, c2 = c + off_dir * (s / 2), c - off_dir * (s / 2)
|
|
3976
|
-
bond_meshes.append(pv.Cylinder(center=c1, direction=d, radius=r, height=h))
|
|
3977
|
-
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))
|
|
3978
4378
|
elif bt == Chem.rdchem.BondType.TRIPLE:
|
|
3979
|
-
|
|
3980
|
-
bond_meshes.append(pv.Cylinder(center=c, direction=d, radius=r, height=h))
|
|
3981
|
-
bond_meshes.append(pv.Cylinder(center=c
|
|
3982
|
-
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))
|
|
3983
4382
|
|
|
3984
4383
|
if bond_meshes:
|
|
3985
4384
|
combined_bonds = pv.merge(bond_meshes)
|
|
3986
|
-
self.plotter.add_mesh(combined_bonds, color=
|
|
4385
|
+
self.plotter.add_mesh(combined_bonds, color='grey', **mesh_props)
|
|
3987
4386
|
|
|
3988
4387
|
if getattr(self, 'show_chiral_labels', False):
|
|
3989
4388
|
try:
|
|
@@ -3998,7 +4397,7 @@ class MainWindow(QMainWindow):
|
|
|
3998
4397
|
except Exception: pass
|
|
3999
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)
|
|
4000
4399
|
except Exception as e: self.statusBar().showMessage(f"3D chiral label drawing error: {e}")
|
|
4001
|
-
|
|
4400
|
+
|
|
4002
4401
|
self.plotter.camera = camera_state
|
|
4003
4402
|
|
|
4004
4403
|
|
|
@@ -4084,6 +4483,8 @@ class MainWindow(QMainWindow):
|
|
|
4084
4483
|
self.statusBar().showMessage("Please generate a 3D structure first to show analysis.")
|
|
4085
4484
|
|
|
4086
4485
|
def closeEvent(self, event):
|
|
4486
|
+
if self.settings != self.initial_settings:
|
|
4487
|
+
self.save_settings()
|
|
4087
4488
|
reply = QMessageBox.question(self, 'Confirm Exit',
|
|
4088
4489
|
"Are you sure you want to exit?",
|
|
4089
4490
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
@@ -4152,36 +4553,170 @@ class MainWindow(QMainWindow):
|
|
|
4152
4553
|
style = CustomInteractorStyle(self)
|
|
4153
4554
|
|
|
4154
4555
|
# 調査の結果、'style' プロパティへの代入が正しい設定方法と判明
|
|
4155
|
-
self.plotter.
|
|
4556
|
+
self.plotter.interactor.SetInteractorStyle(style)
|
|
4557
|
+
self.plotter.interactor.Initialize()
|
|
4156
4558
|
|
|
4157
|
-
# 新しいスタイルを確実に読み込ませるためにインタラクタを初期化
|
|
4158
|
-
self.plotter.iren.initialize()
|
|
4159
|
-
|
|
4160
4559
|
def dragEnterEvent(self, event):
|
|
4161
|
-
"""
|
|
4162
|
-
# ドラッグされたデータにファイルパスが含まれているかチェック
|
|
4560
|
+
"""ウィンドウ全体で .pmeraw ファイルのドラッグのみを受け入れる"""
|
|
4163
4561
|
if event.mimeData().hasUrls():
|
|
4164
|
-
event.
|
|
4165
|
-
|
|
4166
|
-
|
|
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()
|
|
4167
4569
|
|
|
4168
4570
|
def dropEvent(self, event):
|
|
4169
4571
|
"""ファイルがウィンドウ上でドロップされたときに呼び出される"""
|
|
4170
4572
|
urls = event.mimeData().urls()
|
|
4171
|
-
# ドロップされたのがローカルファイルの場合
|
|
4172
4573
|
if urls and urls[0].isLocalFile():
|
|
4173
4574
|
file_path = urls[0].toLocalFile()
|
|
4174
4575
|
|
|
4175
|
-
#
|
|
4576
|
+
# 拡張子に応じて適切な読み込みメソッドを呼び出す
|
|
4176
4577
|
if file_path.lower().endswith('.pmeraw'):
|
|
4177
|
-
# 既存のファイル読み込み関数を呼び出す
|
|
4178
4578
|
self.load_raw_data(file_path=file_path)
|
|
4179
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()
|
|
4180
4588
|
else:
|
|
4181
4589
|
event.ignore()
|
|
4182
4590
|
else:
|
|
4183
4591
|
event.ignore()
|
|
4184
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
|
+
|
|
4185
4720
|
|
|
4186
4721
|
# --- Application Execution ---
|
|
4187
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
|