893 lines
33 KiB
JavaScript
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);
|
|
}
|
|
}
|
|
|