From 37048fc1a2b816e48bdd3793d803f9fa22fa28b4 Mon Sep 17 00:00:00 2001 From: doctorpangloss <2229300+doctorpangloss@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:23:34 -0800 Subject: [PATCH] fix issues with zooming in editor, simplify, improve list inputs and outputs --- comfy_extras/eval_web/ace_utils.js | 769 --------------------------- comfy_extras/eval_web/eval_python.js | 113 +++- comfy_extras/nodes/nodes_eval.py | 32 +- tests/unit/test_eval_nodes.py | 14 +- 4 files changed, 105 insertions(+), 823 deletions(-) delete mode 100644 comfy_extras/eval_web/ace_utils.js diff --git a/comfy_extras/eval_web/ace_utils.js b/comfy_extras/eval_web/ace_utils.js deleted file mode 100644 index 78c00d809..000000000 --- a/comfy_extras/eval_web/ace_utils.js +++ /dev/null @@ -1,769 +0,0 @@ -/** - * Uses code adapted from https://github.com/yorkane/ComfyUI-KYNode - * - * MIT License - * - * Copyright (c) 2024 Kevin Yuan - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -// Make modal window -function makeModal({ title = "Message", text = "No text", type = "info", parent = null, stylePos = "fixed", classes = [] } = {}) { - const overlay = document.createElement("div"); - Object.assign(overlay.style, { - display: "none", - position: stylePos, - background: "rgba(0 0 0 / 0.8)", - opacity: 0, - top: "0", - left: "0", - right: "0", - bottom: "0", - zIndex: "500", - transition: "all .8s", - cursor: "pointer", - }); - - const boxModal = document.createElement("div"); - Object.assign(boxModal.style, { - transition: "all 0.5s", - opacity: 0, - display: "none", - position: stylePos, - top: "50%", - left: "50%", - transform: "translate(-50%,-50%)", - background: "#525252", - minWidth: "300px", - fontFamily: "sans-serif", - zIndex: "501", - border: "1px solid rgb(255 255 255 / 45%)", - }); - - boxModal.className = "alekpet_modal_window"; - boxModal.classList.add(...classes); - - const boxModalBody = document.createElement("div"); - Object.assign(boxModalBody.style, { - display: "flex", - flexDirection: "column", - textAlign: "center", - }); - - boxModalBody.className = "alekpet_modal_body"; - - const boxModalHtml = ` -
-
${title}
-
-
-
${text}
`; - boxModalBody.innerHTML = boxModalHtml; - - const alekpet_modal_header = boxModalBody.querySelector(".alekpet_modal_header"); - Object.assign(alekpet_modal_header.style, { - display: "flex", - alignItems: "center", - }); - - const close = boxModalBody.querySelector(".alekpet_modal_close"); - Object.assign(close.style, { - cursor: "pointer", - }); - - let parentElement = document.body; - if (parent && parent.nodeType === 1) { - parentElement = parent; - } - - boxModal.append(boxModalBody); - parentElement.append(overlay, boxModal); - - const removeEvent = new Event("removeElements"); - const remove = () => { - animateTransitionProps(boxModal, { opacity: 0 }).then(() => - animateTransitionProps(overlay, { opacity: 0 }).then(() => { - parentElement.removeChild(boxModal); - parentElement.removeChild(overlay); - }), - ); - }; - - boxModal.addEventListener("removeElements", remove); - overlay.addEventListener("removeElements", remove); - - animateTransitionProps(overlay) - .then(() => { - overlay.addEventListener("click", () => { - overlay.dispatchEvent(removeEvent); - }); - animateTransitionProps(boxModal); - }) - .then(() => boxModal.querySelector(".alekpet_modal_close").addEventListener("click", () => boxModal.dispatchEvent(removeEvent))); -} - -function findWidget(node, value, attr = "name", func = "find") { - return node?.widgets ? node.widgets[func]((w) => (Array.isArray(value) ? value.includes(w[attr]) : w[attr] === value)) : null; -} - -function animateTransitionProps(el, props = { opacity: 1 }, preStyles = { display: "block" }) { - Object.assign(el.style, preStyles); - - el.style.transition = !el.style.transition || !window.getComputedStyle(el).getPropertyValue("transition") ? "all .8s" : el.style.transition; - - return new Promise((res) => { - setTimeout(() => { - Object.assign(el.style, props); - - const transstart = () => (el.isAnimating = true); - const transchancel = () => (el.isAnimating = false); - el.addEventListener("transitionstart", transstart); - el.addEventListener("transitioncancel", transchancel); - - el.addEventListener("transitionend", function transend() { - el.isAnimating = false; - el.removeEventListener("transitionend", transend); - el.removeEventListener("transitionend", transchancel); - el.removeEventListener("transitionend", transstart); - res(el); - }); - }, 100); - }); -} - -function animateClick(target, params = {}) { - const { opacityVal = 0.9, callback = () => {} } = params; - if (target?.isAnimating) return; - - const hide = +target.style.opacity === 0; - return animateTransitionProps(target, { - opacity: hide ? opacityVal : 0, - }).then((el) => { - const isHide = hide || el.style.display === "none"; - showHide({ elements: [target], hide: !hide }); - callback(); - return isHide; - }); -} - -function showHide({ elements = [], hide = null, displayProp = "block" } = {}) { - Array.from(elements).forEach((el) => { - if (hide !== null) { - el.style.display = !hide ? displayProp : "none"; - } else { - el.style.display = !el.style.display || el.style.display === "none" ? displayProp : "none"; - } - }); -} - -function isEmptyObject(obj) { - if (!obj) return true; - return Object.keys(obj).length === 0 && obj.constructor === Object; -} - -function makeElement(tag, attrs = {}) { - if (!tag) tag = "div"; - const element = document.createElement(tag); - Object.keys(attrs).forEach((key) => { - const currValue = attrs[key]; - if (key === "class") { - if (Array.isArray(currValue)) { - element.classList.add(...currValue); - } else if (currValue instanceof String || typeof currValue === "string") { - element.className = currValue; - } - } else if (key === "dataset") { - try { - if (Array.isArray(currValue)) { - currValue.forEach((datasetArr) => { - const [prop, propval] = Object.entries(datasetArr)[0]; - element.dataset[prop] = propval; - }); - } else { - Object.entries(currValue).forEach((datasetArr) => { - const [prop, propval] = datasetArr; - element.dataset[prop] = propval; - }); - } - } catch (err) { - console.log(err); - } - } else if (key === "style") { - if (typeof currValue === "object" && !Array.isArray(currValue) && Object.keys(currValue).length) { - Object.assign(element[key], currValue); - } else if (typeof currValue === "object" && Array.isArray(currValue) && currValue.length) { - element[key] = [...currValue]; - } else if (currValue instanceof String || typeof currValue === "string") { - element[key] = currValue; - } - } else if (["for"].includes(key)) { - element.setAttribute(key, currValue); - } else if (key === "children") { - element.append(...(currValue instanceof Array ? currValue : [currValue])); - } else if (key === "parent") { - currValue.append(element); - } else { - element[key] = currValue; - } - }); - return element; -} - -function isValidStyle(opt, strColor) { - let op = new Option().style; - if (!op.hasOwnProperty(opt)) return { result: false, color: "", color_hex: "" }; - - op[opt] = strColor; - - return { - result: op[opt] !== "", - color_rgb: op[opt], - color_hex: rgbToHex(op[opt]), - }; -} - -function rgbToHex(rgb) { - const regEx = new RegExp(/\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/); - if (regEx.test(rgb)) { - let [, r, g, b] = regEx.exec(rgb); - r = parseInt(r).toString(16); - g = parseInt(g).toString(16); - b = parseInt(b).toString(16); - - r = r.length === 1 ? r + "0" : r; - g = g.length === 1 ? g + "0" : g; - b = b.length === 1 ? b + "0" : b; - - return `#${r}${g}${b}`; - } -} - -async function getDataJSON(url) { - try { - const response = await fetch(url); - const jsonData = await response.json(); - return jsonData; - } catch (err) { - return new Error(err); - } -} - -function deepMerge(target, source) { - if (source?.nodeType) return; - for (let key in source) { - if (source[key] instanceof Object && key in target) { - Object.assign(source[key], deepMerge(target[key], source[key])); - } - } - - Object.assign(target || {}, source); - return target; -} - -const THEME_MODAL_WINDOW_BASE = { - stylesTitle: { - background: "auto", - padding: "5px", - borderRadius: "6px", - marginBottom: "5px", - alignSelf: "stretch", - }, - stylesWrapper: { - display: "none", - opacity: 0, - minWidth: "220px", - position: "absolute", - left: "50%", - top: "50%", - transform: "translate(-50%, -50%)", - transition: "all .8s", - fontFamily: "monospace", - zIndex: 99999, - }, - stylesBox: { - display: "flex", - flexDirection: "column", - background: "#0e0e0e", - padding: "6px", - justifyContent: "center", - alignItems: "center", - gap: "3px", - textAlign: "center", - borderRadius: "6px", - color: "white", - border: "2px solid silver", - boxShadow: "2px 2px 4px silver", - maxWidth: "300px", - }, - stylesClose: { - position: "absolute", - top: "-10px", - right: "-10px", - background: "silver", - borderRadius: "50%", - width: "20px", - height: "20px", - cursor: "pointer", - display: "flex", - justifyContent: "center", - alignItems: "center", - fontSize: "0.8rem", - }, -}; - -const THEMES_MODAL_WINDOW = { - error: { - stylesTitle: { - ...THEME_MODAL_WINDOW_BASE.stylesTitle, - background: "#8f210f", - }, - stylesBox: { - ...THEME_MODAL_WINDOW_BASE.stylesBox, - background: "#3b2222", - boxShadow: "3px 3px 6px #141414", - border: "1px solid #f91b1b", - }, - stylesWrapper: { ...THEME_MODAL_WINDOW_BASE.stylesWrapper }, - stylesClose: { - ...THEME_MODAL_WINDOW_BASE.stylesClose, - background: "#3b2222", - }, - }, - warning: { - stylesTitle: { - ...THEME_MODAL_WINDOW_BASE.stylesTitle, - background: "#e99818", - }, - stylesBox: { - ...THEME_MODAL_WINDOW_BASE.stylesBox, - background: "#594e32", - boxShadow: "3px 3px 6px #141414", - border: "1px solid #e99818", - }, - stylesWrapper: { ...THEME_MODAL_WINDOW_BASE.stylesWrapper }, - stylesClose: { - ...THEME_MODAL_WINDOW_BASE.stylesClose, - background: "#594e32", - }, - }, - normal: { - stylesTitle: { - ...THEME_MODAL_WINDOW_BASE.stylesTitle, - background: "#108f0f", - }, - stylesBox: { - ...THEME_MODAL_WINDOW_BASE.stylesBox, - background: "#223b2a", - boxShadow: "3px 3px 6px #141414", - border: "1px solid #108f0f", - }, - stylesWrapper: { ...THEME_MODAL_WINDOW_BASE.stylesWrapper }, - stylesClose: { - ...THEME_MODAL_WINDOW_BASE.stylesClose, - background: "#223b2a", - }, - }, -}; - -const defaultOptions = { - auto: { - autohide: false, - autoshow: false, - autoremove: false, - propStyles: { opacity: 0 }, - propPreStyles: {}, - timewait: 2000, - }, - overlay: { - overlay_enabled: false, - overlayClasses: [], - overlayStyles: {}, - }, - close: { closeRemove: false, showClose: true }, - parent: null, -}; - -function createWindowModal({ textTitle = "Message", textBody = "Hello world!", textFooter = null, classesWrapper = [], stylesWrapper = {}, classesBox = [], stylesBox = {}, classesTitle = [], stylesTitle = {}, classesBody = [], stylesBody = {}, classesClose = [], stylesClose = {}, classesFooter = [], stylesFooter = {}, options = defaultOptions } = {}) { - // Check all options exist - const _options = deepMerge(JSON.parse(JSON.stringify(defaultOptions)), options); - - const { - parent, - overlay: { overlay_enabled, overlayClasses, overlayStyles }, - close: { closeRemove, showClose }, - auto: { autohide, autoshow, autoremove, timewait, propStyles, propPreStyles }, - } = _options; - - // Function past text(html) - function addText(text, parent) { - if (!parent) return; - - switch (typeof text) { - case "string": - if (/^\<.*\/?\>$/.test(text)) { - parent.innerHTML = text; - } else { - parent.textContent = text; - } - break; - case "object": - default: - if (Array.isArray(text)) { - text.forEach((element) => (element.nodeType === 1 || element.nodeType === 3) && parent.append(element)); - } else if (text.nodeType === 1 || text.nodeType === 3) parent.append(text); - } - } - - // Overlay - let overlayElement = null; - if (overlay_enabled) { - overlayElement = makeElement("div", { - class: [...overlayClasses], - style: { - display: "none", - position: "fixed", - background: "rgba(0 0 0 / 0.8)", - opacity: 0, - top: 0, - left: 0, - right: 0, - bottom: 0, - zIndex: 99999, - transition: "all .8s", - cursor: "pointer", - ...overlayStyles, - }, - }); - } - - // Wrapper - const wrapper_settings = makeElement("div", { - class: ["alekpet__wrapper__window", ...classesWrapper], - }); - - Object.assign(wrapper_settings.style, { - ...THEME_MODAL_WINDOW_BASE.stylesWrapper, - ...stylesWrapper, - }); - - // Box - const box__settings = makeElement("div", { - class: ["alekpet__window__box", ...classesBox], - }); - Object.assign(box__settings.style, { - ...THEME_MODAL_WINDOW_BASE.stylesBox, - ...stylesBox, - }); - - // Title - let box_settings_title = ""; - if (textTitle) { - box_settings_title = makeElement("div", { - class: ["alekpet__window__title", ...classesTitle], - }); - - Object.assign(box_settings_title.style, { - ...THEME_MODAL_WINDOW_BASE.stylesTitle, - ...stylesTitle, - }); - - // Add text (html) to title - addText(textTitle, box_settings_title); - } - // Body - let box_settings_body = ""; - if (textBody) { - box_settings_body = makeElement("div", { - class: ["alekpet__window__body", ...classesBody], - }); - - Object.assign(box_settings_body.style, { - display: "flex", - flexDirection: "column", - alignItems: "flex-end", - gap: "5px", - textWrap: "wrap", - ...stylesBody, - }); - - // Add text (html) to body - addText(textBody, box_settings_body); - } - - // Close button - const close__box__button = makeElement("div", { - class: ["close__box__button", ...classesClose], - textContent: "✖", - }); - - Object.assign(close__box__button.style, { - ...THEME_MODAL_WINDOW_BASE.stylesClose, - ...stylesClose, - }); - - if (!showClose) close__box__button.style.display = "none"; - - const closeEvent = new Event("closeModal"); - const closeModalWindow = function () { - overlay_enabled - ? animateTransitionProps(overlayElement, { - opacity: 0, - }) - .then(() => - animateTransitionProps(wrapper_settings, { - opacity: 0, - }), - ) - .then(() => { - if (closeRemove) { - parent.removeChild(wrapper_settings); - parent.removeChild(overlayElement); - } else { - showHide({ elements: [wrapper_settings, overlayElement] }); - } - }) - : animateTransitionProps(wrapper_settings, { - opacity: 0, - }).then(() => { - showHide({ elements: [wrapper_settings] }); - }); - }; - - close__box__button.addEventListener("closeModal", closeModalWindow); - - close__box__button.addEventListener("click", () => close__box__button.dispatchEvent(closeEvent)); - - close__box__button.onmouseenter = () => { - close__box__button.style.opacity = 0.8; - }; - - close__box__button.onmouseleave = () => { - close__box__button.style.opacity = 1; - }; - - box__settings.append(box_settings_title, box_settings_body); - - // Footer - if (textFooter) { - const box_settings_footer = makeElement("div", { - class: [...classesFooter], - }); - Object.assign(box_settings_footer.style, { - ...stylesFooter, - }); - - // Add text (html) to body - addText(textFooter, box_settings_footer); - - box__settings.append(box_settings_footer); - } - - wrapper_settings.append(close__box__button, box__settings); - - if (parent && parent.nodeType === 1) { - if (overlay_enabled) parent.append(overlayElement); - parent.append(wrapper_settings); - - if (autoshow) { - overlay_enabled - ? animateClick(overlayElement).then(() => - animateClick(wrapper_settings).then( - () => - autohide && - setTimeout( - () => - animateTransitionProps(wrapper_settings, { ...propStyles }, { ...propPreStyles }) - .then(() => animateTransitionProps(overlayElement, { ...propStyles }, { ...propPreStyles })) - .then(() => { - if (autoremove) { - parent.removeChild(wrapper_settings); - parent.removeChild(overlayElement); - } - }), - timewait, - ), - ), - ) - : animateClick(wrapper_settings).then(() => autohide && setTimeout(() => animateTransitionProps(wrapper_settings, { ...propStyles }, { ...propPreStyles }).then(() => autoremove && parent.removeChild(wrapper_settings)), timewait)); - } - } - - return wrapper_settings; -} - -// Prompt -async function comfyuiDesktopPrompt(title, message, defaultValue) { - try { - return await app.extensionManager.dialog.prompt({ - title, - message, - defaultValue, - }); - } catch (err) { - return prompt(title, message); - } -} - -// Alert -function comfyuiDesktopAlert(message) { - try { - app.extensionManager.toast.addAlert(message); - } catch (err) { - alert(message); - } -} - -// Confirm -function confirmModal({ title, message }) { - return new Promise((res) => { - const overlay = makeElement("div", { - class: ["alekpet_confOverlay"], - style: { - background: "rgba(0, 0, 0, 0.7)", - position: "fixed", - top: 0, - left: 0, - right: 0, - bottom: 0, - zIndex: 9999, - userSelect: "none", - }, - }); - - const modal = makeElement("div", { - class: ["alekpet_confModal"], - style: { - ...THEME_MODAL_WINDOW_BASE.stylesBox, - position: "fixed", - top: "50%", - left: "50%", - fontFamily: "monospace", - background: "rgb(92 186 255 / 20%)", - transform: "translate(-50%, -50%)", - borderColor: "rgba(92, 186, 255, 0.63)", - boxShadow: "rgba(92, 186, 255, 0.63) 2px 2px 4px", - }, - }); - - const titleEl = makeElement("div", { - class: ["alekpet_confTitle"], - style: { - ...THEME_MODAL_WINDOW_BASE.stylesTitle, - background: "rgba(92, 186, 255, 0.63)", - }, - textContent: title, - }); - - const messageEl = makeElement("div", { - class: ["alekpet_confMessage"], - style: { - display: "flex", - flexDirection: "column", - alignItems: "flex-end", - gap: "5px", - textWrap: "wrap", - }, - textContent: message, - }); - - const action_box = makeElement("div", { - class: ["alekpet_confActions"], - style: { - display: "flex", - gap: "5px", - width: "100%", - padding: "4px", - justifyContent: "flex-end", - }, - }); - - const remove = () => { - modal.remove(); - overlay.remove(); - }; - - const ok = makeElement("div", { - class: ["alekpet_confButtons", "alekpet_confButtonOk"], - style: { - background: "linear-gradient(45deg, green, limegreen) rgb(21, 100, 6)", - }, - textContent: "Ok", - onclick: (e) => { - res(true); - remove(); - }, - }); - - const Cancel = makeElement("div", { - class: ["alekpet_confButtons", "alekpet_confButtonCancel"], - style: { - background: "linear-gradient(45deg, #b64396, #a52a8b) rgb(135 3 161)", - }, - textContent: "Cancel", - onclick: (e) => { - res(false); - remove(); - }, - }); - - action_box.append(ok, Cancel); - modal.append(titleEl, messageEl, action_box); - overlay.append(modal); - document.body.append(overlay); - }); -} - -async function comfyuiDesktopConfirm(message) { - try { - const result = await confirmModal({ - title: "Confirm", - message: message, - }); - - // Wait update comfyui frontend! Confirm Cancel not return value! Fixed in ComfyUI_frontend ver. v1.10.8 - // https://github.com/Comfy-Org/ComfyUI_frontend/issues/2649 - // const result = await app.extensionManager.dialog.confirm({ - // title: "Confirm", - // message: message, - // }); - return result; - } catch (err) { - return confirm(message); - } -} - -export { - makeModal, - createWindowModal, - animateTransitionProps, - animateClick, - showHide, - makeElement, - getDataJSON, - isEmptyObject, - isValidStyle, - rgbToHex, - findWidget, - THEMES_MODAL_WINDOW, - // - comfyuiDesktopConfirm, - comfyuiDesktopPrompt, - comfyuiDesktopAlert, -}; diff --git a/comfy_extras/eval_web/eval_python.js b/comfy_extras/eval_web/eval_python.js index d8021a7b2..a7e2fa19f 100644 --- a/comfy_extras/eval_web/eval_python.js +++ b/comfy_extras/eval_web/eval_python.js @@ -24,7 +24,6 @@ * SOFTWARE. */ import { app } from "../../scripts/app.js"; -import { makeElement, findWidget } from "./ace_utils.js"; // Load Ace editor using script tag for Safari compatibility // The noconflict build includes AMD loader that works in all browsers @@ -34,7 +33,7 @@ const aceLoadPromise = new Promise((resolve) => { ace = window.ace; resolve(); } else { - const script = document.createElement('script'); + const script = document.createElement("script"); script.src = "https://cdn.jsdelivr.net/npm/ace-builds@1.43.4/src-noconflict/ace.js"; script.onload = () => { ace = window.ace; @@ -45,44 +44,108 @@ const aceLoadPromise = new Promise((resolve) => { } }); +// todo: do we really want to do this here? await aceLoadPromise; +const findWidget = (node, value, attr = "name", func = "find") => { + return node?.widgets ? node.widgets[func]((w) => (Array.isArray(value) ? value.includes(w[attr]) : w[attr] === value)) : null; +}; +const makeElement = (tag, attrs = {}) => { + if (!tag) tag = "div"; + const element = document.createElement(tag); + Object.keys(attrs).forEach((key) => { + const currValue = attrs[key]; + if (key === "class") { + if (Array.isArray(currValue)) { + element.classList.add(...currValue); + } else if (currValue instanceof String || typeof currValue === "string") { + element.className = currValue; + } + } else if (key === "dataset") { + try { + if (Array.isArray(currValue)) { + currValue.forEach((datasetArr) => { + const [prop, propval] = Object.entries(datasetArr)[0]; + element.dataset[prop] = propval; + }); + } else { + Object.entries(currValue).forEach((datasetArr) => { + const [prop, propval] = datasetArr; + element.dataset[prop] = propval; + }); + } + } catch (err) { + // todo: what is this trying to do? + } + } else if (key === "style") { + if (typeof currValue === "object" && !Array.isArray(currValue) && Object.keys(currValue).length) { + Object.assign(element[key], currValue); + } else if (typeof currValue === "object" && Array.isArray(currValue) && currValue.length) { + element[key] = [...currValue]; + } else if (currValue instanceof String || typeof currValue === "string") { + element[key] = currValue; + } + } else if (["for"].includes(key)) { + element.setAttribute(key, currValue); + } else if (key === "children") { + element.append(...(currValue instanceof Array ? currValue : [currValue])); + } else if (key === "parent") { + currValue.append(element); + } else { + element[key] = currValue; + } + }); + return element; +}; -function getPosition(node, ctx, w_width, y, n_height) { +const getPosition = (node, ctx, w_width, y, n_height) => { const margin = 5; const rect = ctx.canvas.getBoundingClientRect(); - const transform = new DOMMatrix() - .scaleSelf(rect.width / ctx.canvas.width, rect.height / ctx.canvas.height) - .multiplySelf(ctx.getTransform()) - .translateSelf(margin, margin + y); - const scale = new DOMMatrix().scaleSelf(transform.a, transform.d); + const transform = ctx.getTransform(); + const scale = app.canvas.ds.scale; + + // The context is already transformed to draw at the widget position + // transform.e and transform.f give us the canvas coordinates (in canvas pixels) + // We need to convert these to screen pixels by accounting for the canvas scale + // rect gives us the canvas element's position on the page + + // The transform matrix has scale baked in (transform.a = transform.d = scale) + // transform.e and transform.f are the translation in canvas-pixel space + const canvasPixelToScreenPixel = rect.width / ctx.canvas.width; + + const x = transform.e * canvasPixelToScreenPixel + rect.left; + const y_pos = transform.f * canvasPixelToScreenPixel + rect.top; + + // Convert widget dimensions from canvas coordinates to screen pixels + const scaledWidth = w_width * scale; + const scaledHeight = (n_height - y - 15) * scale; + const scaledMargin = margin * scale; + const scaledY = y * scale; return { - transformOrigin: "0 0", - transform: scale, - left: `${transform.a + transform.e + rect.left}px`, - top: `${transform.d + transform.f + rect.top}px`, - maxWidth: `${w_width - margin * 2}px`, - maxHeight: `${n_height - margin * 2 - y - 15}px`, - width: `${w_width - margin * 2}px`, - height: "90%", + left: `${x + scaledMargin}px`, + top: `${y_pos + scaledY + scaledMargin}px`, + width: `${scaledWidth - scaledMargin * 2}px`, + maxWidth: `${scaledWidth - scaledMargin * 2}px`, + height: `${scaledHeight - scaledMargin * 2}px`, + maxHeight: `${scaledHeight - scaledMargin * 2}px`, position: "absolute", scrollbarColor: "var(--descrip-text) var(--bg-color)", scrollbarWidth: "thin", zIndex: app.graph._nodes.indexOf(node), }; -} +}; // Create code editor widget -function codeEditor(node, inputName, inputData) { +const codeEditor = (node, inputName, inputData) => { const widget = { - type: "pycode", + type: "code_block_python", name: inputName, options: { hideOnZoom: true }, value: inputData[1]?.default || "", draw(ctx, node, widgetWidth, y) { - const hidden = node.flags?.collapsed || (!!this.options.hideOnZoom && app.canvas.ds.scale < 0.5) || this.type === "converted-widget" || this.type === "hidden"; + const hidden = node.flags?.collapsed || (!!this.options.hideOnZoom && app.canvas.ds.scale < 0.5) || this.type === "converted-widget" || this.type === "hidden" || this.type === "converted-widget"; this.codeElement.hidden = hidden; @@ -122,19 +185,19 @@ function codeEditor(node, inputName, inputData) { }; return widget; -} +}; // Trigger workflow change tracking -function markWorkflowChanged() { +const markWorkflowChanged = () => { app?.extensionManager?.workflow?.activeWorkflow?.changeTracker?.checkState(); -} +}; // Register extensions app.registerExtension({ name: "Comfy.EvalPython", getCustomWidgets(app) { return { - PYCODE: (node, inputName, inputData) => { + CODE_BLOCK_PYTHON: (node, inputName, inputData) => { const widget = codeEditor(node, inputName, inputData); widget.editor.getSession().on("change", () => { @@ -165,7 +228,7 @@ app.registerExtension({ originalOnConfigure?.apply(this, arguments); if (info?.widgets_values?.length) { - const widgetCodeIndex = findWidget(this, "pycode", "type", "findIndex"); + const widgetCodeIndex = findWidget(this, "code_block_python", "type", "findIndex"); const editor = this.widgets[widgetCodeIndex]?.editor; if (editor) { diff --git a/comfy_extras/nodes/nodes_eval.py b/comfy_extras/nodes/nodes_eval.py index ff04522eb..bda41a02d 100644 --- a/comfy_extras/nodes/nodes_eval.py +++ b/comfy_extras/nodes/nodes_eval.py @@ -36,7 +36,7 @@ return {", ".join([f"value{i}" for i in range(inputs)])} return { "required": { "pycode": ( - "PYCODE", + "CODE_BLOCK_PYTHON", { "default": default_code }, @@ -47,16 +47,19 @@ return {", ".join([f"value{i}" for i in range(inputs)])} RETURN_TYPES = tuple(IO.ANY for _ in range(outputs)) RETURN_NAMES = tuple(f"item{i}" for i in range(outputs)) - OUTPUT_IS_LIST = output_is_list - INPUT_IS_LIST = input_is_list is not None FUNCTION = "exec_py" DESCRIPTION = "" CATEGORY = "eval" + @classmethod + def VALIDATE_INPUTS(cls, *args, **kwargs): + ctx = current_execution_context() + + return ctx.configuration.enable_eval + def exec_py(self, pycode, **kwargs): ctx = current_execution_context() - # Ensure all value inputs have a default of None kwargs = { **{f"value{i}": None for i in range(inputs)}, **kwargs, @@ -68,11 +71,9 @@ return {", ".join([f"value{i}" for i in range(inputs)])} if not ctx.configuration.enable_eval: raise ValueError("Python eval is disabled") - # Extract value arguments in order value_args = [kwargs.pop(f"value{i}") for i in range(inputs)] arg_names = ", ".join(f"value{i}=None" for i in range(inputs)) - # Wrap pycode in a function to support return statements wrapped_code = f"def _eval_func({arg_names}):\n" for line in pycode.splitlines(): wrapped_code += " " + line + "\n" @@ -83,13 +84,8 @@ return {", ".join([f"value{i}" for i in range(inputs)])} "print": print, } - # Execute wrapped function definition exec(wrapped_code, globals_for_eval) - - # Call the function with value arguments results = globals_for_eval["_eval_func"](*value_args) - - # Normalize results to match output count if not isinstance(results, tuple): results = (results,) @@ -100,22 +96,24 @@ return {", ".join([f"value{i}" for i in range(inputs)])} return results - # Set the class name for better debugging/introspection + # todo: interact better with the weird comfyui machinery for this + if input_is_list is not None: + setattr(EvalPythonNode, "INPUT_IS_LIST", input_is_list) + + if output_is_list is not None: + setattr(EvalPythonNode, "OUTPUT_IS_LIST", output_is_list) + EvalPythonNode.__name__ = name EvalPythonNode.__qualname__ = name return EvalPythonNode -# Create the default EvalPython node with 5 inputs and 5 outputs +EvalPython_1_1 = eval_python(inputs=1, outputs=1, name="EvalPython_1_1") EvalPython_5_5 = eval_python(inputs=5, outputs=5, name="EvalPython_5_5") -EvalPython = EvalPython_5_5 # Backward compatibility alias - -# Create list variants EvalPython_List_1 = eval_python(inputs=1, outputs=1, name="EvalPython_List_1", input_is_list=True, output_is_list=None) EvalPython_1_List = eval_python(inputs=1, outputs=1, name="EvalPython_1_List", input_is_list=None, output_is_list=(True,)) EvalPython_List_List = eval_python(inputs=1, outputs=1, name="EvalPython_List_List", input_is_list=True, output_is_list=(True,)) - export_custom_nodes() export_package_as_web_directory("comfy_extras.eval_web") diff --git a/tests/unit/test_eval_nodes.py b/tests/unit/test_eval_nodes.py index f2cb0c763..71076daef 100644 --- a/tests/unit/test_eval_nodes.py +++ b/tests/unit/test_eval_nodes.py @@ -4,9 +4,8 @@ from unittest.mock import Mock, patch from comfy.cli_args import default_configuration from comfy.execution_context import context_configuration from comfy_extras.nodes.nodes_eval import ( - EvalPython, - EvalPython_5_5, eval_python, + EvalPython_5_5, EvalPython_List_1, EvalPython_1_List, EvalPython_List_List, @@ -447,7 +446,7 @@ def test_eval_python_input_types(): assert "required" in input_types assert "optional" in input_types assert "pycode" in input_types["required"] - assert input_types["required"]["pycode"][0] == "PYCODE" + assert input_types["required"]["pycode"][0] == "CODE_BLOCK_PYTHON" # Check optional inputs for i in range(5): @@ -560,7 +559,6 @@ def test_eval_python_list_1_input_is_list(eval_context): # Verify INPUT_IS_LIST is set assert EvalPython_List_1.INPUT_IS_LIST is True - assert EvalPython_List_1.OUTPUT_IS_LIST is None # Test that value0 receives a list result = node.exec_py( @@ -586,7 +584,6 @@ def test_eval_python_1_list_output_is_list(eval_context): node = EvalPython_1_List() # Verify OUTPUT_IS_LIST is set - assert EvalPython_1_List.INPUT_IS_LIST is False assert EvalPython_1_List.OUTPUT_IS_LIST == (True,) # Test that returns a list @@ -652,7 +649,6 @@ def test_eval_python_factory_with_list_flags(eval_context): ListInputNode = eval_python(inputs=1, outputs=1, input_is_list=True, output_is_list=None) assert ListInputNode.INPUT_IS_LIST is True - assert ListInputNode.OUTPUT_IS_LIST is None node = ListInputNode() result = node.exec_py( @@ -666,7 +662,6 @@ def test_eval_python_factory_scalar_output_list(eval_context): """Test factory function with scalar input and list output""" ScalarToListNode = eval_python(inputs=1, outputs=1, input_is_list=None, output_is_list=(True,)) - assert ScalarToListNode.INPUT_IS_LIST is False assert ScalarToListNode.OUTPUT_IS_LIST == (True,) node = ScalarToListNode() @@ -686,8 +681,3 @@ def test_eval_python_list_empty_list(eval_context): value0=[] ) assert result == ([],) - - -def test_eval_python_backward_compatibility(): - """Test that EvalPython is an alias for EvalPython_5_5""" - assert EvalPython is EvalPython_5_5