diff --git a/web/index.js b/web/index.js index 562b568..9ccec33 100644 --- a/web/index.js +++ b/web/index.js @@ -1,17 +1,35 @@ -import './js/betterCombos.js'; -import './js/image_comparer.js'; -import './addons/zip_loader/js/zip_loader.js'; -import './loraloader_mxd_entry.js'; -import './mxd_character_prompts.js'; -import './ltx_sampler_mxd.js'; -import './wan22_video_prep_mxd.js'; -import './load_image_batch_mxd.js'; -import './load_video_batch_mxd.js'; -import './load_single_file_mxd.js'; -import './run_folder_mxd.js'; -import './video_preview_mxd.js'; -import './live_preview_panel_mxd.js'; -import './combine_materials_ffgo_mxd.js'; -import './model_storage_settings_mxd.js'; -import './checkpoint_loader_mxd.js'; +import './lib/mxd_runtime.js'; +import './lib/mxd_shared_utils.js'; +import './lib/mxd_utils_dom.js'; +import './lib/mxd_utils_canvas.js'; +import './lib/mxd_utils_widgets.js'; +import './lib/mxd_utils_menu.js'; +import './lib/mxd_utils.js'; +import './lib/mxd_base_node.js'; +import './lib/mxd_dialog.js'; +import './lib/mxd_dialog_info.js'; +import './lib/mxd_menu.js'; +import './lib/mxd_svgs.js'; +import './lib/mxd_api.js'; +import './lib/mxd_model_info_service.js'; +import './lib/mxd_model_row_widget.js'; +import './lib/mxd_smart_search.js'; +import './nodes/better_combos.js'; +import './nodes/image_comparer.js'; +import './vendor/zip_loader/zip_loader.js'; +import './nodes/power_lora_base.js'; +import './nodes/power_lora_loader.js'; +import './nodes/ltx2_power_lora_loader.js'; +import './nodes/character_prompts.js'; +import './nodes/ltx_sampler.js'; +import './nodes/wan22_video_prep.js'; +import './nodes/load_image_batch.js'; +import './nodes/load_video_batch.js'; +import './nodes/load_single_file.js'; +import './nodes/run_folder.js'; +import './nodes/video_preview.js'; +import './nodes/live_preview_panel.js'; +import './nodes/combine_materials_ffgo.js'; +import './nodes/model_storage_settings.js'; +import './nodes/checkpoint_loader.js'; diff --git a/web/mxd_api.js b/web/lib/mxd_api.js similarity index 100% rename from web/mxd_api.js rename to web/lib/mxd_api.js diff --git a/web/mxd_base_node.js b/web/lib/mxd_base_node.js similarity index 98% rename from web/mxd_base_node.js rename to web/lib/mxd_base_node.js index 650c989..d975541 100644 --- a/web/mxd_base_node.js +++ b/web/lib/mxd_base_node.js @@ -1,5 +1,5 @@ -import { app } from "../../scripts/app.js"; -import { ComfyWidgets } from "../../scripts/widgets.js"; +import { app } from "../../../scripts/app.js"; +import { ComfyWidgets } from "../../../scripts/widgets.js"; import { defineProperty, moveArrayItem } from "./mxd_shared_utils.js"; export class MxdBaseNode extends LGraphNode { diff --git a/web/mxd_buttons.css b/web/lib/mxd_buttons.css similarity index 100% rename from web/mxd_buttons.css rename to web/lib/mxd_buttons.css diff --git a/web/mxd_dialog.css b/web/lib/mxd_dialog.css similarity index 100% rename from web/mxd_dialog.css rename to web/lib/mxd_dialog.css diff --git a/web/mxd_dialog.js b/web/lib/mxd_dialog.js similarity index 100% rename from web/mxd_dialog.js rename to web/lib/mxd_dialog.js diff --git a/web/mxd_dialog_base.css b/web/lib/mxd_dialog_base.css similarity index 100% rename from web/mxd_dialog_base.css rename to web/lib/mxd_dialog_base.css diff --git a/web/mxd_dialog_info.js b/web/lib/mxd_dialog_info.js similarity index 100% rename from web/mxd_dialog_info.js rename to web/lib/mxd_dialog_info.js diff --git a/web/mxd_dialog_model_info.css b/web/lib/mxd_dialog_model_info.css similarity index 100% rename from web/mxd_dialog_model_info.css rename to web/lib/mxd_dialog_model_info.css diff --git a/web/mxd_menu.css b/web/lib/mxd_menu.css similarity index 100% rename from web/mxd_menu.css rename to web/lib/mxd_menu.css diff --git a/web/mxd_menu.js b/web/lib/mxd_menu.js similarity index 100% rename from web/mxd_menu.js rename to web/lib/mxd_menu.js diff --git a/web/mxd_model_info_service.js b/web/lib/mxd_model_info_service.js similarity index 98% rename from web/mxd_model_info_service.js rename to web/lib/mxd_model_info_service.js index 4e2c605..b8a3794 100644 --- a/web/mxd_model_info_service.js +++ b/web/lib/mxd_model_info_service.js @@ -1,5 +1,5 @@ import { mxdApi } from "./mxd_api.js"; -import { api } from "../../scripts/api.js"; +import { api } from "../../../scripts/api.js"; class BaseModelInfoService extends EventTarget { constructor(modelInfoType, apiRefreshEventString) { diff --git a/web/mxd_model_row_widget.js b/web/lib/mxd_model_row_widget.js similarity index 99% rename from web/mxd_model_row_widget.js rename to web/lib/mxd_model_row_widget.js index d7c0040..9a5553c 100644 --- a/web/mxd_model_row_widget.js +++ b/web/lib/mxd_model_row_widget.js @@ -1,4 +1,4 @@ -import { app } from "../../scripts/app.js"; +import { app } from "../../../scripts/app.js"; import { MxdBaseWidget } from "./mxd_utils_widgets.js"; import { drawRoundedRectangle, drawInfoIcon, fitString, isLowQuality } from "./mxd_utils_canvas.js"; import { showModelChooser } from "./mxd_utils_menu.js"; diff --git a/web/mxd_runtime.js b/web/lib/mxd_runtime.js similarity index 98% rename from web/mxd_runtime.js rename to web/lib/mxd_runtime.js index b08260b..878ff73 100644 --- a/web/mxd_runtime.js +++ b/web/lib/mxd_runtime.js @@ -1,4 +1,4 @@ -import { app } from "../../scripts/app.js"; +import { app } from "../../../scripts/app.js"; class LogSession { constructor(name) { diff --git a/web/mxd_shared_utils.js b/web/lib/mxd_shared_utils.js similarity index 100% rename from web/mxd_shared_utils.js rename to web/lib/mxd_shared_utils.js diff --git a/web/mxd_smart_search.js b/web/lib/mxd_smart_search.js similarity index 100% rename from web/mxd_smart_search.js rename to web/lib/mxd_smart_search.js diff --git a/web/mxd_svgs.js b/web/lib/mxd_svgs.js similarity index 100% rename from web/mxd_svgs.js rename to web/lib/mxd_svgs.js diff --git a/web/mxd_utils.js b/web/lib/mxd_utils.js similarity index 99% rename from web/mxd_utils.js rename to web/lib/mxd_utils.js index 52b7619..142d970 100644 --- a/web/mxd_utils.js +++ b/web/lib/mxd_utils.js @@ -1,5 +1,5 @@ -import { app } from "../../scripts/app.js"; -import { api } from "../../scripts/api.js"; +import { app } from "../../../scripts/app.js"; +import { api } from "../../../scripts/api.js"; import { getResolver, wait } from "./mxd_shared_utils.js"; import { MxdHelpDialog } from "./mxd_dialog.js"; const oldApiGetNodeDefs = api.getNodeDefs; diff --git a/web/mxd_utils_canvas.js b/web/lib/mxd_utils_canvas.js similarity index 99% rename from web/mxd_utils_canvas.js rename to web/lib/mxd_utils_canvas.js index dc09972..72c6388 100644 --- a/web/mxd_utils_canvas.js +++ b/web/lib/mxd_utils_canvas.js @@ -1,4 +1,4 @@ -import { app } from "../../scripts/app.js"; +import { app } from "../../../scripts/app.js"; function binarySearch(max, getValue, match) { let min = 0; while (min <= max) { diff --git a/web/mxd_utils_dom.js b/web/lib/mxd_utils_dom.js similarity index 100% rename from web/mxd_utils_dom.js rename to web/lib/mxd_utils_dom.js diff --git a/web/mxd_utils_menu.js b/web/lib/mxd_utils_menu.js similarity index 99% rename from web/mxd_utils_menu.js rename to web/lib/mxd_utils_menu.js index 5d27878..04a1be7 100644 --- a/web/mxd_utils_menu.js +++ b/web/lib/mxd_utils_menu.js @@ -1,4 +1,4 @@ -import { app } from "../../scripts/app.js"; +import { app } from "../../../scripts/app.js"; import { mxdApi } from "./mxd_api.js"; import { isSmartMatch } from "./mxd_smart_search.js"; diff --git a/web/mxd_utils_widgets.js b/web/lib/mxd_utils_widgets.js similarity index 99% rename from web/mxd_utils_widgets.js rename to web/lib/mxd_utils_widgets.js index 084909e..ae110a8 100644 --- a/web/mxd_utils_widgets.js +++ b/web/lib/mxd_utils_widgets.js @@ -1,4 +1,4 @@ -import { app } from "../../scripts/app.js"; +import { app } from "../../../scripts/app.js"; import { drawNodeWidget, drawWidgetButton, fitString, isLowQuality } from "./mxd_utils_canvas.js"; export function drawLabelAndValue(ctx, label, value, width, posY, height, options) { var _a; diff --git a/web/loraloader_mxd_entry.js b/web/loraloader_mxd_entry.js deleted file mode 100644 index c835515..0000000 --- a/web/loraloader_mxd_entry.js +++ /dev/null @@ -1,3 +0,0 @@ -import "./mxd_runtime.js"; -import "./power_lora_loader_mxd.js"; -import "./ltx2_power_lora_loader_mxd.js"; diff --git a/web/js/betterCombos.js b/web/nodes/better_combos.js similarity index 62% rename from web/js/betterCombos.js rename to web/nodes/better_combos.js index a41aa76..2f13268 100644 --- a/web/js/betterCombos.js +++ b/web/nodes/better_combos.js @@ -1,25 +1,19 @@ -import { app } from "../../../scripts/app.js"; -import { ComfyWidgets } from "../../../scripts/widgets.js"; +// Folder-tree / grid display for the MXD latent loader combos (adapted from +// pysssss ComfyUI-Custom-Scripts' betterCombos; scoped to MXD nodes only). +import { app } from "../../../scripts/app.js"; import { $el } from "../../../scripts/ui.js"; import { api } from "../../../scripts/api.js"; -const CHECKPOINT_LOADER = "CheckpointLoader|pysssss"; -const LORA_LOADER = "LoraLoader|pysssss"; const LOAD_LATENT_I2V_MXD = "LoadLatent_I2V_MXD"; -const LOAD_LATENT_I2V_MXD_ALIAS = "LoadLatent_I2V_MXD MXD"; const LOAD_LATENTS_FROM_FOLDER_I2V_MXD = "LoadLatents_FromFolder_I2V_MXD"; -const LOAD_LATENTS_FROM_FOLDER_I2V_MXD_ALIAS = "LoadLatents_FromFolder_I2V_MXD MXD"; const LOAD_LATENT_I2V_PIPE_MXD = "LoadLatent_I2V_Pipe_MXD"; const LOAD_LATENTS_FROM_FOLDER_I2V_PIPE_MXD = "LoadLatents_FromFolder_I2V_Pipe_MXD"; -const ENABLE_PYSSSS_NODE_ENHANCEMENTS = false; const IMAGE_WIDTH = 384; const IMAGE_HEIGHT = 384; const NODE_CONFIGS = { [LOAD_LATENT_I2V_MXD]: { type: "latents", widgetName: "latent", hasImages: false }, - [LOAD_LATENT_I2V_MXD_ALIAS]: { type: "latents", widgetName: "latent", hasImages: false }, [LOAD_LATENTS_FROM_FOLDER_I2V_MXD]: { type: "latents", widgetName: "subfolder", hasImages: false }, - [LOAD_LATENTS_FROM_FOLDER_I2V_MXD_ALIAS]: { type: "latents", widgetName: "subfolder", hasImages: false }, [LOAD_LATENT_I2V_PIPE_MXD]: { type: "latents", widgetName: "latent", hasImages: false }, [LOAD_LATENTS_FROM_FOLDER_I2V_PIPE_MXD]: { type: "latents", widgetName: "subfolder", hasImages: false }, }; @@ -189,20 +183,6 @@ app.registerExtension({ `, parent: document.body, }); - const imageTypes = Object.keys(CONFIG_BY_TYPE).filter((type) => CONFIG_BY_TYPE[type].hasImages); - for (const type of imageTypes) { - ensureImageList(type).catch(() => {}); - } - - const refreshComboInNodes = app.refreshComboInNodes; - app.refreshComboInNodes = async function () { - const r = await Promise.all([ - refreshComboInNodes.apply(this, arguments), - ...imageTypes.map((type) => ensureImageList(type).catch(() => {})), - ]); - return r[0]; - }; - const imageHost = $el("img.pysssss-combo-image"); const positionMenu = (menu, fillWidth) => { @@ -444,151 +424,4 @@ app.registerExtension({ }); mutationObserver.observe(document.body, { childList: true, subtree: false }); }, - async beforeRegisterNodeDef(nodeType, nodeData, app) { - const isCkpt = nodeData.name === CHECKPOINT_LOADER; - const isLora = nodeData.name === LORA_LOADER; - if (ENABLE_PYSSSS_NODE_ENHANCEMENTS && (isCkpt || isLora)) { - const nodeConfig = getNodeConfig(nodeData.name) ?? getNodeConfig(nodeType); - const onAdded = nodeType.prototype.onAdded; - nodeType.prototype.onAdded = function () { - onAdded?.apply(this, arguments); - const { widget: exampleList } = ComfyWidgets["COMBO"](this, "example", [[""], {}], app); - this.widgets.find((w) => w.name === "prompt").computeSize = () => [0, -4]; - let exampleWidget; - - const get = async (route, suffix) => { - const baseType = nodeConfig?.type ?? ""; - const url = encodeRFC3986URIComponent(`${baseType}${suffix || ""}`); - return await api.fetchApi(`/pysssss/${route}/${url}`); - }; - - const getExample = async () => { - if (exampleList.value === "[none]") { - if (exampleWidget) { - exampleWidget.inputEl.remove(); - exampleWidget = null; - this.widgets.length -= 1; - } - return; - } - - const v = this.widgets[0].value; - const pos = v.lastIndexOf("."); - const name = v.substr(0, pos); - let exampleName = exampleList.value; - let viewPath = `/${name}`; - if (exampleName === "notes") { - viewPath += ".txt"; - } else { - viewPath += `/${exampleName}`; - } - const example = await (await get("view", viewPath)).text(); - if (!exampleWidget) { - exampleWidget = ComfyWidgets["STRING"](this, "prompt", ["STRING", { multiline: true }], app).widget; - exampleWidget.inputEl.readOnly = true; - exampleWidget.inputEl.style.opacity = 0.6; - } - exampleWidget.value = example; - }; - - const exampleCb = exampleList.callback; - exampleList.callback = function () { - getExample(); - return exampleCb?.apply(this, arguments) ?? exampleList.value; - }; - - const listExamples = async () => { - exampleList.disabled = true; - exampleList.options.values = ["[none]"]; - exampleList.value = "[none]"; - let examples = []; - if (this.widgets[0].value) { - try { - examples = await (await get("examples", `/${this.widgets[0].value}`)).json(); - } catch (error) {} - } - exampleList.options.values = ["[none]", ...examples]; - exampleList.value = exampleList.options.values[+!!examples.length]; - exampleList.callback(); - exampleList.disabled = !examples.length; - app.graph.setDirtyCanvas(true, true); - }; - - // Expose function to update examples - nodeType.prototype["pysssss.updateExamples"] = listExamples; - - const modelWidget = this.widgets[0]; - const modelCb = modelWidget.callback; - let prev = undefined; - modelWidget.callback = function () { - let ret = modelCb?.apply(this, arguments) ?? modelWidget.value; - if (typeof ret === "object" && "content" in ret) { - ret = ret.content; - modelWidget.value = ret; - } - let v = ret; - if (prev !== v) { - listExamples(); - prev = v; - } - return ret; - }; - setTimeout(() => { - modelWidget.callback(); - }, 30); - }; - } - - const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; - nodeType.prototype.getExtraMenuOptions = function (_, options) { - if (!ENABLE_PYSSSS_NODE_ENHANCEMENTS) { - return getExtraMenuOptions?.apply(this, arguments); - } - 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) { - const nodes = app.graph._nodes.filter((n) => n.comfyClass === LORA_LOADER || n.comfyClass === CHECKPOINT_LOADER); - if (nodes.length) { - options.unshift({ - content: "Save as Preview", - submenu: { - options: nodes.map((n) => ({ - content: n.widgets[0].value, - callback: async () => { - const url = new URL(img.src); - const targetConfig = getNodeConfig(n); - const targetType = targetConfig?.type ?? ""; - await api.fetchApi("/pysssss/save/" + encodeRFC3986URIComponent(`${targetType}/${n.widgets[0].value}`), { - method: "POST", - body: JSON.stringify({ - filename: url.searchParams.get("filename"), - subfolder: url.searchParams.get("subfolder"), - type: url.searchParams.get("type"), - }), - headers: { - "content-type": "application/json", - }, - }); - if (targetConfig?.hasImages) { - loadImageList(targetType); - } - }, - })), - }, - }); - } - } - } - return getExtraMenuOptions?.apply(this, arguments); - }; - }, }); - diff --git a/web/mxd_character_prompts.js b/web/nodes/character_prompts.js similarity index 98% rename from web/mxd_character_prompts.js rename to web/nodes/character_prompts.js index 9854711..faebda9 100644 --- a/web/mxd_character_prompts.js +++ b/web/nodes/character_prompts.js @@ -1,5 +1,5 @@ -import { app } from "../../scripts/app.js"; -import { api } from "../../scripts/api.js"; +import { app } from "../../../scripts/app.js"; +import { api } from "../../../scripts/api.js"; const TARGET_NODE_CLASS = "MXDCharacterEditDelete"; const TARGET_PROMPT_ENCODE_CLASS = "MXDCharacterPromptEncode"; diff --git a/web/checkpoint_loader_mxd.js b/web/nodes/checkpoint_loader.js similarity index 73% rename from web/checkpoint_loader_mxd.js rename to web/nodes/checkpoint_loader.js index 45736b6..94ad0b1 100644 --- a/web/checkpoint_loader_mxd.js +++ b/web/nodes/checkpoint_loader.js @@ -1,7 +1,7 @@ -import { app } from "../../scripts/app.js"; -import { mxdApi } from "./mxd_api.js"; -import { replaceWidgetWithModelRow } from "./mxd_model_row_widget.js"; -import { MxdCheckpointInfoDialog } from "./mxd_dialog_info.js"; +import { app } from "../../../scripts/app.js"; +import { mxdApi } from "../lib/mxd_api.js"; +import { replaceWidgetWithModelRow } from "../lib/mxd_model_row_widget.js"; +import { MxdCheckpointInfoDialog } from "../lib/mxd_dialog_info.js"; const NODE_TYPE = "LoadCheckpointMXD"; diff --git a/web/combine_materials_ffgo_mxd.js b/web/nodes/combine_materials_ffgo.js similarity index 98% rename from web/combine_materials_ffgo_mxd.js rename to web/nodes/combine_materials_ffgo.js index 30d6bd8..7dd6e95 100644 --- a/web/combine_materials_ffgo_mxd.js +++ b/web/nodes/combine_materials_ffgo.js @@ -1,4 +1,4 @@ -import { app } from "../../scripts/app.js"; +import { app } from "../../../scripts/app.js"; // Grows/shrinks the foreground_N input sockets as they're connected, the same // way reference/conditioning-image nodes reveal a new empty slot once the diff --git a/web/js/image_comparer.js b/web/nodes/image_comparer.js similarity index 100% rename from web/js/image_comparer.js rename to web/nodes/image_comparer.js diff --git a/web/live_preview_panel_mxd.js b/web/nodes/live_preview_panel.js similarity index 99% rename from web/live_preview_panel_mxd.js rename to web/nodes/live_preview_panel.js index ebef100..02e6c99 100644 --- a/web/live_preview_panel_mxd.js +++ b/web/nodes/live_preview_panel.js @@ -1,5 +1,5 @@ -import { app } from "../../scripts/app.js"; -import { api } from "../../scripts/api.js"; +import { app } from "../../../scripts/app.js"; +import { api } from "../../../scripts/api.js"; // A single global, floating live-preview panel -- NOT a per-node DOM widget. // ComfyUI tears down/rebuilds node widgets with the graph, so anything diff --git a/web/load_image_batch_mxd.js b/web/nodes/load_image_batch.js similarity index 97% rename from web/load_image_batch_mxd.js rename to web/nodes/load_image_batch.js index 6e3c488..090591b 100644 --- a/web/load_image_batch_mxd.js +++ b/web/nodes/load_image_batch.js @@ -1,5 +1,5 @@ -import { app } from "../../scripts/app.js"; -import { api } from "../../scripts/api.js"; +import { app } from "../../../scripts/app.js"; +import { api } from "../../../scripts/api.js"; const NODE_TYPES = new Set(["Load Image Batch MXD"]); diff --git a/web/load_single_file_mxd.js b/web/nodes/load_single_file.js similarity index 97% rename from web/load_single_file_mxd.js rename to web/nodes/load_single_file.js index 2ff2bd0..c7212da 100644 --- a/web/load_single_file_mxd.js +++ b/web/nodes/load_single_file.js @@ -1,5 +1,5 @@ -import { app } from "../../scripts/app.js"; -import { api } from "../../scripts/api.js"; +import { app } from "../../../scripts/app.js"; +import { api } from "../../../scripts/api.js"; // nodeType -> { widget: pickerWidgetName, kind: "image" | "video" } const NODE_CONFIG = new Map([ diff --git a/web/load_video_batch_mxd.js b/web/nodes/load_video_batch.js similarity index 97% rename from web/load_video_batch_mxd.js rename to web/nodes/load_video_batch.js index f4163c8..3966019 100644 --- a/web/load_video_batch_mxd.js +++ b/web/nodes/load_video_batch.js @@ -1,5 +1,5 @@ -import { app } from "../../scripts/app.js"; -import { api } from "../../scripts/api.js"; +import { app } from "../../../scripts/app.js"; +import { api } from "../../../scripts/api.js"; const NODE_TYPES = new Set(["Load Video Batch MXD"]); diff --git a/web/nodes/ltx2_power_lora_loader.js b/web/nodes/ltx2_power_lora_loader.js new file mode 100644 index 0000000..b81c29d --- /dev/null +++ b/web/nodes/ltx2_power_lora_loader.js @@ -0,0 +1,209 @@ +// "LTX2 Lora Loader MXD" — the Power Lora Loader plus per-layer strength rows +// (video / video→audio / audio / audio→video / other) for LTX2's split +// attention blocks. Shared machinery lives in power_lora_base.js; the lora +// rows themselves are the stock single-strength PowerLoraBaseWidget. +import { app } from "../../../scripts/app.js"; +import { + drawNumberWidgetPart, + drawRoundedRectangle, + isLowQuality, +} from "../lib/mxd_utils_canvas.js"; +import { MxdBaseWidget, MxdDividerWidget, MxdLabelWidget } from "../lib/mxd_utils_widgets.js"; +import { moveArrayItem } from "../lib/mxd_shared_utils.js"; +import { + MxdPowerLoraLoaderBase, + PowerLoraBaseWidget, + PowerLoraLoaderHeaderWidget, +} from "./power_lora_base.js"; + +const NODE_TYPE = "LTX2 Lora Loader MXD"; + +const LTX2_STRENGTH_ROWS = [ + { key: "video", label: "Video" }, + { key: "video_to_audio", label: "Video → Audio" }, + { key: "audio", label: "Audio" }, + { key: "audio_to_video", label: "Audio → Video" }, + { key: "other", label: "Other" }, +]; + +class MxdLtx2PowerLoraLoader extends MxdPowerLoraLoaderBase { + static title = NODE_TYPE; + static type = NODE_TYPE; + static comfyClass = NODE_TYPE; + + constructor(title = NODE_CLASS.title) { + super(title, "[LTX2 Power Lora Loader]"); + this.advancedWidgets = {}; + } + + apiJsonInputFilter(input) { + return typeof input?.["lora"] === "string" || input?.["type"] === "Ltx2StrengthWidget"; + } + + collectNonLoraWidgetValue(widgetValue) { + if (widgetValue?.type === "Ltx2StrengthWidget" && widgetValue?.key) { + this._pendingNonLoraValues[widgetValue.key] = widgetValue.value; + } + } + + applyNonLoraWidgetValues() { + for (const [key, value] of Object.entries(this._pendingNonLoraValues)) { + if (this.advancedWidgets[key]) { + this.advancedWidgets[key].value = { type: "Ltx2StrengthWidget", key, value }; + } + } + } + + addHeaderWidgets() { + let idx = 0; + moveArrayItem( + this.widgets, + this.addCustomWidget(new MxdDividerWidget({ marginTop: 4, marginBottom: 0, thickness: 0 })), + idx++, + ); + moveArrayItem( + this.widgets, + this.addCustomWidget(new MxdLabelWidget("LTX2 Layer Strengths", { size: 11 })), + idx++, + ); + + this.advancedWidgets = {}; + for (const { key, label } of LTX2_STRENGTH_ROWS) { + const widget = this.addCustomWidget(new Ltx2StrengthWidget(key, label)); + this.advancedWidgets[key] = widget; + moveArrayItem(this.widgets, widget, idx++); + } + + moveArrayItem( + this.widgets, + this.addCustomWidget(new MxdDividerWidget({ marginTop: 4, marginBottom: 4, thickness: 1 })), + idx++, + ); + moveArrayItem( + this.widgets, + this.addCustomWidget(new MxdDividerWidget({ marginTop: 4, marginBottom: 0, thickness: 0 })), + idx++, + ); + moveArrayItem(this.widgets, this.addCustomWidget(new PowerLoraLoaderHeaderWidget()), idx++); + } +} + +class Ltx2StrengthWidget extends MxdBaseWidget { + constructor(key, label) { + super("ltx2_" + key); + this.type = "custom"; + this.key = key; + this.label = label; + this.haveMouseMovedStrength = false; + this.hitAreas = { + strengthDec: { bounds: [0, 0], onClick: this.onStrengthDecDown }, + strengthVal: { bounds: [0, 0], onClick: this.onStrengthValUp }, + strengthInc: { bounds: [0, 0], onClick: this.onStrengthIncDown }, + strengthAny: { bounds: [0, 0], onMove: this.onStrengthAnyMove }, + }; + this._value = { type: "Ltx2StrengthWidget", key, value: 1 }; + } + + set value(v) { + this._value = v; + if (typeof this._value !== "object") { + this._value = { type: "Ltx2StrengthWidget", key: this.key, value: 1 }; + } + } + + get value() { + return this._value; + } + + get numValue() { + return this._value?.value ?? 1; + } + + set numValue(v) { + this._value.value = v; + } + + draw(ctx, node, w, posY, height) { + ctx.save(); + const margin = 10; + const innerMargin = margin * 0.33; + const lowQuality = isLowQuality(); + const midY = posY + height * 0.5; + const posX = margin; + + drawRoundedRectangle(ctx, { pos: [posX, posY], size: [node.size[0] - margin * 2, height] }); + + if (lowQuality) { + ctx.restore(); + return; + } + + ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR; + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + ctx.fillText(this.label, posX + innerMargin * 2, midY); + + const [leftArrow, text, rightArrow] = drawNumberWidgetPart(ctx, { + posX: node.size[0] - margin - innerMargin - innerMargin, + posY, + height, + value: this.numValue, + direction: -1, + }); + this.hitAreas.strengthDec.bounds = leftArrow; + this.hitAreas.strengthVal.bounds = text; + this.hitAreas.strengthInc.bounds = rightArrow; + this.hitAreas.strengthAny.bounds = [leftArrow[0], rightArrow[0] + rightArrow[1] - leftArrow[0]]; + + ctx.restore(); + } + + serializeValue(node, index) { + return { ...this.value }; + } + + onStrengthDecDown() { + this.stepStrength(-1); + } + + onStrengthIncDown() { + this.stepStrength(1); + } + + onStrengthAnyMove(event) { + if (event.deltaX) { + this.haveMouseMovedStrength = true; + this.numValue = (this.numValue ?? 1) + event.deltaX * 0.05; + } + } + + onStrengthValUp(event) { + if (this.haveMouseMovedStrength) return; + const canvas = app.canvas; + canvas.prompt("Value", this.numValue, (v) => (this.numValue = Number(v)), event); + } + + onMouseUp(event, pos, node) { + super.onMouseUp(event, pos, node); + this.haveMouseMovedStrength = false; + } + + stepStrength(direction) { + let step = 0.05; + let strength = (this.numValue ?? 1) + step * direction; + this.numValue = Math.round(strength * 100) / 100; + } +} + +MxdLtx2PowerLoraLoader.loraWidgetClass = PowerLoraBaseWidget; + +const NODE_CLASS = MxdLtx2PowerLoraLoader; + +app.registerExtension({ + name: "mxd.Ltx2PowerLoraLoader", + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData.name === NODE_CLASS.type) { + NODE_CLASS.setUp(nodeType, nodeData); + } + }, +}); diff --git a/web/ltx_sampler_mxd.js b/web/nodes/ltx_sampler.js similarity index 98% rename from web/ltx_sampler_mxd.js rename to web/nodes/ltx_sampler.js index 74f07a6..6f96229 100644 --- a/web/ltx_sampler_mxd.js +++ b/web/nodes/ltx_sampler.js @@ -1,4 +1,4 @@ -import { app } from "../../scripts/app.js"; +import { app } from "../../../scripts/app.js"; const LTX_SAMPLER_NODE_TYPES = new Set(["LTXKSampler_MXD", "LTXKSampler2_MXD"]); const CUSTOM_SIGMAS_MODE = "Custom Sigmas"; diff --git a/web/model_storage_settings_mxd.js b/web/nodes/model_storage_settings.js similarity index 95% rename from web/model_storage_settings_mxd.js rename to web/nodes/model_storage_settings.js index 6845019..6dcf949 100644 --- a/web/model_storage_settings_mxd.js +++ b/web/nodes/model_storage_settings.js @@ -1,4 +1,4 @@ -import { app } from "../../scripts/app.js"; +import { app } from "../../../scripts/app.js"; // Exposes the model storage auto-register root as a ComfyUI setting, as an // alternative to MAXEDOUT_MODEL_STORAGE / model_storage_config.json for diff --git a/web/ltx2_power_lora_loader_mxd.js b/web/nodes/power_lora_base.js similarity index 69% rename from web/ltx2_power_lora_loader_mxd.js rename to web/nodes/power_lora_base.js index b7e8ed9..e41032e 100644 --- a/web/ltx2_power_lora_loader_mxd.js +++ b/web/nodes/power_lora_base.js @@ -1,7 +1,20 @@ -import { app } from "../../scripts/app.js"; -import { MxdBaseServerNode } from "./mxd_base_node.js"; -import { mxdRuntime } from "./mxd_runtime.js"; -import { addConnectionLayoutSupport } from "./mxd_utils.js"; +// Shared base for the two Power Lora Loader nodes. +// +// power_lora_loader.js "Lora Loader MXD" (dual model/clip strength) +// ltx2_power_lora_loader.js "LTX2 Lora Loader MXD" (per-layer strength rows) +// +// Everything generic lives here: the node-class machinery (configure / +// serialization round-trip, API-JSON restore, slot menus, toggle-all), the +// header row, and the single-strength lora row widget. The two node files +// subclass and override only what genuinely differs. Serialization formats +// are unchanged from the pre-merge forks — widget names ("lora_N"), value +// shapes ({on, lora, strength[, strengthTwo]} and +// {type: "Ltx2StrengthWidget", key, value}) must never change, or saved +// workflows break. +import { app } from "../../../scripts/app.js"; +import { MxdBaseServerNode } from "../lib/mxd_base_node.js"; +import { mxdRuntime } from "../lib/mxd_runtime.js"; +import { addConnectionLayoutSupport } from "../lib/mxd_utils.js"; import { drawInfoIcon, drawNumberWidgetPart, @@ -9,41 +22,33 @@ import { drawTogglePart, fitString, isLowQuality, -} from "./mxd_utils_canvas.js"; +} from "../lib/mxd_utils_canvas.js"; import { MxdBaseWidget, MxdBetterButtonWidget, MxdDividerWidget, - MxdLabelWidget, -} from "./mxd_utils_widgets.js"; -import { mxdApi } from "./mxd_api.js"; -import { showLoraChooser } from "./mxd_utils_menu.js"; -import { moveArrayItem, removeArrayItem } from "./mxd_shared_utils.js"; -import { MxdLoraInfoDialog } from "./mxd_dialog_info.js"; -import { LORA_INFO_SERVICE } from "./mxd_model_info_service.js"; +} from "../lib/mxd_utils_widgets.js"; +import { mxdApi } from "../lib/mxd_api.js"; +import { showLoraChooser } from "../lib/mxd_utils_menu.js"; +import { moveArrayItem, removeArrayItem } from "../lib/mxd_shared_utils.js"; +import { MxdLoraInfoDialog } from "../lib/mxd_dialog_info.js"; +import { LORA_INFO_SERVICE } from "../lib/mxd_model_info_service.js"; -const NODE_TYPE = "LTX2 Lora Loader MXD"; +export const PROP_LABEL_SHOW_STRENGTHS = "Show Strengths"; +export const PROP_LABEL_SHOW_STRENGTHS_STATIC = `@${PROP_LABEL_SHOW_STRENGTHS}`; +export const PROP_VALUE_SHOW_STRENGTHS_SINGLE = "Single Strength"; +export const PROP_VALUE_SHOW_STRENGTHS_SEPARATE = "Separate Model & Clip"; -const LTX2_STRENGTH_ROWS = [ - { key: "video", label: "Video" }, - { key: "video_to_audio", label: "Video → Audio" }, - { key: "audio", label: "Audio" }, - { key: "audio_to_video", label: "Audio → Video" }, - { key: "other", label: "Other" }, -]; +export class MxdPowerLoraLoaderBase extends MxdBaseServerNode { + // Subclasses set this to their lora row widget class. + static loraWidgetClass = null; -class MxdLtx2PowerLoraLoader extends MxdBaseServerNode { - static title = NODE_TYPE; - static type = NODE_TYPE; - static comfyClass = NODE_TYPE; - - constructor(title = NODE_CLASS.title) { + constructor(title = new.target.title, loggerName = "[Power Lora Loader]") { super(title); this.serialize_widgets = true; - this.logger = mxdRuntime.newLogSession("[LTX2 Power Lora Loader]"); + this.logger = mxdRuntime.newLogSession(loggerName); this.loraWidgetsCounter = 0; this.widgetButtonSpacer = null; - this.advancedWidgets = {}; mxdApi.getLoras(); @@ -55,6 +60,11 @@ class MxdLtx2PowerLoraLoader extends MxdBaseServerNode { } } + // Which entries of an API-format workflow's inputs belong to this node's widgets. + apiJsonInputFilter(input) { + return typeof input?.["lora"] === "string"; + } + configureFromApiJson(fullApiJson) { if (this.id == null) { const [n, v] = this.logger.errorParts("Cannot load from API JSON without node id."); @@ -69,15 +79,14 @@ class MxdLtx2PowerLoraLoader extends MxdBaseServerNode { return; } this.configure({ - widgets_values: Object.values(nodeData.inputs).filter( - (input) => typeof input?.["lora"] === "string" || input?.["type"] === "Ltx2StrengthWidget", - ), + widgets_values: Object.values(nodeData.inputs).filter((input) => this.apiJsonInputFilter(input)), }); } configure(info) { while (this.widgets?.length) this.removeWidget(0); this.widgetButtonSpacer = null; + this._pendingNonLoraValues = {}; const hasSerializedNodeData = info?.id != null || @@ -104,23 +113,17 @@ class MxdLtx2PowerLoraLoader extends MxdBaseServerNode { const baseWidth = this.size?.[0] ?? 0; const baseHeight = this.size?.[1] ?? 0; - const savedAdvanced = {}; for (const widgetValue of info.widgets_values || []) { if (widgetValue?.lora !== undefined) { const widget = this.addNewLoraWidget(); widget.value = { ...widgetValue }; - } else if (widgetValue?.type === "Ltx2StrengthWidget" && widgetValue?.key) { - savedAdvanced[widgetValue.key] = widgetValue.value; + } else { + this.collectNonLoraWidgetValue(widgetValue); } } this.addNonLoraWidgets(); - - for (const [key, value] of Object.entries(savedAdvanced)) { - if (this.advancedWidgets[key]) { - this.advancedWidgets[key].value = { type: "Ltx2StrengthWidget", key, value }; - } - } + this.applyNonLoraWidgetValues(); this.size = this.size || [0, 0]; if (serializedSize) { @@ -135,6 +138,10 @@ class MxdLtx2PowerLoraLoader extends MxdBaseServerNode { this.setDirtyCanvas(true, true); } + // Hooks for subclasses whose serialized widgets_values contain more than lora rows. + collectNonLoraWidgetValue(widgetValue) {} + applyNonLoraWidgetValues() {} + onNodeCreated() { super.onNodeCreated?.(); if (!this.widgets?.length) { @@ -151,7 +158,8 @@ class MxdLtx2PowerLoraLoader extends MxdBaseServerNode { addNewLoraWidget(lora) { this.loraWidgetsCounter++; - const widget = this.addCustomWidget(new Ltx2PowerLoraLoaderWidget("lora_" + this.loraWidgetsCounter)); + const widgetClass = this.constructor.loraWidgetClass; + const widget = this.addCustomWidget(new widgetClass("lora_" + this.loraWidgetsCounter)); if (lora) widget.setLora(lora); if (this.widgetButtonSpacer) { moveArrayItem(this.widgets, widget, this.widgets.indexOf(this.widgetButtonSpacer)); @@ -159,37 +167,18 @@ class MxdLtx2PowerLoraLoader extends MxdBaseServerNode { return widget; } + // The widgets above the lora rows. Subclasses may override to add more. + addHeaderWidgets() { + moveArrayItem( + this.widgets, + this.addCustomWidget(new MxdDividerWidget({ marginTop: 4, marginBottom: 0, thickness: 0 })), + 0, + ); + moveArrayItem(this.widgets, this.addCustomWidget(new PowerLoraLoaderHeaderWidget()), 1); + } + addNonLoraWidgets() { - let idx = 0; - moveArrayItem( - this.widgets, - this.addCustomWidget(new MxdDividerWidget({ marginTop: 4, marginBottom: 0, thickness: 0 })), - idx++, - ); - moveArrayItem( - this.widgets, - this.addCustomWidget(new MxdLabelWidget("LTX2 Layer Strengths", { size: 11 })), - idx++, - ); - - this.advancedWidgets = {}; - for (const { key, label } of LTX2_STRENGTH_ROWS) { - const widget = this.addCustomWidget(new Ltx2StrengthWidget(key, label)); - this.advancedWidgets[key] = widget; - moveArrayItem(this.widgets, widget, idx++); - } - - moveArrayItem( - this.widgets, - this.addCustomWidget(new MxdDividerWidget({ marginTop: 4, marginBottom: 4, thickness: 1 })), - idx++, - ); - moveArrayItem( - this.widgets, - this.addCustomWidget(new MxdDividerWidget({ marginTop: 4, marginBottom: 0, thickness: 0 })), - idx++, - ); - moveArrayItem(this.widgets, this.addCustomWidget(new PowerLoraLoaderHeaderWidget()), idx++); + this.addHeaderWidgets(); this.widgetButtonSpacer = this.addCustomWidget( new MxdDividerWidget({ marginTop: 4, marginBottom: 0, thickness: 0 }), @@ -318,21 +307,21 @@ class MxdLtx2PowerLoraLoader extends MxdBaseServerNode { } static setUp(comfyClass, nodeData) { - MxdBaseServerNode.registerForOverride(comfyClass, nodeData, NODE_CLASS); + MxdBaseServerNode.registerForOverride(comfyClass, nodeData, this); } - static onRegisteredForOverride(comfyClass, ctxClass) { - addConnectionLayoutSupport(NODE_CLASS, app, [ + static onRegisteredForOverride(comfyClass, mxdClass) { + addConnectionLayoutSupport(mxdClass, app, [ ["Left", "Right"], ["Right", "Left"], ]); setTimeout(() => { - NODE_CLASS.category = comfyClass.category; + mxdClass.category = comfyClass.category; }); } } -class PowerLoraLoaderHeaderWidget extends MxdBaseWidget { +export class PowerLoraLoaderHeaderWidget extends MxdBaseWidget { constructor(name = "PowerLoraLoaderHeaderWidget") { super(name); this.value = { type: "PowerLoraLoaderHeaderWidget" }; @@ -340,11 +329,15 @@ class PowerLoraLoaderHeaderWidget extends MxdBaseWidget { this.hitAreas = { toggle: { bounds: [0, 0], onDown: this.onToggleDown }, }; + this.showModelAndClip = null; } draw(ctx, node, w, posY, height) { if (!node.hasLoraWidgets()) return; + // Nodes without the "Show Strengths" property (e.g. the LTX2 loader) + // always get the single "Strength" column. + this.showModelAndClip = node.properties[PROP_LABEL_SHOW_STRENGTHS] === PROP_VALUE_SHOW_STRENGTHS_SEPARATE; const margin = 10; const innerMargin = margin * 0.33; const lowQuality = isLowQuality(); @@ -366,7 +359,11 @@ class PowerLoraLoaderHeaderWidget extends MxdBaseWidget { let rposX = node.size[0] - margin - innerMargin - innerMargin; ctx.textAlign = "center"; - ctx.fillText("Strength", rposX - drawNumberWidgetPart.WIDTH_TOTAL / 2, midY); + ctx.fillText(this.showModelAndClip ? "Clip" : "Strength", rposX - drawNumberWidgetPart.WIDTH_TOTAL / 2, midY); + if (this.showModelAndClip) { + rposX = rposX - drawNumberWidgetPart.WIDTH_TOTAL - innerMargin * 2; + ctx.fillText("Model", rposX - drawNumberWidgetPart.WIDTH_TOTAL / 2, midY); + } } ctx.restore(); } @@ -378,13 +375,9 @@ class PowerLoraLoaderHeaderWidget extends MxdBaseWidget { } } -const DEFAULT_LORA_WIDGET_DATA = { - on: true, - lora: null, - strength: 1, -}; - -class Ltx2PowerLoraLoaderWidget extends MxdBaseWidget { +// Single-strength lora row: toggle | name | [info] | [x] | strength. +// The dual model/clip variant in power_lora_loader.js extends this. +export class PowerLoraBaseWidget extends MxdBaseWidget { constructor(name) { super(name); this.type = "custom"; @@ -401,13 +394,17 @@ class Ltx2PowerLoraLoaderWidget extends MxdBaseWidget { strengthInc: { bounds: [0, 0], onClick: this.onStrengthIncDown }, strengthAny: { bounds: [0, 0], onMove: this.onStrengthAnyMove }, }; - this._value = { ...DEFAULT_LORA_WIDGET_DATA }; + this._value = { ...this.newDefaultValue() }; + } + + newDefaultValue() { + return { on: true, lora: null, strength: 1 }; } set value(v) { this._value = v; if (typeof this._value !== "object") { - this._value = { ...DEFAULT_LORA_WIDGET_DATA }; + this._value = { ...this.newDefaultValue() }; } this.getLoraInfo(); } @@ -421,55 +418,29 @@ class Ltx2PowerLoraLoaderWidget extends MxdBaseWidget { this.getLoraInfo(); } - draw(ctx, node, w, posY, height) { - ctx.save(); - const margin = 10; - const innerMargin = margin * 0.33; - const lowQuality = isLowQuality(); - const midY = posY + height * 0.5; + // Shared drawing pieces ----------------------------------------------- + drawRowBackgroundAndToggle(ctx, node, posY, height, margin, innerMargin) { let posX = margin; - drawRoundedRectangle(ctx, { pos: [posX, posY], size: [node.size[0] - margin * 2, height] }); this.hitAreas.toggle.bounds = drawTogglePart(ctx, { posX, posY, height, value: this.value.on }); posX += this.hitAreas.toggle.bounds[1] + innerMargin; + return posX; + } - if (lowQuality) { - ctx.restore(); - return; - } - - if (!this.value.on) { - ctx.globalAlpha = app.canvas.editor_alpha * 0.4; - } - - ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR; - - const strengthValue = this.value.strength ?? 1; - - let textColor = undefined; + strengthTextColor(strengthValue) { if (this.loraInfo?.strengthMax != null && strengthValue > this.loraInfo?.strengthMax) { - textColor = "#c66"; - } else if (this.loraInfo?.strengthMin != null && strengthValue < this.loraInfo?.strengthMin) { - textColor = "#c66"; + return "#c66"; } + if (this.loraInfo?.strengthMin != null && strengthValue < this.loraInfo?.strengthMin) { + return "#c66"; + } + return undefined; + } - const [leftArrow, text, rightArrow] = drawNumberWidgetPart(ctx, { - posX: node.size[0] - margin - innerMargin - innerMargin, - posY, - height, - value: strengthValue, - direction: -1, - textColor, - }); - - this.hitAreas.strengthDec.bounds = leftArrow; - this.hitAreas.strengthVal.bounds = text; - this.hitAreas.strengthInc.bounds = rightArrow; - this.hitAreas.strengthAny.bounds = [leftArrow[0], rightArrow[0] + rightArrow[1] - leftArrow[0]]; - - let rposX = leftArrow[0] - innerMargin; - + // Draws the info icon (when a lora is set), the remove button, and the + // lora name, right-to-left starting at rposX. Returns nothing; sets bounds. + drawIconsAndName(ctx, node, posX, posY, height, rposX, innerMargin, midY) { const showInfoIcon = this.value?.lora && this.value?.lora !== "None"; const infoIconSize = height * 0.66; const infoWidth = infoIconSize + innerMargin + innerMargin; @@ -513,7 +484,46 @@ class Ltx2PowerLoraLoaderWidget extends MxdBaseWidget { ctx.fillText(fitString(ctx, loraLabel, loraWidth), posX, midY); this.hitAreas.lora.bounds = [posX, loraWidth]; - posX += loraWidth + innerMargin; + } + + draw(ctx, node, w, posY, height) { + ctx.save(); + const margin = 10; + const innerMargin = margin * 0.33; + const lowQuality = isLowQuality(); + const midY = posY + height * 0.5; + + const posX = this.drawRowBackgroundAndToggle(ctx, node, posY, height, margin, innerMargin); + + if (lowQuality) { + ctx.restore(); + return; + } + + if (!this.value.on) { + ctx.globalAlpha = app.canvas.editor_alpha * 0.4; + } + + ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR; + + const strengthValue = this.value.strength ?? 1; + + const [leftArrow, text, rightArrow] = drawNumberWidgetPart(ctx, { + posX: node.size[0] - margin - innerMargin - innerMargin, + posY, + height, + value: strengthValue, + direction: -1, + textColor: this.strengthTextColor(strengthValue), + }); + + this.hitAreas.strengthDec.bounds = leftArrow; + this.hitAreas.strengthVal.bounds = text; + this.hitAreas.strengthInc.bounds = rightArrow; + this.hitAreas.strengthAny.bounds = [leftArrow[0], rightArrow[0] + rightArrow[1] - leftArrow[0]]; + + const rposX = leftArrow[0] - innerMargin; + this.drawIconsAndName(ctx, node, posX, posY, height, rposX, innerMargin, midY); ctx.globalAlpha = app.canvas.editor_alpha; ctx.restore(); @@ -523,6 +533,8 @@ class Ltx2PowerLoraLoaderWidget extends MxdBaseWidget { return { ...this.value }; } + // Interaction ---------------------------------------------------------- + onToggleDown(event, pos, node) { this.value.on = !this.value.on; this.cancelMouseDown(); @@ -611,121 +623,3 @@ class Ltx2PowerLoraLoaderWidget extends MxdBaseWidget { return this.loraInfoPromise; } } - -class Ltx2StrengthWidget extends MxdBaseWidget { - constructor(key, label) { - super("ltx2_" + key); - this.type = "custom"; - this.key = key; - this.label = label; - this.haveMouseMovedStrength = false; - this.hitAreas = { - strengthDec: { bounds: [0, 0], onClick: this.onStrengthDecDown }, - strengthVal: { bounds: [0, 0], onClick: this.onStrengthValUp }, - strengthInc: { bounds: [0, 0], onClick: this.onStrengthIncDown }, - strengthAny: { bounds: [0, 0], onMove: this.onStrengthAnyMove }, - }; - this._value = { type: "Ltx2StrengthWidget", key, value: 1 }; - } - - set value(v) { - this._value = v; - if (typeof this._value !== "object") { - this._value = { type: "Ltx2StrengthWidget", key: this.key, value: 1 }; - } - } - - get value() { - return this._value; - } - - get numValue() { - return this._value?.value ?? 1; - } - - set numValue(v) { - this._value.value = v; - } - - draw(ctx, node, w, posY, height) { - ctx.save(); - const margin = 10; - const innerMargin = margin * 0.33; - const lowQuality = isLowQuality(); - const midY = posY + height * 0.5; - const posX = margin; - - drawRoundedRectangle(ctx, { pos: [posX, posY], size: [node.size[0] - margin * 2, height] }); - - if (lowQuality) { - ctx.restore(); - return; - } - - ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR; - ctx.textAlign = "left"; - ctx.textBaseline = "middle"; - ctx.fillText(this.label, posX + innerMargin * 2, midY); - - const [leftArrow, text, rightArrow] = drawNumberWidgetPart(ctx, { - posX: node.size[0] - margin - innerMargin - innerMargin, - posY, - height, - value: this.numValue, - direction: -1, - }); - this.hitAreas.strengthDec.bounds = leftArrow; - this.hitAreas.strengthVal.bounds = text; - this.hitAreas.strengthInc.bounds = rightArrow; - this.hitAreas.strengthAny.bounds = [leftArrow[0], rightArrow[0] + rightArrow[1] - leftArrow[0]]; - - ctx.restore(); - } - - serializeValue(node, index) { - return { ...this.value }; - } - - onStrengthDecDown() { - this.stepStrength(-1); - } - - onStrengthIncDown() { - this.stepStrength(1); - } - - onStrengthAnyMove(event) { - if (event.deltaX) { - this.haveMouseMovedStrength = true; - this.numValue = (this.numValue ?? 1) + event.deltaX * 0.05; - } - } - - onStrengthValUp(event) { - if (this.haveMouseMovedStrength) return; - const canvas = app.canvas; - canvas.prompt("Value", this.numValue, (v) => (this.numValue = Number(v)), event); - } - - onMouseUp(event, pos, node) { - super.onMouseUp(event, pos, node); - this.haveMouseMovedStrength = false; - } - - stepStrength(direction) { - let step = 0.05; - let strength = (this.numValue ?? 1) + step * direction; - this.numValue = Math.round(strength * 100) / 100; - } -} - -const NODE_CLASS = MxdLtx2PowerLoraLoader; - -app.registerExtension({ - name: "mxd.Ltx2PowerLoraLoader", - async beforeRegisterNodeDef(nodeType, nodeData) { - if (nodeData.name === NODE_CLASS.type) { - NODE_CLASS.setUp(nodeType, nodeData); - } - }, -}); diff --git a/web/nodes/power_lora_loader.js b/web/nodes/power_lora_loader.js new file mode 100644 index 0000000..b663209 --- /dev/null +++ b/web/nodes/power_lora_loader.js @@ -0,0 +1,228 @@ +// "Lora Loader MXD" — multi-lora stack with optional separate model/clip +// strengths (the "Show Strengths" node property). Shared machinery lives in +// power_lora_base.js; this file only adds the dual-strength row behavior. +import { app } from "../../../scripts/app.js"; +import { drawNumberWidgetPart, isLowQuality } from "../lib/mxd_utils_canvas.js"; +import { + MxdPowerLoraLoaderBase, + PowerLoraBaseWidget, + PROP_LABEL_SHOW_STRENGTHS, + PROP_LABEL_SHOW_STRENGTHS_STATIC, + PROP_VALUE_SHOW_STRENGTHS_SINGLE, + PROP_VALUE_SHOW_STRENGTHS_SEPARATE, +} from "./power_lora_base.js"; + +const NODE_TYPE = "Lora Loader MXD"; + +class MxdPowerLoraLoader extends MxdPowerLoraLoaderBase { + static title = NODE_TYPE; + static type = NODE_TYPE; + static comfyClass = NODE_TYPE; + + static [PROP_LABEL_SHOW_STRENGTHS_STATIC] = { + type: "combo", + values: [PROP_VALUE_SHOW_STRENGTHS_SINGLE, PROP_VALUE_SHOW_STRENGTHS_SEPARATE], + }; + + constructor(title = NODE_CLASS.title) { + super(title, "[Power Lora Loader]"); + this.properties[PROP_LABEL_SHOW_STRENGTHS] = PROP_VALUE_SHOW_STRENGTHS_SINGLE; + } +} + +class PowerLoraLoaderWidget extends PowerLoraBaseWidget { + constructor(name) { + super(name); + this.showModelAndClip = null; + Object.assign(this.hitAreas, { + strengthTwoDec: { bounds: [0, 0], onClick: this.onStrengthTwoDecDown }, + strengthTwoVal: { bounds: [0, 0], onClick: this.onStrengthTwoValUp }, + strengthTwoInc: { bounds: [0, 0], onClick: this.onStrengthTwoIncDown }, + strengthTwoAny: { bounds: [0, 0], onMove: this.onStrengthTwoAnyMove }, + }); + this._value = { ...this.newDefaultValue() }; + } + + newDefaultValue() { + return { on: true, lora: null, strength: 1, strengthTwo: null }; + } + + set value(v) { + this._value = v; + if (typeof this._value !== "object") { + this._value = { ...this.newDefaultValue() }; + if (this.showModelAndClip) { + this._value.strengthTwo = this._value.strength; + } + } + this.getLoraInfo(); + } + + get value() { + return this._value; + } + + draw(ctx, node, w, posY, height) { + let currentShowModelAndClip = node.properties[PROP_LABEL_SHOW_STRENGTHS] === PROP_VALUE_SHOW_STRENGTHS_SEPARATE; + if (this.showModelAndClip !== currentShowModelAndClip) { + let oldShowModelAndClip = this.showModelAndClip; + this.showModelAndClip = currentShowModelAndClip; + if (this.showModelAndClip) { + if (oldShowModelAndClip != null) { + this.value.strengthTwo = this.value.strength ?? 1; + } + } else { + this.value.strengthTwo = null; + this.hitAreas.strengthTwoDec.bounds = [0, -1]; + this.hitAreas.strengthTwoVal.bounds = [0, -1]; + this.hitAreas.strengthTwoInc.bounds = [0, -1]; + this.hitAreas.strengthTwoAny.bounds = [0, -1]; + } + } + + ctx.save(); + const margin = 10; + const innerMargin = margin * 0.33; + const lowQuality = isLowQuality(); + const midY = posY + height * 0.5; + + const posX = this.drawRowBackgroundAndToggle(ctx, node, posY, height, margin, innerMargin); + + if (lowQuality) { + ctx.restore(); + return; + } + + if (!this.value.on) { + ctx.globalAlpha = app.canvas.editor_alpha * 0.4; + } + + ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR; + + // Rightmost number: clip strength when split, otherwise the single strength. + const strengthValue = this.showModelAndClip ? (this.value.strengthTwo ?? 1) : (this.value.strength ?? 1); + + const [leftArrow, text, rightArrow] = drawNumberWidgetPart(ctx, { + posX: node.size[0] - margin - innerMargin - innerMargin, + posY, + height, + value: strengthValue, + direction: -1, + textColor: this.strengthTextColor(strengthValue), + }); + + this.hitAreas.strengthDec.bounds = leftArrow; + this.hitAreas.strengthVal.bounds = text; + this.hitAreas.strengthInc.bounds = rightArrow; + this.hitAreas.strengthAny.bounds = [leftArrow[0], rightArrow[0] + rightArrow[1] - leftArrow[0]]; + + let rposX = leftArrow[0] - innerMargin; + + if (this.showModelAndClip) { + rposX -= innerMargin; + // The rightmost number becomes clip (strengthTwo); the model strength + // gets its own number to the left. + this.hitAreas.strengthTwoDec.bounds = this.hitAreas.strengthDec.bounds; + this.hitAreas.strengthTwoVal.bounds = this.hitAreas.strengthVal.bounds; + this.hitAreas.strengthTwoInc.bounds = this.hitAreas.strengthInc.bounds; + this.hitAreas.strengthTwoAny.bounds = this.hitAreas.strengthAny.bounds; + + const [leftArrow2, text2, rightArrow2] = drawNumberWidgetPart(ctx, { + posX: rposX, + posY, + height, + value: this.value.strength ?? 1, + direction: -1, + textColor: this.strengthTextColor(this.value.strength), + }); + this.hitAreas.strengthDec.bounds = leftArrow2; + this.hitAreas.strengthVal.bounds = text2; + this.hitAreas.strengthInc.bounds = rightArrow2; + this.hitAreas.strengthAny.bounds = [leftArrow2[0], rightArrow2[0] + rightArrow2[1] - leftArrow2[0]]; + rposX = leftArrow2[0] - innerMargin; + } + + this.drawIconsAndName(ctx, node, posX, posY, height, rposX, innerMargin, midY); + + ctx.globalAlpha = app.canvas.editor_alpha; + ctx.restore(); + } + + serializeValue(node, index) { + const v = { ...this.value }; + if (!this.showModelAndClip) { + delete v.strengthTwo; + } else { + this.value.strengthTwo = this.value.strengthTwo ?? 1; + v.strengthTwo = this.value.strengthTwo; + } + return v; + } + + onStrengthDecDown(event, pos, node) { + this.stepStrength(-1, false); + } + + onStrengthIncDown(event, pos, node) { + this.stepStrength(1, false); + } + + onStrengthTwoDecDown(event, pos, node) { + this.stepStrength(-1, true); + } + + onStrengthTwoIncDown(event, pos, node) { + this.stepStrength(1, true); + } + + onStrengthAnyMove(event, pos, node) { + this.doOnStrengthAnyMove(event, false); + } + + onStrengthTwoAnyMove(event, pos, node) { + this.doOnStrengthAnyMove(event, true); + } + + doOnStrengthAnyMove(event, isTwo = false) { + if (event.deltaX) { + let prop = isTwo ? "strengthTwo" : "strength"; + this.haveMouseMovedStrength = true; + this.value[prop] = (this.value[prop] ?? 1) + event.deltaX * 0.05; + } + } + + onStrengthValUp(event, pos, node) { + this.doOnStrengthValUp(event, false); + } + + onStrengthTwoValUp(event, pos, node) { + this.doOnStrengthValUp(event, true); + } + + doOnStrengthValUp(event, isTwo = false) { + if (this.haveMouseMovedStrength) return; + let prop = isTwo ? "strengthTwo" : "strength"; + const canvas = app.canvas; + canvas.prompt("Value", this.value[prop], (v) => (this.value[prop] = Number(v)), event); + } + + stepStrength(direction, isTwo = false) { + let step = 0.05; + let prop = isTwo ? "strengthTwo" : "strength"; + let strength = (this.value[prop] ?? 1) + step * direction; + this.value[prop] = Math.round(strength * 100) / 100; + } +} + +MxdPowerLoraLoader.loraWidgetClass = PowerLoraLoaderWidget; + +const NODE_CLASS = MxdPowerLoraLoader; + +app.registerExtension({ + name: "mxd.PowerLoraLoader", + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData.name === NODE_CLASS.type) { + NODE_CLASS.setUp(nodeType, nodeData); + } + }, +}); diff --git a/web/run_folder_mxd.js b/web/nodes/run_folder.js similarity index 97% rename from web/run_folder_mxd.js rename to web/nodes/run_folder.js index a2c3ff0..a762e55 100644 --- a/web/run_folder_mxd.js +++ b/web/nodes/run_folder.js @@ -1,5 +1,5 @@ -import { app } from "../../scripts/app.js"; -import { api } from "../../scripts/api.js"; +import { app } from "../../../scripts/app.js"; +import { api } from "../../../scripts/api.js"; // Node types that get the "run_folder" toggle (declared server-side), mapped // to the name of their single-file picker widget. diff --git a/web/video_preview_mxd.js b/web/nodes/video_preview.js similarity index 97% rename from web/video_preview_mxd.js rename to web/nodes/video_preview.js index 27c30e8..29392a2 100644 --- a/web/video_preview_mxd.js +++ b/web/nodes/video_preview.js @@ -1,4 +1,4 @@ -import { app } from "../../scripts/app.js"; +import { app } from "../../../scripts/app.js"; // Adds a global "preview any sampler as it renders" toggle, backed by // video_preview_mxd.py's general get_previewer hook. Writes its own diff --git a/web/wan22_video_prep_mxd.js b/web/nodes/wan22_video_prep.js similarity index 98% rename from web/wan22_video_prep_mxd.js rename to web/nodes/wan22_video_prep.js index e6a5ccf..6cc8942 100644 --- a/web/wan22_video_prep_mxd.js +++ b/web/nodes/wan22_video_prep.js @@ -1,4 +1,4 @@ -import { app } from "../../scripts/app.js"; +import { app } from "../../../scripts/app.js"; const NODE_TYPES = new Set(["WAN22_I2V_Video_Prep_MXD"]); diff --git a/web/power_lora_loader_mxd.js b/web/power_lora_loader_mxd.js deleted file mode 100644 index 0d1863b..0000000 --- a/web/power_lora_loader_mxd.js +++ /dev/null @@ -1,697 +0,0 @@ -var _a; - -import { app } from "../../scripts/app.js"; -import { MxdBaseServerNode } from "./mxd_base_node.js"; -import { mxdRuntime } from "./mxd_runtime.js"; -import { addConnectionLayoutSupport } from "./mxd_utils.js"; -import { - drawInfoIcon, - drawNumberWidgetPart, - drawRoundedRectangle, - drawTogglePart, - fitString, - isLowQuality, -} from "./mxd_utils_canvas.js"; -import { - MxdBaseWidget, - MxdBetterButtonWidget, - MxdDividerWidget, -} from "./mxd_utils_widgets.js"; -import { mxdApi } from "./mxd_api.js"; -import { showLoraChooser } from "./mxd_utils_menu.js"; -import { moveArrayItem, removeArrayItem } from "./mxd_shared_utils.js"; -import { MxdLoraInfoDialog } from "./mxd_dialog_info.js"; -import { LORA_INFO_SERVICE } from "./mxd_model_info_service.js"; - -const NODE_TYPE = "Lora Loader MXD"; -const PROP_LABEL_SHOW_STRENGTHS = "Show Strengths"; -const PROP_LABEL_SHOW_STRENGTHS_STATIC = `@${PROP_LABEL_SHOW_STRENGTHS}`; -const PROP_VALUE_SHOW_STRENGTHS_SINGLE = "Single Strength"; -const PROP_VALUE_SHOW_STRENGTHS_SEPARATE = "Separate Model & Clip"; - -class MxdPowerLoraLoader extends MxdBaseServerNode { - static title = NODE_TYPE; - static type = NODE_TYPE; - static comfyClass = NODE_TYPE; - - static [PROP_LABEL_SHOW_STRENGTHS_STATIC] = { - type: "combo", - values: [PROP_VALUE_SHOW_STRENGTHS_SINGLE, PROP_VALUE_SHOW_STRENGTHS_SEPARATE], - }; - - constructor(title = NODE_CLASS.title) { - super(title); - this.serialize_widgets = true; - this.logger = mxdRuntime.newLogSession("[Power Lora Loader]"); - this.loraWidgetsCounter = 0; - this.widgetButtonSpacer = null; - this.properties[PROP_LABEL_SHOW_STRENGTHS] = PROP_VALUE_SHOW_STRENGTHS_SINGLE; - - mxdApi.getLoras(); - - if (mxdRuntime.loadingApiJson) { - const fullApiJson = mxdRuntime.loadingApiJson; - setTimeout(() => { - this.configureFromApiJson(fullApiJson); - }, 16); - } - } - - configureFromApiJson(fullApiJson) { - if (this.id == null) { - const [n, v] = this.logger.errorParts("Cannot load from API JSON without node id."); - console[n]?.(...v); - return; - } - const nodeData = - fullApiJson[this.id] || fullApiJson[String(this.id)] || fullApiJson[Number(this.id)]; - if (nodeData == null) { - const [n, v] = this.logger.errorParts(`No node found in API JSON for node id ${this.id}.`); - console[n]?.(...v); - return; - } - this.configure({ - widgets_values: Object.values(nodeData.inputs).filter((input) => typeof input?.["lora"] === "string"), - }); - } - - configure(info) { - while (this.widgets?.length) this.removeWidget(0); - this.widgetButtonSpacer = null; - - const hasSerializedNodeData = - info?.id != null || - [ - "pos", - "size", - "flags", - "mode", - "order", - "properties", - "color", - "bgcolor", - "title", - "inputs", - "outputs", - "type", - ].some((key) => info?.[key] !== undefined); - const serializedSize = Array.isArray(info?.size) ? [...info.size] : null; - - if (hasSerializedNodeData) { - super.configure(info); - } - - const baseWidth = this.size?.[0] ?? 0; - const baseHeight = this.size?.[1] ?? 0; - - for (const widgetValue of info.widgets_values || []) { - if (widgetValue?.lora !== undefined) { - const widget = this.addNewLoraWidget(); - widget.value = { ...widgetValue }; - } - } - - this.addNonLoraWidgets(); - - this.size = this.size || [0, 0]; - if (serializedSize) { - this.size[0] = serializedSize[0]; - this.size[1] = serializedSize[1]; - } else { - const computed = this.computeSize(); - this.size[0] = Math.max(baseWidth, computed[0]); - this.size[1] = Math.max(baseHeight, computed[1]); - } - - this.setDirtyCanvas(true, true); - } - - onNodeCreated() { - super.onNodeCreated?.(); - if (!this.widgets?.length) { - this.addNonLoraWidgets(); - } - if (!this.configuring && !mxdRuntime.canvasCurrentlyPastingFromClipboard) { - const computed = this.computeSize(); - this.size = this.size || [0, 0]; - this.size[0] = Math.max(this.size[0], computed[0]); - this.size[1] = Math.max(this.size[1], computed[1]); - } - this.setDirtyCanvas(true, true); - } - - addNewLoraWidget(lora) { - this.loraWidgetsCounter++; - const widget = this.addCustomWidget(new PowerLoraLoaderWidget("lora_" + this.loraWidgetsCounter)); - if (lora) widget.setLora(lora); - if (this.widgetButtonSpacer) { - moveArrayItem(this.widgets, widget, this.widgets.indexOf(this.widgetButtonSpacer)); - } - return widget; - } - - addNonLoraWidgets() { - moveArrayItem( - this.widgets, - this.addCustomWidget(new MxdDividerWidget({ marginTop: 4, marginBottom: 0, thickness: 0 })), - 0, - ); - moveArrayItem(this.widgets, this.addCustomWidget(new PowerLoraLoaderHeaderWidget()), 1); - - this.widgetButtonSpacer = this.addCustomWidget( - new MxdDividerWidget({ marginTop: 4, marginBottom: 0, thickness: 0 }), - ); - - this.addCustomWidget( - new MxdBetterButtonWidget("+ Add Lora", (event, pos, node) => { - mxdApi.getLoras().then((lorasDetails) => { - const loras = lorasDetails.map((l) => l.file); - showLoraChooser( - event, - (value) => { - if (typeof value === "string" && value !== "NONE") { - this.addNewLoraWidget(value); - const computed = this.computeSize(); - this.size[1] = Math.max(this.size?.[1] ?? 15, computed[1]); - this.setDirtyCanvas(true, true); - } - }, - null, - [...loras], - ); - }); - return true; - }), - ); - } - - getSlotInPosition(canvasX, canvasY) { - const slot = super.getSlotInPosition(canvasX, canvasY); - if (!slot) { - let lastWidget = null; - for (const widget of this.widgets) { - if (!widget.last_y) return; - if (canvasY > this.pos[1] + widget.last_y) { - lastWidget = widget; - continue; - } - break; - } - if (lastWidget?.name?.startsWith("lora_")) { - return { widget: lastWidget, output: { type: "LORA WIDGET" } }; - } - } - return slot; - } - - getSlotMenuOptions(slot) { - if (slot?.widget?.name?.startsWith("lora_")) { - const widget = slot.widget; - const index = this.widgets.indexOf(widget); - const canMoveUp = !!this.widgets[index - 1]?.name?.startsWith("lora_"); - const canMoveDown = !!this.widgets[index + 1]?.name?.startsWith("lora_"); - const menuItems = [ - { - content: `Show Info`, - callback: () => widget.showLoraInfoDialog(), - }, - null, - { - content: `${widget.value.on ? "Disable" : "Enable"}`, - callback: () => { - widget.value.on = !widget.value.on; - }, - }, - { - content: `Move Up`, - disabled: !canMoveUp, - callback: () => { - moveArrayItem(this.widgets, widget, index - 1); - }, - }, - { - content: `Move Down`, - disabled: !canMoveDown, - callback: () => { - moveArrayItem(this.widgets, widget, index + 1); - }, - }, - { - content: `Remove`, - callback: () => { - removeArrayItem(this.widgets, widget); - }, - }, - ]; - new LiteGraph.ContextMenu(menuItems, { - title: "LORA WIDGET", - event: mxdRuntime.lastCanvasMouseEvent, - }); - return undefined; - } - return this.defaultGetSlotMenuOptions(slot); - } - - refreshComboInNode(defs) { - mxdApi.getLoras(true); - } - - hasLoraWidgets() { - return !!this.widgets?.find((w) => w.name?.startsWith("lora_")); - } - - allLorasState() { - let allOn = true; - let allOff = true; - for (const widget of this.widgets) { - if (widget.name?.startsWith("lora_")) { - const on = widget.value?.on; - allOn = allOn && on === true; - allOff = allOff && on === false; - if (!allOn && !allOff) return null; - } - } - return allOn && this.widgets?.length ? true : false; - } - - toggleAllLoras() { - const allOn = this.allLorasState(); - const toggledTo = !allOn; - for (const widget of this.widgets) { - if (widget.name?.startsWith("lora_") && widget.value?.on != null) { - widget.value.on = toggledTo; - } - } - } - - static setUp(comfyClass, nodeData) { - MxdBaseServerNode.registerForOverride(comfyClass, nodeData, NODE_CLASS); - } - - static onRegisteredForOverride(comfyClass, ctxClass) { - addConnectionLayoutSupport(NODE_CLASS, app, [ - ["Left", "Right"], - ["Right", "Left"], - ]); - setTimeout(() => { - NODE_CLASS.category = comfyClass.category; - }); - } -} -_a = PROP_LABEL_SHOW_STRENGTHS_STATIC; -MxdPowerLoraLoader[_a] = { - type: "combo", - values: [PROP_VALUE_SHOW_STRENGTHS_SINGLE, PROP_VALUE_SHOW_STRENGTHS_SEPARATE], -}; - -class PowerLoraLoaderHeaderWidget extends MxdBaseWidget { - constructor(name = "PowerLoraLoaderHeaderWidget") { - super(name); - this.value = { type: "PowerLoraLoaderHeaderWidget" }; - this.type = "custom"; - this.hitAreas = { - toggle: { bounds: [0, 0], onDown: this.onToggleDown }, - }; - this.showModelAndClip = null; - } - - draw(ctx, node, w, posY, height) { - if (!node.hasLoraWidgets()) return; - - this.showModelAndClip = node.properties[PROP_LABEL_SHOW_STRENGTHS] === PROP_VALUE_SHOW_STRENGTHS_SEPARATE; - const margin = 10; - const innerMargin = margin * 0.33; - const lowQuality = isLowQuality(); - const allLoraState = node.allLorasState(); - - posY += 2; - const midY = posY + height * 0.5; - let posX = 10; - ctx.save(); - this.hitAreas.toggle.bounds = drawTogglePart(ctx, { posX, posY, height, value: allLoraState }); - - if (!lowQuality) { - posX += this.hitAreas.toggle.bounds[1] + innerMargin; - ctx.globalAlpha = app.canvas.editor_alpha * 0.55; - ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR; - ctx.textAlign = "left"; - ctx.textBaseline = "middle"; - ctx.fillText("Toggle All", posX, midY); - - let rposX = node.size[0] - margin - innerMargin - innerMargin; - ctx.textAlign = "center"; - ctx.fillText(this.showModelAndClip ? "Clip" : "Strength", rposX - drawNumberWidgetPart.WIDTH_TOTAL / 2, midY); - if (this.showModelAndClip) { - rposX = rposX - drawNumberWidgetPart.WIDTH_TOTAL - innerMargin * 2; - ctx.fillText("Model", rposX - drawNumberWidgetPart.WIDTH_TOTAL / 2, midY); - } - } - ctx.restore(); - } - - onToggleDown(event, pos, node) { - node.toggleAllLoras(); - this.cancelMouseDown(); - return true; - } -} - -const DEFAULT_LORA_WIDGET_DATA = { - on: true, - lora: null, - strength: 1, - strengthTwo: null, -}; - -class PowerLoraLoaderWidget extends MxdBaseWidget { - constructor(name) { - super(name); - this.type = "custom"; - this.haveMouseMovedStrength = false; - this.loraInfoPromise = null; - this.loraInfo = null; - this.showModelAndClip = null; - this.hitAreas = { - toggle: { bounds: [0, 0], onDown: this.onToggleDown }, - lora: { bounds: [0, 0], onClick: this.onLoraClick }, - info: { bounds: [0, 0], onDown: this.onInfoDown }, - remove: { bounds: [0, 0], onDown: this.onRemoveDown }, - strengthDec: { bounds: [0, 0], onClick: this.onStrengthDecDown }, - strengthVal: { bounds: [0, 0], onClick: this.onStrengthValUp }, - strengthInc: { bounds: [0, 0], onClick: this.onStrengthIncDown }, - strengthAny: { bounds: [0, 0], onMove: this.onStrengthAnyMove }, - strengthTwoDec: { bounds: [0, 0], onClick: this.onStrengthTwoDecDown }, - strengthTwoVal: { bounds: [0, 0], onClick: this.onStrengthTwoValUp }, - strengthTwoInc: { bounds: [0, 0], onClick: this.onStrengthTwoIncDown }, - strengthTwoAny: { bounds: [0, 0], onMove: this.onStrengthTwoAnyMove }, - }; - this._value = { - on: true, - lora: null, - strength: 1, - strengthTwo: null, - }; - } - - set value(v) { - this._value = v; - if (typeof this._value !== "object") { - this._value = { ...DEFAULT_LORA_WIDGET_DATA }; - if (this.showModelAndClip) { - this._value.strengthTwo = this._value.strength; - } - } - this.getLoraInfo(); - } - - get value() { - return this._value; - } - - setLora(lora) { - this._value.lora = lora; - this.getLoraInfo(); - } - - draw(ctx, node, w, posY, height) { - let currentShowModelAndClip = node.properties[PROP_LABEL_SHOW_STRENGTHS] === PROP_VALUE_SHOW_STRENGTHS_SEPARATE; - if (this.showModelAndClip !== currentShowModelAndClip) { - let oldShowModelAndClip = this.showModelAndClip; - this.showModelAndClip = currentShowModelAndClip; - if (this.showModelAndClip) { - if (oldShowModelAndClip != null) { - this.value.strengthTwo = this.value.strength ?? 1; - } - } else { - this.value.strengthTwo = null; - this.hitAreas.strengthTwoDec.bounds = [0, -1]; - this.hitAreas.strengthTwoVal.bounds = [0, -1]; - this.hitAreas.strengthTwoInc.bounds = [0, -1]; - this.hitAreas.strengthTwoAny.bounds = [0, -1]; - } - } - - ctx.save(); - const margin = 10; - const innerMargin = margin * 0.33; - const lowQuality = isLowQuality(); - const midY = posY + height * 0.5; - - let posX = margin; - - drawRoundedRectangle(ctx, { pos: [posX, posY], size: [node.size[0] - margin * 2, height] }); - this.hitAreas.toggle.bounds = drawTogglePart(ctx, { posX, posY, height, value: this.value.on }); - posX += this.hitAreas.toggle.bounds[1] + innerMargin; - - if (lowQuality) { - ctx.restore(); - return; - } - - if (!this.value.on) { - ctx.globalAlpha = app.canvas.editor_alpha * 0.4; - } - - ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR; - - let rposX = node.size[0] - margin - innerMargin - innerMargin; - - const strengthValue = this.showModelAndClip ? (this.value.strengthTwo ?? 1) : (this.value.strength ?? 1); - - let textColor = undefined; - if (this.loraInfo?.strengthMax != null && strengthValue > this.loraInfo?.strengthMax) { - textColor = "#c66"; - } else if (this.loraInfo?.strengthMin != null && strengthValue < this.loraInfo?.strengthMin) { - textColor = "#c66"; - } - - const [leftArrow, text, rightArrow] = drawNumberWidgetPart(ctx, { - posX: node.size[0] - margin - innerMargin - innerMargin, - posY, - height, - value: strengthValue, - direction: -1, - textColor, - }); - - this.hitAreas.strengthDec.bounds = leftArrow; - this.hitAreas.strengthVal.bounds = text; - this.hitAreas.strengthInc.bounds = rightArrow; - this.hitAreas.strengthAny.bounds = [leftArrow[0], rightArrow[0] + rightArrow[1] - leftArrow[0]]; - - rposX = leftArrow[0] - innerMargin; - - if (this.showModelAndClip) { - rposX -= innerMargin; - this.hitAreas.strengthTwoDec.bounds = this.hitAreas.strengthDec.bounds; - this.hitAreas.strengthTwoVal.bounds = this.hitAreas.strengthVal.bounds; - this.hitAreas.strengthTwoInc.bounds = this.hitAreas.strengthInc.bounds; - this.hitAreas.strengthTwoAny.bounds = this.hitAreas.strengthAny.bounds; - - let textColor = undefined; - if (this.loraInfo?.strengthMax != null && this.value.strength > this.loraInfo?.strengthMax) { - textColor = "#c66"; - } else if (this.loraInfo?.strengthMin != null && this.value.strength < this.loraInfo?.strengthMin) { - textColor = "#c66"; - } - const [leftArrow2, text2, rightArrow2] = drawNumberWidgetPart(ctx, { - posX: rposX, - posY, - height, - value: this.value.strength ?? 1, - direction: -1, - textColor, - }); - this.hitAreas.strengthDec.bounds = leftArrow2; - this.hitAreas.strengthVal.bounds = text2; - this.hitAreas.strengthInc.bounds = rightArrow2; - this.hitAreas.strengthAny.bounds = [leftArrow2[0], rightArrow2[0] + rightArrow2[1] - leftArrow2[0]]; - rposX = leftArrow2[0] - innerMargin; - } - - const showInfoIcon = this.value?.lora && this.value?.lora !== "None"; - const infoIconSize = height * 0.66; - const infoWidth = infoIconSize + innerMargin + innerMargin; - if (showInfoIcon) { - rposX -= innerMargin; - drawInfoIcon(ctx, rposX - infoIconSize, posY + (height - infoIconSize) / 2, infoIconSize); - this.hitAreas.info.bounds = [rposX - infoIconSize, infoWidth]; - rposX = rposX - infoIconSize - innerMargin; - } else { - this.hitAreas.info.bounds = [0, -1]; - } - - const actionIconSize = infoIconSize; - const actionWidth = actionIconSize + innerMargin; - const drawAction = (key, label, color = LiteGraph.WIDGET_TEXT_COLOR) => { - rposX -= actionWidth; - const x = rposX; - const y = posY + (height - actionIconSize) / 2; - drawRoundedRectangle(ctx, { - pos: [x, y], - size: [actionIconSize, actionIconSize], - borderRadius: actionIconSize * 0.15, - colorBackground: "rgba(0,0,0,0.22)", - colorStroke: "rgba(255,255,255,0.12)", - }); - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillStyle = color; - ctx.fillText(label, x + actionIconSize * 0.5, y + actionIconSize * 0.52); - this.hitAreas[key].bounds = [x, actionIconSize]; - rposX -= innerMargin * 0.25; - }; - - drawAction("remove", "x", "#d88"); - - const loraWidth = rposX - posX; - ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR; - ctx.textAlign = "left"; - ctx.textBaseline = "middle"; - const loraLabel = String(this.value?.lora || "None"); - ctx.fillText(fitString(ctx, loraLabel, loraWidth), posX, midY); - - this.hitAreas.lora.bounds = [posX, loraWidth]; - posX += loraWidth + innerMargin; - - ctx.globalAlpha = app.canvas.editor_alpha; - ctx.restore(); - } - - serializeValue(node, index) { - const v = { ...this.value }; - if (!this.showModelAndClip) { - delete v.strengthTwo; - } else { - this.value.strengthTwo = this.value.strengthTwo ?? 1; - v.strengthTwo = this.value.strengthTwo; - } - return v; - } - - onToggleDown(event, pos, node) { - this.value.on = !this.value.on; - this.cancelMouseDown(); - return true; - } - - onInfoDown(event, pos, node) { - this.showLoraInfoDialog(); - this.cancelMouseDown(); - return true; - } - - onRemoveDown(event, pos, node) { - removeArrayItem(node.widgets, this); - node.setDirtyCanvas(true, true); - this.cancelMouseDown(); - return true; - } - - onLoraClick(event, pos, node) { - showLoraChooser(event, (value) => { - if (typeof value === "string") { - this.value.lora = value; - this.loraInfo = null; - this.getLoraInfo(); - } - node.setDirtyCanvas(true, true); - }); - this.cancelMouseDown(); - } - - onStrengthDecDown(event, pos, node) { - this.stepStrength(-1, false); - } - - onStrengthIncDown(event, pos, node) { - this.stepStrength(1, false); - } - - onStrengthTwoDecDown(event, pos, node) { - this.stepStrength(-1, true); - } - - onStrengthTwoIncDown(event, pos, node) { - this.stepStrength(1, true); - } - - onStrengthAnyMove(event, pos, node) { - this.doOnStrengthAnyMove(event, false); - } - - onStrengthTwoAnyMove(event, pos, node) { - this.doOnStrengthAnyMove(event, true); - } - - doOnStrengthAnyMove(event, isTwo = false) { - if (event.deltaX) { - let prop = isTwo ? "strengthTwo" : "strength"; - this.haveMouseMovedStrength = true; - this.value[prop] = (this.value[prop] ?? 1) + event.deltaX * 0.05; - } - } - - onStrengthValUp(event, pos, node) { - this.doOnStrengthValUp(event, false); - } - - onStrengthTwoValUp(event, pos, node) { - this.doOnStrengthValUp(event, true); - } - - doOnStrengthValUp(event, isTwo = false) { - if (this.haveMouseMovedStrength) return; - let prop = isTwo ? "strengthTwo" : "strength"; - const canvas = app.canvas; - canvas.prompt("Value", this.value[prop], (v) => (this.value[prop] = Number(v)), event); - } - - onMouseUp(event, pos, node) { - super.onMouseUp(event, pos, node); - this.haveMouseMovedStrength = false; - } - - showLoraInfoDialog() { - if (!this.value.lora || this.value.lora === "None") { - return; - } - const infoDialog = new MxdLoraInfoDialog(this.value.lora).show(); - infoDialog.addEventListener("close", (e) => { - if (e.detail.dirty) { - this.getLoraInfo(true); - } - }); - } - - stepStrength(direction, isTwo = false) { - let step = 0.05; - let prop = isTwo ? "strengthTwo" : "strength"; - let strength = (this.value[prop] ?? 1) + step * direction; - this.value[prop] = Math.round(strength * 100) / 100; - } - - getLoraInfo(force = false) { - if (!this.loraInfoPromise || force == true) { - let promise; - if (this.value.lora && this.value.lora != "None") { - promise = LORA_INFO_SERVICE.getInfo(this.value.lora, force, true); - } else { - promise = Promise.resolve(null); - } - this.loraInfoPromise = promise.then((v) => (this.loraInfo = v)); - } - return this.loraInfoPromise; - } -} - -const NODE_CLASS = MxdPowerLoraLoader; - -app.registerExtension({ - name: "mxd.PowerLoraLoader", - async beforeRegisterNodeDef(nodeType, nodeData) { - if (nodeData.name === NODE_CLASS.type) { - NODE_CLASS.setUp(nodeType, nodeData); - } - }, -}); diff --git a/web/addons/zip_loader/js/jszip.min.js b/web/vendor/zip_loader/jszip.min.js similarity index 100% rename from web/addons/zip_loader/js/jszip.min.js rename to web/vendor/zip_loader/jszip.min.js diff --git a/web/addons/zip_loader/js/zip_loader.js b/web/vendor/zip_loader/zip_loader.js similarity index 99% rename from web/addons/zip_loader/js/zip_loader.js rename to web/vendor/zip_loader/zip_loader.js index 60b6277..5f82c56 100644 --- a/web/addons/zip_loader/js/zip_loader.js +++ b/web/vendor/zip_loader/zip_loader.js @@ -1,4 +1,4 @@ -import { app } from "../../../../../scripts/app.js"; +import { app } from "../../../../scripts/app.js"; function get_ext(filename) { const ext = filename.split(".").pop();