MoleditPy-linux 4.11.3__tar.gz → 4.11.5__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_linux-4.11.3 → moleditpy_linux-4.11.5}/PKG-INFO +1 -1
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/pyproject.toml +1 -1
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/MoleditPy_linux.egg-info/PKG-INFO +1 -1
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/base_picking_dialog.py +32 -10
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/calculation_worker.py +2 -1
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/constrained_optimization_dialog.py +93 -14
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/edit_3d_logic.py +40 -1
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/geometry_base_dialog.py +2 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/io_logic.py +7 -3
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/main_window.py +4 -2
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/mirror_dialog.py +9 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/molecular_scene_handler.py +2 -2
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/molecule_scene.py +60 -1
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/ui_manager.py +2 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/LICENSE +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/README.md +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/setup.cfg +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/MoleditPy_linux.egg-info/SOURCES.txt +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/MoleditPy_linux.egg-info/dependency_links.txt +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/MoleditPy_linux.egg-info/entry_points.txt +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/MoleditPy_linux.egg-info/requires.txt +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/MoleditPy_linux.egg-info/top_level.txt +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/__init__.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/__main__.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/assets/file_icon.ico +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/assets/icon.icns +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/assets/icon.ico +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/assets/icon.png +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/core/__init__.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/core/mol_geometry.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/core/molecular_data.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/core/stereo_check.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/main.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/plugins/__init__.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/plugins/plugin_interface.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/plugins/plugin_manager.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/plugins/plugin_manager_window.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/__init__.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/about_dialog.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/align_plane_dialog.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/alignment_dialog.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/analysis_window.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/angle_dialog.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/app_state.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/atom_item.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/atom_picking.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/bond_item.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/bond_length_dialog.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/chain_mixin.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/chirality_warning_dialog.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/color_settings_dialog.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/compute_logic.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/custom_interactor_style.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/custom_qt_interactor.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/dialog_logic.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/dihedral_dialog.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/edit_actions_logic.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/export_logic.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/main_window_init.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/move_dialog_mixin.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/move_group_dialog.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/move_selected_atoms_dialog.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/periodic_table_dialog.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/planarize_dialog.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/plugin_menu_manager.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/preview_molecule.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/sdf_record_dialog.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/settings_dialog.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/settings_tabs/__init__.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/settings_tabs/settings_2d_cleanup_tab.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/settings_tabs/settings_2d_tab.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/settings_tabs/settings_labels_tab.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/settings_tabs/settings_other_tab.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/settings_tabs/settings_tab_base.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/string_importers.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/template_preview_item.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/template_preview_view.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/translation_dialog.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/user_template_dialog.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/view_3d_logic.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/zoomable_view.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/utils/__init__.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/utils/constants.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/utils/default_settings.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/utils/finite_float.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/utils/hit_radius.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/utils/label_style.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/utils/sdf_records.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/utils/sip_isdeleted_safe.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/utils/suppress_log.py +0 -0
- {moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/utils/system_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy-linux
|
|
3
|
-
Version: 4.11.
|
|
3
|
+
Version: 4.11.5
|
|
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
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy-linux
|
|
3
|
-
Version: 4.11.
|
|
3
|
+
Version: 4.11.5
|
|
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
|
License: GNU GENERAL PUBLIC LICENSE
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/base_picking_dialog.py
RENAMED
|
@@ -62,10 +62,21 @@ class BasePickingDialog(Dialog3DPickingMixin, QDialog):
|
|
|
62
62
|
Dialog3DPickingMixin.__init__(self)
|
|
63
63
|
self.mol = mol
|
|
64
64
|
self.main_window = main_window
|
|
65
|
+
self._invalidated = False
|
|
66
|
+
self._geometry_refresh_timer = QTimer(self)
|
|
67
|
+
self._geometry_refresh_timer.setSingleShot(True)
|
|
68
|
+
self._geometry_refresh_timer.timeout.connect(self._refresh_geometry_display)
|
|
65
69
|
self._molecule_modified = (
|
|
66
70
|
False # Track if any modifications were made during this session
|
|
67
71
|
)
|
|
68
72
|
|
|
73
|
+
def invalidate_molecule(self) -> None:
|
|
74
|
+
"""Close a dialog bound to a replaced molecule without recording an edit."""
|
|
75
|
+
self._invalidated = True
|
|
76
|
+
self._molecule_modified = False
|
|
77
|
+
self.setEnabled(False)
|
|
78
|
+
self.reject()
|
|
79
|
+
|
|
69
80
|
def keyPressEvent(self, event: Optional[QKeyEvent]) -> None:
|
|
70
81
|
"""Standard keyboard handler: Enter/Return triggers 'Apply'."""
|
|
71
82
|
if event is None:
|
|
@@ -80,18 +91,21 @@ class BasePickingDialog(Dialog3DPickingMixin, QDialog):
|
|
|
80
91
|
|
|
81
92
|
def closeEvent(self, event: Optional[QCloseEvent]) -> None:
|
|
82
93
|
"""Cleanup on window close."""
|
|
94
|
+
self._geometry_refresh_timer.stop()
|
|
83
95
|
self.clear_atom_labels()
|
|
84
96
|
self.disable_picking()
|
|
85
97
|
super().closeEvent(event)
|
|
86
98
|
|
|
87
99
|
def reject(self) -> None:
|
|
88
100
|
"""Cleanup on cancel."""
|
|
101
|
+
self._geometry_refresh_timer.stop()
|
|
89
102
|
self.clear_atom_labels()
|
|
90
103
|
self.disable_picking()
|
|
91
104
|
super().reject()
|
|
92
105
|
|
|
93
106
|
def accept(self) -> None:
|
|
94
107
|
"""Cleanup on OK."""
|
|
108
|
+
self._geometry_refresh_timer.stop()
|
|
95
109
|
self.clear_atom_labels()
|
|
96
110
|
self.disable_picking()
|
|
97
111
|
super().accept()
|
|
@@ -103,6 +117,8 @@ class BasePickingDialog(Dialog3DPickingMixin, QDialog):
|
|
|
103
117
|
Update the molecule's conformer and the 3D position cache, then redraw.
|
|
104
118
|
:param positions: A numpy array or dictionary of all atom positions.
|
|
105
119
|
"""
|
|
120
|
+
if self._invalidated:
|
|
121
|
+
return
|
|
106
122
|
conf = self.mol.GetConformer()
|
|
107
123
|
num_atoms = conf.GetNumAtoms()
|
|
108
124
|
|
|
@@ -138,24 +154,30 @@ class BasePickingDialog(Dialog3DPickingMixin, QDialog):
|
|
|
138
154
|
self._molecule_modified = True
|
|
139
155
|
|
|
140
156
|
# 4. Refresh display (deferred to ensure stability)
|
|
157
|
+
self._geometry_refresh_timer.start(200)
|
|
158
|
+
|
|
159
|
+
# 5. Refresh chiral/cis-trans labels if applicable
|
|
160
|
+
self.main_window.view_3d_manager.update_chiral_labels()
|
|
161
|
+
|
|
162
|
+
def _refresh_geometry_display(self) -> None:
|
|
163
|
+
"""Refresh labels and rendering while this dialog still owns its molecule."""
|
|
164
|
+
if self._invalidated:
|
|
165
|
+
return
|
|
141
166
|
is_dragging = getattr(self, "_slider_dragging", False)
|
|
142
167
|
|
|
143
168
|
if is_dragging and hasattr(self, "show_atom_labels"):
|
|
144
|
-
|
|
169
|
+
self.show_atom_labels()
|
|
145
170
|
elif hasattr(self, "update_display"):
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (
|
|
149
|
-
hasattr(self.main_window.view_3d_manager, "plotter")
|
|
150
|
-
and self.main_window.view_3d_manager.plotter
|
|
151
|
-
):
|
|
152
|
-
QTimer.singleShot(200, self.main_window.view_3d_manager.plotter.render)
|
|
171
|
+
self.update_display()
|
|
153
172
|
|
|
154
|
-
|
|
155
|
-
|
|
173
|
+
plotter = self.main_window.view_3d_manager.plotter
|
|
174
|
+
if plotter is not None:
|
|
175
|
+
plotter.render()
|
|
156
176
|
|
|
157
177
|
def _push_undo(self) -> None:
|
|
158
178
|
"""Centralized undo logic to push current state to the undo stack."""
|
|
179
|
+
if self._invalidated:
|
|
180
|
+
return
|
|
159
181
|
self.main_window.edit_actions_manager.push_undo_state()
|
|
160
182
|
self._molecule_modified = False
|
|
161
183
|
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/calculation_worker.py
RENAMED
|
@@ -1016,7 +1016,8 @@ class CalculationWorker(QObject):
|
|
|
1016
1016
|
t = 3.0 if s == Chem.BondStereo.STEREOZ else 5.0
|
|
1017
1017
|
bm[satoms[0]][satoms[1]] = bm[satoms[1]][satoms[0]] = t
|
|
1018
1018
|
DoTriangleSmoothing(bm)
|
|
1019
|
-
|
|
1019
|
+
params.SetBoundsMat(bm)
|
|
1020
|
+
conf_id = rdDistGeom.EmbedMolecule(mol, params)
|
|
1020
1021
|
|
|
1021
1022
|
if conf_id == -1:
|
|
1022
1023
|
return False
|
|
@@ -11,9 +11,10 @@ DOI: 10.5281/zenodo.17268532
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import logging
|
|
14
|
-
from typing import Any
|
|
14
|
+
from typing import Any, Optional
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
import numpy as np
|
|
17
|
+
from PyQt6.QtCore import Qt, QThread, pyqtSignal, pyqtSlot
|
|
17
18
|
from PyQt6.QtWidgets import (
|
|
18
19
|
QAbstractItemView,
|
|
19
20
|
QComboBox,
|
|
@@ -28,6 +29,7 @@ from PyQt6.QtWidgets import (
|
|
|
28
29
|
QTableWidgetItem,
|
|
29
30
|
QVBoxLayout,
|
|
30
31
|
)
|
|
32
|
+
from rdkit import Chem
|
|
31
33
|
from rdkit.Chem import rdForceFieldHelpers, rdMolTransforms
|
|
32
34
|
|
|
33
35
|
from .dialog_3d_picking_mixin import Dialog3DPickingMixin
|
|
@@ -40,18 +42,32 @@ class ConstrainedOptimizationThread(QThread):
|
|
|
40
42
|
optimization_finished = pyqtSignal()
|
|
41
43
|
error_occurred = pyqtSignal(str)
|
|
42
44
|
|
|
43
|
-
def __init__(
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
ff: Any,
|
|
48
|
+
max_iters: int = 20000,
|
|
49
|
+
parent: Any = None,
|
|
50
|
+
mol: Optional[Chem.Mol] = None,
|
|
51
|
+
) -> None:
|
|
44
52
|
"""Initialize constrained optimization worker thread."""
|
|
45
53
|
super().__init__(parent)
|
|
46
54
|
self.ff = ff
|
|
47
55
|
self.max_iters = max_iters
|
|
56
|
+
# Keep the private conformer backing the force field alive during minimization.
|
|
57
|
+
self.mol = mol
|
|
48
58
|
|
|
49
59
|
def run(self) -> None:
|
|
50
60
|
"""Execute force-field minimization and emit finished or error signal."""
|
|
51
61
|
try:
|
|
52
|
-
|
|
53
|
-
|
|
62
|
+
for offset in range(0, self.max_iters, 200):
|
|
63
|
+
if self.isInterruptionRequested():
|
|
64
|
+
return
|
|
65
|
+
if self.ff.Minimize(maxIts=min(200, self.max_iters - offset)) == 0:
|
|
66
|
+
break
|
|
67
|
+
if not self.isInterruptionRequested():
|
|
68
|
+
self.optimization_finished.emit()
|
|
54
69
|
except Exception as e:
|
|
70
|
+
logging.exception("Constrained minimization failed")
|
|
55
71
|
self.error_occurred.emit(str(e))
|
|
56
72
|
|
|
57
73
|
|
|
@@ -62,14 +78,17 @@ class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
|
|
|
62
78
|
"""Initialize constrained force field optimization dialog."""
|
|
63
79
|
QDialog.__init__(self, parent)
|
|
64
80
|
Dialog3DPickingMixin.__init__(self)
|
|
65
|
-
self.mol = mol
|
|
81
|
+
self.mol: Chem.Mol = mol
|
|
66
82
|
self.main_window = main_window
|
|
67
83
|
self.selected_atoms: list[int] = [] # Using a list because order matters
|
|
68
84
|
self.constraints: list[Any] = [] # (type, atoms_indices, value)
|
|
69
85
|
self.constraint_labels: list[Any] = [] # 3D label actors
|
|
70
|
-
self._opt_thread:
|
|
86
|
+
self._opt_thread: Optional[ConstrainedOptimizationThread] = None
|
|
71
87
|
# Closed dialogs must discard late optimization-thread results
|
|
72
88
|
self._closed = False
|
|
89
|
+
self._invalidated = False
|
|
90
|
+
self._optimization_baseline: Optional[np.ndarray] = None
|
|
91
|
+
self._optimization_method = ""
|
|
73
92
|
self.init_ui()
|
|
74
93
|
self.enable_picking()
|
|
75
94
|
|
|
@@ -500,12 +519,19 @@ class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
|
|
|
500
519
|
|
|
501
520
|
def apply_optimization(self) -> None:
|
|
502
521
|
"""Run force-field minimization with the configured constraints."""
|
|
522
|
+
if self._closed:
|
|
523
|
+
return
|
|
524
|
+
if self._opt_thread is not None:
|
|
525
|
+
if self._opt_thread.isRunning():
|
|
526
|
+
return
|
|
527
|
+
self._opt_thread = None
|
|
503
528
|
if not self.mol or self.mol.GetNumConformers() == 0:
|
|
504
529
|
QMessageBox.warning(self, "Error", "No valid 3D molecule found.")
|
|
505
530
|
return
|
|
506
531
|
|
|
507
532
|
ff_name = self.ff_combo.currentText()
|
|
508
|
-
|
|
533
|
+
optimization_mol = Chem.Mol(self.mol)
|
|
534
|
+
self._optimization_baseline = self.mol.GetConformer().GetPositions().copy()
|
|
509
535
|
|
|
510
536
|
try:
|
|
511
537
|
ignore_interfrag = not self.main_window.init_manager.settings.get(
|
|
@@ -513,10 +539,10 @@ class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
|
|
|
513
539
|
)
|
|
514
540
|
if ff_name.startswith("MMFF"):
|
|
515
541
|
props = rdForceFieldHelpers.MMFFGetMoleculeProperties(
|
|
516
|
-
|
|
542
|
+
optimization_mol, mmffVariant=ff_name
|
|
517
543
|
)
|
|
518
544
|
ff = rdForceFieldHelpers.MMFFGetMoleculeForceField(
|
|
519
|
-
|
|
545
|
+
optimization_mol,
|
|
520
546
|
props,
|
|
521
547
|
confId=0,
|
|
522
548
|
ignoreInterfragInteractions=ignore_interfrag,
|
|
@@ -526,7 +552,9 @@ class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
|
|
|
526
552
|
add_torsion_constraint = ff.MMFFAddTorsionConstraint
|
|
527
553
|
else: # UFF
|
|
528
554
|
ff = rdForceFieldHelpers.UFFGetMoleculeForceField(
|
|
529
|
-
|
|
555
|
+
optimization_mol,
|
|
556
|
+
confId=0,
|
|
557
|
+
ignoreInterfragInteractions=ignore_interfrag,
|
|
530
558
|
)
|
|
531
559
|
add_dist_constraint = ff.UFFAddDistanceConstraint
|
|
532
560
|
add_angle_constraint = ff.UFFAddAngleConstraint
|
|
@@ -592,17 +620,38 @@ class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
|
|
|
592
620
|
|
|
593
621
|
self.optimize_button.setEnabled(False)
|
|
594
622
|
|
|
595
|
-
self._opt_thread = ConstrainedOptimizationThread(
|
|
596
|
-
|
|
597
|
-
lambda: self._on_optimization_finished(ff_name, conf)
|
|
623
|
+
self._opt_thread = ConstrainedOptimizationThread(
|
|
624
|
+
ff, 20000, mol=optimization_mol
|
|
598
625
|
)
|
|
626
|
+
self._optimization_method = ff_name
|
|
627
|
+
self._opt_thread.optimization_finished.connect(self._on_worker_result)
|
|
599
628
|
self._opt_thread.error_occurred.connect(self._on_optimization_error)
|
|
629
|
+
self._opt_thread.finished.connect(self._on_worker_finished)
|
|
630
|
+
self.main_window.edit_3d_manager.register_optimization_thread(
|
|
631
|
+
self._opt_thread
|
|
632
|
+
)
|
|
600
633
|
self._opt_thread.start()
|
|
601
634
|
|
|
602
635
|
except Exception as e:
|
|
603
636
|
logging.exception("Failed to start optimization: %s", e)
|
|
604
637
|
self.optimize_button.setEnabled(True)
|
|
605
638
|
|
|
639
|
+
@pyqtSlot()
|
|
640
|
+
def _on_worker_result(self) -> None:
|
|
641
|
+
"""Receive a private conformer on the GUI thread while the dialog exists."""
|
|
642
|
+
thread = self._opt_thread
|
|
643
|
+
if thread is None or self.sender() is not thread or thread.mol is None:
|
|
644
|
+
return
|
|
645
|
+
self._on_optimization_finished(
|
|
646
|
+
self._optimization_method, thread.mol.GetConformer()
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
@pyqtSlot()
|
|
650
|
+
def _on_worker_finished(self) -> None:
|
|
651
|
+
"""Drop the completed worker reference before its owner deletes it."""
|
|
652
|
+
if self.sender() is self._opt_thread:
|
|
653
|
+
self._opt_thread = None
|
|
654
|
+
|
|
606
655
|
def _on_optimization_error(self, err_msg: str) -> None:
|
|
607
656
|
"""Handle optimization thread failure callback."""
|
|
608
657
|
if self._closed:
|
|
@@ -622,7 +671,24 @@ class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
|
|
|
622
671
|
logging.info("Discarding constrained optimization result after close.")
|
|
623
672
|
return
|
|
624
673
|
self.optimize_button.setEnabled(True)
|
|
674
|
+
current_mol = self.main_window.view_3d_manager.current_mol
|
|
675
|
+
if current_mol is None or current_mol is not self.mol:
|
|
676
|
+
logging.info("Discarding constrained optimization for a replaced molecule.")
|
|
677
|
+
return
|
|
678
|
+
if self._optimization_baseline is not None and not np.array_equal(
|
|
679
|
+
self.mol.GetConformer().GetPositions(), self._optimization_baseline
|
|
680
|
+
):
|
|
681
|
+
status_bar = self.main_window.statusBar()
|
|
682
|
+
if status_bar is not None:
|
|
683
|
+
status_bar.showMessage(
|
|
684
|
+
"Optimization discarded: molecule geometry changed while running."
|
|
685
|
+
)
|
|
686
|
+
return
|
|
625
687
|
try:
|
|
688
|
+
live_conf = self.mol.GetConformer()
|
|
689
|
+
for i in range(self.mol.GetNumAtoms()):
|
|
690
|
+
pos = conf.GetAtomPosition(i)
|
|
691
|
+
live_conf.SetAtomPosition(i, (float(pos.x), float(pos.y), float(pos.z)))
|
|
626
692
|
# Apply optimized coordinates to the main window's numpy array
|
|
627
693
|
cache = self.main_window.view_3d_manager.atom_positions_3d
|
|
628
694
|
for i in range(self.mol.GetNumAtoms()):
|
|
@@ -670,6 +736,8 @@ class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
|
|
|
670
736
|
self.main_window.view_3d_manager.draw_molecule_3d(self.mol)
|
|
671
737
|
self.main_window.view_3d_manager.update_chiral_labels()
|
|
672
738
|
self.main_window.edit_actions_manager.push_undo_state()
|
|
739
|
+
self.main_window.state_manager.has_unsaved_changes = True
|
|
740
|
+
self.main_window.state_manager.update_window_title()
|
|
673
741
|
status_bar = self.main_window.statusBar()
|
|
674
742
|
if status_bar is not None:
|
|
675
743
|
status_bar.showMessage("Constrained optimization finished.")
|
|
@@ -687,6 +755,12 @@ class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
|
|
|
687
755
|
except (AttributeError, RuntimeError, ValueError, TypeError) as e:
|
|
688
756
|
logging.exception("Optimization failed: %s", e)
|
|
689
757
|
|
|
758
|
+
def invalidate_molecule(self) -> None:
|
|
759
|
+
"""Discard pending work and constraints when the document is replaced."""
|
|
760
|
+
self._invalidated = True
|
|
761
|
+
self.setEnabled(False)
|
|
762
|
+
self.reject()
|
|
763
|
+
|
|
690
764
|
def closeEvent(self, event: Any) -> None:
|
|
691
765
|
"""Delegate window close to reject."""
|
|
692
766
|
self.reject()
|
|
@@ -695,9 +769,14 @@ class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
|
|
|
695
769
|
def reject(self) -> None:
|
|
696
770
|
"""Clear labels, disable picking, and save constraints before closing."""
|
|
697
771
|
self._closed = True
|
|
772
|
+
if self._opt_thread is not None:
|
|
773
|
+
self._opt_thread.requestInterruption()
|
|
698
774
|
self.clear_constraint_labels()
|
|
699
775
|
self.clear_selection_labels()
|
|
700
776
|
self.disable_picking()
|
|
777
|
+
if self._invalidated:
|
|
778
|
+
super().reject()
|
|
779
|
+
return
|
|
701
780
|
|
|
702
781
|
# Save constraints list to MainWindow when closing the dialog
|
|
703
782
|
try:
|
|
@@ -20,7 +20,7 @@ from typing import Any, Dict, List, Optional
|
|
|
20
20
|
|
|
21
21
|
import numpy as np
|
|
22
22
|
import pyvista as pv
|
|
23
|
-
from PyQt6.QtCore import QPointF
|
|
23
|
+
from PyQt6.QtCore import QPointF, QThread, QTimer
|
|
24
24
|
from PyQt6.QtGui import QColor, QFont
|
|
25
25
|
from PyQt6.QtWidgets import QGraphicsTextItem
|
|
26
26
|
|
|
@@ -53,6 +53,38 @@ class Edit3DManager:
|
|
|
53
53
|
self.is_3d_edit_mode = False
|
|
54
54
|
self.dragged_atom_info = None
|
|
55
55
|
self.constraints_3d: list[Any] = []
|
|
56
|
+
self._optimization_threads: set[QThread] = set()
|
|
57
|
+
self._close_after_optimization = False
|
|
58
|
+
|
|
59
|
+
def register_optimization_thread(self, thread: QThread) -> None:
|
|
60
|
+
"""Own minimization workers independently of their modeless dialogs."""
|
|
61
|
+
thread.setParent(self.host)
|
|
62
|
+
self._optimization_threads.add(thread)
|
|
63
|
+
thread.finished.connect(lambda: self._release_optimization_thread(thread))
|
|
64
|
+
|
|
65
|
+
def _release_optimization_thread(self, thread: QThread) -> None:
|
|
66
|
+
"""Delete completed workers and resume a deferred application close."""
|
|
67
|
+
self._optimization_threads.discard(thread)
|
|
68
|
+
thread.deleteLater()
|
|
69
|
+
if self._close_after_optimization and not self._optimization_threads:
|
|
70
|
+
self._close_after_optimization = False
|
|
71
|
+
QTimer.singleShot(0, self.host.close)
|
|
72
|
+
|
|
73
|
+
def prepare_optimization_shutdown(self) -> bool:
|
|
74
|
+
"""Cancel workers and defer window destruction while minimization exits."""
|
|
75
|
+
running = [
|
|
76
|
+
thread for thread in self._optimization_threads if thread.isRunning()
|
|
77
|
+
]
|
|
78
|
+
if not running:
|
|
79
|
+
return True
|
|
80
|
+
self._close_after_optimization = True
|
|
81
|
+
self.close_all_3d_edit_dialogs()
|
|
82
|
+
for thread in running:
|
|
83
|
+
thread.requestInterruption()
|
|
84
|
+
self.host.update_status_message(
|
|
85
|
+
"Canceling constrained optimization before closing..."
|
|
86
|
+
)
|
|
87
|
+
return False
|
|
56
88
|
|
|
57
89
|
def _label_kwargs(self, kind: str) -> Dict[str, Any]:
|
|
58
90
|
"""add_point_labels style arguments for one label kind, from settings."""
|
|
@@ -93,6 +125,13 @@ class Edit3DManager:
|
|
|
93
125
|
else:
|
|
94
126
|
self.host.statusBar().showMessage("Measurement mode disabled.")
|
|
95
127
|
|
|
128
|
+
def invalidate_molecule_dialogs(self) -> None:
|
|
129
|
+
"""Discard molecule-bound dialog sessions before replacing the document."""
|
|
130
|
+
for dialog in self.active_3d_dialogs.copy():
|
|
131
|
+
if not sip_isdeleted_safe(dialog):
|
|
132
|
+
dialog.invalidate_molecule()
|
|
133
|
+
self.active_3d_dialogs.clear()
|
|
134
|
+
|
|
96
135
|
def close_all_3d_edit_dialogs(self) -> None:
|
|
97
136
|
"""Close all active 3D edit dialogs."""
|
|
98
137
|
dialogs_to_close = self.active_3d_dialogs.copy()
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/geometry_base_dialog.py
RENAMED
|
@@ -118,6 +118,8 @@ class GeometryBaseDialog(BasePickingDialog):
|
|
|
118
118
|
def on_slider_released(self) -> None:
|
|
119
119
|
"""Finalize a slider drag operation."""
|
|
120
120
|
self._slider_dragging = False
|
|
121
|
+
if self._invalidated:
|
|
122
|
+
return
|
|
121
123
|
# Snapshot is usually kept until selection changes to preserve turn direction,
|
|
122
124
|
# but subclasses can override this behavior if needed.
|
|
123
125
|
self.main_window.view_3d_manager.draw_molecule_3d(self.mol)
|
|
@@ -445,9 +445,6 @@ class IOManager:
|
|
|
445
445
|
|
|
446
446
|
def load_xyz_file(self, file_path: str) -> Optional[Any]:
|
|
447
447
|
"""Load XYZ file and create RDKit Mol with charge prompt and bond determination."""
|
|
448
|
-
if not self.host.state_manager.check_unsaved_changes():
|
|
449
|
-
return None
|
|
450
|
-
|
|
451
448
|
try:
|
|
452
449
|
return self._mol_from_xyz_lines(self._read_text_lines_flexible(file_path))
|
|
453
450
|
except (RuntimeError, TypeError, ValueError, UnicodeDecodeError) as e:
|
|
@@ -466,6 +463,8 @@ class IOManager:
|
|
|
466
463
|
self, xyz_text: str, source_name: str = "XYZ data"
|
|
467
464
|
) -> Optional[Any]:
|
|
468
465
|
"""Load XYZ text, set it as the current molecule, and draw it in 3D."""
|
|
466
|
+
if not self.host.state_manager.check_unsaved_changes():
|
|
467
|
+
return None
|
|
469
468
|
try:
|
|
470
469
|
mol = self.load_xyz_block(xyz_text)
|
|
471
470
|
if mol is None:
|
|
@@ -490,6 +489,7 @@ class IOManager:
|
|
|
490
489
|
self.host.statusBar().showMessage(
|
|
491
490
|
f"3D Viewer Mode: Loaded {source_name}"
|
|
492
491
|
)
|
|
492
|
+
self.host.edit_actions_manager.reset_history()
|
|
493
493
|
self.host.set_has_unsaved_changes(False)
|
|
494
494
|
self.host.state_manager.update_window_title()
|
|
495
495
|
return mol
|
|
@@ -1055,6 +1055,8 @@ class IOManager:
|
|
|
1055
1055
|
|
|
1056
1056
|
def load_xyz_for_3d_viewing(self, file_path: Optional[str] = None) -> None:
|
|
1057
1057
|
"""Load XYZ file and display in 3D viewer."""
|
|
1058
|
+
if not self.host.state_manager.check_unsaved_changes():
|
|
1059
|
+
return
|
|
1058
1060
|
if not file_path:
|
|
1059
1061
|
default_dir = (
|
|
1060
1062
|
os.path.dirname(self.host.init_manager.current_file_path)
|
|
@@ -1113,6 +1115,7 @@ class IOManager:
|
|
|
1113
1115
|
)
|
|
1114
1116
|
self.host.statusBar().showMessage(message)
|
|
1115
1117
|
self.host.set_current_file_path(file_path)
|
|
1118
|
+
self.host.edit_actions_manager.reset_history()
|
|
1116
1119
|
self.host.set_has_unsaved_changes(False)
|
|
1117
1120
|
self.host.state_manager.update_window_title()
|
|
1118
1121
|
except (OSError, IOError, ValueError, RuntimeError, AttributeError) as e:
|
|
@@ -1160,6 +1163,7 @@ class IOManager:
|
|
|
1160
1163
|
self.host.view_3d_manager.update_atom_id_menu_state()
|
|
1161
1164
|
self.host.update_status_message(f"Loaded {file_path} in 3D viewer")
|
|
1162
1165
|
self.host.set_current_file_path(file_path)
|
|
1166
|
+
self.host.edit_actions_manager.reset_history()
|
|
1163
1167
|
self.host.set_has_unsaved_changes(False)
|
|
1164
1168
|
self.host.state_manager.update_window_title()
|
|
1165
1169
|
except SdfSelectionCancelled:
|
|
@@ -133,6 +133,8 @@ class MainWindow(QMainWindow):
|
|
|
133
133
|
|
|
134
134
|
def set_current_molecule(self, mol: Optional[Chem.Mol]) -> None:
|
|
135
135
|
"""Set the current 3D molecule and trigger rendering updates."""
|
|
136
|
+
if mol is not self.view_3d_manager.current_mol:
|
|
137
|
+
self.edit_3d_manager.invalidate_molecule_dialogs()
|
|
136
138
|
self.view_3d_manager.current_mol = mol
|
|
137
139
|
|
|
138
140
|
def set_3d_atom_positions(self, positions: Any) -> None:
|
|
@@ -141,7 +143,7 @@ class MainWindow(QMainWindow):
|
|
|
141
143
|
|
|
142
144
|
def clear_3d_view(self) -> None:
|
|
143
145
|
"""Clear 3D molecule state and plotter rendering."""
|
|
144
|
-
self.
|
|
146
|
+
self.set_current_molecule(None)
|
|
145
147
|
if self.view_3d_manager.plotter:
|
|
146
148
|
self.view_3d_manager.plotter.clear()
|
|
147
149
|
|
|
@@ -292,7 +294,7 @@ class MainWindow(QMainWindow):
|
|
|
292
294
|
@current_mol.setter
|
|
293
295
|
def current_mol(self, value: Any) -> None:
|
|
294
296
|
"""Proxy for current 3D molecule (read/write)."""
|
|
295
|
-
self.
|
|
297
|
+
self.set_current_molecule(value)
|
|
296
298
|
|
|
297
299
|
@property
|
|
298
300
|
def plotter(self) -> Optional[CustomQtInteractor]:
|
|
@@ -37,6 +37,7 @@ class MirrorDialog(QDialog):
|
|
|
37
37
|
super().__init__(parent)
|
|
38
38
|
self.mol = mol
|
|
39
39
|
self.main_window = main_window
|
|
40
|
+
self._invalidated = False
|
|
40
41
|
self.plane_group: Optional[QButtonGroup] = None
|
|
41
42
|
self.xy_radio: Optional[QRadioButton] = None
|
|
42
43
|
self.xz_radio: Optional[QRadioButton] = None
|
|
@@ -87,8 +88,16 @@ class MirrorDialog(QDialog):
|
|
|
87
88
|
|
|
88
89
|
layout.addLayout(button_layout)
|
|
89
90
|
|
|
91
|
+
def invalidate_molecule(self) -> None:
|
|
92
|
+
"""Close the mirror tool when its molecule is replaced."""
|
|
93
|
+
self._invalidated = True
|
|
94
|
+
self.setEnabled(False)
|
|
95
|
+
self.reject()
|
|
96
|
+
|
|
90
97
|
def apply_mirror(self) -> None:
|
|
91
98
|
"""Apply mirror transformation across the selected plane."""
|
|
99
|
+
if self._invalidated:
|
|
100
|
+
return
|
|
92
101
|
if not self.mol or self.mol.GetNumConformers() == 0:
|
|
93
102
|
QMessageBox.warning(self, "Error", "No 3D coordinates available.")
|
|
94
103
|
return
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/molecular_scene_handler.py
RENAMED
|
@@ -445,10 +445,10 @@ class TemplateMixin:
|
|
|
445
445
|
# policy, so an unchanged order means keep this bond
|
|
446
446
|
if order == exist_b.order:
|
|
447
447
|
continue
|
|
448
|
+
key = (id1, id2) if (id1, id2) in self.data.bonds else (id2, id1)
|
|
449
|
+
self.data.bonds[key].update(order=order, stereo=0)
|
|
448
450
|
exist_b.order = order
|
|
449
451
|
exist_b.stereo = 0
|
|
450
|
-
self.data.bonds[(id1, id2)]["order"] = order
|
|
451
|
-
self.data.bonds[(id1, id2)]["stereo"] = 0
|
|
452
452
|
exist_b.update()
|
|
453
453
|
else:
|
|
454
454
|
# Create new bond
|
|
@@ -12,7 +12,8 @@ DOI: 10.5281/zenodo.17268532
|
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
import logging
|
|
15
|
-
|
|
15
|
+
import copy
|
|
16
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
|
|
16
17
|
|
|
17
18
|
from PyQt6.QtCore import QEvent, QLineF, Qt, QPointF
|
|
18
19
|
from PyQt6.QtGui import QPen
|
|
@@ -216,6 +217,7 @@ class MoleculeScene(
|
|
|
216
217
|
self, raw_atoms: Dict[int, Any], raw_bonds: Dict[Tuple[int, int], Any]
|
|
217
218
|
) -> None:
|
|
218
219
|
"""Restore scene items from undo/redo state (dict-of-dicts format)."""
|
|
220
|
+
self.data.adjacency_list = {atom_id: [] for atom_id in raw_atoms}
|
|
219
221
|
for atom_id, data in raw_atoms.items():
|
|
220
222
|
raw_pos = tuple(data["pos"])
|
|
221
223
|
pos_q = QPointF(raw_pos[0], raw_pos[1])
|
|
@@ -245,6 +247,8 @@ class MoleculeScene(
|
|
|
245
247
|
"order": data.get("order", 1),
|
|
246
248
|
"stereo": data.get("stereo", 0),
|
|
247
249
|
}
|
|
250
|
+
self.data.adjacency_list[id1].append(id2)
|
|
251
|
+
self.data.adjacency_list[id2].append(id1)
|
|
248
252
|
self.bond_items[key_tuple] = bond_item
|
|
249
253
|
atom1_item.bonds.append(bond_item)
|
|
250
254
|
atom2_item.bonds.append(bond_item)
|
|
@@ -258,6 +262,7 @@ class MoleculeScene(
|
|
|
258
262
|
self, atoms_2d: List[Dict[str, Any]], bonds_2d: List[Dict[str, Any]]
|
|
259
263
|
) -> None:
|
|
260
264
|
"""Restore scene items from PMEPRJ JSON (list-of-dicts format)."""
|
|
265
|
+
self.data.adjacency_list = {atom["id"]: [] for atom in atoms_2d}
|
|
261
266
|
for atom_data in atoms_2d:
|
|
262
267
|
atom_id = atom_data["id"]
|
|
263
268
|
symbol = atom_data["symbol"]
|
|
@@ -290,6 +295,8 @@ class MoleculeScene(
|
|
|
290
295
|
"order": bond_order,
|
|
291
296
|
"stereo": stereo,
|
|
292
297
|
}
|
|
298
|
+
self.data.adjacency_list[atom1_id].append(atom2_id)
|
|
299
|
+
self.data.adjacency_list[atom2_id].append(atom1_id)
|
|
293
300
|
self.bond_items[(atom1_id, atom2_id)] = bond_item
|
|
294
301
|
self.addItem(bond_item)
|
|
295
302
|
|
|
@@ -297,6 +304,58 @@ class MoleculeScene(
|
|
|
297
304
|
atom_item.update_style()
|
|
298
305
|
self.update_all_items()
|
|
299
306
|
|
|
307
|
+
def add_molecule_fragment(
|
|
308
|
+
self,
|
|
309
|
+
points: List[Union[QPointF, Tuple[float, float]]],
|
|
310
|
+
bonds_info: List[Tuple[int, int, int]],
|
|
311
|
+
existing_items: Optional[List[AtomItem]] = None,
|
|
312
|
+
symbol: str = "C",
|
|
313
|
+
) -> List[Optional[AtomItem]]:
|
|
314
|
+
"""Insert a template, restoring model and scene if a Qt/model write fails."""
|
|
315
|
+
atoms = copy.deepcopy(self.data.atoms)
|
|
316
|
+
bonds = copy.deepcopy(self.data.bonds)
|
|
317
|
+
next_atom_id = self.data.next_atom_id
|
|
318
|
+
selected_ids = {i for i, item in self.atom_items.items() if item.isSelected()}
|
|
319
|
+
try:
|
|
320
|
+
return TemplateMixin.add_molecule_fragment(
|
|
321
|
+
self, points, bonds_info, existing_items, symbol
|
|
322
|
+
)
|
|
323
|
+
except (RuntimeError, ValueError, TypeError, KeyError, IndexError):
|
|
324
|
+
logging.exception("Template insertion failed; restoring the previous scene")
|
|
325
|
+
self.clear()
|
|
326
|
+
self.data.atoms.clear()
|
|
327
|
+
self.data.bonds.clear()
|
|
328
|
+
self.data.next_atom_id = next_atom_id
|
|
329
|
+
self.reinitialize_items()
|
|
330
|
+
self.restore_atoms_and_bonds(atoms, bonds)
|
|
331
|
+
for atom_id in selected_ids:
|
|
332
|
+
self.atom_items[atom_id].setSelected(True)
|
|
333
|
+
raise
|
|
334
|
+
|
|
335
|
+
def clear(self) -> None:
|
|
336
|
+
"""Release item and interaction references before Qt deletes the scene."""
|
|
337
|
+
self.atom_items.clear()
|
|
338
|
+
self.bond_items.clear()
|
|
339
|
+
self._deleted_items.clear()
|
|
340
|
+
self.initial_positions_in_event.clear()
|
|
341
|
+
self.template_context.clear()
|
|
342
|
+
self.template_preview = None
|
|
343
|
+
self.template_preview_points = []
|
|
344
|
+
self.start_atom = None
|
|
345
|
+
self.temp_line = None
|
|
346
|
+
self.hovered_item = None
|
|
347
|
+
self.start_pos = None
|
|
348
|
+
self.press_pos = None
|
|
349
|
+
self.chain_start_atom = None
|
|
350
|
+
self.chain_end_atom = None
|
|
351
|
+
self.chain_anchor = None
|
|
352
|
+
self.chain_points = []
|
|
353
|
+
self.chain_active = False
|
|
354
|
+
self.data_changed_in_event = False
|
|
355
|
+
self.mouse_moved_since_press = False
|
|
356
|
+
self.was_selected_on_press = False
|
|
357
|
+
super().clear()
|
|
358
|
+
|
|
300
359
|
def reinitialize_items(self) -> None:
|
|
301
360
|
"""Reset transient scene state including template preview and deleted-item list."""
|
|
302
361
|
self.template_preview = TemplatePreviewItem()
|
|
@@ -219,6 +219,8 @@ class UIManager(QObject):
|
|
|
219
219
|
Handle application close logic.
|
|
220
220
|
Returns True if close should proceed, False if it should be cancelled.
|
|
221
221
|
"""
|
|
222
|
+
if not self.host.edit_3d_manager.prepare_optimization_shutdown():
|
|
223
|
+
return False
|
|
222
224
|
# 1. Persist settings
|
|
223
225
|
try:
|
|
224
226
|
modified = self.host.init_manager.settings_dirty or (
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/MoleditPy_linux.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/MoleditPy_linux.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/MoleditPy_linux.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/core/molecular_data.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/plugins/plugin_interface.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/plugins/plugin_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/align_plane_dialog.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/alignment_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/bond_length_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/chirality_warning_dialog.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/color_settings_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/custom_interactor_style.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/custom_qt_interactor.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/edit_actions_logic.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/main_window_init.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/move_dialog_mixin.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/move_group_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/periodic_table_dialog.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/planarize_dialog.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/plugin_menu_manager.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/preview_molecule.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/sdf_record_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/settings_tabs/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/string_importers.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/template_preview_item.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/template_preview_view.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/translation_dialog.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/ui/user_template_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/utils/default_settings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.3 → moleditpy_linux-4.11.5}/src/moleditpy_linux/utils/sip_isdeleted_safe.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|