From cba2123e4eeea0851a9d4d065fb8333e54a3ce07 Mon Sep 17 00:00:00 2001 From: Paul Sardin Date: Tue, 10 Feb 2026 09:42:16 +0100 Subject: [PATCH 1/4] Add context menu signals to HppNativeGraphWidget --- include/hpp/plot/hpp-native-graph.hh | 14 ++++++++++++++ src/hpp-native-graph.cc | 6 ++++++ 2 files changed, 20 insertions(+) diff --git a/include/hpp/plot/hpp-native-graph.hh b/include/hpp/plot/hpp-native-graph.hh index 9407dd6..7d5263e 100644 --- a/include/hpp/plot/hpp-native-graph.hh +++ b/include/hpp/plot/hpp-native-graph.hh @@ -32,6 +32,7 @@ #ifndef HPP_PLOT_HPP_NATIVE_GRAPH_HH #define HPP_PLOT_HPP_NATIVE_GRAPH_HH +#include #include #include #include @@ -87,6 +88,19 @@ class HppNativeGraphWidget : public GraphWidget { /// \brief Display edge target constraints in the constraint panel void displayEdgeTargetConstraints(std::size_t id); + Q_SIGNALS: + /// \brief Emitted before showing node context menu, allows external code to add actions + /// \param nodeId The ID of the node + /// \param nodeName The name of the node (state) + /// \param menu Pointer to the context menu (can add actions before it's shown) + void nodeContextMenuAboutToShow(std::size_t nodeId, QString nodeName, QMenu* menu); + + /// \brief Emitted before showing edge context menu, allows external code to add actions + /// \param edgeId The ID of the edge + /// \param edgeName The name of the edge (transition) + /// \param menu Pointer to the context menu (can add actions before it's shown) + void edgeContextMenuAboutToShow(std::size_t edgeId, QString edgeName, QMenu* menu); + protected: /// \brief Fill scene from Graph object /// Reads nodes and edges directly from the C++ graph structure diff --git a/src/hpp-native-graph.cc b/src/hpp-native-graph.cc index 8d37721..bfce05b 100644 --- a/src/hpp-native-graph.cc +++ b/src/hpp-native-graph.cc @@ -433,6 +433,9 @@ void HppNativeGraphWidget::nodeContextMenu(QGVNode* node) { cm.addAction("Clear highlight", [this]() { highlightNode(-1); }); + // Allow external code (Python) to add custom actions + Q_EMIT nodeContextMenuAboutToShow(ni.id, ni.name, &cm); + cm.exec(QCursor::pos()); currentId_ = savedId; @@ -478,6 +481,9 @@ void HppNativeGraphWidget::edgeContextMenu(QGVEdge* edge) { cm.addAction("Clear highlight", [this]() { highlightEdge(-1); }); + // Allow external code (Python) to add custom actions + Q_EMIT edgeContextMenuAboutToShow(ei.id, ei.name, &cm); + cm.exec(QCursor::pos()); currentId_ = savedId; From 6d0de1b2dbcdac860b4e72a95e54c269393fd5a4 Mon Sep 17 00:00:00 2001 From: Paul Sardin Date: Tue, 10 Feb 2026 09:42:23 +0100 Subject: [PATCH 2/4] Add interactive Python graph viewer with context menu callbacks --- CMakeLists.txt | 6 +- src/pyhpp_plot/__init__.py | 21 +++ src/pyhpp_plot/graph_viewer.cc | 111 +++++++++++ src/pyhpp_plot/interactive_viewer.py | 263 +++++++++++++++++++++++++++ 4 files changed, 398 insertions(+), 3 deletions(-) create mode 100644 src/pyhpp_plot/__init__.py create mode 100644 src/pyhpp_plot/interactive_viewer.py diff --git a/CMakeLists.txt b/CMakeLists.txt index b15c0bb..584f42a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -195,7 +195,7 @@ set_target_properties( install(TARGETS pyhpp_plot_graph_viewer DESTINATION "${PYTHON_SITELIB}/pyhpp_plot") -file(WRITE "${CMAKE_BINARY_DIR}/pyhpp_plot/__init__.py" - "from .graph_viewer import show_graph\n") -install(FILES "${CMAKE_BINARY_DIR}/pyhpp_plot/__init__.py" +# Install Python modules +install(FILES "${CMAKE_SOURCE_DIR}/src/pyhpp_plot/__init__.py" + "${CMAKE_SOURCE_DIR}/src/pyhpp_plot/interactive_viewer.py" DESTINATION "${PYTHON_SITELIB}/pyhpp_plot") diff --git a/src/pyhpp_plot/__init__.py b/src/pyhpp_plot/__init__.py new file mode 100644 index 0000000..7d93d70 --- /dev/null +++ b/src/pyhpp_plot/__init__.py @@ -0,0 +1,21 @@ +from .graph_viewer import ( + MenuActionProxy, + show_graph, + show_graph_blocking, + show_interactive_graph, +) +from .interactive_viewer import ( + GraphViewerThread, + InteractiveGraphViewer, + show_interactive_graph_threaded, +) + +__all__ = [ + "show_graph", + "show_graph_blocking", + "show_interactive_graph", + "MenuActionProxy", + "InteractiveGraphViewer", + "GraphViewerThread", + "show_interactive_graph_threaded", +] diff --git a/src/pyhpp_plot/graph_viewer.cc b/src/pyhpp_plot/graph_viewer.cc index 8127591..2d9b9c9 100644 --- a/src/pyhpp_plot/graph_viewer.cc +++ b/src/pyhpp_plot/graph_viewer.cc @@ -88,6 +88,31 @@ GraphPtr_t extractGraph(bp::object py_graph) { "'_get_native_graph()' method."); } +/// Simple wrapper to allow Python to add menu actions +class MenuActionProxy { +public: + MenuActionProxy(QMenu* menu) : menu_(menu) {} + + void addAction(const std::string& text, bp::object callback) { + QAction* action = menu_->addAction(QString::fromStdString(text)); + // Store callback and connect + QObject::connect(action, &QAction::triggered, [callback]() { + try { + callback(); + } catch (const bp::error_already_set&) { + PyErr_Print(); + } + }); + } + + void addSeparator() { + menu_->addSeparator(); + } + +private: + QMenu* menu_; +}; + /// This function blocks until the window is closed void showGraphBlocking(bp::object py_graph) { GraphPtr_t graph = extractGraph(py_graph); @@ -121,12 +146,98 @@ void showGraphBlocking(bp::object py_graph) { app.exec(); } +/// Interactive version with Python callbacks for context menus +void showInteractiveGraph(bp::object py_graph, + bp::object node_callback, + bp::object edge_callback) { + GraphPtr_t graph = extractGraph(py_graph); + + if (!graph) { + throw std::runtime_error("Graph is null"); + } + + int argc = 1; + static char app_name[] = "hpp-plot-interactive"; + char* argv[] = {app_name, nullptr}; + + QApplication app(argc, argv); + + // Create main window + QMainWindow window; + window.setWindowTitle( + QString::fromStdString("Constraint Graph: " + graph->name())); + window.resize(1200, 800); + + // Create widget + hpp::plot::HppNativeGraphWidget widget(graph, &window); + widget.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + window.setCentralWidget(&widget); + + // Connect Qt signals to Python callbacks + if (!node_callback.is_none()) { + QObject::connect(&widget, &hpp::plot::HppNativeGraphWidget::nodeContextMenuAboutToShow, + [node_callback](std::size_t nodeId, QString nodeName, QMenu* menu) { + try { + MenuActionProxy proxy(menu); + node_callback(nodeId, nodeName.toStdString(), boost::ref(proxy)); + } catch (const bp::error_already_set&) { + PyErr_Print(); + } + }); + } + + if (!edge_callback.is_none()) { + QObject::connect(&widget, &hpp::plot::HppNativeGraphWidget::edgeContextMenuAboutToShow, + [edge_callback](std::size_t edgeId, QString edgeName, QMenu* menu) { + try { + MenuActionProxy proxy(menu); + edge_callback(edgeId, edgeName.toStdString(), boost::ref(proxy)); + } catch (const bp::error_already_set&) { + PyErr_Print(); + } + }); + } + + // Show and refresh + window.show(); + widget.updateGraph(); + + // Run event loop (blocking) + app.exec(); +} + } // namespace BOOST_PYTHON_MODULE(graph_viewer) { + // Expose MenuActionProxy for adding menu actions from Python + bp::class_("MenuActionProxy", bp::no_init) + .def("addAction", &MenuActionProxy::addAction, + (bp::arg("text"), bp::arg("callback")), + "Add an action to the context menu.\n\n" + "Args:\n" + " text: The text label for the menu action\n" + " callback: Python callable to invoke when action is triggered\n") + .def("addSeparator", &MenuActionProxy::addSeparator, + "Add a separator line to the context menu\n"); + bp::def("show_graph", &showGraphBlocking, bp::arg("graph"), "Show constraint graph in a Qt viewer (blocking).\n\n" "This function blocks until the viewer window is closed.\n\n" "Args:\n" " graph: The Graph object from pyhpp.manipulation\n"); + + // Alias for backwards compatibility + bp::def("show_graph_blocking", &showGraphBlocking, bp::arg("graph"), + "Alias for show_graph() for backwards compatibility.\n"); + + bp::def("show_interactive_graph", &showInteractiveGraph, + (bp::arg("graph"), bp::arg("node_callback") = bp::object(), + bp::arg("edge_callback") = bp::object()), + "Show constraint graph with interactive context menu callbacks.\n\n" + "This function blocks until the viewer window is closed.\n" + "Callbacks are invoked when user right-clicks on nodes/edges.\n\n" + "Args:\n" + " graph: The Graph object from pyhpp.manipulation\n" + " node_callback: Optional callback(node_id, node_name, menu_proxy) for node menus\n" + " edge_callback: Optional callback(edge_id, edge_name, menu_proxy) for edge menus\n"); } diff --git a/src/pyhpp_plot/interactive_viewer.py b/src/pyhpp_plot/interactive_viewer.py new file mode 100644 index 0000000..3b61c03 --- /dev/null +++ b/src/pyhpp_plot/interactive_viewer.py @@ -0,0 +1,263 @@ +"""Interactive constraint graph viewer with Python callbacks. + +This module provides a wrapper around the native graph viewer that allows +Python code to add custom actions to context menus for nodes and edges. +Implements all features from the CORBA hpp-monitoring-plugin. +""" + +import threading +import queue + + +class InteractiveGraphViewer: + """Wrapper for HppNativeGraphWidget with Python-based context menu actions. + + """ + + def __init__(self, graph, problem, config_callback=None): + """Initialize the interactive graph viewer. + + Args: + graph: PyWGraph from pyhpp.manipulation + problem: PyWProblem from pyhpp.manipulation + config_callback: Optional callable(config, label) that receives + generated configurations + """ + self.graph = graph + self.problem = problem + self.config_callback = config_callback or (lambda c, l: None) + self.current_config = None + + def show(self): + """Show graph viewer (blocking - runs Qt event loop until window closes).""" + from pyhpp_plot.graph_viewer import show_interactive_graph + + show_interactive_graph( + self.graph, + node_callback=self._on_node_context_menu, + edge_callback=self._on_edge_context_menu + ) + + def _on_node_context_menu(self, node_id, node_name, menu): + """Add custom actions to node context menu. + + Args: + node_id: ID of the node (state) - from C++ graph + node_name: Name of the node (state) - from C++ graph + menu: MenuActionProxy for adding actions + """ + menu.addSeparator() + + menu.addAction( + "&Generate random config", + lambda: self._generate_random_config(node_name) + ) + + menu.addAction( + "Generate from ¤t config", + lambda: self._generate_from_current_config(node_name) + ) + + menu.addAction( + "Set as &target state", + lambda: self._set_target_state(node_name) + ) + + def _on_edge_context_menu(self, edge_id, edge_name, menu): + """Add custom actions to edge context menu. + + Args: + edge_id: ID of the edge (transition) - from C++ graph + edge_name: Name of the edge (transition) - from C++ graph + menu: MenuActionProxy for adding actions + """ + menu.addSeparator() + + menu.addAction( + "&Extend current config", + lambda: self._extend_current_to_current(edge_name) + ) + + menu.addAction( + "&Extend current config to random config", + lambda: self._extend_current_to_random(edge_name) + ) + + def _generate_random_config(self, state_name): + """Generate random config and project to state. + + Args: + state_name: Name of the state + """ + try: + state = self.graph.getState(state_name) + shooter = self.problem.configurationShooter() + + min_error = float('inf') + for i in range(20): + q_random = shooter.shoot() + success, q_proj, error = self.graph.applyStateConstraints( + state, q_random) + + if success: + self.current_config = q_proj + self.config_callback( + q_proj, f"Random config in state: {state_name}") + print(f"Generated config for state '{state_name}'") + return + + if error < min_error: + min_error = error + + print(f"Failed to generate config for state '{state_name}' " + f"after 20 attempts (min error: {min_error:.6f})") + except Exception as e: + print(f"Error generating random config: {e}") + import traceback + traceback.print_exc() + + def _generate_from_current_config(self, state_name): + """Project current config to state. + + Args: + state_name: Name of the state + """ + try: + if self.current_config is None: + print("No current config available. Generate a config first.") + return + + state = self.graph.getState(state_name) + success, q_proj, error = self.graph.applyStateConstraints( + state, self.current_config) + + if success: + self.current_config = q_proj + self.config_callback( + q_proj, f"Current config projected to state: {state_name}") + print(f"Projected current config to state '{state_name}'") + else: + print(f"Failed to project current config to state " + f"'{state_name}' (error: {error:.6f})") + except Exception as e: + print(f"Error projecting current config: {e}") + import traceback + traceback.print_exc() + + def _set_target_state(self, state_name): + """Set state as goal for planning. + + Args: + state_name: Name of the state + """ + try: + state = self.graph.getState(state_name) + shooter = self.problem.configurationShooter() + + for i in range(20): + q_random = shooter.shoot() + success, q_goal, error = self.graph.applyStateConstraints( + state, q_random) + + if success: + self.problem.addGoalConfig(q_goal) + print(f"Added goal config in state '{state_name}'") + return + + print(f"Failed to generate goal config for state '{state_name}'") + except Exception as e: + print(f"Error setting target state: {e}") + import traceback + traceback.print_exc() + + def _extend_current_to_current(self, edge_name): + """Generate target config along edge from current config. + calls generateTargetConfig(edge, current, current). + + Args: + edge_name: Name of the edge + """ + try: + if self.current_config is None: + print("No current config available. Generate a config first.") + return + + edge = self.graph.getTransition(edge_name) + success, q_out, error = self.graph.generateTargetConfig( + edge, self.current_config, self.current_config) + + if success: + self.current_config = q_out + self.config_callback( + q_out, f"Extended along edge: {edge_name}") + print(f"Extended current config along edge '{edge_name}'") + else: + print(f"Failed to extend along edge '{edge_name}' " + f"(error: {error:.6f})") + except Exception as e: + print(f"Error extending current config: {e}") + import traceback + traceback.print_exc() + + def _extend_current_to_random(self, edge_name): + """Generate target config along edge to random config. + calls generateTargetConfig(edge, current, random). + + Args: + edge_name: Name of the edge + """ + try: + if self.current_config is None: + print("No current config available. Generate a config first.") + return + + edge = self.graph.getTransition(edge_name) + shooter = self.problem.configurationShooter() + q_random = shooter.shoot() + + success, q_out, error = self.graph.generateTargetConfig( + edge, self.current_config, q_random) + + if success: + self.current_config = q_out + self.config_callback( + q_out, f"Extended along edge to random: {edge_name}") + print(f"Extended to random config along edge '{edge_name}'") + else: + print(f"Failed to extend to random along edge '{edge_name}' " + f"(error: {error:.6f})") + except Exception as e: + print(f"Error extending to random config: {e}") + import traceback + traceback.print_exc() + + +class GraphViewerThread(threading.Thread): + """Runs graph viewer in separate daemon thread with Qt event loop.""" + + def __init__(self, graph, problem, config_callback): + super().__init__(daemon=True, name="GraphViewerThread") + self.graph = graph + self.problem = problem + self.config_callback = config_callback + + def run(self): + viewer = InteractiveGraphViewer( + self.graph, self.problem, self.config_callback) + viewer.show() + + +def show_interactive_graph_threaded(graph, problem, config_callback): + """Show interactive graph viewer in a separate thread (non-blocking). + + Args: + graph: PyWGraph from pyhpp.manipulation + problem: PyWProblem from pyhpp.manipulation + config_callback: Callable(config, label) called when configs are generated + + Returns: + GraphViewerThread object (already started) + """ + thread = GraphViewerThread(graph, problem, config_callback) + thread.start() + return thread From 61345d44b2f7537dc5e9e71303f8359aa4c40242 Mon Sep 17 00:00:00 2001 From: Paul Sardin Date: Tue, 10 Feb 2026 10:06:33 +0100 Subject: [PATCH 3/4] Fix pre-commit syntax errors --- src/pyhpp_plot/interactive_viewer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyhpp_plot/interactive_viewer.py b/src/pyhpp_plot/interactive_viewer.py index 3b61c03..39a66a7 100644 --- a/src/pyhpp_plot/interactive_viewer.py +++ b/src/pyhpp_plot/interactive_viewer.py @@ -25,7 +25,7 @@ def __init__(self, graph, problem, config_callback=None): """ self.graph = graph self.problem = problem - self.config_callback = config_callback or (lambda c, l: None) + self.config_callback = config_callback or (lambda config, label: None) self.current_config = None def show(self): @@ -156,7 +156,7 @@ def _set_target_state(self, state_name): for i in range(20): q_random = shooter.shoot() - success, q_goal, error = self.graph.applyStateConstraints( + success, q_goal, _error = self.graph.applyStateConstraints( state, q_random) if success: From 9469a1ccf882278a4dafb703dae2e88fcf0ef2bf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 09:07:24 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- include/hpp/plot/hpp-native-graph.hh | 18 ++++--- src/pyhpp_plot/__init__.py | 6 +-- src/pyhpp_plot/graph_viewer.cc | 60 ++++++++++----------- src/pyhpp_plot/interactive_viewer.py | 78 ++++++++++++++++------------ 4 files changed, 90 insertions(+), 72 deletions(-) diff --git a/include/hpp/plot/hpp-native-graph.hh b/include/hpp/plot/hpp-native-graph.hh index 7d5263e..121b2f4 100644 --- a/include/hpp/plot/hpp-native-graph.hh +++ b/include/hpp/plot/hpp-native-graph.hh @@ -89,17 +89,23 @@ class HppNativeGraphWidget : public GraphWidget { void displayEdgeTargetConstraints(std::size_t id); Q_SIGNALS: - /// \brief Emitted before showing node context menu, allows external code to add actions + /// \brief Emitted before showing node context menu, allows external code to + /// add actions /// \param nodeId The ID of the node /// \param nodeName The name of the node (state) - /// \param menu Pointer to the context menu (can add actions before it's shown) - void nodeContextMenuAboutToShow(std::size_t nodeId, QString nodeName, QMenu* menu); + /// \param menu Pointer to the context menu (can add actions before it's + /// shown) + void nodeContextMenuAboutToShow(std::size_t nodeId, QString nodeName, + QMenu* menu); - /// \brief Emitted before showing edge context menu, allows external code to add actions + /// \brief Emitted before showing edge context menu, allows external code to + /// add actions /// \param edgeId The ID of the edge /// \param edgeName The name of the edge (transition) - /// \param menu Pointer to the context menu (can add actions before it's shown) - void edgeContextMenuAboutToShow(std::size_t edgeId, QString edgeName, QMenu* menu); + /// \param menu Pointer to the context menu (can add actions before it's + /// shown) + void edgeContextMenuAboutToShow(std::size_t edgeId, QString edgeName, + QMenu* menu); protected: /// \brief Fill scene from Graph object diff --git a/src/pyhpp_plot/__init__.py b/src/pyhpp_plot/__init__.py index 7d93d70..c5011a2 100644 --- a/src/pyhpp_plot/__init__.py +++ b/src/pyhpp_plot/__init__.py @@ -11,11 +11,11 @@ ) __all__ = [ + "GraphViewerThread", + "InteractiveGraphViewer", + "MenuActionProxy", "show_graph", "show_graph_blocking", "show_interactive_graph", - "MenuActionProxy", - "InteractiveGraphViewer", - "GraphViewerThread", "show_interactive_graph_threaded", ] diff --git a/src/pyhpp_plot/graph_viewer.cc b/src/pyhpp_plot/graph_viewer.cc index 2d9b9c9..a39854d 100644 --- a/src/pyhpp_plot/graph_viewer.cc +++ b/src/pyhpp_plot/graph_viewer.cc @@ -90,7 +90,7 @@ GraphPtr_t extractGraph(bp::object py_graph) { /// Simple wrapper to allow Python to add menu actions class MenuActionProxy { -public: + public: MenuActionProxy(QMenu* menu) : menu_(menu) {} void addAction(const std::string& text, bp::object callback) { @@ -105,11 +105,9 @@ class MenuActionProxy { }); } - void addSeparator() { - menu_->addSeparator(); - } + void addSeparator() { menu_->addSeparator(); } -private: + private: QMenu* menu_; }; @@ -147,9 +145,8 @@ void showGraphBlocking(bp::object py_graph) { } /// Interactive version with Python callbacks for context menus -void showInteractiveGraph(bp::object py_graph, - bp::object node_callback, - bp::object edge_callback) { +void showInteractiveGraph(bp::object py_graph, bp::object node_callback, + bp::object edge_callback) { GraphPtr_t graph = extractGraph(py_graph); if (!graph) { @@ -175,27 +172,29 @@ void showInteractiveGraph(bp::object py_graph, // Connect Qt signals to Python callbacks if (!node_callback.is_none()) { - QObject::connect(&widget, &hpp::plot::HppNativeGraphWidget::nodeContextMenuAboutToShow, - [node_callback](std::size_t nodeId, QString nodeName, QMenu* menu) { - try { - MenuActionProxy proxy(menu); - node_callback(nodeId, nodeName.toStdString(), boost::ref(proxy)); - } catch (const bp::error_already_set&) { - PyErr_Print(); - } - }); + QObject::connect( + &widget, &hpp::plot::HppNativeGraphWidget::nodeContextMenuAboutToShow, + [node_callback](std::size_t nodeId, QString nodeName, QMenu* menu) { + try { + MenuActionProxy proxy(menu); + node_callback(nodeId, nodeName.toStdString(), boost::ref(proxy)); + } catch (const bp::error_already_set&) { + PyErr_Print(); + } + }); } if (!edge_callback.is_none()) { - QObject::connect(&widget, &hpp::plot::HppNativeGraphWidget::edgeContextMenuAboutToShow, - [edge_callback](std::size_t edgeId, QString edgeName, QMenu* menu) { - try { - MenuActionProxy proxy(menu); - edge_callback(edgeId, edgeName.toStdString(), boost::ref(proxy)); - } catch (const bp::error_already_set&) { - PyErr_Print(); - } - }); + QObject::connect( + &widget, &hpp::plot::HppNativeGraphWidget::edgeContextMenuAboutToShow, + [edge_callback](std::size_t edgeId, QString edgeName, QMenu* menu) { + try { + MenuActionProxy proxy(menu); + edge_callback(edgeId, edgeName.toStdString(), boost::ref(proxy)); + } catch (const bp::error_already_set&) { + PyErr_Print(); + } + }); } // Show and refresh @@ -210,7 +209,8 @@ void showInteractiveGraph(bp::object py_graph, BOOST_PYTHON_MODULE(graph_viewer) { // Expose MenuActionProxy for adding menu actions from Python - bp::class_("MenuActionProxy", bp::no_init) + bp::class_("MenuActionProxy", + bp::no_init) .def("addAction", &MenuActionProxy::addAction, (bp::arg("text"), bp::arg("callback")), "Add an action to the context menu.\n\n" @@ -238,6 +238,8 @@ BOOST_PYTHON_MODULE(graph_viewer) { "Callbacks are invoked when user right-clicks on nodes/edges.\n\n" "Args:\n" " graph: The Graph object from pyhpp.manipulation\n" - " node_callback: Optional callback(node_id, node_name, menu_proxy) for node menus\n" - " edge_callback: Optional callback(edge_id, edge_name, menu_proxy) for edge menus\n"); + " node_callback: Optional callback(node_id, node_name, " + "menu_proxy) for node menus\n" + " edge_callback: Optional callback(edge_id, edge_name, " + "menu_proxy) for edge menus\n"); } diff --git a/src/pyhpp_plot/interactive_viewer.py b/src/pyhpp_plot/interactive_viewer.py index 39a66a7..807a157 100644 --- a/src/pyhpp_plot/interactive_viewer.py +++ b/src/pyhpp_plot/interactive_viewer.py @@ -6,13 +6,10 @@ """ import threading -import queue class InteractiveGraphViewer: - """Wrapper for HppNativeGraphWidget with Python-based context menu actions. - - """ + """Wrapper for HppNativeGraphWidget with Python-based context menu actions.""" def __init__(self, graph, problem, config_callback=None): """Initialize the interactive graph viewer. @@ -35,7 +32,7 @@ def show(self): show_interactive_graph( self.graph, node_callback=self._on_node_context_menu, - edge_callback=self._on_edge_context_menu + edge_callback=self._on_edge_context_menu, ) def _on_node_context_menu(self, node_id, node_name, menu): @@ -49,18 +46,16 @@ def _on_node_context_menu(self, node_id, node_name, menu): menu.addSeparator() menu.addAction( - "&Generate random config", - lambda: self._generate_random_config(node_name) + "&Generate random config", lambda: self._generate_random_config(node_name) ) menu.addAction( "Generate from ¤t config", - lambda: self._generate_from_current_config(node_name) + lambda: self._generate_from_current_config(node_name), ) menu.addAction( - "Set as &target state", - lambda: self._set_target_state(node_name) + "Set as &target state", lambda: self._set_target_state(node_name) ) def _on_edge_context_menu(self, edge_id, edge_name, menu): @@ -74,13 +69,12 @@ def _on_edge_context_menu(self, edge_id, edge_name, menu): menu.addSeparator() menu.addAction( - "&Extend current config", - lambda: self._extend_current_to_current(edge_name) + "&Extend current config", lambda: self._extend_current_to_current(edge_name) ) menu.addAction( "&Extend current config to random config", - lambda: self._extend_current_to_random(edge_name) + lambda: self._extend_current_to_random(edge_name), ) def _generate_random_config(self, state_name): @@ -93,27 +87,32 @@ def _generate_random_config(self, state_name): state = self.graph.getState(state_name) shooter = self.problem.configurationShooter() - min_error = float('inf') + min_error = float("inf") for i in range(20): q_random = shooter.shoot() success, q_proj, error = self.graph.applyStateConstraints( - state, q_random) + state, q_random + ) if success: self.current_config = q_proj self.config_callback( - q_proj, f"Random config in state: {state_name}") + q_proj, f"Random config in state: {state_name}" + ) print(f"Generated config for state '{state_name}'") return if error < min_error: min_error = error - print(f"Failed to generate config for state '{state_name}' " - f"after 20 attempts (min error: {min_error:.6f})") + print( + f"Failed to generate config for state '{state_name}' " + f"after 20 attempts (min error: {min_error:.6f})" + ) except Exception as e: print(f"Error generating random config: {e}") import traceback + traceback.print_exc() def _generate_from_current_config(self, state_name): @@ -129,19 +128,24 @@ def _generate_from_current_config(self, state_name): state = self.graph.getState(state_name) success, q_proj, error = self.graph.applyStateConstraints( - state, self.current_config) + state, self.current_config + ) if success: self.current_config = q_proj self.config_callback( - q_proj, f"Current config projected to state: {state_name}") + q_proj, f"Current config projected to state: {state_name}" + ) print(f"Projected current config to state '{state_name}'") else: - print(f"Failed to project current config to state " - f"'{state_name}' (error: {error:.6f})") + print( + f"Failed to project current config to state " + f"'{state_name}' (error: {error:.6f})" + ) except Exception as e: print(f"Error projecting current config: {e}") import traceback + traceback.print_exc() def _set_target_state(self, state_name): @@ -157,7 +161,8 @@ def _set_target_state(self, state_name): for i in range(20): q_random = shooter.shoot() success, q_goal, _error = self.graph.applyStateConstraints( - state, q_random) + state, q_random + ) if success: self.problem.addGoalConfig(q_goal) @@ -168,6 +173,7 @@ def _set_target_state(self, state_name): except Exception as e: print(f"Error setting target state: {e}") import traceback + traceback.print_exc() def _extend_current_to_current(self, edge_name): @@ -184,19 +190,19 @@ def _extend_current_to_current(self, edge_name): edge = self.graph.getTransition(edge_name) success, q_out, error = self.graph.generateTargetConfig( - edge, self.current_config, self.current_config) + edge, self.current_config, self.current_config + ) if success: self.current_config = q_out - self.config_callback( - q_out, f"Extended along edge: {edge_name}") + self.config_callback(q_out, f"Extended along edge: {edge_name}") print(f"Extended current config along edge '{edge_name}'") else: - print(f"Failed to extend along edge '{edge_name}' " - f"(error: {error:.6f})") + print(f"Failed to extend along edge '{edge_name}' (error: {error:.6f})") except Exception as e: print(f"Error extending current config: {e}") import traceback + traceback.print_exc() def _extend_current_to_random(self, edge_name): @@ -216,19 +222,24 @@ def _extend_current_to_random(self, edge_name): q_random = shooter.shoot() success, q_out, error = self.graph.generateTargetConfig( - edge, self.current_config, q_random) + edge, self.current_config, q_random + ) if success: self.current_config = q_out self.config_callback( - q_out, f"Extended along edge to random: {edge_name}") + q_out, f"Extended along edge to random: {edge_name}" + ) print(f"Extended to random config along edge '{edge_name}'") else: - print(f"Failed to extend to random along edge '{edge_name}' " - f"(error: {error:.6f})") + print( + f"Failed to extend to random along edge '{edge_name}' " + f"(error: {error:.6f})" + ) except Exception as e: print(f"Error extending to random config: {e}") import traceback + traceback.print_exc() @@ -242,8 +253,7 @@ def __init__(self, graph, problem, config_callback): self.config_callback = config_callback def run(self): - viewer = InteractiveGraphViewer( - self.graph, self.problem, self.config_callback) + viewer = InteractiveGraphViewer(self.graph, self.problem, self.config_callback) viewer.show()