MoleditPy 3.3.1__tar.gz → 3.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. {moleditpy-3.3.1 → moleditpy-3.4.1}/PKG-INFO +3 -2
  2. {moleditpy-3.3.1 → moleditpy-3.4.1}/README.md +1 -0
  3. {moleditpy-3.3.1 → moleditpy-3.4.1}/pyproject.toml +2 -2
  4. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/MoleditPy.egg-info/PKG-INFO +3 -2
  5. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/MoleditPy.egg-info/requires.txt +1 -1
  6. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/__init__.py +1 -1
  7. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/main.py +2 -1
  8. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/angle_dialog.py +47 -22
  9. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/app_state.py +7 -9
  10. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/atom_item.py +77 -27
  11. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/base_picking_dialog.py +19 -5
  12. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/bond_item.py +93 -16
  13. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/bond_length_dialog.py +25 -10
  14. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/calculation_worker.py +5 -3
  15. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/color_settings_dialog.py +12 -12
  16. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/compute_logic.py +9 -9
  17. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/constrained_optimization_dialog.py +4 -4
  18. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/custom_interactor_style.py +6 -6
  19. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/dialog_logic.py +2 -2
  20. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/dihedral_dialog.py +31 -17
  21. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/edit_3d_logic.py +4 -4
  22. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/edit_actions_logic.py +12 -12
  23. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/export_logic.py +9 -2
  24. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/geometry_base_dialog.py +2 -2
  25. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/io_logic.py +6 -6
  26. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/main_window_init.py +7 -7
  27. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/molecular_scene_handler.py +10 -10
  28. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/molecule_scene.py +8 -8
  29. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/move_group_dialog.py +3 -5
  30. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/planarize_dialog.py +1 -0
  31. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/settings_dialog.py +7 -7
  32. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/ui_manager.py +10 -10
  33. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/user_template_dialog.py +3 -3
  34. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/view_3d_logic.py +3 -3
  35. {moleditpy-3.3.1 → moleditpy-3.4.1}/LICENSE +0 -0
  36. {moleditpy-3.3.1 → moleditpy-3.4.1}/setup.cfg +0 -0
  37. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/MoleditPy.egg-info/SOURCES.txt +0 -0
  38. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
  39. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/MoleditPy.egg-info/entry_points.txt +0 -0
  40. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/MoleditPy.egg-info/top_level.txt +0 -0
  41. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/__main__.py +0 -0
  42. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/assets/file_icon.ico +0 -0
  43. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/assets/icon.icns +0 -0
  44. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/assets/icon.ico +0 -0
  45. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/assets/icon.png +0 -0
  46. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/core/__init__.py +0 -0
  47. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/core/mol_geometry.py +0 -0
  48. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/core/molecular_data.py +0 -0
  49. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/plugins/__init__.py +0 -0
  50. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/plugins/plugin_interface.py +0 -0
  51. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/plugins/plugin_manager.py +0 -0
  52. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/plugins/plugin_manager_window.py +0 -0
  53. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/__init__.py +0 -0
  54. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/about_dialog.py +0 -0
  55. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/align_plane_dialog.py +0 -0
  56. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/alignment_dialog.py +0 -0
  57. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/analysis_window.py +0 -0
  58. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/atom_picking.py +0 -0
  59. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/custom_qt_interactor.py +0 -0
  60. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/dialog_3d_picking_mixin.py +0 -0
  61. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/main_window.py +0 -0
  62. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/mirror_dialog.py +0 -0
  63. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/periodic_table_dialog.py +0 -0
  64. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/settings_tabs/__init__.py +0 -0
  65. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/settings_tabs/settings_2d_tab.py +0 -0
  66. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/settings_tabs/settings_3d_tabs.py +0 -0
  67. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/settings_tabs/settings_other_tab.py +0 -0
  68. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/settings_tabs/settings_tab_base.py +0 -0
  69. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/string_importers.py +0 -0
  70. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/template_preview_item.py +0 -0
  71. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/template_preview_view.py +0 -0
  72. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/translation_dialog.py +0 -0
  73. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/ui/zoomable_view.py +0 -0
  74. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/utils/__init__.py +0 -0
  75. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/utils/constants.py +0 -0
  76. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/utils/default_settings.py +0 -0
  77. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/utils/sip_isdeleted_safe.py +0 -0
  78. {moleditpy-3.3.1 → moleditpy-3.4.1}/src/moleditpy/utils/system_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy
