Add custom nodes, Civitai loras (LFS), and vast.ai setup script
Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Has been cancelled
Execution Tests / test (macos-latest) (push) Has been cancelled
Execution Tests / test (ubuntu-latest) (push) Has been cancelled
Execution Tests / test (windows-latest) (push) Has been cancelled
Test server launches without errors / test (push) Has been cancelled
Unit Tests / test (macos-latest) (push) Has been cancelled
Unit Tests / test (ubuntu-latest) (push) Has been cancelled
Unit Tests / test (windows-2022) (push) Has been cancelled

Includes 30 custom nodes committed directly, 7 Civitai-exclusive
loras stored via Git LFS, and a setup script that installs all
dependencies and downloads HuggingFace-hosted models on vast.ai.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 00:55:26 +00:00
parent 2b70ab9ad0
commit f09734b0ee
2274 changed files with 748556 additions and 3 deletions

View File

@@ -0,0 +1,281 @@
import { api } from "../../scripts/api.js";
import { app } from "../../scripts/app.js";
let original_show = app.ui.dialog.show;
export function customAlert(message) {
try {
app.extensionManager.toast.addAlert(message);
}
catch {
alert(message);
}
}
export function isBeforeFrontendVersion(compareVersion) {
try {
const frontendVersion = window['__COMFYUI_FRONTEND_VERSION__'];
if (typeof frontendVersion !== 'string') {
return false;
}
function parseVersion(versionString) {
const parts = versionString.split('.').map(Number);
return parts.length === 3 && parts.every(part => !isNaN(part)) ? parts : null;
}
const currentVersion = parseVersion(frontendVersion);
const comparisonVersion = parseVersion(compareVersion);
if (!currentVersion || !comparisonVersion) {
return false;
}
for (let i = 0; i < 3; i++) {
if (currentVersion[i] > comparisonVersion[i]) {
return false;
} else if (currentVersion[i] < comparisonVersion[i]) {
return true;
}
}
return false;
} catch {
return true;
}
}
function dialog_show_wrapper(html) {
if (typeof html === "string") {
if(html.includes("IMPACT-PACK-SIGNAL: STOP CONTROL BRIDGE")) {
return;
}
this.textElement.innerHTML = html;
} else {
this.textElement.replaceChildren(html);
}
this.element.style.display = "flex";
}
app.ui.dialog.show = dialog_show_wrapper;
function nodeFeedbackHandler(event) {
let nodes = app.graph._nodes_by_id;
let node = nodes[event.detail.node_id];
if(node) {
const w = node.widgets.find((w) => event.detail.widget_name === w.name);
if(w) {
w.value = event.detail.value;
}
}
}
api.addEventListener("impact-node-feedback", nodeFeedbackHandler);
function setMuteState(event) {
let nodes = app.graph._nodes_by_id;
let node = nodes[event.detail.node_id];
if(node) {
if(event.detail.is_active)
node.mode = 0;
else
node.mode = 2;
}
}
api.addEventListener("impact-node-mute-state", setMuteState);
async function bridgeContinue(event) {
let nodes = app.graph._nodes_by_id;
let node = nodes[event.detail.node_id];
if(node) {
const mutes = new Set(event.detail.mutes);
const actives = new Set(event.detail.actives);
const bypasses = new Set(event.detail.bypasses);
for(let i in app.graph._nodes_by_id) {
let this_node = app.graph._nodes_by_id[i];
if(mutes.has(i)) {
this_node.mode = 2;
}
else if(actives.has(i)) {
this_node.mode = 0;
}
else if(bypasses.has(i)) {
this_node.mode = 4;
}
}
await app.queuePrompt(0, 1);
}
}
api.addEventListener("impact-bridge-continue", bridgeContinue);
function addQueue(event) {
app.queuePrompt(0, 1);
}
api.addEventListener("impact-add-queue", addQueue);
function refreshPreview(event) {
let node_id = event.detail.node_id;
let item = event.detail.item;
let img = new Image();
img.src = `/view?filename=${item.filename}&subfolder=${item.subfolder}&type=${item.type}&no-cache=${Date.now()}`;
let node = app.graph._nodes_by_id[node_id];
if(node)
node.imgs = [img];
}
api.addEventListener("impact-preview", refreshPreview);
// ============================================================================
// MaskRectArea Shared Utilities
// ============================================================================
/**
* Reads a numeric value from a connected link by inspecting the origin node widget.
* More reliable than getInputData() in ComfyUI's frontend execution model.
*
* @param {LGraphNode} node - LiteGraph node instance
* @param {string} inputName - Name of the input to read
* @returns {number|null} The numeric value or null if not available
*/
export function readLinkedNumber(node, inputName) {
try {
if (!node || !node.graph || !Array.isArray(node.inputs)) {
return null;
}
const inp = node.inputs.find(i => i && i.name === inputName);
if (!inp || inp.link == null) {
return null;
}
const link = node.graph.links && node.graph.links[inp.link];
if (!link) {
return null;
}
const originNode = node.graph.getNodeById
? node.graph.getNodeById(link.origin_id)
: null;
if (!originNode || !Array.isArray(originNode.widgets) || originNode.widgets.length === 0) {
return null;
}
const w = originNode.widgets.find(ww => ww && ww.name === "value")
|| originNode.widgets[0];
const v = w ? w.value : null;
return (typeof v === "number") ? v : null;
} catch (e) {
return null;
}
}
/**
* Generates a color based on percentage using HSL color space.
*
* @param {number} percent - Value between 0 and 1
* @param {string} alpha - Hex alpha value (e.g., "ff", "80")
* @returns {string} Hex color string with alpha (e.g., "#ff8040ff")
*/
export function getDrawColor(percent, alpha) {
let h = 360 * percent;
let s = 50;
let l = 50;
l /= 100;
const a = s * Math.min(l, 1 - l) / 100;
const f = n => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color).toString(16).padStart(2, '0');
};
return `#${f(0)}${f(8)}${f(4)}${alpha}`;
}
/**
* Computes and adjusts canvas size for preview widgets.
*
* @param {LGraphNode} node - LiteGraph node instance
* @param {[number, number]} size - [width, height] array
* @param {number} minHeight - Minimum canvas height (REQUIRED)
* @param {number} minWidth - Minimum canvas width (REQUIRED)
* @returns {void}
*/
export function computeCanvasSize(node, size, minHeight, minWidth) {
// Validate required parameters
if (typeof minHeight !== 'number' || typeof minWidth !== 'number') {
console.warn('[computeCanvasSize] minHeight and minWidth are required parameters');
return;
}
// Null safety check for widgets array
if (!node.widgets?.length || node.widgets[0].last_y == null) {
return;
}
// LiteGraph global availability check
const NODE_WIDGET_HEIGHT = (typeof LiteGraph !== 'undefined' && LiteGraph.NODE_WIDGET_HEIGHT)
? LiteGraph.NODE_WIDGET_HEIGHT
: 20;
let y = node.widgets[0].last_y + 5;
let freeSpace = size[1] - y;
// Compute the height of all non-customCanvas widgets
let widgetHeight = 0;
for (let i = 0; i < node.widgets.length; i++) {
const w = node.widgets[i];
if (w.type !== "customCanvas") {
if (w.computeSize) {
widgetHeight += w.computeSize()[1] + 4;
} else {
widgetHeight += NODE_WIDGET_HEIGHT + 5;
}
}
}
// Ensure there is enough vertical space
freeSpace -= widgetHeight;
// Clamp minimum canvas height
if (freeSpace < minHeight) {
freeSpace = minHeight;
}
// Allow both grow and shrink to fit content
const targetHeight = y + widgetHeight + freeSpace;
if (node.size[1] !== targetHeight) {
node.size[1] = targetHeight;
node.graph.setDirtyCanvas(true);
}
// Ensure the node width meets the minimum width requirement
if (node.size[0] < minWidth) {
node.size[0] = minWidth;
node.graph.setDirtyCanvas(true);
}
// Position each of the widgets
for (const w of node.widgets) {
w.y = y;
if (w.type === "customCanvas") {
y += freeSpace;
} else if (w.computeSize) {
y += w.computeSize()[1] + 4;
} else {
y += NODE_WIDGET_HEIGHT + 4;
}
}
node.canvasHeight = freeSpace;
}

