Spaces:
Running
Running
import { app } from "scripts/app.js"; | |
import type { | |
ContextMenuItem, | |
LGraphNode as TLGraphNode, | |
IWidget, | |
LGraphCanvas, | |
SerializedLGraphNode, | |
Vector2, | |
AdjustedMouseEvent, | |
} from "typings/litegraph.js"; | |
import type { ComfyObjectInfo, ComfyNodeConstructor } from "typings/comfy.js"; | |
import { RgthreeBaseServerNode } from "./base_node.js"; | |
import { rgthree } from "./rgthree.js"; | |
import { addConnectionLayoutSupport } from "./utils.js"; | |
import { NodeTypesString } from "./constants.js"; | |
import { | |
drawInfoIcon, | |
drawNumberWidgetPart, | |
drawRoundedRectangle, | |
drawTogglePart, | |
fitString, | |
isLowQuality, | |
} from "./utils_canvas.js"; | |
import { | |
RgthreeBaseHitAreas, | |
RgthreeBaseWidget, | |
RgthreeBetterButtonWidget, | |
RgthreeDividerWidget, | |
} from "./utils_widgets.js"; | |
import { rgthreeApi } from "rgthree/common/rgthree_api.js"; | |
import { showLoraChooser } from "./utils_menu.js"; | |
import { moveArrayItem, removeArrayItem } from "rgthree/common/shared_utils.js"; | |
import { RgthreeLoraInfoDialog } from "./dialog_info.js"; | |
import type { RgthreeModelInfo } from "typings/rgthree.js"; | |
import { LORA_INFO_SERVICE } from "rgthree/common/model_info_service.js"; | |
// import { RgthreePowerLoraChooserDialog } from "./dialog_power_lora_chooser.js"; | |
const PROP_LABEL_SHOW_STRENGTHS = "Show Strengths"; | |
const PROP_LABEL_SHOW_STRENGTHS_STATIC = `@${PROP_LABEL_SHOW_STRENGTHS}`; | |
const PROP_VALUE_SHOW_STRENGTHS_SINGLE = "Single Strength"; | |
const PROP_VALUE_SHOW_STRENGTHS_SEPARATE = "Separate Model & Clip"; | |
/** | |
* The Power Lora Loader is a super-simply Lora Loader node that can load multiple Loras at once | |
* in an ultra-condensed node allowing fast toggling, and advanced strength setting. | |
*/ | |
class RgthreePowerLoraLoader extends RgthreeBaseServerNode { | |
static override title = NodeTypesString.POWER_LORA_LOADER; | |
static override type = NodeTypesString.POWER_LORA_LOADER; | |
static comfyClass = NodeTypesString.POWER_LORA_LOADER; | |
override serialize_widgets = true; | |
private logger = rgthree.newLogSession(`[Power Lora Stack]`); | |
static [PROP_LABEL_SHOW_STRENGTHS_STATIC] = { | |
type: "combo", | |
values: [PROP_VALUE_SHOW_STRENGTHS_SINGLE, PROP_VALUE_SHOW_STRENGTHS_SEPARATE], | |
}; | |
/** Counts the number of lora widgets. This is used to give unique names. */ | |
private loraWidgetsCounter = 0; | |
/** Keep track of the spacer, new lora widgets will go before it when it exists. */ | |
private widgetButtonSpacer: IWidget | null = null; | |
constructor(title = NODE_CLASS.title) { | |
super(title); | |
this.properties[PROP_LABEL_SHOW_STRENGTHS] = PROP_VALUE_SHOW_STRENGTHS_SINGLE; | |
// Prefetch loras list. | |
rgthreeApi.getLoras(); | |
} | |
/** | |
* Handles configuration from a saved workflow by first removing our default widgets that were | |
* added in `onNodeCreated`, letting `super.configure` and do nothing, then create our lora | |
* widgets and, finally, add back in our default widgets. | |
*/ | |
override configure(info: SerializedLGraphNode<TLGraphNode>): void { | |
while (this.widgets?.length) this.removeWidget(0); | |
this.widgetButtonSpacer = null; | |
super.configure(info); | |
(this as any)._tempWidth = this.size[0]; | |
(this as any)._tempHeight = this.size[1]; | |
for (const widgetValue of info.widgets_values || []) { | |
if (widgetValue?.lora !== undefined) { | |
const widget = this.addNewLoraWidget(); | |
widget.value = { ...widgetValue }; | |
} | |
} | |
this.addNonLoraWidgets(); | |
this.size[0] = (this as any)._tempWidth; | |
this.size[1] = Math.max((this as any)._tempHeight, this.computeSize()[1]); | |
} | |
/** | |
* Adds the non-lora widgets. If we'll be configured then we remove them and add them back, so | |
* this is really only for newly created nodes in the current session. | |
*/ | |
override onNodeCreated() { | |
super.onNodeCreated?.(); | |
this.addNonLoraWidgets(); | |
const computed = this.computeSize(); | |
this.size = this.size || [0, 0]; | |
this.size[0] = Math.max(this.size[0], computed[0]); | |
this.size[1] = Math.max(this.size[1], computed[1]); | |
this.setDirtyCanvas(true, true); | |
} | |
/** Adds a new lora widget in the proper slot. */ | |
private addNewLoraWidget(lora?: string) { | |
this.loraWidgetsCounter++; | |
const widget = this.addCustomWidget( | |
new PowerLoraLoaderWidget("lora_" + this.loraWidgetsCounter), | |
); | |
if (lora) widget.setLora(lora); | |
if (this.widgetButtonSpacer) { | |
moveArrayItem(this.widgets, widget, this.widgets.indexOf(this.widgetButtonSpacer)); | |
} | |
return widget; | |
} | |
/** Adds the non-lora widgets around any lora ones that may be there from configuration. */ | |
private addNonLoraWidgets() { | |
moveArrayItem( | |
this.widgets, | |
this.addCustomWidget( | |
new RgthreeDividerWidget({ marginTop: 4, marginBottom: 0, thickness: 0 }), | |
), | |
0, | |
); | |
moveArrayItem(this.widgets, this.addCustomWidget(new PowerLoraLoaderHeaderWidget()), 1); | |
this.widgetButtonSpacer = this.addCustomWidget( | |
new RgthreeDividerWidget({ marginTop: 4, marginBottom: 0, thickness: 0 }), | |
); | |
this.addCustomWidget( | |
new RgthreeBetterButtonWidget( | |
"➕ Add Lora", | |
(event: AdjustedMouseEvent, pos: Vector2, node: TLGraphNode) => { | |
rgthreeApi.getLoras().then((loras) => { | |
showLoraChooser( | |
event as PointerEvent, | |
(value: ContextMenuItem | string) => { | |
if (typeof value === "string") { | |
if (value.includes("Power Lora Chooser")) { | |
// new RgthreePowerLoraChooserDialog().show(); | |
} else if (value !== "NONE") { | |
this.addNewLoraWidget(value); | |
const computed = this.computeSize(); | |
const tempHeight = (this as any)._tempHeight ?? 15; | |
this.size[1] = Math.max(tempHeight, computed[1]); | |
this.setDirtyCanvas(true, true); | |
} | |
} | |
// }, null, ["⚡️ Power Lora Chooser", ...loras]); | |
}, | |
null, | |
[...loras], | |
); | |
}); | |
return true; | |
}, | |
), | |
); | |
} | |
/** | |
* Hacks the `getSlotInPosition` call made from LiteGraph so we can show a custom context menu | |
* for widgets. | |
* | |
* Normally this method, called from LiteGraph's processContextMenu, will only get Inputs or | |
* Outputs. But that's not good enough because we we also want to provide a custom menu when | |
* clicking a widget for this node... so we are left to HACK once again! | |
* | |
* To achieve this: | |
* - Here, in LiteGraph's processContextMenu it asks the clicked node to tell it which input or | |
* output the user clicked on in `getSlotInPosition` | |
* - We check, and if we didn't, then we see if we clicked a widget and, if so, pass back some | |
* data that looks like we clicked an output to fool LiteGraph like a silly child. | |
* - As LiteGraph continues in its `processContextMenu`, it will then immediately call | |
* the clicked node's `getSlotMenuOptions` when `getSlotInPosition` returns data. | |
* - So, just below, we can then give LiteGraph the ContextMenu options we have. | |
* | |
* The only issue is that LiteGraph also checks `input/output.type` to set the ContextMenu title, | |
* so we need to supply that property (and set it to what we want our title). Otherwise, this | |
* should be pretty clean. | |
*/ | |
override getSlotInPosition(canvasX: number, canvasY: number): any { | |
const slot = super.getSlotInPosition(canvasX, canvasY); | |
// No slot, let's see if it's a widget. | |
if (!slot) { | |
let lastWidget = null; | |
for (const widget of this.widgets) { | |
// If last_y isn't set, something is wrong. Bail. | |
if (!widget.last_y) return; | |
if (canvasY > this.pos[1] + widget.last_y) { | |
lastWidget = widget; | |
continue; | |
} | |
break; | |
} | |
// Only care about lora widget clicks. | |
if (lastWidget?.name?.startsWith("lora_")) { | |
return { widget: lastWidget, output: { type: "LORA WIDGET" } }; | |
} | |
} | |
return slot; | |
} | |
/** | |
* Working with the overridden `getSlotInPosition` above, this method checks if the passed in | |
* option is actually a widget from it and then hijacks the context menu all together. | |
*/ | |
override getSlotMenuOptions(slot: any): ContextMenuItem[] | null { | |
// Oddly, LiteGraph doesn't call back into our node with a custom menu (even though it let's us | |
// define a custom menu to begin with... wtf?). So, we'll return null so the default is not | |
// triggered and then we'll just show one ourselves because.. yea. | |
if (slot?.widget?.name?.startsWith("lora_")) { | |
const widget = slot.widget as PowerLoraLoaderWidget; | |
const index = this.widgets.indexOf(widget); | |
const canMoveUp = !!this.widgets[index - 1]?.name?.startsWith("lora_"); | |
const canMoveDown = !!this.widgets[index + 1]?.name?.startsWith("lora_"); | |
const menuItems: ContextMenuItem[] = [ | |
{ | |
content: `ℹ️ Show Info`, | |
callback: () => { | |
widget.showLoraInfoDialog(); | |
}, | |
}, | |
null, // Divider | |
{ | |
content: `${widget.value.on ? "⚫" : "🟢"} Toggle ${widget.value.on ? "Off" : "On"}`, | |
callback: () => { | |
widget.value.on = !widget.value.on; | |
}, | |
}, | |
{ | |
content: `⬆️ Move Up`, | |
disabled: !canMoveUp, | |
callback: () => { | |
moveArrayItem(this.widgets, widget, index - 1); | |
}, | |
}, | |
{ | |
content: `⬇️ Move Down`, | |
disabled: !canMoveDown, | |
callback: () => { | |
moveArrayItem(this.widgets, widget, index + 1); | |
}, | |
}, | |
{ | |
content: `🗑️ Remove`, | |
callback: () => { | |
removeArrayItem(this.widgets, widget); | |
}, | |
}, | |
]; | |
let canvas = app.canvas as LGraphCanvas; | |
new LiteGraph.ContextMenu( | |
menuItems, | |
{ title: "LORA WIDGET", event: rgthree.lastAdjustedMouseEvent! }, | |
canvas.getCanvasWindow(), | |
); | |
return null; | |
} | |
return this.defaultGetSlotMenuOptions(slot); | |
} | |
/** | |
* When `refreshComboInNode` is called from ComfyUI, then we'll kick off a fresh loras fetch. | |
*/ | |
refreshComboInNode(defs: any) { | |
rgthreeApi.getLoras(true); | |
} | |
/** | |
* Returns true if there are any Lora Widgets. Useful for widgets to ask as they render. | |
*/ | |
hasLoraWidgets() { | |
return !!this.widgets?.find((w) => w.name?.startsWith("lora_")); | |
} | |
/** | |
* This will return true when all lora widgets are on, false when all are off, or null if it's | |
* mixed. | |
*/ | |
allLorasState() { | |
let allOn = true; | |
let allOff = true; | |
for (const widget of this.widgets) { | |
if (widget.name?.startsWith("lora_")) { | |
const on = widget.value?.on; | |
allOn = allOn && on === true; | |
allOff = allOff && on === false; | |
if (!allOn && !allOff) { | |
return null; | |
} | |
} | |
} | |
return allOn && this.widgets?.length ? true : false; | |
} | |
/** | |
* Toggles all the loras on or off. | |
*/ | |
toggleAllLoras() { | |
const allOn = this.allLorasState(); | |
const toggledTo = !allOn ? true : false; | |
for (const widget of this.widgets) { | |
if (widget.name?.startsWith("lora_")) { | |
widget.value.on = toggledTo; | |
} | |
} | |
} | |
static override setUp(comfyClass: ComfyNodeConstructor, nodeData: ComfyObjectInfo) { | |
RgthreeBaseServerNode.registerForOverride(comfyClass, nodeData, NODE_CLASS); | |
} | |
static override onRegisteredForOverride(comfyClass: any, ctxClass: any) { | |
addConnectionLayoutSupport(NODE_CLASS, app, [ | |
["Left", "Right"], | |
["Right", "Left"], | |
]); | |
setTimeout(() => { | |
NODE_CLASS.category = comfyClass.category; | |
}); | |
} | |
override getHelp() { | |
return ` | |
<p> | |
The ${this.type!.replace("(rgthree)", "")} is a powerful node that condenses 100s of pixels | |
of functionality in a single, dynamic node that allows you to add loras, change strengths, | |
and quickly toggle on/off all without taking up half your screen. | |
</p> | |
<ul> | |
<li><p> | |
Add as many Lora's as you would like by clicking the "+ Add Lora" button. | |
There's no real limit! | |
</p></li> | |
<li><p> | |
Right-click on a Lora widget for special options to move the lora up or down | |
(no image affect, only presentational), toggle it on/off, or delete the row all together. | |
</p></li> | |
<li> | |
<p> | |
<strong>Properties.</strong> You can change the following properties (by right-clicking | |
on the node, and select "Properties" or "Properties Panel" from the menu): | |
</p> | |
<ul> | |
<li><p> | |
<code>${PROP_LABEL_SHOW_STRENGTHS}</code> - Change between showing a single, simple | |
strength (which will be used for both model and clip), or a more advanced view with | |
both model and clip strengths being modifiable. | |
</p></li> | |
</ul> | |
</li> | |
</ul>`; | |
} | |
} | |
/** | |
* The PowerLoraLoaderHeaderWidget that renders a toggle all switch, as well as some title info | |
* (more necessary for the double model & clip strengths to label them). | |
*/ | |
class PowerLoraLoaderHeaderWidget extends RgthreeBaseWidget<{ type: string }> { | |
private showModelAndClip: boolean | null = null; | |
value = { type: "PowerLoraLoaderHeaderWidget" }; | |
protected override hitAreas: RgthreeBaseHitAreas<"toggle"> = { | |
toggle: { bounds: [0, 0] as Vector2, onDown: this.onToggleDown }, | |
}; | |
constructor(name: string = "PowerLoraLoaderHeaderWidget") { | |
super(name); | |
} | |
draw( | |
ctx: CanvasRenderingContext2D, | |
node: RgthreePowerLoraLoader, | |
w: number, | |
posY: number, | |
height: number, | |
) { | |
if (!node.hasLoraWidgets()) { | |
return; | |
} | |
// Since draw is the loop that runs, this is where we'll check the property state (rather than | |
// expect the node to tell us it's state etc). | |
this.showModelAndClip = | |
node.properties[PROP_LABEL_SHOW_STRENGTHS] === PROP_VALUE_SHOW_STRENGTHS_SEPARATE; | |
const margin = 10; | |
const innerMargin = margin * 0.33; | |
const lowQuality = isLowQuality(); | |
const allLoraState = node.allLorasState(); | |
// Move slightly down. We don't have a border and this feels a bit nicer. | |
posY += 2; | |
const midY = posY + height * 0.5; | |
let posX = 10; | |
ctx.save(); | |
this.hitAreas.toggle.bounds = drawTogglePart(ctx, { posX, posY, height, value: allLoraState }); | |
if (!lowQuality) { | |
posX += this.hitAreas.toggle.bounds[1] + innerMargin; | |
ctx.globalAlpha = app.canvas.editor_alpha * 0.55; | |
ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR; | |
ctx.textAlign = "left"; | |
ctx.textBaseline = "middle"; | |
ctx.fillText("Toggle All", posX, midY); | |
let rposX = node.size[0] - margin - innerMargin - innerMargin; | |
ctx.textAlign = "center"; | |
ctx.fillText( | |
this.showModelAndClip ? "Clip" : "Strength", | |
rposX - drawNumberWidgetPart.WIDTH_TOTAL / 2, | |
midY, | |
); | |
if (this.showModelAndClip) { | |
rposX = rposX - drawNumberWidgetPart.WIDTH_TOTAL - innerMargin * 2; | |
ctx.fillText("Model", rposX - drawNumberWidgetPart.WIDTH_TOTAL / 2, midY); | |
} | |
} | |
ctx.restore(); | |
} | |
/** | |
* Handles a pointer down on the toggle's defined hit area. | |
*/ | |
onToggleDown(event: AdjustedMouseEvent, pos: Vector2, node: TLGraphNode) { | |
(node as RgthreePowerLoraLoader).toggleAllLoras(); | |
this.cancelMouseDown(); | |
return true; | |
} | |
} | |
const DEFAULT_LORA_WIDGET_DATA: PowerLoraLoaderWidgetValue = { | |
on: true, | |
lora: null as string | null, | |
strength: 1, | |
strengthTwo: null as number | null, | |
}; | |
type PowerLoraLoaderWidgetValue = { | |
on: boolean; | |
lora: string | null; | |
strength: number; | |
strengthTwo: number | null; | |
}; | |
/** | |
* The PowerLoaderWidget that combines several custom drawing and functionality in a single row. | |
*/ | |
class PowerLoraLoaderWidget extends RgthreeBaseWidget<PowerLoraLoaderWidgetValue> { | |
/** Whether the strength has changed with mouse move (to cancel mouse up). */ | |
private haveMouseMovedStrength = false; | |
private loraInfoPromise: Promise<RgthreeModelInfo | null> | null = null; | |
private loraInfo: RgthreeModelInfo | null = null; | |
private showModelAndClip: boolean | null = null; | |
protected override hitAreas: RgthreeBaseHitAreas< | |
| "toggle" | |
| "lora" | |
// | "info" | |
| "strengthDec" | |
| "strengthVal" | |
| "strengthInc" | |
| "strengthAny" | |
| "strengthTwoDec" | |
| "strengthTwoVal" | |
| "strengthTwoInc" | |
| "strengthTwoAny" | |
> = { | |
toggle: { bounds: [0, 0] as Vector2, onDown: this.onToggleDown }, | |
lora: { bounds: [0, 0] as Vector2, onDown: this.onLoraDown }, | |
// info: { bounds: [0, 0] as Vector2, onDown: this.onInfoDown }, | |
strengthDec: { bounds: [0, 0] as Vector2, onDown: this.onStrengthDecDown }, | |
strengthVal: { bounds: [0, 0] as Vector2, onUp: this.onStrengthValUp }, | |
strengthInc: { bounds: [0, 0] as Vector2, onDown: this.onStrengthIncDown }, | |
strengthAny: { bounds: [0, 0] as Vector2, onMove: this.onStrengthAnyMove }, | |
strengthTwoDec: { bounds: [0, 0] as Vector2, onDown: this.onStrengthTwoDecDown }, | |
strengthTwoVal: { bounds: [0, 0] as Vector2, onUp: this.onStrengthTwoValUp }, | |
strengthTwoInc: { bounds: [0, 0] as Vector2, onDown: this.onStrengthTwoIncDown }, | |
strengthTwoAny: { bounds: [0, 0] as Vector2, onMove: this.onStrengthTwoAnyMove }, | |
}; | |
constructor(name: string) { | |
super(name); | |
} | |
private _value = { | |
on: true, | |
lora: null as string | null, | |
strength: 1, | |
strengthTwo: null as number | null, | |
}; | |
set value(v) { | |
this._value = v; | |
// In case widgets are messed up, we can correct course here. | |
if (typeof this._value !== "object") { | |
this._value = { ...DEFAULT_LORA_WIDGET_DATA }; | |
if (this.showModelAndClip) { | |
this._value.strengthTwo = this._value.strength; | |
} | |
} | |
this.getLoraInfo(); | |
} | |
get value() { | |
return this._value; | |
} | |
setLora(lora: string) { | |
this._value.lora = lora; | |
this.getLoraInfo(); | |
} | |
/** Draws our widget with a toggle, lora selector, and number selector all in a single row. */ | |
draw(ctx: CanvasRenderingContext2D, node: TLGraphNode, w: number, posY: number, height: number) { | |
// Since draw is the loop that runs, this is where we'll check the property state (rather than | |
// expect the node to tell us it's state etc). | |
let currentShowModelAndClip = | |
node.properties[PROP_LABEL_SHOW_STRENGTHS] === PROP_VALUE_SHOW_STRENGTHS_SEPARATE; | |
if (this.showModelAndClip !== currentShowModelAndClip) { | |
let oldShowModelAndClip = this.showModelAndClip; | |
this.showModelAndClip = currentShowModelAndClip; | |
if (this.showModelAndClip) { | |
// If we're setting show both AND we're not null, then re-set to the current strength. | |
if (oldShowModelAndClip != null) { | |
this.value.strengthTwo = this.value.strength ?? 1; | |
} | |
} else { | |
this.value.strengthTwo = null; | |
this.hitAreas.strengthTwoDec.bounds = [0, -1]; | |
this.hitAreas.strengthTwoVal.bounds = [0, -1]; | |
this.hitAreas.strengthTwoInc.bounds = [0, -1]; | |
this.hitAreas.strengthTwoAny.bounds = [0, -1]; | |
} | |
} | |
ctx.save(); | |
const margin = 10; | |
const innerMargin = margin * 0.33; | |
const lowQuality = isLowQuality(); | |
const midY = posY + height * 0.5; | |
// We'll move posX along as we draw things. | |
let posX = margin; | |
// Draw the background. | |
drawRoundedRectangle(ctx, { posX, posY, height, width: node.size[0] - margin * 2 }); | |
// Draw the toggle | |
this.hitAreas.toggle.bounds = drawTogglePart(ctx, { posX, posY, height, value: this.value.on }); | |
posX += this.hitAreas.toggle.bounds[1] + innerMargin; | |
// If low quality, then we're done rendering. | |
if (lowQuality) { | |
ctx.restore(); | |
return; | |
} | |
// If we're not toggled on, then make everything after faded. | |
if (!this.value.on) { | |
ctx.globalAlpha = app.canvas.editor_alpha * 0.4; | |
} | |
ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR; | |
// Now, we draw the strength number part on the right, so we know the width of it to draw the | |
// lora label as flexible. | |
let rposX = node.size[0] - margin - innerMargin - innerMargin; | |
const strengthValue = this.showModelAndClip | |
? this.value.strengthTwo ?? 1 | |
: this.value.strength ?? 1; | |
let textColor: string | undefined = undefined; | |
if (this.loraInfo?.strengthMax != null && strengthValue > this.loraInfo?.strengthMax) { | |
textColor = "#c66"; | |
} else if (this.loraInfo?.strengthMin != null && strengthValue < this.loraInfo?.strengthMin) { | |
textColor = "#c66"; | |
} | |
const [leftArrow, text, rightArrow] = drawNumberWidgetPart(ctx, { | |
posX: node.size[0] - margin - innerMargin - innerMargin, | |
posY, | |
height, | |
value: strengthValue, | |
direction: -1, | |
textColor, | |
}); | |
this.hitAreas.strengthDec.bounds = leftArrow; | |
this.hitAreas.strengthVal.bounds = text; | |
this.hitAreas.strengthInc.bounds = rightArrow; | |
this.hitAreas.strengthAny.bounds = [leftArrow[0], rightArrow[0] + rightArrow[1] - leftArrow[0]]; | |
rposX = leftArrow[0] - innerMargin; | |
if (this.showModelAndClip) { | |
rposX -= innerMargin; | |
// If we're showing both, then the rightmost we just drew is our "strengthTwo", so reset and | |
// then draw our model ("strength" one) to the left. | |
this.hitAreas.strengthTwoDec.bounds = this.hitAreas.strengthDec.bounds; | |
this.hitAreas.strengthTwoVal.bounds = this.hitAreas.strengthVal.bounds; | |
this.hitAreas.strengthTwoInc.bounds = this.hitAreas.strengthInc.bounds; | |
this.hitAreas.strengthTwoAny.bounds = this.hitAreas.strengthAny.bounds; | |
let textColor: string | undefined = undefined; | |
if (this.loraInfo?.strengthMax != null && this.value.strength > this.loraInfo?.strengthMax) { | |
textColor = "#c66"; | |
} else if ( | |
this.loraInfo?.strengthMin != null && | |
this.value.strength < this.loraInfo?.strengthMin | |
) { | |
textColor = "#c66"; | |
} | |
const [leftArrow, text, rightArrow] = drawNumberWidgetPart(ctx, { | |
posX: rposX, | |
posY, | |
height, | |
value: this.value.strength ?? 1, | |
direction: -1, | |
textColor, | |
}); | |
this.hitAreas.strengthDec.bounds = leftArrow; | |
this.hitAreas.strengthVal.bounds = text; | |
this.hitAreas.strengthInc.bounds = rightArrow; | |
this.hitAreas.strengthAny.bounds = [ | |
leftArrow[0], | |
rightArrow[0] + rightArrow[1] - leftArrow[0], | |
]; | |
rposX = leftArrow[0] - innerMargin; | |
} | |
const infoIconSize = height * 0.66; | |
const infoWidth = infoIconSize + innerMargin + innerMargin; | |
// Draw an info emoji; if checks if it's enabled (to quickly turn it on or off) | |
if ((this.hitAreas as any)["info"]) { | |
rposX -= innerMargin; | |
drawInfoIcon(ctx, rposX - infoIconSize, posY + (height - infoIconSize) / 2, infoIconSize); | |
// ctx.fillText('ℹ', posX, midY); | |
(this.hitAreas as any).info.bounds = [rposX - infoIconSize, infoWidth]; | |
rposX = rposX - infoIconSize - innerMargin; | |
} | |
// Draw lora label | |
const loraWidth = rposX - posX; | |
ctx.textAlign = "left"; | |
ctx.textBaseline = "middle"; | |
const loraLabel = String(this.value?.lora || "None"); | |
ctx.fillText(fitString(ctx, loraLabel, loraWidth), posX, midY); | |
this.hitAreas.lora.bounds = [posX, loraWidth]; | |
posX += loraWidth + innerMargin; | |
ctx.globalAlpha = app.canvas.editor_alpha; | |
ctx.restore(); | |
} | |
serializeValue(serializedNode: SerializedLGraphNode, widgetIndex: number) { | |
const v = { ...this.value }; | |
// Never send the second value to the backend if we're not showing it, otherwise, let's just | |
// make sure it's not null. | |
if (!this.showModelAndClip) { | |
delete (v as any).strengthTwo; | |
} else { | |
this.value.strengthTwo = this.value.strengthTwo ?? 1; | |
v.strengthTwo = this.value.strengthTwo; | |
} | |
return v; | |
} | |
onToggleDown(event: AdjustedMouseEvent, pos: Vector2, node: TLGraphNode) { | |
this.value.on = !this.value.on; | |
this.cancelMouseDown(); // Clear the down since we handle it. | |
return true; | |
} | |
onInfoDown(event: AdjustedMouseEvent, pos: Vector2, node: TLGraphNode) { | |
this.showLoraInfoDialog(); | |
} | |
onLoraDown(event: AdjustedMouseEvent, pos: Vector2, node: TLGraphNode) { | |
showLoraChooser(event, (value: ContextMenuItem) => { | |
if (typeof value === "string") { | |
this.value.lora = value; | |
this.loraInfo = null; | |
this.getLoraInfo(); | |
} | |
node.setDirtyCanvas(true, true); | |
}); | |
this.cancelMouseDown(); | |
} | |
onStrengthDecDown(event: AdjustedMouseEvent, pos: Vector2, node: TLGraphNode) { | |
this.stepStrength(-1, false); | |
} | |
onStrengthIncDown(event: AdjustedMouseEvent, pos: Vector2, node: TLGraphNode) { | |
this.stepStrength(1, false); | |
} | |
onStrengthTwoDecDown(event: AdjustedMouseEvent, pos: Vector2, node: TLGraphNode) { | |
this.stepStrength(-1, true); | |
} | |
onStrengthTwoIncDown(event: AdjustedMouseEvent, pos: Vector2, node: TLGraphNode) { | |
this.stepStrength(1, true); | |
} | |
onStrengthAnyMove(event: AdjustedMouseEvent, pos: Vector2, node: TLGraphNode) { | |
this.doOnStrengthAnyMove(event, false); | |
} | |
onStrengthTwoAnyMove(event: AdjustedMouseEvent, pos: Vector2, node: TLGraphNode) { | |
this.doOnStrengthAnyMove(event, true); | |
} | |
private doOnStrengthAnyMove(event: AdjustedMouseEvent, isTwo = false) { | |
if (event.deltaX) { | |
let prop: "strengthTwo" | "strength" = isTwo ? "strengthTwo" : "strength"; | |
this.haveMouseMovedStrength = true; | |
this.value[prop] = (this.value[prop] ?? 1) + event.deltaX * 0.05; | |
} | |
} | |
onStrengthValUp(event: AdjustedMouseEvent, pos: Vector2, node: TLGraphNode) { | |
this.doOnStrengthValUp(event, false); | |
} | |
onStrengthTwoValUp(event: AdjustedMouseEvent, pos: Vector2, node: TLGraphNode) { | |
this.doOnStrengthValUp(event, true); | |
} | |
private doOnStrengthValUp(event: AdjustedMouseEvent, isTwo = false) { | |
if (this.haveMouseMovedStrength) return; | |
let prop: "strengthTwo" | "strength" = isTwo ? "strengthTwo" : "strength"; | |
const canvas = app.canvas as LGraphCanvas; | |
canvas.prompt("Value", this.value[prop], (v: string) => (this.value[prop] = Number(v)), event); | |
} | |
override onMouseUp(event: AdjustedMouseEvent, pos: Vector2, node: TLGraphNode): boolean | void { | |
super.onMouseUp(event, pos, node); | |
this.haveMouseMovedStrength = false; | |
} | |
showLoraInfoDialog() { | |
if (!this.value.lora || this.value.lora === "None") { | |
return; | |
} | |
const infoDialog = new RgthreeLoraInfoDialog(this.value.lora).show(); | |
infoDialog.addEventListener("close", ((e: CustomEvent<{ dirty: boolean }>) => { | |
if (e.detail.dirty) { | |
this.getLoraInfo(true); | |
} | |
}) as EventListener); | |
} | |
private stepStrength(direction: -1 | 1, isTwo = false) { | |
let step = 0.05; | |
let prop: "strengthTwo" | "strength" = isTwo ? "strengthTwo" : "strength"; | |
let strength = (this.value[prop] ?? 1) + step * direction; | |
this.value[prop] = Math.round(strength * 100) / 100; | |
} | |
private getLoraInfo(force = false) { | |
if (!this.loraInfoPromise || force == true) { | |
let promise; | |
if (this.value.lora && this.value.lora != "None") { | |
promise = LORA_INFO_SERVICE.getInfo(this.value.lora, force, true); | |
} else { | |
promise = Promise.resolve(null); | |
} | |
this.loraInfoPromise = promise.then((v) => (this.loraInfo = v)); | |
} | |
return this.loraInfoPromise; | |
} | |
} | |
/** An uniformed name reference to the node class. */ | |
const NODE_CLASS = RgthreePowerLoraLoader; | |
/** Register the node. */ | |
app.registerExtension({ | |
name: "rgthree.PowerLoraLoader", | |
async beforeRegisterNodeDef(nodeType: ComfyNodeConstructor, nodeData: ComfyObjectInfo) { | |
if (nodeData.name === NODE_CLASS.type) { | |
NODE_CLASS.setUp(nodeType, nodeData); | |
} | |
}, | |
}); | |