import { app } from "/scripts/app.js"; import parseCss from '/extensions/mtb/extern/parse-css.js' import * as shared from '/extensions/mtb/comfy_shared.js' import { api } from "/scripts/api.js"; import { ComfyWidgets } from "/scripts/widgets.js"; const newTypes = ["BOOL", "COLOR", "BBOX"] export const MtbWidgets = { BBOX: (key, val) => { /** @type {import("./types/litegraph").IWidget} */ const widget = { name: key, type: "BBOX", // options: val, y: 0, value: val?.default || [0, 0, 0, 0], options: {}, draw: function (ctx, node, widget_width, widgetY, height) { const hide = this.type !== "BBOX" && app.canvas.ds.scale > 0.5; const show_text = true; const outline_color = LiteGraph.WIDGET_OUTLINE_COLOR; const background_color = LiteGraph.WIDGET_BGCOLOR; const text_color = LiteGraph.WIDGET_TEXT_COLOR; const secondary_text_color = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR; const H = LiteGraph.NODE_WIDGET_HEIGHT; var margin = 15; var numWidgets = 4; // Number of stacked widgets if (hide) return; for (let i = 0; i < numWidgets; i++) { let currentY = widgetY + i * (H + margin); // Adjust Y position for each widget ctx.textAlign = "left"; ctx.strokeStyle = outline_color; ctx.fillStyle = background_color; ctx.beginPath(); if (show_text) ctx.roundRect(margin, currentY, widget_width - margin * 2, H, [H * 0.5]); else ctx.rect(margin, currentY, widget_width - margin * 2, H); ctx.fill(); if (show_text) { if (!this.disabled) ctx.stroke(); ctx.fillStyle = text_color; if (!this.disabled) { ctx.beginPath(); ctx.moveTo(margin + 16, currentY + 5); ctx.lineTo(margin + 6, currentY + H * 0.5); ctx.lineTo(margin + 16, currentY + H - 5); ctx.fill(); ctx.beginPath(); ctx.moveTo(widget_width - margin - 16, currentY + 5); ctx.lineTo(widget_width - margin - 6, currentY + H * 0.5); ctx.lineTo(widget_width - margin - 16, currentY + H - 5); ctx.fill(); } ctx.fillStyle = secondary_text_color; ctx.fillText(this.label || this.name, margin * 2 + 5, currentY + H * 0.7); ctx.fillStyle = text_color; ctx.textAlign = "right"; ctx.fillText( Number(this.value).toFixed( this.options?.precision !== undefined ? this.options.precision : 3 ), widget_width - margin * 2 - 20, currentY + H * 0.7 ); } } }, mouse: function (event, pos, node) { var old_value = this.value; var x = pos[0] - node.pos[0]; var y = pos[1] - node.pos[1]; var width = node.size[0]; var H = LiteGraph.NODE_WIDGET_HEIGHT; var margin = 5; var numWidgets = 4; // Number of stacked widgets for (let i = 0; i < numWidgets; i++) { let currentY = y + i * (H + margin); // Adjust Y position for each widget if (event.type == LiteGraph.pointerevents_method + "move" && this.type == "BBOX") { if (event.deltaX) this.value += event.deltaX * 0.1 * (this.options?.step || 1); if (this.options.min != null && this.value < this.options.min) { this.value = this.options.min; } if (this.options.max != null && this.value > this.options.max) { this.value = this.options.max; } } else if (event.type == LiteGraph.pointerevents_method + "down") { var values = this.options?.values; if (values && values.constructor === Function) { values = this.options.values(w, node); } var values_list = null; var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; if (this.type == "BBOX") { this.value += delta * 0.1 * (this.options.step || 1); if (this.options.min != null && this.value < this.options.min) { this.value = this.options.min; } if (this.options.max != null && this.value > this.options.max) { this.value = this.options.max; } } else if (delta) { //clicked in arrow, used for combos var index = -1; this.last_mouseclick = 0; //avoids dobl click event if (values.constructor === Object) index = values_list.indexOf(String(this.value)) + delta; else index = values_list.indexOf(this.value) + delta; if (index >= values_list.length) { index = values_list.length - 1; } if (index < 0) { index = 0; } if (values.constructor === Array) this.value = values[index]; else this.value = index; } } //end mousedown else if (event.type == LiteGraph.pointerevents_method + "up" && this.type == "BBOX") { var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0; if (event.click_time < 200 && delta == 0) { this.prompt("Value", this.value, function (v) { // check if v is a valid equation or a number if (/^[0-9+\-*/()\s]+|\d+\.\d+$/.test(v)) { try {//solve the equation if possible v = eval(v); } catch (e) { } } this.value = Number(v); shared.inner_value_change(this, this.value, event); }.bind(w), event); } } if (old_value != this.value) setTimeout( function () { shared.inner_value_change(this, this.value, event); }.bind(this), 20 ); app.canvas.setDirty(true); } }, computeSize: function (width) { return [width, LiteGraph.NODE_WIDGET_HEIGHT * 4]; }, // onDrawBackground: function (ctx) { // if (!this.flags.collapsed) return; // this.inputEl.style.display = "block"; // this.inputEl.style.top = this.graphcanvas.offsetTop + this.pos[1] + "px"; // this.inputEl.style.left = this.graphcanvas.offsetLeft + this.pos[0] + "px"; // }, // onInputChange: function (e) { // const property = e.target.dataset.property; // const bbox = this.getInputData(0); // if (!bbox) return; // bbox[property] = parseFloat(e.target.value); // this.setOutputData(0, bbox); // } } widget.desc = "Represents a Bounding Box with x, y, width, and height."; return widget }, BOOL: (key, val, compute = false) => { /** @type {import("/types/litegraph").IWidget} */ const widget = { name: key, type: "BOOL", options: { default: false }, y: 0, value: val || false, draw: function (ctx, node, widget_width, widgetY, height) { const hide = this.type !== "BOOL" && app.canvas.ds.scale > 0.5 if (hide) { return } const outline_color = LiteGraph.WIDGET_OUTLINE_COLOR; const background_color = LiteGraph.WIDGET_BGCOLOR; const text_color = LiteGraph.WIDGET_TEXT_COLOR; const H = LiteGraph.NODE_WIDGET_HEIGHT; const arrowSize = 8; var margin = 15; if (hide) return; var currentY = widgetY; ctx.textAlign = "left"; ctx.strokeStyle = outline_color; ctx.fillStyle = background_color; ctx.beginPath(); // ctx.roundRect(margin, currentY, widget_width - margin * 2, H, [H * 0.5]); ctx.rect(margin, currentY, H, H); // Draw checkbox square ctx.fill(); ctx.stroke(); ctx.fillStyle = text_color; // ctx.fillText(this.label || this.name, margin * 2 + 5, currentY + H * 0.7); ctx.fillText(this.label || this.name, H + margin * 2, currentY + H * 0.7); // Draw arrow if the value is true // Draw checkmark if the value is true if (this.value) { ctx.fillStyle = text_color; ctx.beginPath(); ctx.moveTo(margin + H * 0.15, currentY + H * 0.5); ctx.lineTo(margin + H * 0.4, currentY + H * 0.8); ctx.lineTo(margin + H * 0.85, currentY + H * 0.2); ctx.stroke(); } }, get value() { return this.inputEl.value === "true"; }, set value(x) { this.inputEl.value = x; }, computeSize: function (width) { return [width, 32]; }, mouse: function (event, pos, node) { // var x = pos[0] - node.pos[0]; // var y = pos[1] - node.pos[1]; // var width = node.size[0]; // var H = LiteGraph.NODE_WIDGET_HEIGHT; // var margin = 15; // if (event.type == LiteGraph.pointerevents_method + "down") { // if (x > margin && x < widget_width - margin && y > widgetY && y < widgetY + H) { // this.value = !this.value; // Toggle checkbox value // shared.inner_value_change(this, this.value, event); // app.canvas.setDirty(true); // } // } if (event.type === "pointerdown") { // get widgets of type type : "COLOR" const widgets = node.widgets.filter(w => w.type === "BOOL"); for (const w of widgets) { // color picker const rect = [w.last_y, w.last_y + 32]; if (pos[1] > rect[0] && pos[1] < rect[1]) { // picker.style.position = "absolute"; // picker.style.left = ( pos[0]) + "px"; // picker.style.top = ( pos[1]) + "px"; // place at screen center // picker.style.position = "absolute"; // picker.style.left = (window.innerWidth / 2) + "px"; // picker.style.top = (window.innerHeight / 2) + "px"; // picker.style.transform = "translate(-50%, -50%)"; // picker.style.zIndex = 1000; console.log("Clicked a BOOL", this.value) this.value = this.value ? "false" : "true" } } } } } // create a checkbox widget.inputEl = document.createElement("input") widget.inputEl.type = "checkbox" widget.inputEl.value = false document.body.appendChild(widget.inputEl); return widget }, COLOR: (key, val, compute = false) => { /** @type {import("/types/litegraph").IWidget} */ const widget = {} widget.y = 0 widget.name = key; widget.type = "COLOR"; widget.options = { default: "#ff0000" }; widget.value = val || "#ff0000"; widget.draw = function (ctx, node, widgetWidth, widgetY, height) { const hide = this.type !== "COLOR" && app.canvas.ds.scale > 0.5 if (hide) { return } const border = 3; // draw a rect with a border and a fill color ctx.fillStyle = "#000"; ctx.fillRect(0, widgetY, widgetWidth, height); ctx.fillStyle = this.value; ctx.fillRect(border, widgetY + border, widgetWidth - border * 2, height - border * 2); // write the input name // choose the fill based on the luminoisty of this.value color const color = parseCss(this.value.default || this.value) if (!color) { return } ctx.fillStyle = shared.isColorBright(color.values, 125) ? "#000" : "#fff"; ctx.font = "14px Arial"; ctx.textAlign = "center"; ctx.fillText(this.name, widgetWidth * 0.5, widgetY + 14); // ctx.strokeStyle = "#fff"; // ctx.strokeRect(border, widgetY + border, widgetWidth - border * 2, height - border * 2); // ctx.fillStyle = "#000"; // ctx.fillRect(widgetWidth/2 - border / 2 , widgetY + border / 2 , widgetWidth/2 + border / 2, height + border / 2); // ctx.fillStyle = this.value; // ctx.fillRect(widgetWidth/2, widgetY, widgetWidth/2, height); } widget.mouse = function (e, pos, node) { if (e.type === "pointerdown") { // get widgets of type type : "COLOR" const widgets = node.widgets.filter(w => w.type === "COLOR"); for (const w of widgets) { // color picker const rect = [w.last_y, w.last_y + 32]; if (pos[1] > rect[0] && pos[1] < rect[1]) { console.log("color picker", node) const picker = document.createElement("input"); picker.type = "color"; picker.value = this.value; // picker.style.position = "absolute"; // picker.style.left = ( pos[0]) + "px"; // picker.style.top = ( pos[1]) + "px"; // place at screen center picker.style.position = "absolute"; picker.style.left = "999999px"//(window.innerWidth / 2) + "px"; picker.style.top = "999999px" //(window.innerHeight / 2) + "px"; // picker.style.transform = "translate(-50%, -50%)"; // picker.style.zIndex = 1000; document.body.appendChild(picker); picker.addEventListener("change", () => { this.value = picker.value; node.graph._version++; node.setDirtyCanvas(true, true); picker.remove(); }); picker.click() } } } } widget.computeSize = function (width) { return [width, 32]; } return widget; }, DEBUG_IMG: (val, index) => { const w = { name: `anything_${index}`, type: "image", value: val, draw: function (ctx, node, widgetWidth, widgetY, height) { const [cw, ch] = this.computeSize(widgetWidth) shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch) }, computeSize: function (width) { const ratio = this.inputRatio || 1; if (width) { return [width, width / ratio + 4] } return [128, 128] }, onRemove: function () { if (this.inputEl) { this.inputEl.remove(); } } } w.inputEl = document.createElement("img"); w.inputEl.src = w.value; w.inputEl.onload = function () { w.inputRatio = w.inputEl.naturalWidth / w.inputEl.naturalHeight; } document.body.appendChild(w.inputEl); return w }, DEBUG_STRING: (val, index) => { const w = { name: `anything_${index}`, type: "debug_text", val: val, draw: function (ctx, node, widgetWidth, widgetY, height) { // const [cw, ch] = this.computeSize(widgetWidth) shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, height) }, computeSize: function (width) { const value = this.inputEl.innerHTML if (!value) { return [32, 32] } if (!width) { log(`No width ${this.parent.size}`) } const fontSize = 25; // Assuming 1rem = 16px const oldFont = app.ctx.font app.ctx.font = `${fontSize}px Arial`; const words = value.split(" "); const lines = []; let currentLine = ""; for (const word of words) { const testLine = currentLine.length === 0 ? word : `${currentLine} ${word}`; const testWidth = app.ctx.measureText(testLine).width; // log(`Testing line ${testLine}, width: ${testWidth}, width: ${width}, ratio: ${testWidth / width}`) if (testWidth > width) { lines.push(currentLine); currentLine = word; } else { currentLine = testLine; } } app.ctx.font = oldFont; lines.push(currentLine); // Step 3: Calculate the widget width and height const textHeight = lines.length * (fontSize + 2); // You can adjust the line height (2 in this case) const maxLineWidth = lines.reduce((maxWidth, line) => Math.max(maxWidth, app.ctx.measureText(line).width), 0); const widgetWidth = Math.max(width || this.width || 32, maxLineWidth); const widgetHeight = textHeight + 10; // Additional padding for spacing return [widgetWidth, widgetHeight + 4] }, onRemove: function () { if (this.inputEl) { this.inputEl.remove(); } } } w.inputEl = document.createElement("p"); w.inputEl.style.textAlign = "center"; w.inputEl.style.fontSize = "1.5em"; w.inputEl.style.color = "var(--input-text)"; w.inputEl.style.fontFamily = "monospace"; w.inputEl.innerHTML = val document.body.appendChild(w.inputEl); return w } } const bboxWidgetDOM = (key, val) => { /** @type {import("./types/litegraph").IWidget} */ const widget = { name: key, type: "BBOX", // options: val, y: 0, value: val || [0, 0, 0, 0], draw: function (ctx, node, widgetWidth, widgetY, height) { const hide = this.type !== "BBOX" && app.canvas.ds.scale > 0.5 this.inputEl.style.display = hide ? "none" : "block"; if (hide) return; shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, height) }, computeSize: function (width) { return [width, 32]; }, // onDrawBackground: function (ctx) { // if (!this.flags.collapsed) return; // this.inputEl.style.display = "block"; // this.inputEl.style.top = this.graphcanvas.offsetTop + this.pos[1] + "px"; // this.inputEl.style.left = this.graphcanvas.offsetLeft + this.pos[0] + "px"; // }, onInputChange: function (e) { const property = e.target.dataset.property; const bbox = this.getInputData(0); if (!bbox) return; bbox[property] = parseFloat(e.target.value); this.setOutputData(0, bbox); } } widget.inputEl = document.createElement("div") widget.parent = this widget.inputEl.innerHTML = `
`; // set the class document wide if not present shared.defineClass("bbox-input", `background-color: var(--comfy-input-bg); color: var(--input-text); overflow: hidden; width:100%; overflow-y: auto; padding: 2px; resize: none; border: none; box-sizing: border-box; font-size: 10px;`) const bboxInputs = widget.inputEl.querySelectorAll(".bbox-input"); bboxInputs.forEach((input) => { input.addEventListener("change", widget.onInputChange.bind(this)); }); widget.desc = "Represents a Bounding Box with x, y, width, and height."; document.body.appendChild(widget.inputEl); console.log("Bounding Box Widget DOM", widget.inputEl) return widget } /** * @returns {import("./types/litegraph").IWidget} widget */ /** * @returns {import("./types/litegraph").IWidget} widget */ // VIDEO: (node, inputName, inputData, app) => { // console.log("video") // const videoWidget = { // name: "VideoWidget", // description: "Video Player Widget", // value: inputData, // properties: {}, // widget: null, // init: function () { // this.widget = document.createElement("video"); // this.widget.width = 200; // this.widget.height = 120; // this.widget.controls = true; // this.widget.style.width = "100%"; // this.widget.style.height = "100%"; // this.widget.style.objectFit = "contain"; // this.widget.style.backgroundColor = "black"; // this.widget.style.pointerEvents = "none"; // node.addWidget(inputName, videoWidget.widget, inputData); // }, // setValue: function (value, options) { // if (value instanceof HTMLVideoElement) { // this.widget.src = value.src; // } else if (typeof value === "string") { // this.widget.src = value; // } // }, // getValue: function () { // return this.widget.src; // }, // append: function (parent) { // parent.appendChild(this.widget); // }, // remove: function () { // this.widget.parentNode.removeChild(this.widget); // } // }; // return { // widget: videoWidget, // } // } /** * @returns {import("./types/comfy").ComfyExtension} extension */ const mtb_widgets = { name: "mtb.widgets", init: async () => { console.log("Registering mtb.widgets") try { const res = await api.fetchApi('/mtb/debug') const msg = await res.json() window.MTB_DEBUG = msg.enabled; } catch (e) { console.error('Error:', error); } }, setup: () => { app.ui.settings.addSetting({ id: "mtb.Debug.enabled", name: "[mtb] Enable Debug (py and js)", type: "boolean", defaultValue: false, tooltip: "This will enable debug messages in the console and in the python console respectively", attrs: { style: { fontFamily: "monospace", }, }, async onChange(value) { if (value) { console.log("Enabled DEBUG mode") } window.MTB_DEBUG = value; await api.fetchApi('/mtb/debug', { method: 'POST', body: JSON.stringify({ enabled: value }) }).then(response => { }).catch(error => { console.error('Error:', error); }); }, }); }, getCustomWidgets: function () { return { BOOL: (node, inputName, inputData, app) => { console.debug("Registering bool") return { widget: node.addCustomWidget(MtbWidgets.BOOL(inputName, inputData[1]?.default || false)), minWidth: 150, minHeight: 30, }; }, COLOR: (node, inputName, inputData, app) => { console.debug("Registering color") return { widget: node.addCustomWidget(MtbWidgets.COLOR(inputName, inputData[1]?.default || "#ff0000")), minWidth: 150, minHeight: 30, } }, // BBOX: (node, inputName, inputData, app) => { // console.debug("Registering bbox") // return { // widget: node.addCustomWidget(MtbWidgets.BBOX(inputName, inputData[1]?.default || [0, 0, 0, 0])), // minWidth: 150, // minHeight: 30, // } // } } }, /** * @param {import("./types/comfy").NodeType} nodeType * @param {import("./types/comfy").NodeDef} nodeData * @param {import("./types/comfy").App} app */ async beforeRegisterNodeDef(nodeType, nodeData, app) { const rinputs = nodeData.input?.required; let has_custom = false if (nodeData.input && nodeData.input.required) { for (const i of Object.keys(nodeData.input.required)) { const input_type = nodeData.input.required[i][0] if (newTypes.includes(input_type)) { has_custom = true break; } } } if (has_custom) { //- Add widgets on node creation const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function () { const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; this.serialize_widgets = true; for (const [key, input] of Object.entries(rinputs)) { switch (input[0]) { case "COLOR": // const colW = colorWidget(key, input[1]) // this.addCustomWidget(colW) // const associated_input = this.inputs.findIndex((i) => i.widget?.name === key); // if (associated_input !== -1) { // this.inputs[associated_input].widget = colW // } break; case "BOOL": // const widg = boolWidget(key, input[1]) // this.addCustomWidget(widg) // this.addWidget("toggle", key, false, function (value, widget, node) { // console.log(value) // }) //this.removeInput(this.inputs.findIndex((i) => i.widget?.name === key)); break; case "BBOX": // const bboxW = bboxWidget(key, input[1]) // this.addCustomWidget(bboxW) break; default: break } // } } this.setSize?.(this.computeSize()) this.onRemoved = function () { // When removing this node we need to remove the input from the DOM for (let y in this.widgets) { if (this.widgets[y].canvas) { this.widgets[y].canvas.remove(); } } }; } //- Extra menus const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; nodeType.prototype.getExtraMenuOptions = function (_, options) { const r = origGetExtraMenuOptions ? origGetExtraMenuOptions.apply(this, arguments) : undefined; if (this.widgets) { let toInput = []; let toWidget = []; for (const w of this.widgets) { if (w.type === shared.CONVERTED_TYPE) { //- This is already handled by widgetinputs.js // toWidget.push({ // content: `Convert ${w.name} to widget`, // callback: () => shared.convertToWidget(this, w), // }); } else if (newTypes.includes(w.type)) { const config = nodeData?.input?.required[w.name] || nodeData?.input?.optional?.[w.name] || [w.type, w.options || {}]; toInput.push({ content: `Convert ${w.name} to input`, callback: () => shared.convertToInput(this, w, config), }); } } if (toInput.length) { options.push(...toInput, null); } if (toWidget.length) { options.push(...toWidget, null); } } return r; }; } //- Extending Python Nodes switch (nodeData.name) { case "Psd Save (mtb)": { // const onConnectionsChange = nodeType.prototype.onConnectionsChange; nodeType.prototype.onConnectionsChange = function (type, index, connected, link_info) { // const r = onConnectionsChange ? onConnectionsChange.apply(this, arguments) : undefined; shared.dynamic_connection(this, index, connected) } break } case "Save Gif (mtb)": { const onExecuted = nodeType.prototype.onExecuted; nodeType.prototype.onExecuted = function (message) { const r = onExecuted ? onExecuted.apply(this, message) : undefined; console.log(message) if (this.widgets) { const pos = this.widgets.findIndex((w) => w.name === "anything_0"); if (pos !== -1) { for (let i = pos; i < this.widgets.length; i++) { console.log(this.widgets[i]) console.log(this) this.widgets[i].onRemove?.(); } this.widgets.length = pos } let imgURLs = [] if (message && message.gif) { imgURLs = imgURLs.concat(message.gif.map(params => { return api.apiURL("/view?" + new URLSearchParams(params).toString()); })) console.log(imgURLs) for (const img of imgURLs) { const w = this.addCustomWidget(MtbWidgets.DEBUG_IMG(img, 0)) w.parent = this; } } this.setSize?.(this.computeSize()) return r } const onRemoved = nodeType.prototype.onRemoved; nodeType.prototype.onRemoved = function (message) { const r = onRemoved ? onRemoved.apply(this, message) : undefined; if (!this.widgets) return r for (const w of this.widgets) { if (w.canvas) { w.canvas.remove(); } w.onRemove?.() w.onRemoved?.() } return r } } break } case "Animation Builder (mtb)": { // console.log(nodeType.prototype) const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function () { const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; this.changeMode(LiteGraph.ALWAYS) // api.addEventListener("executed", ({ detail }) => { // console.log("executed", detail) // console.log(this) // }) const raw_iteration = this.widgets.find((w) => w.name === "raw_iteration"); const raw_loop = this.widgets.find((w) => w.name === "raw_loop"); const total_frames = this.widgets.find((w) => w.name === "total_frames"); const loop_count = this.widgets.find((w) => w.name === "loop_count"); shared.hideWidgetForGood(this, raw_iteration); shared.hideWidgetForGood(this, raw_loop); raw_iteration._value = 0 // Object.defineProperty(raw_iteration, "value", { // get() { // return this._value // }, // set(value) { // this._value = value; // }, // }); const value_preview = ComfyWidgets["STRING"](this, "PREVIEW_raw_iteration", ["STRING", { multiline: true }], app).widget; value_preview.inputEl.readOnly = true; value_preview.inputEl.disabled = true; // value_preview.inputEl.style.opacity = 0.6; value_preview.inputEl.style.textAlign = "center"; value_preview.inputEl.style.fontSize = "2.5em"; value_preview.inputEl.style.backgroundColor = "black"; value_preview.inputEl.style.setProperty("--comfy-input-bg", "transparent"); value_preview.inputEl.style.setProperty("background", "red", "important"); // remove the comfy-multiline-input class // disallow user selection value_preview.inputEl.style.userSelect = "none"; const loop_preview = ComfyWidgets["STRING"](this, "PREVIEW_raw_iteration", ["STRING", { multiline: true }], app).widget; loop_preview.inputEl.readOnly = true; loop_preview.inputEl.disabled = true; // loop_preview.inputEl.style.opacity = 0.6; loop_preview.inputEl.style.textAlign = "center"; loop_preview.inputEl.style.fontSize = "1.5em"; loop_preview.inputEl.style.backgroundColor = "black"; loop_preview.inputEl.style.setProperty("--comfy-input-bg", "transparent"); loop_preview.inputEl.style.setProperty("background", "red", "important"); // remove the comfy-multiline-input class // disallow user selection loop_preview.inputEl.style.userSelect = "none"; const onReset = () => { raw_iteration.value = 0; raw_loop.value = 0; value_preview.value = 0; loop_preview.value = 0; app.canvas.setDirty(true); } const reset_button = this.addWidget("button", `Reset`, "reset", onReset); const run_button = this.addWidget("button", `Queue`, "queue", () => { onReset() // this could maybe be a setting or checkbox app.queuePrompt(0, total_frames.value * loop_count.value) }); raw_iteration.afterQueued = function () { this.value++; raw_loop.value = Math.floor(this.value / total_frames.value); value_preview.value = `raw: ${this.value} frame: ${this.value % total_frames.value}`; if (raw_loop.value + 1 > loop_count.value) { loop_preview.value = `Done 😎!` return } loop_preview.value = `current loop: ${raw_loop.value + 1}/${loop_count.value}` } return r } const onExecuted = nodeType.prototype.onExecuted; nodeType.prototype.onExecuted = function (data) { onExecuted?.apply(this, data) if (this.widgets) { const pos = this.widgets.findIndex((w) => w.name === "preview"); if (pos !== -1) { for (let i = pos; i < this.widgets.length; i++) { this.widgets[i].onRemove?.(); } this.widgets.length = pos; } } const w = ComfyWidgets["STRING"](this, "preview", ["STRING", { multiline: true }], app).widget; w.inputEl.readOnly = true; w.inputEl.style.opacity = 0.6; w.value = data.total_frames; // this.onResize?.(this.size); this.setSize?.(this.computeSize()) } // const onAfterExecuteNode = nodeType.prototype.onAfterExecuteNode; // nodeType.prototype.onAfterExecuteNode = function () { // onAfterExecuteNode?.apply(this) // console.log("after", this) // } console.debug(`Registered ${nodeType.name} node extra events`) break } default: { break } } // const onNodeCreated = nodeType.prototype.onNodeCreated; // nodeType.prototype.onNodeCreated = function () { // const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; // } // console.log(nodeData.output) // if (nodeData.output.includes("VIDEO") && nodeData.output_node) { // console.log(`Found video output for ${nodeType}`) // console.log(nodeData) // } // if (nodeData.name === "Psd Save (mtb)") { // console.log(`Found psd node`) // console.log(nodeData) // } } }; app.registerExtension(mtb_widgets);