View File

@@ -0,0 +1,229 @@
import { ComfyApp, app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
function load_image(str) {
let base64String = canvas.toDataURL('image/png');
let img = new Image();
img.src = base64String;
}
function getFileItem(baseType, path) {
try {
let pathType = baseType;
if (path.endsWith("[output]")) {
pathType = "output";
path = path.slice(0, -9);
} else if (path.endsWith("[input]")) {
pathType = "input";
path = path.slice(0, -8);
} else if (path.endsWith("[temp]")) {
pathType = "temp";
path = path.slice(0, -7);
}
const subfolder = path.substring(0, path.lastIndexOf('/'));
const filename = path.substring(path.lastIndexOf('/') + 1);
return {
filename: filename,
subfolder: subfolder,
type: pathType
};
}
catch(exception) {
return null;
}
}
async function loadImageFromUrl(image, node_id, v, need_to_load) {
let item = getFileItem('temp', v);
if(item) {
let params = `?node_id=${node_id}&filename=${item.filename}&type=${item.type}&subfolder=${item.subfolder}`;
let res = await api.fetchApi('/impact/set/pb_id_image'+params, { cache: "no-store" });
if(res.status == 200) {
let pb_id = await res.text();
if(need_to_load) {;
image.src = api.apiURL(`/view?filename=${item.filename}&type=${item.type}&subfolder=${item.subfolder}`);
}
return pb_id;
}
else {
return `$${node_id}-0`;
}
}
else {
return `$${node_id}-0`;
}
}
async function loadImageFromId(image, v) {
let res = await api.fetchApi('/impact/get/pb_id_image?id='+v, { cache: "no-store" });
if(res.status == 200) {
let item = await res.json();
image.src = api.apiURL(`/view?filename=${item.filename}&type=${item.type}&subfolder=${item.subfolder}`);
return true;
}
return false;
}
app.registerExtension({
name: "Comfy.Impact.img",
nodeCreated(node, app) {
if(node.comfyClass == "PreviewBridge" || node.comfyClass == "PreviewBridgeLatent") {
let w = node.widgets.find(obj => obj.name === 'image');
node._imgs = [new Image()];
node.imageIndex = 0;
Object.defineProperty(w, 'value', {
async set(v) {
if(w._lock)
return;
const stackTrace = new Error().stack;
if(stackTrace.includes('presetText.js'))
return;
var image = new Image();
if(v && v.constructor == String && v.startsWith('$')) {
// from node feedback
let need_to_load = node._imgs[0].src == '';
if(await loadImageFromId(image, v, need_to_load)) {
w._value = v;
if(node._imgs[0].src == '') {
node._imgs = [image];
}
}
else {
w._value = `$${node.id}-0`;
}
}
else {
// from clipspace
w._lock = true;
w._value = await loadImageFromUrl(image, node.id, v, false);
w._lock = false;
}
},
get() {
if(w._value == undefined) {
w._value = `$${node.id}-0`;
}
return w._value;
}
});
Object.defineProperty(node, 'imgs', {
set(v) {
const stackTrace = new Error().stack;
if(v && v.length == 0)
return;
else if(stackTrace.includes('pasteFromClipspace')) {
let sp = new URLSearchParams(v[0].src.split("?")[1]);
let str = "";
if(sp.get('subfolder')) {
str += sp.get('subfolder') + '/';
}
str += `${sp.get("filename")} [${sp.get("type")}]`;
w.value = str;
}
node._imgs = v;
},
get() {
return node._imgs;
}
});
}
if(node.comfyClass == "ImageReceiver") {
let path_widget = node.widgets.find(obj => obj.name === 'image');
let w = node.widgets.find(obj => obj.name === 'image_data');
let stw_widget = node.widgets.find(obj => obj.name === 'save_to_workflow');
w._value = "";
Object.defineProperty(w, 'value', {
set(v) {
if(v != '[IMAGE DATA]')
w._value = v;
},
get() {
const stackTrace = new Error().stack;
if(!stackTrace.includes('draw') && !stackTrace.includes('graphToPrompt') && stackTrace.includes('app.js')) {
return "[IMAGE DATA]";
}
else {
if(stw_widget.value)
return w._value;
else
return "";
}
}
});
let set_img_act = (v) => {
node._img = v;
var canvas = document.createElement('canvas');
canvas.width = v[0].width;
canvas.height = v[0].height;
var context = canvas.getContext('2d');
context.drawImage(v[0], 0, 0, v[0].width, v[0].height);
var base64Image = canvas.toDataURL('image/png');
w.value = base64Image;
};
Object.defineProperty(node, 'imgs', {
set(v) {
if (v && !v[0].complete) {
let orig_onload = v[0].onload;
v[0].onload = function(v2) {
if(orig_onload)
orig_onload();
set_img_act(v);
};
}
else {
set_img_act(v);
}
},
get() {
if(this._img == undefined && w.value != '') {
this._img = [new Image()];
if(stw_widget.value && w.value != '[IMAGE DATA]')
this._img[0].src = w.value;
}
else if(this._img == undefined && path_widget.value) {
let image = new Image();
image.src = path_widget.value;
try {
let item = getFileItem('temp', path_widget.value);
let params = `?filename=${item.filename}&type=${item.type}&subfolder=${item.subfolder}`;
let res = api.fetchApi('/view/validate'+params, { cache: "no-store" }).then(response => response);
if(res.status == 200) {
image.src = api.apiURL('/view'+params);
}
this._img = [new Image()]; // placeholder
image.onload = function(v) {
set_img_act([image]);
};
}
catch {
}
}
return this._img;
}
});
}
}
})

View File

@@ -0,0 +1,988 @@
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')) {
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')) {
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(link_info.target_slot == 0 && this.inputs.length > 3) { // NOTE: widgets are regarded as input since new front
origin_type = this.inputs[1].type;
node.connect(link_info.origin_slot, node.id, 'input1');
}
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);
}
}
}
},
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;
}
}
});
}
}
});

View File

