Make the canvas lora rows work under Nodes 2.0; drop the DOM rewrite
power_lora_dom.js existed because a canvas custom widget could not repaint itself inside core's WidgetLegacy wrapper, so toggling a lora or nudging a strength changed the value with no visible feedback. Frontend 1.48.7's WidgetLegacy.vue now assigns `widget.triggerDraw`, documented in litegraph's widget types as the compatibility hook for "widgets implementing the draw method when displayed in non-canvas renderers". That is the missing piece, so the parallel 528-line DOM implementation is no longer needed. - mxd_nodes2.js: add redrawWidget()/redrawWidgets(). Optional-call triggerDraw, so both are no-ops in classic mode and on frontends that lack it. - MxdBaseWidget.mouse(): repaint after every pointer event. Also fix its "pointer left the widget" test, which compared node-relative bounds against row-relative coordinates under Nodes 2.0 and swallowed clicks. - power_lora_base/loader: repaint on the mutations that happen outside a pointer handler — slot menu, toggle-all, async lora info, Show Strengths. - Delete power_lora_dom.js, mxd_power_lora.css, and the MXD.PowerLora.RowStyle setting. None of them were ever published, so no workflow or setting breaks. No Python touched; harness still reports 65 nodes identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0836bfd573
commit
48981e19d3
+3
-4
@@ -36,10 +36,9 @@ enforces this.
|
||||
`beforeRegisterNodeDef` (names must match `NODE_CLASS_MAPPINGS` keys).
|
||||
- `power_lora_base.js` — shared base for the lora loader: node machinery,
|
||||
header row, single-strength row widget. `power_lora_loader.js` adds the
|
||||
dual model/clip strength mode. `power_lora_dom.js` is a parallel DOM-widget
|
||||
implementation of the same rows for Nodes 2.0 — see the note in CLAUDE.md,
|
||||
it is unfinished. Serialization shapes are frozen (see the CLAUDE.md
|
||||
contract).
|
||||
dual model/clip strength mode. Rows are canvas-drawn in both classic and
|
||||
Nodes 2.0; `mxd_nodes2.js` supplies the width and repaint shims that make
|
||||
that work. Serialization shapes are frozen (see the CLAUDE.md contract).
|
||||
- `better_combos.js` — folder-tree/grid combo display for the MXD latent
|
||||
loaders (adapted from pysssss; scoped to MXD nodes only; keeps its BOM).
|
||||
- `run_folder.js` — wraps `app.queuePrompt` for batch folder runs; uses
|
||||
|
||||
@@ -18,7 +18,6 @@ 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_dom.js';
|
||||
import './nodes/power_lora_base.js';
|
||||
import './nodes/power_lora_loader.js';
|
||||
import './nodes/ltx_sampler.js';
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
// Deliberately, with Nodes 2.0 OFF it returns `node.size[0]` — identical to the
|
||||
// pre-Nodes-2.0 behavior — so turning the setting off is a full revert and
|
||||
// classic rendering can never be affected by this module.
|
||||
//
|
||||
// Repainting: see redrawWidgets() below.
|
||||
import { app } from "../../../scripts/app.js";
|
||||
|
||||
const SETTING_ID = "Comfy.VueNodes.Enabled";
|
||||
@@ -64,3 +66,29 @@ export function nodeDrawWidth(node, w) {
|
||||
}
|
||||
return node?.size?.[0] ?? 0;
|
||||
}
|
||||
|
||||
// Repaint a canvas-drawn custom widget after code changed its value directly.
|
||||
//
|
||||
// Classic mode repaints the whole node canvas, so setDirtyCanvas() is enough.
|
||||
// Under Nodes 2.0 each legacy widget lives in its OWN <canvas> inside core's
|
||||
// WidgetLegacy component, and the graph canvas being dirty says nothing about
|
||||
// that private canvas. Core's fix is `widget.triggerDraw`, which it documents as
|
||||
// "compatibility method for widgets implementing the draw method when displayed
|
||||
// in non-canvas renderers ... set by the current renderer implementation".
|
||||
// Core only wires it into `widget.callback`; our widgets mutate `this.value`
|
||||
// directly, so we call it ourselves.
|
||||
//
|
||||
// It is absent on classic frontends and on older ones that predate it, hence
|
||||
// the optional call — this is a no-op there, which is exactly right.
|
||||
export function redrawWidget(widget) {
|
||||
widget?.triggerDraw?.();
|
||||
}
|
||||
|
||||
// Repaint every widget on a node. Used when one interaction changes several
|
||||
// rows at once (toggle-all, reordering, a row being removed).
|
||||
export function redrawWidgets(node) {
|
||||
for (const widget of node?.widgets || []) {
|
||||
widget.triggerDraw?.();
|
||||
}
|
||||
node?.setDirtyCanvas?.(true, true);
|
||||
}
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
/* HTML (DOM-widget) rows for the Power Lora Loader nodes.
|
||||
*
|
||||
* Used only when the HTML row style is active (see power_lora_dom.js); the
|
||||
* classic canvas-painted rows do not load or need this file. Colors come from
|
||||
* ComfyUI's theme variables so the rows track light/dark and custom palettes,
|
||||
* with fallbacks for older frontends that lack a given variable. */
|
||||
|
||||
.mxd-lora-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0 2px;
|
||||
font-family: inherit;
|
||||
font-size: 11px;
|
||||
color: var(--input-text, #ddd);
|
||||
/* Nothing here should trigger the canvas' own drag/pan handling. */
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.mxd-lora-row.mxd-lora-off > *:not(.mxd-lora-toggle) {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* Shared look for every clickable part of a row. */
|
||||
.mxd-lora-row button,
|
||||
.mxd-lora-row input {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
background: var(--comfy-input-bg, #222);
|
||||
border: 1px solid var(--border-color, #4e4e4e);
|
||||
border-radius: 4px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mxd-lora-row button:hover,
|
||||
.mxd-lora-row input:hover {
|
||||
border-color: var(--p-primary-color, #66a);
|
||||
}
|
||||
|
||||
.mxd-lora-row button:focus-visible,
|
||||
.mxd-lora-row input:focus-visible {
|
||||
outline: 1px solid var(--p-primary-color, #66a);
|
||||
outline-offset: 0;
|
||||
}
|
||||
|
||||
/* On/off switch --------------------------------------------------------- */
|
||||
|
||||
.mxd-lora-toggle {
|
||||
flex: 0 0 auto;
|
||||
position: relative;
|
||||
width: 22px;
|
||||
height: 12px;
|
||||
border-radius: 6px !important;
|
||||
background: var(--comfy-input-bg, #222);
|
||||
}
|
||||
|
||||
.mxd-lora-toggle::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--border-color, #666);
|
||||
transition: transform 0.12s ease, background-color 0.12s ease;
|
||||
}
|
||||
|
||||
.mxd-lora-toggle[aria-checked="true"] {
|
||||
background: color-mix(in srgb, var(--p-primary-color, #66a) 35%, transparent);
|
||||
border-color: var(--p-primary-color, #66a);
|
||||
}
|
||||
|
||||
.mxd-lora-toggle[aria-checked="true"]::after {
|
||||
transform: translateX(10px);
|
||||
background: var(--p-primary-color, #8ae);
|
||||
}
|
||||
|
||||
/* Indeterminate state, used by the header's toggle-all. */
|
||||
.mxd-lora-toggle[aria-checked="mixed"]::after {
|
||||
transform: translateX(5px);
|
||||
background: var(--p-primary-color, #8ae);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Lora name ------------------------------------------------------------- */
|
||||
|
||||
.mxd-lora-name {
|
||||
flex: 1 1 auto;
|
||||
height: 16px;
|
||||
text-align: left;
|
||||
padding: 0 4px !important;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
direction: rtl; /* keep the filename's tail (the distinctive part) visible */
|
||||
}
|
||||
|
||||
.mxd-lora-name > span {
|
||||
direction: ltr;
|
||||
unicode-bidi: embed;
|
||||
}
|
||||
|
||||
/* Small square icon buttons (info / remove) ----------------------------- */
|
||||
|
||||
.mxd-lora-icon {
|
||||
flex: 0 0 auto;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.mxd-lora-remove {
|
||||
color: #d88;
|
||||
}
|
||||
|
||||
.mxd-lora-remove:hover {
|
||||
border-color: #d88;
|
||||
}
|
||||
|
||||
/* Strength stepper ------------------------------------------------------ */
|
||||
|
||||
.mxd-lora-strength {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.mxd-lora-strength button {
|
||||
width: 12px;
|
||||
height: 15px;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.mxd-lora-strength input {
|
||||
width: 38px;
|
||||
height: 15px;
|
||||
text-align: center;
|
||||
/* Horizontal drag scrubs the value; a plain click still types. */
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.mxd-lora-strength input.mxd-lora-out-of-range {
|
||||
color: #c66;
|
||||
}
|
||||
|
||||
/* Header row ------------------------------------------------------------ */
|
||||
|
||||
.mxd-lora-header {
|
||||
font-size: 10px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.mxd-lora-header .mxd-lora-header-label {
|
||||
flex: 1 1 auto;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Column captions sit above the strength steppers, so they must match the
|
||||
* stepper's total width (12 + 1 + 38 + 1 + 12) to stay aligned. */
|
||||
.mxd-lora-header .mxd-lora-header-col {
|
||||
flex: 0 0 auto;
|
||||
width: 64px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Occupies the space of a row's info/remove button without drawing anything. */
|
||||
.mxd-lora-header .mxd-lora-spacer {
|
||||
visibility: hidden;
|
||||
border: 0 !important;
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
/* "+ Add Lora" ---------------------------------------------------------- */
|
||||
|
||||
.mxd-lora-add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
font-family: inherit;
|
||||
font-size: 11px;
|
||||
color: var(--input-text, #ddd);
|
||||
background: var(--comfy-input-bg, #222);
|
||||
border: 1px solid var(--border-color, #4e4e4e);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.mxd-lora-add:hover {
|
||||
border-color: var(--p-primary-color, #66a);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { drawNodeWidget, drawWidgetButton, fitString, isLowQuality } from "./mxd_utils_canvas.js";
|
||||
import { isNodes2Enabled, nodeDrawWidth } from "./mxd_nodes2.js";
|
||||
export function drawLabelAndValue(ctx, label, value, width, posY, height, options) {
|
||||
var _a;
|
||||
const outerMargin = 15;
|
||||
@@ -46,6 +47,12 @@ export class MxdBaseWidget {
|
||||
mouse(event, pos, node) {
|
||||
var _a, _b, _c;
|
||||
const canvas = app.canvas;
|
||||
// Any pointer interaction can change what this widget looks like — a
|
||||
// pressed button, a flipped toggle, a new number. Under Nodes 2.0 the
|
||||
// graph canvas going dirty does not repaint this widget's own canvas,
|
||||
// so ask core's renderer for a redraw once the handlers below have run.
|
||||
// No-op in classic mode and on frontends without `triggerDraw`.
|
||||
const redraw = () => this.triggerDraw?.();
|
||||
if (event.type == "pointerdown") {
|
||||
this.mouseDowned = [...pos];
|
||||
this.isMouseDownedAndOver = true;
|
||||
@@ -67,7 +74,9 @@ export class MxdBaseWidget {
|
||||
part.wasMouseClickedAndIsOver = true;
|
||||
}
|
||||
}
|
||||
return (_a = this.onMouseDown(event, pos, node)) !== null && _a !== void 0 ? _a : anyHandled;
|
||||
const downResult = (_a = this.onMouseDown(event, pos, node)) !== null && _a !== void 0 ? _a : anyHandled;
|
||||
redraw();
|
||||
return downResult;
|
||||
}
|
||||
if (event.type == "pointerup") {
|
||||
if (!this.mouseDowned)
|
||||
@@ -94,15 +103,24 @@ export class MxdBaseWidget {
|
||||
const thisHandled = this.onMouseClick(event, pos, node);
|
||||
anyHandled = anyHandled || thisHandled == true;
|
||||
}
|
||||
return (_b = this.onMouseUp(event, pos, node)) !== null && _b !== void 0 ? _b : anyHandled;
|
||||
const upResult = (_b = this.onMouseUp(event, pos, node)) !== null && _b !== void 0 ? _b : anyHandled;
|
||||
redraw();
|
||||
return upResult;
|
||||
}
|
||||
if (event.type == "pointermove") {
|
||||
this.isMouseDownedAndOver = !!this.mouseDowned;
|
||||
// Under Nodes 2.0 `pos` is relative to this widget's own row canvas,
|
||||
// whose top is 0 and whose width core stores in `this.width`. In
|
||||
// classic mode `pos` is node-relative, so the row top is `last_y`
|
||||
// and the surface is the whole node. Using the wrong pair here makes
|
||||
// the widget think the pointer left it and swallows the click.
|
||||
const surfaceWidth = nodeDrawWidth(node, this.width);
|
||||
const rowTop = isNodes2Enabled() ? 0 : this.last_y;
|
||||
if (this.mouseDowned &&
|
||||
(pos[0] < 15 ||
|
||||
pos[0] > node.size[0] - 15 ||
|
||||
pos[1] < this.last_y ||
|
||||
pos[1] > this.last_y + LiteGraph.NODE_WIDGET_HEIGHT)) {
|
||||
pos[0] > surfaceWidth - 15 ||
|
||||
pos[1] < rowTop ||
|
||||
pos[1] > rowTop + LiteGraph.NODE_WIDGET_HEIGHT)) {
|
||||
this.isMouseDownedAndOver = false;
|
||||
}
|
||||
for (const part of Object.values(this.hitAreas)) {
|
||||
@@ -113,7 +131,9 @@ export class MxdBaseWidget {
|
||||
part.wasMouseClickedAndIsOver = this.clickWasWithinBounds(pos, part.bounds);
|
||||
}
|
||||
}
|
||||
return (_c = this.onMouseMove(event, pos, node)) !== null && _c !== void 0 ? _c : true;
|
||||
const moveResult = (_c = this.onMouseMove(event, pos, node)) !== null && _c !== void 0 ? _c : true;
|
||||
redraw();
|
||||
return moveResult;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -31,18 +31,11 @@ import {
|
||||
MxdDividerWidget,
|
||||
} from "../lib/mxd_utils_widgets.js";
|
||||
import { mxdApi } from "../lib/mxd_api.js";
|
||||
import { nodeDrawWidth } from "../lib/mxd_nodes2.js";
|
||||
import { nodeDrawWidth, redrawWidget, redrawWidgets } from "../lib/mxd_nodes2.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";
|
||||
import {
|
||||
addDomAddLoraButton,
|
||||
addDomHeaderRow,
|
||||
addDomLoraRow,
|
||||
refreshPowerLoraDom,
|
||||
usePowerLoraDom,
|
||||
} from "./power_lora_dom.js";
|
||||
|
||||
export const PROP_LABEL_SHOW_STRENGTHS = "Show Strengths";
|
||||
export const PROP_LABEL_SHOW_STRENGTHS_STATIC = `@${PROP_LABEL_SHOW_STRENGTHS}`;
|
||||
@@ -52,9 +45,6 @@ export const PROP_VALUE_SHOW_STRENGTHS_SEPARATE = "Separate Model & Clip";
|
||||
export class MxdPowerLoraLoaderBase extends MxdBaseServerNode {
|
||||
// Subclasses set this to their canvas-drawn lora row widget class.
|
||||
static loraWidgetClass = null;
|
||||
// Whether this node's rows can show separate model/clip strengths. Only
|
||||
// consulted by the HTML rows; the canvas rows read the property directly.
|
||||
static loraRowSupportsDual = false;
|
||||
|
||||
constructor(title = new.target.title, loggerName = "[Power Lora Loader]") {
|
||||
super(title);
|
||||
@@ -62,9 +52,6 @@ export class MxdPowerLoraLoaderBase extends MxdBaseServerNode {
|
||||
this.logger = mxdRuntime.newLogSession(loggerName);
|
||||
this.loraWidgetsCounter = 0;
|
||||
this.widgetButtonSpacer = null;
|
||||
// Decided once per node so a node never mixes canvas and HTML rows if the
|
||||
// setting changes while it exists. Both implementations serialize the same.
|
||||
this.usesDomRows = usePowerLoraDom();
|
||||
|
||||
mxdApi.getLoras();
|
||||
|
||||
@@ -172,8 +159,7 @@ export class MxdPowerLoraLoaderBase extends MxdBaseServerNode {
|
||||
this.setDirtyCanvas(true, true);
|
||||
}
|
||||
|
||||
// True when the node shows separate model & clip strength columns. Nodes
|
||||
// without the "Show Strengths" property (e.g. the LTX2 loader) never do.
|
||||
// True when the node shows separate model & clip strength columns.
|
||||
isShowingSeparateStrengths() {
|
||||
return this.properties?.[PROP_LABEL_SHOW_STRENGTHS] === PROP_VALUE_SHOW_STRENGTHS_SEPARATE;
|
||||
}
|
||||
@@ -181,9 +167,7 @@ export class MxdPowerLoraLoaderBase extends MxdBaseServerNode {
|
||||
addNewLoraWidget(lora) {
|
||||
this.loraWidgetsCounter++;
|
||||
const name = "lora_" + this.loraWidgetsCounter;
|
||||
const widget = this.usesDomRows
|
||||
? addDomLoraRow(this, name, { dual: this.constructor.loraRowSupportsDual })
|
||||
: this.addCustomWidget(new this.constructor.loraWidgetClass(name));
|
||||
const widget = this.addCustomWidget(new this.constructor.loraWidgetClass(name));
|
||||
if (lora) widget.setLora(lora);
|
||||
if (this.widgetButtonSpacer) {
|
||||
moveArrayItem(this.widgets, widget, this.widgets.indexOf(this.widgetButtonSpacer));
|
||||
@@ -201,11 +185,9 @@ export class MxdPowerLoraLoaderBase extends MxdBaseServerNode {
|
||||
moveArrayItem(this.widgets, this.addHeaderRowWidget(), 1);
|
||||
}
|
||||
|
||||
// The toggle-all / column-caption row, in whichever style this node uses.
|
||||
// The toggle-all / column-caption row.
|
||||
addHeaderRowWidget() {
|
||||
return this.usesDomRows
|
||||
? addDomHeaderRow(this, { dual: this.constructor.loraRowSupportsDual })
|
||||
: this.addCustomWidget(new PowerLoraLoaderHeaderWidget());
|
||||
return this.addCustomWidget(new PowerLoraLoaderHeaderWidget());
|
||||
}
|
||||
|
||||
addNonLoraWidgets() {
|
||||
@@ -215,11 +197,6 @@ export class MxdPowerLoraLoaderBase extends MxdBaseServerNode {
|
||||
new MxdDividerWidget({ marginTop: 4, marginBottom: 0, thickness: 0 }),
|
||||
);
|
||||
|
||||
if (this.usesDomRows) {
|
||||
addDomAddLoraButton(this, (value) => this.addNewLoraWidget(value));
|
||||
return;
|
||||
}
|
||||
|
||||
this.addCustomWidget(
|
||||
new MxdBetterButtonWidget("+ Add Lora", (event, pos, node) => {
|
||||
mxdApi.getLoras().then((lorasDetails) => {
|
||||
@@ -231,7 +208,8 @@ export class MxdPowerLoraLoaderBase extends MxdBaseServerNode {
|
||||
this.addNewLoraWidget(value);
|
||||
const computed = this.computeSize();
|
||||
this.size[1] = Math.max(this.size?.[1] ?? 15, computed[1]);
|
||||
this.setDirtyCanvas(true, true);
|
||||
// A new row shifts every row below it, so repaint them all.
|
||||
redrawWidgets(this);
|
||||
}
|
||||
},
|
||||
null,
|
||||
@@ -278,7 +256,7 @@ export class MxdPowerLoraLoaderBase extends MxdBaseServerNode {
|
||||
content: `${widget.value.on ? "Disable" : "Enable"}`,
|
||||
callback: () => {
|
||||
widget.value.on = !widget.value.on;
|
||||
refreshPowerLoraDom(this);
|
||||
redrawWidgets(this);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -286,7 +264,7 @@ export class MxdPowerLoraLoaderBase extends MxdBaseServerNode {
|
||||
disabled: !canMoveUp,
|
||||
callback: () => {
|
||||
moveArrayItem(this.widgets, widget, index - 1);
|
||||
refreshPowerLoraDom(this);
|
||||
redrawWidgets(this);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -294,7 +272,7 @@ export class MxdPowerLoraLoaderBase extends MxdBaseServerNode {
|
||||
disabled: !canMoveDown,
|
||||
callback: () => {
|
||||
moveArrayItem(this.widgets, widget, index + 1);
|
||||
refreshPowerLoraDom(this);
|
||||
redrawWidgets(this);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -303,7 +281,7 @@ export class MxdPowerLoraLoaderBase extends MxdBaseServerNode {
|
||||
// removeWidget (unlike removeArrayItem) fires onRemove, which the
|
||||
// HTML rows need in order to detach their element.
|
||||
this.removeWidget(widget);
|
||||
refreshPowerLoraDom(this);
|
||||
redrawWidgets(this);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -320,6 +298,16 @@ export class MxdPowerLoraLoaderBase extends MxdBaseServerNode {
|
||||
mxdApi.getLoras(true);
|
||||
}
|
||||
|
||||
// Flipping "Show Strengths" changes what every row draws. Classic mode
|
||||
// repaints on the next canvas frame anyway; Nodes 2.0 needs to be told.
|
||||
onPropertyChanged(name, value, prevValue) {
|
||||
const handled = super.onPropertyChanged?.(name, value, prevValue);
|
||||
if (name === PROP_LABEL_SHOW_STRENGTHS) {
|
||||
redrawWidgets(this);
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
|
||||
hasLoraWidgets() {
|
||||
return !!this.widgets?.find((w) => w.name?.startsWith("lora_"));
|
||||
}
|
||||
@@ -346,7 +334,7 @@ export class MxdPowerLoraLoaderBase extends MxdBaseServerNode {
|
||||
widget.value.on = toggledTo;
|
||||
}
|
||||
}
|
||||
refreshPowerLoraDom(this);
|
||||
redrawWidgets(this);
|
||||
}
|
||||
|
||||
static setUp(comfyClass, nodeData) {
|
||||
@@ -460,6 +448,7 @@ export class PowerLoraBaseWidget extends MxdBaseWidget {
|
||||
setLora(lora) {
|
||||
this._value.lora = lora;
|
||||
this.getLoraInfo();
|
||||
redrawWidget(this);
|
||||
}
|
||||
|
||||
// Shared drawing pieces -----------------------------------------------
|
||||
@@ -584,6 +573,8 @@ export class PowerLoraBaseWidget extends MxdBaseWidget {
|
||||
|
||||
onToggleDown(event, pos, node) {
|
||||
this.value.on = !this.value.on;
|
||||
// The header's toggle-all reflects every row, so repaint the whole node.
|
||||
redrawWidgets(node);
|
||||
this.cancelMouseDown();
|
||||
return true;
|
||||
}
|
||||
@@ -596,7 +587,7 @@ export class PowerLoraBaseWidget extends MxdBaseWidget {
|
||||
|
||||
onRemoveDown(event, pos, node) {
|
||||
removeArrayItem(node.widgets, this);
|
||||
node.setDirtyCanvas(true, true);
|
||||
redrawWidgets(node);
|
||||
this.cancelMouseDown();
|
||||
return true;
|
||||
}
|
||||
@@ -609,6 +600,7 @@ export class PowerLoraBaseWidget extends MxdBaseWidget {
|
||||
this.getLoraInfo();
|
||||
}
|
||||
node.setDirtyCanvas(true, true);
|
||||
redrawWidget(this);
|
||||
});
|
||||
this.cancelMouseDown();
|
||||
}
|
||||
@@ -625,13 +617,22 @@ export class PowerLoraBaseWidget extends MxdBaseWidget {
|
||||
if (event.deltaX) {
|
||||
this.haveMouseMovedStrength = true;
|
||||
this.value.strength = (this.value.strength ?? 1) + event.deltaX * 0.05;
|
||||
redrawWidget(this);
|
||||
}
|
||||
}
|
||||
|
||||
onStrengthValUp(event, pos, node) {
|
||||
if (this.haveMouseMovedStrength) return;
|
||||
const canvas = app.canvas;
|
||||
canvas.prompt("Value", this.value.strength, (v) => (this.value.strength = Number(v)), event);
|
||||
canvas.prompt(
|
||||
"Value",
|
||||
this.value.strength,
|
||||
(v) => {
|
||||
this.value.strength = Number(v);
|
||||
redrawWidget(this);
|
||||
},
|
||||
event,
|
||||
);
|
||||
}
|
||||
|
||||
onMouseUp(event, pos, node) {
|
||||
@@ -655,6 +656,7 @@ export class PowerLoraBaseWidget extends MxdBaseWidget {
|
||||
let step = 0.05;
|
||||
let strength = (this.value.strength ?? 1) + step * direction;
|
||||
this.value.strength = Math.round(strength * 100) / 100;
|
||||
redrawWidget(this);
|
||||
}
|
||||
|
||||
getLoraInfo(force = false) {
|
||||
@@ -665,7 +667,12 @@ export class PowerLoraBaseWidget extends MxdBaseWidget {
|
||||
} else {
|
||||
promise = Promise.resolve(null);
|
||||
}
|
||||
this.loraInfoPromise = promise.then((v) => (this.loraInfo = v));
|
||||
// Arrives asynchronously and decides the out-of-range strength color.
|
||||
this.loraInfoPromise = promise.then((v) => {
|
||||
this.loraInfo = v;
|
||||
redrawWidget(this);
|
||||
return v;
|
||||
});
|
||||
}
|
||||
return this.loraInfoPromise;
|
||||
}
|
||||
|
||||
@@ -1,528 +0,0 @@
|
||||
// HTML (DOM-widget) rows for the Power Lora Loader nodes.
|
||||
//
|
||||
// This file is a PARALLEL implementation of the row widgets that live in
|
||||
// power_lora_base.js / power_lora_loader.js. Those
|
||||
// paint themselves onto the LiteGraph canvas; these build real HTML elements and
|
||||
// register them with `node.addDOMWidget()`. Nothing here modifies the canvas
|
||||
// widgets — `usePowerLoraDom()` decides which of the two a node is built from,
|
||||
// so the classic rows remain reachable and unchanged forever.
|
||||
//
|
||||
// Why an HTML version exists
|
||||
// --------------------------
|
||||
// Under ComfyUI's Nodes 2.0 (DOM node rendering), core hosts a canvas-drawing
|
||||
// custom widget in its `WidgetLegacy` component, which only repaints that
|
||||
// widget's private canvas when the widget's `callback` fires. Our canvas rows
|
||||
// mutate `this.value` directly and rely on `node.setDirtyCanvas()`, which
|
||||
// repaints the graph canvas but NOT the widget's private canvas — so toggling a
|
||||
// lora or nudging a strength changed the value with no visible feedback. Real
|
||||
// DOM elements sidestep the whole bridge: the browser renders them, and both
|
||||
// classic ComfyUI and Nodes 2.0 mount DOM widgets natively (core uses the same
|
||||
// API for every multiline prompt box).
|
||||
//
|
||||
// Serialization is deliberately IDENTICAL to the canvas rows: widget names stay
|
||||
// `lora_N` and values stay `{on, lora, strength[, strengthTwo]}`. Saved
|
||||
// workflows cannot tell which implementation drew them.
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { isNodes2Enabled } from "../lib/mxd_nodes2.js";
|
||||
import { injectCss } from "../lib/mxd_shared_utils.js";
|
||||
import { showLoraChooser } from "../lib/mxd_utils_menu.js";
|
||||
import { MxdLoraInfoDialog } from "../lib/mxd_dialog_info.js";
|
||||
import { LORA_INFO_SERVICE } from "../lib/mxd_model_info_service.js";
|
||||
|
||||
const CSS_HREF = new URL("../lib/mxd_power_lora.css", import.meta.url).pathname;
|
||||
|
||||
export const ROW_HEIGHT = 20;
|
||||
export const HEADER_HEIGHT = 16;
|
||||
export const BUTTON_HEIGHT = 22;
|
||||
|
||||
const STRENGTH_STEP = 0.05;
|
||||
const SETTING_ROW_STYLE = "MXD.PowerLora.RowStyle";
|
||||
const STYLE_AUTO = "Auto (follow Nodes 2.0)";
|
||||
const STYLE_CLASSIC = "Classic canvas";
|
||||
const STYLE_HTML = "HTML";
|
||||
|
||||
// Which row implementation new nodes should be built from.
|
||||
// Auto keeps classic behavior whenever Nodes 2.0 is off, so turning Nodes 2.0
|
||||
// off is a complete return to the original rendering.
|
||||
export function usePowerLoraDom() {
|
||||
let style = STYLE_AUTO;
|
||||
try {
|
||||
style = app?.extensionManager?.setting?.get(SETTING_ROW_STYLE) ?? STYLE_AUTO;
|
||||
} catch (e) {
|
||||
style = STYLE_AUTO;
|
||||
}
|
||||
if (style === STYLE_CLASSIC) return false;
|
||||
if (style === STYLE_HTML) return true;
|
||||
return isNodes2Enabled();
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "mxd.PowerLoraRowStyle",
|
||||
settings: [
|
||||
{
|
||||
id: SETTING_ROW_STYLE,
|
||||
category: ["MaxedOut", "Power Lora Loader", "Row style"],
|
||||
name: "Power Lora Loader row style",
|
||||
tooltip:
|
||||
"How the Lora Loader MXD / LTX2 Lora Loader MXD rows are drawn. " +
|
||||
"Auto uses HTML rows when ComfyUI's Nodes 2.0 is on and the original " +
|
||||
"canvas rows when it is off. Reload the page after changing this.",
|
||||
type: "combo",
|
||||
options: [STYLE_AUTO, STYLE_CLASSIC, STYLE_HTML],
|
||||
defaultValue: STYLE_AUTO,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// --- small helpers -------------------------------------------------------
|
||||
|
||||
function fmt(value) {
|
||||
return Number(value ?? 0).toFixed(2);
|
||||
}
|
||||
|
||||
function el(tag, className, parent) {
|
||||
const node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (parent) parent.appendChild(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
function button(className, parent, label) {
|
||||
const b = el("button", className, parent);
|
||||
b.type = "button";
|
||||
if (label != null) b.textContent = label;
|
||||
// Keep presses from reaching the canvas (node drag) or the Vue node body.
|
||||
b.addEventListener("pointerdown", (e) => e.stopPropagation());
|
||||
return b;
|
||||
}
|
||||
|
||||
// Every DOM row on a node registers a refresher so that changes made elsewhere
|
||||
// (toggle-all, the slot context menu, a workflow load) can repaint the row.
|
||||
function registerRefresher(node, fn) {
|
||||
if (!node.__mxdDomRefreshers) node.__mxdDomRefreshers = [];
|
||||
node.__mxdDomRefreshers.push(fn);
|
||||
return fn;
|
||||
}
|
||||
|
||||
export function refreshPowerLoraDom(node) {
|
||||
for (const fn of node?.__mxdDomRefreshers || []) {
|
||||
try {
|
||||
fn();
|
||||
} catch (e) {
|
||||
/* a broken row must not stop the rest from repainting */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Grow the node to fit its widgets, matching what the canvas rows do.
|
||||
function growNodeToFit(node) {
|
||||
const computed = node.computeSize();
|
||||
node.size = node.size || [0, 0];
|
||||
node.size[1] = Math.max(node.size[1] ?? 15, computed[1]);
|
||||
node.setDirtyCanvas(true, true);
|
||||
}
|
||||
|
||||
// Registers `element` as a DOM widget and wires the shared plumbing:
|
||||
// value <-> state, serialization, fixed row height, and element teardown.
|
||||
//
|
||||
// Serialization note: core saves workflows from `widget.value` (JSON-cloned)
|
||||
// but builds prompts from `serializeValue()`. Every widget here keeps the same
|
||||
// `value` its canvas counterpart had, and no widget opts out of serialization,
|
||||
// so `widgets_values` comes out byte-identical either way — a workflow saved in
|
||||
// one row style loads correctly in the other.
|
||||
function addRowWidget(node, { name, element, height, getState, setState, serializeState }) {
|
||||
injectCss(CSS_HREF);
|
||||
|
||||
const widget = node.addDOMWidget(name, "custom", element, {
|
||||
hideOnZoom: true,
|
||||
getValue: getState,
|
||||
setValue: setState,
|
||||
getMinHeight: () => height,
|
||||
getMaxHeight: () => height,
|
||||
getHeight: () => height,
|
||||
});
|
||||
|
||||
// Hand back a plain snapshot, never the live proxy.
|
||||
widget.serializeValue = serializeState ?? (() => ({ ...getState() }));
|
||||
|
||||
const priorOnRemove = widget.onRemove?.bind(widget);
|
||||
widget.onRemove = () => {
|
||||
priorOnRemove?.();
|
||||
element.remove();
|
||||
const list = node.__mxdDomRefreshers;
|
||||
if (list && widget.__mxdRefresher) {
|
||||
const i = list.indexOf(widget.__mxdRefresher);
|
||||
if (i > -1) list.splice(i, 1);
|
||||
}
|
||||
};
|
||||
|
||||
return widget;
|
||||
}
|
||||
|
||||
// Wraps the row's raw value object so that ANY external mutation — including
|
||||
// `widget.value.on = false` from toggleAllLoras or the slot context menu —
|
||||
// repaints the HTML. Without this, code written for the canvas rows would
|
||||
// silently change state that the DOM never re-rendered.
|
||||
function reactiveState(raw, onChange) {
|
||||
return new Proxy(raw, {
|
||||
set(target, prop, value) {
|
||||
target[prop] = value;
|
||||
onChange();
|
||||
return true;
|
||||
},
|
||||
deleteProperty(target, prop) {
|
||||
delete target[prop];
|
||||
onChange();
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// A `- value +` stepper with click-to-type and horizontal drag-to-scrub,
|
||||
// mirroring the canvas rows' strength control.
|
||||
function strengthControl(parent, { get, set, onCommit }) {
|
||||
const wrap = el("div", "mxd-lora-strength", parent);
|
||||
const dec = button(null, wrap, "‹");
|
||||
const input = el("input", null, wrap);
|
||||
const inc = button(null, wrap, "›");
|
||||
|
||||
input.type = "text";
|
||||
input.spellcheck = false;
|
||||
|
||||
const render = () => {
|
||||
if (document.activeElement !== input) {
|
||||
input.value = fmt(get());
|
||||
}
|
||||
};
|
||||
|
||||
const step = (direction) => {
|
||||
set(Math.round((Number(get() ?? 1) + STRENGTH_STEP * direction) * 100) / 100);
|
||||
render();
|
||||
onCommit?.();
|
||||
};
|
||||
|
||||
dec.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
step(-1);
|
||||
});
|
||||
inc.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
step(1);
|
||||
});
|
||||
|
||||
// Drag horizontally to scrub; a click that never moved focuses for typing.
|
||||
let dragStartX = null;
|
||||
let dragStartValue = 0;
|
||||
let dragged = false;
|
||||
|
||||
input.addEventListener("pointerdown", (e) => {
|
||||
e.stopPropagation();
|
||||
if (document.activeElement === input) return; // already typing
|
||||
e.preventDefault();
|
||||
dragStartX = e.clientX;
|
||||
dragStartValue = Number(get() ?? 1);
|
||||
dragged = false;
|
||||
input.setPointerCapture(e.pointerId);
|
||||
});
|
||||
|
||||
input.addEventListener("pointermove", (e) => {
|
||||
if (dragStartX == null) return;
|
||||
const dx = e.clientX - dragStartX;
|
||||
if (!dragged && Math.abs(dx) < 3) return;
|
||||
dragged = true;
|
||||
set(Math.round((dragStartValue + dx * STRENGTH_STEP) * 100) / 100);
|
||||
render();
|
||||
});
|
||||
|
||||
input.addEventListener("pointerup", (e) => {
|
||||
if (dragStartX == null) return;
|
||||
input.releasePointerCapture?.(e.pointerId);
|
||||
dragStartX = null;
|
||||
if (dragged) {
|
||||
onCommit?.();
|
||||
} else {
|
||||
input.focus();
|
||||
input.select();
|
||||
}
|
||||
});
|
||||
|
||||
const commitTyped = () => {
|
||||
const parsed = Number.parseFloat(input.value);
|
||||
if (Number.isFinite(parsed)) {
|
||||
set(parsed);
|
||||
}
|
||||
render();
|
||||
onCommit?.();
|
||||
};
|
||||
|
||||
input.addEventListener("change", commitTyped);
|
||||
input.addEventListener("blur", commitTyped);
|
||||
input.addEventListener("keydown", (e) => {
|
||||
e.stopPropagation(); // don't let the canvas' hotkeys steal typing
|
||||
if (e.key === "Enter") {
|
||||
input.blur();
|
||||
} else if (e.key === "Escape") {
|
||||
render();
|
||||
input.blur();
|
||||
}
|
||||
});
|
||||
|
||||
return { render, input };
|
||||
}
|
||||
|
||||
// --- lora row ------------------------------------------------------------
|
||||
|
||||
// One lora: [on/off] name [i] [x] [strength] (+ a second strength when `dual`).
|
||||
// `dual` mirrors the "Show Strengths" property of Lora Loader MXD; the row
|
||||
// re-reads it on every refresh so switching the property updates live.
|
||||
export function addDomLoraRow(node, name, { dual = false } = {}) {
|
||||
const raw = { on: true, lora: null, strength: 1 };
|
||||
if (dual) raw.strengthTwo = null;
|
||||
|
||||
let loraInfo = null;
|
||||
let loraInfoPromise = null;
|
||||
|
||||
const root = el("div", "mxd-lora-row");
|
||||
const toggle = button("mxd-lora-toggle", root);
|
||||
toggle.setAttribute("role", "switch");
|
||||
const nameBtn = button("mxd-lora-name", root);
|
||||
const nameText = el("span", null, nameBtn);
|
||||
const infoBtn = button("mxd-lora-icon mxd-lora-info", root, "i");
|
||||
const removeBtn = button("mxd-lora-icon mxd-lora-remove", root, "✕");
|
||||
|
||||
let widget = null;
|
||||
const state = reactiveState(raw, () => render());
|
||||
|
||||
const isDualActive = () => dual && node.isShowingSeparateStrengths?.() === true;
|
||||
|
||||
const outOfRange = (value) => {
|
||||
if (loraInfo?.strengthMax != null && value > loraInfo.strengthMax) return true;
|
||||
if (loraInfo?.strengthMin != null && value < loraInfo.strengthMin) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
// Left stepper only exists in dual mode: it is the MODEL strength, and the
|
||||
// right stepper becomes CLIP (strengthTwo) — same column order as the canvas
|
||||
// rows, where the rightmost number is clip.
|
||||
const modelStrength = strengthControl(root, {
|
||||
get: () => raw.strength ?? 1,
|
||||
set: (v) => {
|
||||
raw.strength = v;
|
||||
},
|
||||
onCommit: () => refreshPowerLoraDom(node),
|
||||
});
|
||||
|
||||
const clipStrength = strengthControl(root, {
|
||||
get: () => (isDualActive() ? (raw.strengthTwo ?? 1) : (raw.strength ?? 1)),
|
||||
set: (v) => {
|
||||
if (isDualActive()) raw.strengthTwo = v;
|
||||
else raw.strength = v;
|
||||
},
|
||||
onCommit: () => refreshPowerLoraDom(node),
|
||||
});
|
||||
|
||||
function render() {
|
||||
const dualActive = isDualActive();
|
||||
if (dualActive && raw.strengthTwo == null) {
|
||||
raw.strengthTwo = raw.strength ?? 1;
|
||||
} else if (!dualActive && dual && raw.strengthTwo != null) {
|
||||
raw.strengthTwo = null;
|
||||
}
|
||||
|
||||
root.classList.toggle("mxd-lora-off", raw.on !== true);
|
||||
toggle.setAttribute("aria-checked", raw.on === true ? "true" : "false");
|
||||
|
||||
const label = String(raw.lora || "None");
|
||||
nameText.textContent = label;
|
||||
nameBtn.title = label;
|
||||
|
||||
const hasLora = !!raw.lora && raw.lora !== "None";
|
||||
infoBtn.style.display = hasLora ? "" : "none";
|
||||
|
||||
// In dual mode both steppers show; otherwise only the single (clip slot).
|
||||
modelStrength.input.parentElement.style.display = dualActive ? "" : "none";
|
||||
modelStrength.render();
|
||||
clipStrength.render();
|
||||
|
||||
modelStrength.input.classList.toggle("mxd-lora-out-of-range", outOfRange(raw.strength ?? 1));
|
||||
clipStrength.input.classList.toggle(
|
||||
"mxd-lora-out-of-range",
|
||||
outOfRange(dualActive ? (raw.strengthTwo ?? 1) : (raw.strength ?? 1)),
|
||||
);
|
||||
}
|
||||
|
||||
function loadLoraInfo(force = false) {
|
||||
if (!loraInfoPromise || force) {
|
||||
const hasLora = raw.lora && raw.lora !== "None";
|
||||
loraInfoPromise = (hasLora
|
||||
? LORA_INFO_SERVICE.getInfo(raw.lora, force, true)
|
||||
: Promise.resolve(null)
|
||||
).then((info) => {
|
||||
loraInfo = info;
|
||||
render();
|
||||
});
|
||||
}
|
||||
return loraInfoPromise;
|
||||
}
|
||||
|
||||
toggle.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
raw.on = raw.on !== true;
|
||||
render();
|
||||
refreshPowerLoraDom(node);
|
||||
});
|
||||
|
||||
nameBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
showLoraChooser(e, (value) => {
|
||||
if (typeof value === "string") {
|
||||
raw.lora = value;
|
||||
loraInfo = null;
|
||||
loadLoraInfo(true);
|
||||
render();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
infoBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
showInfoDialog();
|
||||
});
|
||||
|
||||
removeBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
node.removeWidget(widget);
|
||||
refreshPowerLoraDom(node);
|
||||
node.setDirtyCanvas(true, true);
|
||||
});
|
||||
|
||||
function showInfoDialog() {
|
||||
if (!raw.lora || raw.lora === "None") return;
|
||||
const dialog = new MxdLoraInfoDialog(raw.lora).show();
|
||||
dialog.addEventListener("close", (e) => {
|
||||
if (e.detail?.dirty) loadLoraInfo(true);
|
||||
});
|
||||
}
|
||||
|
||||
widget = addRowWidget(node, {
|
||||
name,
|
||||
element: root,
|
||||
height: ROW_HEIGHT,
|
||||
getState: () => state,
|
||||
// Mirrors PowerLoraLoaderWidget.serializeValue exactly: `strengthTwo` is
|
||||
// OMITTED (not null) when strengths aren't split, because the Python side
|
||||
// tests `"strengthTwo" in lora` when building its lora list output.
|
||||
serializeState: () => {
|
||||
const out = { ...raw };
|
||||
if (!dual || !isDualActive()) {
|
||||
delete out.strengthTwo;
|
||||
} else {
|
||||
raw.strengthTwo = raw.strengthTwo ?? 1;
|
||||
out.strengthTwo = raw.strengthTwo;
|
||||
}
|
||||
return out;
|
||||
},
|
||||
setState: (v) => {
|
||||
if (v && typeof v === "object") {
|
||||
Object.assign(raw, v);
|
||||
}
|
||||
loraInfo = null;
|
||||
loraInfoPromise = null;
|
||||
loadLoraInfo();
|
||||
render();
|
||||
},
|
||||
});
|
||||
|
||||
// Keep the API the canvas rows expose, so the node class and its slot menus
|
||||
// work against either implementation.
|
||||
widget.setLora = (lora) => {
|
||||
raw.lora = lora;
|
||||
loraInfo = null;
|
||||
loadLoraInfo(true);
|
||||
render();
|
||||
};
|
||||
widget.showLoraInfoDialog = showInfoDialog;
|
||||
widget.__mxdRefresher = registerRefresher(node, render);
|
||||
|
||||
render();
|
||||
loadLoraInfo();
|
||||
return widget;
|
||||
}
|
||||
|
||||
// --- header row ----------------------------------------------------------
|
||||
|
||||
// "[toggle all] Toggle All Model Strength/Clip"
|
||||
export function addDomHeaderRow(node, { dual = false } = {}) {
|
||||
const root = el("div", "mxd-lora-row mxd-lora-header");
|
||||
const toggle = button("mxd-lora-toggle", root);
|
||||
toggle.setAttribute("role", "switch");
|
||||
const label = el("span", "mxd-lora-header-label", root);
|
||||
label.textContent = "Toggle All";
|
||||
// Invisible stand-ins for each row's info/remove buttons, so the captions
|
||||
// below line up with the strength steppers under the same flex math.
|
||||
el("span", "mxd-lora-icon mxd-lora-spacer", root);
|
||||
el("span", "mxd-lora-icon mxd-lora-spacer", root);
|
||||
const modelCol = el("span", "mxd-lora-header-col", root);
|
||||
const strengthCol = el("span", "mxd-lora-header-col", root);
|
||||
|
||||
function render() {
|
||||
const anyRows = node.hasLoraWidgets?.() === true;
|
||||
root.style.display = anyRows ? "" : "none";
|
||||
if (!anyRows) return;
|
||||
|
||||
const dualActive = dual && node.isShowingSeparateStrengths?.() === true;
|
||||
const all = node.allLorasState?.();
|
||||
toggle.setAttribute("aria-checked", all === true ? "true" : all === null ? "mixed" : "false");
|
||||
modelCol.style.display = dualActive ? "" : "none";
|
||||
modelCol.textContent = "Model";
|
||||
strengthCol.textContent = dualActive ? "Clip" : "Strength";
|
||||
}
|
||||
|
||||
toggle.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
node.toggleAllLoras?.();
|
||||
refreshPowerLoraDom(node);
|
||||
});
|
||||
|
||||
// Same `value` the canvas PowerLoraLoaderHeaderWidget carries, so it occupies
|
||||
// the same slot in widgets_values.
|
||||
const widget = addRowWidget(node, {
|
||||
name: "PowerLoraLoaderHeaderWidget",
|
||||
element: root,
|
||||
height: HEADER_HEIGHT,
|
||||
getState: () => ({ type: "PowerLoraLoaderHeaderWidget" }),
|
||||
setState: () => {},
|
||||
});
|
||||
|
||||
widget.__mxdRefresher = registerRefresher(node, render);
|
||||
render();
|
||||
return widget;
|
||||
}
|
||||
|
||||
// --- "+ Add Lora" --------------------------------------------------------
|
||||
|
||||
export function addDomAddLoraButton(node, onPick) {
|
||||
const root = el("button", "mxd-lora-add");
|
||||
root.type = "button";
|
||||
root.textContent = "+ Add Lora";
|
||||
root.addEventListener("pointerdown", (e) => e.stopPropagation());
|
||||
root.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
showLoraChooser(e, (value) => {
|
||||
if (typeof value === "string" && value !== "NONE") {
|
||||
onPick(value);
|
||||
growNodeToFit(node);
|
||||
refreshPowerLoraDom(node);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Empty-string value matches the canvas MxdBetterButtonWidget it replaces.
|
||||
return addRowWidget(node, {
|
||||
name: "mxd_add_lora_button",
|
||||
element: root,
|
||||
height: BUTTON_HEIGHT,
|
||||
getState: () => "",
|
||||
setState: () => {},
|
||||
});
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// 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 { nodeDrawWidth } from "../lib/mxd_nodes2.js";
|
||||
import { nodeDrawWidth, redrawWidget } from "../lib/mxd_nodes2.js";
|
||||
import { drawNumberWidgetPart, isLowQuality } from "../lib/mxd_utils_canvas.js";
|
||||
import {
|
||||
MxdPowerLoraLoaderBase,
|
||||
@@ -19,8 +19,6 @@ class MxdPowerLoraLoader extends MxdPowerLoraLoaderBase {
|
||||
static title = NODE_TYPE;
|
||||
static type = NODE_TYPE;
|
||||
static comfyClass = NODE_TYPE;
|
||||
// This node's "Show Strengths" property can split model/clip strengths.
|
||||
static loraRowSupportsDual = true;
|
||||
|
||||
static [PROP_LABEL_SHOW_STRENGTHS_STATIC] = {
|
||||
type: "combo",
|
||||
@@ -192,6 +190,7 @@ class PowerLoraLoaderWidget extends PowerLoraBaseWidget {
|
||||
let prop = isTwo ? "strengthTwo" : "strength";
|
||||
this.haveMouseMovedStrength = true;
|
||||
this.value[prop] = (this.value[prop] ?? 1) + event.deltaX * 0.05;
|
||||
redrawWidget(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +206,15 @@ class PowerLoraLoaderWidget extends PowerLoraBaseWidget {
|
||||
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);
|
||||
canvas.prompt(
|
||||
"Value",
|
||||
this.value[prop],
|
||||
(v) => {
|
||||
this.value[prop] = Number(v);
|
||||
redrawWidget(this);
|
||||
},
|
||||
event,
|
||||
);
|
||||
}
|
||||
|
||||
stepStrength(direction, isTwo = false) {
|
||||
@@ -215,6 +222,7 @@ class PowerLoraLoaderWidget extends PowerLoraBaseWidget {
|
||||
let prop = isTwo ? "strengthTwo" : "strength";
|
||||
let strength = (this.value[prop] ?? 1) + step * direction;
|
||||
this.value[prop] = Math.round(strength * 100) / 100;
|
||||
redrawWidget(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user