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); import { Logger } from "../shared/logger.js"; import { StateManager } from "./StateManager.js"; 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"; import { getSlotLinks, getImportantNodeParameters } from "./NodeDataExtractor.js"; import { WidgetSyncManager } from "./widget-editors/WidgetSyncManager.js"; const AIM_REST_MS = 300; function rayHitsRect(x, y, dx, dy, rect, margin = 12) { let near = 0; let far = Infinity; for (const [origin, step, low, high] of [[x, dx, rect.left - margin, rect.right + margin], [y, dy, rect.top - margin, rect.bottom + margin]]) { if (step === 0) { if (origin < low || origin > high) return false; continue; } const a = (low - origin) / step; const b = (high - origin) / step; near = Math.max(near, Math.min(a, b)); far = Math.min(far, Math.max(a, b)); } return near <= far; } class InfoPanel { constructor(magnifyGlass) { __publicField(this, "magnifyGlass"); __publicField(this, "stateManager"); __publicField(this, "uiManager"); __publicField(this, "positionManager"); __publicField(this, "eventManager"); __publicField(this, "informationGatherer"); __publicField(this, "canvasHighlighter"); __publicField(this, "originalUpdateMagnifiedView"); __publicField(this, "originalShow"); __publicField(this, "originalHide"); __publicField(this, "originalPositionGlass"); __publicField(this, "updateFrameId", null); __publicField(this, "boundScheduleSelectionUpdate"); // Refreshes the pop-out while it is connected, since it gets no input of its own here __publicField(this, "popOutTimer", null); // What the pop-out was last sent from, null to send the next update whatever it holds __publicField(this, "popOutKey", null); // The node the pop-out shows and the graph it came from; its edits apply only there __publicField(this, "popOutNodeId", null); __publicField(this, "popOutGraph", null); // The results the pop-out's /view URLs were built from; each build gets a new rand param, which loads every image again __publicField(this, "popOutImages", null); __publicField(this, "popOutImageUrls", []); // The hovered node, kept while the pointer is over other UI or heads for the panel __publicField(this, "hoverNodeId", null); __publicField(this, "hoverGraph", null); // Where the pointer last was over that node, where it is and when it last moved __publicField(this, "hoverAnchor", null); __publicField(this, "pointer", { x: 0, y: 0, movedAt: 0 }); __publicField(this, "aiming", false); __publicField(this, "aimTimer", null); // The node Sticky keeps, looked up again on each update like the locked one __publicField(this, "stickyNodeId", null); __publicField(this, "stickyGraph", null); this.magnifyGlass = magnifyGlass; this.stateManager = new StateManager(); this.uiManager = new UIManager(this.stateManager); this.informationGatherer = new InformationGatherer(); this.canvasHighlighter = new CanvasHighlighter(this.stateManager.state.settings); this.positionManager = new PositionManager(this.stateManager, this.uiManager.elements.panel); this.eventManager = new EventManager(this.stateManager, this.uiManager.elements.panel, { toggleVisibility: () => { if (this.stateManager.state.isPanelVisible) { this.uiManager.hide(); } else { this.uiManager.show(); } }, toggleFloat: () => this.uiManager.toggleFloating(), toggleSticky: () => this.uiManager.setPersist(!this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelPersist"]), updateLayout: () => { this.uiManager.applyStyles(); this.uiManager.updateControlStates(); this.positionManager.positionPanel(); this.positionManager.positionFloatingControls(this.uiManager.elements.controls); }, isGlassActive: () => this.magnifyGlass.state.active }); this.boundScheduleSelectionUpdate = () => { if (this.magnifyGlass.state.active && this.stateManager.state.settings["🔍MagnifyGlass.InfoPanelFollowSelection"]) { this.scheduleInfoUpdate(); } }; document.addEventListener("pointerup", this.boundScheduleSelectionUpdate, true); document.addEventListener("keyup", this.boundScheduleSelectionUpdate, true); this.originalUpdateMagnifiedView = magnifyGlass.updateMagnifiedView; this.originalShow = magnifyGlass.ui.show; this.originalHide = magnifyGlass.ui.hide; this.originalPositionGlass = magnifyGlass.ui.positionGlass; this.hookIntoMagnifyGlass(); const popOut = this.magnifyGlass.popOutManager; if (popOut) { popOut.onStateChange = (isOpen) => { this.uiManager.updateControlStates(); this.setPopOutRefresh(isOpen); }; popOut.onNodeSelect = (nodeId) => { this.uiManager.jumpToNode(nodeId); }; popOut.onWidgetEdit = (data) => { var _a; if (this.uiManager.canEdit() && ((_a = this.popOutRow(data)) == null ? void 0 : _a.isEditable)) { WidgetSyncManager.syncWidgetValue(data.nodeId, data.widgetName, data.value); } this.refreshPopOut(); }; popOut.onButtonClick = (data) => { var _a; if ((_a = this.popOutRow(data)) == null ? void 0 : _a.isActionable) { WidgetSyncManager.clickButton(data.nodeId, data.widgetName); } this.refreshPopOut(); }; popOut.onRequestOptions = (data) => { var _a, _b; return ((_b = (_a = this.popOutRow(data)) == null ? void 0 : _a.constraints) == null ? void 0 : _b.options) ?? []; }; popOut.onSearchNodes = (query) => this.uiManager.nodeSelector.search(query); popOut.onPanelAction = (name, nodeId) => { var _a; if (nodeId === this.popOutNodeId && ((_a = app.canvas) == null ? void 0 : _a.graph) === this.popOutGraph) { this.uiManager.runAction(name, nodeId); } }; this.setPopOutRefresh(popOut.isPopOutOpen()); } this.uiManager.onRefresh = () => this.updateInfo(); Logger.debug("Info Panel initialized successfully"); } hookIntoMagnifyGlass() { const mg = this.magnifyGlass; mg.updateMagnifiedView = () => { this.originalUpdateMagnifiedView.call(mg); if (mg.state.active) { this.scheduleInfoUpdate(); } }; mg.ui.show = () => { this.originalShow.call(mg.ui); if (!this.magnifyGlass.state.active) { return; } if (this.stateManager.state.wasPanelVisibleBeforeHide) { this.uiManager.show(); setTimeout(() => { this.positionManager.positionPanel(); this.positionManager.positionFloatingControls(this.uiManager.elements.controls); }, 10); } else { const showControls = this.stateManager.state.settings["🔍MagnifyGlass.ShowHoveringControls"] !== false; if (showControls && this.uiManager.elements.controls) { this.uiManager.elements.controls.style.display = "flex"; this.uiManager.elements.controls.style.pointerEvents = "auto"; } this.uiManager.updateControlStates(); setTimeout(() => { this.positionManager.positionFloatingControls(this.uiManager.elements.controls); }, 10); } }; mg.ui.positionGlass = (x, y) => { if (this.aiming || this.isPointerOnPanel()) return; this.originalPositionGlass.call(mg.ui, x, y); }; mg.ui.hide = () => { this.stateManager.state.wasPanelVisibleBeforeHide = this.stateManager.state.isPanelVisible; this.originalHide.call(mg.ui); this.uiManager.hide(); if (this.uiManager.elements.controls) { this.uiManager.elements.controls.style.display = "none"; this.uiManager.elements.controls.style.pointerEvents = "none"; } this.canvasHighlighter.setHighlightedNode(null); }; } scheduleInfoUpdate() { if (this.updateFrameId !== null) return; this.updateFrameId = requestAnimationFrame(() => { this.updateFrameId = null; this.updateInfo(); }); } /** * Undo everything the constructor set up: glass hooks, pop-out callbacks, * listeners, timers and DOM. */ cleanup() { const mg = this.magnifyGlass; mg.updateMagnifiedView = this.originalUpdateMagnifiedView; mg.ui.show = this.originalShow; mg.ui.hide = this.originalHide; mg.ui.positionGlass = this.originalPositionGlass; if (mg.popOutManager) { mg.popOutManager.onStateChange = null; mg.popOutManager.onNodeSelect = null; mg.popOutManager.onWidgetEdit = null; mg.popOutManager.onButtonClick = null; mg.popOutManager.onRequestOptions = null; mg.popOutManager.onSearchNodes = null; mg.popOutManager.onPanelAction = null; } this.setPopOutRefresh(false); if (this.updateFrameId !== null) { cancelAnimationFrame(this.updateFrameId); this.updateFrameId = null; } if (this.aimTimer !== null) { clearTimeout(this.aimTimer); this.aimTimer = null; } document.removeEventListener("pointerup", this.boundScheduleSelectionUpdate, true); document.removeEventListener("keyup", this.boundScheduleSelectionUpdate, true); this.eventManager.cleanup(); this.uiManager.cleanup(); this.canvasHighlighter.cleanup(); } /** * Follow the glass again from where it starts. */ resetPosition() { if (!this.stateManager.state.isGlassPreviewVisible) return; this.stateManager.state.isFloating = false; this.stateManager.state.floatPosition = null; this.stateManager.saveLayout(); this.positionManager.positionPanel(); this.uiManager.updateControlStates(); } /** * Update the info panel and the pop-out with the node to show. * @param fromTick - The pop-out's refresh: the glass has not moved, so the panel is not placed again */ updateInfo(fromTick = false) { var _a, _b, _c, _d, _e; const settings = this.stateManager.state.settings; const isActive = this.magnifyGlass.state.active; const popOutOpen = !!((_a = this.magnifyGlass.popOutManager) == null ? void 0 : _a.isPopOutOpen()); if (!isActive) { if (this.stateManager.state.isPanelVisible) this.uiManager.hide(); if (this.uiManager.elements.controls && this.uiManager.elements.controls.style.display !== "none") { this.uiManager.elements.controls.style.display = "none"; } this.canvasHighlighter.setHighlightedNode(null); if (!popOutOpen) return; } const hovered = isActive ? this.resolveHover() : null; let node = hovered ? this.informationGatherer.getDetailedNodeInfo(hovered) : null; let source = "hover"; const state = this.stateManager.state; if (state.lockedNodeId !== null && state.lockedGraph !== ((_b = app.canvas) == null ? void 0 : _b.graph)) { state.lockedNodeId = null; } const lockedNode = state.lockedNodeId === null ? null : this.uiManager.nodeSelector.getNodeById(state.lockedNodeId); if (!lockedNode) { state.lockedNodeId = null; } const selectedNode = !lockedNode && settings["🔍MagnifyGlass.InfoPanelFollowSelection"] ? Object.values(((_c = app.canvas) == null ? void 0 : _c.selected_nodes) ?? {})[0] : void 0; state.showsSelection = !!selectedNode; const shownNode = lockedNode ?? selectedNode; if (shownNode) { node = this.informationGatherer.getDetailedNodeInfo(shownNode); source = lockedNode ? "locked" : "selection"; } if (node) { this.stickyNodeId = node.id; this.stickyGraph = (_d = app.canvas) == null ? void 0 : _d.graph; } else if (settings["🔍MagnifyGlass.InfoPanelPersist"] && this.stickyNodeId !== null) { const stickyNode = this.stickyGraph === ((_e = app.canvas) == null ? void 0 : _e.graph) ? this.uiManager.nodeSelector.getNodeById(this.stickyNodeId) : null; if (stickyNode) { node = this.informationGatherer.getDetailedNodeInfo(stickyNode); source = "sticky"; } else { this.stickyNodeId = null; } } state.currentNodeId = (node == null ? void 0 : node.id) ?? null; const info = { hoveredNode: node }; if (isActive) { this.uiManager.displayInfo(info); this.canvasHighlighter.setHighlightedNode(node ? node.id : null); if (!fromTick && !this.aiming && !this.isPointerOnPanel()) { this.positionManager.positionPanel(); this.positionManager.positionFloatingControls(this.uiManager.elements.controls); } } if (popOutOpen) { this.sendPopOutInfo(info, source); } } /** * The node under the pointer. Over other UI (the sidebar, a menu, this panel) the last one stays, and so it * does while the pointer heads from it for the panel, crossing canvas and other nodes, until the pointer rests. */ resolveHover() { var _a; const { x, y } = this.magnifyGlass.lastKnownMousePosition; const now = performance.now(); if (x !== this.pointer.x || y !== this.pointer.y) this.pointer = { x, y, movedAt: now }; const graph = (_a = app.canvas) == null ? void 0 : _a.graph; const kept = this.hoverNodeId !== null && this.hoverGraph === graph ? this.uiManager.nodeSelector.getNodeById(this.hoverNodeId) : null; this.aiming = false; if (!this.informationGatherer.isPointerOverGraph(x, y)) return kept; const node = this.informationGatherer.getNodeUnderCursor(); if (node !== kept && kept && now - this.pointer.movedAt < AIM_REST_MS && this.headsForPanel(x, y)) { this.aiming = true; this.aimTimer ?? (this.aimTimer = window.setTimeout(() => { this.aimTimer = null; this.scheduleInfoUpdate(); }, AIM_REST_MS)); return kept; } this.hoverAnchor = node ? { x, y } : null; this.hoverNodeId = (node == null ? void 0 : node.id) ?? null; this.hoverGraph = graph; return node; } /** * Whether the pointer moves from where it left the shown node toward the panel or its controls. */ headsForPanel(x, y) { const anchor = this.hoverAnchor; if (!anchor || !this.stateManager.state.isPanelVisible) return false; const dx = x - anchor.x; const dy = y - anchor.y; if (dx === 0 && dy === 0) return false; const { panel, controls } = this.uiManager.elements; return [panel, controls].some((element) => { const rect = element == null ? void 0 : element.getBoundingClientRect(); const covers = !!rect && anchor.x >= rect.left && anchor.x <= rect.right && anchor.y >= rect.top && anchor.y <= rect.bottom; return !!rect && rect.width > 0 && !covers && rayHitsRect(anchor.x, anchor.y, dx, dy, rect); }); } isPointerOnPanel() { const { x, y } = this.magnifyGlass.lastKnownMousePosition; const element = document.elementFromPoint(x, y); const { panel, controls } = this.uiManager.elements; return !!element && (!!(panel == null ? void 0 : panel.contains(element)) || !!(controls == null ? void 0 : controls.contains(element))); } /** * Send the pop-out what the Inspector shows, when it changed since the last send. */ sendPopOutInfo(info, source) { var _a, _b; const nav = { source, locked: this.stateManager.state.lockedNodeId !== null, ...this.uiManager.getHistoryNav(), canEdit: this.uiManager.canEdit() }; const node = info.hoveredNode; const liveNode = node && this.uiManager.nodeSelector.getNodeById(node.id); const links = liveNode ? getSlotLinks(liveNode) : { inputs: [], outputs: [] }; const key = [ ...this.uiManager.getRenderKey(info), ...Object.values(nav), app.lastNodeErrors, node && ((_a = app.nodeOutputs[node.locatorId]) == null ? void 0 : _a.images), node && app.nodePreviewImages[node.locatorId], JSON.stringify(links) ]; const sent = this.popOutKey; if (sent && key.length === sent.length && key.every((v, i) => Object.is(v, sent[i]))) return; this.popOutKey = key; this.popOutNodeId = (node == null ? void 0 : node.id) ?? null; this.popOutGraph = (_b = app.canvas) == null ? void 0 : _b.graph; this.magnifyGlass.popOutManager.sendInfo(this.buildPopOutInfo(node, nav, links)); } /** * The pop-out payload: the node's identity and place, its widget rows, outputs and links as plain data. */ buildPopOutInfo(node, nav, links) { var _a, _b, _c; if (!node) return { node: null, nav, rows: [], outputs: [], links }; const errors = /* @__PURE__ */ new Map(); for (const error of ((_b = (_a = app.lastNodeErrors) == null ? void 0 : _a[node.executionId]) == null ? void 0 : _b.errors) ?? []) { const input = (_c = error.extra_info) == null ? void 0 : _c.input_name; if (input) errors.set(input, error.details ? `${error.message}: ${error.details}` : error.message); } return { node: { id: node.id, title: node.title, type: node.type, mode: node.mode, execOrder: node.executionOrder, executionId: node.executionId, graphPath: node.graphPath, group: node.group, isSubgraph: node.isSubgraph }, nav, rows: getImportantNodeParameters(node).map((row) => { const { options, ...constraints } = row.constraints ?? {}; const raw = row.rawValue; return { label: row.label, widgetName: row.widgetName, widgetType: row.widgetType, value: row.value, rawValue: raw === null || typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean" ? raw : void 0, isEditable: !!row.isEditable, isLinked: !!row.isLinked, isActionable: !!row.isActionable, constraints, error: row.widgetName ? errors.get(row.widgetName) : void 0 }; }), outputs: this.popOutOutputs(node.locatorId), links }; } /** * The node's images as ComfyUI previews them on the node: its live previews while it runs, otherwise its last results. */ popOutOutputs(locatorId) { var _a; const previews = app.nodePreviewImages[locatorId]; if (previews == null ? void 0 : previews.length) return previews; const output = app.nodeOutputs[locatorId]; if (!(output == null ? void 0 : output.images)) return []; if (output.images !== this.popOutImages) { this.popOutImages = output.images; const video = ((_a = output.animated) == null ? void 0 : _a.some(Boolean)) && !output.images.some((image) => /webp|png/.test(image.filename)); this.popOutImageUrls = video ? [] : output.images.map(({ filename, subfolder, type }) => app.api.apiURL(`/view?${new URLSearchParams({ filename, subfolder, type })}${app.getRandParam()}`)); } return this.popOutImageUrls; } /** * The row of a widget on the node the pop-out shows, while its graph is still on screen. */ popOutRow({ nodeId, widgetName }) { var _a; if (nodeId !== this.popOutNodeId || ((_a = app.canvas) == null ? void 0 : _a.graph) !== this.popOutGraph) return void 0; const node = this.uiManager.nodeSelector.getNodeById(nodeId); return node ? getImportantNodeParameters(this.informationGatherer.getDetailedNodeInfo(node)).find((row) => row.widgetName === widgetName) : void 0; } /** * Send the pop-out its info now, even if nothing changed. */ refreshPopOut() { this.popOutKey = null; this.updateInfo(); } /** * Refresh the pop-out a few times a second while it is connected: values change after a run * or from another window without any input in this one. */ setPopOutRefresh(connected) { if (this.popOutTimer !== null) { clearInterval(this.popOutTimer); this.popOutTimer = null; } if (!connected) return; this.popOutTimer = window.setInterval(() => this.updateInfo(true), 250); this.refreshPopOut(); } } function mountInfoPanel(magnifyGlass) { if (window.infoPanelManager) return; const infoPanel = new InfoPanel(magnifyGlass); window.infoPanelManager = infoPanel; (window.comfyUIMagnifyGlassExtensions ?? (window.comfyUIMagnifyGlassExtensions = [])).push(infoPanel); magnifyGlass.setGlassPreviewActive(true); if (magnifyGlass.state.active) { magnifyGlass.ui.show(); } } function unmountInfoPanel(magnifyGlass) { var _a; const infoPanel = window.infoPanelManager; if (!infoPanel) return; infoPanel.cleanup(); window.infoPanelManager = void 0; window.comfyUIMagnifyGlassExtensions = (_a = window.comfyUIMagnifyGlassExtensions) == null ? void 0 : _a.filter((ext) => ext !== infoPanel); magnifyGlass.setGlassPreviewActive(true); } export { InfoPanel, mountInfoPanel, unmountInfoPanel };