MoleditPy-linux 4.2.0__tar.gz → 4.2.2__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 (81) hide show
  1. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/PKG-INFO +1 -1
  2. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/pyproject.toml +1 -1
  3. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/MoleditPy_linux.egg-info/PKG-INFO +1 -1
  4. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/plugins/plugin_manager.py +8 -2
  5. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/compute_logic.py +39 -15
  6. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/main_window_init.py +32 -7
  7. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/plugin_menu_manager.py +28 -0
  8. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/LICENSE +0 -0
  9. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/README.md +0 -0
  10. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/setup.cfg +0 -0
  11. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/MoleditPy_linux.egg-info/SOURCES.txt +0 -0
  12. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/MoleditPy_linux.egg-info/dependency_links.txt +0 -0
  13. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/MoleditPy_linux.egg-info/entry_points.txt +0 -0
  14. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/MoleditPy_linux.egg-info/requires.txt +0 -0
  15. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/MoleditPy_linux.egg-info/top_level.txt +0 -0
  16. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/__init__.py +0 -0
  17. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/__main__.py +0 -0
  18. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/assets/file_icon.ico +0 -0
  19. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/assets/icon.icns +0 -0
  20. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/assets/icon.ico +0 -0
  21. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/assets/icon.png +0 -0
  22. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/core/__init__.py +0 -0
  23. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/core/mol_geometry.py +0 -0
  24. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/core/molecular_data.py +0 -0
  25. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/main.py +0 -0
  26. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/plugins/__init__.py +0 -0
  27. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/plugins/plugin_interface.py +0 -0
  28. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/plugins/plugin_manager_window.py +0 -0
  29. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/__init__.py +0 -0
  30. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/about_dialog.py +0 -0
  31. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/align_plane_dialog.py +0 -0
  32. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/alignment_dialog.py +0 -0
  33. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/analysis_window.py +0 -0
  34. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/angle_dialog.py +0 -0
  35. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/app_state.py +0 -0
  36. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/atom_item.py +0 -0
  37. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/atom_picking.py +0 -0
  38. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/base_picking_dialog.py +0 -0
  39. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/bond_item.py +0 -0
  40. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/bond_length_dialog.py +0 -0
  41. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/calculation_worker.py +0 -0
  42. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/color_settings_dialog.py +0 -0
  43. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/constrained_optimization_dialog.py +0 -0
  44. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/custom_interactor_style.py +0 -0
  45. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/custom_qt_interactor.py +0 -0
  46. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py +0 -0
  47. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/dialog_logic.py +0 -0
  48. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/dihedral_dialog.py +0 -0
  49. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/edit_3d_logic.py +0 -0
  50. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/edit_actions_logic.py +0 -0
  51. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/export_logic.py +0 -0
  52. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/geometry_base_dialog.py +0 -0
  53. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/io_logic.py +0 -0
  54. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/main_window.py +0 -0
  55. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/mirror_dialog.py +0 -0
  56. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/molecular_scene_handler.py +0 -0
  57. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/molecule_scene.py +0 -0
  58. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/move_group_dialog.py +0 -0
  59. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/move_selected_atoms_dialog.py +0 -0
  60. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/periodic_table_dialog.py +0 -0
  61. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/planarize_dialog.py +0 -0
  62. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/settings_dialog.py +0 -0
  63. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/settings_tabs/__init__.py +0 -0
  64. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/settings_tabs/settings_2d_tab.py +0 -0
  65. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py +0 -0
  66. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/settings_tabs/settings_other_tab.py +0 -0
  67. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/settings_tabs/settings_tab_base.py +0 -0
  68. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/string_importers.py +0 -0
  69. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/template_preview_item.py +0 -0
  70. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/template_preview_view.py +0 -0
  71. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/translation_dialog.py +0 -0
  72. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/ui_manager.py +0 -0
  73. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/user_template_dialog.py +0 -0
  74. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/view_3d_logic.py +0 -0
  75. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/ui/zoomable_view.py +0 -0
  76. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/utils/__init__.py +0 -0
  77. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/utils/constants.py +0 -0
  78. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/utils/default_settings.py +0 -0
  79. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/utils/sip_isdeleted_safe.py +0 -0
  80. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/src/moleditpy_linux/utils/suppress_log.py +0 -0
  81. {moleditpy_linux-4.2.0 → moleditpy_linux-4.2.2}/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.2.0
3
+ Version: 4.2.2
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
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "MoleditPy-linux"
7
7
 
8
- version = "4.2.0"
8
+ version = "4.2.2"
9
9
 
