feat: add canvas node highlighting for the info panel and refine its update logic to manage highlights.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user