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
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:
281
custom_nodes/ComfyUI-Impact-Pack/js/common.js
Normal file
281
custom_nodes/ComfyUI-Impact-Pack/js/common.js
Normal 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;
|
||||
}
|
||||
229
custom_nodes/ComfyUI-Impact-Pack/js/impact-image-util.js
Normal file
229
custom_nodes/ComfyUI-Impact-Pack/js/impact-image-util.js
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
988
custom_nodes/ComfyUI-Impact-Pack/js/impact-pack.js
Normal file
988
custom_nodes/ComfyUI-Impact-Pack/js/impact-pack.js
Normal 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
641
custom_nodes/ComfyUI-Impact-Pack/js/impact-sam-editor.js
Normal file
641
custom_nodes/ComfyUI-Impact-Pack/js/impact-sam-editor.js
Normal 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();
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
182
custom_nodes/ComfyUI-Impact-Pack/js/impact-segs-picker.js
Normal file
182
custom_nodes/ComfyUI-Impact-Pack/js/impact-segs-picker.js
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
459
custom_nodes/ComfyUI-Impact-Pack/js/mask-rect-area-advanced.js
Normal file
459
custom_nodes/ComfyUI-Impact-Pack/js/mask-rect-area-advanced.js
Normal 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;
|
||||
}
|
||||
|
||||
494
custom_nodes/ComfyUI-Impact-Pack/js/mask-rect-area.js
Normal file
494
custom_nodes/ComfyUI-Impact-Pack/js/mask-rect-area.js
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user