Fixed transform issue and added reset
This commit is contained in:
@@ -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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,10 +81,22 @@ 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):
|
||||||
"""A canvas-based hierarchical graph visualization control.
|
"""A canvas-based hierarchical graph visualization control.
|
||||||
@@ -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:
|
||||||
|
# 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:
|
def handle_update_view_state(self, transform: Optional[dict] = None, layout_mode: Optional[str] = None):
|
||||||
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):
|
|
||||||
"""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:
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|
||||||
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.
|
"""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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user