Fixed transform issue and added reset

This commit is contained in:
2026-02-22 19:06:40 +01:00
parent 3715954222
commit b5abb59332
3 changed files with 98 additions and 49 deletions

View File

@@ -675,7 +675,7 @@ function initHierarchicalCanvasGraph(containerId, options = {}) {
function saveViewState() { function saveViewState() {
postEvent('_internal_update_state', { postEvent('_internal_update_state', {
transform: transform, transform: JSON.stringify(transform), // Serialize to JSON string for server
layout_mode: layoutMode layout_mode: layoutMode
}); });
} }

View File

@@ -69,7 +69,7 @@ class Commands(BaseCommands):
"UpdateViewState", "UpdateViewState",
"Update view transform and layout mode", "Update view transform and layout mode",
self._owner, self._owner,
self._owner._handle_update_view_state self._owner.handle_update_view_state
).htmx(target=f"#{self._id}", swap='none') ).htmx(target=f"#{self._id}", swap='none')
def apply_filter(self): def apply_filter(self):
@@ -81,9 +81,21 @@ class Commands(BaseCommands):
"ApplyFilter", "ApplyFilter",
"Apply filter to graph", "Apply filter to graph",
self._owner, self._owner,
self._owner._handle_apply_filter, self._owner.handle_apply_filter,
key="#{id}-apply-filter", key="#{id}-apply-filter",
).htmx(target=f"#{self._id}") ).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): class HierarchicalCanvasGraph(MultipleInstance):
@@ -135,64 +147,81 @@ class HierarchicalCanvasGraph(MultipleInstance):
""" """
return self._state return self._state
def get_selected_id(self) -> Optional[str]: # def get_selected_id(self) -> Optional[str]:
"""Get the currently selected node ID. # """Get the currently selected node ID.
#
Returns: # Returns:
str or None: The selected node ID, or None if no selection # str or None: The selected node ID, or None if no selection
""" # """
return self._state.ns_selected_id # 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): def handle_update_view_state(self, transform: Optional[dict] = None, layout_mode: Optional[str] = None):
"""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):
"""Internal handler to update view state from client. """Internal handler to update view state from client.
Args: 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 layout_mode: Optional string with layout orientation
Returns: Returns:
str: Empty string (no UI update needed) str: Empty string (no UI update needed)
""" """
if transform is not None: 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 self._state.transform = transform
logger.debug(f"Transform updated: {self._state.transform}")
if layout_mode is not None: if layout_mode is not None:
self._state.layout_mode = layout_mode self._state.layout_mode = layout_mode
logger.debug(f"Layout mode updated: {self._state.layout_mode}")
return "" 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. """Internal handler to apply filter and re-render the graph.
Args: Args:

View File

@@ -1,8 +1,11 @@
from dataclasses import dataclass from dataclasses import dataclass
from fasthtml.components import Div, Button
from myfasthtml.controls.HierarchicalCanvasGraph import HierarchicalCanvasGraph, HierarchicalCanvasGraphConf from myfasthtml.controls.HierarchicalCanvasGraph import HierarchicalCanvasGraph, HierarchicalCanvasGraphConf
from myfasthtml.controls.Panel import Panel from myfasthtml.controls.Panel import Panel
from myfasthtml.controls.Properties import Properties from myfasthtml.controls.Properties import Properties
from myfasthtml.controls.helpers import mk
from myfasthtml.core.commands import Command from myfasthtml.core.commands import Command
from myfasthtml.core.instances import SingleInstance, UniqueInstance, MultipleInstance, InstancesManager from myfasthtml.core.instances import SingleInstance, UniqueInstance, MultipleInstance, InstancesManager
@@ -24,6 +27,7 @@ class InstancesDebugger(SingleInstance):
super().__init__(parent, _id=_id) super().__init__(parent, _id=_id)
self.conf = conf if conf is not None else InstancesDebuggerConf() self.conf = conf if conf is not None else InstancesDebuggerConf()
self._panel = Panel(self, _id="-panel") self._panel = Panel(self, _id="-panel")
self._canvas_graph = None # Will be created in render
self._select_command = Command("ShowInstance", self._select_command = Command("ShowInstance",
"Display selected Instance", "Display selected Instance",
self, self,
@@ -38,16 +42,32 @@ class InstancesDebugger(SingleInstance):
"select_node": self._select_command "select_node": self._select_command
} }
) )
canvas_graph = HierarchicalCanvasGraph(self, conf=graph_conf, _id="-canvas-graph") self._canvas_graph = HierarchicalCanvasGraph(self, conf=graph_conf, _id="-canvas-graph")
return self._panel.set_main(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. """Handle node selection event from canvas graph.
Args: 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: if not node_id:
return None return None