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() {
postEvent('_internal_update_state', {
transform: transform,
transform: JSON.stringify(transform), // Serialize to JSON string for server
layout_mode: layoutMode
});
}

View File

@@ -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,10 +81,22 @@ 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):
"""A canvas-based hierarchical graph visualization control.
@@ -135,64 +147,81 @@ class HierarchicalCanvasGraph(MultipleInstance):
"""
return self._state
def get_selected_id(self) -> Optional[str]:
"""Get the currently selected node 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
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 _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:

View File

@@ -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")
def on_select_node(self, event_data: dict):
# 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, 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