@@ -0,0 +1,641 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import { ComfyDialog, $el } from "../../scripts/ui.js";
import { ComfyApp } from "../../scripts/app.js";
import { ClipspaceDialog } from "../../extensions/core/clipspace.js";
function addMenuHandler(nodeType, cb) {
const getOpts = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function () {
const r = getOpts.apply(this, arguments);
cb.apply(this, arguments);
return r;
};
}
// Helper function to convert a data URL to a Blob object
function dataURLToBlob(dataURL) {
const parts = dataURL.split(';base64,');
const contentType = parts[0].split(':')[1];
const byteString = atob(parts[1]);
const arrayBuffer = new ArrayBuffer(byteString.length);
const uint8Array = new Uint8Array(arrayBuffer);
for (let i = 0; i < byteString.length; i++) {
uint8Array[i] = byteString.charCodeAt(i);
}
return new Blob([arrayBuffer], { type: contentType });
}
function loadedImageToBlob(image) {
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
const dataURL = canvas.toDataURL('image/png', 1);
const blob = dataURLToBlob(dataURL);
return blob;
}
async function uploadMask(filepath, formData) {
await api.fetchApi('/upload/mask', {
method: 'POST',
body: formData
}).then(response => {}).catch(error => {
console.error('Error:', error);
});
ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']] = new Image();
ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src = `view?filename=${filepath.filename}&type=${filepath.type}`;
if(ComfyApp.clipspace.images)
ComfyApp.clipspace.images[ComfyApp.clipspace['selectedIndex']] = filepath;
ClipspaceDialog.invalidatePreview();
}
class ImpactSamEditorDialog extends ComfyDialog {
static instance = null;
static getInstance() {
if(!ImpactSamEditorDialog.instance) {
ImpactSamEditorDialog.instance = new ImpactSamEditorDialog();
}
return ImpactSamEditorDialog.instance;
}
constructor() {
super();
this.element = $el("div.comfy-modal", { parent: document.body },
[ $el("div.comfy-modal-content",
[...this.createButtons()]),
]);
}
createButtons() {
return [];
}
createButton(name, callback) {
var button = document.createElement("button");
button.innerText = name;
button.addEventListener("click", callback);
return button;
}
createLeftButton(name, callback) {
var button = this.createButton(name, callback);
button.style.cssFloat = "left";
button.style.marginRight = "4px";
return button;
}
createRightButton(name, callback) {
var button = this.createButton(name, callback);
button.style.cssFloat = "right";
button.style.marginLeft = "4px";
return button;
}
createLeftSlider(self, name, callback) {
const divElement = document.createElement('div');
divElement.id = "sam-confidence-slider";
divElement.style.cssFloat = "left";
divElement.style.fontFamily = "sans-serif";
divElement.style.marginRight = "4px";
divElement.style.color = "var(--input-text)";
divElement.style.backgroundColor = "var(--comfy-input-bg)";
divElement.style.borderRadius = "8px";
divElement.style.borderColor = "var(--border-color)";
divElement.style.borderStyle = "solid";
divElement.style.fontSize = "15px";
divElement.style.height = "21px";
divElement.style.padding = "1px 6px";
divElement.style.display = "flex";
divElement.style.position = "relative";
divElement.style.top = "2px";
self.confidence_slider_input = document.createElement('input');
self.confidence_slider_input.setAttribute('type', 'range');
self.confidence_slider_input.setAttribute('min', '0');
self.confidence_slider_input.setAttribute('max', '100');
self.confidence_slider_input.setAttribute('value', '70');
const labelElement = document.createElement("label");
labelElement.textContent = name;
divElement.appendChild(labelElement);
divElement.appendChild(self.confidence_slider_input);
self.confidence_slider_input.addEventListener("change", callback);
return divElement;
}
async detect_and_invalidate_mask_canvas(self) {
const mask_img = await self.detect(self);
const canvas = self.maskCtx.canvas;
const ctx = self.maskCtx;
ctx.clearRect(0, 0, canvas.width, canvas.height);
await new Promise((resolve, reject) => {
self.mask_image = new Image();
self.mask_image.onload = function() {
ctx.drawImage(self.mask_image, 0, 0, canvas.width, canvas.height);
resolve();
};
self.mask_image.onerror = reject;
self.mask_image.src = mask_img.src;
});
}
setlayout(imgCanvas, maskCanvas, pointsCanvas) {
const self = this;
// If it is specified as relative, using it only as a hidden placeholder for padding is recommended
// to prevent anomalies where it exceeds a certain size and goes outside of the window.
var placeholder = document.createElement("div");
placeholder.style.position = "relative";
placeholder.style.height = "50px";
var bottom_panel = document.createElement("div");
bottom_panel.style.position = "absolute";
bottom_panel.style.bottom = "0px";
bottom_panel.style.left = "20px";
bottom_panel.style.right = "20px";
bottom_panel.style.height = "50px";
var brush = document.createElement("div");
brush.id = "sam-brush";
brush.style.backgroundColor = "blue";
brush.style.outline = "2px solid pink";
brush.style.borderRadius = "50%";
brush.style.MozBorderRadius = "50%";
brush.style.WebkitBorderRadius = "50%";
brush.style.position = "absolute";
brush.style.zIndex = 100;
brush.style.pointerEvents = "none";
this.brush = brush;
this.element.appendChild(imgCanvas);
this.element.appendChild(maskCanvas);
this.element.appendChild(pointsCanvas);
this.element.appendChild(placeholder); // must below z-index than bottom_panel to avoid covering button
this.element.appendChild(bottom_panel);
document.body.appendChild(brush);
this.brush_size = 5;
var confidence_slider = this.createLeftSlider(self, "Confidence", (event) => {
self.confidence = event.target.value;
});
var clearButton = this.createLeftButton("Clear", () => {
self.maskCtx.clearRect(0, 0, self.maskCanvas.width, self.maskCanvas.height);
self.pointsCtx.clearRect(0, 0, self.pointsCanvas.width, self.pointsCanvas.height);
self.prompt_points = [];
self.invalidatePointsCanvas(self);
});
var detectButton = this.createLeftButton("Detect", () => self.detect_and_invalidate_mask_canvas(self));
var cancelButton = this.createRightButton("Cancel", () => {
document.removeEventListener("mouseup", ImpactSamEditorDialog.handleMouseUp);
document.removeEventListener("keydown", ImpactSamEditorDialog.handleKeyDown);
self.close();
});
self.saveButton = this.createRightButton("Save", () => {
document.removeEventListener("mouseup", ImpactSamEditorDialog.handleMouseUp);
document.removeEventListener("keydown", ImpactSamEditorDialog.handleKeyDown);
self.save(self);
});
var undoButton = this.createLeftButton("Undo", () => {
if(self.prompt_points.length > 0) {
self.prompt_points.pop();
self.pointsCtx.clearRect(0, 0, self.pointsCanvas.width, self.pointsCanvas.height);
self.invalidatePointsCanvas(self);
}
});
bottom_panel.appendChild(clearButton);
bottom_panel.appendChild(detectButton);
bottom_panel.appendChild(self.saveButton);
bottom_panel.appendChild(cancelButton);
bottom_panel.appendChild(confidence_slider);
bottom_panel.appendChild(undoButton);
imgCanvas.style.position = "relative";
imgCanvas.style.top = "200";
imgCanvas.style.left = "0";
maskCanvas.style.position = "absolute";
maskCanvas.style.opacity = 0.5;
pointsCanvas.style.position = "absolute";
}
show() {
this.mask_image = null;
self.prompt_points = [];
this.message_box = $el("p", ["Please wait a moment while the SAM model and the image are being loaded."]);
this.element.appendChild(this.message_box);
if(self.imgCtx) {
self.imgCtx.clearRect(0, 0, self.imageCanvas.width, self.imageCanvas.height);
}
const target_image_path = ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src;
this.load_sam(target_image_path);
if(!this.is_layout_created) {
// layout
const imgCanvas = document.createElement('canvas');
const maskCanvas = document.createElement('canvas');
const pointsCanvas = document.createElement('canvas');
imgCanvas.id = "imageCanvas";
maskCanvas.id = "samEditorMaskCanvas";
pointsCanvas.id = "pointsCanvas";
this.setlayout(imgCanvas, maskCanvas, pointsCanvas);
// prepare content
this.imgCanvas = imgCanvas;
this.maskCanvas = maskCanvas;
this.pointsCanvas = pointsCanvas;
this.maskCtx = maskCanvas.getContext('2d');
this.pointsCtx = pointsCanvas.getContext('2d');
this.is_layout_created = true;
// replacement of onClose hook since close is not real close
const self = this;
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
if(self.last_display_style && self.last_display_style != 'none' && self.element.style.display == 'none') {
ComfyApp.onClipspaceEditorClosed();
}
self.last_display_style = self.element.style.display;
}
});
});
const config = { attributes: true };
observer.observe(this.element, config);
}
this.setImages(target_image_path, this.imgCanvas, this.pointsCanvas);
if(ComfyApp.clipspace_return_node) {
this.saveButton.innerText = "Save to node";
}
else {
this.saveButton.innerText = "Save";
}
this.saveButton.disabled = true;
this.element.style.display = "block";
this.element.style.zIndex = 8888; // NOTE: alert dialog must be high priority.
}
updateBrushPreview(self, event) {
event.preventDefault();
const centerX = event.pageX;
const centerY = event.pageY;
const brush = self.brush;
brush.style.width = self.brush_size * 2 + "px";
brush.style.height = self.brush_size * 2 + "px";
brush.style.left = (centerX - self.brush_size) + "px";
brush.style.top = (centerY - self.brush_size) + "px";
}
setImages(target_image_path, imgCanvas, pointsCanvas) {
const imgCtx = imgCanvas.getContext('2d');
const maskCtx = this.maskCtx;
const maskCanvas = this.maskCanvas;
const self = this;
// image load
const orig_image = new Image();
window.addEventListener("resize", () => {
// repositioning
imgCanvas.width = window.innerWidth - 250;
imgCanvas.height = window.innerHeight - 200;
// redraw image
let drawWidth = orig_image.width;
let drawHeight = orig_image.height;
if (orig_image.width > imgCanvas.width) {
drawWidth = imgCanvas.width;
drawHeight = (drawWidth / orig_image.width) * orig_image.height;
}
if (drawHeight > imgCanvas.height) {
drawHeight = imgCanvas.height;
drawWidth = (drawHeight / orig_image.height) * orig_image.width;
}
imgCtx.drawImage(orig_image, 0, 0, drawWidth, drawHeight);
// update mask
let w = (drawWidth * imgCanvas.clientWidth/imgCanvas.width) + "px";
let h = (drawHeight * imgCanvas.clientHeight/imgCanvas.height) + "px";
pointsCanvas.width = drawWidth * imgCanvas.clientWidth/imgCanvas.width;
pointsCanvas.height = drawHeight * imgCanvas.clientHeight/imgCanvas.height;
pointsCanvas.style.top = imgCanvas.offsetTop + "px";
pointsCanvas.style.left = imgCanvas.offsetLeft + "px";
maskCanvas.width = pointsCanvas.width;
maskCanvas.height = pointsCanvas.height;
maskCanvas.style.top = imgCanvas.offsetTop + "px";
maskCanvas.style.left = imgCanvas.offsetLeft + "px";
self.invalidateMaskCanvas(self);
self.invalidatePointsCanvas(self);
});
// original image load
orig_image.onload = () => self.onLoaded(self);
const rgb_url = new URL(target_image_path);
rgb_url.searchParams.delete('channel');
rgb_url.searchParams.set('channel', 'rgb');
orig_image.src = rgb_url;
self.image = orig_image;
}
onLoaded(self) {
if(self.message_box) {
self.element.removeChild(self.message_box);
self.message_box = null;
}
window.dispatchEvent(new Event('resize'));
self.setEventHandler(pointsCanvas);
self.saveButton.disabled = false;
}
setEventHandler(targetCanvas) {
targetCanvas.addEventListener("contextmenu", (event) => {
event.preventDefault();
});
const self = this;
targetCanvas.addEventListener('pointermove', (event) => this.updateBrushPreview(self,event));
targetCanvas.addEventListener('pointerdown', (event) => this.handlePointerDown(self,event));
targetCanvas.addEventListener('pointerover', (event) => { this.brush.style.display = "block"; });
targetCanvas.addEventListener('pointerleave', (event) => { this.brush.style.display = "none"; });
document.addEventListener('keydown', ImpactSamEditorDialog.handleKeyDown);
}
static handleKeyDown(event) {
const self = ImpactSamEditorDialog.instance;
if (event.key === '=') { // positive
brush.style.backgroundColor = "blue";
brush.style.outline = "2px solid pink";
self.is_positive_mode = true;
} else if (event.key === '-') { // negative
brush.style.backgroundColor = "red";
brush.style.outline = "2px solid skyblue";
self.is_positive_mode = false;
}
}
is_positive_mode = true;
prompt_points = [];
confidence = 70;
invalidatePointsCanvas(self) {
const ctx = self.pointsCtx;
for (const i in self.prompt_points) {
const [is_positive, x, y] = self.prompt_points[i];
const scaledX = x * ctx.canvas.width / self.image.width;
const scaledY = y * ctx.canvas.height / self.image.height;
if(is_positive)
ctx.fillStyle = "blue";
else
ctx.fillStyle = "red";
ctx.beginPath();
ctx.arc(scaledX, scaledY, 3, 0, 3 * Math.PI);
ctx.fill();
}
}
invalidateMaskCanvas(self) {
if(self.mask_image) {
self.maskCtx.clearRect(0, 0, self.maskCanvas.width, self.maskCanvas.height);
self.maskCtx.drawImage(self.mask_image, 0, 0, self.maskCanvas.width, self.maskCanvas.height);
}
}
async load_sam(url) {
const parsedUrl = new URL(url);
const searchParams = new URLSearchParams(parsedUrl.search);
const filename = searchParams.get("filename") || "";
const fileType = searchParams.get("type") || "";
const subfolder = searchParams.get("subfolder") || "";
const data = {
sam_model_name: "auto",
filename: filename,
type: fileType,
subfolder: subfolder
};
api.fetchApi('/sam/prepare', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
}
async detect(self) {
const positive_points = [];
const negative_points = [];
for(const i in self.prompt_points) {
const [is_positive, x, y] = self.prompt_points[i];
const point = [x,y];
if(is_positive) {
positive_points.push(point);
}
else
negative_points.push(point);
}
const data = {
positive_points: positive_points,
negative_points: negative_points,
threshold: self.confidence/100
};
const response = await api.fetchApi('/sam/detect', {
method: 'POST',
headers: { 'Content-Type': 'image/png' },
body: JSON.stringify(data)
});
const blob = await response.blob();
const url = URL.createObjectURL(blob);
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = reject;
image.src = url;
});
}
handlePointerDown(self, event) {
if ([0, 2, 5].includes(event.button)) {
event.preventDefault();
const x = event.offsetX || event.targetTouches[0].clientX - maskRect.left;
const y = event.offsetY || event.targetTouches[0].clientY - maskRect.top;
const originalX = x * self.image.width / self.pointsCanvas.clientWidth;
const originalY = y * self.image.height / self.pointsCanvas.clientHeight;
var point = null;
if (event.button == 0) {
// positive
point = [true, originalX, originalY];
} else {
// negative
point = [false, originalX, originalY];
}
self.prompt_points.push(point);
self.invalidatePointsCanvas(self);
}
}
async save(self) {
if(!self.mask_image) {
this.close();
return;
}
const save_canvas = document.createElement('canvas');
const save_ctx = save_canvas.getContext('2d', {willReadFrequently:true});
save_canvas.width = self.mask_image.width;
save_canvas.height = self.mask_image.height;
save_ctx.drawImage(self.mask_image, 0, 0, save_canvas.width, save_canvas.height);
const save_data = save_ctx.getImageData(0, 0, save_canvas.width, save_canvas.height);
// refine mask image
for (let i = 0; i < save_data.data.length; i += 4) {
if(save_data.data[i]) {
save_data.data[i+3] = 0;
}
else {
save_data.data[i+3] = 255;
}
save_data.data[i] = 0;
save_data.data[i+1] = 0;
save_data.data[i+2] = 0;
}
save_ctx.globalCompositeOperation = 'source-over';
save_ctx.putImageData(save_data, 0, 0);
const formData = new FormData();
const filename = "clipspace-mask-" + performance.now() + ".png";
const item =
{
"filename": filename,
"subfolder": "",
"type": "temp",
};
if(ComfyApp.clipspace.images)
ComfyApp.clipspace.images[0] = item;
if(ComfyApp.clipspace.widgets) {
const index = ComfyApp.clipspace.widgets.findIndex(obj => obj.name === 'image');
if(index >= 0)
ComfyApp.clipspace.widgets[index].value = `${filename} [temp]`;
}
const dataURL = save_canvas.toDataURL();
const blob = dataURLToBlob(dataURL);
let original_url = new URL(this.image.src);
const original_ref = { filename: original_url.searchParams.get('filename') };
let original_subfolder = original_url.searchParams.get("subfolder");
if(original_subfolder)
original_ref.subfolder = original_subfolder;
let original_type = original_url.searchParams.get("type");
if(original_type)
original_ref.type = original_type;
formData.append('image', blob, filename);
formData.append('original_ref', JSON.stringify(original_ref));
formData.append('type', "temp");
await uploadMask(item, formData);
ComfyApp.onClipspaceEditorSave();
this.close();
}
}
app.registerExtension({
name: "Comfy.Impact.SAMEditor",
init(app) {
const callback =
function () {
let dlg = ImpactSamEditorDialog.getInstance();
dlg.show();
};
const context_predicate = () => ComfyApp.clipspace && ComfyApp.clipspace.imgs && ComfyApp.clipspace.imgs.length > 0
ClipspaceDialog.registerButton("Impact SAM Detector", context_predicate, callback);
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (Array.isArray(nodeData.output) && (nodeData.output.includes("MASK") || nodeData.output.includes("IMAGE"))) {
addMenuHandler(nodeType, function (_, options) {
options.unshift({
content: "Open in SAM Detector",
callback: () => {
ComfyApp.copyToClipspace(this);
ComfyApp.clipspace_return_node = this;
let dlg = ImpactSamEditorDialog.getInstance();
dlg.show();
},
});
});
}
}
});

