From 90edf93e571c9d95a66cf44f4c7e2528f52670df Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Wed, 23 Aug 2023 11:08:13 -0500 Subject: [PATCH] Restore more core extensions --- web/extensions/core/invertMenuScrolling.js | 8 +- web/extensions/core/linkRenderMode.js | 2 +- web/extensions/core/maskeditor.js | 5 +- web/extensions/core/noteNode.js | 8 +- web/extensions/core/rerouteNode.js | 213 +++++----- web/extensions/core/slotDefaults.js | 3 +- web/extensions/core/snapToGrid.js | 66 +-- web/extensions/core/widgetInputs.js | 10 +- web/lib/litegraph.core.js | 4 +- web/scripts/app.js | 423 +------------------ web/scripts/graphCanvas.js | 453 ++++++++++++--------- web/scripts/graphNode.js | 134 +++++- web/scripts/widgets.js | 2 +- 13 files changed, 513 insertions(+), 818 deletions(-) diff --git a/web/extensions/core/invertMenuScrolling.js b/web/extensions/core/invertMenuScrolling.js index 0b24cd5a3..fa5c44c57 100644 --- a/web/extensions/core/invertMenuScrolling.js +++ b/web/extensions/core/invertMenuScrolling.js @@ -8,19 +8,15 @@ const id = "Comfy.InvertMenuScrolling"; app.registerExtension({ name: id, init() { - let invert = false; hook(LiteGraph, "onContextMenuCreated", (orig, contextMenu) => { orig?.(contextMenu); - contextMenu.invert_scrolling = invert; + contextMenu.options.invert_scrolling = localStorage[`Comfy.Settings.${id}`] === "true"; }) app.ui.settings.addSetting({ id, name: "Invert Menu Scrolling", type: "boolean", - defaultValue: false, - onChange(value) { - invert = value; - }, + defaultValue: false }); }, }); diff --git a/web/extensions/core/linkRenderMode.js b/web/extensions/core/linkRenderMode.js index 4d7304a88..afcc059ef 100644 --- a/web/extensions/core/linkRenderMode.js +++ b/web/extensions/core/linkRenderMode.js @@ -17,7 +17,7 @@ const ext = { })), onChange(value) { app.canvas.links_render_mode = +value; - app.graph.setDirtyCanvas(true); + app.canvas.draw(true, true); }, }); }, diff --git a/web/extensions/core/maskeditor.js b/web/extensions/core/maskeditor.js index f6292b9e3..e6f8663d3 100644 --- a/web/extensions/core/maskeditor.js +++ b/web/extensions/core/maskeditor.js @@ -1,6 +1,5 @@ -import { app } from "../../scripts/app.js"; +import { app, ComfyApp } from "../../scripts/app.js"; import { ComfyDialog, $el } from "../../scripts/ui.js"; -import { ComfyApp } from "../../scripts/app.js"; import { api } from "../../scripts/api.js" import { ClipspaceDialog } from "./clipspace.js"; @@ -657,4 +656,4 @@ app.registerExtension({ const context_predicate = () => ComfyApp.clipspace && ComfyApp.clipspace.imgs && ComfyApp.clipspace.imgs.length > 0 ClipspaceDialog.registerButton("MaskEditor", context_predicate, ComfyApp.open_maskeditor); } -}); \ No newline at end of file +}); diff --git a/web/extensions/core/noteNode.js b/web/extensions/core/noteNode.js index 0eeaa042d..fded4a849 100644 --- a/web/extensions/core/noteNode.js +++ b/web/extensions/core/noteNode.js @@ -1,7 +1,7 @@ import { app } from "../../scripts/app.js"; import { ComfyWidgets } from "../../scripts/widgets.js"; import { ComfyGraphNode } from "../../scripts/graphNode.js"; -import { LiteGraph } from "../../lib/litegraph.core.js" +import { LiteGraph, LGraphCanvas } from "../../lib/litegraph.core.js" // Node that add notes to your project @@ -12,7 +12,8 @@ app.registerExtension({ color=LGraphCanvas.node_colors.yellow.color; bgcolor=LGraphCanvas.node_colors.yellow.bgcolor; groupcolor = LGraphCanvas.node_colors.yellow.groupcolor; - constructor() { + constructor(title) { + super(title) if (!this.properties) { this.properties = {}; this.properties.text=""; @@ -30,11 +31,10 @@ app.registerExtension({ LiteGraph.registerNodeType({ class: NoteNode, title_mode: LiteGraph.NORMAL_TITLE, + category: "utils", type: "Note", title: "Note", collapsable: true, }); - - NoteNode.category = "utils"; }, }); diff --git a/web/extensions/core/rerouteNode.js b/web/extensions/core/rerouteNode.js index 7ccd1bb7e..afc3a69b2 100644 --- a/web/extensions/core/rerouteNode.js +++ b/web/extensions/core/rerouteNode.js @@ -8,7 +8,7 @@ app.registerExtension({ name: "Comfy.RerouteNode", registerCustomNodes() { class RerouteNode extends ComfyGraphNode { - constructor() { + onNodeCreated() { if (!this.properties) { this.properties = {}; } @@ -18,137 +18,137 @@ app.registerExtension({ this.addInput("", "*"); this.addOutput(this.properties.showOutputText ? "*" : "", "*"); - this.onConnectionsChange = function (type, index, connected, link_info) { - this.applyOrientation(); + // This node is purely frontend and does not impact the resulting prompt so should not be serialized + this.isVirtualNode = true; + } - // Prevent multiple connections to different types when we have no input - if (connected && type === LiteGraph.OUTPUT) { - // Ignore wildcard nodes as these will be updated to real types - const types = new Set(this.outputs[0].links.map((l) => app.graph.links[l].type).filter((t) => t !== "*")); - if (types.size > 1) { - const linksToDisconnect = []; - for (let i = 0; i < this.outputs[0].links.length - 1; i++) { - const linkId = this.outputs[0].links[i]; - const link = app.graph.links[linkId]; - linksToDisconnect.push(link); - } - for (const link of linksToDisconnect) { - const node = app.graph.getNodeById(link.target_id); - node.disconnectInput(link.target_slot); - } + onConnectionsChange(type, index, connected, link_info) { + this.applyOrientation(); + + // Prevent multiple connections to different types when we have no input + if (connected && type === LiteGraph.OUTPUT) { + // Ignore wildcard nodes as these will be updated to real types + const types = new Set(this.outputs[0].links.map((l) => app.graph.links[l].type).filter((t) => t !== "*")); + if (types.size > 1) { + const linksToDisconnect = []; + for (let i = 0; i < this.outputs[0].links.length - 1; i++) { + const linkId = this.outputs[0].links[i]; + const link = app.graph.links[linkId]; + linksToDisconnect.push(link); + } + for (const link of linksToDisconnect) { + const node = app.graph.getNodeById(link.target_id); + node.disconnectInput(link.target_slot); } } + } - // Find root input - let currentNode = this; - let updateNodes = []; - let inputType = null; - let inputNode = null; - while (currentNode) { - updateNodes.unshift(currentNode); - const linkId = currentNode.inputs[0].link; - if (linkId !== null) { - const link = app.graph.links[linkId]; - const node = app.graph.getNodeById(link.origin_id); - const type = node.constructor.type; - if (type === "Reroute") { - if (node === this) { - // We've found a circle - currentNode.disconnectInput(link.target_slot); - currentNode = null; - } - else { - // Move the previous node - currentNode = node; - } - } else { - // We've found the end - inputNode = currentNode; - inputType = node.outputs[link.origin_slot]?.type ?? null; - break; + // Find root input + let currentNode = this; + let updateNodes = []; + let inputType = null; + let inputNode = null; + while (currentNode) { + updateNodes.unshift(currentNode); + const linkId = currentNode.inputs[0].link; + if (linkId !== null) { + const link = app.graph.links[linkId]; + const node = app.graph.getNodeById(link.origin_id); + const type = node.constructor.type; + if (type === "Reroute") { + if (node === this) { + // We've found a circle + currentNode.disconnectInput(link.target_slot); + currentNode = null; + } + else { + // Move the previous node + currentNode = node; } } else { - // This path has no input node - currentNode = null; + // We've found the end + inputNode = currentNode; + inputType = node.outputs[link.origin_slot]?.type ?? null; break; } + } else { + // This path has no input node + currentNode = null; + break; } + } - // Find all outputs - const nodes = [this]; - let outputType = null; - while (nodes.length) { - currentNode = nodes.pop(); - const outputs = (currentNode.outputs ? currentNode.outputs[0].links : []) || []; - if (outputs.length) { - for (const linkId of outputs) { - const link = app.graph.links[linkId]; + // Find all outputs + const nodes = [this]; + let outputType = null; + while (nodes.length) { + currentNode = nodes.pop(); + const outputs = (currentNode.outputs ? currentNode.outputs[0].links : []) || []; + if (outputs.length) { + for (const linkId of outputs) { + const link = app.graph.links[linkId]; - // When disconnecting sometimes the link is still registered - if (!link) continue; + // When disconnecting sometimes the link is still registered + if (!link) continue; - const node = app.graph.getNodeById(link.target_id); - const type = node.constructor.type; + const node = app.graph.getNodeById(link.target_id); + const type = node.constructor.type; - if (type === "Reroute") { - // Follow reroute nodes - nodes.push(node); - updateNodes.push(node); + if (type === "Reroute") { + // Follow reroute nodes + nodes.push(node); + updateNodes.push(node); + } else { + // We've found an output + const nodeOutType = node.inputs && node.inputs[link?.target_slot] && node.inputs[link.target_slot].type ? node.inputs[link.target_slot].type : null; + if (inputType && nodeOutType !== inputType) { + // The output doesnt match our input so disconnect it + node.disconnectInput(link.target_slot); } else { - // We've found an output - const nodeOutType = node.inputs && node.inputs[link?.target_slot] && node.inputs[link.target_slot].type ? node.inputs[link.target_slot].type : null; - if (inputType && nodeOutType !== inputType) { - // The output doesnt match our input so disconnect it - node.disconnectInput(link.target_slot); - } else { - outputType = nodeOutType; - } + outputType = nodeOutType; } } - } else { - // No more outputs for this path } + } else { + // No more outputs for this path } + } - const displayType = inputType || outputType || "*"; - const color = app.canvas.link_type_colors[displayType]; + const displayType = inputType || outputType || "*"; + const color = app.canvas.link_type_colors[displayType]; - // Update the types of each node - for (const node of updateNodes) { - // If we dont have an input type we are always wildcard but we'll show the output type - // This lets you change the output link to a different type and all nodes will update - node.outputs[0].type = inputType || "*"; - node.__outputType = displayType; - node.outputs[0].name = node.properties.showOutputText ? displayType : ""; - node.size = node.computeSize(); - node.applyOrientation(); + // Update the types of each node + for (const node of updateNodes) { + // If we dont have an input type we are always wildcard but we'll show the output type + // This lets you change the output link to a different type and all nodes will update + node.outputs[0].type = inputType || "*"; + node.__outputType = displayType; + node.outputs[0].name = node.properties.showOutputText ? displayType : ""; + node.size = node.computeSize(); + node.applyOrientation(); - for (const l of node.outputs[0].links || []) { - const link = app.graph.links[l]; - if (link) { - link.color = color; - } - } - } - - if (inputNode) { - const link = app.graph.links[inputNode.inputs[0].link]; + for (const l of node.outputs[0].links || []) { + const link = app.graph.links[l]; if (link) { link.color = color; } } - }; + } - this.clone = function () { - const cloned = RerouteNode.prototype.clone.apply(this); - cloned.removeOutput(0); - cloned.addOutput(this.properties.showOutputText ? "*" : "", "*"); - cloned.size = cloned.computeSize(); - return cloned; - }; + if (inputNode) { + const link = app.graph.links[inputNode.inputs[0].link]; + if (link) { + link.color = color; + } + } + } - // This node is purely frontend and does not impact the resulting prompt so should not be serialized - this.isVirtualNode = true; + clone() { + const cloned = RerouteNode.prototype.clone.apply(this); + cloned.removeOutput(0); + cloned.addOutput(this.properties.showOutputText ? "*" : "", "*"); + cloned.size = cloned.computeSize(); + return cloned; } getExtraMenuOptions(_, options) { @@ -224,11 +224,10 @@ app.registerExtension({ LiteGraph.registerNodeType({ class: RerouteNode, title_mode: LiteGraph.NO_TITLE, + category: "utils", type: "Reroute", title: "Reroute", collapsable: false, }); - - RerouteNode.category = "utils"; }, }); diff --git a/web/extensions/core/slotDefaults.js b/web/extensions/core/slotDefaults.js index 72863e2eb..0018eb242 100644 --- a/web/extensions/core/slotDefaults.js +++ b/web/extensions/core/slotDefaults.js @@ -28,7 +28,7 @@ app.registerExtension({ slot_types_default_out: {}, slot_types_default_in: {}, async beforeRegisterNodeDef(nodeType, nodeData, app) { - var nodeId = nodeData.name; + var nodeId = nodeData.name; var inputs = []; inputs = nodeData["input"]["required"]; //only show required inputs to reduce the mess also not logical to create node with optional inputs for (const inputKey in inputs) { @@ -79,7 +79,6 @@ app.registerExtension({ this.setDefaults(maxNum); }, setDefaults(maxNum) { - LiteGraph.slot_types_default_out = {}; LiteGraph.slot_types_default_in = {}; diff --git a/web/extensions/core/snapToGrid.js b/web/extensions/core/snapToGrid.js index 4cd3c1263..4e0bfc0b9 100644 --- a/web/extensions/core/snapToGrid.js +++ b/web/extensions/core/snapToGrid.js @@ -1,5 +1,5 @@ import { app } from "../../scripts/app.js"; -import { LiteGraph } from "../../lib/litegraph.core.js" +import { LiteGraph, LGraphCanvas } from "../../lib/litegraph.core.js" // Shift + drag/resize to snap to grid @@ -22,69 +22,5 @@ app.registerExtension({ LiteGraph.CANVAS_GRID_SIZE = +value; }, }); - - // After moving a node, if the shift key is down align it to grid - const onNodeMoved = app.canvas.onNodeMoved; - app.canvas.onNodeMoved = function (node) { - const r = onNodeMoved?.apply(this, arguments); - - if (app.shiftDown) { - // Ensure all selected nodes are realigned - for (const id in this.selected_nodes) { - this.selected_nodes[id].alignToGrid(); - } - } - - return r; - }; - - // When a node is added, add a resize handler to it so we can fix align the size with the grid - const onNodeAdded = app.graph.onNodeAdded; - app.graph.onNodeAdded = function (node) { - const onResize = node.onResize; - node.onResize = function () { - if (app.shiftDown) { - const w = LiteGraph.CANVAS_GRID_SIZE * Math.round(node.size[0] / LiteGraph.CANVAS_GRID_SIZE); - const h = LiteGraph.CANVAS_GRID_SIZE * Math.round(node.size[1] / LiteGraph.CANVAS_GRID_SIZE); - node.size[0] = w; - node.size[1] = h; - } - return onResize?.apply(this, arguments); - }; - return onNodeAdded?.apply(this, arguments); - }; - - // Draw a preview of where the node will go if holding shift and the node is selected - const origDrawNode = LGraphCanvas.prototype.drawNode; - LGraphCanvas.prototype.drawNode = function (node, ctx) { - if (app.shiftDown && this.node_dragged && node.id in this.selected_nodes) { - const x = LiteGraph.CANVAS_GRID_SIZE * Math.round(node.pos[0] / LiteGraph.CANVAS_GRID_SIZE); - const y = LiteGraph.CANVAS_GRID_SIZE * Math.round(node.pos[1] / LiteGraph.CANVAS_GRID_SIZE); - - const shiftX = x - node.pos[0]; - let shiftY = y - node.pos[1]; - - let w, h; - if (node.flags.collapsed) { - w = node._collapsed_width; - h = LiteGraph.NODE_TITLE_HEIGHT; - shiftY -= LiteGraph.NODE_TITLE_HEIGHT; - } else { - w = node.size[0]; - h = node.size[1]; - let titleMode = node.constructor.title_mode; - if (titleMode !== LiteGraph.TRANSPARENT_TITLE && titleMode !== LiteGraph.NO_TITLE) { - h += LiteGraph.NODE_TITLE_HEIGHT; - shiftY -= LiteGraph.NODE_TITLE_HEIGHT; - } - } - const f = ctx.fillStyle; - ctx.fillStyle = "rgba(100, 100, 100, 0.5)"; - ctx.fillRect(shiftX, shiftY, w, h); - ctx.fillStyle = f; - } - - return origDrawNode.apply(this, arguments); - }; }, }); diff --git a/web/extensions/core/widgetInputs.js b/web/extensions/core/widgetInputs.js index 2e30680e7..7e00771cd 100644 --- a/web/extensions/core/widgetInputs.js +++ b/web/extensions/core/widgetInputs.js @@ -1,5 +1,6 @@ import { ComfyWidgets, addValueControlWidget } from "../../scripts/widgets.js"; import { app } from "../../scripts/app.js"; +import { ComfyGraphNode } from "../../scripts/graphNode.js"; import { LiteGraph } from "../../lib/litegraph.core.js" const CONVERTED_TYPE = "converted-widget"; @@ -199,8 +200,9 @@ app.registerExtension({ }; }, registerCustomNodes() { - class PrimitiveNode { - constructor() { + class PrimitiveNode extends ComfyGraphNode { + constructor(title) { + super(title); this.addOutput("connect to widget input", "*"); this.serialize_widgets = true; this.isVirtualNode = true; @@ -214,7 +216,7 @@ app.registerExtension({ for (const l of node.outputs[0].links) { const linkInfo = app.graph.links[l]; const n = node.graph.getNodeById(linkInfo.target_id); - if (n.type == "Reroute") { + if (n.type === "Reroute") { links = links.concat(get_links(n)); } else { links.push(l); @@ -408,7 +410,7 @@ app.registerExtension({ class: PrimitiveNode, type: "PrimitiveNode", title: "Primitive", + category: "utils" }); - PrimitiveNode.category = "utils"; }, }); diff --git a/web/lib/litegraph.core.js b/web/lib/litegraph.core.js index 4021f2cdf..bf57fd0d1 100644 --- a/web/lib/litegraph.core.js +++ b/web/lib/litegraph.core.js @@ -82,7 +82,7 @@ const S = class { static registerNodeAndSlotType(t, e, i = !1) { let n; if (typeof t == "string" ? n = S.registered_node_types[t] : "type" in t ? n = S.registered_node_types[t.type] : n = t, !n) - throw "Node not registered!" + t; + throw new Error("Node not registered!" + t); var s = n.class.__litegraph_type__; if (typeof e == "string") var r = e.split(","); @@ -7102,7 +7102,7 @@ class X { return s.close(), d.preventDefault(), !0; }, !0 - ), i.scroll_speed = i.scroll_speed || 0.1, i.invert_scrolling = i.invert_scrolling || !1, o.addEventListener("wheel", this.onMouseWheel.bind(this), !0), o.addEventListener("mousewheel", this.onMouseWheel.bind(this), !0), this.root = o, i.title) { + ), i.scroll_speed || (i.scroll_speed = 0.1), o.addEventListener("wheel", this.onMouseWheel.bind(this), !0), o.addEventListener("mousewheel", this.onMouseWheel.bind(this), !0), this.root = o, i.title) { var a = document.createElement("div"); a.className = "litemenu-title", a.innerHTML = i.title, o.appendChild(a); } diff --git a/web/scripts/app.js b/web/scripts/app.js index bb070fcef..166a749d6 100644 --- a/web/scripts/app.js +++ b/web/scripts/app.js @@ -244,369 +244,6 @@ export class ComfyApp { ); } - /** - * Adds special context menu handling for nodes - * e.g. this adds Open Image functionality for nodes that show images - * @param {*} node The node to add the menu handler - */ - #addNodeContextMenuHandler(node) { - node.prototype.getExtraMenuOptions = function (_, options) { - if (this.imgs) { - // If this node has images then we add an open in new tab item - let img; - if (this.imageIndex != null) { - // An image is selected so select that - img = this.imgs[this.imageIndex]; - } else if (this.overIndex != null) { - // No image is selected but one is hovered - img = this.imgs[this.overIndex]; - } - if (img) { - options.unshift( - { - content: "Open Image", - callback: () => { - let url = new URL(img.src); - url.searchParams.delete('preview'); - window.open(url, "_blank") - }, - }, - { - content: "Save Image", - callback: () => { - const a = document.createElement("a"); - let url = new URL(img.src); - url.searchParams.delete('preview'); - a.href = url; - a.setAttribute("download", new URLSearchParams(url.search).get("filename")); - document.body.append(a); - a.click(); - requestAnimationFrame(() => a.remove()); - }, - } - ); - } - } - - options.push({ - content: "Bypass", - callback: (obj) => { if (this.mode === 4) this.mode = 0; else this.mode = 4; this.graph.change(); } - }); - - // prevent conflict of clipspace content - if(!ComfyApp.clipspace_return_node) { - options.push({ - content: "Copy (Clipspace)", - callback: (obj) => { ComfyApp.copyToClipspace(this); } - }); - - if(ComfyApp.clipspace != null) { - options.push({ - content: "Paste (Clipspace)", - callback: () => { ComfyApp.pasteFromClipspace(this); } - }); - } - - if(ComfyApp.isImageNode(this)) { - options.push({ - content: "Open in MaskEditor", - callback: (obj) => { - ComfyApp.copyToClipspace(this); - ComfyApp.clipspace_return_node = this; - ComfyApp.open_maskeditor(); - } - }); - } - } - }; - } - - #addNodeKeyHandler(node) { - const app = this; - const origNodeOnKeyDown = node.prototype.onKeyDown; - - node.prototype.onKeyDown = function(e) { - if (origNodeOnKeyDown && origNodeOnKeyDown.apply(this, e) === false) { - return false; - } - - if (this.flags.collapsed || !this.imgs || this.imageIndex === null) { - return; - } - - let handled = false; - - if (e.key === "ArrowLeft" || e.key === "ArrowRight") { - if (e.key === "ArrowLeft") { - this.imageIndex -= 1; - } else if (e.key === "ArrowRight") { - this.imageIndex += 1; - } - this.imageIndex %= this.imgs.length; - - if (this.imageIndex < 0) { - this.imageIndex = this.imgs.length + this.imageIndex; - } - handled = true; - } else if (e.key === "Escape") { - this.imageIndex = null; - handled = true; - } - - if (handled === true) { - e.preventDefault(); - e.stopImmediatePropagation(); - return false; - } - } - } - - /** - * Adds Custom drawing logic for nodes - * e.g. Draws images and handles thumbnail navigation on nodes that output images - * @param {*} node The node to add the draw handler - */ - #addDrawBackgroundHandler(node) { - const app = this; - - function getImageTop(node) { - let shiftY; - if (node.imageOffset != null) { - shiftY = node.imageOffset; - } else { - if (node.widgets?.length) { - const w = node.widgets[node.widgets.length - 1]; - shiftY = w.last_y; - if (w.computeSize) { - shiftY += w.computeSize()[1] + 4; - } - else if(w.computedHeight) { - shiftY += w.computedHeight; - } - else { - shiftY += LiteGraph.NODE_WIDGET_HEIGHT + 4; - } - } else { - shiftY = node.computeSize()[1]; - } - } - return shiftY; - } - - node.prototype.setSizeForImage = function () { - if (this.inputHeight) { - this.setSize(this.size); - return; - } - const minHeight = getImageTop(this) + 220; - if (this.size[1] < minHeight) { - this.setSize([this.size[0], minHeight]); - } - }; - - node.prototype.onDrawBackground = function (ctx) { - if (!this.flags.collapsed) { - let imgURLs = [] - let imagesChanged = false - - const output = app.nodeOutputs[this.id + ""]; - if (output && output.images) { - if (this.images !== output.images) { - this.images = output.images; - imagesChanged = true; - imgURLs = imgURLs.concat(output.images.map(params => { - return api.apiURL("/view?" + new URLSearchParams(params).toString() + app.getPreviewFormatParam()); - })) - } - } - - const preview = app.nodePreviewImages[this.id + ""] - if (this.preview !== preview) { - this.preview = preview - imagesChanged = true; - if (preview != null) { - imgURLs.push(preview); - } - } - - if (imagesChanged) { - this.imageIndex = null; - if (imgURLs.length > 0) { - Promise.all( - imgURLs.map((src) => { - return new Promise((r) => { - const img = new Image(); - img.onload = () => r(img); - img.onerror = () => r(null); - img.src = src - }); - }) - ).then((imgs) => { - if ((!output || this.images === output.images) && (!preview || this.preview === preview)) { - this.imgs = imgs.filter(Boolean); - this.setSizeForImage?.(); - app.graph.setDirtyCanvas(true); - } - }); - } - else { - this.imgs = null; - } - } - - if (this.imgs && this.imgs.length) { - const canvas = graph.list_of_graphcanvas[0]; - const mouse = canvas.graph_mouse; - if (!canvas.pointer_is_down && this.pointerDown) { - if (mouse[0] === this.pointerDown.pos[0] && mouse[1] === this.pointerDown.pos[1]) { - this.imageIndex = this.pointerDown.index; - } - this.pointerDown = null; - } - - let w = this.imgs[0].naturalWidth; - let h = this.imgs[0].naturalHeight; - let imageIndex = this.imageIndex; - const numImages = this.imgs.length; - if (numImages === 1 && !imageIndex) { - this.imageIndex = imageIndex = 0; - } - - const shiftY = getImageTop(this); - - let dw = this.size[0]; - let dh = this.size[1]; - dh -= shiftY; - - if (imageIndex == null) { - let best = 0; - let cellWidth; - let cellHeight; - let cols = 0; - let shiftX = 0; - for (let c = 1; c <= numImages; c++) { - const rows = Math.ceil(numImages / c); - const cW = dw / c; - const cH = dh / rows; - const scaleX = cW / w; - const scaleY = cH / h; - - const scale = Math.min(scaleX, scaleY, 1); - const imageW = w * scale; - const imageH = h * scale; - const area = imageW * imageH * numImages; - - if (area > best) { - best = area; - cellWidth = imageW; - cellHeight = imageH; - cols = c; - shiftX = c * ((cW - imageW) / 2); - } - } - - let anyHovered = false; - this.imageRects = []; - for (let i = 0; i < numImages; i++) { - const img = this.imgs[i]; - const row = Math.floor(i / cols); - const col = i % cols; - const x = col * cellWidth + shiftX; - const y = row * cellHeight + shiftY; - if (!anyHovered) { - anyHovered = LiteGraph.isInsideRectangle( - mouse[0], - mouse[1], - x + this.pos[0], - y + this.pos[1], - cellWidth, - cellHeight - ); - if (anyHovered) { - this.overIndex = i; - let value = 110; - if (canvas.pointer_is_down) { - if (!this.pointerDown || this.pointerDown.index !== i) { - this.pointerDown = { index: i, pos: [...mouse] }; - } - value = 125; - } - ctx.filter = `contrast(${value}%) brightness(${value}%)`; - canvas.canvas.style.cursor = "pointer"; - } - } - this.imageRects.push([x, y, cellWidth, cellHeight]); - ctx.drawImage(img, x, y, cellWidth, cellHeight); - ctx.filter = "none"; - } - - if (!anyHovered) { - this.pointerDown = null; - this.overIndex = null; - } - } else { - // Draw individual - const scaleX = dw / w; - const scaleY = dh / h; - const scale = Math.min(scaleX, scaleY, 1); - - w *= scale; - h *= scale; - - let x = (dw - w) / 2; - let y = (dh - h) / 2 + shiftY; - ctx.drawImage(this.imgs[imageIndex], x, y, w, h); - - const drawButton = (x, y, sz, text) => { - const hovered = LiteGraph.isInsideRectangle(mouse[0], mouse[1], x + this.pos[0], y + this.pos[1], sz, sz); - let fill = "#333"; - let textFill = "#fff"; - let isClicking = false; - if (hovered) { - canvas.canvas.style.cursor = "pointer"; - if (canvas.pointer_is_down) { - fill = "#1e90ff"; - isClicking = true; - } else { - fill = "#eee"; - textFill = "#000"; - } - } else { - this.pointerWasDown = null; - } - - ctx.fillStyle = fill; - ctx.beginPath(); - ctx.roundRect(x, y, sz, sz, [4]); - ctx.fill(); - ctx.fillStyle = textFill; - ctx.font = "12px Arial"; - ctx.textAlign = "center"; - ctx.fillText(text, x + 15, y + 20); - - return isClicking; - }; - - if (numImages > 1) { - if (drawButton(x + w - 35, y + h - 35, 30, `${this.imageIndex + 1}/${numImages}`)) { - let i = this.imageIndex + 1 >= numImages ? 0 : this.imageIndex + 1; - if (!this.pointerDown || !this.pointerDown.index === i) { - this.pointerDown = { index: i, pos: [...mouse] }; - } - } - - if (drawButton(x + w - 35, y + 5, 30, `x`)) { - if (!this.pointerDown || !this.pointerDown.index === null) { - this.pointerDown = { index: null, pos: [...mouse] }; - } - } - } - } - } - } - }; - } - /** * Adds a handler allowing drag+drop of files onto the window to load workflows */ @@ -860,6 +497,9 @@ export class ComfyApp { const defs = await api.getNodeDefs(); await this.registerNodesFromDefs(defs); await this.#invokeExtensionsAsync("registerCustomNodes"); + + // Hide litegraph Subgraph from menu until it's implemented + LiteGraph.registered_node_types["graph/subgraph"].hide_in_node_lists = true; } async registerNodesFromDefs(defs) { @@ -882,7 +522,7 @@ export class ComfyApp { const ctor = class extends ComfyBackendNode { constructor(title) { - super(title, app, nodeId, nodeData); + super(title, nodeId, nodeData); } } @@ -892,63 +532,8 @@ export class ComfyApp { desc: `ComfyNode: ${nodeId}` } - // const node = Object.assign( - // function ComfyNode() { - // var inputs = nodeData["input"]["required"]; - // if (nodeData["input"]["optional"] != undefined){ - // inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"]) - // } - // const config = { minWidth: 1, minHeight: 1 }; - // for (const inputName in inputs) { - // const inputData = inputs[inputName]; - // const type = inputData[0]; - - // if(inputData[1]?.forceInput) { - // this.addInput(inputName, type); - // } else { - // if (Array.isArray(type)) { - // // Enums - // Object.assign(config, widgets.COMBO(this, inputName, inputData, app) || {}); - // } else if (`${type}:${inputName}` in widgets) { - // // Support custom widgets by Type:Name - // Object.assign(config, widgets[`${type}:${inputName}`](this, inputName, inputData, app) || {}); - // } else if (type in widgets) { - // // Standard type widgets - // Object.assign(config, widgets[type](this, inputName, inputData, app) || {}); - // } else { - // // Node connection inputs - // this.addInput(inputName, type); - // } - // } - // } - - // for (const o in nodeData["output"]) { - // const output = nodeData["output"][o]; - // const outputName = nodeData["output_name"][o] || output; - // const outputShape = nodeData["output_is_list"][o] ? LiteGraph.GRID_SHAPE : LiteGraph.CIRCLE_SHAPE ; - // this.addOutput(outputName, output, { shape: outputShape }); - // } - - // const s = this.computeSize(); - // s[0] = Math.max(config.minWidth, s[0] * 1.5); - // s[1] = Math.max(config.minHeight, s[1]); - // this.size = s; - // this.serialize_widgets = true; - - // app.#invokeExtensionsAsync("nodeCreated", this); - // }, - // { - // title: nodeData.display_name || nodeData.name, - // comfyClass: nodeData.name, - // } - // ); - // node.prototype.comfyClass = nodeData.name; node.type = nodeId; - // this.#addNodeContextMenuHandler(node); - // this.#addDrawBackgroundHandler(node, app); - // this.#addNodeKeyHandler(node); - await this.#invokeExtensionsAsync("beforeRegisterNodeDef", node, nodeData); LiteGraph.registerNodeType(node); node.category = nodeData.category; diff --git a/web/scripts/graphCanvas.js b/web/scripts/graphCanvas.js index 42ffae61a..48ebb452f 100644 --- a/web/scripts/graphCanvas.js +++ b/web/scripts/graphCanvas.js @@ -6,236 +6,289 @@ export default class ComfyGraphCanvas extends LGraphCanvas { this.app = app; } - processKey(e) { - const res = super.processKey(e); + /* + * SnapToGrid feature + */ - if (res === false) { - return res; - } + // After moving a node, if the shift key is down align it to grid + onNodeMoved(node) { + const r = super.onNodeMoved?.(node); - if (!this.graph) { - return; - } - - var block_default = false; - - if (e.target.localName == "input") { - return; - } - - if (e.type == "keydown") { - // Ctrl + M mute/unmute - if (e.keyCode == 77 && e.ctrlKey) { - if (this.selected_nodes) { - for (var i in this.selected_nodes) { - if (this.selected_nodes[i].mode === 2) { // never - this.selected_nodes[i].mode = 0; // always - } else { - this.selected_nodes[i].mode = 2; // never + if (app.shiftDown) { + // Ensure all selected nodes are realigned + for (const id in this.selected_nodes) { + this.selected_nodes[id].alignToGrid(); } - } } - block_default = true; - } - if (e.keyCode == 66 && e.ctrlKey) { - if (this.selected_nodes) { - for (var i in this.selected_nodes) { - if (this.selected_nodes[i].mode === 4) { // never - this.selected_nodes[i].mode = 0; // always - } else { - this.selected_nodes[i].mode = 4; // never - } - } - } - block_default = true; - } + return r; } - this.graph.change(); + processKey(e) { + const res = super.processKey(e); - if (block_default) { - e.preventDefault(); - e.stopImmediatePropagation(); - return false; - } + if (res === false) { + return res; + } - return res; - } + if (!this.graph) { + return; + } - processMouseDown(e) { - const res = super.processMouseDown(e); + var block_default = false; - this.selected_group_moving = false; + if (e.target.localName == "input") { + return; + } - if (this.selected_group && !this.selected_group_resizing) { - var font_size = - this.selected_group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE; - var height = font_size * 1.4; + if (e.type == "keydown") { + // Ctrl + M mute/unmute + if (e.keyCode == 77 && e.ctrlKey) { + if (this.selected_nodes) { + for (var i in this.selected_nodes) { + if (this.selected_nodes[i].mode === 2) { // never + this.selected_nodes[i].mode = 0; // always + } else { + this.selected_nodes[i].mode = 2; // never + } + } + } + block_default = true; + } - // Move group by header - if (LiteGraph.isInsideRectangle(e.canvasX, e.canvasY, this.selected_group.pos[0], this.selected_group.pos[1], this.selected_group.size[0], height)) { - this.selected_group_moving = true; - } - } + if (e.keyCode == 66 && e.ctrlKey) { + if (this.selected_nodes) { + for (var i in this.selected_nodes) { + if (this.selected_nodes[i].mode === 4) { // never + this.selected_nodes[i].mode = 0; // always + } else { + this.selected_nodes[i].mode = 4; // never + } + } + } + block_default = true; + } + } - return res; - } + this.graph.change(); - processMouseMove(e) { - const orig_selected_group = this.selected_group; + if (block_default) { + e.preventDefault(); + e.stopImmediatePropagation(); + return false; + } - if (this.selected_group && !this.selected_group_resizing && !this.selected_group_moving) { - this.selected_group = null; - } - - const res = super.processMouseMove(e); - - if (orig_selected_group && !this.selected_group_resizing && !this.selected_group_moving) { - this.selected_group = orig_selected_group; - } - - return res; - } - - /** - * Draws group header bar - */ - drawGroups(canvas, ctx) { - if (!this.graph) { - return; + return res; } - var groups = this.graph._groups; + processMouseDown(e) { + const res = super.processMouseDown(e); - ctx.save(); - ctx.globalAlpha = 0.7 * this.editor_alpha; + this.selected_group_moving = false; - for (var i = 0; i < groups.length; ++i) { - var group = groups[i]; + if (this.selected_group && !this.selected_group_resizing) { + var font_size = + this.selected_group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE; + var height = font_size * 1.4; - if (!LiteGraph.overlapBounding(this.visible_area, group._bounding)) { - continue; - } //out of the visible area + // Move group by header + if (LiteGraph.isInsideRectangle(e.canvasX, e.canvasY, this.selected_group.pos[0], this.selected_group.pos[1], this.selected_group.size[0], height)) { + this.selected_group_moving = true; + } + } - ctx.fillStyle = group.color || "#335"; - ctx.strokeStyle = group.color || "#335"; - var pos = group._pos; - var size = group._size; - ctx.globalAlpha = 0.25 * this.editor_alpha; - ctx.beginPath(); - var font_size = - group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE; - ctx.rect(pos[0] + 0.5, pos[1] + 0.5, size[0], font_size * 1.4); - ctx.fill(); - ctx.globalAlpha = this.editor_alpha; - } + return res; + } - ctx.restore(); + processMouseMove(e) { + const orig_selected_group = this.selected_group; - const res = super.drawGroups(canvas, ctx); - return res; - } + if (this.selected_group && !this.selected_group_resizing && !this.selected_group_moving) { + this.selected_group = null; + } - /** - * Draws node highlights (executing, drag drop) and progress bar - */ - drawNodeShape(node, ctx, size, fgcolor, bgcolor, selected, mouse_over) { - const res = super.drawNodeShape(node, ctx, size, fgcolor, bgcolor, selected, mouse_over); + const res = super.processMouseMove(e); - const nodeErrors = app.lastNodeErrors?.[node.id]; + if (orig_selected_group && !this.selected_group_resizing && !this.selected_group_moving) { + this.selected_group = orig_selected_group; + } - let color = null; - let lineWidth = 1; - if (node.id === +app.runningNodeId) { - color = "#0f0"; - } else if (app.dragOverNode && node.id === app.dragOverNode.id) { - color = "dodgerblue"; - } - else if (nodeErrors?.errors) { - color = "red"; - lineWidth = 2; - } - else if (app.lastExecutionError && +app.lastExecutionError.node_id === node.id) { - color = "#f0f"; - lineWidth = 2; - } + return res; + } - if (color) { - const shape = node.shape || BuiltInSlotShape.ROUND_SHAPE; - ctx.lineWidth = lineWidth; - ctx.globalAlpha = 0.8; - ctx.beginPath(); - if (shape == BuiltInSlotShape.BOX_SHAPE) - ctx.rect(-6, -6 - LiteGraph.NODE_TITLE_HEIGHT, 12 + size[0] + 1, 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT); - else if (shape == BuiltInSlotShape.ROUND_SHAPE || (shape == BuiltInSlotShape.CARD_SHAPE && node.flags.collapsed)) - ctx.roundRect( - -6, - -6 - LiteGraph.NODE_TITLE_HEIGHT, - 12 + size[0] + 1, - 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT, - this.round_radius * 2 - ); - else if (shape == BuiltInSlotShape.CARD_SHAPE) - ctx.roundRect( - -6, - -6 - LiteGraph.NODE_TITLE_HEIGHT, - 12 + size[0] + 1, - 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT, - [this.round_radius * 2, this.round_radius * 2, 2, 2] - ); - else if (shape == BuiltInSlotShape.CIRCLE_SHAPE) - ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5 + 6, 0, Math.PI * 2); - ctx.strokeStyle = color; - ctx.stroke(); - ctx.strokeStyle = fgcolor; - ctx.globalAlpha = 1; - } + /** + * Draws group header bar + */ + drawGroups(canvas, ctx) { + if (!this.graph) { + return; + } - if (app.progress && node.id === +app.runningNodeId) { - ctx.fillStyle = "green"; - ctx.fillRect(0, 0, size[0] * (app.progress.value / app.progress.max), 6); - ctx.fillStyle = bgcolor; - } + var groups = this.graph._groups; - // Highlight inputs that failed validation - if (nodeErrors) { - ctx.lineWidth = 2; - ctx.strokeStyle = "red"; - for (const error of nodeErrors.errors) { - if (error.extra_info && error.extra_info.input_name) { - const inputIndex = node.findInputSlot(error.extra_info.input_name) - if (inputIndex !== -1) { - let pos = node.getConnectionPos(true, inputIndex); - ctx.beginPath(); - ctx.arc(pos[0] - node.pos[0], pos[1] - node.pos[1], 12, 0, 2 * Math.PI, false) - ctx.stroke(); - } + ctx.save(); + ctx.globalAlpha = 0.7 * this.editor_alpha; + + for (var i = 0; i < groups.length; ++i) { + var group = groups[i]; + + if (!LiteGraph.overlapBounding(this.visible_area, group._bounding)) { + continue; + } //out of the visible area + + ctx.fillStyle = group.color || "#335"; + ctx.strokeStyle = group.color || "#335"; + var pos = group._pos; + var size = group._size; + ctx.globalAlpha = 0.25 * this.editor_alpha; + ctx.beginPath(); + var font_size = + group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE; + ctx.rect(pos[0] + 0.5, pos[1] + 0.5, size[0], font_size * 1.4); + ctx.fill(); + ctx.globalAlpha = this.editor_alpha; + } + + ctx.restore(); + + const res = super.drawGroups(canvas, ctx); + return res; + } + + /** + * Draws node highlights (executing, drag drop) and progress bar + */ + drawNodeShape(node, ctx, size, fgcolor, bgcolor, selected, mouse_over) { + const res = super.drawNodeShape(node, ctx, size, fgcolor, bgcolor, selected, mouse_over); + + const nodeErrors = app.lastNodeErrors?.[node.id]; + + let color = null; + let lineWidth = 1; + if (node.id === +app.runningNodeId) { + color = "#0f0"; + } else if (app.dragOverNode && node.id === app.dragOverNode.id) { + color = "dodgerblue"; + } + else if (nodeErrors?.errors) { + color = "red"; + lineWidth = 2; + } + else if (app.lastExecutionError && +app.lastExecutionError.node_id === node.id) { + color = "#f0f"; + lineWidth = 2; + } + + if (color) { + const shape = node.shape || BuiltInSlotShape.ROUND_SHAPE; + ctx.lineWidth = lineWidth; + ctx.globalAlpha = 0.8; + ctx.beginPath(); + if (shape == BuiltInSlotShape.BOX_SHAPE) + ctx.rect(-6, -6 - LiteGraph.NODE_TITLE_HEIGHT, 12 + size[0] + 1, 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT); + else if (shape == BuiltInSlotShape.ROUND_SHAPE || (shape == BuiltInSlotShape.CARD_SHAPE && node.flags.collapsed)) + ctx.roundRect( + -6, + -6 - LiteGraph.NODE_TITLE_HEIGHT, + 12 + size[0] + 1, + 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT, + this.round_radius * 2 + ); + else if (shape == BuiltInSlotShape.CARD_SHAPE) + ctx.roundRect( + -6, + -6 - LiteGraph.NODE_TITLE_HEIGHT, + 12 + size[0] + 1, + 12 + size[1] + LiteGraph.NODE_TITLE_HEIGHT, + [this.round_radius * 2, this.round_radius * 2, 2, 2] + ); + else if (shape == BuiltInSlotShape.CIRCLE_SHAPE) + ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5 + 6, 0, Math.PI * 2); + ctx.strokeStyle = color; + ctx.stroke(); + ctx.strokeStyle = fgcolor; + ctx.globalAlpha = 1; + } + + if (app.progress && node.id === +app.runningNodeId) { + ctx.fillStyle = "green"; + ctx.fillRect(0, 0, size[0] * (app.progress.value / app.progress.max), 6); + ctx.fillStyle = bgcolor; + } + + // Highlight inputs that failed validation + if (nodeErrors) { + ctx.lineWidth = 2; + ctx.strokeStyle = "red"; + for (const error of nodeErrors.errors) { + if (error.extra_info && error.extra_info.input_name) { + const inputIndex = node.findInputSlot(error.extra_info.input_name) + if (inputIndex !== -1) { + let pos = node.getConnectionPos(true, inputIndex); + ctx.beginPath(); + ctx.arc(pos[0] - node.pos[0], pos[1] - node.pos[1], 12, 0, 2 * Math.PI, false) + ctx.stroke(); + } + } + } + } + + return res; + } + + drawNode(node, ctx) { + const editor_alpha = this.editor_alpha; + const old_color = node.bgcolor; + + if (node.mode === 2) { // never + this.editor_alpha = 0.4; + } + + if (node.mode === 4) { // never + node.bgcolor = "#FF00FF"; + this.editor_alpha = 0.2; + } + + this.#drawNodeGridPreview(node, ctx); + const res = super.drawNode(node, ctx); + + this.editor_alpha = editor_alpha; + node.bgcolor = old_color; + + return res + } + + /* + * SnapToGrid feature + */ + + // Draw a preview of where the node will go if holding shift and the node is selected + #drawNodeGridPreview = function (node, ctx) { + if (app.shiftDown && this.node_dragged && node.id in this.selected_nodes) { + const x = LiteGraph.CANVAS_GRID_SIZE * Math.round(node.pos[0] / LiteGraph.CANVAS_GRID_SIZE); + const y = LiteGraph.CANVAS_GRID_SIZE * Math.round(node.pos[1] / LiteGraph.CANVAS_GRID_SIZE); + + const shiftX = x - node.pos[0]; + let shiftY = y - node.pos[1]; + + let w, h; + if (node.flags.collapsed) { + w = node._collapsed_width; + h = LiteGraph.NODE_TITLE_HEIGHT; + shiftY -= LiteGraph.NODE_TITLE_HEIGHT; + } else { + w = node.size[0]; + h = node.size[1]; + let titleMode = node.constructor.title_mode; + if (titleMode !== LiteGraph.TRANSPARENT_TITLE && titleMode !== LiteGraph.NO_TITLE) { + h += LiteGraph.NODE_TITLE_HEIGHT; + shiftY -= LiteGraph.NODE_TITLE_HEIGHT; + } + } + const f = ctx.fillStyle; + ctx.fillStyle = "rgba(100, 100, 100, 0.5)"; + ctx.fillRect(shiftX, shiftY, w, h); + ctx.fillStyle = f; } - } } - - return res; - } - - drawNode(node, ctx) { - var editor_alpha = this.editor_alpha; - var old_color = node.bgcolor; - - if (node.mode === 2) { // never - this.editor_alpha = 0.4; - } - - if (node.mode === 4) { // never - node.bgcolor = "#FF00FF"; - this.editor_alpha = 0.2; - } - - const res = super.drawNode(node, ctx); - - this.editor_alpha = editor_alpha; - node.bgcolor = old_color; - - return res; - } } diff --git a/web/scripts/graphNode.js b/web/scripts/graphNode.js index 47dfab70e..57530af13 100644 --- a/web/scripts/graphNode.js +++ b/web/scripts/graphNode.js @@ -2,14 +2,140 @@ import { LiteGraph, LGraphNode, LGraphCanvas, BuiltInSlotType, BuiltInSlotShape import { ComfyWidgets } from "./widgets.js"; import { iterateNodeDefOutputs, iterateNodeDefInputs } from "./nodeDef.js"; import { api } from "./api.js"; +import { ComfyApp } from "./app.js" export class ComfyGraphNode extends LGraphNode { - constructor(title, app) { + constructor(title) { super(title) - this.app = app; this.serialize_widgets = true; } + onKeyDown(e) { + if (super.onKeyDown && super.onKeyDown.apply(this, e) === false) { + return false; + } + + if (this.flags.collapsed || !this.imgs || this.imageIndex === null) { + return; + } + + let handled = false; + + if (e.key === "ArrowLeft" || e.key === "ArrowRight") { + if (e.key === "ArrowLeft") { + this.imageIndex -= 1; + } else if (e.key === "ArrowRight") { + this.imageIndex += 1; + } + this.imageIndex %= this.imgs.length; + + if (this.imageIndex < 0) { + this.imageIndex = this.imgs.length + this.imageIndex; + } + handled = true; + } else if (e.key === "Escape") { + this.imageIndex = null; + handled = true; + } + + if (handled === true) { + e.preventDefault(); + e.stopImmediatePropagation(); + return false; + } + } + + /* + * SnapToGrid functionality + */ + onResize() { + if (app.shiftDown) { + const w = LiteGraph.CANVAS_GRID_SIZE * Math.round(node.size[0] / LiteGraph.CANVAS_GRID_SIZE); + const h = LiteGraph.CANVAS_GRID_SIZE * Math.round(node.size[1] / LiteGraph.CANVAS_GRID_SIZE); + node.size[0] = w; + node.size[1] = h; + } + return super.onResize?.(); + } + + /** + * Adds special context menu handling for nodes + * e.g. this adds Open Image functionality for nodes that show images + * @param {*} node The node to add the menu handler + */ + getExtraMenuOptions(_, options) { + if (super.getExtraMenuOptions) + super.getExtraMenuOptions(_, options); + + if (this.imgs) { + // If this node has images then we add an open in new tab item + let img; + if (this.imageIndex != null) { + // An image is selected so select that + img = this.imgs[this.imageIndex]; + } else if (this.overIndex != null) { + // No image is selected but one is hovered + img = this.imgs[this.overIndex]; + } + if (img) { + options.unshift( + { + content: "Open Image", + callback: () => { + let url = new URL(img.src); + url.searchParams.delete('preview'); + window.open(url, "_blank") + }, + }, + { + content: "Save Image", + callback: () => { + const a = document.createElement("a"); + let url = new URL(img.src); + url.searchParams.delete('preview'); + a.href = url; + a.setAttribute("download", new URLSearchParams(url.search).get("filename")); + document.body.append(a); + a.click(); + requestAnimationFrame(() => a.remove()); + }, + } + ); + } + } + + options.push({ + content: "Bypass", + callback: (obj) => { if (this.mode === 4) this.mode = 0; else this.mode = 4; this.graph.change(); } + }); + + // prevent conflict of clipspace content + if(!ComfyApp.clipspace_return_node) { + options.push({ + content: "Copy (Clipspace)", + callback: (obj) => { ComfyApp.copyToClipspace(this); } + }); + + if(ComfyApp.clipspace != null) { + options.push({ + content: "Paste (Clipspace)", + callback: () => { ComfyApp.pasteFromClipspace(this); } + }); + } + + if(ComfyApp.isImageNode(this)) { + options.push({ + content: "Open in MaskEditor", + callback: (obj) => { + ComfyApp.copyToClipspace(this); + ComfyApp.clipspace_return_node = this; + ComfyApp.open_maskeditor(); + } + }); + } + } + }; + getImageTop() { let shiftY; if (this.imageOffset != null) { @@ -253,8 +379,8 @@ const defaultInputConfigs = {} export class ComfyBackendNode extends ComfyGraphNode { - constructor(title, app, comfyClass, nodeDef) { - super(title, app) + constructor(title, comfyClass, nodeDef) { + super(title) this.type = comfyClass; // XXX: workaround dependency in LGraphNode.addInput() this.displayName = nodeDef.display_name; this.comfyNodeDef = nodeDef; diff --git a/web/scripts/widgets.js b/web/scripts/widgets.js index 8d61a4c34..d9128c377 100644 --- a/web/scripts/widgets.js +++ b/web/scripts/widgets.js @@ -200,7 +200,7 @@ function addMultilineWidget(node, name, opts, app) { // if it goes off screen quickly, the input may not be removed // this shifts it off screen so it can be moved back if the node is visible. for (let n in window.app.graph._nodes) { - n = graph._nodes[n]; + n = window.app.graph._nodes[n]; for (let w in n.widgets) { let wid = n.widgets[w]; if (Object.hasOwn(wid, "inputEl")) {