Implemented new InstancesDebugger.py. Based on the HierarchicalCanvasGraph.py
This commit is contained in:
@@ -65,16 +65,23 @@ function initHierarchicalCanvasGraph(containerId, options = {}) {
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Configuration & Constants
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
const NODES = options.nodes || [];
|
||||
const EDGES = options.edges || [];
|
||||
const EVENTS = options.events || {};
|
||||
const NODES = options.nodes || [];
|
||||
const EDGES = options.edges || [];
|
||||
const EVENTS = options.events || {};
|
||||
// filtered_nodes: null = no filter, [] = filter but no matches, [ids] = filter with matches
|
||||
const FILTERED_NODES = options.filtered_nodes === null ? null : new Set(options.filtered_nodes);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Visual Constants
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
const NODE_W = 178; // Node width in pixels
|
||||
const NODE_H = 36; // Node height in pixels
|
||||
const CHEV_ZONE = 26; // Toggle button hit area width (rightmost px of node)
|
||||
const NODE_W = 178; // Node width in pixels
|
||||
const NODE_H_SMALL = 36; // Node height without description
|
||||
const NODE_H_LARGE = 54; // Node height with description
|
||||
const CHEV_ZONE = 26; // Toggle button hit area width (rightmost px of node)
|
||||
|
||||
function getNodeHeight(node) {
|
||||
return node.description ? NODE_H_LARGE : NODE_H_SMALL;
|
||||
}
|
||||
|
||||
const TOGGLE_BTN_SIZE = 32; // Toggle button dimensions
|
||||
const TOGGLE_BTN_POS = 12; // Toggle button offset from corner
|
||||
@@ -97,7 +104,7 @@ function initHierarchicalCanvasGraph(containerId, options = {}) {
|
||||
|
||||
const VERTICAL_MODE_SPACING = {
|
||||
levelGap: 220, // horizontal distance between parent-child (after swap)
|
||||
siblingGap: 14 // gap between siblings (in addition to NODE_H)
|
||||
siblingGap: 14 // gap between siblings (in addition to NODE_H_LARGE)
|
||||
};
|
||||
|
||||
function getSpacing() {
|
||||
@@ -167,6 +174,24 @@ function initHierarchicalCanvasGraph(containerId, options = {}) {
|
||||
return hidden;
|
||||
}
|
||||
|
||||
function getDescendants(nodeId) {
|
||||
/**
|
||||
* Get all descendant node IDs for a given node.
|
||||
* Used to highlight descendants when a node is selected.
|
||||
*/
|
||||
const descendants = new Set();
|
||||
function addDesc(id) {
|
||||
for (const child of childMap[id] || []) {
|
||||
if (!descendants.has(child)) {
|
||||
descendants.add(child);
|
||||
addDesc(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
addDesc(nodeId);
|
||||
return descendants;
|
||||
}
|
||||
|
||||
function visNodes() {
|
||||
const h = getHiddenSet();
|
||||
return NODES.filter(n => !h.has(n.id));
|
||||
@@ -203,16 +228,34 @@ function initHierarchicalCanvasGraph(containerId, options = {}) {
|
||||
const positions = {};
|
||||
for (const n of nodes) positions[n.id] = { x: 0, y: (depth[n.id] || 0) * spacing.levelGap };
|
||||
|
||||
// Sibling stride: in vertical mode, use NODE_H (height); in horizontal mode, use NODE_W (width)
|
||||
const siblingStride = layoutMode === 'vertical'
|
||||
? NODE_H + spacing.siblingGap // Vertical: nodes stack by height
|
||||
: NODE_W + spacing.siblingGap; // Horizontal: nodes spread by width
|
||||
// Create node map for quick access by ID
|
||||
const nodeMap = {};
|
||||
for (const n of nodes) nodeMap[n.id] = n;
|
||||
|
||||
// Sibling stride for horizontal mode
|
||||
const siblingStride = NODE_W + spacing.siblingGap;
|
||||
|
||||
// DFS to assign x (sibling spacing)
|
||||
let slot = 0;
|
||||
let currentX = 0; // For dynamic spacing in vertical mode
|
||||
|
||||
function dfs(id) {
|
||||
const children = cm[id] || [];
|
||||
if (children.length === 0) { positions[id].x = slot++ * siblingStride; return; }
|
||||
if (children.length === 0) {
|
||||
// Leaf node: assign x position based on layout mode
|
||||
if (layoutMode === 'vertical') {
|
||||
// Dynamic spacing based on actual node height
|
||||
const node = nodeMap[id];
|
||||
const h = getNodeHeight(node);
|
||||
positions[id].x = currentX + h / 2; // Center of the node
|
||||
currentX += h + spacing.siblingGap; // Move to next position
|
||||
} else {
|
||||
// Horizontal mode: constant spacing
|
||||
positions[id].x = slot++ * siblingStride;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Non-leaf: recurse children, then center between them
|
||||
for (const c of children) dfs(c);
|
||||
const xs = children.map(c => positions[c].x);
|
||||
positions[id].x = (Math.min(...xs) + Math.max(...xs)) / 2;
|
||||
@@ -320,12 +363,14 @@ function initHierarchicalCanvasGraph(containerId, options = {}) {
|
||||
ctx.clearRect(0, 0, logicalWidth, logicalHeight);
|
||||
drawDotGrid();
|
||||
|
||||
const q = filterQuery.trim().toLowerCase();
|
||||
const matchIds = q
|
||||
? new Set(NODES.filter(n =>
|
||||
n.label.toLowerCase().includes(q) || n.type.toLowerCase().includes(q)
|
||||
).map(n => n.id))
|
||||
: null;
|
||||
// Calculate matchIds based on filter state:
|
||||
// - FILTERED_NODES === null: no filter active → matchIds = null (nothing dimmed)
|
||||
// - FILTERED_NODES.size === 0: filter active, no matches → matchIds = empty Set (everything dimmed)
|
||||
// - FILTERED_NODES.size > 0: filter active with matches → matchIds = FILTERED_NODES (dim non-matches)
|
||||
const matchIds = FILTERED_NODES === null ? null : FILTERED_NODES;
|
||||
|
||||
// Get descendants of selected node for highlighting
|
||||
const descendantIds = selectedId ? getDescendants(selectedId) : new Set();
|
||||
|
||||
const vn = visNodes();
|
||||
|
||||
@@ -338,6 +383,13 @@ function initHierarchicalCanvasGraph(containerId, options = {}) {
|
||||
const p1 = pos[edge.from], p2 = pos[edge.to];
|
||||
if (!p1 || !p2) continue;
|
||||
const dimmed = matchIds && !matchIds.has(edge.from) && !matchIds.has(edge.to);
|
||||
const isHighlighted = selectedId && (edge.from === selectedId || descendantIds.has(edge.from));
|
||||
|
||||
// Get dynamic heights for source and target nodes
|
||||
const node1 = NODES.find(n => n.id === edge.from);
|
||||
const node2 = NODES.find(n => n.id === edge.to);
|
||||
const h1 = node1 ? getNodeHeight(node1) : NODE_H_SMALL;
|
||||
const h2 = node2 ? getNodeHeight(node2) : NODE_H_SMALL;
|
||||
|
||||
const tp1 = transformPos(p1);
|
||||
const tp2 = transformPos(p2);
|
||||
@@ -345,8 +397,8 @@ function initHierarchicalCanvasGraph(containerId, options = {}) {
|
||||
let x1, y1, x2, y2, cx, cy;
|
||||
if (layoutMode === 'horizontal') {
|
||||
// Horizontal: edges go from bottom of parent to top of child
|
||||
x1 = tp1.x; y1 = tp1.y + NODE_H / 2;
|
||||
x2 = tp2.x; y2 = tp2.y - NODE_H / 2;
|
||||
x1 = tp1.x; y1 = tp1.y + h1 / 2;
|
||||
x2 = tp2.x; y2 = tp2.y - h2 / 2;
|
||||
cy = (y1 + y2) / 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
@@ -361,8 +413,16 @@ function initHierarchicalCanvasGraph(containerId, options = {}) {
|
||||
ctx.bezierCurveTo(cx, y1, cx, y2, x2, y2);
|
||||
}
|
||||
|
||||
ctx.strokeStyle = dimmed ? UI_COLORS.edgeDimmed : UI_COLORS.edge;
|
||||
ctx.lineWidth = 1.5;
|
||||
if (isHighlighted) {
|
||||
ctx.strokeStyle = UI_COLORS.nodeBorderSel;
|
||||
ctx.lineWidth = 2.5;
|
||||
} else if (dimmed) {
|
||||
ctx.strokeStyle = UI_COLORS.edgeDimmed;
|
||||
ctx.lineWidth = 1.5;
|
||||
} else {
|
||||
ctx.strokeStyle = UI_COLORS.edge;
|
||||
ctx.lineWidth = 1.5;
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
@@ -372,17 +432,19 @@ function initHierarchicalCanvasGraph(containerId, options = {}) {
|
||||
if (!p) continue;
|
||||
const tp = transformPos(p);
|
||||
const isSel = node.id === selectedId;
|
||||
const isDesc = descendantIds.has(node.id);
|
||||
const isMatch = matchIds !== null && matchIds.has(node.id);
|
||||
const isDim = matchIds !== null && !matchIds.has(node.id);
|
||||
drawNode(node, tp.x, tp.y, isSel, isMatch, isDim, transform.scale);
|
||||
drawNode(node, tp.x, tp.y, isSel, isDesc, isMatch, isDim, transform.scale);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawNode(node, cx, cy, isSel, isMatch, isDim, zoomLevel) {
|
||||
// Nodes always keep same dimensions and horizontal text
|
||||
const hw = NODE_W / 2, hh = NODE_H / 2, r = 6;
|
||||
function drawNode(node, cx, cy, isSel, isDesc, isMatch, isDim, zoomLevel) {
|
||||
// Nodes have dynamic height (with or without description)
|
||||
const nodeH = getNodeHeight(node);
|
||||
const hw = NODE_W / 2, hh = nodeH / 2, r = 6;
|
||||
const x = cx - hw, y = cy - hh;
|
||||
const color = KIND_COLOR[node.kind] || '#334155';
|
||||
|
||||
@@ -393,7 +455,7 @@ function initHierarchicalCanvasGraph(containerId, options = {}) {
|
||||
|
||||
// Background
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x, y, NODE_W, NODE_H, r);
|
||||
ctx.roundRect(x, y, NODE_W, nodeH, r);
|
||||
ctx.fillStyle = isSel ? UI_COLORS.nodeBgSelected : UI_COLORS.nodeBg;
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
@@ -401,16 +463,17 @@ function initHierarchicalCanvasGraph(containerId, options = {}) {
|
||||
// Left color strip (always on left, regardless of mode)
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x, y, NODE_W, NODE_H, r);
|
||||
ctx.roundRect(x, y, NODE_W, nodeH, r);
|
||||
ctx.clip();
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(x, y, 4, NODE_H);
|
||||
ctx.fillRect(x, y, 4, nodeH);
|
||||
ctx.restore();
|
||||
|
||||
// Border
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x, y, NODE_W, NODE_H, r);
|
||||
if (isSel) {
|
||||
ctx.roundRect(x, y, NODE_W, nodeH, r);
|
||||
if (isSel || isDesc) {
|
||||
// Selected node or descendant: orange border
|
||||
ctx.strokeStyle = UI_COLORS.nodeBorderSel;
|
||||
ctx.lineWidth = 1.5;
|
||||
} else if (isMatch) {
|
||||
@@ -432,7 +495,7 @@ function initHierarchicalCanvasGraph(containerId, options = {}) {
|
||||
const badgeW = Math.min(rawW + 8, 66 * zoomLevel);
|
||||
const chevSpace = hasChildren(node.id) ? CHEV_ZONE : 8;
|
||||
const badgeX = (x + NODE_W - chevSpace - badgeW / zoomLevel - 2) * zoomLevel;
|
||||
const badgeY = (y + (NODE_H - 14) / 2) * zoomLevel;
|
||||
const badgeY = (y + (nodeH - 14) / 2) * zoomLevel;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(badgeX, badgeY, badgeW, 14 * zoomLevel, 3 * zoomLevel);
|
||||
ctx.fillStyle = `${color}22`;
|
||||
@@ -446,7 +509,7 @@ function initHierarchicalCanvasGraph(containerId, options = {}) {
|
||||
ctx.fillText(kLabel, Math.round(badgeX + badgeW / 2), Math.round(badgeY + 7 * zoomLevel));
|
||||
ctx.restore();
|
||||
|
||||
// Label (always horizontal) - with dynamic font size for sharp rendering at all zoom levels
|
||||
// Label (centered if no description, top if description) - with dynamic font size for sharp rendering at all zoom levels
|
||||
const labelFontSize = 12 * zoomLevel;
|
||||
ctx.save();
|
||||
ctx.scale(1 / zoomLevel, 1 / zoomLevel);
|
||||
@@ -459,7 +522,19 @@ function initHierarchicalCanvasGraph(containerId, options = {}) {
|
||||
let label = node.label;
|
||||
while (label.length > 3 && ctx.measureText(label).width > labelMaxW) label = label.slice(0, -1);
|
||||
if (label !== node.label) label += '…';
|
||||
ctx.fillText(label, Math.round(labelX), Math.round(cy * zoomLevel));
|
||||
const labelY = node.description ? (cy - 9) * zoomLevel : cy * zoomLevel;
|
||||
ctx.fillText(label, Math.round(labelX), Math.round(labelY));
|
||||
|
||||
// Description (bottom line, only if present)
|
||||
if (node.description) {
|
||||
const descFontSize = 9 * zoomLevel;
|
||||
ctx.font = `${descFontSize}px -apple-system, BlinkMacSystemFont, "Segoe UI", "Inter", "Roboto", sans-serif`;
|
||||
ctx.fillStyle = isDim ? 'rgba(125,133,144,0.3)' : 'rgba(125,133,144,0.7)';
|
||||
let desc = node.description;
|
||||
while (desc.length > 3 && ctx.measureText(desc).width > labelMaxW) desc = desc.slice(0, -1);
|
||||
if (desc !== node.description) desc += '…';
|
||||
ctx.fillText(desc, Math.round(labelX), Math.round((cy + 8) * zoomLevel));
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
// Chevron toggle (same position in both modes)
|
||||
@@ -499,16 +574,26 @@ function initHierarchicalCanvasGraph(containerId, options = {}) {
|
||||
const vn = visNodes();
|
||||
if (vn.length === 0) return;
|
||||
|
||||
// Get transformed positions
|
||||
const tps = vn.map(n => pos[n.id] ? transformPos(pos[n.id]) : null).filter(p => p !== null);
|
||||
if (tps.length === 0) return;
|
||||
// Calculate bounds using dynamic node heights
|
||||
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
||||
for (const n of vn) {
|
||||
const p = pos[n.id];
|
||||
if (!p) continue;
|
||||
const tp = transformPos(p);
|
||||
const h = getNodeHeight(n);
|
||||
minX = Math.min(minX, tp.x - NODE_W / 2);
|
||||
maxX = Math.max(maxX, tp.x + NODE_W / 2);
|
||||
minY = Math.min(minY, tp.y - h / 2);
|
||||
maxY = Math.max(maxY, tp.y + h / 2);
|
||||
}
|
||||
|
||||
if (!isFinite(minX)) return;
|
||||
|
||||
minX -= FIT_PADDING;
|
||||
maxX += FIT_PADDING;
|
||||
minY -= FIT_PADDING;
|
||||
maxY += FIT_PADDING;
|
||||
|
||||
const xs = tps.map(p => p.x);
|
||||
const ys = tps.map(p => p.y);
|
||||
const minX = Math.min(...xs) - NODE_W / 2 - FIT_PADDING;
|
||||
const maxX = Math.max(...xs) + NODE_W / 2 + FIT_PADDING;
|
||||
const minY = Math.min(...ys) - NODE_H / 2 - FIT_PADDING;
|
||||
const maxY = Math.max(...ys) + NODE_H / 2 + FIT_PADDING;
|
||||
const scale = Math.min(logicalWidth / (maxX - minX), logicalHeight / (maxY - minY), FIT_MAX_SCALE);
|
||||
transform.scale = scale;
|
||||
transform.x = (logicalWidth - (minX + maxX) * scale) / 2;
|
||||
@@ -543,12 +628,30 @@ function initHierarchicalCanvasGraph(containerId, options = {}) {
|
||||
const p = pos[n.id];
|
||||
if (!p) continue;
|
||||
const tp = transformPos(p);
|
||||
const nodeH = getNodeHeight(n);
|
||||
|
||||
// Nodes keep same dimensions in both modes
|
||||
if (Math.abs(wx - tp.x) <= NODE_W / 2 && Math.abs(wy - tp.y) <= NODE_H / 2) {
|
||||
// Toggle zone always on the right side of node
|
||||
// Nodes have dynamic height based on description
|
||||
if (Math.abs(wx - tp.x) <= NODE_W / 2 && Math.abs(wy - tp.y) <= nodeH / 2) {
|
||||
const hw = NODE_W / 2, hh = nodeH / 2;
|
||||
const x = tp.x - hw, y = tp.y - hh;
|
||||
|
||||
// Check border (left strip) - 4px wide
|
||||
const isBorder = wx >= x && wx <= x + 4;
|
||||
|
||||
// Check toggle zone (chevron on right)
|
||||
const isToggle = hasChildren(n.id) && wx >= tp.x + NODE_W / 2 - CHEV_ZONE;
|
||||
return { node: n, isToggle };
|
||||
|
||||
// Check badge (type badge) - approximate zone (right side, excluding toggle)
|
||||
// Badge is positioned at: x + NODE_W - chevSpace - badgeW - 2
|
||||
// For hit testing, we use a simplified zone: last ~70px before toggle area
|
||||
const chevSpace = hasChildren(n.id) ? CHEV_ZONE : 8;
|
||||
const badgeZoneStart = x + NODE_W - chevSpace - 70;
|
||||
const badgeZoneEnd = x + NODE_W - chevSpace - 2;
|
||||
const badgeY = y + (nodeH - 14) / 2;
|
||||
const isBadge = !isToggle && wx >= badgeZoneStart && wx <= badgeZoneEnd
|
||||
&& wy >= badgeY && wy <= badgeY + 14;
|
||||
|
||||
return { node: n, isToggle, isBadge, isBorder };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -564,7 +667,7 @@ function initHierarchicalCanvasGraph(containerId, options = {}) {
|
||||
return;
|
||||
}
|
||||
htmx.ajax('POST', handler.url, {
|
||||
values: { event_data: JSON.stringify(eventData) },
|
||||
values: eventData, // Send data as separate fields (default command engine behavior)
|
||||
target: handler.target || 'body',
|
||||
swap: handler.swap || 'none'
|
||||
});
|
||||
@@ -643,11 +746,25 @@ function initHierarchicalCanvasGraph(containerId, options = {}) {
|
||||
const hit = hitTest(e.clientX - rect.left, e.clientY - rect.top);
|
||||
if (hit) {
|
||||
if (hit.isToggle) {
|
||||
// Save screen position of clicked node before layout change
|
||||
const oldPos = pos[hit.node.id];
|
||||
const oldTp = transformPos(oldPos);
|
||||
const screenX = oldTp.x * transform.scale + transform.x;
|
||||
const screenY = oldTp.y * transform.scale + transform.y;
|
||||
|
||||
// Toggle collapse
|
||||
if (collapsed.has(hit.node.id)) collapsed.delete(hit.node.id);
|
||||
else collapsed.add(hit.node.id);
|
||||
recomputeLayout();
|
||||
|
||||
// Adjust transform to keep clicked node at same screen position
|
||||
const newPos = pos[hit.node.id];
|
||||
if (newPos) {
|
||||
const newTp = transformPos(newPos);
|
||||
transform.x = screenX - newTp.x * transform.scale;
|
||||
transform.y = screenY - newTp.y * transform.scale;
|
||||
}
|
||||
|
||||
// Post toggle_node event
|
||||
postEvent('toggle_node', {
|
||||
node_id: hit.node.id,
|
||||
@@ -658,6 +775,12 @@ function initHierarchicalCanvasGraph(containerId, options = {}) {
|
||||
if (selectedId && !visNodes().find(n => n.id === selectedId)) {
|
||||
selectedId = null;
|
||||
}
|
||||
} else if (hit.isBadge) {
|
||||
// Badge click: filter by type
|
||||
postEvent('_internal_filter_by_type', { query_param: 'type', value: hit.node.type });
|
||||
} else if (hit.isBorder) {
|
||||
// Border click: filter by kind
|
||||
postEvent('_internal_filter_by_kind', { query_param: 'kind', value: hit.node.kind });
|
||||
} else {
|
||||
selectedId = hit.node.id;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user