3
- Version: 3.3.1
3
+ Version: 3.4.1
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
@@ -698,7 +698,7 @@ Requires-Dist: pyqt6<6.10; sys_platform == "darwin"
698
698
  Requires-Dist: pyqt6<6.11; sys_platform != "darwin"
699
699
  Requires-Dist: pyvista<0.48
700
700
  Requires-Dist: pyvistaqt<0.12
701
- Requires-Dist: rdkit<2025.10
701
+ Requires-Dist: rdkit<2026.4
702
702
  Requires-Dist: openbabel-wheel<3.2
703
703
  Dynamic: license-file
704
704
 
@@ -717,6 +717,7 @@ Dynamic: license-file
717
717
  [![PyPI Downloads](https://static.pepy.tech/personalized-badge/moleditpy?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/moleditpy)
718
718
  [![PyPI Downloads](https://static.pepy.tech/personalized-badge/moleditpy?period=monthly&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=monthly+downloads)](https://pepy.tech/projects/moleditpy)
719
719
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/HiroYokoyama/python_molecular_editor)
720
+ [![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/HiroYokoyama)
720
721
 
721
722
  [🇯🇵 日本語 (Japanese)](#japanese)
722
723
 
@@ -13,6 +13,7 @@
13
13
  [![PyPI Downloads](https://static.pepy.tech/personalized-badge/moleditpy?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/moleditpy)
14
14
  [![PyPI Downloads](https://static.pepy.tech/personalized-badge/moleditpy?period=monthly&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=monthly+downloads)](https://pepy.tech/projects/moleditpy)
15
15
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/HiroYokoyama/python_molecular_editor)
16
+ [![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/HiroYokoyama)
16
17
 
17
18
  [🇯🇵 日本語 (Japanese)](#japanese)
18
19
 
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "MoleditPy"
7
7
 
8
- version = "3.3.1"
8
+ version = "3.4.1"
9
9
 
10
10
  license = {file = "LICENSE"}
11
11
 
@@ -38,7 +38,7 @@ dependencies = [
38
38
  "pyqt6 < 6.11; sys_platform != 'darwin'",
39
39
  "pyvista < 0.48",
40
40
  "pyvistaqt < 0.12",
41
- "rdkit < 2025.10",
41
+ "rdkit < 2026.4",
42
42
  "openbabel-wheel < 3.2"
43
43
  ]
44
44
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy
3
- Version: 3.3.1
3
+ Version: 3.4.1
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
@@ -698,7 +698,7 @@ Requires-Dist: pyqt6<6.10; sys_platform == "darwin"
698
698
  Requires-Dist: pyqt6<6.11; sys_platform != "darwin"
699
699
  Requires-Dist: pyvista<0.48
700
700
  Requires-Dist: pyvistaqt<0.12
701
- Requires-Dist: rdkit<2025.10
701
+ Requires-Dist: rdkit<2026.4
702
702
  Requires-Dist: openbabel-wheel<3.2
703
703
  Dynamic: license-file
704
704
 
@@ -717,6 +717,7 @@ Dynamic: license-file
717
717
  [![PyPI Downloads](https://static.pepy.tech/personalized-badge/moleditpy?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/moleditpy)
718
718
  [![PyPI Downloads](https://static.pepy.tech/personalized-badge/moleditpy?period=monthly&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=monthly+downloads)](https://pepy.tech/projects/moleditpy)
719
719
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/HiroYokoyama/python_molecular_editor)
720
+ [![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/HiroYokoyama)
720
721
 
721
722
  [🇯🇵 日本語 (Japanese)](#japanese)
722
723
 
@@ -1,7 +1,7 @@
1
1
  numpy
2
2
  pyvista<0.48
3
3
  pyvistaqt<0.12
4
- rdkit<2025.10
4
+ rdkit<2026.4
5
5
  openbabel-wheel<3.2
6
6
 
7
7
  [:sys_platform != "darwin"]
@@ -19,4 +19,4 @@ try:
19
19
  except ImportError:
20
20
  OBABEL_AVAILABLE = False
21
21
 
22
- from .utils.constants import VERSION as __version__
22
+ from .utils.constants import VERSION as __version__ # noqa: F401
@@ -68,7 +68,8 @@ def main() -> None:
68
68
  ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
69
69
 
70
70
  parser = argparse.ArgumentParser(
71
- prog="moleditpy", description="MoleditPy molecular editor"
71
+ prog="moleditpy",
72
+ description="MoleditPy — A Python-based molecular editing software",
72
73
  )
73
74
  parser.add_argument("file", nargs="?", default=None, help="File to open on startup")
74
75
  _variant = " (Linux)" if "moleditpy_linux" in (__file__ or "") else ""
@@ -189,8 +189,10 @@ class AngleDialog(GeometryBaseDialog):
189
189
  self.atom2_idx = atom_idx
190
190
  elif self.atom3_idx is None:
191
191
  self.atom3_idx = atom_idx
192
- # Take a fresh snapshot immediately upon completing the triad selection
193
- self._snapshot_positions = self.mol.GetConformer().GetPositions().copy()
192
+ # Capture the ABSOLUTE baseline for this selection session.
193
+ # This prevents 'direction change' drift during multiple slider drags.
194
+ self._baseline_positions = self.mol.GetConformer().GetPositions().copy()
195
+ self._snapshot_positions = self._baseline_positions.copy()
194
196
  else:
195
197
  # Reset and start over
196
198
  self.atom1_idx = atom_idx
@@ -205,6 +207,7 @@ class AngleDialog(GeometryBaseDialog):
205
207
  self.atom1_idx = None
206
208
  self.atom2_idx = None # vertex atom
207
209
  self.atom3_idx = None
210
+ self._baseline_positions = None
208
211
  self._snapshot_positions = None
209
212
  self.clear_selection_labels()
210
213
  self.update_display()
@@ -212,7 +215,7 @@ class AngleDialog(GeometryBaseDialog):
212
215
  def show_atom_labels(self) -> None:
213
216
  """Display labels on the selected atoms."""
214
217
  selected_atoms = [self.atom1_idx, self.atom2_idx, self.atom3_idx]
215
- labels = ["1st", "2nd (vertex)", "3rd"]
218
+ labels = ["1", "2", "3"]
216
219
  pairs = [
217
220
  (idx, labels[i]) for i, idx in enumerate(selected_atoms) if idx is not None
218
221
  ]
@@ -271,7 +274,7 @@ class AngleDialog(GeometryBaseDialog):
271
274
  self._snapshot_positions = None
272
275
  # Add labels
273
276
  self.add_selection_label(self.atom1_idx, "1")
274
- self.add_selection_label(self.atom2_idx, "2(vertex)")
277
+ self.add_selection_label(self.atom2_idx, "2")
275
278
  # Clear angle input while selection is incomplete
276
279
  try:
277
280
  self.angle_input.blockSignals(True)
@@ -295,23 +298,35 @@ class AngleDialog(GeometryBaseDialog):
295
298
  current_angle = self.calculate_angle()
296
299
  self.angle_label.setText(f"Current angle: {current_angle:.2f}°")
297
300
  self.apply_button.setEnabled(True)
298
- # Update angle input box with current angle
301
+ # Update angle input box and slider
299
302
  try:
300
- self.angle_input.blockSignals(True)
301
- self.angle_input.setText(f"{current_angle:.2f}")
302
- self.angle_input.blockSignals(False)
303
- self.angle_slider.blockSignals(True)
304
- slider_val = int(round(current_angle))
305
- slider_val = max(-180, min(180, slider_val))
306
- self.angle_slider.setValue(slider_val)
307
- self.angle_slider.setEnabled(True)
308
- self.angle_slider.blockSignals(False)
303
+ # Update input box and slider only if not dragging
304
+ if not self._slider_dragging:
305
+ self.angle_input.blockSignals(True)
306
+ self.angle_input.setText(f"{current_angle:.2f}")
307
+ self.angle_input.blockSignals(False)
308
+
309
+ # UPDATE SLIDER: Logic to prevent 'jumping' to positive value
310
+ slider_val = int(round(current_angle))
311
+ current_slider_val = self.angle_slider.value()
312
+
313
+ # If the user is on the negative side of the slider, keep the sign
314
+ if current_slider_val < 0:
315
+ slider_val = -slider_val
316
+
317
+ if current_slider_val != slider_val:
318
+ self.angle_slider.blockSignals(True)
319
+ self.angle_slider.setValue(slider_val)
320
+ self.angle_slider.blockSignals(False)
321
+ self.angle_slider.setEnabled(True)
322
+ else:
323
+ self.angle_slider.setEnabled(True)
309
324
  except (AttributeError, RuntimeError, TypeError):
310
325
  pass
311
326
 
312
327
  # Add labels
313
328
  self.add_selection_label(self.atom1_idx, "1")
314
- self.add_selection_label(self.atom2_idx, "2(vertex)")
329
+ self.add_selection_label(self.atom2_idx, "2")
315
330
  self.add_selection_label(self.atom3_idx, "3")
316
331
 
317
332
  def calculate_angle(self) -> float:
@@ -366,10 +381,14 @@ class AngleDialog(GeometryBaseDialog):
366
381
  """Adjust the bond angle."""
367
382
  conf = self.mol.GetConformer()
368
383
 
369
- # Use snapshot if available (slider dragging) to keep the rotation axis stable
370
- snapshot = self._snapshot_positions
371
- if snapshot is not None:
372
- positions = snapshot.copy()
384
+ # Use baseline positions (fixed for dialog session) to keep the rotation axis stable.
385
+ if (
386
+ hasattr(self, "_baseline_positions")
387
+ and self._baseline_positions is not None
388
+ ):
389
+ positions = self._baseline_positions.copy()
390
+ elif self._snapshot_positions is not None:
391
+ positions = self._snapshot_positions.copy()
373
392
  else:
374
393
  positions = conf.GetPositions()
375
394
 
@@ -379,10 +398,16 @@ class AngleDialog(GeometryBaseDialog):
379
398
  idx_b: int = self.atom2_idx # vertex
380
399
  idx_c: int = self.atom3_idx
381
400
 
401
+ # Calculate baseline angle from the POSITIONS we are working on (important for snapshot stability)
402
+ p_a, p_b, p_c = positions[idx_a], positions[idx_b], positions[idx_c]
403
+ from moleditpy.core.mol_geometry import calc_angle_deg
404
+
405
+ baseline_angle = calc_angle_deg(p_a, p_b, p_c)
406
+
382
407
  if self.both_groups_radio.isChecked():
383
408
  # Both arms rotate equally (half angle each)
384
- current_angle = self.calculate_angle()
385
- half_delta_deg = (new_angle_deg - current_angle) / 2.0
409
+ # Use baseline_angle from snapshot to avoid drift/jumps
410
+ half_delta_deg = (new_angle_deg - baseline_angle) / 2.0
386
411
 
387
412
  group1 = get_connected_group(self.mol, idx_a, exclude=idx_b)
388
413
  group3 = get_connected_group(self.mol, idx_c, exclude=idx_b)
@@ -393,7 +418,7 @@ class AngleDialog(GeometryBaseDialog):
393
418
  idx_c,
394
419
  idx_b,
395
420
  idx_a,
396
- current_angle + half_delta_deg,
421
+ baseline_angle + half_delta_deg,
397
422
  group1,
398
423
  )
399
424
  # Arm 3 rotates to the FINAL angle (relative to the now-moved Arm 1)
@@ -17,7 +17,8 @@ import binascii
17
17
  import copy
18
18
  import logging
19
19
  import os
20
- from typing import Any, Dict, Optional, Tuple, TYPE_CHECKING
20
+ from typing import Any, Dict, Optional, Tuple
21
+
21
22
 
22
23
  import numpy as np
23
24
 
@@ -45,9 +46,6 @@ try:
45
46
  except (AttributeError, RuntimeError, TypeError, ImportError):
46
47
  from moleditpy.core.molecular_data import MolecularData
47
48
 
48
- if TYPE_CHECKING:
49
- from .main_window import MainWindow
50
-
51
49
 
52
50
  # --- Class Definition ---
53
51
  class StateManager:
@@ -634,7 +632,7 @@ class StateManager:
634
632
  method = json_data.get("last_successful_optimization_method", None)
635
633
  if hasattr(self.host, "compute_manager"):
636
634
  self.host.compute_manager.last_successful_optimization_method = method
637
- else: # [REPORT ERROR MISSING ATTRIBUTE]
635
+ else:
638
636
  logging.error(
639
637
  "REPORT ERROR: Missing attribute 'compute_manager' on self.host"
640
638
  )
@@ -808,7 +806,7 @@ class StateManager:
808
806
  "update_atom_id_menu_text",
809
807
  ):
810
808
  self.host.view_3d_manager.update_atom_id_menu_text()
811
- else: # [REPORT ERROR MISSING ATTRIBUTE]
809
+ else:
812
810
  logging.error(
813
811
  "REPORT ERROR: Missing attribute 'update_atom_id_menu_text' on object"
814
812
  )
@@ -817,13 +815,13 @@ class StateManager:
817
815
  "update_atom_id_menu_state",
818
816
  ):
819
817
  self.host.view_3d_manager.update_atom_id_menu_state()
820
- else: # [REPORT ERROR MISSING ATTRIBUTE]
818
+ else:
821
819
  logging.error(
822
820
  "REPORT ERROR: Missing attribute 'update_atom_id_menu_state' on object"
823
821
  )
824
822
  except (RuntimeError, TypeError, AttributeError):
825
823
  pass
826
- else: # [REPORT ERROR MISSING ATTRIBUTE]
824
+ else:
827
825
  logging.error(
828
826
  "REPORT ERROR: Missing attribute 'create_atom_id_mapping' on object"
829
827
  )
@@ -833,7 +831,7 @@ class StateManager:
833
831
  self.host.view_3d_manager.draw_molecule_3d(
834
832
  self.host.view_3d_manager.current_mol
835
833
  )
836
- else: # [REPORT ERROR MISSING ATTRIBUTE]
834
+ else:
837
835
  logging.error(
838
836
  "REPORT ERROR: Missing attribute 'draw_molecule_3d' on object"
839
837
  )
@@ -11,7 +11,7 @@ DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
13
  from __future__ import annotations
14
- import logging # [REPORT ERROR MISSING ATTRIBUTE]
14
+ import logging
15
15
  from typing import Any, List, Optional
16
16
 
17
17
  from PyQt6.QtCore import QPointF, QRectF, Qt
@@ -92,7 +92,7 @@ class AtomItem(QGraphicsItem):
92
92
  if hasattr(scene, "get_setting"):
93
93
  font_size = scene.get_setting("atom_font_size_2d", 20)
94
94
  font_family = scene.get_setting("atom_font_family_2d", FONT_FAMILY)
95
- else: # [REPORT ERROR MISSING ATTRIBUTE]
95
+ else:
96
96
  logging.error(
97
97
  f"REPORT ERROR: Missing attribute 'get_setting' on scene of type {type(scene)}"
98
98
  )
@@ -119,7 +119,7 @@ class AtomItem(QGraphicsItem):
119
119
  if hasattr(scene, "get_setting"):
120
120
  font_size = scene.get_setting("atom_font_size_2d", 20)
121
121
  font_family = scene.get_setting("atom_font_family_2d", FONT_FAMILY)
122
- else: # [REPORT ERROR MISSING ATTRIBUTE]
122
+ else:
123
123
  logging.error(
124
124
  f"REPORT ERROR: Missing attribute 'get_setting' on scene of type {type(scene)}"
125
125
  )
@@ -225,6 +225,77 @@ class AtomItem(QGraphicsItem):
225
225
  # 3. Add final margins for selection highlights, etc.
226
226
  return full_visual_rect.adjusted(-3, -3, 3, 3)
227
227
 
228
+ def get_bg_ellipse_path(self) -> QPainterPath:
229
+ path = QPainterPath()
230
+ if not self.is_visible:
231
+ return path
232
+
233
+ font_size = 20
234
+ font_family = FONT_FAMILY
235
+ scene = self.scene()
236
+ if scene is not None:
237
+ if hasattr(scene, "get_setting"):
238
+ font_size = scene.get_setting("atom_font_size_2d", 20)
239
+ font_family = scene.get_setting("atom_font_family_2d", FONT_FAMILY)
240
+
241
+ font = QFont(font_family, font_size, FONT_WEIGHT_BOLD)
242
+ fm = QFontMetricsF(font)
243
+
244
+ hydrogen_part = ""
245
+ if self.implicit_h_count > 0:
246
+ is_skeletal_carbon = (
247
+ self.symbol == "C"
248
+ and self.charge == 0
249
+ and self.radical == 0
250
+ and len(self.bonds) > 0
251
+ )
252
+ if not is_skeletal_carbon:
253
+ hydrogen_part = "H"
254
+ if self.implicit_h_count > 1:
255
+ subscript_map = str.maketrans("0123456789", "₀₁₂₃₄₅₆₇₈₉")
256
+ hydrogen_part += str(self.implicit_h_count).translate(subscript_map)
257
+
258
+ flip_text = False
259
+ if hydrogen_part and self.bonds:
260
+ my_pos_x = self.pos().x()
261
+ total_dx = 0.0
262
+ for b in self.bonds:
263
+ partner = b.atom2 if b.atom1 is self else b.atom1
264
+ try:
265
+ if partner is None or sip_isdeleted_safe(partner):
266
+ continue
267
+ partner_pos = partner.pos()
268
+ if partner_pos is None:
269
+ continue
270
+ total_dx += partner_pos.x() - my_pos_x
271
+ except (AttributeError, RuntimeError, TypeError, ValueError):
272
+ # Suppress non-critical UI sync errors during atom position retrieval
273
+ continue
274
+ if total_dx > 0:
275
+ flip_text = True
276
+
277
+ if flip_text:
278
+ display_text = hydrogen_part + self.symbol
279
+ else:
280
+ display_text = self.symbol + hydrogen_part
281
+
282
+ text_rect = fm.boundingRect(display_text)
283
+ text_rect.adjust(-2, -2, 2, 2)
284
+ if hydrogen_part:
285
+ symbol_rect = fm.boundingRect(self.symbol)
286
+ if flip_text:
287
+ offset_x = symbol_rect.width() // 2
288
+ text_rect.moveTo(offset_x - text_rect.width(), -text_rect.height() / 2)
289
+ else:
290
+ offset_x = -symbol_rect.width() // 2
291
+ text_rect.moveTo(offset_x, -text_rect.height() / 2)
292
+ else:
293
+ text_rect.moveCenter(QPointF(0, 0))
294
+
295
+ bg_rect = text_rect.adjusted(-5, -8, 5, 8)
296
+ path.addEllipse(bg_rect)
297
+ return path
298
+
228
299
  def shape(self) -> QPainterPath:
229
300
  """Define the shape of the atom item for collision detection."""
230
301
  scene = self.scene()
@@ -360,29 +431,6 @@ class AtomItem(QGraphicsItem):
360
431
  offset_x = -symbol_rect.width() // 2
361
432
  text_rect.moveTo(offset_x, -text_rect.height() // 2)
362
433
 
363
- # 2. Handle background (fill with white or clear if transparent)
364
- if self.scene():
365
- bg_brush = self.scene().backgroundBrush()
366
- bg_rect = text_rect.adjusted(-5, -8, 5, 8)
367
-
368
- if bg_brush.style() == Qt.BrushStyle.NoBrush:
369
- # Use CompositionMode_Clear to erase overlapping bond lines
370
- painter.save()
371
- painter.setCompositionMode(
372
- QPainter.CompositionMode.CompositionMode_Clear
373
- )
374
- painter.setBrush(
375
- QColor(0, 0, 0, 255)
376
- ) # Color doesn't matter (alpha is key)
377
- painter.setPen(Qt.PenStyle.NoPen)
378
- painter.drawEllipse(bg_rect)
379
- painter.restore()
380
- else:
381
- # Fill with background color if it exists
382
- painter.setBrush(bg_brush)
383
- painter.setPen(Qt.PenStyle.NoPen)
384
- painter.drawEllipse(bg_rect)
385
-
386
434
  # 3. Draw the atom symbol itself
387
435
  # Color is already determined above
388
436
  painter.setPen(QPen(color))
@@ -454,7 +502,9 @@ class AtomItem(QGraphicsItem):
454
502
  for bond in self.bonds:
455
503
  if bond.scene():
456
504
  bond.update_position()
457
-
505
+ elif change == QGraphicsItem.GraphicsItemChange.ItemSceneHasChanged:
506
+ if self.scene() is not None:
507
+ self.update_style()
458
508
  return res
459
509
 
460
510
  def hoverEnterEvent(self, event: Any) -> None:
@@ -14,7 +14,7 @@ import logging
14
14
  from typing import TYPE_CHECKING, Optional, Union
15
15
 
16
16
  import numpy as np
17
- from PyQt6.QtCore import Qt
17
+ from PyQt6.QtCore import Qt, QTimer
18
18
  from PyQt6.QtGui import QCloseEvent, QKeyEvent
19
19
  from PyQt6.QtWidgets import QDialog, QWidget
20
20
  from rdkit import Chem, Geometry
@@ -114,14 +114,28 @@ class BasePickingDialog(Dialog3DPickingMixin, QDialog):
114
114
  # If for some reason the cache is incompatible, draw_molecule_3d below will rebuild it
115
115
  pass
116
116
 
117
- # 3. Redraw
117
+ # 3. Redraw molecule
118
118
  self.main_window.view_3d_manager.draw_molecule_3d(self.mol)
119
119
  self._molecule_modified = True
120
120
 
121
- # 4. Refresh chiral/cis-trans labels if applicable
121
+ # 4. Refresh display (deferred to ensure stability)
122
+ is_dragging = getattr(self, "_slider_dragging", False)
123
+
124
+ if is_dragging and hasattr(self, "show_atom_labels"):
125
+ QTimer.singleShot(200, self.show_atom_labels)
126
+ elif hasattr(self, "update_display"):
127
+ QTimer.singleShot(200, self.update_display)
128
+
129
+ if (
130
+ hasattr(self.main_window.view_3d_manager, "plotter")
131
+ and self.main_window.view_3d_manager.plotter
132
+ ):
133
+ QTimer.singleShot(200, self.main_window.view_3d_manager.plotter.render)
134
+
135
+ # 5. Refresh chiral/cis-trans labels if applicable
122
136
  if hasattr(self.main_window.view_3d_manager, "update_chiral_labels"):
123
137
  self.main_window.view_3d_manager.update_chiral_labels()
124
- else: # [REPORT ERROR MISSING ATTRIBUTE]
138
+ else:
125
139
  logging.error(
126
140
  "REPORT ERROR: Missing attribute 'update_chiral_labels' on object"
127
141
  )
@@ -131,7 +145,7 @@ class BasePickingDialog(Dialog3DPickingMixin, QDialog):
131
145
  if hasattr(self.main_window, "state_manager"):
132
146
  self.main_window.edit_actions_manager.push_undo_state()
133
147
  self._molecule_modified = False
134
- else: # [REPORT ERROR MISSING ATTRIBUTE]
148
+ else:
135
149
  logging.error(
136
150
  "REPORT ERROR: Missing attribute 'state_manager' on self.main_window"
137
151
  )