feat: add canvas node highlighting for the info panel and refine its update logic to manage highlights.

This commit is contained in:
AEmotionStudio
2026-01-05 17:22:24 -08:00
parent a02efd032d
commit 56e0277b6c
10 changed files with 258 additions and 34 deletions
+117
View File
@@ -0,0 +1,117 @@
/**
* CanvasHighlighter.ts
*
* Handles drawing a visual highlight on the main canvas around the currently inspected node.
* Hooks into LiteGraph's canvas drawing cycle.
*/
import { ComfyApp, ComfyNode } from '../types/comfyui';
declare const app: ComfyApp;
export class CanvasHighlighter {
private originalOnDrawForeground: ((ctx: CanvasRenderingContext2D, visible_nodes: any) => void) | null = null;
private highlightedNodeId: number | null = null;
// Configuration
private readonly HIGHLIGHT_COLOR = '#007bff'; // Bootstrap blue
private readonly HIGHLIGHT_WIDTH = 2; // px
private readonly HIGHLIGHT_PADDING = 0; // px
constructor() {
this.hookCanvas();
}
/**
* Hook into the main canvas onDrawForeground method.
*/
private hookCanvas(): void {
const app = (window as any).app;
if (!app || !app.canvas) {
console.warn('[MagnifyGlass] Canvas not found, cannot hook highlighter');
return;
}
const canvas = app.canvas;
// Save original method
this.originalOnDrawForeground = canvas.onDrawForeground;
// Override
canvas.onDrawForeground = (ctx: CanvasRenderingContext2D, visible_nodes: any) => {
// Call original first
if (this.originalOnDrawForeground) {
this.originalOnDrawForeground.call(canvas, ctx, visible_nodes);
}
// Draw our highlight
this.drawHighlight(ctx, canvas.ds.scale);
};
}
/**
* Set the node ID to highlight.
*/
setHighlightedNode(nodeId: number | null): void {
if (this.highlightedNodeId === nodeId) return;
this.highlightedNodeId = nodeId;
// Force redraw to update highlight immediately
const app = (window as any).app;
if (app && app.canvas) {
app.canvas.setDirty(true, true);
}
}
/**
* Draw the highlight rectangle around the target node.
*/
private drawHighlight(ctx: CanvasRenderingContext2D, scale: number): void {
if (this.highlightedNodeId === null) return;
const app = (window as any).app;
if (!app) return;
const node = app.graph.getNodeById(this.highlightedNodeId);
if (!node) return;
// Save context
ctx.save();
// Reset transform to draw in graph coordinates
// Note: ctx passed to onDrawForeground is already transformed by LiteGraph to graph coordinates
// so we can draw using node.pos directly.
const x = node.pos[0] - this.HIGHLIGHT_PADDING;
const y = node.pos[1] - this.HIGHLIGHT_PADDING;
const w = node.size[0] + (this.HIGHLIGHT_PADDING * 2);
const h = node.size[1] + (this.HIGHLIGHT_PADDING * 2);
// Draw glow/outline
ctx.lineWidth = this.HIGHLIGHT_WIDTH;
ctx.strokeStyle = this.HIGHLIGHT_COLOR;
ctx.shadowColor = this.HIGHLIGHT_COLOR;
ctx.shadowBlur = 10 * scale; // Scale shadow with zoom
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
// Use roundRect if available (modern browsers), else rect
ctx.beginPath();
if (typeof ctx.roundRect === 'function') {
const radius = 10; // LiteGraph nodes usually have round corners
ctx.roundRect(x, y, w, h, radius);
} else {
ctx.rect(x, y, w, h);
}
ctx.stroke();
// Restore context
ctx.restore();
}
/**
* Clean up hooks.
*/
cleanup(): void {
const app = (window as any).app;
if (app && app.canvas && this.originalOnDrawForeground) {
app.canvas.onDrawForeground = this.originalOnDrawForeground;
}
}
}
+16 -4
View File
@@ -13,6 +13,7 @@ import { UIManager } from './UIManager';
import { PositionManager } from './PositionManager';
import { EventManager } from './EventManager';
import { InformationGatherer } from './InformationGatherer';
import { CanvasHighlighter } from './CanvasHighlighter';
/**
* Professional Info Panel Manager.
@@ -25,6 +26,7 @@ export class InfoPanel {
positionManager: PositionManager;
eventManager: EventManager;
informationGatherer: InformationGatherer;
canvasHighlighter: CanvasHighlighter;
constructor(magnifyGlass: MagnifyGlassInstance) {
this.magnifyGlass = magnifyGlass;
@@ -33,6 +35,7 @@ export class InfoPanel {
this.stateManager = new StateManager();
this.uiManager = new UIManager(this.stateManager);
this.informationGatherer = new InformationGatherer();
this.canvasHighlighter = new CanvasHighlighter();
// Position Manager needs the UI elements
this.positionManager = new PositionManager(this.stateManager, this.uiManager.elements.panel!);
@@ -165,7 +168,11 @@ export class InfoPanel {
// State for persistence
private lastValidNodeInfo: any = null;
updateInfo(): void {
/**
* Update the info panel with current information.
* @param forceUpdate - If true, bypass the isInfoHeld check to force an update
*/
updateInfo(forceUpdate: boolean = false): void {
const settings = this.stateManager.state.settings;
const isActive = this.magnifyGlass.state.active;
@@ -176,11 +183,13 @@ export class InfoPanel {
if (this.uiManager.elements.controls && this.uiManager.elements.controls.style.display !== 'none') {
this.uiManager.elements.controls.style.display = 'none';
}
this.canvasHighlighter.setHighlightedNode(null);
return;
}
// Logic for "Hold Info" (Freeze)
if (this.stateManager.state.isInfoHeld) {
// Skip if held, UNLESS we're forcing an update (e.g., from dropdown selection)
if (this.stateManager.state.isInfoHeld && !forceUpdate) {
// If held, we skip gathering new info and skip updating the display content.
// But we MUST still update positioning to keep the UI responsive to movement.
this.positionManager.positionPanel();
@@ -229,6 +238,9 @@ export class InfoPanel {
// Update the UI
this.uiManager.displayInfo(info);
// Update canvas highlight
this.canvasHighlighter.setHighlightedNode(info.hoveredNode ? info.hoveredNode.id : null);
// Send info to pop-out viewer if active
if (this.magnifyGlass.popOutManager && this.magnifyGlass.popOutManager.isPopOutOpen()) {
this.magnifyGlass.popOutManager.sendInfo(info as any);
@@ -281,8 +293,8 @@ export class InfoPanel {
// Set the selected node in state manager
this.stateManager.setSelectedNode(nodeId);
// Force an immediate info update
this.updateInfo();
// Force an immediate info update (bypasses isInfoHeld)
this.updateInfo(true);
}
/**
+9 -7
View File
@@ -760,11 +760,13 @@ export class UIManager {
// Node Details section
if (info.hoveredNode) {
const nodeContent: any[] = [
{ label: 'Title', value: info.hoveredNode.title, clickable: 'title' },
{ label: 'ID', value: `#${info.hoveredNode.id}`, clickable: 'id' }
];
const nodeContent: any[] = []; // Initial Node details
nodeContent.push({
label: 'Title',
value: `${info.hoveredNode.title || 'Untitled'} (#${info.hoveredNode.id})`,
clickable: 'title'
});
nodeContent.push({ label: 'Type', value: info.hoveredNode.type });
// Add execution order if available
if (info.hoveredNode.executionOrder !== undefined) {
nodeContent.push({ label: 'Exec Order', value: info.hoveredNode.executionOrder, clickable: 'execOrder' });
@@ -1075,12 +1077,12 @@ export class UIManager {
const execNode = node as NodeExecOrderEntry;
item.innerHTML = `
<span style="color: var(--info-panel-accent-color, #4ecdc4); font-weight: 600; min-width: 24px;">#${execNode.order}</span>
<span style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${execNode.title}</span>
<span style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${execNode.title} (#${execNode.id})</span>
<span style="color: #888; font-size: 11px;">${execNode.type}</span>
`;
} else {
item.innerHTML = `
<span style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${node.title}</span>
<span style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${node.title} (#${node.id})</span>
<span style="color: #888; font-size: 11px;">${node.type}</span>
`;
}
+89
View File
@@ -0,0 +1,89 @@
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
class CanvasHighlighter {
// px
constructor() {
__publicField(this, "originalOnDrawForeground", null);
__publicField(this, "highlightedNodeId", null);
// Configuration
__publicField(this, "HIGHLIGHT_COLOR", "#007bff");
// Bootstrap blue
__publicField(this, "HIGHLIGHT_WIDTH", 2);
// px
__publicField(this, "HIGHLIGHT_PADDING", 0);
this.hookCanvas();
}
/**
* Hook into the main canvas onDrawForeground method.
*/
hookCanvas() {
const app = window.app;
if (!app || !app.canvas) {
console.warn("[MagnifyGlass] Canvas not found, cannot hook highlighter");
return;
}
const canvas = app.canvas;
this.originalOnDrawForeground = canvas.onDrawForeground;
canvas.onDrawForeground = (ctx, visible_nodes) => {
if (this.originalOnDrawForeground) {
this.originalOnDrawForeground.call(canvas, ctx, visible_nodes);
}
this.drawHighlight(ctx, canvas.ds.scale);
};
}
/**
* Set the node ID to highlight.
*/
setHighlightedNode(nodeId) {
if (this.highlightedNodeId === nodeId) return;
this.highlightedNodeId = nodeId;
const app = window.app;
if (app && app.canvas) {
app.canvas.setDirty(true, true);
}
}
/**
* Draw the highlight rectangle around the target node.
*/
drawHighlight(ctx, scale) {
if (this.highlightedNodeId === null) return;
const app = window.app;
if (!app) return;
const node = app.graph.getNodeById(this.highlightedNodeId);
if (!node) return;
ctx.save();
const x = node.pos[0] - this.HIGHLIGHT_PADDING;
const y = node.pos[1] - this.HIGHLIGHT_PADDING;
const w = node.size[0] + this.HIGHLIGHT_PADDING * 2;
const h = node.size[1] + this.HIGHLIGHT_PADDING * 2;
ctx.lineWidth = this.HIGHLIGHT_WIDTH;
ctx.strokeStyle = this.HIGHLIGHT_COLOR;
ctx.shadowColor = this.HIGHLIGHT_COLOR;
ctx.shadowBlur = 10 * scale;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.beginPath();
if (typeof ctx.roundRect === "function") {
const radius = 10;
ctx.roundRect(x, y, w, h, radius);
} else {
ctx.rect(x, y, w, h);
}
ctx.stroke();
ctx.restore();
}
/**
* Clean up hooks.
*/
cleanup() {
const app = window.app;
if (app && app.canvas && this.originalOnDrawForeground) {
app.canvas.onDrawForeground = this.originalOnDrawForeground;
}
}
}
export {
CanvasHighlighter
};
//# sourceMappingURL=CanvasHighlighter.js.map
File diff suppressed because one or more lines are too long
+12 -3
View File
@@ -9,6 +9,7 @@ import { UIManager } from "./UIManager.js";
import { PositionManager } from "./PositionManager.js";
import { EventManager } from "./EventManager.js";
import { InformationGatherer } from "./InformationGatherer.js";
import { CanvasHighlighter } from "./CanvasHighlighter.js";
class InfoPanel {
constructor(magnifyGlass) {
__publicField(this, "magnifyGlass");
@@ -17,12 +18,14 @@ class InfoPanel {
__publicField(this, "positionManager");
__publicField(this, "eventManager");
__publicField(this, "informationGatherer");
__publicField(this, "canvasHighlighter");
// State for persistence
__publicField(this, "lastValidNodeInfo", null);
this.magnifyGlass = magnifyGlass;
this.stateManager = new StateManager();
this.uiManager = new UIManager(this.stateManager);
this.informationGatherer = new InformationGatherer();
this.canvasHighlighter = new CanvasHighlighter();
this.positionManager = new PositionManager(this.stateManager, this.uiManager.elements.panel);
this.eventManager = new EventManager(this.stateManager, this.uiManager.elements.panel, this.positionManager, {
toggleVisibility: () => {
@@ -107,7 +110,11 @@ class InfoPanel {
this.stateManager.state.updateScheduled = false;
});
}
updateInfo() {
/**
* Update the info panel with current information.
* @param forceUpdate - If true, bypass the isInfoHeld check to force an update
*/
updateInfo(forceUpdate = false) {
const settings = this.stateManager.state.settings;
const isActive = this.magnifyGlass.state.active;
if (!settings["🔍MagnifyGlass.InfoPanelEnabled"] || !isActive) {
@@ -115,9 +122,10 @@ class InfoPanel {
if (this.uiManager.elements.controls && this.uiManager.elements.controls.style.display !== "none") {
this.uiManager.elements.controls.style.display = "none";
}
this.canvasHighlighter.setHighlightedNode(null);
return;
}
if (this.stateManager.state.isInfoHeld) {
if (this.stateManager.state.isInfoHeld && !forceUpdate) {
this.positionManager.positionPanel();
this.positionManager.positionFloatingControls(this.uiManager.elements.controls);
return;
@@ -151,6 +159,7 @@ class InfoPanel {
}
this.stateManager.setCurrentInfo(info);
this.uiManager.displayInfo(info);
this.canvasHighlighter.setHighlightedNode(info.hoveredNode ? info.hoveredNode.id : null);
if (this.magnifyGlass.popOutManager && this.magnifyGlass.popOutManager.isPopOutOpen()) {
this.magnifyGlass.popOutManager.sendInfo(info);
}
@@ -191,7 +200,7 @@ class InfoPanel {
onNodeSelected(nodeId) {
Logger.debug(`Node selected from dropdown: ${nodeId}`);
this.stateManager.setSelectedNode(nodeId);
this.updateInfo();
this.updateInfo(true);
}
/**
* Clear the selected node, returning to hover-based detection.
File diff suppressed because one or more lines are too long
+9 -6
View File
@@ -546,10 +546,13 @@ class UIManager {
});
}
if (info.hoveredNode) {
const nodeContent = [
{ label: "Title", value: info.hoveredNode.title, clickable: "title" },
{ label: "ID", value: `#${info.hoveredNode.id}`, clickable: "id" }
];
const nodeContent = [];
nodeContent.push({
label: "Title",
value: `${info.hoveredNode.title || "Untitled"} (#${info.hoveredNode.id})`,
clickable: "title"
});
nodeContent.push({ label: "Type", value: info.hoveredNode.type });
if (info.hoveredNode.executionOrder !== void 0) {
nodeContent.push({ label: "Exec Order", value: info.hoveredNode.executionOrder, clickable: "execOrder" });
}
@@ -790,12 +793,12 @@ class UIManager {
const execNode = node;
item.innerHTML = `
<span style="color: var(--info-panel-accent-color, #4ecdc4); font-weight: 600; min-width: 24px;">#${execNode.order}</span>
<span style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${execNode.title}</span>
<span style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${execNode.title} (#${execNode.id})</span>
<span style="color: #888; font-size: 11px;">${execNode.type}</span>
`;
} else {
item.innerHTML = `
<span style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${node.title}</span>
<span style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${node.title} (#${node.id})</span>
<span style="color: #888; font-size: 11px;">${node.type}</span>
`;
}
File diff suppressed because one or more lines are too long
+3 -12
View File
@@ -1080,13 +1080,8 @@
<div class="section-body">
<div class="info-row clickable-row" onclick="requestNodeDropdown('title', this)">
<span class="info-label">Title</span>
<span class="info-value">${escapeHtml(info.hoveredNode.title || 'Untitled')}<span class="dropdown-indicator">▼</span></span>
<span class="info-value">${escapeHtml(info.hoveredNode.title || 'Untitled')} (#${info.hoveredNode.id})<span class="dropdown-indicator">▼</span></span>
</div>
${info.hoveredNode.id !== undefined ? `
<div class="info-row clickable-row" onclick="requestNodeDropdown('id', this)">
<span class="info-label">ID</span>
<span class="info-value">#${info.hoveredNode.id}<span class="dropdown-indicator">▼</span></span>
</div>` : ''}
${info.hoveredNode.executionOrder !== undefined ? `
<div class="info-row clickable-row" onclick="requestNodeDropdown('execOrder', this)">
<span class="info-label">Exec Order</span>
@@ -1104,10 +1099,6 @@
</div>` : ''}
<!-- Extended Node Properties -->
<div class="info-row">
<span class="info-label">ID</span>
<span class="info-value">#${info.hoveredNode.id || '?'}</span>
</div>
${info.hoveredNode.mode !== undefined ? `
<div class="info-row">
<span class="info-label">Mode</span>
@@ -1285,7 +1276,7 @@
if (sortBy === 'execOrder' && node.order !== undefined) {
item.innerHTML = `
<span class="node-order">#${node.order}</span>
<span class="node-title">${escapeHtml(node.title)}</span>
<span class="node-title">${escapeHtml(node.title)} (#${node.id})</span>
<span class="node-type">${escapeHtml(node.type)}</span>
`;
} else if (sortBy === 'id') {
@@ -1296,7 +1287,7 @@
`;
} else {
item.innerHTML = `
<span class="node-title">${escapeHtml(node.title)}</span>
<span class="node-title">${escapeHtml(node.title)} (#${node.id})</span>
<span class="node-type">${escapeHtml(node.type)}</span>
`;
}