Fixed transform issue and added reset
This commit is contained in:
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user