Restore a valid selection after reconnecting the last Switch input and remove the redundant loading check after the early return. Add Chrome regressions for workflow loading, routing, reconnection, legacy Switch names, and image, mask, SEGS, conditioning, and regional-prompt combine nodes. Wait for imported nodes to replace the previous graph, and check non-default selections and fresh outputs after editing. Keep the different-size mask batch test as a documented pre-existing failure; its backend implementation is outside this change.
986 lines
28 KiB
JavaScript
986 lines
28 KiB
JavaScript
import { ComfyApp, app } from "../../scripts/app.js";
|
|
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
|
import { api } from "../../scripts/api.js";
|
|
import { customAlert, isBeforeFrontendVersion } from "./common.js";
|
|
|
|
const is_legacy_front = () => isBeforeFrontendVersion('1.16.9');
|
|
|
|
if(is_legacy_front()) {
|
|
customAlert("An outdated version(<1.16.9) of the `comfyui-frontend-package` is installed. It is not compatible with the current version of the Impact Pack.");
|
|
}
|
|
|
|
let wildcards_list = [];
|
|
let wildcard_status = {
|
|
on_demand_mode: false,
|
|
total_available: 0,
|
|
loaded_count: 0,
|
|
last_update: null
|
|
};
|
|
|
|
async function load_wildcards() {
|
|
let res = await api.fetchApi('/impact/wildcards/list');
|
|
let data = await res.json();
|
|
wildcards_list = data.data;
|
|
}
|
|
|
|
async function load_wildcard_status() {
|
|
try {
|
|
let res = await api.fetchApi('/impact/wildcards/list/loaded');
|
|
let data = await res.json();
|
|
wildcard_status = {
|
|
on_demand_mode: data.on_demand_mode || false,
|
|
total_available: data.total_available || 0,
|
|
loaded_count: data.data ? data.data.length : 0,
|
|
last_update: new Date()
|
|
};
|
|
} catch (error) {
|
|
console.error('Failed to load wildcard status:', error);
|
|
}
|
|
}
|
|
|
|
export function get_wildcard_label() {
|
|
if (wildcard_status.on_demand_mode) {
|
|
return `Select Wildcard 🔵 On-Demand: ${wildcard_status.loaded_count} loaded`;
|
|
} else {
|
|
return `Select Wildcard 🟢 Full Cache`;
|
|
}
|
|
}
|
|
|
|
export function is_wildcard_label(value) {
|
|
// Check if value is a label (not an actual wildcard selection)
|
|
return value === "Select the Wildcard to add to the text" ||
|
|
value.startsWith("Select Wildcard 🔵 On-Demand:") ||
|
|
value === "Select Wildcard 🟢 Full Cache";
|
|
}
|
|
|
|
Promise.all([load_wildcards(), load_wildcard_status()]);
|
|
|
|
export function get_wildcards_list() {
|
|
return wildcards_list;
|
|
}
|
|
|
|
export { load_wildcard_status };
|
|
|
|
// temporary implementation (copying from https://github.com/pythongosssss/ComfyUI-WD14-Tagger)
|
|
// I think this should be included into master!!
|
|
class ImpactProgressBadge {
|
|
constructor() {
|
|
if (!window.__progress_badge__) {
|
|
window.__progress_badge__ = Symbol("__impact_progress_badge__");
|
|
}
|
|
this.symbol = window.__progress_badge__;
|
|
}
|
|
|
|
getState(node) {
|
|
return node[this.symbol] || {};
|
|
}
|
|
|
|
setState(node, state) {
|
|
node[this.symbol] = state;
|
|
app.canvas.setDirty(true);
|
|
}
|
|
|
|
addStatusHandler(nodeType) {
|
|
if (nodeType[this.symbol]?.statusTagHandler) {
|
|
return;
|
|
}
|
|
if (!nodeType[this.symbol]) {
|
|
nodeType[this.symbol] = {};
|
|
}
|
|
nodeType[this.symbol] = {
|
|
statusTagHandler: true,
|
|
};
|
|
|
|
api.addEventListener("impact/update_status", ({ detail }) => {
|
|
let { node, progress, text } = detail;
|
|
const n = app.graph.getNodeById(+(node || app.runningNodeId));
|
|
if (!n) return;
|
|
const state = this.getState(n);
|
|
state.status = Object.assign(state.status || {}, { progress: text ? progress : null, text: text || null });
|
|
this.setState(n, state);
|
|
});
|
|
|
|
const self = this;
|
|
const onDrawForeground = nodeType.prototype.onDrawForeground;
|
|
nodeType.prototype.onDrawForeground = function (ctx) {
|
|
const r = onDrawForeground?.apply?.(this, arguments);
|
|
const state = self.getState(this);
|
|
if (!state?.status?.text) {
|
|
return r;
|
|
}
|
|
|
|
const { fgColor, bgColor, text, progress, progressColor } = { ...state.status };
|
|
|
|
ctx.save();
|
|
ctx.font = "12px sans-serif";
|
|
const sz = ctx.measureText(text);
|
|
ctx.fillStyle = bgColor || "dodgerblue";
|
|
ctx.beginPath();
|
|
ctx.roundRect(0, -LiteGraph.NODE_TITLE_HEIGHT - 20, sz.width + 12, 20, 5);
|
|
ctx.fill();
|
|
|
|
if (progress) {
|
|
ctx.fillStyle = progressColor || "green";
|
|
ctx.beginPath();
|
|
ctx.roundRect(0, -LiteGraph.NODE_TITLE_HEIGHT - 20, (sz.width + 12) * progress, 20, 5);
|
|
ctx.fill();
|
|
}
|
|
|
|
ctx.fillStyle = fgColor || "#fff";
|
|
ctx.fillText(text, 6, -LiteGraph.NODE_TITLE_HEIGHT - 6);
|
|
ctx.restore();
|
|
return r;
|
|
};
|
|
}
|
|
}
|
|
|
|
const input_tracking = {};
|
|
const input_dirty = {};
|
|
const output_tracking = {};
|
|
|
|
function progressExecuteHandler(event) {
|
|
if(event.detail?.output?.aux){
|
|
const id = event.detail.node;
|
|
if(input_tracking.hasOwnProperty(id)) {
|
|
if(input_tracking.hasOwnProperty(id) && input_tracking[id][0] != event.detail.output.aux[0]) {
|
|
input_dirty[id] = true;
|
|
}
|
|
else{
|
|
|
|
}
|
|
}
|
|
|
|
input_tracking[id] = event.detail.output.aux;
|
|
}
|
|
}
|
|
|
|
function imgSendHandler(event) {
|
|
if(event.detail.images.length > 0){
|
|
let data = event.detail.images[0];
|
|
let filename = `${data.filename} [${data.type}]`;
|
|
|
|
let nodes = app.graph._nodes;
|
|
for(let i in nodes) {
|
|
if(nodes[i].type == 'ImageReceiver') {
|
|
let is_linked = false;
|
|
|
|
if(nodes[i].widgets[1].type == 'converted-widget') {
|
|
for(let j in nodes[i].inputs) {
|
|
let input = nodes[i].inputs[j];
|
|
if(input.name === 'link_id') {
|
|
if(input.link) {
|
|
let src_node = app.graph._nodes_by_id[app.graph.links[input.link].origin_id];
|
|
if(src_node.type == 'ImpactInt' || src_node.type == 'PrimitiveNode') {
|
|
is_linked = true;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else if(nodes[i].widgets[1].value == event.detail.link_id) {
|
|
is_linked = true;
|
|
}
|
|
|
|
if(is_linked) {
|
|
if(data.subfolder)
|
|
nodes[i].widgets[0].value = `${data.subfolder}/${data.filename} [${data.type}]`;
|
|
else
|
|
nodes[i].widgets[0].value = `${data.filename} [${data.type}]`;
|
|
|
|
let img = new Image();
|
|
img.onload = (event) => {
|
|
nodes[i].imgs = [img];
|
|
nodes[i].size[1] = Math.max(200, nodes[i].size[1]);
|
|
app.canvas.setDirty(true);
|
|
};
|
|
img.src = `/view?filename=${data.filename}&type=${data.type}&subfolder=${data.subfolder}`+app.getPreviewFormatParam();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function latentSendHandler(event) {
|
|
if(event.detail.images.length > 0){
|
|
let data = event.detail.images[0];
|
|
let filename = `${data.filename} [${data.type}]`;
|
|
|
|
let nodes = app.graph._nodes;
|
|
for(let i in nodes) {
|
|
if(nodes[i].type == 'LatentReceiver') {
|
|
if(nodes[i].widgets[1].value == event.detail.link_id) {
|
|
if(data.subfolder)
|
|
nodes[i].widgets[0].value = `${data.subfolder}/${data.filename} [${data.type}]`;
|
|
else
|
|
nodes[i].widgets[0].value = `${data.filename} [${data.type}]`;
|
|
|
|
let img = new Image();
|
|
img.src = `/view?filename=${data.filename}&type=${data.type}&subfolder=${data.subfolder}`+app.getPreviewFormatParam();
|
|
nodes[i].imgs = [img];
|
|
nodes[i].size[1] = Math.max(200, nodes[i].size[1]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function valueSendHandler(event) {
|
|
let nodes = app.graph._nodes;
|
|
for(let i in nodes) {
|
|
if(nodes[i].type == 'ImpactValueReceiver') {
|
|
if(nodes[i].widgets[2].value == event.detail.link_id) {
|
|
nodes[i].widgets[1].value = event.detail.value;
|
|
|
|
let typ = typeof event.detail.value;
|
|
if(typ == 'string') {
|
|
nodes[i].widgets[0].value = "STRING";
|
|
}
|
|
else if(typ == "boolean") {
|
|
nodes[i].widgets[0].value = "BOOLEAN";
|
|
}
|
|
else if(typ != "number") {
|
|
nodes[i].widgets[0].value = typeof event.detail.value;
|
|
}
|
|
else if(Number.isInteger(event.detail.value)) {
|
|
nodes[i].widgets[0].value = "INT";
|
|
}
|
|
else {
|
|
nodes[i].widgets[0].value = "FLOAT";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
const impactProgressBadge = new ImpactProgressBadge();
|
|
|
|
api.addEventListener("stop-iteration", () => {
|
|
document.getElementById("autoQueueCheckbox").checked = false;
|
|
});
|
|
api.addEventListener("value-send", valueSendHandler);
|
|
api.addEventListener("img-send", imgSendHandler);
|
|
api.addEventListener("latent-send", latentSendHandler);
|
|
api.addEventListener("executed", progressExecuteHandler);
|
|
|
|
// Update wildcard status after workflow execution (on-demand mode)
|
|
api.addEventListener("executed", async (event) => {
|
|
if (wildcard_status.on_demand_mode) {
|
|
await load_wildcard_status();
|
|
await load_wildcards();
|
|
app.canvas.setDirty(true);
|
|
}
|
|
});
|
|
|
|
app.registerExtension({
|
|
name: "Comfy.Impack",
|
|
|
|
commands: [
|
|
{
|
|
id: 'refresh-impact-wildcard',
|
|
label: 'Impact: Refresh Wildcard',
|
|
function: async () => {
|
|
await api.fetchApi('/impact/wildcards/refresh');
|
|
await Promise.all([load_wildcards(), load_wildcard_status()]);
|
|
app.extensionManager.toast.add({
|
|
severity: 'info',
|
|
summary: 'Refreshed!',
|
|
detail: 'Impact Wildcard List is refreshed!!',
|
|
life: 3000
|
|
});
|
|
}
|
|
}
|
|
],
|
|
|
|
menuCommands: [
|
|
{
|
|
path: ['Edit'],
|
|
commands: ['refresh-impact-wildcard']
|
|
}
|
|
],
|
|
|
|
loadedGraphNode(node, app) {
|
|
if (node.comfyClass == "MaskPainter") {
|
|
input_dirty[node.id + ""] = true;
|
|
}
|
|
},
|
|
|
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
|
if (nodeData.name == "IterativeLatentUpscale" || nodeData.name == "IterativeImageUpscale"
|
|
|| nodeData.name == "RegionalSampler"|| nodeData.name == "RegionalSamplerAdvanced") {
|
|
impactProgressBadge.addStatusHandler(nodeType);
|
|
}
|
|
|
|
if(nodeData.name == "ImpactControlBridge") {
|
|
const onConnectionsChange = nodeType.prototype.onConnectionsChange;
|
|
nodeType.prototype.onConnectionsChange = function (type, index, connected, link_info) {
|
|
if(index != 0 || !link_info || this.inputs[0].type != '*')
|
|
return;
|
|
|
|
// assign type
|
|
let slot_type = '*';
|
|
|
|
if(type == 2) {
|
|
slot_type = link_info.type;
|
|
}
|
|
else {
|
|
const node = app.graph.getNodeById(link_info.origin_id);
|
|
slot_type = node?.outputs[link_info.origin_slot]?.type;
|
|
}
|
|
|
|
this.inputs[0].type = slot_type;
|
|
this.outputs[0].type = slot_type;
|
|
this.outputs[0].label = slot_type;
|
|
}
|
|
}
|
|
|
|
if(nodeData.name == "ImpactConditionalBranch" || nodeData.name == "ImpactConditionalBranchSelMode") {
|
|
const onConnectionsChange = nodeType.prototype.onConnectionsChange;
|
|
nodeType.prototype.onConnectionsChange = function (type, index, connected, link_info) {
|
|
if(!link_info || this.inputs[0].type != '*')
|
|
return;
|
|
|
|
if(index >= 2)
|
|
return;
|
|
|
|
// assign type
|
|
let slot_type = '*';
|
|
|
|
if(type == 2) {
|
|
slot_type = link_info.type;
|
|
}
|
|
else {
|
|
const node = app.graph.getNodeById(link_info.origin_id);
|
|
slot_type = node?.outputs[link_info.origin_slot].type;
|
|
}
|
|
|
|
this.inputs[0].type = slot_type;
|
|
this.inputs[1].type = slot_type;
|
|
this.outputs[0].type = slot_type;
|
|
this.outputs[0].label = slot_type;
|
|
}
|
|
}
|
|
|
|
if(nodeData.name == "ImpactCompare") {
|
|
const onConnectionsChange = nodeType.prototype.onConnectionsChange;
|
|
nodeType.prototype.onConnectionsChange = function (type, index, connected, link_info) {
|
|
if(!link_info || this.inputs[0].type != '*' || type == 2)
|
|
return;
|
|
|
|
// assign type
|
|
const node = app.graph.getNodeById(link_info.origin_id);
|
|
let slot_type = node?.outputs[link_info.origin_slot].type;
|
|
|
|
this.inputs[0].type = slot_type;
|
|
this.inputs[1].type = slot_type;
|
|
}
|
|
}
|
|
|
|
if(nodeData.name == "ImpactSelectNthItemOfAnyList") {
|
|
const onConnectionsChange = nodeType.prototype.onConnectionsChange;
|
|
nodeType.prototype.onConnectionsChange = function (type, index, connected, link_info) {
|
|
if(!link_info || this.inputs[0].type != '*')
|
|
return;
|
|
|
|
if(index >= 2)
|
|
return;
|
|
|
|
// assign type
|
|
let slot_type = '*';
|
|
|
|
if(type == 2) {
|
|
slot_type = link_info.type;
|
|
}
|
|
else {
|
|
const node = app.graph.getNodeById(link_info.origin_id);
|
|
slot_type = node?.outputs[link_info.origin_slot].type;
|
|
}
|
|
|
|
this.inputs[0].type = slot_type;
|
|
this.outputs[0].type = slot_type;
|
|
this.outputs[0].label = slot_type;
|
|
}
|
|
}
|
|
|
|
if(nodeData.name === 'ImpactInversedSwitch') {
|
|
nodeData.output = ['*'];
|
|
nodeData.output_is_list = [false];
|
|
nodeData.output_name = ['output1'];
|
|
|
|
const onConnectionsChange = nodeType.prototype.onConnectionsChange;
|
|
nodeType.prototype.onConnectionsChange = function (type, index, connected, link_info) {
|
|
if(!link_info)
|
|
return;
|
|
|
|
// HOTFIX: subgraph
|
|
const stackTrace = new Error().stack;
|
|
|
|
if(stackTrace.includes('convertToSubgraph') || stackTrace.includes('Subgraph.configure')) {
|
|
return;
|
|
}
|
|
|
|
if(type == 2) {
|
|
// connect output
|
|
if(connected){
|
|
if(app.graph._nodes_by_id[link_info.target_id]?.type == 'Reroute') {
|
|
app.graph._nodes_by_id[link_info.target_id].disconnectInput(link_info.target_slot);
|
|
}
|
|
|
|
if(this.outputs[0].type == '*'){
|
|
if(link_info.type == '*' && app.graph.getNodeById(link_info.target_id).slots[link_info.target_slot].type != '*') {
|
|
app.graph._nodes_by_id[link_info.target_id].disconnectInput(link_info.target_slot);
|
|
}
|
|
else {
|
|
// propagate type
|
|
this.outputs[0].type = link_info.type;
|
|
this.outputs[0].name = link_info.type;
|
|
|
|
for(let i in this.inputs) {
|
|
if(this.inputs[i].name != 'select')
|
|
this.inputs[i].type = link_info.type;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
if(app.graph._nodes_by_id[link_info.origin_id]?.type == 'Reroute')
|
|
this.disconnectInput(link_info.target_slot);
|
|
|
|
// connect input
|
|
if(this.inputs[0].type == '*'){
|
|
const node = app.graph.getNodeById(link_info.origin_id);
|
|
let origin_type = node?.outputs[link_info.origin_slot]?.type;
|
|
|
|
if(origin_type==undefined) {
|
|
return; // fallback
|
|
}
|
|
|
|
if(origin_type == '*' && app.graph.getNodeById(link_info.origin_id).slots[link_info.origin_slot].type != '*') {
|
|
this.disconnectInput(link_info.target_slot);
|
|
return;
|
|
}
|
|
|
|
for(let i in this.inputs) {
|
|
if(this.inputs[i].name != 'select')
|
|
this.inputs[i].type = origin_type;
|
|
}
|
|
|
|
this.outputs[0].type = origin_type;
|
|
this.outputs[0].name = 'output1';
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (!connected && this.outputs.length > 1) {
|
|
const stackTrace = new Error().stack;
|
|
|
|
if(
|
|
!stackTrace.includes('LGraphNode.prototype.connect') && // for touch device
|
|
!stackTrace.includes('LGraphNode.connect') && // for mouse device
|
|
!stackTrace.includes('loadGraphData') && !app.configuringGraph) {
|
|
if(this.outputs[link_info.origin_slot].links.length == 0) {
|
|
this.removeOutput(link_info.origin_slot);
|
|
}
|
|
}
|
|
}
|
|
|
|
let slot_i = 1;
|
|
for (let i = 0; i < this.outputs.length; i++) {
|
|
this.outputs[i].name = `output${slot_i}`
|
|
if (this.outputs[i].slot_index === undefined) {
|
|
this.outputs[i].slot_index = i;
|
|
}
|
|
slot_i++;
|
|
}
|
|
|
|
if(connected) {
|
|
// NOTE: node.slot_index is different with link_info.origin_slot
|
|
let last_slot_index = this.outputs.length - 1;
|
|
if (last_slot_index == link_info.origin_slot) {
|
|
this.addOutput(`output${slot_i}`, this.outputs[0].type);
|
|
}
|
|
}
|
|
|
|
let select_slot = this.inputs.find(x => x.name == "select");
|
|
if(this.widgets?.length) {
|
|
this.widgets[0].options.max = select_slot?this.outputs.length-1:this.outputs.length;
|
|
this.widgets[0].value = Math.min(this.widgets[0].value, this.widgets[0].options.max);
|
|
if(this.widgets[0].options.max > 0 && this.widgets[0].value == 0)
|
|
this.widgets[0].value = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (nodeData.name === 'ImpactMakeImageList' || nodeData.name === 'ImpactMakeImageBatch' ||
|
|
nodeData.name === 'ImpactMakeMaskList' || nodeData.name === 'ImpactMakeMaskBatch' ||
|
|
nodeData.name === 'ImpactMakeAnyList' || nodeData.name === 'CombineRegionalPrompts' ||
|
|
nodeData.name === 'ImpactCombineConditionings' || nodeData.name === 'ImpactConcatConditionings' ||
|
|
nodeData.name === 'ImpactSEGSConcat' ||
|
|
nodeData.name === 'ImpactSwitch' || nodeData.name === 'LatentSwitch' || nodeData.name == 'SEGSSwitch') {
|
|
var input_name = "input";
|
|
|
|
switch(nodeData.name) {
|
|
case 'ImpactMakeImageList':
|
|
case 'ImpactMakeImageBatch':
|
|
input_name = "image";
|
|
break;
|
|
|
|
case 'ImpactMakeMaskList':
|
|
case 'ImpactMakeMaskBatch':
|
|
input_name = "mask";
|
|
break;
|
|
|
|
case 'ImpactMakeAnyList':
|
|
input_name = "value";
|
|
break;
|
|
|
|
case 'ImpactSEGSConcat':
|
|
input_name = "segs";
|
|
break;
|
|
|
|
case 'CombineRegionalPrompts':
|
|
input_name = "regional_prompts";
|
|
break;
|
|
|
|
case 'ImpactCombineConditionings':
|
|
case 'ImpactConcatConditionings':
|
|
input_name = "conditioning";
|
|
break;
|
|
|
|
case 'LatentSwitch':
|
|
input_name = "input";
|
|
break;
|
|
|
|
case 'SEGSSwitch':
|
|
input_name = "input";
|
|
break;
|
|
|
|
case 'ImpactSwitch':
|
|
input_name = "input";
|
|
}
|
|
|
|
const onConnectionsChange = nodeType.prototype.onConnectionsChange;
|
|
nodeType.prototype.onConnectionsChange = function (type, index, connected, link_info) {
|
|
const stackTrace = new Error().stack;
|
|
|
|
// HOTFIX: subgraph
|
|
if(stackTrace.includes('convertToSubgraph') || stackTrace.includes('Subgraph.configure')) {
|
|
return;
|
|
}
|
|
|
|
if(stackTrace.includes('loadGraphData') || app.configuringGraph) {
|
|
if(this.widgets?.[0]) {
|
|
this.widgets[0].options.max = this.inputs.length-3;
|
|
this.widgets[0].value = Math.min(this.widgets[0].value, this.widgets[0].options.max);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if(stackTrace.includes('pasteFromClipboard')) {
|
|
if(this.widgets?.[0]) {
|
|
this.widgets[0].options.max = this.inputs.length-3;
|
|
this.widgets[0].value = Math.min(this.widgets[0].value, this.widgets[0].options.max);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if(!link_info)
|
|
return;
|
|
|
|
if(type == 2) {
|
|
// connect output
|
|
if(connected && index == 0){
|
|
if(nodeData.name == 'ImpactSwitch' && app.graph._nodes_by_id[link_info.target_id]?.type == 'Reroute') {
|
|
app.graph._nodes_by_id[link_info.target_id].disconnectInput(link_info.target_slot);
|
|
}
|
|
|
|
if(this.outputs[0].type == '*'){
|
|
if(link_info.type == '*' && app.graph.getNodeById(link_info.target_id).slots[link_info.target_slot].type != '*') {
|
|
app.graph._nodes_by_id[link_info.target_id].disconnectInput(link_info.target_slot);
|
|
}
|
|
else {
|
|
// propagate type
|
|
this.outputs[0].type = link_info.type;
|
|
this.outputs[0].label = link_info.type;
|
|
this.outputs[0].name = link_info.type;
|
|
|
|
for(let i in this.inputs) {
|
|
let input_i = this.inputs[i];
|
|
if(input_i.name != 'select' && input_i.name != 'sel_mode')
|
|
input_i.type = link_info.type;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
else {
|
|
if(nodeData.name == 'ImpactSwitch' && app.graph._nodes_by_id[link_info.origin_id]?.type == 'Reroute')
|
|
this.disconnectInput(link_info.target_slot);
|
|
|
|
// connect input
|
|
if(this.inputs[index].name == 'select' || this.inputs[index].name == 'sel_mode')
|
|
return;
|
|
|
|
if(this.inputs[0].type == '*'){
|
|
const node = app.graph.getNodeById(link_info.origin_id);
|
|
|
|
// NOTE: node is undefined when subgraph editing mode
|
|
if(node) {
|
|
let origin_type = node.outputs[link_info.origin_slot]?.type;
|
|
if(origin_type == '*' && app.graph.getNodeById(link_info.origin_id).slots[link_info.origin_slot].type != '*') {
|
|
this.disconnectInput(link_info.target_slot);
|
|
return;
|
|
}
|
|
|
|
for(let i in this.inputs) {
|
|
let input_i = this.inputs[i];
|
|
if(input_i.name != 'select' && input_i.name != 'sel_mode')
|
|
input_i.type = origin_type;
|
|
}
|
|
|
|
this.outputs[0].type = origin_type;
|
|
this.outputs[0].label = origin_type;
|
|
this.outputs[0].name = origin_type;
|
|
}
|
|
}
|
|
}
|
|
|
|
let widget_count = 0;
|
|
if(nodeData.name == 'ImpactSwitch' || nodeData.name == 'LatentSwitch' || nodeData.name == 'SEGSSwitch') {
|
|
widget_count += 1;
|
|
}
|
|
|
|
if (!connected && (this.inputs.length > widget_count+1)) {
|
|
if(
|
|
!stackTrace.includes('LGraphNode.prototype.connect') && // for touch device
|
|
!stackTrace.includes('LGraphNode.connect') && // for mouse device
|
|
!stackTrace.includes('loadGraphData') &&
|
|
this.inputs[index].name != 'select') {
|
|
this.removeInput(index);
|
|
}
|
|
}
|
|
|
|
let slot_i = 1;
|
|
for (let i = 0; i < this.inputs.length; i++) {
|
|
let input_i = this.inputs[i];
|
|
if(input_i.name != 'select'&& input_i.name != 'sel_mode') {
|
|
input_i.name = `${input_name}${slot_i}`
|
|
slot_i++;
|
|
}
|
|
}
|
|
|
|
if(connected) {
|
|
this.addInput(`${input_name}${slot_i}`, this.outputs[0].type);
|
|
}
|
|
|
|
if(this.widgets?.[0]) {
|
|
this.widgets[0].options.max = this.inputs.length-3;
|
|
this.widgets[0].value = Math.min(this.widgets[0].value, this.widgets[0].options.max);
|
|
if(this.widgets[0].options.max > 0 && this.widgets[0].value == 0)
|
|
this.widgets[0].value = 1;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
nodeCreated(node, app) {
|
|
if(node.comfyClass == "MaskPainter") {
|
|
node.addWidget("button", "Edit mask", null, () => {
|
|
ComfyApp.copyToClipspace(node);
|
|
ComfyApp.clipspace_return_node = node;
|
|
ComfyApp.open_maskeditor();
|
|
});
|
|
}
|
|
|
|
switch(node.comfyClass) {
|
|
case "ToDetailerPipe":
|
|
case "ToDetailerPipeSDXL":
|
|
case "BasicPipeToDetailerPipe":
|
|
case "BasicPipeToDetailerPipeSDXL":
|
|
case "EditDetailerPipe":
|
|
case "FaceDetailer":
|
|
case "DetailerForEach":
|
|
case "DetailerForEachDebug":
|
|
case "DetailerForEachPipe":
|
|
case "DetailerForEachDebugPipe":
|
|
{
|
|
for(let i in node.widgets) {
|
|
let widget = node.widgets[i];
|
|
if(widget.type === "customtext") {
|
|
widget.dynamicPrompts = false;
|
|
widget.inputEl.placeholder = "wildcard spec: if kept empty, this option will be ignored";
|
|
widget.serializeValue = () => {
|
|
return node.widgets[i].value;
|
|
};
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
if(node.comfyClass == "ImpactSEGSLabelFilter" || node.comfyClass == "SEGSLabelFilterDetailerHookProvider") {
|
|
node.widgets[0].callback = (value, canvas, node, pos, e) => {
|
|
if(node) {
|
|
if(node.widgets[1].value.trim() != "" && !node.widgets[1].value.trim().endsWith(","))
|
|
node.widgets[1].value += ", "
|
|
|
|
node.widgets[1].value += value;
|
|
if(node.widgets_values)
|
|
node.widgets_values[1] = node.widgets[1].value;
|
|
}
|
|
}
|
|
|
|
Object.defineProperty(node.widgets[0], "value", {
|
|
set: (value) => {
|
|
node._value = value;
|
|
},
|
|
get: () => {
|
|
return node._value;
|
|
}
|
|
});
|
|
}
|
|
|
|
if(node.comfyClass == "UltralyticsDetectorProvider") {
|
|
let model_name_widget = node.widgets.find((w) => w.name === "model_name");
|
|
let orig_draw = node.onDrawForeground;
|
|
node.onDrawForeground = function (ctx) {
|
|
const r = orig_draw?.apply?.(this, arguments);
|
|
|
|
let is_seg = model_name_widget.value?.startsWith('segm/') || model_name_widget.value?.includes('-seg');
|
|
if(!is_seg) {
|
|
var slot_pos = new Float32Array(2);
|
|
var pos = node.getConnectionPos(false, 1, slot_pos);
|
|
|
|
pos[0] -= node.pos[0] - 10;
|
|
pos[1] -= node.pos[1];
|
|
|
|
ctx.beginPath();
|
|
ctx.strokeStyle = "red";
|
|
ctx.lineWidth = 4;
|
|
ctx.moveTo(pos[0] - 5, pos[1] - 5);
|
|
ctx.lineTo(pos[0] + 5, pos[1] + 5);
|
|
ctx.moveTo(pos[0] + 5, pos[1] - 5);
|
|
ctx.lineTo(pos[0] - 5, pos[1] + 5);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
}
|
|
|
|
if(
|
|
node.comfyClass == "ImpactWildcardEncode" || node.comfyClass == "ImpactWildcardProcessor"
|
|
|| node.comfyClass == "ToDetailerPipe" || node.comfyClass == "ToDetailerPipeSDXL"
|
|
|| node.comfyClass == "EditDetailerPipe" || node.comfyClass == "EditDetailerPipeSDXL"
|
|
|| node.comfyClass == "BasicPipeToDetailerPipe" || node.comfyClass == "BasicPipeToDetailerPipeSDXL") {
|
|
node._value = "Select the LoRA to add to the text";
|
|
node._wvalue = "Select the Wildcard to add to the text";
|
|
|
|
var tbox_id = 0;
|
|
var combo_id = 3;
|
|
var has_lora = true;
|
|
|
|
switch(node.comfyClass){
|
|
case "ImpactWildcardEncode":
|
|
tbox_id = 0;
|
|
combo_id = 3;
|
|
break;
|
|
|
|
case "ImpactWildcardProcessor":
|
|
tbox_id = 0;
|
|
combo_id = 4;
|
|
has_lora = false;
|
|
break;
|
|
|
|
case "ToDetailerPipe":
|
|
case "ToDetailerPipeSDXL":
|
|
case "EditDetailerPipe":
|
|
case "EditDetailerPipeSDXL":
|
|
case "BasicPipeToDetailerPipe":
|
|
case "BasicPipeToDetailerPipeSDXL":
|
|
tbox_id = 0;
|
|
combo_id = 1;
|
|
break;
|
|
}
|
|
|
|
node.widgets[combo_id+1].callback = async (value, canvas, node, pos, e) => {
|
|
if(node) {
|
|
if(node.widgets[tbox_id].value != '')
|
|
node.widgets[tbox_id].value += ', '
|
|
|
|
node.widgets[tbox_id].value += node._wildcard_value;
|
|
|
|
// Reload wildcard status to update loaded count
|
|
if (wildcard_status.on_demand_mode) {
|
|
await load_wildcard_status();
|
|
await load_wildcards();
|
|
app.canvas.setDirty(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
Object.defineProperty(node.widgets[combo_id+1], "value", {
|
|
set: (value) => {
|
|
if (!is_wildcard_label(value))
|
|
node._wildcard_value = value;
|
|
},
|
|
get: () => { return get_wildcard_label(); }
|
|
});
|
|
|
|
Object.defineProperty(node.widgets[combo_id+1].options, "values", {
|
|
set: (x) => {},
|
|
get: () => {
|
|
return wildcards_list;
|
|
}
|
|
});
|
|
|
|
if(has_lora) {
|
|
node.widgets[combo_id].callback = (value, canvas, node, pos, e) => {
|
|
if(node) {
|
|
let lora_name = node._value;
|
|
if(lora_name.endsWith('.safetensors')) {
|
|
lora_name = lora_name.slice(0, -12);
|
|
}
|
|
|
|
node.widgets[tbox_id].value += `<lora:${lora_name}>`;
|
|
if(node.widgets_values) {
|
|
node.widgets_values[tbox_id] = node.widgets[tbox_id].value;
|
|
}
|
|
}
|
|
}
|
|
|
|
Object.defineProperty(node.widgets[combo_id], "value", {
|
|
set: (value) => {
|
|
if (value !== "Select the LoRA to add to the text")
|
|
node._value = value;
|
|
},
|
|
|
|
get: () => { return "Select the LoRA to add to the text"; }
|
|
});
|
|
}
|
|
|
|
// Preventing validation errors from occurring in any situation.
|
|
if(has_lora) {
|
|
node.widgets[combo_id].serializeValue = () => { return "Select the LoRA to add to the text"; }
|
|
}
|
|
node.widgets[combo_id+1].serializeValue = () => { return "Select the Wildcard to add to the text"; }
|
|
}
|
|
|
|
if(node.comfyClass == "ImpactWildcardProcessor" || node.comfyClass == "ImpactWildcardEncode") {
|
|
node.widgets[0].inputEl.placeholder = "Wildcard Prompt (User input)";
|
|
node.widgets[1].inputEl.placeholder = "Populated Prompt (Will be generated automatically)";
|
|
node.widgets[1].inputEl.disabled = true;
|
|
|
|
const populated_text_widget = node.widgets.find((w) => w.name == 'populated_text');
|
|
const mode_widget = node.widgets.find((w) => w.name == 'mode');
|
|
|
|
// mode combo
|
|
Object.defineProperty(mode_widget, "value", {
|
|
set: (value) => {
|
|
if(value == true)
|
|
node._mode_value = "populate";
|
|
else if(value == false)
|
|
node._mode_value = "fixed";
|
|
else
|
|
node._mode_value = value; // combo value
|
|
|
|
populated_text_widget.inputEl.disabled = node._mode_value == 'populate';
|
|
},
|
|
get: () => {
|
|
if(node._mode_value != undefined)
|
|
return node._mode_value;
|
|
else
|
|
return 'populate';
|
|
}
|
|
});
|
|
}
|
|
|
|
if (node.comfyClass == "MaskPainter") {
|
|
node.widgets[0].value = '#placeholder';
|
|
|
|
Object.defineProperty(node, "images", {
|
|
set: function(value) {
|
|
node._images = value;
|
|
},
|
|
get: function() {
|
|
const id = node.id+"";
|
|
if(node.widgets[0].value != '#placeholder') {
|
|
var need_invalidate = false;
|
|
|
|
if(input_dirty.hasOwnProperty(id) && input_dirty[id]) {
|
|
node.widgets[0].value = {...input_tracking[id][1]};
|
|
input_dirty[id] = false;
|
|
need_invalidate = true
|
|
this._images = app.nodeOutputs[id].images;
|
|
}
|
|
|
|
let filename = app.nodeOutputs[id]['aux'][1][0]['filename'];
|
|
let subfolder = app.nodeOutputs[id]['aux'][1][0]['subfolder'];
|
|
let type = app.nodeOutputs[id]['aux'][1][0]['type'];
|
|
|
|
let item =
|
|
{
|
|
image_hash: app.nodeOutputs[id]['aux'][0],
|
|
forward_filename: app.nodeOutputs[id]['aux'][1][0]['filename'],
|
|
forward_subfolder: app.nodeOutputs[id]['aux'][1][0]['subfolder'],
|
|
forward_type: app.nodeOutputs[id]['aux'][1][0]['type']
|
|
};
|
|
|
|
if(node._images) {
|
|
app.nodeOutputs[id].images = [{
|
|
...node._images[0],
|
|
...item
|
|
}];
|
|
|
|
node.widgets[0].value =
|
|
{
|
|
...node._images[0],
|
|
...item
|
|
};
|
|
}
|
|
else {
|
|
app.nodeOutputs[id].images = [{
|
|
...item
|
|
}];
|
|
|
|
node.widgets[0].value =
|
|
{
|
|
...item
|
|
};
|
|
}
|
|
|
|
if(need_invalidate) {
|
|
Promise.all(
|
|
app.nodeOutputs[id].images.map((src) => {
|
|
return new Promise((r) => {
|
|
const img = new Image();
|
|
img.onload = () => r(img);
|
|
img.onerror = () => r(null);
|
|
img.src = "/view?" + new URLSearchParams(src).toString();
|
|
});
|
|
})
|
|
).then((imgs) => {
|
|
this.imgs = imgs.filter(Boolean);
|
|
this.setSizeForImage?.();
|
|
app.graph.setDirtyCanvas(true);
|
|
});
|
|
|
|
app.nodeOutputs[id].images[0] = { ...node.widgets[0].value };
|
|
}
|
|
|
|
return app.nodeOutputs[id].images;
|
|
}
|
|
else {
|
|
return node._images;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|