Restructure web/ into lib+nodes+vendor; merge power lora loader forks

- web/lib/: the shared rgthree-derived layer (mxd_* utils, dialogs, api,
  runtime) + its CSS, moved as a unit.
- web/nodes/: one extension file per node/feature, _mxd suffix dropped.
- web/vendor/zip_loader/: vendored zip import addon.
- index.js explicitly imports every module; loraloader_mxd_entry.js
  folded into it and deleted.

Power Lora Loader merge: new web/nodes/power_lora_base.js holds the
shared node machinery, header row, and single-strength lora row widget.
power_lora_loader.js (dual model/clip strengths) and
ltx2_power_lora_loader.js (per-layer strength rows) are now thin
subclasses (~450 duplicated lines removed). Serialization formats,
widget names, extension names, and node targets unchanged.

better_combos.js scoped to MXD latent loaders only: removed the
pysssss CheckpointLoader/LoraLoader patching (already disabled behind
ENABLE_PYSSSS_NODE_ENHANCEMENTS=false, so provably inert), the dead
' MXD' alias config entries, and the no-op refreshComboInNodes wrapper.

scripts/check_web.py clean; node schema unchanged (68 identical).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Maxed-Out-99
2026-07-10 06:41:35 -07:00
co-authored by Claude Fable 5
parent 0447eb3172
commit 4cb19be2e0
43 changed files with 645 additions and 1163 deletions
+34 -16
View File
@@ -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';
@@ -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 {
@@ -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) {
@@ -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";
@@ -1,4 +1,4 @@
import { app } from "../../scripts/app.js";
import { app } from "../../../scripts/app.js";
class LogSession {
constructor(name) {
+2 -2
View File
@@ -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;
@@ -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) {
@@ -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";
@@ -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;
-3
View File
@@ -1,3 +0,0 @@
import "./mxd_runtime.js";
import "./power_lora_loader_mxd.js";
import "./ltx2_power_lora_loader_mxd.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);
};
},
});
@@ -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";
@@ -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";
@@ -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
@@ -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
@@ -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"]);
@@ -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([
@@ -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"]);
+209
View File
@@ -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);
}
},
});
@@ -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";
@@ -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
@@ -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);
}
},
});
+228
View File
@@ -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);
}
},
});
@@ -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.
@@ -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
@@ -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"]);
-697
View File
@@ -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);
}
},
});
@@ -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();