import { ComfyApp, app } from "../../scripts/app.js"; import { ComfyDialog, $el } from "../../scripts/ui.js"; import { api } from "../../scripts/api.js"; let wildcards_list = []; async function load_wildcards() { let res = await api.fetchApi('/impact/wildcards/list'); let data = await res.json(); wildcards_list = data.data; } load_wildcards(); export function get_wildcards_list() { return wildcards_list; } // 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); app.registerExtension({ name: "Comfy.Impack", 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(!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 === '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; 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._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 == '*') { 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 = origin_type; } 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')) { 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}` slot_i++; } let last_slot = this.outputs[this.outputs.length - 1]; if (last_slot.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) { 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 === '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 '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) { 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._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); let origin_type = node.outputs[link_info.origin_slot].type; if(origin_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 select_slot = this.inputs.find(x => x.name == "select"); let mode_slot = this.inputs.find(x => x.name == "sel_mode"); let converted_count = 0; converted_count += select_slot?1:0; converted_count += mode_slot?1:0; if (!connected && (this.inputs.length > 1+converted_count)) { const stackTrace = new Error().stack; 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++; } } let last_slot = this.inputs[this.inputs.length - 1]; if ( (last_slot.name == 'select' && last_slot.name != 'sel_mode' && this.inputs[this.inputs.length - 2].link != undefined) || (last_slot.name != 'select' && last_slot.name != 'sel_mode' && last_slot.link != undefined)) { this.addInput(`${input_name}${slot_i}`, this.outputs[0].type); } if(this.widgets) { this.widgets[0].options.max = select_slot?this.inputs.length-1:this.inputs.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; } } } }, 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") { Object.defineProperty(node.widgets[0], "value", { set: (value) => { const stackTrace = new Error().stack; if(stackTrace.includes('inner_value_change')) { if(node.widgets[1].value.trim() != "" && !node.widgets[1].value.trim().endsWith(",")) node.widgets[1].value += ", " node.widgets[1].value += value; node.widgets_values[1] = node.widgets[1].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; } Object.defineProperty(node.widgets[combo_id+1], "value", { set: (value) => { const stackTrace = new Error().stack; if(stackTrace.includes('inner_value_change')) { if(value != "Select the Wildcard to add to the text") { if(node.widgets[tbox_id].value != '') node.widgets[tbox_id].value += ', ' node.widgets[tbox_id].value += value; } } }, get: () => { return "Select the Wildcard to add to the text"; } }); Object.defineProperty(node.widgets[combo_id+1].options, "values", { set: (x) => {}, get: () => { return wildcards_list; } }); if(has_lora) { Object.defineProperty(node.widgets[combo_id], "value", { set: (value) => { const stackTrace = new Error().stack; if(stackTrace.includes('inner_value_change')) { if(value != "Select the LoRA to add to the text") { let lora_name = value; if (lora_name.endsWith('.safetensors')) { lora_name = lora_name.slice(0, -12); } node.widgets[tbox_id].value += ``; if(node.widgets_values) { node.widgets_values[tbox_id] = node.widgets[tbox_id].value; } } } 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) => { node._mode_value = value == true || value == "Populate"; populated_text_widget.inputEl.disabled = value == true || value == "Populate"; }, get: () => { if(node._mode_value != undefined) return node._mode_value; else return true; } }); } 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; } } }); } } });