From b5abb59332cb369fa31017e1b7a14837faaafe08 Mon Sep 17 00:00:00 2001 From: Kodjo Sossouvi Date: Sun, 22 Feb 2026 19:06:40 +0100 Subject: [PATCH] Fixed transform issue and added reset --- .../assets/core/hierarchical_canvas_graph.js | 2 +- .../controls/HierarchicalCanvasGraph.py | 115 +++++++++++------- src/myfasthtml/controls/InstancesDebugger.py | 30 ++++- 3 files changed, 98 insertions(+), 49 deletions(-) diff --git a/src/myfasthtml/assets/core/hierarchical_canvas_graph.js b/src/myfasthtml/assets/core/hierarchical_canvas_graph.js index 66d073a..894e455 100644 --- a/src/myfasthtml/assets/core/hierarchical_canvas_graph.js +++ b/src/myfasthtml/assets/core/hierarchical_canvas_graph.js @@ -675,7 +675,7 @@ function initHierarchicalCanvasGraph(containerId, options = {}) { function saveViewState() { postEvent('_internal_update_state', { - transform: transform, + transform: JSON.stringify(transform), // Serialize to JSON string for server layout_mode: layoutMode }); } diff --git a/src/myfasthtml/controls/HierarchicalCanvasGraph.py b/src/myfasthtml/controls/HierarchicalCanvasGraph.py index 547e418..8ba80bf 100644 --- a/src/myfasthtml/controls/HierarchicalCanvasGraph.py +++ b/src/myfasthtml/controls/HierarchicalCanvasGraph.py @@ -69,7 +69,7 @@ class Commands(BaseCommands): "UpdateViewState", "Update view transform and layout mode", self._owner, - self._owner._handle_update_view_state + self._owner.handle_update_view_state ).htmx(target=f"#{self._id}", swap='none') def apply_filter(self): @@ -81,9 +81,21 @@ class Commands(BaseCommands): "ApplyFilter", "Apply filter to graph", self._owner, - self._owner._handle_apply_filter, + self._owner.handle_apply_filter, key="#{id}-apply-filter", ).htmx(target=f"#{self._id}") + + def reset_view(self): + """Reset the view transform to default values. + + This command can be used to fix stuck/frozen canvas by resetting zoom/pan state. + """ + return Command( + "ResetTransform", + "Reset view transform", + self._owner, + self._owner.handle_reset_view + ).htmx(target=f"#{self._id}") class HierarchicalCanvasGraph(MultipleInstance): @@ -135,64 +147,81 @@ class HierarchicalCanvasGraph(MultipleInstance): """ return self._state - def get_selected_id(self) -> Optional[str]: - """Get the currently selected node ID. - - Returns: - str or None: The selected node ID, or None if no selection - """ - return self._state.ns_selected_id + # def get_selected_id(self) -> Optional[str]: + # """Get the currently selected node ID. + # + # Returns: + # str or None: The selected node ID, or None if no selection + # """ + # return self._state.ns_selected_id + # + # def set_collapsed(self, node_ids: set): + # """Set the collapsed state of nodes. + # + # Args: + # node_ids: Set of node IDs to mark as collapsed + # """ + # self._state.collapsed = list(node_ids) + # logger.debug(f"set_collapsed: {len(node_ids)} nodes collapsed") + # + # def toggle_node(self, node_id: str): + # """Toggle the collapsed state of a node. + # + # Args: + # node_id: The ID of the node to toggle + # + # Returns: + # self: For chaining + # """ + # collapsed_set = set(self._state.collapsed) + # if node_id in collapsed_set: + # collapsed_set.remove(node_id) + # logger.debug(f"toggle_node: expanded {node_id}") + # else: + # collapsed_set.add(node_id) + # logger.debug(f"toggle_node: collapsed {node_id}") + # + # self._state.collapsed = list(collapsed_set) + # return self - def set_collapsed(self, node_ids: set): - """Set the collapsed state of nodes. - - Args: - node_ids: Set of node IDs to mark as collapsed - """ - self._state.collapsed = list(node_ids) - logger.debug(f"set_collapsed: {len(node_ids)} nodes collapsed") - - def toggle_node(self, node_id: str): - """Toggle the collapsed state of a node. - - Args: - node_id: The ID of the node to toggle - - Returns: - self: For chaining - """ - collapsed_set = set(self._state.collapsed) - if node_id in collapsed_set: - collapsed_set.remove(node_id) - logger.debug(f"toggle_node: expanded {node_id}") - else: - collapsed_set.add(node_id) - logger.debug(f"toggle_node: collapsed {node_id}") - - self._state.collapsed = list(collapsed_set) - return self - - def _handle_update_view_state(self, transform=None, layout_mode=None): + def handle_update_view_state(self, transform: Optional[dict] = None, layout_mode: Optional[str] = None): """Internal handler to update view state from client. Args: - transform: Optional dict with zoom/pan transform state + transform: Optional dict with zoom/pan transform state (received as JSON string) layout_mode: Optional string with layout orientation Returns: str: Empty string (no UI update needed) """ if transform is not None: + # Parse JSON string to dict (sent as JSON.stringify() from JS) + if isinstance(transform, str): + try: + transform = json.loads(transform) + except (json.JSONDecodeError, TypeError) as e: + logger.error(f"Failed to parse transform JSON: {e}") + return "" + self._state.transform = transform - logger.debug(f"Transform updated: {self._state.transform}") if layout_mode is not None: self._state.layout_mode = layout_mode - logger.debug(f"Layout mode updated: {self._state.layout_mode}") return "" - def _handle_apply_filter(self, query_param="text", value=None): + def handle_reset_view(self): + """Internal handler to reset view transform to default values. + + Returns: + self: For HTMX to render the updated graph with reset transform + """ + self._state.transform = {"x": 0, "y": 0, "scale": 1} + self._state.collapsed = [] + logger.debug("Transform and collapsed state reset to defaults") + return self + + def handle_apply_filter(self, query_param="text", value=None): """Internal handler to apply filter and re-render the graph. Args: diff --git a/src/myfasthtml/controls/InstancesDebugger.py b/src/myfasthtml/controls/InstancesDebugger.py index 86161aa..62aaac8 100644 --- a/src/myfasthtml/controls/InstancesDebugger.py +++ b/src/myfasthtml/controls/InstancesDebugger.py @@ -1,8 +1,11 @@ from dataclasses import dataclass +from fasthtml.components import Div, Button + from myfasthtml.controls.HierarchicalCanvasGraph import HierarchicalCanvasGraph, HierarchicalCanvasGraphConf from myfasthtml.controls.Panel import Panel from myfasthtml.controls.Properties import Properties +from myfasthtml.controls.helpers import mk from myfasthtml.core.commands import Command from myfasthtml.core.instances import SingleInstance, UniqueInstance, MultipleInstance, InstancesManager @@ -24,6 +27,7 @@ class InstancesDebugger(SingleInstance): super().__init__(parent, _id=_id) self.conf = conf if conf is not None else InstancesDebuggerConf() self._panel = Panel(self, _id="-panel") + self._canvas_graph = None # Will be created in render self._select_command = Command("ShowInstance", "Display selected Instance", self, @@ -38,16 +42,32 @@ class InstancesDebugger(SingleInstance): "select_node": self._select_command } ) - canvas_graph = HierarchicalCanvasGraph(self, conf=graph_conf, _id="-canvas-graph") - return self._panel.set_main(canvas_graph) + self._canvas_graph = HierarchicalCanvasGraph(self, conf=graph_conf, _id="-canvas-graph") + + # Debug button to reset transform + reset_btn = mk.button( + "🔄 Reset View", + command=self._canvas_graph.commands.reset_view(), + cls="btn btn-sm btn-ghost" + ) + + main_content = Div( + Div(reset_btn, cls="p-2 flex justify-end bg-base-200"), + self._canvas_graph, + cls="flex flex-col h-full" + ) + + return self._panel.set_main(main_content) - def on_select_node(self, event_data: dict): + def on_select_node(self, node_id=None, label=None, kind=None, type=None): """Handle node selection event from canvas graph. Args: - event_data: dict with keys: node_id, label, kind, type + node_id: Selected node's full ID (session#instance_id) + label: Selected node's label + kind: Selected node's kind (root|single|unique|multiple) + type: Selected node's type (class name) """ - node_id = event_data.get("node_id") if not node_id: return None