10
10
  license = {file = "LICENSE"}
11
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy-linux
3
- Version: 4.2.0
3
+ Version: 4.2.2
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
@@ -485,12 +485,18 @@ class PluginManager:
485
485
  self, plugin_name: str, method_name: str, callback: Callable
486
486
  ) -> None:
487
487
  """Register a named 3D optimization method provided by a plugin."""
488
- # Key by upper-case method name for consistency
489
- self.optimization_methods[method_name.upper()] = {
488
+ method_key = method_name.upper()
489
+ self.optimization_methods[method_key] = {
490
490
  "plugin": plugin_name,
491
491
  "callback": callback,
492
492
  "label": method_name,
493
493
  }
494
+ if hasattr(self.main_window, "init_manager") and hasattr(
495
+ self.main_window.init_manager, "add_optimization_method"
496
+ ):
497
+ self.main_window.init_manager.add_optimization_method(
498
+ method_name, method_key
499
+ )
494
500
 
495
501
  def register_file_opener(
496
502
  self, plugin_name: str, extension: str, callback: Callable, priority: int = 0
@@ -45,6 +45,9 @@ class ComputeManager:
45
45
  self.halt_ids: Set[int] = set()
46
46
  self.next_conversion_id: int = 1
47
47
  self.active_worker_ids: Set[int] = set()
48
+ # worker_id -> (method_key, plugin_entry) for a plugin optimizer to run
49
+ # as a post-step once conversion's built-in pre-optimization completes.
50
+ self._pending_plugin_opt: Dict[int, Tuple[str, Dict[str, Any]]] = {}
48
51
 
49
52
  def reset_active_threads(self) -> None:
50
53
  """Reset active calculation threads list."""
@@ -178,20 +181,10 @@ class ComputeManager:
178
181
  return
179
182
 
180
183
  menu = QMenu(self.host)
181
- opt_list = [
182
- ("MMFF94s (RDKit)", "MMFF_RDKIT"),
183
- ("MMFF94 (RDKit)", "MMFF94_RDKIT"),
184
- ("UFF (RDKit)", "UFF_RDKIT"),
185
- ("MMFF94s (Open Babel)", "MMFF94s_OBABEL"),
186
- ("MMFF94 (Open Babel)", "MMFF94_OBABEL"),
187
- ("UFF (Open Babel)", "UFF_OBABEL"),
188
- ("GAFF (Open Babel)", "GAFF_OBABEL"),
189
- ("Ghemical (Open Babel)", "GHEMICAL_OBABEL"),
190
- ]
191
- for label, key in opt_list:
184
+ for key, source_action in self.host.init_manager.opt3d_actions.items():
185
+ label = source_action.text().replace("&", "")
192
186
  a = QAction(label, self.host)
193
- if key in self.host.init_manager.opt3d_actions:
194
- a.setEnabled(self.host.init_manager.opt3d_actions[key].isEnabled())
187
+ a.setEnabled(source_action.isEnabled())
195
188
  a.triggered.connect(
196
189
  lambda checked=False, k=key: self._trigger_optimize_with_temp_method(k)
197
190
  )
@@ -249,6 +242,25 @@ class ComputeManager:
249
242
  self.next_conversion_id = run_id + 1
250
243
  self.active_worker_ids.add(run_id)
251
244
 
245
+ # The worker only runs built-in force fields. If the selected method is a
246
+ # plugin optimizer, pre-optimize with MMFF (RDKit) in the worker (which
247
+ # auto-falls-back to UFF if MMFF setup fails), then run the plugin
248
+ # callback as a post-step in on_calculation_finished.
249
+ effective_method = (
250
+ optimization_method
251
+ or self.host.init_manager.optimization_method
252
+ or "MMFF_RDKIT"
253
+ ).upper()
254
+ _reg = getattr(
255
+ getattr(self.host, "plugin_manager", None), "optimization_methods", None
256
+ )
257
+ plugin_entry = _reg.get(effective_method) if isinstance(_reg, dict) else None
258
+ if plugin_entry:
259
+ self._pending_plugin_opt[run_id] = (effective_method, plugin_entry)
260
+ worker_method = "MMFF_RDKIT"
261
+ else:
262
+ worker_method = effective_method
263
+
252
264
  # UI Updates
253
265
  self.host.init_manager.convert_button.setText("Halt conversion") # type: ignore[union-attr]
254
266
  self._safe_disconnect(self.host.init_manager.convert_button.clicked) # type: ignore[union-attr]
@@ -277,8 +289,7 @@ class ComputeManager:
277
289
  options = {
278
290
  "conversion_mode": conversion_mode
279
291
  or self.host.init_manager.settings.get("3d_conversion_mode", "fallback"),
280
- "optimization_method": optimization_method
281
- or self.host.init_manager.optimization_method,
292
+ "optimization_method": worker_method,
282
293
  "optimize_intermolecular_interaction_rdkit": self.host.init_manager.settings.get(
283
294
  "optimize_intermolecular_interaction_rdkit", True
284
295
  ),
@@ -295,6 +306,7 @@ class ComputeManager:
295
306
  if wids_to_halt:
296
307
  self.halt_ids.update(wids_to_halt)
297
308
  self.active_worker_ids.clear()
309
+ self._pending_plugin_opt.clear()
298
310
 
299
311
  self._restore_button_ui()
300
312
  self.host.init_manager.cleanup_button.setEnabled(True) # type: ignore[union-attr]
@@ -539,6 +551,16 @@ class ComputeManager:
539
551
  self.host.view_3d_manager.update_atom_id_menu_text()
540
552
  self.host.view_3d_manager.update_atom_id_menu_state()
541
553
 
554
+ # If the selected method was a plugin optimizer, the worker only did the
555
+ # built-in pre-optimization; run the plugin callback now on the result.
556
+ pending = (
557
+ self._pending_plugin_opt.pop(worker_id, None)
558
+ if worker_id is not None
559
+ else None
560
+ )
561
+ if pending and mol:
562
+ self._run_plugin_optimization(*pending)
563
+
542
564
  def on_calculation_error(self, message: Union[str, Tuple[int, str]]) -> None:
543
565
  """Handle an error or halt signal from the background optimization worker."""
544
566
  # Accept either a string or (worker_id, message) tuple from the worker signal
@@ -557,8 +579,10 @@ class ComputeManager:
557
579
  self.host.statusBar().showMessage( # type: ignore[union-attr]
558
580
  f"Ignored stale worker error: {msg} (ID = {worker_id})"
559
581
  )
582
+ self._pending_plugin_opt.pop(worker_id, None)
560
583
  return # stale worker, ignore
561
584
  self.active_worker_ids.discard(worker_id)
585
+ self._pending_plugin_opt.pop(worker_id, None)
562
586
  else:
563
587
  msg = str(message)
564
588
 
@@ -1648,7 +1648,7 @@ class MainInitManager:
1648
1648
  self.conv_actions[saved_conv].setChecked(True)
1649
1649
  self.settings["3d_conversion_mode"] = saved_conv
1650
1650
 
1651
- optimization_menu = settings_menu.addMenu("3D Optimization Settings")
1651
+ self.optimization_menu = settings_menu.addMenu("3D Optimization Settings")
1652
1652
  opt_methods = [
1653
1653
  ("MMFF94s (RDKit)", "MMFF_RDKIT"),
1654
1654
  ("MMFF94 (RDKit)", "MMFF94_RDKIT"),
@@ -1660,8 +1660,8 @@ class MainInitManager:
1660
1660
  ("Ghemical (Open Babel)", "GHEMICAL_OBABEL"),
1661
1661
  ]
1662
1662
  self.opt3d_method_labels = {key.upper(): label for (label, key) in opt_methods}
1663
- opt_group = QActionGroup(self.host)
1664
- opt_group.setExclusive(True)
1663
+ self.opt_group = QActionGroup(self.host)
1664
+ self.opt_group.setExclusive(True)
1665
1665
  self.opt3d_actions = {}
1666
1666
  for label, key in opt_methods:
1667
1667
  action = QAction(label, self.host)
@@ -1672,11 +1672,11 @@ class MainInitManager:
1672
1672
  lambda checked,
1673
1673
  m=key: self.host.compute_manager.set_optimization_method(m)
1674
1674
  )
1675
- optimization_menu.addAction(action)
1676
- opt_group.addAction(action)
1675
+ self.optimization_menu.addAction(action)
1676
+ self.opt_group.addAction(action)
1677
1677
  self.opt3d_actions[key] = action
1678
1678
 
1679
- optimization_menu.addSeparator()
1679
+ self.opt3d_separator = self.optimization_menu.addSeparator()
1680
1680
  self.host.intermolecular_rdkit_action = QAction(
1681
1681
  "Consider Intermolecular Interaction for RDKit", self.host
1682
1682
  )
@@ -1687,7 +1687,7 @@ class MainInitManager:
1687
1687
  self.host.intermolecular_rdkit_action.triggered.connect(
1688
1688
  self.host.compute_manager.toggle_intermolecular_interaction_rdkit
1689
1689
  )
1690
- optimization_menu.addAction(self.host.intermolecular_rdkit_action)
1690
+ self.optimization_menu.addAction(self.host.intermolecular_rdkit_action)
1691
1691
 
1692
1692
  saved_opt = (self.settings.get("optimization_method") or "MMFF_RDKIT").upper()
1693
1693
  if (
@@ -1705,6 +1705,31 @@ class MainInitManager:
1705
1705
  reset_settings_action.triggered.connect(self.reset_all_settings_menu)
1706
1706
  settings_menu.addAction(reset_settings_action)
1707
1707
 
1708
+ def add_optimization_method(self, label: str, key: str) -> None:
1709
+ """Dynamically add an optimization method to the menu."""
1710
+ key_upper = key.upper()
1711
+ if key_upper in self.opt3d_actions:
1712
+ return
1713
+
1714
+ self.opt3d_method_labels[key_upper] = label
1715
+ action = QAction(label, self.host)
1716
+ action.setCheckable(True)
1717
+ action.triggered.connect(
1718
+ lambda checked,
1719
+ m=key_upper: self.host.compute_manager.set_optimization_method(m)
1720
+ )
1721
+
1722
+ self.optimization_menu.insertAction(self.opt3d_separator, action)
1723
+ self.opt_group.addAction(action)
1724
+ self.opt3d_actions[key_upper] = action
1725
+
1726
+ # Plugins register after menu-init, so restore the saved default here
1727
+ # if it names this method (menu-init fell back to MMFF_RDKIT).
1728
+ saved_opt = (self.settings.get("optimization_method") or "").upper()
1729
+ if key_upper == saved_opt:
1730
+ action.setChecked(True)
1731
+ self.optimization_method = key_upper
1732
+
1708
1733
  def _init_help_menu(self, menu_bar: Any) -> None:
1709
1734
  """Initialize the Help menu."""
1710
1735
  help_menu = menu_bar.addMenu("&Help")
@@ -81,6 +81,7 @@ class PluginMenuManager:
81
81
  self.integrate_plugin_export_actions()
82
82
  self.integrate_plugin_file_openers()
83
83
  self.integrate_plugin_analysis_tools()
84
+ self.integrate_plugin_optimization_methods()
84
85
 
85
86
  def rebuild_plugin_menus(self) -> None:
86
87
  """Fully rebuild all plugin-managed UI after an install/uninstall.
@@ -120,6 +121,7 @@ class PluginMenuManager:
120
121
  (self.integrate_plugin_file_openers, "file openers"),
121
122
  (self.integrate_plugin_analysis_tools, "analysis tools"),
122
123
  (self.update_style_menu_with_plugins, "style menu"),
124
+ (self.integrate_plugin_optimization_methods, "optimization methods"),
123
125
  ]:
124
126
  try:
125
127
  method()
@@ -366,6 +368,32 @@ class PluginMenuManager:
366
368
  a.setData(PLUGIN_ACTION_TAG)
367
369
  menu.addAction(a)
368
370
 
371
+ def integrate_plugin_optimization_methods(self) -> None:
372
+ """Inject plugin optimization methods into the 3D Optimization Settings menu.
373
+
374
+ Idempotent: previously-added plugin methods are purged first, so a menu
375
+ rebuild (install/uninstall/reload) re-adds only the currently registered
376
+ ones. Without this, _clean_menu removes the menu action while
377
+ opt3d_actions still holds it, and add_optimization_method's dedupe guard
378
+ would then skip re-adding a still-installed method.
379
+ """
380
+ im = self._im
381
+ methods = getattr(im.host.plugin_manager, "optimization_methods", None)
382
+ if not isinstance(methods, dict):
383
+ return
384
+
385
+ for key, action in list(im.opt3d_actions.items()):
386
+ if action.data() == "plugin_managed":
387
+ im.opt_group.removeAction(action)
388
+ im.optimization_menu.removeAction(action)
389
+ del im.opt3d_actions[key]
390
+ im.opt3d_method_labels.pop(key, None)
391
+
392
+ for key, entry in methods.items():
393
+ im.add_optimization_method(entry["label"], key)
394
+ if key in im.opt3d_actions:
395
+ im.opt3d_actions[key].setData("plugin_managed")
396
+
369
397
  def integrate_plugin_file_openers(self) -> None:
370
398
  """Inject plugin file-opener entries into the File > Import menu."""
371
399
  if not self._im.host.plugin_manager.file_openers:
File without changes