MoleditPy-linux 4.4.1__py3-none-any.whl → 4.5.0__py3-none-any.whl
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/plugins/plugin_interface.py +23 -1
- moleditpy_linux/plugins/plugin_manager.py +50 -1
- moleditpy_linux/ui/atom_item.py +2 -0
- moleditpy_linux/ui/custom_interactor_style.py +570 -183
- moleditpy_linux/ui/move_group_dialog.py +5 -5
- moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py +28 -0
- moleditpy_linux/utils/constants.py +3 -0
- moleditpy_linux/utils/default_settings.py +2 -0
- {moleditpy_linux-4.4.1.dist-info → moleditpy_linux-4.5.0.dist-info}/METADATA +1 -1
- {moleditpy_linux-4.4.1.dist-info → moleditpy_linux-4.5.0.dist-info}/RECORD +14 -14
- {moleditpy_linux-4.4.1.dist-info → moleditpy_linux-4.5.0.dist-info}/WHEEL +0 -0
- {moleditpy_linux-4.4.1.dist-info → moleditpy_linux-4.5.0.dist-info}/entry_points.txt +0 -0
- {moleditpy_linux-4.4.1.dist-info → moleditpy_linux-4.5.0.dist-info}/licenses/LICENSE +0 -0
- {moleditpy_linux-4.4.1.dist-info → moleditpy_linux-4.5.0.dist-info}/top_level.txt +0 -0
|
@@ -11,7 +11,7 @@ DOI: 10.5281/zenodo.17268532
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import logging
|
|
14
|
-
from typing import Any, Callable, List, Optional, Union
|
|
14
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class PluginContext:
|
|
@@ -353,6 +353,28 @@ class PluginContext:
|
|
|
353
353
|
"""
|
|
354
354
|
self._manager.register_document_reset_handler(self._plugin_name, callback)
|
|
355
355
|
|
|
356
|
+
def register_atom_drag_handler(
|
|
357
|
+
self,
|
|
358
|
+
callback: Callable[
|
|
359
|
+
[str, List[int], Dict[int, Tuple[float, float, float]]], None
|
|
360
|
+
],
|
|
361
|
+
) -> None:
|
|
362
|
+
"""Register a handler called during 3D atom or group dragging.
|
|
363
|
+
|
|
364
|
+
The callback receives:
|
|
365
|
+
event_type (str): ``"start"``, ``"move"``, or ``"end"``.
|
|
366
|
+
atom_indices (List[int]): RDKit atom indices being dragged.
|
|
367
|
+
positions (dict[int, tuple[float, float, float]]):
|
|
368
|
+
Current 3D positions of the dragged atoms.
|
|
369
|
+
Empty dict on ``"start"``.
|
|
370
|
+
"""
|
|
371
|
+
self._manager.register_atom_drag_handler(self._plugin_name, callback)
|
|
372
|
+
|
|
373
|
+
@property
|
|
374
|
+
def is_dragging_atom(self) -> bool:
|
|
375
|
+
"""Return True if an atom or group is currently being dragged in 3D."""
|
|
376
|
+
return self._manager.is_dragging_atom() # type: ignore[no-any-return]
|
|
377
|
+
|
|
356
378
|
def get_setting(self, key: str, default: Any = None) -> Any:
|
|
357
379
|
"""
|
|
358
380
|
Get a plugin-specific persistent setting.
|
|
@@ -23,8 +23,9 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
|
23
23
|
|
|
24
24
|
from PyQt6.QtCore import QUrl
|
|
25
25
|
from PyQt6.QtGui import QDesktopServices
|
|
26
|
-
from PyQt6.QtWidgets import QMessageBox
|
|
26
|
+
from PyQt6.QtWidgets import QApplication, QMessageBox
|
|
27
27
|
|
|
28
|
+
from ..utils.constants import MOVE_DIALOG_TYPES
|
|
28
29
|
from .plugin_interface import PluginContext
|
|
29
30
|
|
|
30
31
|
|
|
@@ -95,6 +96,7 @@ class PluginManager:
|
|
|
95
96
|
self.load_handlers: Dict[str, Callable] = {}
|
|
96
97
|
self.custom_3d_styles: Dict[str, Dict[str, Any]] = {}
|
|
97
98
|
self.document_reset_handlers: List[Dict[str, Any]] = []
|
|
99
|
+
self.atom_drag_handlers: List[Dict[str, Any]] = []
|
|
98
100
|
self.plugin_windows: Dict[
|
|
99
101
|
str, Dict[str, Any]
|
|
100
102
|
] = {} # Map of plugin_name -> {window_id -> window}
|
|
@@ -247,6 +249,7 @@ class PluginManager:
|
|
|
247
249
|
self.load_handlers = {}
|
|
248
250
|
self.custom_3d_styles = {}
|
|
249
251
|
self.document_reset_handlers = []
|
|
252
|
+
self.atom_drag_handlers = []
|
|
250
253
|
|
|
251
254
|
if not os.path.exists(self.plugin_dir):
|
|
252
255
|
return []
|
|
@@ -558,6 +561,10 @@ class PluginManager:
|
|
|
558
561
|
{"plugin": plugin_name, "callback": callback}
|
|
559
562
|
)
|
|
560
563
|
|
|
564
|
+
def register_atom_drag_handler(self, plugin_name: str, callback: Callable) -> None:
|
|
565
|
+
"""Register a handler called during 3D atom/group dragging."""
|
|
566
|
+
self.atom_drag_handlers.append({"plugin": plugin_name, "callback": callback})
|
|
567
|
+
|
|
561
568
|
# --- New API Implementation ---
|
|
562
569
|
def show_status_message(self, message: str, timeout: int = 3000) -> None:
|
|
563
570
|
"""Display a message in the MainWindow status bar."""
|
|
@@ -653,6 +660,48 @@ class PluginManager:
|
|
|
653
660
|
exc_info=True,
|
|
654
661
|
)
|
|
655
662
|
|
|
663
|
+
def invoke_atom_drag_handlers(
|
|
664
|
+
self,
|
|
665
|
+
event_type: str,
|
|
666
|
+
atom_indices: List[int],
|
|
667
|
+
positions: Dict[int, tuple],
|
|
668
|
+
) -> None:
|
|
669
|
+
"""Call all registered atom drag handlers."""
|
|
670
|
+
for handler in self.atom_drag_handlers:
|
|
671
|
+
try:
|
|
672
|
+
handler["callback"](event_type, atom_indices, positions)
|
|
673
|
+
except Exception as e: # plugins have full app access; catch everything to keep the drag alive
|
|
674
|
+
logging.warning(
|
|
675
|
+
"Error in atom drag handler for %s: %s",
|
|
676
|
+
handler["plugin"],
|
|
677
|
+
e,
|
|
678
|
+
exc_info=True,
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
def is_dragging_atom(self) -> bool:
|
|
682
|
+
"""Return True if an atom or group is currently being dragged in 3D."""
|
|
683
|
+
mw = self.main_window
|
|
684
|
+
if mw is None:
|
|
685
|
+
return False
|
|
686
|
+
if getattr(mw, "dragged_atom_info", None) is not None:
|
|
687
|
+
return True
|
|
688
|
+
return self._is_dragging_group()
|
|
689
|
+
|
|
690
|
+
@staticmethod
|
|
691
|
+
def _is_dragging_group() -> bool:
|
|
692
|
+
"""Return True while a Move Group / Move Selected Atoms gesture is active."""
|
|
693
|
+
try:
|
|
694
|
+
for widget in QApplication.topLevelWidgets():
|
|
695
|
+
if type(widget).__name__ not in MOVE_DIALOG_TYPES:
|
|
696
|
+
continue
|
|
697
|
+
if getattr(widget, "is_dragging_group_vtk", False) or getattr(
|
|
698
|
+
widget, "is_rotating_group_vtk", False
|
|
699
|
+
):
|
|
700
|
+
return True
|
|
701
|
+
except (AttributeError, RuntimeError, TypeError):
|
|
702
|
+
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
703
|
+
return False
|
|
704
|
+
|
|
656
705
|
def get_plugin_info_safe(self, file_path: str) -> Dict[str, str]:
|
|
657
706
|
"""Extracts plugin metadata using AST parsing (safe, no execution)."""
|
|
658
707
|
info = {
|
moleditpy_linux/ui/atom_item.py
CHANGED
|
@@ -63,6 +63,8 @@ class AtomItem(QGraphicsItem):
|
|
|
63
63
|
self.setFlags(
|
|
64
64
|
QGraphicsItem.GraphicsItemFlag.ItemIsMovable
|
|
65
65
|
| QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
|
|
66
|
+
# Without this Qt never calls itemChange for moves, so bonds lag.
|
|
67
|
+
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
|
|
66
68
|
)
|
|
67
69
|
self.setZValue(1)
|
|
68
70
|
self.update_style()
|
|
@@ -13,7 +13,8 @@ DOI: 10.5281/zenodo.17268532
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
import logging
|
|
16
|
-
|
|
16
|
+
import time
|
|
17
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
17
18
|
|
|
18
19
|
import numpy as np
|
|
19
20
|
from PyQt6.QtCore import Qt, QTimer
|
|
@@ -22,6 +23,7 @@ from vtkmodules.vtkInteractionStyle import vtkInteractorStyleTrackballCamera #
|
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
from .atom_picking import pick_atom_index_from_screen
|
|
26
|
+
from ..utils.constants import MOVE_DIALOG_TYPES
|
|
25
27
|
|
|
26
28
|
from rdkit import Geometry
|
|
27
29
|
|
|
@@ -35,6 +37,32 @@ _VTKIS_ROTATE = 1
|
|
|
35
37
|
# window-size independent and matches the feel at the default window size.
|
|
36
38
|
_ROTATION_REFERENCE_SIZE = 640.0
|
|
37
39
|
|
|
40
|
+
# Minimum seconds between real-time drag redraws (~30 fps).
|
|
41
|
+
_DRAG_REDRAW_INTERVAL = 0.033
|
|
42
|
+
|
|
43
|
+
# Largest molecule that still gets real-time drag feedback (each frame is a
|
|
44
|
+
# full scene rebuild).
|
|
45
|
+
_REALTIME_DRAG_MAX_ATOMS = 300
|
|
46
|
+
|
|
47
|
+
# Radians of group rotation per pixel of mouse travel, before sensitivity.
|
|
48
|
+
_GROUP_ROTATION_RADIANS_PER_PIXEL = 0.008
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _rodrigues_matrix(axis: np.ndarray, angle: float) -> np.ndarray:
|
|
52
|
+
"""Return the rotation matrix for `angle` radians about `axis` (Rodrigues)."""
|
|
53
|
+
norm = float(np.linalg.norm(axis))
|
|
54
|
+
if norm < 1e-6 or abs(angle) < 1e-6:
|
|
55
|
+
return np.eye(3)
|
|
56
|
+
unit = axis / norm
|
|
57
|
+
k_mat = np.array(
|
|
58
|
+
[
|
|
59
|
+
[0.0, -unit[2], unit[1]],
|
|
60
|
+
[unit[2], 0.0, -unit[0]],
|
|
61
|
+
[-unit[1], unit[0], 0.0],
|
|
62
|
+
]
|
|
63
|
+
)
|
|
64
|
+
return np.eye(3) + np.sin(angle) * k_mat + (1.0 - np.cos(angle)) * (k_mat @ k_mat)
|
|
65
|
+
|
|
38
66
|
|
|
39
67
|
class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
40
68
|
"""VTK interactor style extending trackball-camera with 3D atom drag and measurement."""
|
|
@@ -48,6 +76,10 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
48
76
|
self._mouse_moved_during_drag = False
|
|
49
77
|
self._mouse_press_pos: Optional[tuple[int, int]] = None
|
|
50
78
|
self._suppress_next_left_button_up = False
|
|
79
|
+
# Throttle for real-time drag redraws
|
|
80
|
+
self._last_drag_redraw_time: float = 0.0
|
|
81
|
+
self._drag_redraw_pending = False
|
|
82
|
+
self._active_drag_atoms: Optional[List[int]] = None
|
|
51
83
|
|
|
52
84
|
self.AddObserver("LeftButtonPressEvent", self.on_left_button_down) # type: ignore[arg-type]
|
|
53
85
|
# self.AddObserver("LeftButtonDoubleClickEvent", self.on_left_button_down)
|
|
@@ -70,6 +102,8 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
70
102
|
# Safe defensive fallback catching AttributeError, RuntimeError
|
|
71
103
|
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
72
104
|
|
|
105
|
+
self._end_drag_event()
|
|
106
|
+
|
|
73
107
|
# Reset all custom flags
|
|
74
108
|
self._is_dragging_atom = False
|
|
75
109
|
self.is_dragging = False
|
|
@@ -91,6 +125,198 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
91
125
|
# Safe defensive fallback catching AttributeError, RuntimeError
|
|
92
126
|
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
93
127
|
|
|
128
|
+
def _find_move_dialog(self) -> Any:
|
|
129
|
+
"""Return the visible Move Group / Move Selected Atoms dialog, if any."""
|
|
130
|
+
try:
|
|
131
|
+
for widget in QApplication.topLevelWidgets():
|
|
132
|
+
if type(widget).__name__ in MOVE_DIALOG_TYPES and widget.isVisible():
|
|
133
|
+
return widget
|
|
134
|
+
except (AttributeError, RuntimeError, TypeError):
|
|
135
|
+
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
def _display_to_world(
|
|
139
|
+
self,
|
|
140
|
+
renderer: Any,
|
|
141
|
+
screen_x: float,
|
|
142
|
+
screen_y: float,
|
|
143
|
+
depth_z: float,
|
|
144
|
+
) -> Optional[Tuple[float, float, float]]:
|
|
145
|
+
"""Convert screen (display) coordinates to 3D world coordinates.
|
|
146
|
+
|
|
147
|
+
Uses the given depth_z (already in display-space) so the result
|
|
148
|
+
lies on the same depth plane as the reference atom.
|
|
149
|
+
"""
|
|
150
|
+
try:
|
|
151
|
+
renderer.SetDisplayPoint(screen_x, screen_y, depth_z)
|
|
152
|
+
renderer.DisplayToWorld()
|
|
153
|
+
wp = renderer.GetWorldPoint()
|
|
154
|
+
return (wp[0], wp[1], wp[2])
|
|
155
|
+
except (AttributeError, RuntimeError, TypeError, ValueError):
|
|
156
|
+
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
def _world_to_display_depth(
|
|
160
|
+
self, renderer: Any, x: float, y: float, z: float
|
|
161
|
+
) -> Optional[float]:
|
|
162
|
+
"""Return the display-space Z depth for a world-space point."""
|
|
163
|
+
try:
|
|
164
|
+
renderer.SetWorldPoint(x, y, z, 1.0)
|
|
165
|
+
renderer.WorldToDisplay()
|
|
166
|
+
return float(renderer.GetDisplayPoint()[2])
|
|
167
|
+
except (AttributeError, RuntimeError, TypeError, ValueError):
|
|
168
|
+
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
def _invoke_drag_handlers(
|
|
172
|
+
self,
|
|
173
|
+
event_type: str,
|
|
174
|
+
atom_indices: List[int],
|
|
175
|
+
positions: Dict[int, Tuple[float, float, float]],
|
|
176
|
+
) -> None:
|
|
177
|
+
"""Forward a drag event to all registered plugin handlers."""
|
|
178
|
+
mw = self.main_window
|
|
179
|
+
if mw is None:
|
|
180
|
+
return
|
|
181
|
+
pm = getattr(mw, "plugin_manager", None)
|
|
182
|
+
if pm is None:
|
|
183
|
+
return
|
|
184
|
+
invoke = getattr(pm, "invoke_atom_drag_handlers", None)
|
|
185
|
+
if invoke is not None:
|
|
186
|
+
try:
|
|
187
|
+
invoke(event_type, atom_indices, positions)
|
|
188
|
+
# Plugin faults are already isolated per handler by the manager.
|
|
189
|
+
except (AttributeError, RuntimeError, TypeError, ValueError):
|
|
190
|
+
logging.warning("invoke_atom_drag_handlers raised", exc_info=True)
|
|
191
|
+
|
|
192
|
+
def _current_positions(
|
|
193
|
+
self, atom_indices: List[int]
|
|
194
|
+
) -> Dict[int, Tuple[float, float, float]]:
|
|
195
|
+
"""Read the current conformer coordinates of the given atoms."""
|
|
196
|
+
positions: Dict[int, Tuple[float, float, float]] = {}
|
|
197
|
+
try:
|
|
198
|
+
mol = self.main_window.view_3d_manager.current_mol
|
|
199
|
+
if mol is None or mol.GetNumConformers() == 0:
|
|
200
|
+
return positions
|
|
201
|
+
conf = mol.GetConformer()
|
|
202
|
+
for idx in atom_indices:
|
|
203
|
+
pos = conf.GetAtomPosition(idx)
|
|
204
|
+
positions[idx] = (float(pos.x), float(pos.y), float(pos.z))
|
|
205
|
+
except (AttributeError, RuntimeError, TypeError, ValueError):
|
|
206
|
+
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
207
|
+
return positions
|
|
208
|
+
|
|
209
|
+
def _begin_drag_event(self, atom_indices: Any) -> None:
|
|
210
|
+
"""Report the start of a drag gesture and arm the redraw throttle.
|
|
211
|
+
|
|
212
|
+
A right-button press never passes through reset_interactor_state, so
|
|
213
|
+
close any gesture whose end was missed before opening a new one.
|
|
214
|
+
"""
|
|
215
|
+
self._end_drag_event()
|
|
216
|
+
atoms = [int(idx) for idx in atom_indices]
|
|
217
|
+
self._active_drag_atoms = atoms
|
|
218
|
+
self._last_drag_redraw_time = 0.0
|
|
219
|
+
self._drag_redraw_pending = False
|
|
220
|
+
self._invoke_drag_handlers("start", list(atoms), {})
|
|
221
|
+
|
|
222
|
+
def _end_drag_event(self) -> None:
|
|
223
|
+
"""Report the end of the active drag gesture exactly once.
|
|
224
|
+
|
|
225
|
+
Every exit path (normal release, aborted gesture, forced reset) routes
|
|
226
|
+
through here so a plugin can never be left believing a drag is still
|
|
227
|
+
in progress.
|
|
228
|
+
"""
|
|
229
|
+
atoms = self._active_drag_atoms
|
|
230
|
+
self._active_drag_atoms = None
|
|
231
|
+
self._drag_redraw_pending = False
|
|
232
|
+
if atoms is None:
|
|
233
|
+
return
|
|
234
|
+
self._invoke_drag_handlers("end", list(atoms), self._current_positions(atoms))
|
|
235
|
+
|
|
236
|
+
def _realtime_drag_active(self) -> bool:
|
|
237
|
+
"""Return True when real-time drag is both enabled and affordable here.
|
|
238
|
+
|
|
239
|
+
Every frame rebuilds the whole 3D scene, so past a few hundred atoms
|
|
240
|
+
the rebuild costs more than the throttle interval and dragging feels
|
|
241
|
+
worse with it than without; the move is then applied on release only.
|
|
242
|
+
"""
|
|
243
|
+
if not self._realtime_drag_enabled():
|
|
244
|
+
return False
|
|
245
|
+
return not self._molecule_too_large_for_realtime()
|
|
246
|
+
|
|
247
|
+
def _molecule_too_large_for_realtime(self) -> bool:
|
|
248
|
+
"""Return True when the molecule exceeds the real-time drag size limit."""
|
|
249
|
+
try:
|
|
250
|
+
mol = self.main_window.view_3d_manager.current_mol
|
|
251
|
+
if mol is None:
|
|
252
|
+
return False
|
|
253
|
+
return int(mol.GetNumAtoms()) > _REALTIME_DRAG_MAX_ATOMS
|
|
254
|
+
except (AttributeError, RuntimeError, TypeError, ValueError):
|
|
255
|
+
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
def _should_drag_redraw(self) -> bool:
|
|
259
|
+
"""Return True when a real-time drag frame may be rendered now.
|
|
260
|
+
|
|
261
|
+
Skips while a previous deferred redraw is still queued so a scene that
|
|
262
|
+
rebuilds slower than the throttle interval cannot pile up frames.
|
|
263
|
+
"""
|
|
264
|
+
if not self._realtime_drag_active():
|
|
265
|
+
return False
|
|
266
|
+
if self._drag_redraw_pending:
|
|
267
|
+
return False
|
|
268
|
+
return time.monotonic() - self._last_drag_redraw_time >= _DRAG_REDRAW_INTERVAL
|
|
269
|
+
|
|
270
|
+
def _finish_drag_redraw(self) -> None:
|
|
271
|
+
"""Release the redraw gate once a deferred drag frame has rendered."""
|
|
272
|
+
self._drag_redraw_pending = False
|
|
273
|
+
self._last_drag_redraw_time = time.monotonic()
|
|
274
|
+
|
|
275
|
+
def _abort_active_drag(self, moved: bool) -> None:
|
|
276
|
+
"""Close out a gesture whose release event was lost.
|
|
277
|
+
|
|
278
|
+
Real-time dragging has already written the new geometry, so a moved
|
|
279
|
+
gesture still needs an undo entry even though it never reached the
|
|
280
|
+
release handler.
|
|
281
|
+
"""
|
|
282
|
+
if self._active_drag_atoms is None:
|
|
283
|
+
return
|
|
284
|
+
if moved and self._realtime_drag_active():
|
|
285
|
+
self._push_undo_after_aborted_drag()
|
|
286
|
+
self._end_drag_event()
|
|
287
|
+
|
|
288
|
+
def _push_undo_after_aborted_drag(self) -> None:
|
|
289
|
+
"""Record geometry left behind by a gesture whose release was lost.
|
|
290
|
+
|
|
291
|
+
Real-time dragging mutates the conformer on every frame, so an aborted
|
|
292
|
+
gesture would otherwise leave the molecule moved with nothing on the
|
|
293
|
+
undo stack.
|
|
294
|
+
"""
|
|
295
|
+
try:
|
|
296
|
+
self.main_window.edit_actions_manager.push_undo_state()
|
|
297
|
+
except (AttributeError, RuntimeError, TypeError, ValueError):
|
|
298
|
+
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
299
|
+
|
|
300
|
+
def _realtime_drag_enabled(self) -> bool:
|
|
301
|
+
"""Return True if real-time drag redraw is enabled in settings."""
|
|
302
|
+
mw = self.main_window
|
|
303
|
+
if mw is None:
|
|
304
|
+
return True
|
|
305
|
+
try:
|
|
306
|
+
return bool(mw.get_settings().get("realtime_3d_drag", True))
|
|
307
|
+
except (AttributeError, RuntimeError, TypeError):
|
|
308
|
+
return True
|
|
309
|
+
|
|
310
|
+
def _rotation_sensitivity(self) -> float:
|
|
311
|
+
"""Return the user's rotation-speed multiplier, shared by camera and group rotation."""
|
|
312
|
+
mw = self.main_window
|
|
313
|
+
if mw is None:
|
|
314
|
+
return 1.0
|
|
315
|
+
try:
|
|
316
|
+
return float(mw.get_settings().get("mouse_rotation_sensitivity", 1.0))
|
|
317
|
+
except (AttributeError, TypeError, ValueError):
|
|
318
|
+
return 1.0
|
|
319
|
+
|
|
94
320
|
def _rotate_size_independent(self) -> None:
|
|
95
321
|
"""Rotate the camera at a speed that does not depend on canvas size.
|
|
96
322
|
|
|
@@ -106,17 +332,12 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
106
332
|
dx = interactor.GetEventPosition()[0] - interactor.GetLastEventPosition()[0]
|
|
107
333
|
dy = interactor.GetEventPosition()[1] - interactor.GetLastEventPosition()[1]
|
|
108
334
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
)
|
|
116
|
-
except (AttributeError, TypeError, ValueError):
|
|
117
|
-
sensitivity = 1.0
|
|
118
|
-
|
|
119
|
-
delta = -20.0 / _ROTATION_REFERENCE_SIZE * self.GetMotionFactor() * sensitivity
|
|
335
|
+
delta = (
|
|
336
|
+
-20.0
|
|
337
|
+
/ _ROTATION_REFERENCE_SIZE
|
|
338
|
+
* self.GetMotionFactor()
|
|
339
|
+
* self._rotation_sensitivity()
|
|
340
|
+
)
|
|
120
341
|
camera = renderer.GetActiveCamera()
|
|
121
342
|
camera.Azimuth(dx * delta)
|
|
122
343
|
camera.Elevation(dy * delta)
|
|
@@ -154,20 +375,7 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
154
375
|
self._mouse_moved_during_drag = False
|
|
155
376
|
self._mouse_press_pos = None
|
|
156
377
|
|
|
157
|
-
|
|
158
|
-
# Check Move Group or Move Selected Atoms dialog
|
|
159
|
-
move_group_dialog: Any = None
|
|
160
|
-
for widget in QApplication.topLevelWidgets():
|
|
161
|
-
try:
|
|
162
|
-
if (
|
|
163
|
-
type(widget).__name__
|
|
164
|
-
in ("MoveGroupDialog", "MoveSelectedAtomsDialog")
|
|
165
|
-
and widget.isVisible()
|
|
166
|
-
):
|
|
167
|
-
move_group_dialog = widget
|
|
168
|
-
break
|
|
169
|
-
except (AttributeError, RuntimeError, TypeError):
|
|
170
|
-
logging.warning("Caught exception in " + __file__, exc_info=True)
|
|
378
|
+
move_group_dialog = self._find_move_dialog()
|
|
171
379
|
|
|
172
380
|
if move_group_dialog and move_group_dialog.group_atoms:
|
|
173
381
|
# Group drag if selected
|
|
@@ -197,6 +405,7 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
197
405
|
mw.view_3d_manager.plotter.setCursor(
|
|
198
406
|
Qt.CursorShape.ClosedHandCursor
|
|
199
407
|
)
|
|
408
|
+
self._begin_drag_event(move_group_dialog.group_atoms)
|
|
200
409
|
self._suppress_next_left_button_up = True
|
|
201
410
|
return # Disable camera rotation
|
|
202
411
|
else:
|
|
@@ -358,6 +567,7 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
358
567
|
mw.view_3d_manager.plotter.setCursor(
|
|
359
568
|
Qt.CursorShape.ClosedHandCursor
|
|
360
569
|
)
|
|
570
|
+
self._begin_drag_event([int(closest_atom_idx)])
|
|
361
571
|
self._suppress_next_left_button_up = True
|
|
362
572
|
return # Prevent camera rotation
|
|
363
573
|
|
|
@@ -373,21 +583,7 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
373
583
|
"""
|
|
374
584
|
mw = self.main_window
|
|
375
585
|
|
|
376
|
-
|
|
377
|
-
move_group_dialog: Any = None
|
|
378
|
-
try:
|
|
379
|
-
for widget in QApplication.topLevelWidgets():
|
|
380
|
-
if (
|
|
381
|
-
type(widget).__name__
|
|
382
|
-
in ("MoveGroupDialog", "MoveSelectedAtomsDialog")
|
|
383
|
-
and widget.isVisible()
|
|
384
|
-
):
|
|
385
|
-
move_group_dialog = widget
|
|
386
|
-
break
|
|
387
|
-
except (AttributeError, RuntimeError, TypeError) as e:
|
|
388
|
-
logging.debug(
|
|
389
|
-
f"Suppressed exception: {e}"
|
|
390
|
-
) # Suppress non-critical widget search noise
|
|
586
|
+
move_group_dialog = self._find_move_dialog()
|
|
391
587
|
|
|
392
588
|
if move_group_dialog and move_group_dialog.group_atoms:
|
|
393
589
|
# Start rotation drag if group selected
|
|
@@ -398,17 +594,29 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
398
594
|
mw.view_3d_manager.current_mol,
|
|
399
595
|
)
|
|
400
596
|
|
|
401
|
-
|
|
402
|
-
|
|
597
|
+
follow_mouse = False
|
|
598
|
+
try:
|
|
599
|
+
follow_mouse = bool(
|
|
600
|
+
mw.get_settings().get("rotate_group_follow_mouse", False)
|
|
601
|
+
)
|
|
602
|
+
except (AttributeError, RuntimeError, TypeError):
|
|
603
|
+
follow_mouse = False
|
|
604
|
+
|
|
605
|
+
# If follow_mouse is True, require clicking directly on an atom in the group.
|
|
606
|
+
# If follow_mouse is False (default), allow right-clicking anywhere on display.
|
|
607
|
+
if not follow_mouse or (
|
|
403
608
|
clicked_atom_idx is not None
|
|
404
609
|
and clicked_atom_idx in move_group_dialog.group_atoms
|
|
405
610
|
):
|
|
406
611
|
move_group_dialog.is_rotating_group_vtk = True
|
|
407
612
|
move_group_dialog.rotation_start_pos = click_pos
|
|
408
613
|
move_group_dialog.rotation_mouse_moved = False
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
614
|
+
if clicked_atom_idx is not None:
|
|
615
|
+
move_group_dialog.rotation_atom_idx = (
|
|
616
|
+
clicked_atom_idx # Record grabbed atom
|
|
617
|
+
)
|
|
618
|
+
elif hasattr(move_group_dialog, "rotation_atom_idx"):
|
|
619
|
+
delattr(move_group_dialog, "rotation_atom_idx")
|
|
412
620
|
|
|
413
621
|
# Save initial positions and centroid
|
|
414
622
|
move_group_dialog.initial_positions = {}
|
|
@@ -423,6 +631,7 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
423
631
|
move_group_dialog.group_centroid = centroid
|
|
424
632
|
|
|
425
633
|
mw.view_3d_manager.plotter.setCursor(Qt.CursorShape.ClosedHandCursor)
|
|
634
|
+
self._begin_drag_event(move_group_dialog.group_atoms)
|
|
426
635
|
return # Disable camera rotation
|
|
427
636
|
|
|
428
637
|
# Standard right-click
|
|
@@ -446,6 +655,7 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
446
655
|
return
|
|
447
656
|
|
|
448
657
|
if self._is_dragging_atom and not left_held:
|
|
658
|
+
self._abort_active_drag(self.is_dragging)
|
|
449
659
|
self.reset_interactor_state()
|
|
450
660
|
|
|
451
661
|
if not any_held:
|
|
@@ -462,16 +672,20 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
462
672
|
getattr(move_group_dialog, "is_dragging_group_vtk", False)
|
|
463
673
|
and not left_held
|
|
464
674
|
):
|
|
675
|
+
moved = getattr(move_group_dialog, "mouse_moved_vtk", False)
|
|
465
676
|
move_group_dialog.is_dragging_group_vtk = False
|
|
466
677
|
move_group_dialog.drag_start_pos_vtk = None
|
|
467
678
|
move_group_dialog.mouse_moved_vtk = False
|
|
679
|
+
self._abort_active_drag(moved)
|
|
468
680
|
if (
|
|
469
681
|
getattr(move_group_dialog, "is_rotating_group_vtk", False)
|
|
470
682
|
and not right_held
|
|
471
683
|
):
|
|
684
|
+
moved = getattr(move_group_dialog, "rotation_mouse_moved", False)
|
|
472
685
|
move_group_dialog.is_rotating_group_vtk = False
|
|
473
686
|
move_group_dialog.rotation_start_pos = None
|
|
474
687
|
move_group_dialog.rotation_mouse_moved = False
|
|
688
|
+
self._abort_active_drag(moved)
|
|
475
689
|
except (AttributeError, RuntimeError, TypeError):
|
|
476
690
|
# Safe defensive fallback catching AttributeError, RuntimeError, TypeError
|
|
477
691
|
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
@@ -482,25 +696,13 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
482
696
|
"""
|
|
483
697
|
mw = self.main_window
|
|
484
698
|
|
|
485
|
-
|
|
486
|
-
move_group_dialog: Any = None
|
|
487
|
-
try:
|
|
488
|
-
for widget in QApplication.topLevelWidgets():
|
|
489
|
-
if (
|
|
490
|
-
type(widget).__name__
|
|
491
|
-
in ("MoveGroupDialog", "MoveSelectedAtomsDialog")
|
|
492
|
-
and widget.isVisible()
|
|
493
|
-
):
|
|
494
|
-
move_group_dialog = widget
|
|
495
|
-
break
|
|
496
|
-
except (AttributeError, RuntimeError, TypeError):
|
|
497
|
-
logging.warning("Caught exception in " + __file__, exc_info=True)
|
|
699
|
+
move_group_dialog = self._find_move_dialog()
|
|
498
700
|
|
|
499
701
|
self._heal_stuck_pointer_state(move_group_dialog)
|
|
500
702
|
if move_group_dialog and getattr(
|
|
501
703
|
move_group_dialog, "is_dragging_group_vtk", False
|
|
502
704
|
):
|
|
503
|
-
# Dragging group - record offset
|
|
705
|
+
# Dragging group - record offset and update positions in real-time
|
|
504
706
|
interactor = self.GetInteractor()
|
|
505
707
|
current_pos = interactor.GetEventPosition()
|
|
506
708
|
|
|
@@ -513,6 +715,13 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
513
715
|
if abs(dx) > 5 or abs(dy) > 5:
|
|
514
716
|
move_group_dialog.mouse_moved_vtk = True
|
|
515
717
|
|
|
718
|
+
if (
|
|
719
|
+
move_group_dialog.mouse_moved_vtk
|
|
720
|
+
and hasattr(move_group_dialog, "initial_positions")
|
|
721
|
+
and self._should_drag_redraw()
|
|
722
|
+
):
|
|
723
|
+
self._do_realtime_group_translate(mw, move_group_dialog, current_pos)
|
|
724
|
+
|
|
516
725
|
return # Disable camera rotation
|
|
517
726
|
|
|
518
727
|
# Group rotation handling
|
|
@@ -531,6 +740,14 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
531
740
|
if abs(dx) > 5 or abs(dy) > 5:
|
|
532
741
|
move_group_dialog.rotation_mouse_moved = True
|
|
533
742
|
|
|
743
|
+
if (
|
|
744
|
+
move_group_dialog.rotation_mouse_moved
|
|
745
|
+
and hasattr(move_group_dialog, "initial_positions")
|
|
746
|
+
and hasattr(move_group_dialog, "group_centroid")
|
|
747
|
+
and self._should_drag_redraw()
|
|
748
|
+
):
|
|
749
|
+
self._do_realtime_group_rotate(mw, move_group_dialog, current_pos)
|
|
750
|
+
|
|
534
751
|
return # Disable camera rotation
|
|
535
752
|
|
|
536
753
|
interactor = self.GetInteractor()
|
|
@@ -547,6 +764,8 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
547
764
|
if self._is_dragging_atom and mw.dragged_atom_info is not None:
|
|
548
765
|
# Custom atom drag
|
|
549
766
|
self.is_dragging = True
|
|
767
|
+
if self._should_drag_redraw():
|
|
768
|
+
self._do_realtime_atom_drag(mw)
|
|
550
769
|
else:
|
|
551
770
|
# Camera interaction. VTK's built-in Rotate divides mouse motion by
|
|
552
771
|
# the live render size, so rotation slows as the canvas grows. Handle
|
|
@@ -580,25 +799,276 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
580
799
|
else:
|
|
581
800
|
mw.view_3d_manager.plotter.setCursor(Qt.CursorShape.ArrowCursor)
|
|
582
801
|
|
|
802
|
+
def _do_realtime_atom_drag(self, mw: Any) -> None:
|
|
803
|
+
"""Update the dragged atom's position and redraw during mouse-move."""
|
|
804
|
+
try:
|
|
805
|
+
atom_id = mw.dragged_atom_info.get("id") if mw.dragged_atom_info else None
|
|
806
|
+
if atom_id is None:
|
|
807
|
+
return
|
|
808
|
+
if not (
|
|
809
|
+
mw.view_3d_manager.current_mol
|
|
810
|
+
and mw.view_3d_manager.current_mol.GetNumConformers() > 0
|
|
811
|
+
):
|
|
812
|
+
return
|
|
813
|
+
|
|
814
|
+
interactor = self.GetInteractor()
|
|
815
|
+
renderer = mw.view_3d_manager.plotter.renderer
|
|
816
|
+
current_pos = interactor.GetEventPosition()
|
|
817
|
+
conf = mw.view_3d_manager.current_mol.GetConformer()
|
|
818
|
+
pos_3d = conf.GetAtomPosition(atom_id)
|
|
819
|
+
|
|
820
|
+
depth_z = self._world_to_display_depth(
|
|
821
|
+
renderer, pos_3d.x, pos_3d.y, pos_3d.z
|
|
822
|
+
)
|
|
823
|
+
if depth_z is None:
|
|
824
|
+
return
|
|
825
|
+
new_world = self._display_to_world(
|
|
826
|
+
renderer, current_pos[0], current_pos[1], depth_z
|
|
827
|
+
)
|
|
828
|
+
if new_world is None:
|
|
829
|
+
return
|
|
830
|
+
|
|
831
|
+
conf.SetAtomPosition(
|
|
832
|
+
atom_id,
|
|
833
|
+
Geometry.Point3D(
|
|
834
|
+
float(new_world[0]), float(new_world[1]), float(new_world[2])
|
|
835
|
+
),
|
|
836
|
+
)
|
|
837
|
+
if isinstance(
|
|
838
|
+
mw.view_3d_manager.atom_positions_3d, (list, np.ndarray)
|
|
839
|
+
) and atom_id < len(mw.view_3d_manager.atom_positions_3d):
|
|
840
|
+
mw.view_3d_manager.atom_positions_3d[atom_id] = list(new_world)
|
|
841
|
+
|
|
842
|
+
_rt_mol = mw.view_3d_manager.current_mol
|
|
843
|
+
|
|
844
|
+
def _deferred_rt_atom() -> None:
|
|
845
|
+
try:
|
|
846
|
+
mw.view_3d_manager.draw_molecule_3d(_rt_mol)
|
|
847
|
+
except (AttributeError, RuntimeError, ValueError, TypeError):
|
|
848
|
+
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
849
|
+
finally:
|
|
850
|
+
self._finish_drag_redraw()
|
|
851
|
+
|
|
852
|
+
self._drag_redraw_pending = True
|
|
853
|
+
QTimer.singleShot(0, _deferred_rt_atom)
|
|
854
|
+
self._invoke_drag_handlers(
|
|
855
|
+
"move",
|
|
856
|
+
[atom_id],
|
|
857
|
+
{atom_id: new_world},
|
|
858
|
+
)
|
|
859
|
+
except (AttributeError, RuntimeError, TypeError, ValueError):
|
|
860
|
+
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
861
|
+
|
|
862
|
+
def _do_realtime_group_translate(
|
|
863
|
+
self, mw: Any, move_group_dialog: Any, current_pos: Any
|
|
864
|
+
) -> None:
|
|
865
|
+
"""Translate group atoms and redraw during mouse-move."""
|
|
866
|
+
try:
|
|
867
|
+
renderer = mw.view_3d_manager.plotter.renderer
|
|
868
|
+
drag_atom_idx = move_group_dialog.drag_atom_idx_vtk
|
|
869
|
+
drag_initial_pos = move_group_dialog.initial_positions[drag_atom_idx]
|
|
870
|
+
|
|
871
|
+
depth_z = self._world_to_display_depth(
|
|
872
|
+
renderer, drag_initial_pos[0], drag_initial_pos[1], drag_initial_pos[2]
|
|
873
|
+
)
|
|
874
|
+
if depth_z is None:
|
|
875
|
+
return
|
|
876
|
+
new_world = self._display_to_world(
|
|
877
|
+
renderer, current_pos[0], current_pos[1], depth_z
|
|
878
|
+
)
|
|
879
|
+
if new_world is None:
|
|
880
|
+
return
|
|
881
|
+
|
|
882
|
+
conf = mw.view_3d_manager.current_mol.GetConformer()
|
|
883
|
+
translation = np.array(new_world) - drag_initial_pos
|
|
884
|
+
new_positions: Dict[int, Tuple[float, float, float]] = {}
|
|
885
|
+
for atom_idx in move_group_dialog.group_atoms:
|
|
886
|
+
initial_pos = move_group_dialog.initial_positions[atom_idx]
|
|
887
|
+
new_pos = initial_pos + translation
|
|
888
|
+
mw.view_3d_manager.atom_positions_3d[atom_idx] = new_pos
|
|
889
|
+
conf.SetAtomPosition(
|
|
890
|
+
atom_idx,
|
|
891
|
+
Geometry.Point3D(
|
|
892
|
+
float(new_pos[0]), float(new_pos[1]), float(new_pos[2])
|
|
893
|
+
),
|
|
894
|
+
)
|
|
895
|
+
new_positions[atom_idx] = (
|
|
896
|
+
float(new_pos[0]),
|
|
897
|
+
float(new_pos[1]),
|
|
898
|
+
float(new_pos[2]),
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
_rt_mol = mw.view_3d_manager.current_mol
|
|
902
|
+
_rt_dlg = move_group_dialog
|
|
903
|
+
|
|
904
|
+
def _deferred_rt_grp() -> None:
|
|
905
|
+
try:
|
|
906
|
+
mw.view_3d_manager.draw_molecule_3d(_rt_mol)
|
|
907
|
+
_rt_dlg.show_atom_labels()
|
|
908
|
+
except (AttributeError, RuntimeError, ValueError, TypeError):
|
|
909
|
+
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
910
|
+
finally:
|
|
911
|
+
self._finish_drag_redraw()
|
|
912
|
+
|
|
913
|
+
self._drag_redraw_pending = True
|
|
914
|
+
QTimer.singleShot(0, _deferred_rt_grp)
|
|
915
|
+
self._invoke_drag_handlers(
|
|
916
|
+
"move", list(move_group_dialog.group_atoms), new_positions
|
|
917
|
+
)
|
|
918
|
+
except (AttributeError, RuntimeError, TypeError, ValueError):
|
|
919
|
+
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
920
|
+
|
|
921
|
+
def _compute_delta_rotation_matrix(
|
|
922
|
+
self, renderer: Any, start_pos: tuple, current_pos: tuple
|
|
923
|
+
) -> Optional[np.ndarray]:
|
|
924
|
+
"""Compute 3D rotation matrix from screen mouse displacement (dx, dy)."""
|
|
925
|
+
try:
|
|
926
|
+
dx = current_pos[0] - start_pos[0]
|
|
927
|
+
dy = current_pos[1] - start_pos[1]
|
|
928
|
+
if abs(dx) < 1e-3 and abs(dy) < 1e-3:
|
|
929
|
+
return None
|
|
930
|
+
|
|
931
|
+
camera = renderer.GetActiveCamera()
|
|
932
|
+
view_up = np.array(camera.GetViewUp())
|
|
933
|
+
v_dir = np.array(camera.GetDirectionOfProjection())
|
|
934
|
+
right = np.cross(v_dir, view_up)
|
|
935
|
+
|
|
936
|
+
right_norm = np.linalg.norm(right)
|
|
937
|
+
if right_norm > 1e-6:
|
|
938
|
+
right = right / right_norm
|
|
939
|
+
|
|
940
|
+
view_up_norm = np.linalg.norm(view_up)
|
|
941
|
+
if view_up_norm > 1e-6:
|
|
942
|
+
view_up = view_up / view_up_norm
|
|
943
|
+
|
|
944
|
+
speed = _GROUP_ROTATION_RADIANS_PER_PIXEL * self._rotation_sensitivity()
|
|
945
|
+
rot_y = _rodrigues_matrix(view_up, float(dx) * speed)
|
|
946
|
+
rot_x = _rodrigues_matrix(right, -float(dy) * speed)
|
|
947
|
+
return rot_x @ rot_y
|
|
948
|
+
except (AttributeError, RuntimeError, TypeError, ValueError):
|
|
949
|
+
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
950
|
+
return None
|
|
951
|
+
|
|
952
|
+
def _get_group_rotation_matrix(
|
|
953
|
+
self, mw: Any, move_group_dialog: Any, current_pos: Any
|
|
954
|
+
) -> Optional[np.ndarray]:
|
|
955
|
+
"""Compute group rotation matrix using delta or follow-mouse method."""
|
|
956
|
+
try:
|
|
957
|
+
renderer = mw.view_3d_manager.plotter.renderer
|
|
958
|
+
centroid = move_group_dialog.group_centroid
|
|
959
|
+
start_pos = getattr(move_group_dialog, "rotation_start_pos", None)
|
|
960
|
+
if start_pos is None:
|
|
961
|
+
return None
|
|
962
|
+
|
|
963
|
+
follow_mouse = bool(
|
|
964
|
+
mw.get_settings().get("rotate_group_follow_mouse", False)
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
if follow_mouse:
|
|
968
|
+
if not hasattr(move_group_dialog, "rotation_atom_idx"):
|
|
969
|
+
move_group_dialog.rotation_atom_idx = next(
|
|
970
|
+
iter(move_group_dialog.group_atoms)
|
|
971
|
+
)
|
|
972
|
+
grabbed_atom_idx = move_group_dialog.rotation_atom_idx
|
|
973
|
+
grabbed_initial_pos = move_group_dialog.initial_positions[
|
|
974
|
+
grabbed_atom_idx
|
|
975
|
+
]
|
|
976
|
+
|
|
977
|
+
depth_z = self._world_to_display_depth(
|
|
978
|
+
renderer,
|
|
979
|
+
grabbed_initial_pos[0],
|
|
980
|
+
grabbed_initial_pos[1],
|
|
981
|
+
grabbed_initial_pos[2],
|
|
982
|
+
)
|
|
983
|
+
if depth_z is None:
|
|
984
|
+
return None
|
|
985
|
+
target_world = self._display_to_world(
|
|
986
|
+
renderer, current_pos[0], current_pos[1], depth_z
|
|
987
|
+
)
|
|
988
|
+
if target_world is None:
|
|
989
|
+
return None
|
|
990
|
+
target_pos = np.array(target_world)
|
|
991
|
+
|
|
992
|
+
v1 = grabbed_initial_pos - centroid
|
|
993
|
+
v2 = target_pos - centroid
|
|
994
|
+
v1_norm = np.linalg.norm(v1)
|
|
995
|
+
v2_norm = np.linalg.norm(v2)
|
|
996
|
+
if v1_norm < 1e-6 or v2_norm < 1e-6:
|
|
997
|
+
return None
|
|
998
|
+
|
|
999
|
+
v1_n = v1 / v1_norm
|
|
1000
|
+
v2_n = v2 / v2_norm
|
|
1001
|
+
rotation_axis = np.cross(v1_n, v2_n)
|
|
1002
|
+
axis_norm = np.linalg.norm(rotation_axis)
|
|
1003
|
+
if axis_norm < 1e-6:
|
|
1004
|
+
return None
|
|
1005
|
+
rotation_axis /= axis_norm
|
|
1006
|
+
|
|
1007
|
+
cos_angle = float(np.clip(np.dot(v1_n, v2_n), -1.0, 1.0))
|
|
1008
|
+
return _rodrigues_matrix(rotation_axis, float(np.arccos(cos_angle)))
|
|
1009
|
+
return self._compute_delta_rotation_matrix(renderer, start_pos, current_pos)
|
|
1010
|
+
except (AttributeError, RuntimeError, TypeError, ValueError):
|
|
1011
|
+
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
1012
|
+
return None
|
|
1013
|
+
|
|
1014
|
+
def _do_realtime_group_rotate(
|
|
1015
|
+
self, mw: Any, move_group_dialog: Any, current_pos: Any
|
|
1016
|
+
) -> None:
|
|
1017
|
+
"""Rotate group atoms around their centroid and redraw during mouse-move."""
|
|
1018
|
+
try:
|
|
1019
|
+
rot_matrix = self._get_group_rotation_matrix(
|
|
1020
|
+
mw, move_group_dialog, current_pos
|
|
1021
|
+
)
|
|
1022
|
+
if rot_matrix is None:
|
|
1023
|
+
return
|
|
1024
|
+
|
|
1025
|
+
centroid = move_group_dialog.group_centroid
|
|
1026
|
+
|
|
1027
|
+
conf = mw.view_3d_manager.current_mol.GetConformer()
|
|
1028
|
+
new_positions: Dict[int, Tuple[float, float, float]] = {}
|
|
1029
|
+
for atom_idx in move_group_dialog.group_atoms:
|
|
1030
|
+
initial_pos = move_group_dialog.initial_positions[atom_idx]
|
|
1031
|
+
new_pos = rot_matrix @ (initial_pos - centroid) + centroid
|
|
1032
|
+
mw.view_3d_manager.atom_positions_3d[atom_idx] = new_pos
|
|
1033
|
+
conf.SetAtomPosition(
|
|
1034
|
+
atom_idx,
|
|
1035
|
+
Geometry.Point3D(
|
|
1036
|
+
float(new_pos[0]), float(new_pos[1]), float(new_pos[2])
|
|
1037
|
+
),
|
|
1038
|
+
)
|
|
1039
|
+
new_positions[atom_idx] = (
|
|
1040
|
+
float(new_pos[0]),
|
|
1041
|
+
float(new_pos[1]),
|
|
1042
|
+
float(new_pos[2]),
|
|
1043
|
+
)
|
|
1044
|
+
|
|
1045
|
+
_rt_mol = mw.view_3d_manager.current_mol
|
|
1046
|
+
_rt_dlg = move_group_dialog
|
|
1047
|
+
|
|
1048
|
+
def _deferred_rt_rot() -> None:
|
|
1049
|
+
try:
|
|
1050
|
+
mw.view_3d_manager.draw_molecule_3d(_rt_mol)
|
|
1051
|
+
_rt_dlg.show_atom_labels()
|
|
1052
|
+
except (AttributeError, RuntimeError, ValueError, TypeError):
|
|
1053
|
+
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
1054
|
+
finally:
|
|
1055
|
+
self._finish_drag_redraw()
|
|
1056
|
+
|
|
1057
|
+
self._drag_redraw_pending = True
|
|
1058
|
+
QTimer.singleShot(0, _deferred_rt_rot)
|
|
1059
|
+
self._invoke_drag_handlers(
|
|
1060
|
+
"move", list(move_group_dialog.group_atoms), new_positions
|
|
1061
|
+
)
|
|
1062
|
+
except (AttributeError, RuntimeError, TypeError, ValueError):
|
|
1063
|
+
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
1064
|
+
|
|
583
1065
|
def on_left_button_up(self, obj: Any, event: Any) -> None:
|
|
584
1066
|
"""
|
|
585
1067
|
Handle click release and reset state.
|
|
586
1068
|
"""
|
|
587
1069
|
mw = self.main_window
|
|
588
1070
|
|
|
589
|
-
|
|
590
|
-
move_group_dialog: Any = None
|
|
591
|
-
try:
|
|
592
|
-
for widget in QApplication.topLevelWidgets():
|
|
593
|
-
if (
|
|
594
|
-
type(widget).__name__
|
|
595
|
-
in ("MoveGroupDialog", "MoveSelectedAtomsDialog")
|
|
596
|
-
and widget.isVisible()
|
|
597
|
-
):
|
|
598
|
-
move_group_dialog = widget
|
|
599
|
-
break
|
|
600
|
-
except (AttributeError, RuntimeError, TypeError):
|
|
601
|
-
logging.warning("Caught exception in " + __file__, exc_info=True)
|
|
1071
|
+
move_group_dialog = self._find_move_dialog()
|
|
602
1072
|
# Prevent multi-click issues
|
|
603
1073
|
if move_group_dialog:
|
|
604
1074
|
if getattr(
|
|
@@ -699,6 +1169,7 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
699
1169
|
move_group_dialog.on_atom_picked(clicked_atom)
|
|
700
1170
|
except (AttributeError, RuntimeError, TypeError, ValueError):
|
|
701
1171
|
logging.warning("Error in toggle", exc_info=True)
|
|
1172
|
+
self._end_drag_event()
|
|
702
1173
|
|
|
703
1174
|
# Background click: deselect Move Group
|
|
704
1175
|
if move_group_dialog and not getattr(
|
|
@@ -844,6 +1315,8 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
844
1315
|
mw.edit_actions_manager.push_undo_state()
|
|
845
1316
|
|
|
846
1317
|
QTimer.singleShot(0, _deferred_atom_redraw)
|
|
1318
|
+
|
|
1319
|
+
self._end_drag_event()
|
|
847
1320
|
mw.dragged_atom_info = None
|
|
848
1321
|
self._stop_vtk_left_button_state()
|
|
849
1322
|
|
|
@@ -911,19 +1384,7 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
911
1384
|
"""
|
|
912
1385
|
mw = self.main_window
|
|
913
1386
|
|
|
914
|
-
|
|
915
|
-
move_group_dialog: Any = None
|
|
916
|
-
try:
|
|
917
|
-
for widget in QApplication.topLevelWidgets():
|
|
918
|
-
if (
|
|
919
|
-
type(widget).__name__
|
|
920
|
-
in ("MoveGroupDialog", "MoveSelectedAtomsDialog")
|
|
921
|
-
and widget.isVisible()
|
|
922
|
-
):
|
|
923
|
-
move_group_dialog = widget
|
|
924
|
-
break
|
|
925
|
-
except (AttributeError, RuntimeError, TypeError):
|
|
926
|
-
logging.warning("Caught exception in " + __file__, exc_info=True)
|
|
1387
|
+
move_group_dialog = self._find_move_dialog()
|
|
927
1388
|
if move_group_dialog and getattr(
|
|
928
1389
|
move_group_dialog, "is_rotating_group_vtk", False
|
|
929
1390
|
):
|
|
@@ -932,113 +1393,39 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
932
1393
|
# Apply rotation on release if moved
|
|
933
1394
|
try:
|
|
934
1395
|
interactor = self.GetInteractor()
|
|
935
|
-
renderer = mw.view_3d_manager.plotter.renderer
|
|
936
1396
|
current_pos = interactor.GetEventPosition()
|
|
937
1397
|
conf = mw.view_3d_manager.current_mol.GetConformer()
|
|
938
1398
|
centroid = move_group_dialog.group_centroid
|
|
939
1399
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
move_group_dialog.rotation_atom_idx = next(
|
|
943
|
-
iter(move_group_dialog.group_atoms)
|
|
944
|
-
)
|
|
945
|
-
|
|
946
|
-
grabbed_atom_idx = move_group_dialog.rotation_atom_idx
|
|
947
|
-
grabbed_initial_pos = move_group_dialog.initial_positions[
|
|
948
|
-
grabbed_atom_idx
|
|
949
|
-
]
|
|
950
|
-
|
|
951
|
-
# Get start screen coordinates
|
|
952
|
-
renderer.SetWorldPoint(
|
|
953
|
-
grabbed_initial_pos[0],
|
|
954
|
-
grabbed_initial_pos[1],
|
|
955
|
-
grabbed_initial_pos[2],
|
|
956
|
-
1.0,
|
|
957
|
-
)
|
|
958
|
-
renderer.WorldToDisplay()
|
|
959
|
-
start_display = renderer.GetDisplayPoint()
|
|
960
|
-
|
|
961
|
-
# Convert current mouse pos to world (same depth)
|
|
962
|
-
renderer.SetDisplayPoint(
|
|
963
|
-
current_pos[0], current_pos[1], start_display[2]
|
|
964
|
-
)
|
|
965
|
-
renderer.DisplayToWorld()
|
|
966
|
-
target_world = renderer.GetWorldPoint()
|
|
967
|
-
target_pos = np.array(
|
|
968
|
-
[target_world[0], target_world[1], target_world[2]]
|
|
1400
|
+
rot_matrix = self._get_group_rotation_matrix(
|
|
1401
|
+
mw, move_group_dialog, current_pos
|
|
969
1402
|
)
|
|
1403
|
+
if rot_matrix is not None:
|
|
1404
|
+
for atom_idx in move_group_dialog.group_atoms:
|
|
1405
|
+
initial_pos = move_group_dialog.initial_positions[atom_idx]
|
|
1406
|
+
relative_pos = initial_pos - centroid
|
|
1407
|
+
rotated_pos = rot_matrix @ relative_pos
|
|
1408
|
+
new_pos = rotated_pos + centroid
|
|
970
1409
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
if v1_norm > 1e-6 and v2_norm > 1e-6:
|
|
980
|
-
v1_normalized = v1 / v1_norm
|
|
981
|
-
v2_normalized = v2 / v2_norm
|
|
982
|
-
|
|
983
|
-
# Rotation axis
|
|
984
|
-
rotation_axis = np.cross(v1_normalized, v2_normalized)
|
|
985
|
-
axis_norm = np.linalg.norm(rotation_axis)
|
|
986
|
-
|
|
987
|
-
if axis_norm > 1e-6:
|
|
988
|
-
rotation_axis = rotation_axis / axis_norm
|
|
989
|
-
|
|
990
|
-
# Rotation angle
|
|
991
|
-
cos_angle = np.clip(
|
|
992
|
-
np.dot(v1_normalized, v2_normalized), -1.0, 1.0
|
|
993
|
-
)
|
|
994
|
-
angle = np.arccos(cos_angle)
|
|
995
|
-
|
|
996
|
-
# Create rotation matrix
|
|
997
|
-
K = np.array(
|
|
998
|
-
[
|
|
999
|
-
[0, -rotation_axis[2], rotation_axis[1]],
|
|
1000
|
-
[rotation_axis[2], 0, -rotation_axis[0]],
|
|
1001
|
-
[-rotation_axis[1], rotation_axis[0], 0],
|
|
1002
|
-
]
|
|
1003
|
-
)
|
|
1004
|
-
|
|
1005
|
-
rot_matrix = (
|
|
1006
|
-
np.eye(3)
|
|
1007
|
-
+ np.sin(angle) * K
|
|
1008
|
-
+ (1 - np.cos(angle)) * (K @ K)
|
|
1410
|
+
conf.SetAtomPosition(
|
|
1411
|
+
atom_idx,
|
|
1412
|
+
Geometry.Point3D(
|
|
1413
|
+
float(new_pos[0]),
|
|
1414
|
+
float(new_pos[1]),
|
|
1415
|
+
float(new_pos[2]),
|
|
1416
|
+
),
|
|
1009
1417
|
)
|
|
1418
|
+
mw.view_3d_manager.atom_positions_3d[atom_idx] = new_pos
|
|
1010
1419
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
relative_pos = initial_pos - centroid
|
|
1018
|
-
# Apply rotation
|
|
1019
|
-
rotated_pos = rot_matrix @ relative_pos
|
|
1020
|
-
# Restore absolute position
|
|
1021
|
-
new_pos = rotated_pos + centroid
|
|
1022
|
-
|
|
1023
|
-
conf.SetAtomPosition(
|
|
1024
|
-
atom_idx,
|
|
1025
|
-
Geometry.Point3D(
|
|
1026
|
-
float(new_pos[0]),
|
|
1027
|
-
float(new_pos[1]),
|
|
1028
|
-
float(new_pos[2]),
|
|
1029
|
-
),
|
|
1030
|
-
)
|
|
1031
|
-
mw.view_3d_manager.atom_positions_3d[atom_idx] = new_pos
|
|
1032
|
-
|
|
1033
|
-
# Update 3D display
|
|
1034
|
-
mw.view_3d_manager.draw_molecule_3d(
|
|
1035
|
-
mw.view_3d_manager.current_mol
|
|
1036
|
-
)
|
|
1037
|
-
mw.view_3d_manager.update_chiral_labels()
|
|
1038
|
-
move_group_dialog.show_atom_labels()
|
|
1039
|
-
mw.edit_actions_manager.push_undo_state()
|
|
1420
|
+
mw.view_3d_manager.draw_molecule_3d(
|
|
1421
|
+
mw.view_3d_manager.current_mol
|
|
1422
|
+
)
|
|
1423
|
+
mw.view_3d_manager.update_chiral_labels()
|
|
1424
|
+
move_group_dialog.show_atom_labels()
|
|
1425
|
+
mw.edit_actions_manager.push_undo_state()
|
|
1040
1426
|
except (AttributeError, RuntimeError, TypeError, ValueError):
|
|
1041
1427
|
logging.warning("Error finalizing group rotation", exc_info=True)
|
|
1428
|
+
self._end_drag_event()
|
|
1042
1429
|
|
|
1043
1430
|
# Reset state
|
|
1044
1431
|
move_group_dialog.is_rotating_group_vtk = False
|
|
@@ -53,11 +53,6 @@ class MoveGroupDialog(BasePickingDialog):
|
|
|
53
53
|
self.selected_atoms: set[int] = set()
|
|
54
54
|
self.group_atoms: set[int] = set() # All atoms connected to selected atoms
|
|
55
55
|
|
|
56
|
-
# Add preselected atoms
|
|
57
|
-
if preselected_atoms:
|
|
58
|
-
# For MoveGroup, we pick the first atom and select its connected group
|
|
59
|
-
self.on_atom_picked(preselected_atoms[0])
|
|
60
|
-
|
|
61
56
|
self.clicked_atom_for_toggle: Optional[int] = None
|
|
62
57
|
# State for group movement (used by CustomInteractorStyle)
|
|
63
58
|
self.initial_positions: dict = {}
|
|
@@ -82,6 +77,11 @@ class MoveGroupDialog(BasePickingDialog):
|
|
|
82
77
|
|
|
83
78
|
self.init_ui()
|
|
84
79
|
|
|
80
|
+
# After init_ui: picking an atom highlights it and updates the labels,
|
|
81
|
+
# which needs both the actor state and the widgets to exist.
|
|
82
|
+
if preselected_atoms:
|
|
83
|
+
self.on_atom_picked(preselected_atoms[0])
|
|
84
|
+
|
|
85
85
|
def init_ui(self) -> None:
|
|
86
86
|
"""Build the move-group dialog with atom picker, translate/rotate inputs, and controls."""
|
|
87
87
|
self.setWindowTitle("Move Group")
|
|
@@ -37,6 +37,8 @@ class Settings3DSceneTab(SettingsTabBase):
|
|
|
37
37
|
self.bg_button: Any = None
|
|
38
38
|
self.light_checkbox: Any = None
|
|
39
39
|
self.projection_combo: Any = None
|
|
40
|
+
self.realtime_drag_checkbox: Any = None
|
|
41
|
+
self.rotate_group_follow_mouse_checkbox: Any = None
|
|
40
42
|
self.current_bg_color = default_settings["background_color"]
|
|
41
43
|
self._setup_ui()
|
|
42
44
|
|
|
@@ -88,6 +90,24 @@ class Settings3DSceneTab(SettingsTabBase):
|
|
|
88
90
|
self._wrap_layout(self.rotation_sens_slider, self.rotation_sens_label),
|
|
89
91
|
)
|
|
90
92
|
|
|
93
|
+
self.realtime_drag_checkbox = QCheckBox()
|
|
94
|
+
self.realtime_drag_checkbox.setToolTip(
|
|
95
|
+
"Update the structure continuously while dragging an atom or group.\n"
|
|
96
|
+
"When off, the move is applied only on mouse release.\n"
|
|
97
|
+
"Structures larger than 300 atoms always use release-only updates."
|
|
98
|
+
)
|
|
99
|
+
form_layout.addRow("Real-time 3D Drag:", self.realtime_drag_checkbox)
|
|
100
|
+
|
|
101
|
+
self.rotate_group_follow_mouse_checkbox = QCheckBox()
|
|
102
|
+
self.rotate_group_follow_mouse_checkbox.setToolTip(
|
|
103
|
+
"On: right-drag rotation must start on an atom of the group,\n"
|
|
104
|
+
"and that atom follows the cursor.\n"
|
|
105
|
+
"Off: right-dragging anywhere on the 3D view rotates the group."
|
|
106
|
+
)
|
|
107
|
+
form_layout.addRow(
|
|
108
|
+
"Rotate Groups: Follow Mouse:", self.rotate_group_follow_mouse_checkbox
|
|
109
|
+
)
|
|
110
|
+
|
|
91
111
|
def _select_color(self) -> None:
|
|
92
112
|
color = QColorDialog.getColor(QColor(self.current_bg_color), self)
|
|
93
113
|
if color.isValid():
|
|
@@ -123,6 +143,12 @@ class Settings3DSceneTab(SettingsTabBase):
|
|
|
123
143
|
|
|
124
144
|
sens_val = settings_dict.get("mouse_rotation_sensitivity", 1.0)
|
|
125
145
|
self.rotation_sens_slider.setValue(int(sens_val * 100))
|
|
146
|
+
self.realtime_drag_checkbox.setChecked(
|
|
147
|
+
settings_dict.get("realtime_3d_drag", True)
|
|
148
|
+
)
|
|
149
|
+
self.rotate_group_follow_mouse_checkbox.setChecked(
|
|
150
|
+
settings_dict.get("rotate_group_follow_mouse", False)
|
|
151
|
+
)
|
|
126
152
|
|
|
127
153
|
def get_settings(self) -> dict[str, Any]:
|
|
128
154
|
return {
|
|
@@ -134,6 +160,8 @@ class Settings3DSceneTab(SettingsTabBase):
|
|
|
134
160
|
"specular_power": self.spec_power_slider.value(),
|
|
135
161
|
"projection_mode": self.projection_combo.currentText(),
|
|
136
162
|
"mouse_rotation_sensitivity": self.rotation_sens_slider.value() / 100.0,
|
|
163
|
+
"realtime_3d_drag": self.realtime_drag_checkbox.isChecked(),
|
|
164
|
+
"rotate_group_follow_mouse": self.rotate_group_follow_mouse_checkbox.isChecked(),
|
|
137
165
|
}
|
|
138
166
|
|
|
139
167
|
|
|
@@ -17,6 +17,9 @@ import os
|
|
|
17
17
|
from PyQt6.QtGui import QColor, QFont
|
|
18
18
|
from rdkit import Chem
|
|
19
19
|
|
|
20
|
+
# Dialog classes that drive group drag / rotate gestures in the 3D view.
|
|
21
|
+
MOVE_DIALOG_TYPES = ("MoveGroupDialog", "MoveSelectedAtomsDialog")
|
|
22
|
+
|
|
20
23
|
|
|
21
24
|
def _get_version() -> str:
|
|
22
25
|
try:
|
|
@@ -21,6 +21,8 @@ DEFAULT_SETTINGS = {
|
|
|
21
21
|
"show_3d_axes": True,
|
|
22
22
|
# Camera rotation speed multiplier for the 3D scene (window-size independent).
|
|
23
23
|
"mouse_rotation_sensitivity": 1.0,
|
|
24
|
+
"realtime_3d_drag": True,
|
|
25
|
+
"rotate_group_follow_mouse": False,
|
|
24
26
|
# --- 3D Model Parameters (Ball and Stick) ---
|
|
25
27
|
"ball_stick_atom_scale": 1.0,
|
|
26
28
|
"ball_stick_bond_radius": 0.1,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy-linux
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.5.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
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -9,8 +9,8 @@ moleditpy_linux/core/__init__.py,sha256=BEOaCz93HzgTe0FhBzSj6Sfk8MAWxcY9vCDGmOri
|
|
|
9
9
|
moleditpy_linux/core/mol_geometry.py,sha256=PzdJGxWs6vXwVyILBG6-6qhiVYuB33YTr8lcHRJVLNE,23933
|
|
10
10
|
moleditpy_linux/core/molecular_data.py,sha256=CboEOZMjtbyRSQ7-f6kj0jngUM0Tw-7L6lU9eDtdycQ,18606
|
|
11
11
|
moleditpy_linux/plugins/__init__.py,sha256=BEOaCz93HzgTe0FhBzSj6Sfk8MAWxcY9vCDGmOriJx4,267
|
|
12
|
-
moleditpy_linux/plugins/plugin_interface.py,sha256=
|
|
13
|
-
moleditpy_linux/plugins/plugin_manager.py,sha256=
|
|
12
|
+
moleditpy_linux/plugins/plugin_interface.py,sha256=shBuVwQu3cQxhhM5M82EkQWMt19RcOxyXZ6CvkjXHtg,22981
|
|
13
|
+
moleditpy_linux/plugins/plugin_manager.py,sha256=NzDKllVYLNaFDyV_rEDxtMyKfMWl4LFHfEKnEcOUtcA,34390
|
|
14
14
|
moleditpy_linux/plugins/plugin_manager_window.py,sha256=M3JJhMWLYQ2jRZcLiPj2Ho5xXqwRwT_sb5fJaaqjr7U,13375
|
|
15
15
|
moleditpy_linux/ui/__init__.py,sha256=_BslFbhSM4Ba4GDzH1XpvlqlGgvi4OFkcuU3pPeIz3s,751
|
|
16
16
|
moleditpy_linux/ui/about_dialog.py,sha256=saCFGcJXoWTIwWkH3iwIPy9dRy-zlgVUKfN6x2E1S2Y,4552
|
|
@@ -19,7 +19,7 @@ moleditpy_linux/ui/alignment_dialog.py,sha256=21-RCNXHwdeD9ARQ1GZxdOS0wTeYwzQmFm
|
|
|
19
19
|
moleditpy_linux/ui/analysis_window.py,sha256=tAsm8GLPjXGuMupM3uHbcaMVuJ3X_MZNB8yBcjECcyk,9800
|
|
20
20
|
moleditpy_linux/ui/angle_dialog.py,sha256=Wgyz3VYTVJZYILxCuj_0NGtKBnq47sSg0IwlXqFRNK8,18842
|
|
21
21
|
moleditpy_linux/ui/app_state.py,sha256=4Q1Lygj_1DbG83prV1I3eLbuNWtF3_19SGpVHhCxOxE,32442
|
|
22
|
-
moleditpy_linux/ui/atom_item.py,sha256=
|
|
22
|
+
moleditpy_linux/ui/atom_item.py,sha256=vWBINnyhN8w8GzEflibfQ268wVQJerqE0rHA-HEUGlQ,22551
|
|
23
23
|
moleditpy_linux/ui/atom_picking.py,sha256=nbdFdcaEOgLQtqZXYoKQUvFyVHagPKlms96chw00m2Y,10566
|
|
24
24
|
moleditpy_linux/ui/base_picking_dialog.py,sha256=F4XAx7Rrk6pZG7KECrJxNghtz4sB1JIaOd2mDsz2BfU,5975
|
|
25
25
|
moleditpy_linux/ui/bond_item.py,sha256=rVjORuGLp_Cxhmw7ARTE83QM614VSIDA5LVcIYpWWc0,25995
|
|
@@ -28,7 +28,7 @@ moleditpy_linux/ui/calculation_worker.py,sha256=bwrzCW5935Twe5EY7Mtd35YQaWJILvBP
|
|
|
28
28
|
moleditpy_linux/ui/color_settings_dialog.py,sha256=RFp8h0R1yC8YwqOxc972Da9xO95Gu-0qywAAEeXMj4E,14224
|
|
29
29
|
moleditpy_linux/ui/compute_logic.py,sha256=OBEJvw4atAaEXIv4eueoilIjTF1sxvJQGQB4ABOthYA,31462
|
|
30
30
|
moleditpy_linux/ui/constrained_optimization_dialog.py,sha256=rc3YK6DOJdIbhriwsM2jOAW9j_-3PvFz1XtFHpGw4E4,36346
|
|
31
|
-
moleditpy_linux/ui/custom_interactor_style.py,sha256=
|
|
31
|
+
moleditpy_linux/ui/custom_interactor_style.py,sha256=ut4p-S4iUr8lTVp2T1m_-d7l9pSUCoOdzk08qssECn8,65499
|
|
32
32
|
moleditpy_linux/ui/custom_qt_interactor.py,sha256=76L0Jpbg09VXzHOzJs9kG260Vs9qcwbt8Chn0iOopDg,2820
|
|
33
33
|
moleditpy_linux/ui/dialog_3d_picking_mixin.py,sha256=nsZQ27PYZrO9s_w5EXxxluVfOOmNKcv0vvm89IWN7mU,10604
|
|
34
34
|
moleditpy_linux/ui/dialog_logic.py,sha256=KCz3ZPJap4K_tiNDjCzGqQHYxdEsvcUG6Lws2tDhvx4,19617
|
|
@@ -43,7 +43,7 @@ moleditpy_linux/ui/main_window_init.py,sha256=94_Sf_E5ZTtkyQvSIURWXbuXxabg46L0Ax
|
|
|
43
43
|
moleditpy_linux/ui/mirror_dialog.py,sha256=Tsc2O9Ktjz0zzJWB_YkVgkqgDhytQ7ksN_PwpcY9vJI,5169
|
|
44
44
|
moleditpy_linux/ui/molecular_scene_handler.py,sha256=p_OhYC965qden-aFQVifNTcMMi_Ikx8TzCwH9XgWR3A,70110
|
|
45
45
|
moleditpy_linux/ui/molecule_scene.py,sha256=ZUZBaCyWaNEAITHI4JmJ__gQcI9kgttj8qBP6dgF6LU,44090
|
|
46
|
-
moleditpy_linux/ui/move_group_dialog.py,sha256=
|
|
46
|
+
moleditpy_linux/ui/move_group_dialog.py,sha256=elgrjcSh3uP6sPc5WLgDI9EvtpiGPcutaKkmNJg-taM,27096
|
|
47
47
|
moleditpy_linux/ui/move_selected_atoms_dialog.py,sha256=7_Krz69LDx71_pBr9_bP55UhT97SJZU-W6OEN59waDg,29889
|
|
48
48
|
moleditpy_linux/ui/periodic_table_dialog.py,sha256=yadE1bbJaObAiIfpKwANqiDOBI8R3yOHvDTZYrw1J4Q,6134
|
|
49
49
|
moleditpy_linux/ui/planarize_dialog.py,sha256=PvEXAx27iO3d9OWrr0kL4WTeNI2eMIx-FRZaFWW-cao,7936
|
|
@@ -59,18 +59,18 @@ moleditpy_linux/ui/view_3d_logic.py,sha256=fuXQKAQFH8e_Xm6e8mG2y4EN2EnBqFxJRqslo
|
|
|
59
59
|
moleditpy_linux/ui/zoomable_view.py,sha256=Kpr9SaIYAKH-Fexm5-gi3cswsiT5V7g7yTk7acutZjA,6239
|
|
60
60
|
moleditpy_linux/ui/settings_tabs/__init__.py,sha256=BEOaCz93HzgTe0FhBzSj6Sfk8MAWxcY9vCDGmOriJx4,267
|
|
61
61
|
moleditpy_linux/ui/settings_tabs/settings_2d_tab.py,sha256=GbiRYuf1Jem7TsdXL9Kcdp_SNx4vuBG6f5_PanJgBU0,15294
|
|
62
|
-
moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py,sha256=
|
|
62
|
+
moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py,sha256=GqHS8Ni17fPyPAS1ElnLr4Q5Fr2zDjsU2qA8VHe-fP0,13910
|
|
63
63
|
moleditpy_linux/ui/settings_tabs/settings_other_tab.py,sha256=gCg9ambXvN9WCYWaH5qDgbQzvom02Vscrbp0Pt4NrFM,7295
|
|
64
64
|
moleditpy_linux/ui/settings_tabs/settings_tab_base.py,sha256=c7nX6I0-8mgemI9q9cVSwDWtJSOU36laEfNiq7hOprE,3619
|
|
65
65
|
moleditpy_linux/utils/__init__.py,sha256=BEOaCz93HzgTe0FhBzSj6Sfk8MAWxcY9vCDGmOriJx4,267
|
|
66
|
-
moleditpy_linux/utils/constants.py,sha256=
|
|
67
|
-
moleditpy_linux/utils/default_settings.py,sha256=
|
|
66
|
+
moleditpy_linux/utils/constants.py,sha256=73UzGo1aJBp_jDulb_1F2TUz6YTcI_4_wWuOV3qnexM,8016
|
|
67
|
+
moleditpy_linux/utils/default_settings.py,sha256=c3FfGWpaCvUYLJ57fdMhWyqGEeaYEbClPoVKZ9LPxn8,3090
|
|
68
68
|
moleditpy_linux/utils/sip_isdeleted_safe.py,sha256=aXoW_PDHAsu0SkqcvkUZAsWDhl9Ka8-VnzgNE5ub41Y,1215
|
|
69
69
|
moleditpy_linux/utils/suppress_log.py,sha256=EFkN79q6QfYXAuii8g1fBuvFJrnVsio3aFTKMvAmXW4,928
|
|
70
70
|
moleditpy_linux/utils/system_utils.py,sha256=j1RAiiKXg3N-rjpTh7yxKVAYED_kUJy4aVrY-9Hh0Mc,2674
|
|
71
|
-
moleditpy_linux-4.
|
|
72
|
-
moleditpy_linux-4.
|
|
73
|
-
moleditpy_linux-4.
|
|
74
|
-
moleditpy_linux-4.
|
|
75
|
-
moleditpy_linux-4.
|
|
76
|
-
moleditpy_linux-4.
|
|
71
|
+
moleditpy_linux-4.5.0.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
72
|
+
moleditpy_linux-4.5.0.dist-info/METADATA,sha256=K2BAcuLvCky-mQGYbzrNRCgpOhn5Zh_CUjmyc_Emmas,65353
|
|
73
|
+
moleditpy_linux-4.5.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
74
|
+
moleditpy_linux-4.5.0.dist-info/entry_points.txt,sha256=-OzipSi__yVwlimNtu3eiRP5t5UMg55Cs0udyhXYiyw,60
|
|
75
|
+
moleditpy_linux-4.5.0.dist-info/top_level.txt,sha256=qyqe-hDYL6CXyin9E5Me5rVl3PG84VqiOjf9bQvfJLs,16
|
|
76
|
+
moleditpy_linux-4.5.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|