Files
2026-03-14 22:59:56 +00:00

893 lines
33 KiB
JavaScript

import { app } from "../../../scripts/app.js";
import { loraPatternMatcher } from "./lora_pattern_matcher.js";
// From rgthree/src_web/comfyui/utils_canvas.ts
export function fitString(ctx, str, maxWidth) {
let width = ctx.measureText(str).width;
const ellipsis = "…";
const ellipsisWidth = ctx.measureText(ellipsis).width;
if (width <= maxWidth || width <= ellipsisWidth) {
return str;
}
let len = str.length;
while (width >= maxWidth - ellipsisWidth && len-- > 0) {
str = str.substring(0, len);
width = ctx.measureText(str).width;
}
return str + ellipsis;
}
export function isLowQuality() {
return app.canvas.ds.scale <= 0.5;
}
export function drawRoundedRectangle(ctx, pos, size, borderRadius) {
const lowQuality = isLowQuality();
ctx.save();
ctx.strokeStyle = LiteGraph.WIDGET_OUTLINE_COLOR;
ctx.fillStyle = LiteGraph.WIDGET_BGCOLOR;
ctx.beginPath();
ctx.roundRect(pos[0], pos[1], size[0], size[1], lowQuality ? [0] : [borderRadius]);
ctx.fill();
if (!lowQuality) {
ctx.stroke();
}
ctx.restore();
}
export function drawTogglePart(ctx, pos, height, value) {
const lowQuality = isLowQuality();
ctx.save();
const toggleRadius = height * 0.36;
const toggleBgWidth = height * 1.5;
if (!lowQuality) {
ctx.beginPath();
ctx.roundRect(pos[0] + 4, pos[1] + 4, toggleBgWidth - 8, height - 8, [height * 0.5]);
ctx.globalAlpha = app.canvas.editor_alpha * 0.25;
ctx.fillStyle = "rgba(255,255,255,0.45)";
ctx.fill();
ctx.globalAlpha = app.canvas.editor_alpha;
}
ctx.fillStyle = value === true ? "#89B" : "#888";
const toggleX = lowQuality || value === false ? pos[0] + height * 0.5 : value === true ? pos[0] + height : pos[0] + height * 0.75;
ctx.beginPath();
ctx.arc(toggleX, pos[1] + height * 0.5, toggleRadius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
return [pos[0], toggleBgWidth];
}
export function drawWidgetButton(ctx, pos, size, text, isMouseDownedAndOver) {
const borderRadius = isLowQuality() ? 0 : 4;
ctx.save();
if (!isLowQuality() && !isMouseDownedAndOver) {
drawRoundedRectangle(ctx, [pos[0] + 1, pos[1] + 1], [size[0] - 2, size[1]], borderRadius);
}
drawRoundedRectangle(ctx, [pos[0], pos[1] + (isMouseDownedAndOver ? 1 : 0)], size, borderRadius);
if (!isLowQuality()) {
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR;
ctx.fillText(text, pos[0] + size[0] / 2, pos[1] + size[1] / 2 + (isMouseDownedAndOver ? 1 : 0));
}
ctx.restore();
}
// From rgthree/src_web/comfyui/utils_widgets.ts
export class RgthreeBaseWidget {
constructor(name) {
this.name = name;
this.type = "custom";
this.options = {};
this.y = 0;
this.last_y = 0;
this.mouseDowned = null;
this.isMouseDownedAndOver = false;
this.hitAreas = {};
this.downedHitAreasForMove = [];
this.downedHitAreasForClick = [];
}
serializeValue(node, index) {
return this.value;
}
clickWasWithinBounds(pos, bounds) {
let xStart = bounds[0];
let xEnd = xStart + (bounds.length > 2 ? bounds[2] : bounds[1]);
const clickedX = pos[0] >= xStart && pos[0] <= xEnd;
if (bounds.length === 2) {
return clickedX;
}
return clickedX && pos[1] >= bounds[1] && pos[1] <= bounds[1] + bounds[3];
}
mouse(event, pos, node) {
if (event.type == "pointerdown") {
this.mouseDowned = [...pos];
this.isMouseDownedAndOver = true;
this.downedHitAreasForMove.length = 0;
this.downedHitAreasForClick.length = 0;
let anyHandled = false;
for (const part of Object.values(this.hitAreas)) {
if (this.clickWasWithinBounds(pos, part.bounds)) {
if (part.onMove) {
this.downedHitAreasForMove.push(part);
}
if (part.onClick) {
this.downedHitAreasForClick.push(part);
}
if (part.onDown) {
const thisHandled = part.onDown.apply(this, [event, pos, node, part]);
anyHandled = anyHandled || thisHandled == true;
}
part.wasMouseClickedAndIsOver = true;
}
}
return this.onMouseDown(event, pos, node) || anyHandled;
}
if (event.type == "pointerup") {
if (!this.mouseDowned) return true;
this.downedHitAreasForMove.length = 0;
const wasMouseDownedAndOver = this.isMouseDownedAndOver;
this.cancelMouseDown();
let anyHandled = false;
for (const part of Object.values(this.hitAreas)) {
if (part.onUp && this.clickWasWithinBounds(pos, part.bounds)) {
const thisHandled = part.onUp.apply(this, [event, pos, node, part]);
anyHandled = anyHandled || thisHandled == true;
}
part.wasMouseClickedAndIsOver = false;
}
for (const part of this.downedHitAreasForClick) {
if (this.clickWasWithinBounds(pos, part.bounds)) {
const thisHandled = part.onClick.apply(this, [event, pos, node, part]);
anyHandled = anyHandled || thisHandled == true;
}
}
this.downedHitAreasForClick.length = 0;
if (wasMouseDownedAndOver) {
const thisHandled = this.onMouseClick(event, pos, node);
anyHandled = anyHandled || thisHandled == true;
}
return this.onMouseUp(event, pos, node) || anyHandled;
}
if (event.type == "pointermove") {
this.isMouseDownedAndOver = !!this.mouseDowned;
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)) {
this.isMouseDownedAndOver = false;
}
for (const part of Object.values(this.hitAreas)) {
if (this.downedHitAreasForMove.includes(part)) {
part.onMove.apply(this, [event, pos, node, part]);
}
if (this.downedHitAreasForClick.includes(part)) {
part.wasMouseClickedAndIsOver = this.clickWasWithinBounds(pos, part.bounds);
}
}
// Always call onMouseMove for hover detection
this.onMouseMove(event, pos, node);
return true;
}
return false;
}
cancelMouseDown() {
this.mouseDowned = null;
this.isMouseDownedAndOver = false;
this.downedHitAreasForMove.length = 0;
}
onMouseDown(event, pos, node) { return false; }
onMouseUp(event, pos, node) { return false; }
onMouseClick(event, pos, node) { return false; }
onMouseMove(event, pos, node) { return true; }
}
export class RgthreeBetterButtonWidget extends RgthreeBaseWidget {
constructor(name, callback, label) {
super(name);
this.callback = callback;
this.label = label || name;
this.value = "";
}
draw(ctx, node, width, y, height) {
drawWidgetButton(ctx, [15, y], [width - 30, height], this.label, this.isMouseDownedAndOver);
}
onMouseClick(event, pos, node) {
return this.callback(event, pos, node);
}
}
export class RgthreeDividerWidget extends RgthreeBaseWidget {
constructor(options) {
super("divider");
this.value = {};
this.options = { serialize: false };
this.widgetOptions = {
marginTop: 7,
marginBottom: 7,
marginLeft: 15,
marginRight: 15,
color: LiteGraph.WIDGET_OUTLINE_COLOR,
thickness: 1,
...(options || {}),
};
}
draw(ctx, node, width, y, h) {
if (this.widgetOptions.thickness) {
ctx.strokeStyle = this.widgetOptions.color;
const x = this.widgetOptions.marginLeft;
const y2 = y + this.widgetOptions.marginTop;
const w = width - this.widgetOptions.marginLeft - this.widgetOptions.marginRight;
ctx.stroke(new Path2D(`M ${x} ${y2} h ${w}`));
}
}
computeSize(width) {
return [width, this.widgetOptions.marginTop + this.widgetOptions.marginBottom + this.widgetOptions.thickness];
}
}
const STRENGTH_WIDTH = 60;
const ICON_WIDTH = 20;
const MARGIN = 10;
export class StrengthCopyWidget extends RgthreeBaseWidget {
constructor() {
super("StrengthCopyWidget");
this.value = {};
this.hitAreas = {
output_both_toggle: { bounds: [0, 0] },
high_to_low_button: { bounds: [0, 0] },
low_to_high_button: { bounds: [0, 0] },
};
this.highToLowPressed = false;
this.lowToHighPressed = false;
}
draw(ctx, node, w, posY, height) {
// Ensure property exists (off by default)
if (!node.properties) node.properties = {};
if (node.properties['output_both'] === undefined) node.properties['output_both'] = false;
// Constants
const margin = 20;
const buttonWidth = 25;
const buttonHeight = height;
const buttonSpacing = 36;
const rightOffset = 25;
const innerMargin = 8;
// Calculated values
const midY = posY + height * 0.5;
const totalButtonsWidth = (buttonWidth * 2) + buttonSpacing;
ctx.save();
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR;
// Draw "Output both" toggle
const toggleSpacing = 15; // Extra spacing between toggle and buttons
let posX = w - margin - totalButtonsWidth - rightOffset - toggleSpacing;
const outputBothValue = node.properties['output_both'] || false;
// Draw label and toggle from right to left
const toggleBounds = drawTogglePart(ctx, [posX - height * 1.5, posY], height, outputBothValue);
this.hitAreas['output_both_toggle'].bounds = toggleBounds;
const labelText = "Output both";
ctx.fillText(labelText, posX - height * 1.5 - ctx.measureText(labelText).width - innerMargin, midY);
// Adjust rightX for buttons (they stay in same position)
const rightX = w - margin - totalButtonsWidth - rightOffset;
// Draw ">" button (high to low)
const highToLowX = rightX;
drawWidgetButton(ctx, [highToLowX, posY], [buttonWidth, buttonHeight], ">", this.highToLowPressed);
this.hitAreas['high_to_low_button'].bounds = [highToLowX, buttonWidth];
// Draw "<" button (low to high)
const lowToHighX = rightX + buttonWidth + buttonSpacing;
drawWidgetButton(ctx, [lowToHighX, posY], [buttonWidth, buttonHeight], "<", this.lowToHighPressed);
this.hitAreas['low_to_high_button'].bounds = [lowToHighX, buttonWidth];
ctx.restore();
}
onMouseDown(event, pos, node) {
// Ensure property exists
if (!node.properties) node.properties = {};
if (node.properties['output_both'] === undefined) node.properties['output_both'] = false;
if (this.clickWasWithinBounds(pos, this.hitAreas.output_both_toggle.bounds)) {
node.properties['output_both'] = !node.properties['output_both'];
// Also sync to hidden widget for Python to read
const outputBothWidget = node.widgets.find(w => w.name === "output_both");
if (outputBothWidget) {
outputBothWidget.value = node.properties['output_both'];
}
this.cancelMouseDown();
node.setDirtyCanvas(true, true);
return true;
}
if (this.clickWasWithinBounds(pos, this.hitAreas.high_to_low_button.bounds)) {
this.highToLowPressed = true;
node.setDirtyCanvas(true, true);
return true;
}
if (this.clickWasWithinBounds(pos, this.hitAreas.low_to_high_button.bounds)) {
this.lowToHighPressed = true;
node.setDirtyCanvas(true, true);
return true;
}
return false;
}
onMouseUp(event, pos, node) {
if (this.highToLowPressed && this.clickWasWithinBounds(pos, this.hitAreas.high_to_low_button.bounds)) {
this.copyHighToLowStrengths(node);
this.cancelMouseDown();
node.setDirtyCanvas(true, true);
}
if (this.lowToHighPressed && this.clickWasWithinBounds(pos, this.hitAreas.low_to_high_button.bounds)) {
this.copyLowToHighStrengths(node);
this.cancelMouseDown();
node.setDirtyCanvas(true, true);
}
this.highToLowPressed = false;
this.lowToHighPressed = false;
return super.onMouseUp(event, pos, node);
}
copyHighToLowStrengths(node) {
// Copy high strength values to low strength values (> button)
for (const widget of node.widgets || []) {
if (widget.name?.startsWith("lora_") && widget.value) {
widget.value.low_strength = widget.value.strength || 1;
}
}
}
copyLowToHighStrengths(node) {
// Copy low strength values to high strength values (< button)
for (const widget of node.widgets || []) {
if (widget.name?.startsWith("lora_") && widget.value) {
widget.value.strength = widget.value.low_strength || 1;
// Also update strengthTwo if in model/clip mode
if (widget.showModelAndClip && widget.value.strengthTwo !== undefined) {
widget.value.strengthTwo = widget.value.low_strength || 1;
}
}
}
}
serializeValue(node, index) {
// Ensure output_both property exists and is synchronized
if (!node.properties) node.properties = {};
if (node.properties['output_both'] === undefined) node.properties['output_both'] = false;
// Return the value to ensure it's serialized
node.properties['output_both'] = node.properties['output_both'] || false;
return {};
}
computeSize(width) {
return [width, LiteGraph.NODE_WIDGET_HEIGHT];
}
}
export class CombinedOptionsWidget extends RgthreeBaseWidget {
constructor() {
super("CombinedOptionsWidget");
this.value = {};
this.optionsHitAreas = {
low_mem_toggle: { bounds: [0, 0] },
merge_loras_toggle: { bounds: [0, 0] },
overwrite_toggle: { bounds: [0, 0] },
};
this.copyHitAreas = {
high_to_low_button: { bounds: [0, 0] },
low_to_high_button: { bounds: [0, 0] },
};
this.highToLowPressed = false;
this.lowToHighPressed = false;
}
serializeValue(node, index) {
if (!node.properties) node.properties = {};
if (node.properties['low_mem_load'] === undefined) node.properties['low_mem_load'] = true;
if (node.properties['merge_loras'] === undefined) node.properties['merge_loras'] = false;
if (node.properties['overwrite_duplicates'] === undefined) node.properties['overwrite_duplicates'] = false;
const value = {
low_mem_load: node.properties['low_mem_load'] || false,
merge_loras: node.properties['merge_loras'] === true ? true : false,
overwrite_duplicates: node.properties['overwrite_duplicates'] || false
};
// Update node properties to ensure they're in sync
node.properties['low_mem_load'] = value.low_mem_load;
node.properties['merge_loras'] = value.merge_loras;
node.properties['overwrite_duplicates'] = value.overwrite_duplicates;
return value;
}
draw(ctx, node, w, posY, height) {
// Ensure properties exist
if (!node.properties) node.properties = {};
if (node.properties['low_mem_load'] === undefined) node.properties['low_mem_load'] = true;
if (node.properties['merge_loras'] === undefined) node.properties['merge_loras'] = false;
if (node.properties['overwrite_duplicates'] === undefined) node.properties['overwrite_duplicates'] = false;
// Calculate column widths
const optionsWidth = w * 0.65;
const copyWidth = w * 0.35;
// Draw divider between columns
ctx.strokeStyle = LiteGraph.WIDGET_OUTLINE_COLOR;
ctx.beginPath();
ctx.moveTo(optionsWidth, posY);
ctx.lineTo(optionsWidth, posY + height);
ctx.stroke();
// Draw OptionsWidget on the left (70% width)
this.drawOptionsSection(ctx, node, optionsWidth, posY, height);
// Draw StrengthCopyWidget on the right (30% width)
this.drawCopySection(ctx, node, optionsWidth, copyWidth, posY, height);
}
drawOptionsSection(ctx, node, w, posY, height) {
// Constants
const margin = 20;
const innerMargin = margin * 0.33;
let posX = 10;
// Calculated values
const midY = posY + height * 0.5;
// Widget state
const lowMemValue = node.properties['low_mem_load'] || false;
const mergeValue = node.properties['merge_loras'] === true ? true : false;
const overwriteValue = node.properties['overwrite_duplicates'] || false;
ctx.save();
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR;
// Draw Low Mem toggle
ctx.fillText("Low Mem", posX, midY);
posX += ctx.measureText("Low Mem").width + innerMargin;
let bounds = drawTogglePart(ctx, [posX, posY], height, lowMemValue);
this.optionsHitAreas['low_mem_toggle'].bounds = bounds;
posX += bounds[1] + innerMargin * 3;
// Draw Merge toggle
ctx.fillText("Merge", posX, midY);
posX += ctx.measureText("Merge").width + innerMargin;
bounds = drawTogglePart(ctx, [posX, posY], height, mergeValue);
this.optionsHitAreas['merge_loras_toggle'].bounds = bounds;
posX += bounds[1] + innerMargin * 3;
// Draw Overwrite toggle
ctx.fillText("Overwrite", posX, midY);
posX += ctx.measureText("Overwrite").width + innerMargin;
bounds = drawTogglePart(ctx, [posX, posY], height, overwriteValue);
this.optionsHitAreas['overwrite_toggle'].bounds = bounds;
ctx.restore();
}
drawCopySection(ctx, node, xOffset, w, posY, height) {
// Constants
const margin = 20;
const buttonWidth = 25;
const buttonHeight = height;
const buttonSpacing = 36;
const rightOffset = 25;
// Calculated values
const midY = posY + height * 0.5;
const totalButtonsWidth = (buttonWidth * 2) + buttonSpacing;
const rightX = xOffset + w - margin - rightOffset;
ctx.save();
// Draw ">" button (high to low)
const highToLowX = rightX - totalButtonsWidth;
drawWidgetButton(ctx, [highToLowX, posY], [buttonWidth, buttonHeight], ">", this.highToLowPressed);
this.copyHitAreas['high_to_low_button'].bounds = [highToLowX, buttonWidth];
// Draw "<" button (low to high)
const lowToHighX = rightX - totalButtonsWidth + buttonWidth + buttonSpacing;
drawWidgetButton(ctx, [lowToHighX, posY], [buttonWidth, buttonHeight], "<", this.lowToHighPressed);
this.copyHitAreas['low_to_high_button'].bounds = [lowToHighX, buttonWidth];
ctx.restore();
}
onMouseDown(event, pos, node) {
// Check options toggles
if (this.clickWasWithinBounds(pos, this.optionsHitAreas.low_mem_toggle.bounds)) {
node.properties['low_mem_load'] = !node.properties['low_mem_load'];
this.cancelMouseDown();
node.setDirtyCanvas(true, true);
return true;
}
if (this.clickWasWithinBounds(pos, this.optionsHitAreas.merge_loras_toggle.bounds)) {
// Properly toggle the merge_loras property
const currentValue = node.properties['merge_loras'];
node.properties['merge_loras'] = currentValue === false ? true : false;
this.cancelMouseDown();
node.setDirtyCanvas(true, true);
return true;
}
if (this.clickWasWithinBounds(pos, this.optionsHitAreas.overwrite_toggle.bounds)) {
node.properties['overwrite_duplicates'] = !node.properties['overwrite_duplicates'];
this.cancelMouseDown();
node.setDirtyCanvas(true, true);
return true;
}
// Check copy buttons
if (this.clickWasWithinBounds(pos, this.copyHitAreas.high_to_low_button.bounds)) {
this.highToLowPressed = true;
node.setDirtyCanvas(true, true);
return true;
}
if (this.clickWasWithinBounds(pos, this.copyHitAreas.low_to_high_button.bounds)) {
this.lowToHighPressed = true;
node.setDirtyCanvas(true, true);
return true;
}
return false;
}
onMouseUp(event, pos, node) {
if (this.highToLowPressed && this.clickWasWithinBounds(pos, this.copyHitAreas.high_to_low_button.bounds)) {
this.copyHighToLowStrengths(node);
this.cancelMouseDown();
node.setDirtyCanvas(true, true);
}
if (this.lowToHighPressed && this.clickWasWithinBounds(pos, this.copyHitAreas.low_to_high_button.bounds)) {
this.copyLowToHighStrengths(node);
this.cancelMouseDown();
node.setDirtyCanvas(true, true);
}
this.highToLowPressed = false;
this.lowToHighPressed = false;
return super.onMouseUp(event, pos, node);
}
copyHighToLowStrengths(node) {
// Copy high strength values to low strength values (> button)
for (const widget of node.widgets || []) {
if (widget.name?.startsWith("lora_") && widget.value) {
widget.value.low_strength = widget.value.strength || 1;
}
}
}
copyLowToHighStrengths(node) {
// Copy low strength values to high strength values (< button)
for (const widget of node.widgets || []) {
if (widget.name?.startsWith("lora_") && widget.value) {
widget.value.strength = widget.value.low_strength || 1;
// Also update strengthTwo if in model/clip mode
if (widget.showModelAndClip && widget.value.strengthTwo !== undefined) {
widget.value.strengthTwo = widget.value.low_strength || 1;
}
}
}
}
computeSize(width) {
return [width, LiteGraph.NODE_WIDGET_HEIGHT * 0.7];
}
}
export class PowerLoraLoaderHeaderWidget extends RgthreeBaseWidget {
constructor() {
super("PowerLoraLoaderHeaderWidget");
this.value = { type: "PowerLoraLoaderHeaderWidget" };
this.hitAreas = {
toggle: { bounds: [0, 0], onDown: this.onToggleDown },
};
}
draw(ctx, node, w, y, h) {
const allLoraState = node.allLorasState();
let posX = MARGIN;
this.hitAreas.toggle.bounds = drawTogglePart(ctx, [posX, y], h, allLoraState);
posX += this.hitAreas.toggle.bounds[1] + MARGIN;
ctx.textAlign = "left";
ctx.fillStyle = "white";
ctx.textBaseline = "middle";
ctx.fillText("Toggle All", posX, y + h / 2);
ctx.textAlign = "right";
let rposX = w - MARGIN;
ctx.fillText("*", rposX - ICON_WIDTH / 2, y + h / 2);
rposX -= ICON_WIDTH + MARGIN;
ctx.fillText("Low", rposX, y + h / 2); // Changed from centered to right-aligned
rposX -= STRENGTH_WIDTH + MARGIN;
ctx.fillText("High", rposX, y + h / 2); // Changed from centered to right-aligned
}
onToggleDown(event, pos, node) {
node.toggleAllLoras();
this.cancelMouseDown();
return true;
}
}
export class PowerLoraLoaderWidget extends RgthreeBaseWidget {
constructor(name, showLoraChooser) {
super(name);
this.showLoraChooser = showLoraChooser;
this.value = { on: true, lora: "None", strength: 1, low_strength: 1, is_low: false, low_active: true, use_for_both: false, low_only: false, high_only: false };
this.haveMouseMovedStrength = false;
this.hitAreas = {
toggle: { bounds: [0, 0], onDown: this.onToggleDown },
lora: { bounds: [0, 0], onClick: this.onLoraClick },
strength: { bounds: [0, 0], onClick: this.onStrengthClick, onMove: this.onStrengthAnyMove },
low_strength: { bounds: [0, 0], onClick: this.onLowStrengthClick, onMove: this.onLowStrengthAnyMove },
low_variant_icon: { bounds: [0, 0] },
};
}
set value(v) {
this._value = v;
if (typeof this._value !== "object") {
this._value = { on: true, lora: "None", strength: 1, low_strength: 1, is_low: false, low_active: true };
}
// Ensure low_strength is always initialized
if (this._value.low_strength === undefined) {
this._value.low_strength = 1;
}
// Ensure low_active is always initialized (default to true for backward compatibility)
if (this._value.low_active === undefined) {
this._value.low_active = true;
}
// Ensure use_for_both is always initialized (default to false)
if (this._value.use_for_both === undefined) {
this._value.use_for_both = false;
}
// Ensure low_only is always initialized (default to false)
if (this._value.low_only === undefined) {
this._value.low_only = false;
}
// Ensure high_only is always initialized (default to false)
if (this._value.high_only === undefined) {
this._value.high_only = false;
}
}
get value() {
return this._value;
}
draw(ctx, node, w, y, h) {
drawRoundedRectangle(ctx, [MARGIN, y], [w - MARGIN * 2, h], h * 0.5);
if (!this.value.on) {
ctx.globalAlpha = 0.5;
}
let posX = MARGIN;
this.hitAreas.toggle.bounds = drawTogglePart(ctx, [posX, y], h, this.value.on);
posX += this.hitAreas.toggle.bounds[1] + MARGIN;
let rposX = w - MARGIN;
if (this.value.use_for_both) {
ctx.fillStyle = "#9370DB"; // Purple for "both" state
} else if (this.value.low_only) {
ctx.fillStyle = "#4169E1"; // Blue for "low only" state
} else if (this.value.high_only) {
ctx.fillStyle = "#DC143C"; // Red for "high only" state
} else if (this.value.is_low) {
ctx.fillStyle = this.value.low_active ? "lime" : "orange";
} else {
ctx.fillStyle = "#555";
}
ctx.beginPath();
ctx.arc(rposX - ICON_WIDTH / 2, y + h / 2, h * 0.2, 0, Math.PI * 2);
ctx.fill();
// Store the bounds for the low variant icon
this.hitAreas.low_variant_icon.bounds = [
rposX - ICON_WIDTH,
y,
ICON_WIDTH,
h
];
rposX -= ICON_WIDTH + MARGIN;
this.hitAreas.low_strength.bounds = [rposX - STRENGTH_WIDTH, STRENGTH_WIDTH];
ctx.textAlign = "right";
ctx.fillStyle = "white";
ctx.textBaseline = "middle";
ctx.fillText(this.value.low_strength.toFixed(2), rposX, y + h / 2);
rposX -= STRENGTH_WIDTH + MARGIN;
this.hitAreas.strength.bounds = [rposX - STRENGTH_WIDTH, STRENGTH_WIDTH];
ctx.fillText(this.value.strength.toFixed(2), rposX, y + h / 2);
rposX -= STRENGTH_WIDTH + MARGIN;
const loraWidth = rposX - posX;
this.hitAreas.lora.bounds = [posX, loraWidth];
ctx.textAlign = "left";
ctx.fillText(fitString(ctx, this.value.lora, loraWidth), posX, y + h / 2);
ctx.globalAlpha = 1;
}
onToggleDown(event, pos, node) {
this.value.on = !this.value.on;
this.cancelMouseDown();
node.setDirtyCanvas(true, true);
return true;
}
setLora(lora) {
// Store the original lora path/name as received from the picker
this.value.lora = lora;
// Reset all flags when a new LoRA is selected
this.value.use_for_both = false;
this.value.low_only = false;
this.value.high_only = false;
this.checkLowLoraVariant(lora);
}
checkLowLoraVariant(loraName) {
if (!loraName || loraName === "None") {
this.value.is_low = false;
this.value.low_variant_name = null;
return;
}
const allLoras = this.parent.lorasCache || [];
if (!allLoras.length) {
return;
}
// Use the full loraName for pattern matching
// The pattern matcher now handles full paths properly
const result = loraPatternMatcher.checkLowLoraVariant(loraName, allLoras);
this.value.is_low = result.found;
this.value.low_variant_name = result.variantName;
}
onLoraClick(event, pos, node) {
this.showLoraChooser(event, (lora) => {
if (lora) {
this.setLora(lora);
}
}, node);
}
onStrengthClick(event, pos, node) {
if (this.haveMouseMovedStrength) return;
const canvas = app.canvas;
canvas.prompt("Enter high strength", this.value.strength, (v) => {
this.value.strength = parseFloat(v);
}, event);
}
onLowStrengthClick(event, pos, node) {
if (this.haveMouseMovedStrength) return;
const canvas = app.canvas;
canvas.prompt("Enter low strength", this.value.low_strength, (v) => {
this.value.low_strength = parseFloat(v);
}, event);
}
onStrengthAnyMove(event, pos, node) {
this.haveMouseMovedStrength = true;
this.value.strength += event.deltaX * 0.01;
}
onLowStrengthAnyMove(event, pos, node) {
this.haveMouseMovedStrength = true;
this.value.low_strength += event.deltaX * 0.01;
}
onMouseUp(event, pos, node) {
super.onMouseUp(event, pos, node);
this.haveMouseMovedStrength = false;
}
showLoraInfoDialog(loraType = 'auto') {
// Determine which LoRA to show info for based on loraType parameter
let loraToShow = this.value.lora;
if (loraType === 'auto') {
// Auto-detect: show low variant if it exists, otherwise show main LoRA
loraToShow = this.value.is_low && this.value.low_variant_name ?
this.value.low_variant_name : this.value.lora;
} else if (loraType === 'low' && this.value.is_low && this.value.low_variant_name) {
// Explicitly show low variant
loraToShow = this.value.low_variant_name;
} else if (loraType === 'high') {
// Explicitly show high variant (main LoRA)
loraToShow = this.value.lora;
}
// Import and show the LoRA info dialog
import("./dialog_info.js").then(({ WanLoraInfoDialog }) => {
const infoDialog = new WanLoraInfoDialog(loraToShow).show();
// Store the item type for use in preview generation (this is for LoRA widgets)
infoDialog.itemType = 'loras';
console.log(`[Widgets Debug] Created LoRA widget dialog with itemType: ${infoDialog.itemType}`);
infoDialog.addEventListener("close", ((e) => {
if (e.detail.dirty) {
// Dialog was modified, could trigger refresh if needed
console.log("LoRA info was modified");
}
}));
}).catch(error => {
// Fallback to simple alert if dialog fails to load
console.error("Failed to load LoRA info dialog:", error);
const loraInfo = `LoRA Information:
File: ${loraToShow}`;
alert(loraInfo);
});
}
serializeValue(node, index) {
const v = { ...this.value };
// Ensure low_strength is always included
if (v.low_strength === undefined) {
v.low_strength = 1;
}
// Ensure is_low and low_variant_name are preserved
v.is_low = this.value.is_low || false;
v.low_variant_name = this.value.low_variant_name || null;
// Ensure low_active is preserved (default to true for backward compatibility)
v.low_active = this.value.low_active !== undefined ? this.value.low_active : true;
return v;
}
}
export function moveArrayItem(arr, itemOrFrom, to) {
const from = typeof itemOrFrom === "number" ? itemOrFrom : arr.indexOf(itemOrFrom);
if (from === -1 || to < 0 || to >= arr.length || from === to) return;
const item = arr.splice(from, 1)[0];
arr.splice(to, 0, item);
}
export function removeArrayItem(arr, itemOrIndex) {
const index = typeof itemOrIndex === "number" ? itemOrIndex : arr.indexOf(itemOrIndex);
if (index !== -1) {
arr.splice(index, 1);
}
}