View File

@@ -0,0 +1,182 @@
import { ComfyApp, app } from "../../scripts/app.js";
import { ComfyDialog, $el } from "../../scripts/ui.js";
import { api } from "../../scripts/api.js";
async function open_picker(node) {
const resp = await api.fetchApi(`/impact/segs/picker/count?id=${node.id}`);
const body = await resp.text();
let cnt = parseInt(body);
var existingPicker = document.getElementById('impact-picker');
if (existingPicker) {
existingPicker.parentNode.removeChild(existingPicker);
}
var gallery = document.createElement('div');
gallery.id = 'impact-picker';
gallery.style.position = "absolute";
gallery.style.height = "80%";
gallery.style.width = "80%";
gallery.style.top = "10%";
gallery.style.left = "10%";
gallery.style.display = 'flex';
gallery.style.flexWrap = 'wrap';
gallery.style.maxHeight = '600px';
gallery.style.overflow = 'auto';
gallery.style.backgroundColor = 'rgba(0,0,0,0.3)';
gallery.style.padding = '20px';
gallery.draggable = false;
gallery.style.zIndex = 5000;
var doneButton = document.createElement('button');
doneButton.textContent = 'Done';
doneButton.style.padding = '10px 10px';
doneButton.style.border = 'none';
doneButton.style.borderRadius = '5px';
doneButton.style.fontFamily = 'Arial, sans-serif';
doneButton.style.fontSize = '16px';
doneButton.style.fontWeight = 'bold';
doneButton.style.color = '#fff';
doneButton.style.background = 'linear-gradient(to bottom, #0070B8, #003D66)';
doneButton.style.boxShadow = '0 2px 4px rgba(0, 0, 0, 0.4)';
doneButton.style.margin = "20px";
doneButton.style.height = "40px";
var cancelButton = document.createElement('button');
cancelButton.textContent = 'Cancel';
cancelButton.style.padding = '10px 10px';
cancelButton.style.border = 'none';
cancelButton.style.borderRadius = '5px';
cancelButton.style.fontFamily = 'Arial, sans-serif';
cancelButton.style.fontSize = '16px';
cancelButton.style.fontWeight = 'bold';
cancelButton.style.color = '#fff';
cancelButton.style.background = 'linear-gradient(to bottom, #ff70B8, #ff3D66)';
cancelButton.style.boxShadow = '0 2px 4px rgba(0, 0, 0, 0.4)';
cancelButton.style.margin = "20px";
cancelButton.style.height = "40px";
const w = node.widgets.find((w) => w.name == 'picks');
let prev_selected = w.value.split(',').map(function(item) {
return parseInt(item, 10);
});
let images = [];
doneButton.onclick = () => {
var result = '';
for(let i in images) {
if(images[i].isSelected) {
if(result != '')
result += ', ';
result += (parseInt(i)+1);
}
}
w.value = result;
gallery.parentNode.removeChild(gallery);
}
cancelButton.onclick = () => {
gallery.parentNode.removeChild(gallery);
}
var panel = document.createElement('div');
panel.style.clear = 'both';
panel.style.width = '100%';
panel.style.height = '40px';
panel.style.justifyContent = 'center';
panel.style.alignItems = 'center';
panel.style.display = 'flex';
panel.appendChild(doneButton);
panel.appendChild(cancelButton);
gallery.appendChild(panel);
var hint = document.createElement('label');
hint.style.position = 'absolute';
hint.innerHTML = 'Click: Toggle Selection<BR>Ctrl-click: Single Selection';
gallery.appendChild(hint);
let max_size = 300;
for(let i=0; i<cnt; i++) {
let image = new Image();
image.src = `/impact/segs/picker/view?id=${node.id}&idx=${i}`;
image.style.margin = '10px';
image.draggable = false;
images.push(image);
image.isSelected = prev_selected.includes(i + 1);
if(image.isSelected) {
image.style.border = '2px solid #006699';
}
image.onload = function() {
var ratio = 1.0;
if(image.naturalWidth > image.naturalHeight) {
ratio = max_size/image.naturalWidth;
}
else {
ratio = max_size/image.naturalHeight;
}
let width = image.naturalWidth * ratio;
let height = image.naturalHeight * ratio;
if(width < height) {
this.style.marginLeft = (200-width)/2+"px";
}
else{
this.style.marginTop = (200-height)/2+"px";
}
this.style.width = width+"px";
this.style.height = height+"px";
this.style.objectFit = 'cover';
}
image.addEventListener('click', function(event) {
if(event.ctrlKey) {
for(let i in images) {
if(images[i].isSelected) {
images[i].style.border = 'none';
images[i].isSelected = false;
}
}
image.style.border = '2px solid #006699';
image.isSelected = true;
return;
}
if(image.isSelected) {
image.style.border = 'none';
image.isSelected = false;
}
else {
image.style.border = '2px solid #006699';
image.isSelected = true;
}
});
gallery.appendChild(image);
}
document.body.appendChild(gallery);
}
app.registerExtension({
name: "Comfy.Impack.Picker",
nodeCreated(node, app) {
if(node.comfyClass == "ImpactSEGSPicker") {
node.addWidget("button", "pick", "image", () => {
open_picker(node);
});
}
}
});

