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:
co-authored by
Claude Fable 5
parent
0447eb3172
commit
4cb19be2e0
+34
-16
@@ -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) {
|
||||
@@ -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;
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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
-1
@@ -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();
|
||||
Reference in New Issue
Block a user