diff --git a/CHANGELOG.md b/CHANGELOG.md index 79a0196..d552091 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Nodes 2.0 (Vue nodes)**: The glass shows the nodes, painted from the page: boxes, borders, text, widget values and prompts (icons are left out). The Inspector finds the node under the cursor, also in pan mode, and the highlight fits collapsed nodes. Turning Nodes 2.0 on and off no longer makes the highlight recurse without end. - **Node Title Emphasis** sits on the title bar instead of the first slot row, and is not drawn on nodes without a title bar such as Reroute. - The glass background matches the page past the canvas edge and at canvas zoom of 1.5 and above. -- **Subgraphs**: The panel, picker, highlight and editing use the graph on screen, so they work inside subgraphs. Title bars and collapsed nodes are hit. +- **Subgraphs**: The panel, picker, highlight and editing use the graph on screen, so they work inside subgraphs. Title bars and collapsed nodes are hit. The fallback hit test converts the pointer the way LiteGraph draws, so prompt text boxes and other DOM widgets find their node at any canvas zoom. - **Widget edits** run the widget's callback, redraw the canvas and record undo. CLIPLoader's `type` widget shows again. The frontend's internal `$$` preview widgets (e.g. on Load Image) are no longer listed. - Dropdowns no longer get orphaned or keep writing after the panel re-renders, and they can be filtered by typing. - The panel re-renders only when the shown node changes, so hover, focus and text selection survive. diff --git a/src/info-panel/InformationGatherer.ts b/src/info-panel/InformationGatherer.ts index 29fa113..92e32a0 100644 --- a/src/info-panel/InformationGatherer.ts +++ b/src/info-panel/InformationGatherer.ts @@ -37,28 +37,11 @@ export class InformationGatherer { return app.canvas.node_over; } - try { - const canvasRect = app.canvas.canvas.getBoundingClientRect(); - const mouseX = magnifyGlass.lastKnownMousePosition.x - canvasRect.left; - const mouseY = magnifyGlass.lastKnownMousePosition.y - canvasRect.top; - - let graphPos: [number, number]; - if (app.canvas.convertOffsetToCanvasPos) { - graphPos = app.canvas.convertOffsetToCanvasPos([mouseX, mouseY]); - } else { - const ds = app.canvas.ds || { scale: 1, offset: [0, 0] }; - graphPos = [ - (mouseX / ds.scale) - (ds.offset[0] / ds.scale), - (mouseY / ds.scale) - (ds.offset[1] / ds.scale) - ]; - } - - return graph.getNodeOnPos(graphPos[0], graphPos[1]) ?? null; - } catch (err) { - console.warn("Error in node detection:", err); - } - - return null; + const canvasRect = app.canvas.canvas.getBoundingClientRect(); + const { x, y } = magnifyGlass.lastKnownMousePosition; + // LiteGraph draws a graph position at (pos + offset) * scale from the canvas corner + const [graphX, graphY] = app.canvas.ds.convertCanvasToOffset([x - canvasRect.left, y - canvasRect.top]); + return graph.getNodeOnPos(graphX, graphY) ?? null; } getDetailedNodeInfo(node: ComfyNode): NodeInfo { diff --git a/src/types/comfyui.d.ts b/src/types/comfyui.d.ts index 4218550..6132e92 100644 --- a/src/types/comfyui.d.ts +++ b/src/types/comfyui.d.ts @@ -237,12 +237,13 @@ export interface ComfyCanvas { ds: { scale: number; offset: [number, number]; + /** A position on the canvas element to graph coordinates */ + convertCanvasToOffset(pos: [number, number]): [number, number]; }; node_over: ComfyNode | null; /** True in pan mode: space held or the hand tool */ read_only: boolean; selected_nodes: Record; - convertOffsetToCanvasPos?(pos: [number, number]): [number, number]; centerOnNode(node: ComfyNode): void; openSubgraph(subgraph: ComfyGraph, fromNode: ComfyNode): void; } diff --git a/tests/unit/InfoPanelLifecycle.test.ts b/tests/unit/InfoPanelLifecycle.test.ts index c6d76bd..3f15211 100644 --- a/tests/unit/InfoPanelLifecycle.test.ts +++ b/tests/unit/InfoPanelLifecycle.test.ts @@ -79,7 +79,8 @@ describe('Info Panel lifecycle', () => { // Node's own localStorage global hides happy-dom's vi.stubGlobal('localStorage', new Storage()); addSetting.mockClear(); - (window as any).app = { canvas: { onDrawForeground: null, setDirty: vi.fn() }, graph: null }; + const ds = { convertCanvasToOffset: (pos: [number, number]) => pos }; + (window as any).app = { canvas: { canvas: document.createElement('canvas'), ds, onDrawForeground: null, setDirty: vi.fn() }, graph: null }; mg = new FakeGlass(); }); @@ -996,7 +997,7 @@ describe('Info Panel lifecycle', () => { beforeEach(() => { nodes = [makeNode(1, 'Loader'), makeNode(2, 'Sampler')]; - const graph = { _nodes: nodes, getNodeById: (id: number) => nodes.find(n => n.id === id) ?? null }; + const graph = { _nodes: nodes, getNodeById: (id: number) => nodes.find(n => n.id === id) ?? null, getNodeOnPos: () => null }; Object.assign((window as any).app.canvas, { graph, node_over: nodes[0], selected_nodes: {}, centerOnNode: vi.fn() }); (mg as any).state = { active: true, x: 0, y: 0, canvasScale: 1 }; (mg as any).config = { showCursorPreview: false }; @@ -1204,7 +1205,7 @@ describe('Info Panel lifecycle', () => { panelManager().updateInfo(); expect(shown()).toBe('Loader (undone)'); - canvas.graph = { _nodes: [], getNodeById: () => nodes[0] }; + canvas.graph = { _nodes: [], getNodeById: () => nodes[0], getNodeOnPos: () => null }; panelManager().updateInfo(); expect(state().currentNodeId).toBeNull(); }); diff --git a/tests/unit/InformationGatherer.test.ts b/tests/unit/InformationGatherer.test.ts index db2db1a..f9fbbe5 100644 --- a/tests/unit/InformationGatherer.test.ts +++ b/tests/unit/InformationGatherer.test.ts @@ -17,7 +17,11 @@ const mockApp: any = { getBoundingClientRect: () => ({ left: 0, top: 0, width: 800, height: 600, right: 800, bottom: 600 }) }, node_over: null, - ds: { scale: 1, offset: [0, 0] } + ds: { + scale: 1, + offset: [0, 0], + convertCanvasToOffset(pos: number[]) { return [pos[0] / this.scale - this.offset[0], pos[1] / this.scale - this.offset[1]]; } + } } }; @@ -78,6 +82,12 @@ describe('InformationGatherer', () => { expect(getNodeOnPos).toHaveBeenCalledWith(200, 200); expect(hit).toBe(node); + + // Zoomed and panned: graph = canvas / scale - offset, as LiteGraph draws (pos + offset) * scale + Object.assign(mockApp.canvas.ds, { scale: 2, offset: [10, 20] }); + gatherer.getNodeUnderCursor(); + expect(getNodeOnPos).toHaveBeenLastCalledWith(90, 80); + Object.assign(mockApp.canvas.ds, { scale: 1, offset: [0, 0] }); mockApp.canvas.graph = canvasGraph; }); diff --git a/web/info-panel/InformationGatherer.js b/web/info-panel/InformationGatherer.js index e3d267f..18fe54d 100644 --- a/web/info-panel/InformationGatherer.js +++ b/web/info-panel/InformationGatherer.js @@ -9,32 +9,17 @@ class InformationGatherer { return null; } if (((_b = window.LiteGraph) == null ? void 0 : _b.vueNodesMode) && !app.canvas.read_only) { - const { x, y } = magnifyGlass.lastKnownMousePosition; - const nodeEl = document.elementsFromPoint(x, y).map((el) => el.closest(".lg-node[data-node-id]")).find(Boolean); + const { x: x2, y: y2 } = magnifyGlass.lastKnownMousePosition; + const nodeEl = document.elementsFromPoint(x2, y2).map((el) => el.closest(".lg-node[data-node-id]")).find(Boolean); return nodeEl ? graph.getNodeById(Number(nodeEl.dataset.nodeId)) ?? null : null; } if (app.canvas.node_over) { return app.canvas.node_over; } - try { - const canvasRect = app.canvas.canvas.getBoundingClientRect(); - const mouseX = magnifyGlass.lastKnownMousePosition.x - canvasRect.left; - const mouseY = magnifyGlass.lastKnownMousePosition.y - canvasRect.top; - let graphPos; - if (app.canvas.convertOffsetToCanvasPos) { - graphPos = app.canvas.convertOffsetToCanvasPos([mouseX, mouseY]); - } else { - const ds = app.canvas.ds || { scale: 1, offset: [0, 0] }; - graphPos = [ - mouseX / ds.scale - ds.offset[0] / ds.scale, - mouseY / ds.scale - ds.offset[1] / ds.scale - ]; - } - return graph.getNodeOnPos(graphPos[0], graphPos[1]) ?? null; - } catch (err) { - console.warn("Error in node detection:", err); - } - return null; + const canvasRect = app.canvas.canvas.getBoundingClientRect(); + const { x, y } = magnifyGlass.lastKnownMousePosition; + const [graphX, graphY] = app.canvas.ds.convertCanvasToOffset([x - canvasRect.left, y - canvasRect.top]); + return graph.getNodeOnPos(graphX, graphY) ?? null; } getDetailedNodeInfo(node) { var _a, _b, _c, _d, _e, _f;