View File

@@ -0,0 +1,459 @@
import { app } from "../../scripts/app.js";
import { readLinkedNumber, getDrawColor, computeCanvasSize } from "./common.js";
function showPreviewCanvas(node, app) {
const widget = {
type: "customCanvas",
name: "mask-rect-area-canvas",
get value() {
return this.canvas.value;
},
set value(x) {
this.canvas.value = x;
},
draw: function (ctx, node, widgetWidth, widgetY) {
// If we are initially offscreen when created we wont have received a resize event
// Calculate it here instead
if (!node.canvasHeight) {
computeCanvasSize(node, node.size, 220, 240);
}
const visible = true;
const t = ctx.getTransform();
const margin = 12;
const border = 2;
const widgetHeight = node.canvasHeight;
// Keep preview in sync when inputs are driven by links.
syncLinkedInputsToPropertiesAdvanced(node);
const width = Math.max(1, Math.round(node.properties["width"]));
const height = Math.max(1, Math.round(node.properties["height"]));
const scale = Math.min(
(widgetWidth - margin * 3) / width,
(widgetHeight - margin * 3) / height
);
const blurRadius = node.properties["blur_radius"] || 0;
const index = 0;
Object.assign(this.canvas.style, {
left: `${t.e}px`,
top: `${t.f + (widgetY * t.d)}px`,
width: `${widgetWidth * t.a}px`,
height: `${widgetHeight * t.d}px`,
position: "absolute",
zIndex: 1,
fontSize: `${t.d * 10.0}px`,
pointerEvents: "none"
});
this.canvas.hidden = !visible;
let backgroundWidth = width * scale;
let backgroundHeight = height * scale;
let xOffset = margin;
if (backgroundWidth < widgetWidth) {
xOffset += (widgetWidth - backgroundWidth) / 2 - margin;
}
let yOffset = (margin / 2);
if (backgroundHeight < widgetHeight) {
yOffset += (widgetHeight - backgroundHeight) / 2 - margin;
}
let widgetX = xOffset;
widgetY = widgetY + yOffset;
// Draw the background border
ctx.fillStyle = globalThis.LiteGraph.WIDGET_OUTLINE_COLOR;
ctx.fillRect(widgetX - border, widgetY - border, backgroundWidth + border * 2, backgroundHeight + border * 2)
// Draw the main background area
ctx.fillStyle = globalThis.LiteGraph.WIDGET_BGCOLOR;
ctx.fillRect(widgetX, widgetY, backgroundWidth, backgroundHeight);
// Draw the conditioning zone
let [x, y, w, h] = getDrawArea(node, backgroundWidth, backgroundHeight);
ctx.fillStyle = getDrawColor(0, "80");
ctx.fillRect(widgetX + x, widgetY + y, w, h);
ctx.beginPath();
ctx.lineWidth = 1;
// Draw grid lines
for (let x = 0; x <= width / 64; x += 1) {
ctx.moveTo(widgetX + x * 64 * scale, widgetY);
ctx.lineTo(widgetX + x * 64 * scale, widgetY + backgroundHeight);
}
for (let y = 0; y <= height / 64; y += 1) {
ctx.moveTo(widgetX, widgetY + y * 64 * scale);
ctx.lineTo(widgetX + backgroundWidth, widgetY + y * 64 * scale);
}
ctx.strokeStyle = "#66666650";
ctx.stroke();
ctx.closePath();
// Draw current zone
let [sx, sy, sw, sh] = getDrawArea(node, backgroundWidth, backgroundHeight);
ctx.fillStyle = getDrawColor(0, "80");
ctx.fillRect(widgetX + sx, widgetY + sy, sw, sh);
ctx.fillStyle = getDrawColor(0, "40");
ctx.fillRect(widgetX + sx + border, widgetY + sy + border, sw - border * 2, sh - border * 2);
// Draw white border around the current zone
ctx.strokeStyle = globalThis.LiteGraph.NODE_SELECTED_TITLE_COLOR;
ctx.lineWidth = 2;
ctx.strokeRect(widgetX + sx, widgetY + sy, sw, sh);
// Display
ctx.beginPath();
ctx.arc(LiteGraph.NODE_SLOT_HEIGHT * 0.5, LiteGraph.NODE_SLOT_HEIGHT * (index + 0.5) + 4, 4, 0, Math.PI * 2);
ctx.fill();
ctx.lineWidth = 1;
ctx.strokeStyle = "white";
ctx.stroke();
ctx.lineWidth = 1;
ctx.closePath();
// Draw progress bar canvas
if (backgroundWidth < widgetWidth) {
xOffset += (widgetWidth - backgroundWidth) / 2 - margin;
}
// Adjust X and Y coordinates
const barHeight = 8;
let widgetYBar = widgetY + backgroundHeight + margin;
// Draw the border around the progress bar
ctx.fillStyle = globalThis.LiteGraph.WIDGET_OUTLINE_COLOR;
ctx.fillRect(
widgetX - border,
widgetYBar - border,
backgroundWidth + border * 2,
barHeight + border * 2
);
// Draw the main bar area (background)
ctx.fillStyle = globalThis.LiteGraph.WIDGET_BGCOLOR;
ctx.fillRect(
widgetX,
widgetYBar,
backgroundWidth,
barHeight
);
// Draw progress bar grid
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = "#66666650";
// Calculate the number of grid lines based on the bar size
const numLines = Math.floor(backgroundWidth / 64);
// Draw grid lines
for (let x = 0; x <= width / 64; x += 1) {
ctx.moveTo(widgetX + x * 64 * scale, widgetYBar);
ctx.lineTo(widgetX + x * 64 * scale, widgetYBar + barHeight);
}
ctx.stroke();
ctx.closePath();
// Draw progress (based on blur_radius)
const progress = Math.min(blurRadius / 255, 1);
ctx.fillStyle = "rgba(0, 120, 255, 0.5)";
ctx.fillRect(
widgetX,
widgetYBar,
backgroundWidth * progress,
barHeight
);
}
};
widget.canvas = document.createElement("canvas");
widget.canvas.className = "mask-rect-area-canvas";
widget.parent = node;
widget.computeLayoutSize = function (node) {
return {
minHeight: 200,
maxHeight: 300
};
};
document.body.appendChild(widget.canvas);
node.addCustomWidget(widget);
app.canvas.onDrawBackground = function () {
// Draw node isnt fired once the node is off the screen
// if it goes off screen quickly, the input may not be removed
// this shifts it off screen so it can be moved back if the node is visible.
for (let n in app.graph._nodes) {
n = app.graph._nodes[n];
for (let w in n.widgets) {
let wid = n.widgets[w];
if (Object.hasOwn(wid, "canvas")) {
wid.canvas.style.left = -8000 + "px";
wid.canvas.style.position = "absolute";
}
}
}
};
node.onResize = function (size) {
computeCanvasSize(node, size, 220, 240);
};
return {minWidth: 200, minHeight: 200, widget};
}
app.registerExtension({
name: "drltdata.MaskRectAreaAdvanced",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== "MaskRectAreaAdvanced") {
return;
}
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
this.setProperty("width", 512);
this.setProperty("height", 512);
this.setProperty("x", 0);
this.setProperty("y", 0);
this.setProperty("w", 256);
this.setProperty("h", 256);
this.setProperty("blur_radius", 0);
this.selected = false;
this.index = 3;
this.serialize_widgets = true;
// If the node already provides widgets from Python/ComfyUI, do NOT recreate them
const hasExisting = Array.isArray(this.widgets) && this.widgets.some(w => w && w.name === "x");
// Helper: attach callbacks to existing widgets to keep node.properties in sync (canvas preview).
const hookWidget = (node, widgetName, propName, opts) => {
if (!Array.isArray(node.widgets)) {
return;
}
const w = node.widgets.find(ww => ww && ww.name === widgetName);
if (!w) {
return;
}
const min = (opts && typeof opts.min === "number") ? opts.min : undefined;
const max = (opts && typeof opts.max === "number") ? opts.max : undefined;
const step = (opts && typeof opts.step === "number") ? opts.step : undefined;
if (node.properties && Object.prototype.hasOwnProperty.call(node.properties, propName)) {
w.value = node.properties[propName];
} else {
node.properties[propName] = w.value;
}
const prevCb = w.callback;
w.callback = function (v, ...args) {
let val = v;
if (typeof val === "number") {
if (typeof step === "number" && step > 0) {
const s = step / 10;
val = Math.round(val / s) * s;
} else {
val = Math.round(val);
}
if (typeof min === "number") {
val = Math.max(min, val);
}
if (typeof max === "number") {
val = Math.min(max, val);
}
}
this.value = val;
node.properties[propName] = val;
if (prevCb) {
return prevCb.call(this, val, ...args);
}
};
};
if (hasExisting) {
hookWidget(this, "x", "x", {"step": 10});
hookWidget(this, "y", "y", {"step": 10});
hookWidget(this, "width", "w", {"step": 10});
hookWidget(this, "height", "h", {"step": 10});
hookWidget(this, "image_width", "width", {"step": 10});
hookWidget(this, "image_height", "height", {"step": 10});
hookWidget(this, "blur_radius", "blur_radius", {"min": 0, "max": 255, "step": 10});
} else {
CUSTOM_INT(this, "x", 0, function (v, _, node) {
const s = this.options.step / 10;
this.value = Math.round(v / s) * s;
node.properties["x"] = this.value;
});
CUSTOM_INT(this, "y", 0, function (v, _, node) {
const s = this.options.step / 10;
this.value = Math.round(v / s) * s;
node.properties["y"] = this.value;
});
CUSTOM_INT(this, "width", 256, function (v, _, node) {
const s = this.options.step / 10;
this.value = Math.round(v / s) * s;
node.properties["w"] = this.value;
});
CUSTOM_INT(this, "height", 256, function (v, _, node) {
const s = this.options.step / 10;
this.value = Math.round(v / s) * s;
node.properties["h"] = this.value;
});
CUSTOM_INT(this, "image_width", 512, function (v, _, node) {
const s = this.options.step / 10;
this.value = Math.round(v / s) * s;
node.properties["width"] = this.value;
});
CUSTOM_INT(this, "image_height", 512, function (v, _, node) {
const s = this.options.step / 10;
this.value = Math.round(v / s) * s;
node.properties["height"] = this.value;
});
CUSTOM_INT(this, "blur_radius", 0, function (v, _, node) {
this.value = Math.round(v) || 0;
node.properties["blur_radius"] = this.value;
},
{"min": 0, "max": 255, "step": 10}
);
}
showPreviewCanvas(this, app);
this.onSelected = function () {
this.selected = true;
};
this.onDeselected = function () {
this.selected = false;
};
return r;
};
}
});
// Calculate the drawing area using individual properties.
function getDrawArea(node, backgroundWidth, backgroundHeight) {
let x = node.properties["x"] * backgroundWidth / node.properties["width"];
let y = node.properties["y"] * backgroundHeight / node.properties["height"];
let w = node.properties["w"] * backgroundWidth / node.properties["width"];
let h = node.properties["h"] * backgroundHeight / node.properties["height"];
if (x > backgroundWidth) {
x = backgroundWidth;
}
if (y > backgroundHeight) {
y = backgroundHeight;
}
if (x + w > backgroundWidth) {
w = Math.max(0, backgroundWidth - x);
}
if (y + h > backgroundHeight) {
h = Math.max(0, backgroundHeight - y);
}
return [x, y, w, h];
}
function CUSTOM_INT(node, inputName, val, func, config = {}) {
return {
widget: node.addWidget(
"number",
inputName,
val,
func,
Object.assign({}, {min: 0, max: 4096, step: 640, precision: 0}, config)
)
};
}
function syncLinkedInputsToPropertiesAdvanced(node) {
let changed = false;
const vx = readLinkedNumber(node, "x");
if (vx != null) {
const nv = Math.max(0, Math.round(vx));
if (node.properties["x"] !== nv) {
node.properties["x"] = nv;
changed = true;
}
}
const vy = readLinkedNumber(node, "y");
if (vy != null) {
const nv = Math.max(0, Math.round(vy));
if (node.properties["y"] !== nv) {
node.properties["y"] = nv;
changed = true;
}
}
// Input "width" is the rectangle width in px -> property "w"
const vw = readLinkedNumber(node, "width");
if (vw != null) {
const nv = Math.max(0, Math.round(vw));
if (node.properties["w"] !== nv) {
node.properties["w"] = nv;
changed = true;
}
}
// Input "height" is the rectangle height in px -> property "h"
const vh = readLinkedNumber(node, "height");
if (vh != null) {
const nv = Math.max(0, Math.round(vh));
if (node.properties["h"] !== nv) {
node.properties["h"] = nv;
changed = true;
}
}
// Image size (must be >=1 to avoid division by zero in getDrawArea)
const viw = readLinkedNumber(node, "image_width");
if (viw != null) {
const nv = Math.max(1, Math.round(viw));
if (node.properties["width"] !== nv) {
node.properties["width"] = nv;
changed = true;
}
}
const vih = readLinkedNumber(node, "image_height");
if (vih != null) {
const nv = Math.max(1, Math.round(vih));
if (node.properties["height"] !== nv) {
node.properties["height"] = nv;
changed = true;
}
}
const vbr = readLinkedNumber(node, "blur_radius");
if (vbr != null) {
const nv = Math.max(0, Math.min(255, Math.round(vbr)));
if (node.properties["blur_radius"] !== nv) {
node.properties["blur_radius"] = nv;
changed = true;
}
}
return changed;
}

View File

@@ -0,0 +1,494 @@
import { app } from "../../scripts/app.js";
import { readLinkedNumber, getDrawColor, computeCanvasSize } from "./common.js";
function showPreviewCanvas(node, app) {
const widget = {
type: "customCanvas",
name: "mask-rect-area-canvas",
get value() {
return this.canvas.value;
},
set value(x) {
this.canvas.value = x;
},
draw: function (ctx, node, widgetWidth, widgetY) {
// If we are initially offscreen when created we wont have received a resize event
// Calculate it here instead
if (!node.canvasHeight) {
computeCanvasSize(node, node.size, 200, 200);
}
const visible = true;
const t = ctx.getTransform();
const margin = 12;
const border = 2;
const widgetHeight = node.canvasHeight;
const width = 512;
const height = 512;
const scale = Math.min((widgetWidth - margin * 3) / width, (widgetHeight - margin * 3) / height);
const blurRadius = node.properties["blur_radius"] || 0;
const index = 0;
Object.assign(this.canvas.style, {
left: `${t.e}px`,
top: `${t.f + (widgetY * t.d)}px`,
width: `${widgetWidth * t.a}px`,
height: `${widgetHeight * t.d}px`,
position: "absolute",
zIndex: 1,
fontSize: `${t.d * 10.0}px`,
pointerEvents: "none"
});
this.canvas.hidden = !visible;
let backgroundWidth = width * scale;
let backgroundHeight = height * scale;
let xOffset = margin;
if (backgroundWidth < widgetWidth) {
xOffset += (widgetWidth - backgroundWidth) / 2 - margin;
}
let yOffset = (margin / 2);
if (backgroundHeight < widgetHeight) {
yOffset += (widgetHeight - backgroundHeight) / 2 - margin;
}
let widgetX = xOffset;
widgetY = widgetY + yOffset;
// Draw the background border
ctx.fillStyle = globalThis.LiteGraph.WIDGET_OUTLINE_COLOR;
ctx.fillRect(widgetX - border, widgetY - border, backgroundWidth + border * 2, backgroundHeight + border * 2);
// Draw the main background area
ctx.fillStyle = globalThis.LiteGraph.WIDGET_BGCOLOR;
ctx.fillRect(widgetX, widgetY, backgroundWidth, backgroundHeight);
// Keep preview in sync when inputs are driven by links.
syncLinkedInputsToProperties(node);
// Draw the conditioning zone
let [x, y, w, h] = getDrawArea(node, backgroundWidth, backgroundHeight);
ctx.fillStyle = getDrawColor(0, "80");
ctx.fillRect(widgetX + x, widgetY + y, w, h);
ctx.beginPath();
ctx.lineWidth = 1;
// Draw grid lines
for (let x = 0; x <= width / 64; x += 1) {
ctx.moveTo(widgetX + x * 64 * scale, widgetY);
ctx.lineTo(widgetX + x * 64 * scale, widgetY + backgroundHeight);
}
for (let y = 0; y <= height / 64; y += 1) {
ctx.moveTo(widgetX, widgetY + y * 64 * scale);
ctx.lineTo(widgetX + backgroundWidth, widgetY + y * 64 * scale);
}
ctx.strokeStyle = "#66666650";
ctx.stroke();
ctx.closePath();
// Draw current zone
let [sx, sy, sw, sh] = getDrawArea(node, backgroundWidth, backgroundHeight);
ctx.fillStyle = getDrawColor(0, "80");
ctx.fillRect(widgetX + sx, widgetY + sy, sw, sh);
ctx.fillStyle = getDrawColor(0, "40");
ctx.fillRect(widgetX + sx + border, widgetY + sy + border, sw - border * 2, sh - border * 2);
// Draw white border around the current zone
ctx.strokeStyle = globalThis.LiteGraph.NODE_SELECTED_TITLE_COLOR;
ctx.lineWidth = 2;
ctx.strokeRect(widgetX + sx, widgetY + sy, sw, sh);
// Display
ctx.beginPath();
ctx.arc(LiteGraph.NODE_SLOT_HEIGHT * 0.5, LiteGraph.NODE_SLOT_HEIGHT * (index + 0.5) + 4, 4, 0, Math.PI * 2);
ctx.fill();
ctx.lineWidth = 1;
ctx.strokeStyle = "white";
ctx.stroke();
ctx.lineWidth = 1;
ctx.closePath();
// Draw progress bar canvas
if (backgroundWidth < widgetWidth) {
xOffset += (widgetWidth - backgroundWidth) / 2 - margin;
}
const barHeight = 8;
let widgetYBar = widgetY + backgroundHeight + margin;
// Draw progress bar border
ctx.fillStyle = globalThis.LiteGraph.WIDGET_OUTLINE_COLOR;
ctx.fillRect(
widgetX - border,
widgetYBar - border,
backgroundWidth + border * 2,
barHeight + border * 2
);
// Draw progress bar area
ctx.fillStyle = globalThis.LiteGraph.WIDGET_BGCOLOR; // Mismo color de fondo que el canvas
ctx.fillRect(
widgetX,
widgetYBar,
backgroundWidth,
barHeight
);
// Draw progress bar grid
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = "#66666650";
// Determine max lines
const numLines = Math.floor(backgroundWidth / 64);
// Draw progress bar grid
for (let x = 0; x <= width / 64; x += 1) {
ctx.moveTo(widgetX + x * 64 * scale, widgetYBar);
ctx.lineTo(widgetX + x * 64 * scale, widgetYBar + barHeight);
}
ctx.stroke();
ctx.closePath();
// Draw progress bar
const progress = Math.min(blurRadius / 255, 1);
ctx.fillStyle = "rgba(0, 120, 255, 0.5)";
ctx.fillRect(
widgetX,
widgetYBar,
backgroundWidth * progress,
barHeight
);
}
};
widget.canvas = document.createElement("canvas");
widget.canvas.className = "mask-rect-area-canvas";
widget.parent = node;
widget.computeLayoutSize = function (node) {
return {
minHeight: 200,
maxHeight: 300
};
};
document.body.appendChild(widget.canvas);
node.addCustomWidget(widget);
app.canvas.onDrawBackground = function () {
// Draw node isnt fired once the node is off the screen
// if it goes off screen quickly, the input may not be removed
// this shifts it off screen so it can be moved back if the node is visible.
for (let n in app.graph._nodes) {
n = app.graph._nodes[n];
for (let w in n.widgets) {
let wid = n.widgets[w];
if (Object.hasOwn(wid, "canvas")) {
wid.canvas.style.left = -8000 + "px";
wid.canvas.style.position = "absolute";
}
}
}
};
node.onResize = function (size) {
computeCanvasSize(node, size, 200, 200);
};
return {minWidth: 200, minHeight: 200, widget};
}
app.registerExtension({
name: 'drltdata.MaskRectArea',
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== "MaskRectArea") {
return;
}
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
this.setProperty("width", 512);
this.setProperty("height", 512);
this.setProperty("x", 0);
this.setProperty("y", 0);
this.setProperty("w", 50);
this.setProperty("h", 50);
this.setProperty("blur_radius", 0);
this.selected = false;
this.index = 3;
this.serialize_widgets = true;
// If Python/ComfyUI already created typed widgets, do not recreate them (avoid duplicates).
const hasExisting = Array.isArray(this.widgets) && this.widgets.some(w => w && w.name === "x");
// Hook existing widgets to keep node.properties in sync (canvas uses properties).
const hookWidget = (node, widgetName, propName, opts) => {
if (!Array.isArray(node.widgets)) {
return;
}
const w = node.widgets.find(ww => ww && ww.name === widgetName);
if (!w) {
return;
}
const min = (opts && typeof opts.min === "number") ? opts.min : undefined;
const max = (opts && typeof opts.max === "number") ? opts.max : undefined;
if (node.properties && Object.prototype.hasOwnProperty.call(node.properties, propName)) {
w.value = node.properties[propName];
} else {
node.properties[propName] = w.value;
}
const prevCb = w.callback;
w.callback = function (v, ...args) {
let val = v;
if (typeof val === "number") {
val = Math.round(val);
if (typeof min === "number") {
val = Math.max(min, val);
}
if (typeof max === "number") {
val = Math.min(max, val);
}
}
this.value = val;
node.properties[propName] = val;
if (prevCb) {
return prevCb.call(this, val, ...args);
}
};
};
if (hasExisting) {
// Note: "width"/"height" widgets map to "w"/"h" properties (percent-based).
hookWidget(this, "x", "x", {"min": 0, "max": 100});
hookWidget(this, "y", "y", {"min": 0, "max": 100});
hookWidget(this, "width", "w", {"min": 0, "max": 100});
hookWidget(this, "height", "h", {"min": 0, "max": 100});
hookWidget(this, "blur_radius", "blur_radius", {"min": 0, "max": 255});
} else {
CUSTOM_INT(this, "x", 0, function (v, _, node) {
this.value = Math.max(0, Math.min(100, Math.round(v)));
node.properties["x"] = this.value;
});
CUSTOM_INT(this, "y", 0, function (v, _, node) {
this.value = Math.max(0, Math.min(100, Math.round(v)));
node.properties["y"] = this.value;
});
CUSTOM_INT(this, "w", 50, function (v, _, node) {
this.value = Math.max(0, Math.min(100, Math.round(v)));
node.properties["w"] = this.value;
});
CUSTOM_INT(this, "h", 50, function (v, _, node) {
this.value = Math.max(0, Math.min(100, Math.round(v)));
node.properties["h"] = this.value;
});
CUSTOM_INT(this, "blur_radius", 0, function (v, _, node) {
this.value = Math.round(v) || 0;
node.properties["blur_radius"] = this.value;
}, {"min": 0, "max": 255, "step": 10});
// If Python widgets exist, they will be used instead; this is back-compat only.
}
showPreviewCanvas(this, app);
// Sync linked input values -> node.properties so the preview updates when driven by connections.
const prevOnExecute = this.onExecute;
this.onExecute = function () {
const rr = prevOnExecute ? prevOnExecute.apply(this, arguments) : undefined;
const readLinkedInt = (inputName) => {
if (!Array.isArray(this.inputs)) {
return null;
}
const inp = this.inputs.find(i => i && i.name === inputName);
if (!inp || !inp.link) {
return null;
}
try {
const v = this.getInputData(inputName);
return (typeof v === "number") ? v : null;
} catch (e) {
return null;
}
};
let changed = false;
const vx = readLinkedInt("x");
if (vx != null) {
const nv = Math.max(0, Math.min(100, Math.round(vx)));
if (this.properties["x"] !== nv) {
this.properties["x"] = nv;
changed = true;
}
}
const vy = readLinkedInt("y");
if (vy != null) {
const nv = Math.max(0, Math.min(100, Math.round(vy)));
if (this.properties["y"] !== nv) {
this.properties["y"] = nv;
changed = true;
}
}
const vw = readLinkedInt("width");
if (vw != null) {
const nv = Math.max(0, Math.min(100, Math.round(vw)));
if (this.properties["w"] !== nv) {
this.properties["w"] = nv;
changed = true;
}
}
const vh = readLinkedInt("height");
if (vh != null) {
const nv = Math.max(0, Math.min(100, Math.round(vh)));
if (this.properties["h"] !== nv) {
this.properties["h"] = nv;
changed = true;
}
}
const vbr = readLinkedInt("blur_radius");
if (vbr != null) {
const nv = Math.max(0, Math.min(255, Math.round(vbr)));
if (this.properties["blur_radius"] !== nv) {
this.properties["blur_radius"] = nv;
changed = true;
}
}
if (changed) {
this.setDirtyCanvas(true, true);
if (this.graph) {
this.graph.setDirtyCanvas(true, true);
}
}
return rr;
};
this.onSelected = function () {
this.selected = true;
};
this.onDeselected = function () {
this.selected = false;
};
return r;
};
}
});
// Calculate the drawing area using percentage-based properties.
function getDrawArea(node, backgroundWidth, backgroundHeight) {
// Convert percentages to actual pixel values based on the background dimensions
let x = (node.properties["x"] / 100) * backgroundWidth;
let y = (node.properties["y"] / 100) * backgroundHeight;
let w = (node.properties["w"] / 100) * backgroundWidth;
let h = (node.properties["h"] / 100) * backgroundHeight;
// Ensure the values do not exceed the background boundaries
if (x > backgroundWidth) {
x = backgroundWidth;
}
if (y > backgroundHeight) {
y = backgroundHeight;
}
// Adjust width and height to fit within the background dimensions
if (x + w > backgroundWidth) {
w = Math.max(0, backgroundWidth - x);
}
if (y + h > backgroundHeight) {
h = Math.max(0, backgroundHeight - y);
}
return [x, y, w, h];
}
function CUSTOM_INT(node, inputName, val, func, config = {}) {
return {
widget: node.addWidget(
"number",
inputName,
val,
func,
Object.assign({}, {min: 0, max: 100, step: 10, precision: 0}, config)
)
};
}
function syncLinkedInputsToProperties(node) {
let changed = false;
const vx = readLinkedNumber(node, "x");
if (vx != null) {
const nv = Math.max(0, Math.min(100, Math.round(vx)));
if (node.properties["x"] !== nv) {
node.properties["x"] = nv;
changed = true;
}
}
const vy = readLinkedNumber(node, "y");
if (vy != null) {
const nv = Math.max(0, Math.min(100, Math.round(vy)));
if (node.properties["y"] !== nv) {
node.properties["y"] = nv;
changed = true;
}
}
const vw = readLinkedNumber(node, "width");
if (vw != null) {
const nv = Math.max(0, Math.min(100, Math.round(vw)));
if (node.properties["w"] !== nv) {
node.properties["w"] = nv;
changed = true;
}
}
const vh = readLinkedNumber(node, "height");
if (vh != null) {
const nv = Math.max(0, Math.min(100, Math.round(vh)));
if (node.properties["h"] !== nv) {
node.properties["h"] = nv;
changed = true;
}
}
const vbr = readLinkedNumber(node, "blur_radius");
if (vbr != null) {
const nv = Math.max(0, Math.min(255, Math.round(vbr)));
if (node.properties["blur_radius"] !== nv) {
node.properties["blur_radius"] = nv;
changed = true;
}
}
return changed;
}