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:
23
custom_nodes/ComfyUI-KJNodes/web/js/appearance.js
Normal file
23
custom_nodes/ComfyUI-KJNodes/web/js/appearance.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const { app } = window.comfyAPI.app;
|
||||
|
||||
app.registerExtension({
|
||||
name: "KJNodes.appearance",
|
||||
nodeCreated(node) {
|
||||
switch (node.comfyClass) {
|
||||
case "INTConstant":
|
||||
node.setSize([200, 58]);
|
||||
node.color = "#1b4669";
|
||||
node.bgcolor = "#29699c";
|
||||
break;
|
||||
case "FloatConstant":
|
||||
node.setSize([200, 58]);
|
||||
node.color = LGraphCanvas.node_colors.green.color;
|
||||
node.bgcolor = LGraphCanvas.node_colors.green.bgcolor;
|
||||
break;
|
||||
case "ConditioningMultiCombine":
|
||||
node.color = LGraphCanvas.node_colors.brown.color;
|
||||
node.bgcolor = LGraphCanvas.node_colors.brown.bgcolor;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
55
custom_nodes/ComfyUI-KJNodes/web/js/browserstatus.js
Normal file
55
custom_nodes/ComfyUI-KJNodes/web/js/browserstatus.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const { api } = window.comfyAPI.api;
|
||||
const { app } = window.comfyAPI.app;
|
||||
|
||||
app.registerExtension({
|
||||
name: "KJNodes.browserstatus",
|
||||
setup() {
|
||||
if (!app.ui.settings.getSettingValue("KJNodes.browserStatus")) {
|
||||
return;
|
||||
}
|
||||
api.addEventListener("status", ({ detail }) => {
|
||||
let title = "ComfyUI";
|
||||
let favicon = "green";
|
||||
let queueRemaining = detail && detail.exec_info.queue_remaining;
|
||||
|
||||
if (queueRemaining) {
|
||||
favicon = "red";
|
||||
title = `00% - ${queueRemaining} | ${title}`;
|
||||
}
|
||||
let link = document.querySelector("link[rel~='icon']");
|
||||
if (!link) {
|
||||
link = document.createElement("link");
|
||||
link.rel = "icon";
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
link.href = new URL(`../${favicon}.png`, import.meta.url);
|
||||
document.title = title;
|
||||
});
|
||||
//add progress to the title
|
||||
api.addEventListener("progress", ({ detail }) => {
|
||||
const { value, max } = detail;
|
||||
const progress = Math.floor((value / max) * 100);
|
||||
let title = document.title;
|
||||
|
||||
if (!isNaN(progress) && progress >= 0 && progress <= 100) {
|
||||
const paddedProgress = String(progress).padStart(2, '0');
|
||||
title = `${paddedProgress}% ${title.replace(/^\d+%\s/, '')}`;
|
||||
}
|
||||
document.title = title;
|
||||
});
|
||||
},
|
||||
init() {
|
||||
if (!app.ui.settings.getSettingValue("KJNodes.browserStatus")) {
|
||||
return;
|
||||
}
|
||||
const pythongossFeed = app.extensions.find(
|
||||
(e) => e.name === 'pysssss.FaviconStatus',
|
||||
)
|
||||
if (pythongossFeed) {
|
||||
console.warn("KJNodes - Overriding pysssss.FaviconStatus")
|
||||
pythongossFeed.setup = function() {
|
||||
console.warn("Disabled by KJNodes")
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
175
custom_nodes/ComfyUI-KJNodes/web/js/contextmenu.js
Normal file
175
custom_nodes/ComfyUI-KJNodes/web/js/contextmenu.js
Normal file
@@ -0,0 +1,175 @@
|
||||
const { app } = window.comfyAPI.app;
|
||||
|
||||
// Adds context menu entries, code partly from pyssssscustom-scripts
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
function addNode(name, nextTo, options) {
|
||||
console.log("name:", name);
|
||||
console.log("nextTo:", nextTo);
|
||||
options = { side: "left", select: true, shiftY: 0, shiftX: 0, ...(options || {}) };
|
||||
const node = LiteGraph.createNode(name);
|
||||
app.graph.add(node);
|
||||
|
||||
node.pos = [
|
||||
options.side === "left" ? nextTo.pos[0] - (node.size[0] + options.offset): nextTo.pos[0] + nextTo.size[0] + options.offset,
|
||||
|
||||
nextTo.pos[1] + options.shiftY,
|
||||
];
|
||||
|
||||
// Automatically connect nodes
|
||||
if (options.side === "left") {
|
||||
// New node on left: connect new node's output to nextTo's first available input
|
||||
if (node.outputs && node.outputs.length > 0 && nextTo.inputs && nextTo.inputs.length > 0) {
|
||||
for (let i = 0; i < nextTo.inputs.length; i++) {
|
||||
if (!nextTo.inputs[i].link) {
|
||||
node.connect(0, nextTo, i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// New node on right: connect nextTo's output to new node's first available input
|
||||
if (nextTo.outputs && nextTo.outputs.length > 0 && node.inputs && node.inputs.length > 0) {
|
||||
for (let i = 0; i < node.inputs.length; i++) {
|
||||
if (!node.inputs[i].link) {
|
||||
nextTo.connect(0, node, i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (options.select) {
|
||||
app.canvas.selectNode(node, false);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "KJNodesContextmenu",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.input && nodeData.input.required) {
|
||||
addMenuHandler(nodeType, function (_, options) {
|
||||
options.unshift(
|
||||
{
|
||||
content: "Add GetNode",
|
||||
callback: () => {addNode("GetNode", this, { side:"left", offset: 30});}
|
||||
},
|
||||
{
|
||||
content: "Add SetNode",
|
||||
callback: () => {addNode("SetNode", this, { side:"right", offset: 30 });}
|
||||
},
|
||||
{
|
||||
content: "Add PreviewAsTextNode",
|
||||
callback: () => {addNode("PreviewAny", this, { side:"right", offset: 30 });
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
async setup(app) {
|
||||
const updateSlots = (value) => {
|
||||
const valuesToAddToIn = ["GetNode"];
|
||||
const valuesToAddToOut = ["SetNode"];
|
||||
// Remove entries if they exist
|
||||
for (const arr of Object.values(LiteGraph.slot_types_default_in)) {
|
||||
for (const valueToAdd of valuesToAddToIn) {
|
||||
const idx = arr.indexOf(valueToAdd);
|
||||
if (idx !== -1) {
|
||||
arr.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const arr of Object.values(LiteGraph.slot_types_default_out)) {
|
||||
for (const valueToAdd of valuesToAddToOut) {
|
||||
const idx = arr.indexOf(valueToAdd);
|
||||
if (idx !== -1) {
|
||||
arr.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (value!="disabled") {
|
||||
for (const arr of Object.values(LiteGraph.slot_types_default_in)) {
|
||||
for (const valueToAdd of valuesToAddToIn) {
|
||||
const idx = arr.indexOf(valueToAdd);
|
||||
if (idx !== -1) {
|
||||
arr.splice(idx, 1);
|
||||
}
|
||||
if (value === "top") {
|
||||
arr.unshift(valueToAdd);
|
||||
} else {
|
||||
arr.push(valueToAdd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const arr of Object.values(LiteGraph.slot_types_default_out)) {
|
||||
for (const valueToAdd of valuesToAddToOut) {
|
||||
const idx = arr.indexOf(valueToAdd);
|
||||
if (idx !== -1) {
|
||||
arr.splice(idx, 1);
|
||||
}
|
||||
if (value === "top") {
|
||||
arr.unshift(valueToAdd);
|
||||
} else {
|
||||
arr.push(valueToAdd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: "KJNodes.SetGetMenu",
|
||||
name: "KJNodes: Make Set/Get -nodes defaults",
|
||||
tooltip: 'Adds Set/Get nodes to the top or bottom of the list of available node suggestions.',
|
||||
options: ['disabled', 'top', 'bottom'],
|
||||
defaultValue: 'disabled',
|
||||
type: "combo",
|
||||
onChange: updateSlots,
|
||||
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "KJNodes.MiddleClickDefault",
|
||||
name: "KJNodes: Middle click default node adding",
|
||||
defaultValue: false,
|
||||
type: "boolean",
|
||||
onChange: (value) => {
|
||||
LiteGraph.middle_click_slot_add_default_node = value;
|
||||
},
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "KJNodes.nodeAutoColor",
|
||||
name: "KJNodes: Automatically set node colors",
|
||||
type: "boolean",
|
||||
defaultValue: true,
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "KJNodes.helpPopup",
|
||||
name: "KJNodes: Help popups",
|
||||
defaultValue: true,
|
||||
type: "boolean",
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "KJNodes.disablePrefix",
|
||||
name: "KJNodes: Disable automatic Set_ and Get_ prefix",
|
||||
defaultValue: true,
|
||||
type: "boolean",
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: "KJNodes.browserStatus",
|
||||
name: "KJNodes: 🟢 Stoplight browser status icon 🔴",
|
||||
defaultValue: false,
|
||||
type: "boolean",
|
||||
});
|
||||
}
|
||||
});
|
||||
95
custom_nodes/ComfyUI-KJNodes/web/js/fast_preview.js
Normal file
95
custom_nodes/ComfyUI-KJNodes/web/js/fast_preview.js
Normal file
@@ -0,0 +1,95 @@
|
||||
const { app } = window.comfyAPI.app;
|
||||
|
||||
//from melmass
|
||||
export function makeUUID() {
|
||||
let dt = new Date().getTime()
|
||||
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = ((dt + Math.random() * 16) % 16) | 0
|
||||
dt = Math.floor(dt / 16)
|
||||
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
|
||||
})
|
||||
return uuid
|
||||
}
|
||||
|
||||
function chainCallback(object, property, callback) {
|
||||
if (object == undefined) {
|
||||
//This should not happen.
|
||||
console.error("Tried to add callback to non-existant object")
|
||||
return;
|
||||
}
|
||||
if (property in object) {
|
||||
const callback_orig = object[property]
|
||||
object[property] = function () {
|
||||
const r = callback_orig.apply(this, arguments);
|
||||
callback.apply(this, arguments);
|
||||
return r
|
||||
};
|
||||
} else {
|
||||
object[property] = callback;
|
||||
}
|
||||
}
|
||||
app.registerExtension({
|
||||
name: 'KJNodes.FastPreview',
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
if (nodeData?.name === 'FastPreview') {
|
||||
chainCallback(nodeType.prototype, "onNodeCreated", function () {
|
||||
|
||||
var element = document.createElement("div");
|
||||
this.uuid = makeUUID()
|
||||
element.id = `fast-preview-${this.uuid}`
|
||||
|
||||
this.previewWidget = this.addDOMWidget(nodeData.name, "FastPreviewWidget", element, {
|
||||
serialize: false,
|
||||
hideOnZoom: false,
|
||||
});
|
||||
|
||||
this.previewer = new Previewer(this);
|
||||
|
||||
this.setSize([550, 550]);
|
||||
this.resizable = false;
|
||||
this.previewWidget.parentEl = document.createElement("div");
|
||||
this.previewWidget.parentEl.className = "fast-preview";
|
||||
this.previewWidget.parentEl.id = `fast-preview-${this.uuid}`
|
||||
element.appendChild(this.previewWidget.parentEl);
|
||||
|
||||
chainCallback(this, "onExecuted", function (message) {
|
||||
let bg_image = message["bg_image"];
|
||||
this.properties.imgData = {
|
||||
name: "bg_image",
|
||||
base64: bg_image
|
||||
};
|
||||
this.previewer.refreshBackgroundImage(this);
|
||||
});
|
||||
|
||||
|
||||
}); // onAfterGraphConfigured
|
||||
}//node created
|
||||
} //before register
|
||||
})//register
|
||||
|
||||
class Previewer {
|
||||
constructor(context) {
|
||||
this.node = context;
|
||||
this.previousWidth = null;
|
||||
this.previousHeight = null;
|
||||
}
|
||||
refreshBackgroundImage = () => {
|
||||
const imgData = this.node?.properties?.imgData;
|
||||
if (imgData?.base64) {
|
||||
const base64String = imgData.base64;
|
||||
const imageUrl = `data:${imgData.type};base64,${base64String}`;
|
||||
const img = new Image();
|
||||
img.src = imageUrl;
|
||||
img.onload = () => {
|
||||
const { width, height } = img;
|
||||
if (width !== this.previousWidth || height !== this.previousHeight) {
|
||||
this.node.setSize([width, height]);
|
||||
this.previousWidth = width;
|
||||
this.previousHeight = height;
|
||||
}
|
||||
this.node.previewWidget.element.style.backgroundImage = `url(${imageUrl})`;
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
326
custom_nodes/ComfyUI-KJNodes/web/js/help_popup.js
Normal file
326
custom_nodes/ComfyUI-KJNodes/web/js/help_popup.js
Normal file
@@ -0,0 +1,326 @@
|
||||
const { app } = window.comfyAPI.app;
|
||||
|
||||
// code based on mtb nodes by Mel Massadian https://github.com/melMass/comfy_mtb/
|
||||
export const loadScript = (
|
||||
FILE_URL,
|
||||
async = true,
|
||||
type = 'text/javascript',
|
||||
) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// Check if the script already exists
|
||||
const existingScript = document.querySelector(`script[src="${FILE_URL}"]`)
|
||||
if (existingScript) {
|
||||
resolve({ status: true, message: 'Script already loaded' })
|
||||
return
|
||||
}
|
||||
|
||||
const scriptEle = document.createElement('script')
|
||||
scriptEle.type = type
|
||||
scriptEle.async = async
|
||||
scriptEle.src = FILE_URL
|
||||
|
||||
scriptEle.addEventListener('load', (ev) => {
|
||||
resolve({ status: true })
|
||||
})
|
||||
|
||||
scriptEle.addEventListener('error', (ev) => {
|
||||
reject({
|
||||
status: false,
|
||||
message: `Failed to load the script ${FILE_URL}`,
|
||||
})
|
||||
})
|
||||
|
||||
document.body.appendChild(scriptEle)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
loadScript('kjweb_async/marked.min.js').catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
loadScript('kjweb_async/purify.min.js').catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
|
||||
const categories = ["KJNodes", "SUPIR", "VoiceCraft", "Marigold", "IC-Light", "WanVideoWrapper"];
|
||||
app.registerExtension({
|
||||
name: "KJNodes.HelpPopup",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
|
||||
if (app.ui.settings.getSettingValue("KJNodes.helpPopup") === false) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
categories.forEach(category => {
|
||||
if (nodeData?.category?.startsWith(category)) {
|
||||
addDocumentation(nodeData, nodeType);
|
||||
}
|
||||
else return
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error in registering KJNodes.HelpPopup", error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const create_documentation_stylesheet = () => {
|
||||
const tag = 'kj-documentation-stylesheet'
|
||||
|
||||
let styleTag = document.head.querySelector(tag)
|
||||
|
||||
if (!styleTag) {
|
||||
styleTag = document.createElement('style')
|
||||
styleTag.type = 'text/css'
|
||||
styleTag.id = tag
|
||||
styleTag.innerHTML = `
|
||||
.kj-documentation-popup {
|
||||
background: var(--comfy-menu-bg);
|
||||
position: absolute;
|
||||
color: var(--fg-color);
|
||||
font: 12px monospace;
|
||||
line-height: 1.5em;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
border-style: solid;
|
||||
border-width: medium;
|
||||
border-color: var(--border-color);
|
||||
z-index: 5;
|
||||
overflow: hidden;
|
||||
}
|
||||
.content-wrapper {
|
||||
overflow: auto;
|
||||
max-height: 100%;
|
||||
/* Scrollbar styling for Chrome */
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--bg-color);
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--fg-color);
|
||||
border-radius: 6px;
|
||||
border: 3px solid var(--bg-color);
|
||||
}
|
||||
|
||||
/* Scrollbar styling for Firefox */
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--fg-color) var(--bg-color);
|
||||
a {
|
||||
color: yellow;
|
||||
}
|
||||
a:visited {
|
||||
color: orange;
|
||||
}
|
||||
a:hover {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
`
|
||||
document.head.appendChild(styleTag)
|
||||
}
|
||||
}
|
||||
|
||||
/** Add documentation widget to the selected node */
|
||||
export const addDocumentation = (
|
||||
nodeData,
|
||||
nodeType,
|
||||
opts = { icon_size: 14, icon_margin: 4 },) => {
|
||||
|
||||
opts = opts || {}
|
||||
const iconSize = opts.icon_size ? opts.icon_size : 14
|
||||
const iconMargin = opts.icon_margin ? opts.icon_margin : 4
|
||||
let docElement = null
|
||||
let contentWrapper = null
|
||||
//if no description in the node python code, don't do anything
|
||||
if (!nodeData.description) {
|
||||
return
|
||||
}
|
||||
|
||||
const drawFg = nodeType.prototype.onDrawForeground
|
||||
nodeType.prototype.onDrawForeground = function (ctx) {
|
||||
const r = drawFg ? drawFg.apply(this, arguments) : undefined
|
||||
if (this.flags.collapsed) return r
|
||||
|
||||
// icon position
|
||||
const x = this.size[0] - iconSize - iconMargin
|
||||
|
||||
// create the popup
|
||||
if (this.show_doc && docElement === null) {
|
||||
docElement = document.createElement('div')
|
||||
contentWrapper = document.createElement('div');
|
||||
docElement.appendChild(contentWrapper);
|
||||
|
||||
create_documentation_stylesheet()
|
||||
contentWrapper.classList.add('content-wrapper');
|
||||
docElement.classList.add('kj-documentation-popup')
|
||||
|
||||
//parse the string from the python node code to html with marked, and sanitize the html with DOMPurify
|
||||
contentWrapper.innerHTML = DOMPurify.sanitize(marked.parse(nodeData.description,))
|
||||
|
||||
// resize handle
|
||||
const resizeHandle = document.createElement('div');
|
||||
resizeHandle.style.width = '0';
|
||||
resizeHandle.style.height = '0';
|
||||
resizeHandle.style.position = 'absolute';
|
||||
resizeHandle.style.bottom = '0';
|
||||
resizeHandle.style.right = '0';
|
||||
resizeHandle.style.cursor = 'se-resize';
|
||||
|
||||
// Add pseudo-elements to create a triangle shape
|
||||
const borderColor = getComputedStyle(document.documentElement).getPropertyValue('--border-color').trim();
|
||||
resizeHandle.style.borderTop = '10px solid transparent';
|
||||
resizeHandle.style.borderLeft = '10px solid transparent';
|
||||
resizeHandle.style.borderBottom = `10px solid ${borderColor}`;
|
||||
resizeHandle.style.borderRight = `10px solid ${borderColor}`;
|
||||
|
||||
docElement.appendChild(resizeHandle)
|
||||
let isResizing = false
|
||||
let startX, startY, startWidth, startHeight
|
||||
|
||||
resizeHandle.addEventListener('mousedown', function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
isResizing = true;
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
startWidth = parseInt(document.defaultView.getComputedStyle(docElement).width, 10);
|
||||
startHeight = parseInt(document.defaultView.getComputedStyle(docElement).height, 10);
|
||||
},
|
||||
{ signal: this.docCtrl.signal },
|
||||
);
|
||||
|
||||
// close button
|
||||
const closeButton = document.createElement('div');
|
||||
closeButton.textContent = '❌';
|
||||
closeButton.style.position = 'absolute';
|
||||
closeButton.style.top = '0';
|
||||
closeButton.style.right = '0';
|
||||
closeButton.style.cursor = 'pointer';
|
||||
closeButton.style.padding = '5px';
|
||||
closeButton.style.color = 'red';
|
||||
closeButton.style.fontSize = '12px';
|
||||
|
||||
docElement.appendChild(closeButton)
|
||||
|
||||
closeButton.addEventListener('mousedown', (e) => {
|
||||
e.stopPropagation();
|
||||
this.show_doc = !this.show_doc
|
||||
docElement.parentNode.removeChild(docElement)
|
||||
docElement = null
|
||||
if (contentWrapper) {
|
||||
contentWrapper.remove()
|
||||
contentWrapper = null
|
||||
}
|
||||
},
|
||||
{ signal: this.docCtrl.signal },
|
||||
);
|
||||
|
||||
document.addEventListener('mousemove', function (e) {
|
||||
if (!isResizing) return;
|
||||
const scale = app.canvas.ds.scale;
|
||||
const newWidth = startWidth + (e.clientX - startX) / scale;
|
||||
const newHeight = startHeight + (e.clientY - startY) / scale;;
|
||||
docElement.style.width = `${newWidth}px`;
|
||||
docElement.style.height = `${newHeight}px`;
|
||||
},
|
||||
{ signal: this.docCtrl.signal },
|
||||
);
|
||||
|
||||
document.addEventListener('mouseup', function () {
|
||||
isResizing = false
|
||||
},
|
||||
{ signal: this.docCtrl.signal },
|
||||
)
|
||||
|
||||
document.body.appendChild(docElement)
|
||||
}
|
||||
// close the popup
|
||||
else if (!this.show_doc && docElement !== null) {
|
||||
docElement.parentNode.removeChild(docElement)
|
||||
docElement = null
|
||||
}
|
||||
// update position of the popup
|
||||
if (this.show_doc && docElement !== null) {
|
||||
const rect = ctx.canvas.getBoundingClientRect()
|
||||
const scaleX = rect.width / ctx.canvas.width
|
||||
const scaleY = rect.height / ctx.canvas.height
|
||||
|
||||
const transform = new DOMMatrix()
|
||||
.scaleSelf(scaleX, scaleY)
|
||||
.multiplySelf(ctx.getTransform())
|
||||
.translateSelf(this.size[0] * scaleX * Math.max(1.0,window.devicePixelRatio) , 0)
|
||||
.translateSelf(10, -32)
|
||||
|
||||
const scale = new DOMMatrix()
|
||||
.scaleSelf(transform.a, transform.d);
|
||||
const bcr = app.canvas.canvas.getBoundingClientRect()
|
||||
|
||||
const styleObject = {
|
||||
transformOrigin: '0 0',
|
||||
transform: scale,
|
||||
left: `${transform.a + bcr.x + transform.e}px`,
|
||||
top: `${transform.d + bcr.y + transform.f}px`,
|
||||
};
|
||||
Object.assign(docElement.style, styleObject);
|
||||
}
|
||||
|
||||
ctx.save()
|
||||
ctx.translate(x - 2, iconSize - 34)
|
||||
ctx.scale(iconSize / 32, iconSize / 32)
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.3)'
|
||||
ctx.lineCap = 'round'
|
||||
ctx.lineJoin = 'round'
|
||||
ctx.lineWidth = 2.4
|
||||
ctx.font = 'bold 36px monospace'
|
||||
ctx.fillStyle = 'orange';
|
||||
ctx.fillText('?', 0, 24)
|
||||
ctx.restore()
|
||||
return r
|
||||
}
|
||||
// handle clicking of the icon
|
||||
const mouseDown = nodeType.prototype.onMouseDown
|
||||
nodeType.prototype.onMouseDown = function (e, localPos, canvas) {
|
||||
const r = mouseDown ? mouseDown.apply(this, arguments) : undefined
|
||||
const iconX = this.size[0] - iconSize - iconMargin
|
||||
const iconY = iconSize - 34
|
||||
if (
|
||||
localPos[0] > iconX &&
|
||||
localPos[0] < iconX + iconSize &&
|
||||
localPos[1] > iconY &&
|
||||
localPos[1] < iconY + iconSize
|
||||
) {
|
||||
if (this.show_doc === undefined) {
|
||||
this.show_doc = true
|
||||
} else {
|
||||
this.show_doc = !this.show_doc
|
||||
}
|
||||
if (this.show_doc) {
|
||||
this.docCtrl = new AbortController()
|
||||
} else {
|
||||
this.docCtrl.abort()
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
const onRem = nodeType.prototype.onRemoved
|
||||
|
||||
nodeType.prototype.onRemoved = function () {
|
||||
const r = onRem ? onRem.apply(this, []) : undefined
|
||||
|
||||
if (docElement) {
|
||||
docElement.remove()
|
||||
docElement = null
|
||||
}
|
||||
|
||||
if (contentWrapper) {
|
||||
contentWrapper.remove()
|
||||
contentWrapper = null
|
||||
}
|
||||
return r
|
||||
}
|
||||
}
|
||||
416
custom_nodes/ComfyUI-KJNodes/web/js/jsnodes.js
Normal file
416
custom_nodes/ComfyUI-KJNodes/web/js/jsnodes.js
Normal file
@@ -0,0 +1,416 @@
|
||||
const { app } = window.comfyAPI.app;
|
||||
const { applyTextReplacements } = window.comfyAPI.utils;
|
||||
|
||||
app.registerExtension({
|
||||
name: "KJNodes.jsnodes",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if(!nodeData?.category?.startsWith("KJNodes")) {
|
||||
return;
|
||||
}
|
||||
switch (nodeData.name) {
|
||||
case "ConditioningMultiCombine":
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
this._type = "CONDITIONING"
|
||||
this.inputs_offset = nodeData.name.includes("selective")?1:0
|
||||
this.addWidget("button", "Update inputs", null, () => {
|
||||
if (!this.inputs) {
|
||||
this.inputs = [];
|
||||
}
|
||||
const target_number_of_inputs = this.widgets.find(w => w.name === "inputcount")["value"];
|
||||
const num_inputs = this.inputs.filter(input => input.type === this._type).length
|
||||
if(target_number_of_inputs===num_inputs)return; // already set, do nothing
|
||||
|
||||
if(target_number_of_inputs < num_inputs){
|
||||
const inputs_to_remove = num_inputs - target_number_of_inputs;
|
||||
for(let i = 0; i < inputs_to_remove; i++) {
|
||||
this.removeInput(this.inputs.length - 1);
|
||||
}
|
||||
}
|
||||
else{
|
||||
for(let i = num_inputs+1; i <= target_number_of_inputs; ++i)
|
||||
this.addInput(`conditioning_${i}`, this._type)
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "ImageBatchMulti":
|
||||
case "ImageAddMulti":
|
||||
case "ImageConcatMulti":
|
||||
case "CrossFadeImagesMulti":
|
||||
case "TransitionImagesMulti":
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
this._type = "IMAGE"
|
||||
this.addWidget("button", "Update inputs", null, () => {
|
||||
if (!this.inputs) {
|
||||
this.inputs = [];
|
||||
}
|
||||
const target_number_of_inputs = this.widgets.find(w => w.name === "inputcount")["value"];
|
||||
const num_inputs = this.inputs.filter(input => input.type === this._type).length
|
||||
if(target_number_of_inputs===num_inputs)return; // already set, do nothing
|
||||
|
||||
if(target_number_of_inputs < num_inputs){
|
||||
const inputs_to_remove = num_inputs - target_number_of_inputs;
|
||||
for(let i = 0; i < inputs_to_remove; i++) {
|
||||
this.removeInput(this.inputs.length - 1);
|
||||
}
|
||||
}
|
||||
else{
|
||||
for(let i = num_inputs+1; i <= target_number_of_inputs; ++i)
|
||||
this.addInput(`image_${i}`, this._type, {shape: 7});
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "MaskBatchMulti":
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
this._type = "MASK"
|
||||
this.addWidget("button", "Update inputs", null, () => {
|
||||
if (!this.inputs) {
|
||||
this.inputs = [];
|
||||
}
|
||||
const target_number_of_inputs = this.widgets.find(w => w.name === "inputcount")["value"];
|
||||
const num_inputs = this.inputs.filter(input => input.type === this._type).length
|
||||
if(target_number_of_inputs===num_inputs)return; // already set, do nothing
|
||||
|
||||
if(target_number_of_inputs < num_inputs){
|
||||
const inputs_to_remove = num_inputs - target_number_of_inputs;
|
||||
for(let i = 0; i < inputs_to_remove; i++) {
|
||||
this.removeInput(this.inputs.length - 1);
|
||||
}
|
||||
}
|
||||
else{
|
||||
for(let i = num_inputs+1; i <= target_number_of_inputs; ++i)
|
||||
this.addInput(`mask_${i}`, this._type)
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "FluxBlockLoraSelect":
|
||||
case "HunyuanVideoBlockLoraSelect":
|
||||
case "Wan21BlockLoraSelect":
|
||||
case "LTX2BlockLoraSelect":
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
this.addWidget("button", "Set all", null, () => {
|
||||
const userInput = prompt("Enter the values to set for widgets (e.g., s0,1,2-7=2.0, d0,1,2-7=2.0, or 1.0):", "");
|
||||
if (userInput) {
|
||||
const regex = /([sd])?(\d+(?:,\d+|-?\d+)*?)?=(\d+(\.\d+)?)/;
|
||||
const match = userInput.match(regex);
|
||||
if (match) {
|
||||
const type = match[1];
|
||||
const indicesPart = match[2];
|
||||
const value = parseFloat(match[3]);
|
||||
|
||||
let targetWidgets = [];
|
||||
if (type === 's') {
|
||||
targetWidgets = this.widgets.filter(widget => widget.name.includes("single"));
|
||||
} else if (type === 'd') {
|
||||
targetWidgets = this.widgets.filter(widget => widget.name.includes("double"));
|
||||
} else {
|
||||
targetWidgets = this.widgets; // No type specified, all widgets
|
||||
}
|
||||
|
||||
if (indicesPart) {
|
||||
const indices = indicesPart.split(',').flatMap(part => {
|
||||
if (part.includes('-')) {
|
||||
const [start, end] = part.split('-').map(Number);
|
||||
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
|
||||
}
|
||||
return Number(part);
|
||||
});
|
||||
|
||||
for (const index of indices) {
|
||||
if (index < targetWidgets.length) {
|
||||
targetWidgets[index].value = value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No indices provided, set value for all target widgets
|
||||
for (const widget of targetWidgets) {
|
||||
widget.value = value;
|
||||
}
|
||||
}
|
||||
} else if (!isNaN(parseFloat(userInput))) {
|
||||
// Single value provided, set it for all widgets
|
||||
const value = parseFloat(userInput);
|
||||
for (const widget of this.widgets) {
|
||||
widget.value = value;
|
||||
}
|
||||
} else {
|
||||
alert("Invalid input format. Please use the format s0,1,2-7=2.0, d0,1,2-7=2.0, or 1.0");
|
||||
}
|
||||
} else {
|
||||
alert("Invalid input. Please enter a value.");
|
||||
}
|
||||
});
|
||||
};
|
||||
break;
|
||||
|
||||
case "GetMaskSizeAndCount":
|
||||
const onGetMaskSizeConnectInput = nodeType.prototype.onConnectInput;
|
||||
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
|
||||
const v = onGetMaskSizeConnectInput? onGetMaskSizeConnectInput.apply(this, arguments): undefined
|
||||
this.outputs[1]["label"] = "width"
|
||||
this.outputs[2]["label"] = "height"
|
||||
this.outputs[3]["label"] = "count"
|
||||
return v;
|
||||
}
|
||||
const onGetMaskSizeExecuted = nodeType.prototype.onAfterExecuteNode;
|
||||
nodeType.prototype.onExecuted = function(message) {
|
||||
const r = onGetMaskSizeExecuted? onGetMaskSizeExecuted.apply(this,arguments): undefined
|
||||
let values = message["text"].toString().split('x').map(Number);
|
||||
this.outputs[1]["label"] = values[1] + " width"
|
||||
this.outputs[2]["label"] = values[2] + " height"
|
||||
this.outputs[3]["label"] = values[0] + " count"
|
||||
return r
|
||||
}
|
||||
break;
|
||||
|
||||
case "GetImageSizeAndCount":
|
||||
const onGetImageSizeConnectInput = nodeType.prototype.onConnectInput;
|
||||
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
|
||||
console.log(this)
|
||||
const v = onGetImageSizeConnectInput? onGetImageSizeConnectInput.apply(this, arguments): undefined
|
||||
//console.log(this)
|
||||
this.outputs[1]["label"] = "width"
|
||||
this.outputs[2]["label"] = "height"
|
||||
this.outputs[3]["label"] = "count"
|
||||
return v;
|
||||
}
|
||||
//const onGetImageSizeExecuted = nodeType.prototype.onExecuted;
|
||||
const onGetImageSizeExecuted = nodeType.prototype.onAfterExecuteNode;
|
||||
nodeType.prototype.onExecuted = function(message) {
|
||||
console.log(this)
|
||||
const r = onGetImageSizeExecuted? onGetImageSizeExecuted.apply(this,arguments): undefined
|
||||
let values = message["text"].toString().split('x').map(Number);
|
||||
console.log(values)
|
||||
this.outputs[1]["label"] = values[1] + " width"
|
||||
this.outputs[2]["label"] = values[2] + " height"
|
||||
this.outputs[3]["label"] = values[0] + " count"
|
||||
return r
|
||||
}
|
||||
break;
|
||||
|
||||
case "GetLatentSizeAndCount":
|
||||
const onGetLatentConnectInput = nodeType.prototype.onConnectInput;
|
||||
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
|
||||
console.log(this)
|
||||
const v = onGetLatentConnectInput? onGetLatentConnectInput.apply(this, arguments): undefined
|
||||
//console.log(this)
|
||||
this.outputs[1]["label"] = "batch_size"
|
||||
this.outputs[2]["label"] = "channels"
|
||||
this.outputs[3]["label"] = "frames"
|
||||
this.outputs[4]["label"] = "height"
|
||||
this.outputs[5]["label"] = "width"
|
||||
return v;
|
||||
}
|
||||
//const onGetImageSizeExecuted = nodeType.prototype.onExecuted;
|
||||
const onGetLatentSizeExecuted = nodeType.prototype.onAfterExecuteNode;
|
||||
nodeType.prototype.onExecuted = function(message) {
|
||||
console.log(this)
|
||||
const r = onGetLatentSizeExecuted? onGetLatentSizeExecuted.apply(this,arguments): undefined
|
||||
let values = message["text"].toString().split('x').map(Number);
|
||||
console.log(values)
|
||||
this.outputs[1]["label"] = values[0] + " batch"
|
||||
this.outputs[2]["label"] = values[1] + " channels"
|
||||
this.outputs[3]["label"] = values[2] + " frames"
|
||||
this.outputs[4]["label"] = values[3] + " height"
|
||||
this.outputs[5]["label"] = values[4] + " width"
|
||||
return r
|
||||
}
|
||||
break;
|
||||
|
||||
case "PreviewAnimation":
|
||||
const onPreviewAnimationConnectInput = nodeType.prototype.onConnectInput;
|
||||
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
|
||||
const v = onPreviewAnimationConnectInput? onPreviewAnimationConnectInput.apply(this, arguments): undefined
|
||||
this.title = "Preview Animation"
|
||||
return v;
|
||||
}
|
||||
const onPreviewAnimationExecuted = nodeType.prototype.onAfterExecuteNode;
|
||||
nodeType.prototype.onExecuted = function(message) {
|
||||
const r = onPreviewAnimationExecuted? onPreviewAnimationExecuted.apply(this,arguments): undefined
|
||||
let values = message["text"].toString();
|
||||
this.title = "Preview Animation " + values
|
||||
return r
|
||||
}
|
||||
break;
|
||||
|
||||
case "VRAM_Debug":
|
||||
const onVRAM_DebugConnectInput = nodeType.prototype.onConnectInput;
|
||||
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
|
||||
const v = onVRAM_DebugConnectInput? onVRAM_DebugConnectInput.apply(this, arguments): undefined
|
||||
this.outputs[3]["label"] = "freemem_before"
|
||||
this.outputs[4]["label"] = "freemem_after"
|
||||
return v;
|
||||
}
|
||||
const onVRAM_DebugExecuted = nodeType.prototype.onAfterExecuteNode;
|
||||
nodeType.prototype.onExecuted = function(message) {
|
||||
const r = onVRAM_DebugExecuted? onVRAM_DebugExecuted.apply(this,arguments): undefined
|
||||
let values = message["text"].toString().split('x');
|
||||
this.outputs[3]["label"] = values[0] + " freemem_before"
|
||||
this.outputs[4]["label"] = values[1] + " freemem_after"
|
||||
return r
|
||||
}
|
||||
break;
|
||||
|
||||
case "JoinStringMulti":
|
||||
const originalOnNodeCreated = nodeType.prototype.onNodeCreated || function() {};
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
originalOnNodeCreated.apply(this, arguments);
|
||||
|
||||
this._type = "STRING";
|
||||
this.addWidget("button", "Update inputs", null, () => {
|
||||
if (!this.inputs) {
|
||||
this.inputs = [];
|
||||
}
|
||||
const target_number_of_inputs = this.widgets.find(w => w.name === "inputcount")["value"];
|
||||
const num_inputs = this.inputs.filter(input => input.name && input.name.toLowerCase().includes("string_")).length
|
||||
if (target_number_of_inputs === num_inputs) return; // already set, do nothing
|
||||
|
||||
if(target_number_of_inputs < num_inputs){
|
||||
const inputs_to_remove = num_inputs - target_number_of_inputs;
|
||||
for(let i = 0; i < inputs_to_remove; i++) {
|
||||
this.removeInput(this.inputs.length - 1);
|
||||
}
|
||||
}
|
||||
else{
|
||||
for(let i = num_inputs+1; i <= target_number_of_inputs; ++i)
|
||||
this.addInput(`string_${i}`, this._type, {shape: 7});
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "SoundReactive":
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
let audioContext;
|
||||
let microphoneStream;
|
||||
let animationFrameId;
|
||||
let analyser;
|
||||
let dataArray;
|
||||
let startRangeHz;
|
||||
let endRangeHz;
|
||||
let smoothingFactor = 0.5;
|
||||
let smoothedSoundLevel = 0;
|
||||
|
||||
// Function to update the widget value in real-time
|
||||
const updateWidgetValueInRealTime = () => {
|
||||
// Ensure analyser and dataArray are defined before using them
|
||||
if (analyser && dataArray) {
|
||||
analyser.getByteFrequencyData(dataArray);
|
||||
|
||||
const startRangeHzWidget = this.widgets.find(w => w.name === "start_range_hz");
|
||||
if (startRangeHzWidget) startRangeHz = startRangeHzWidget.value;
|
||||
const endRangeHzWidget = this.widgets.find(w => w.name === "end_range_hz");
|
||||
if (endRangeHzWidget) endRangeHz = endRangeHzWidget.value;
|
||||
const smoothingFactorWidget = this.widgets.find(w => w.name === "smoothing_factor");
|
||||
if (smoothingFactorWidget) smoothingFactor = smoothingFactorWidget.value;
|
||||
|
||||
// Calculate frequency bin width (frequency resolution)
|
||||
const frequencyBinWidth = audioContext.sampleRate / analyser.fftSize;
|
||||
// Convert the widget values from Hz to indices
|
||||
const startRangeIndex = Math.floor(startRangeHz / frequencyBinWidth);
|
||||
const endRangeIndex = Math.floor(endRangeHz / frequencyBinWidth);
|
||||
|
||||
// Function to calculate the average value for a frequency range
|
||||
const calculateAverage = (start, end) => {
|
||||
const sum = dataArray.slice(start, end).reduce((acc, val) => acc + val, 0);
|
||||
const average = sum / (end - start);
|
||||
|
||||
// Apply exponential moving average smoothing
|
||||
smoothedSoundLevel = (average * (1 - smoothingFactor)) + (smoothedSoundLevel * smoothingFactor);
|
||||
return smoothedSoundLevel;
|
||||
};
|
||||
// Calculate the average levels for each frequency range
|
||||
const soundLevel = calculateAverage(startRangeIndex, endRangeIndex);
|
||||
|
||||
// Update the widget values
|
||||
|
||||
const lowLevelWidget = this.widgets.find(w => w.name === "sound_level");
|
||||
if (lowLevelWidget) lowLevelWidget.value = soundLevel;
|
||||
|
||||
animationFrameId = requestAnimationFrame(updateWidgetValueInRealTime);
|
||||
}
|
||||
};
|
||||
|
||||
// Function to start capturing audio from the microphone
|
||||
const startMicrophoneCapture = () => {
|
||||
// Only create the audio context and analyser once
|
||||
if (!audioContext) {
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
// Access the sample rate of the audio context
|
||||
console.log(`Sample rate: ${audioContext.sampleRate}Hz`);
|
||||
analyser = audioContext.createAnalyser();
|
||||
analyser.fftSize = 2048;
|
||||
dataArray = new Uint8Array(analyser.frequencyBinCount);
|
||||
// Get the range values from widgets (assumed to be in Hz)
|
||||
const lowRangeWidget = this.widgets.find(w => w.name === "low_range_hz");
|
||||
if (lowRangeWidget) startRangeHz = lowRangeWidget.value;
|
||||
|
||||
const midRangeWidget = this.widgets.find(w => w.name === "mid_range_hz");
|
||||
if (midRangeWidget) endRangeHz = midRangeWidget.value;
|
||||
}
|
||||
|
||||
navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
|
||||
microphoneStream = stream;
|
||||
const microphone = audioContext.createMediaStreamSource(stream);
|
||||
microphone.connect(analyser);
|
||||
updateWidgetValueInRealTime();
|
||||
}).catch(error => {
|
||||
console.error('Access to microphone was denied or an error occurred:', error);
|
||||
});
|
||||
};
|
||||
|
||||
// Function to stop capturing audio from the microphone
|
||||
const stopMicrophoneCapture = () => {
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
}
|
||||
if (microphoneStream) {
|
||||
microphoneStream.getTracks().forEach(track => track.stop());
|
||||
}
|
||||
if (audioContext) {
|
||||
audioContext.close();
|
||||
// Reset audioContext to ensure it can be created again when starting
|
||||
audioContext = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Add start button
|
||||
this.addWidget("button", "Start mic capture", null, startMicrophoneCapture);
|
||||
|
||||
// Add stop button
|
||||
this.addWidget("button", "Stop mic capture", null, stopMicrophoneCapture);
|
||||
};
|
||||
break;
|
||||
case "SaveImageKJ":
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function() {
|
||||
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : void 0;
|
||||
const widget = this.widgets.find((w) => w.name === "filename_prefix");
|
||||
widget.serializeValue = () => {
|
||||
return applyTextReplacements(app, widget.value);
|
||||
};
|
||||
return r;
|
||||
};
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
async setup() {
|
||||
// to keep Set/Get node virtual connections visible when offscreen
|
||||
const originalComputeVisibleNodes = LGraphCanvas.prototype.computeVisibleNodes;
|
||||
LGraphCanvas.prototype.computeVisibleNodes = function () {
|
||||
const visibleNodesSet = new Set(originalComputeVisibleNodes.apply(this, arguments));
|
||||
for (const node of this.graph._nodes) {
|
||||
if ((node.type === "SetNode" || node.type === "GetNode") && node.drawConnection) {
|
||||
visibleNodesSet.add(node);
|
||||
}
|
||||
}
|
||||
return Array.from(visibleNodesSet);
|
||||
};
|
||||
|
||||
}
|
||||
});
|
||||
744
custom_nodes/ComfyUI-KJNodes/web/js/point_editor.js
Normal file
744
custom_nodes/ComfyUI-KJNodes/web/js/point_editor.js
Normal file
@@ -0,0 +1,744 @@
|
||||
const { app } = window.comfyAPI.app;
|
||||
import { getLocalMouse } from './protovisUtil.js';
|
||||
|
||||
//from melmass
|
||||
export function makeUUID() {
|
||||
let dt = new Date().getTime()
|
||||
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = ((dt + Math.random() * 16) % 16) | 0
|
||||
dt = Math.floor(dt / 16)
|
||||
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
|
||||
})
|
||||
return uuid
|
||||
}
|
||||
|
||||
export const loadScript = (
|
||||
FILE_URL,
|
||||
async = true,
|
||||
type = 'text/javascript',
|
||||
) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// Check if the script already exists
|
||||
const existingScript = document.querySelector(`script[src="${FILE_URL}"]`)
|
||||
if (existingScript) {
|
||||
resolve({ status: true, message: 'Script already loaded' })
|
||||
return
|
||||
}
|
||||
|
||||
const scriptEle = document.createElement('script')
|
||||
scriptEle.type = type
|
||||
scriptEle.async = async
|
||||
scriptEle.src = FILE_URL
|
||||
|
||||
scriptEle.addEventListener('load', (ev) => {
|
||||
resolve({ status: true })
|
||||
})
|
||||
|
||||
scriptEle.addEventListener('error', (ev) => {
|
||||
reject({
|
||||
status: false,
|
||||
message: `Failed to load the script ${FILE_URL}`,
|
||||
})
|
||||
})
|
||||
|
||||
document.body.appendChild(scriptEle)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
const create_documentation_stylesheet = () => {
|
||||
const tag = 'kj-pointseditor-stylesheet'
|
||||
|
||||
let styleTag = document.head.querySelector(tag)
|
||||
|
||||
if (!styleTag) {
|
||||
styleTag = document.createElement('style')
|
||||
styleTag.type = 'text/css'
|
||||
styleTag.id = tag
|
||||
styleTag.innerHTML = `
|
||||
.points-editor {
|
||||
|
||||
position: absolute;
|
||||
|
||||
font: 12px monospace;
|
||||
line-height: 1.5em;
|
||||
padding: 10px;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
`
|
||||
document.head.appendChild(styleTag)
|
||||
}
|
||||
}
|
||||
|
||||
loadScript('kjweb_async/svg-path-properties.min.js').catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
loadScript('kjweb_async/protovis.min.js').catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
create_documentation_stylesheet()
|
||||
|
||||
function chainCallback(object, property, callback) {
|
||||
if (object == undefined) {
|
||||
//This should not happen.
|
||||
console.error("Tried to add callback to non-existant object")
|
||||
return;
|
||||
}
|
||||
if (property in object) {
|
||||
const callback_orig = object[property]
|
||||
object[property] = function () {
|
||||
const r = callback_orig.apply(this, arguments);
|
||||
callback.apply(this, arguments);
|
||||
return r
|
||||
};
|
||||
} else {
|
||||
object[property] = callback;
|
||||
}
|
||||
}
|
||||
app.registerExtension({
|
||||
name: 'KJNodes.PointEditor',
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
if (nodeData?.name === 'PointsEditor') {
|
||||
chainCallback(nodeType.prototype, "onNodeCreated", function () {
|
||||
|
||||
hideWidgetForGood(this, this.widgets.find(w => w.name === "coordinates"))
|
||||
hideWidgetForGood(this, this.widgets.find(w => w.name === "neg_coordinates"))
|
||||
hideWidgetForGood(this, this.widgets.find(w => w.name === "bboxes"))
|
||||
|
||||
var element = document.createElement("div");
|
||||
this.uuid = makeUUID()
|
||||
element.id = `points-editor-${this.uuid}`
|
||||
|
||||
this.previewMediaType = 'image'
|
||||
|
||||
this.pointsEditor = this.addDOMWidget(nodeData.name, "PointsEditorWidget", element, {
|
||||
serialize: false,
|
||||
hideOnZoom: false,
|
||||
});
|
||||
|
||||
// context menu
|
||||
this.contextMenu = document.createElement("div");
|
||||
this.contextMenu.id = "context-menu";
|
||||
this.contextMenu.style.display = "none";
|
||||
this.contextMenu.style.position = "absolute";
|
||||
this.contextMenu.style.backgroundColor = "#202020";
|
||||
this.contextMenu.style.minWidth = "100px";
|
||||
this.contextMenu.style.boxShadow = "0px 8px 16px 0px rgba(0,0,0,0.2)";
|
||||
this.contextMenu.style.zIndex = "100";
|
||||
this.contextMenu.style.padding = "5px";
|
||||
|
||||
function styleMenuItem(menuItem) {
|
||||
menuItem.style.display = "block";
|
||||
menuItem.style.padding = "5px";
|
||||
menuItem.style.color = "#FFF";
|
||||
menuItem.style.fontFamily = "Arial, sans-serif";
|
||||
menuItem.style.fontSize = "16px";
|
||||
menuItem.style.textDecoration = "none";
|
||||
menuItem.style.marginBottom = "5px";
|
||||
}
|
||||
function createMenuItem(id, textContent) {
|
||||
let menuItem = document.createElement("a");
|
||||
menuItem.href = "#";
|
||||
menuItem.id = `menu-item-${id}`;
|
||||
menuItem.textContent = textContent;
|
||||
styleMenuItem(menuItem);
|
||||
return menuItem;
|
||||
}
|
||||
|
||||
// Create an array of menu items using the createMenuItem function
|
||||
this.menuItems = [
|
||||
createMenuItem(0, "Load Image"),
|
||||
createMenuItem(1, "Clear Image"),
|
||||
];
|
||||
|
||||
// Add mouseover and mouseout event listeners to each menu item for styling
|
||||
this.menuItems.forEach(menuItem => {
|
||||
menuItem.addEventListener('mouseover', function () {
|
||||
this.style.backgroundColor = "gray";
|
||||
});
|
||||
|
||||
menuItem.addEventListener('mouseout', function () {
|
||||
this.style.backgroundColor = "#202020";
|
||||
});
|
||||
});
|
||||
|
||||
// Append each menu item to the context menu
|
||||
this.menuItems.forEach(menuItem => {
|
||||
this.contextMenu.appendChild(menuItem);
|
||||
});
|
||||
|
||||
document.body.appendChild(this.contextMenu);
|
||||
|
||||
this.addWidget("button", "New canvas", null, () => {
|
||||
if (!this.properties || !("points" in this.properties)) {
|
||||
this.editor = new PointsEditor(this);
|
||||
this.addProperty("points", this.constructor.type, "string");
|
||||
this.addProperty("neg_points", this.constructor.type, "string");
|
||||
|
||||
}
|
||||
else {
|
||||
this.editor = new PointsEditor(this, true);
|
||||
}
|
||||
});
|
||||
|
||||
this.setSize([550, 550]);
|
||||
this.resizable = false;
|
||||
this.pointsEditor.parentEl = document.createElement("div");
|
||||
this.pointsEditor.parentEl.className = "points-editor";
|
||||
this.pointsEditor.parentEl.id = `points-editor-${this.uuid}`
|
||||
element.appendChild(this.pointsEditor.parentEl);
|
||||
|
||||
chainCallback(this, "onConfigure", function () {
|
||||
try {
|
||||
this.editor = new PointsEditor(this);
|
||||
} catch (error) {
|
||||
console.error("An error occurred while configuring the editor:", error);
|
||||
}
|
||||
});
|
||||
chainCallback(this, "onExecuted", function (message) {
|
||||
let bg_image = message["bg_image"];
|
||||
this.properties.imgData = {
|
||||
name: "bg_image",
|
||||
base64: bg_image
|
||||
};
|
||||
this.editor.refreshBackgroundImage(this);
|
||||
});
|
||||
|
||||
}); // onAfterGraphConfigured
|
||||
}//node created
|
||||
} //before register
|
||||
})//register
|
||||
|
||||
class PointsEditor {
|
||||
constructor(context, reset = false) {
|
||||
this.node = context;
|
||||
this.reset = reset;
|
||||
const self = this; // Keep a reference to the main class context
|
||||
|
||||
console.log("creatingPointEditor")
|
||||
|
||||
this.node.pasteFile = (file) => {
|
||||
if (file.type.startsWith("image/")) {
|
||||
this.handleImageFile(file);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
this.node.onDragOver = function (e) {
|
||||
if (e.dataTransfer && e.dataTransfer.items) {
|
||||
return [...e.dataTransfer.items].some(f => f.kind === "file" && f.type.startsWith("image/"));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// On drop upload files
|
||||
this.node.onDragDrop = (e) => {
|
||||
console.log("onDragDrop called");
|
||||
let handled = false;
|
||||
for (const file of e.dataTransfer.files) {
|
||||
if (file.type.startsWith("image/")) {
|
||||
this.handleImageFile(file);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
return handled;
|
||||
};
|
||||
|
||||
// context menu
|
||||
this.createContextMenu();
|
||||
|
||||
if (reset && context.pointsEditor.element) {
|
||||
context.pointsEditor.element.innerHTML = ''; // Clear the container
|
||||
}
|
||||
this.pos_coordWidget = context.widgets.find(w => w.name === "coordinates");
|
||||
this.neg_coordWidget = context.widgets.find(w => w.name === "neg_coordinates");
|
||||
this.pointsStoreWidget = context.widgets.find(w => w.name === "points_store");
|
||||
this.widthWidget = context.widgets.find(w => w.name === "width");
|
||||
this.heightWidget = context.widgets.find(w => w.name === "height");
|
||||
this.bboxStoreWidget = context.widgets.find(w => w.name === "bbox_store");
|
||||
this.bboxWidget = context.widgets.find(w => w.name === "bboxes");
|
||||
|
||||
//widget callbacks
|
||||
this.widthWidget.callback = () => {
|
||||
this.width = this.widthWidget.value;
|
||||
if (this.width > 256) {
|
||||
context.setSize([this.width + 45, context.size[1]]);
|
||||
}
|
||||
this.vis.width(this.width);
|
||||
this.updateData();
|
||||
}
|
||||
this.heightWidget.callback = () => {
|
||||
this.height = this.heightWidget.value
|
||||
this.vis.height(this.height)
|
||||
context.setSize([context.size[0], this.height + 300]);
|
||||
this.updateData();
|
||||
}
|
||||
this.pointsStoreWidget.callback = () => {
|
||||
this.points = JSON.parse(pointsStoreWidget.value).positive;
|
||||
this.neg_points = JSON.parse(pointsStoreWidget.value).negative;
|
||||
this.updateData();
|
||||
}
|
||||
this.bboxStoreWidget.callback = () => {
|
||||
this.bbox = JSON.parse(bboxStoreWidget.value)
|
||||
this.updateData();
|
||||
}
|
||||
|
||||
this.width = this.widthWidget.value;
|
||||
this.height = this.heightWidget.value;
|
||||
var i = 3;
|
||||
this.points = [];
|
||||
this.neg_points = [];
|
||||
this.bbox = [{}];
|
||||
var drawing = false;
|
||||
|
||||
// Initialize or reset points array
|
||||
if (!reset && this.pointsStoreWidget.value != "") {
|
||||
this.points = JSON.parse(this.pointsStoreWidget.value).positive;
|
||||
this.neg_points = JSON.parse(this.pointsStoreWidget.value).negative;
|
||||
this.bbox = JSON.parse(this.bboxStoreWidget.value);
|
||||
console.log(this.bbox)
|
||||
} else {
|
||||
this.points = [
|
||||
{
|
||||
x: this.width / 2, // Middle point horizontally centered
|
||||
y: this.height / 2 // Middle point vertically centered
|
||||
}
|
||||
];
|
||||
this.neg_points = [
|
||||
{
|
||||
x: 0, // Middle point horizontally centered
|
||||
y: 0 // Middle point vertically centered
|
||||
}
|
||||
];
|
||||
const combinedPoints = {
|
||||
positive: this.points,
|
||||
negative: this.neg_points,
|
||||
};
|
||||
this.pointsStoreWidget.value = JSON.stringify(combinedPoints);
|
||||
this.bboxStoreWidget.value = JSON.stringify(this.bbox);
|
||||
}
|
||||
|
||||
//create main canvas panel
|
||||
this.vis = new pv.Panel()
|
||||
.width(this.width)
|
||||
.height(this.height)
|
||||
.fillStyle("#222")
|
||||
.strokeStyle("gray")
|
||||
.lineWidth(2)
|
||||
.antialias(false)
|
||||
.margin(10)
|
||||
.event("mousedown", function () {
|
||||
let mouse = getLocalMouse(this);
|
||||
if (pv.event.shiftKey && pv.event.button === 2) { // Use pv.event to access the event object
|
||||
let scaledMouse = {
|
||||
x: mouse.x / app.canvas.ds.scale,
|
||||
y: mouse.y / app.canvas.ds.scale
|
||||
};
|
||||
i = self.neg_points.push(scaledMouse) - 1;
|
||||
self.updateData();
|
||||
return this;
|
||||
}
|
||||
else if (pv.event.shiftKey) {
|
||||
let scaledMouse = {
|
||||
x: mouse.x / app.canvas.ds.scale,
|
||||
y: mouse.y / app.canvas.ds.scale
|
||||
};
|
||||
i = self.points.push(scaledMouse) - 1;
|
||||
self.updateData();
|
||||
return this;
|
||||
}
|
||||
else if (pv.event.ctrlKey) {
|
||||
console.log("start drawing at " + mouse.x / app.canvas.ds.scale + ", " + mouse.y / app.canvas.ds.scale);
|
||||
drawing = true;
|
||||
self.bbox[0].startX = mouse.x / app.canvas.ds.scale;
|
||||
self.bbox[0].startY = mouse.y / app.canvas.ds.scale;
|
||||
}
|
||||
else if (pv.event.button === 2) {
|
||||
self.node.contextMenu.style.display = 'block';
|
||||
self.node.contextMenu.style.left = `${pv.event.clientX}px`;
|
||||
self.node.contextMenu.style.top = `${pv.event.clientY}px`;
|
||||
}
|
||||
})
|
||||
.event("mousemove", function () {
|
||||
if (drawing) {
|
||||
let mouse = getLocalMouse(this);
|
||||
self.bbox[0].endX = mouse.x / app.canvas.ds.scale;
|
||||
self.bbox[0].endY = mouse.y / app.canvas.ds.scale;
|
||||
self.vis.render();
|
||||
}
|
||||
})
|
||||
.event("mouseup", function () {
|
||||
let mouse = getLocalMouse(this);
|
||||
console.log("end drawing at " + mouse.x / app.canvas.ds.scale + ", " + mouse.y / app.canvas.ds.scale);
|
||||
drawing = false;
|
||||
self.updateData();
|
||||
});
|
||||
|
||||
this.backgroundImage = this.vis.add(pv.Image).visible(false)
|
||||
|
||||
//create bounding box
|
||||
this.bounding_box = this.vis.add(pv.Area)
|
||||
.data(function () {
|
||||
if (drawing || (self.bbox && self.bbox[0] && Object.keys(self.bbox[0]).length > 0)) {
|
||||
return [self.bbox[0].startX, self.bbox[0].endX];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
})
|
||||
.bottom(function () {return self.height - Math.max(self.bbox[0].startY, self.bbox[0].endY); })
|
||||
.left(function (d) {return d; })
|
||||
.height(function () {return Math.abs(self.bbox[0].startY - self.bbox[0].endY);})
|
||||
.fillStyle("rgba(70, 130, 180, 0.5)")
|
||||
.strokeStyle("steelblue")
|
||||
.visible(function () {return drawing || Object.keys(self.bbox[0]).length > 0; })
|
||||
.add(pv.Dot)
|
||||
.visible(function () {return drawing || Object.keys(self.bbox[0]).length > 0; })
|
||||
.data(() => {
|
||||
if (self.bbox && Object.keys(self.bbox[0]).length > 0) {
|
||||
return [{
|
||||
x: self.bbox[0].endX,
|
||||
y: self.bbox[0].endY
|
||||
}];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
})
|
||||
.left(d => d.x)
|
||||
.top(d => d.y)
|
||||
.radius(Math.log(Math.min(self.width, self.height)) * 1)
|
||||
.shape("square")
|
||||
.cursor("move")
|
||||
.strokeStyle("steelblue")
|
||||
.lineWidth(2)
|
||||
.fillStyle(function () { return "rgba(100, 100, 100, 0.6)"; })
|
||||
.event("mousedown", pv.Behavior.drag())
|
||||
.event("drag", function () {
|
||||
let mouse = getLocalMouse(this);
|
||||
let adjustedX = mouse.x / app.canvas.ds.scale; // Adjust the new position by the inverse of the scale factor
|
||||
let adjustedY = mouse.y / app.canvas.ds.scale;
|
||||
|
||||
// Adjust the new position if it would place the dot outside the bounds of the vis.Panel
|
||||
adjustedX = Math.max(0, Math.min(self.vis.width(), adjustedX));
|
||||
adjustedY = Math.max(0, Math.min(self.vis.height(), adjustedY));
|
||||
self.bbox[0].endX = adjustedX;
|
||||
self.bbox[0].endY = adjustedY;
|
||||
self.vis.render();
|
||||
})
|
||||
.event("dragend", function () {
|
||||
self.updateData();
|
||||
});
|
||||
|
||||
//create positive points
|
||||
this.vis.add(pv.Dot)
|
||||
.data(() => this.points)
|
||||
.left(d => d.x)
|
||||
.top(d => d.y)
|
||||
.radius(Math.log(Math.min(self.width, self.height)) * 4)
|
||||
.shape("circle")
|
||||
.cursor("move")
|
||||
.strokeStyle(function () { return i == this.index ? "#07f907" : "#139613"; })
|
||||
.lineWidth(4)
|
||||
.fillStyle(function () { return "rgba(100, 100, 100, 0.6)"; })
|
||||
.event("mousedown", pv.Behavior.drag())
|
||||
.event("dragstart", function () {
|
||||
i = this.index;
|
||||
})
|
||||
.event("dragend", function () {
|
||||
if (pv.event.button === 2 && i !== 0 && i !== self.points.length - 1) {
|
||||
this.index = i;
|
||||
self.points.splice(i--, 1);
|
||||
}
|
||||
self.updateData();
|
||||
|
||||
})
|
||||
.event("drag", function () {
|
||||
let mouse = getLocalMouse(this);
|
||||
let adjustedX = mouse.x / app.canvas.ds.scale; // Adjust the new X position by the inverse of the scale factor
|
||||
let adjustedY = mouse.y / app.canvas.ds.scale; // Adjust the new Y position by the inverse of the scale factor
|
||||
// Determine the bounds of the vis.Panel
|
||||
const panelWidth = self.vis.width();
|
||||
const panelHeight = self.vis.height();
|
||||
|
||||
// Adjust the new position if it would place the dot outside the bounds of the vis.Panel
|
||||
adjustedX = Math.max(0, Math.min(panelWidth, adjustedX));
|
||||
adjustedY = Math.max(0, Math.min(panelHeight, adjustedY));
|
||||
self.points[this.index] = { x: adjustedX, y: adjustedY }; // Update the point's position
|
||||
self.vis.render(); // Re-render the visualization to reflect the new position
|
||||
})
|
||||
|
||||
.anchor("center")
|
||||
.add(pv.Label)
|
||||
.left(d => d.x < this.width / 2 ? d.x + 30 : d.x - 35) // Shift label to right if on left half, otherwise shift to left
|
||||
.top(d => d.y < this.height / 2 ? d.y + 25 : d.y - 25) // Shift label down if on top half, otherwise shift up
|
||||
.font(25 + "px sans-serif")
|
||||
.text(d => {return this.points.indexOf(d); })
|
||||
.textStyle("#139613")
|
||||
.textShadow("2px 2px 2px black")
|
||||
.add(pv.Dot) // Add smaller point in the center
|
||||
.data(() => this.points)
|
||||
.left(d => d.x)
|
||||
.top(d => d.y)
|
||||
.radius(2) // Smaller radius for the center point
|
||||
.shape("circle")
|
||||
.fillStyle("red") // Color for the center point
|
||||
.lineWidth(1); // Stroke thickness for the center point
|
||||
|
||||
//create negative points
|
||||
this.vis.add(pv.Dot)
|
||||
.data(() => this.neg_points)
|
||||
.left(d => d.x)
|
||||
.top(d => d.y)
|
||||
.radius(Math.log(Math.min(self.width, self.height)) * 4)
|
||||
.shape("circle")
|
||||
.cursor("move")
|
||||
.strokeStyle(function () { return i == this.index ? "#f91111" : "#891616"; })
|
||||
.lineWidth(4)
|
||||
.fillStyle(function () { return "rgba(100, 100, 100, 0.6)"; })
|
||||
.event("mousedown", pv.Behavior.drag())
|
||||
.event("dragstart", function () {
|
||||
i = this.index;
|
||||
})
|
||||
.event("dragend", function () {
|
||||
if (pv.event.button === 2 && i !== 0 && i !== self.neg_points.length - 1) {
|
||||
this.index = i;
|
||||
self.neg_points.splice(i--, 1);
|
||||
}
|
||||
self.updateData();
|
||||
|
||||
})
|
||||
.event("drag", function () {
|
||||
let mouse = getLocalMouse(this);
|
||||
let adjustedX = mouse.x / app.canvas.ds.scale; // Adjust the new X position by the inverse of the scale factor
|
||||
let adjustedY = mouse.y / app.canvas.ds.scale; // Adjust the new Y position by the inverse of the scale factor
|
||||
// Determine the bounds of the vis.Panel
|
||||
const panelWidth = self.vis.width();
|
||||
const panelHeight = self.vis.height();
|
||||
|
||||
// Adjust the new position if it would place the dot outside the bounds of the vis.Panel
|
||||
adjustedX = Math.max(0, Math.min(panelWidth, adjustedX));
|
||||
adjustedY = Math.max(0, Math.min(panelHeight, adjustedY));
|
||||
self.neg_points[this.index] = { x: adjustedX, y: adjustedY }; // Update the point's position
|
||||
self.vis.render(); // Re-render the visualization to reflect the new position
|
||||
})
|
||||
.anchor("center")
|
||||
.add(pv.Label)
|
||||
.left(d => d.x < this.width / 2 ? d.x + 30 : d.x - 35) // Shift label to right if on left half, otherwise shift to left
|
||||
.top(d => d.y < this.height / 2 ? d.y + 25 : d.y - 25) // Shift label down if on top half, otherwise shift up
|
||||
.font(25 + "px sans-serif")
|
||||
.text(d => {return this.neg_points.indexOf(d); })
|
||||
.textStyle("red")
|
||||
.textShadow("2px 2px 2px black")
|
||||
.add(pv.Dot) // Add smaller point in the center
|
||||
.data(() => this.neg_points)
|
||||
.left(d => d.x)
|
||||
.top(d => d.y)
|
||||
.radius(2) // Smaller radius for the center point
|
||||
.shape("circle")
|
||||
.fillStyle("red") // Color for the center point
|
||||
.lineWidth(1); // Stroke thickness for the center point
|
||||
|
||||
if (this.points.length != 0) {
|
||||
this.vis.render();
|
||||
}
|
||||
|
||||
var svgElement = this.vis.canvas();
|
||||
svgElement.style['zIndex'] = "2"
|
||||
svgElement.style['position'] = "relative"
|
||||
this.node.pointsEditor.element.appendChild(svgElement);
|
||||
|
||||
if (this.width > 256) {
|
||||
this.node.setSize([this.width + 45, this.node.size[1]]);
|
||||
}
|
||||
this.node.setSize([this.node.size[0], this.height + 300]);
|
||||
this.updateData();
|
||||
this.refreshBackgroundImage();
|
||||
|
||||
}//end constructor
|
||||
|
||||
updateData = () => {
|
||||
if (!this.points || this.points.length === 0) {
|
||||
console.log("no points");
|
||||
return;
|
||||
}
|
||||
const combinedPoints = {
|
||||
positive: this.points,
|
||||
negative: this.neg_points,
|
||||
};
|
||||
this.pointsStoreWidget.value = JSON.stringify(combinedPoints);
|
||||
this.pos_coordWidget.value = JSON.stringify(this.points);
|
||||
this.neg_coordWidget.value = JSON.stringify(this.neg_points);
|
||||
|
||||
if (this.bbox.length != 0) {
|
||||
let bboxString = JSON.stringify(this.bbox);
|
||||
this.bboxStoreWidget.value = bboxString;
|
||||
this.bboxWidget.value = bboxString;
|
||||
}
|
||||
|
||||
this.vis.render();
|
||||
};
|
||||
|
||||
handleImageLoad = (img, file, base64String) => {
|
||||
console.log(img.width, img.height); // Access width and height here
|
||||
this.widthWidget.value = img.width;
|
||||
this.heightWidget.value = img.height;
|
||||
|
||||
if (img.width != this.vis.width() || img.height != this.vis.height()) {
|
||||
if (img.width > 256) {
|
||||
this.node.setSize([img.width + 45, this.node.size[1]]);
|
||||
}
|
||||
this.node.setSize([this.node.size[0], img.height + 300]);
|
||||
this.vis.width(img.width);
|
||||
this.vis.height(img.height);
|
||||
this.height = img.height;
|
||||
this.width = img.width;
|
||||
this.updateData();
|
||||
}
|
||||
this.backgroundImage.url(file ? URL.createObjectURL(file) : `data:${this.node.properties.imgData.type};base64,${base64String}`).visible(true).root.render();
|
||||
};
|
||||
|
||||
processImage = (img, file) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const maxWidth = 800; // maximum width
|
||||
const maxHeight = 600; // maximum height
|
||||
let width = img.width;
|
||||
let height = img.height;
|
||||
|
||||
// Calculate the new dimensions while preserving the aspect ratio
|
||||
if (width > height) {
|
||||
if (width > maxWidth) {
|
||||
height *= maxWidth / width;
|
||||
width = maxWidth;
|
||||
}
|
||||
} else {
|
||||
if (height > maxHeight) {
|
||||
width *= maxHeight / height;
|
||||
height = maxHeight;
|
||||
}
|
||||
}
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
|
||||
// Get the compressed image data as a Base64 string
|
||||
const base64String = canvas.toDataURL('image/jpeg', 0.5).replace('data:', '').replace(/^.+,/, ''); // 0.5 is the quality from 0 to 1
|
||||
|
||||
this.node.properties.imgData = {
|
||||
name: file.name,
|
||||
lastModified: file.lastModified,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
base64: base64String
|
||||
};
|
||||
handleImageLoad(img, file, base64String);
|
||||
};
|
||||
|
||||
handleImageFile = (file) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const img = new Image();
|
||||
img.src = reader.result;
|
||||
img.onload = () => processImage(img, file);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
const imageUrl = URL.createObjectURL(file);
|
||||
const img = new Image();
|
||||
img.src = imageUrl;
|
||||
img.onload = () => this.handleImageLoad(img, file, null);
|
||||
};
|
||||
|
||||
refreshBackgroundImage = () => {
|
||||
if (this.node.properties.imgData && this.node.properties.imgData.base64) {
|
||||
const base64String = this.node.properties.imgData.base64;
|
||||
const imageUrl = `data:${this.node.properties.imgData.type};base64,${base64String}`;
|
||||
const img = new Image();
|
||||
img.src = imageUrl;
|
||||
img.onload = () => this.handleImageLoad(img, null, base64String);
|
||||
}
|
||||
};
|
||||
|
||||
createContextMenu = () => {
|
||||
self = this;
|
||||
document.addEventListener('contextmenu', function (e) {
|
||||
if (e.target.closest(`#points-editor-${self.node.uuid}`) ||
|
||||
e.target.closest('#context-menu')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
if (!self.node.contextMenu.contains(e.target)) {
|
||||
self.node.contextMenu.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
this.node.menuItems.forEach((menuItem, index) => {
|
||||
self = this;
|
||||
menuItem.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
switch (index) {
|
||||
case 0:
|
||||
// Create file input element
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'image/*'; // Accept only image files
|
||||
|
||||
// Listen for file selection
|
||||
fileInput.addEventListener('change', function (event) {
|
||||
const file = event.target.files[0]; // Get the selected file
|
||||
|
||||
if (file) {
|
||||
const imageUrl = URL.createObjectURL(file);
|
||||
let img = new Image();
|
||||
img.src = imageUrl;
|
||||
img.onload = () => self.handleImageLoad(img, file, null);
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.click();
|
||||
|
||||
self.node.contextMenu.style.display = 'none';
|
||||
break;
|
||||
case 1:
|
||||
self.backgroundImage.visible(false).root.render();
|
||||
self.node.properties.imgData = null;
|
||||
self.node.contextMenu.style.display = 'none';
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}//end createContextMenu
|
||||
}//end class
|
||||
|
||||
|
||||
//from melmass
|
||||
export function hideWidgetForGood(node, widget, suffix = '') {
|
||||
widget.origType = widget.type
|
||||
widget.origComputeSize = widget.computeSize
|
||||
widget.origSerializeValue = widget.serializeValue
|
||||
widget.computeSize = () => [0, -4] // -4 is due to the gap litegraph adds between widgets automatically
|
||||
widget.type = "converted-widget" + suffix
|
||||
// widget.serializeValue = () => {
|
||||
// // Prevent serializing the widget if we have no input linked
|
||||
// const w = node.inputs?.find((i) => i.widget?.name === widget.name);
|
||||
// if (w?.link == null) {
|
||||
// return undefined;
|
||||
// }
|
||||
// return widget.origSerializeValue ? widget.origSerializeValue() : widget.value;
|
||||
// };
|
||||
|
||||
// Hide any linked widgets, e.g. seed+seedControl
|
||||
if (widget.linkedWidgets) {
|
||||
for (const w of widget.linkedWidgets) {
|
||||
hideWidgetForGood(node, w, ':' + widget.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
25
custom_nodes/ComfyUI-KJNodes/web/js/protovisUtil.js
Normal file
25
custom_nodes/ComfyUI-KJNodes/web/js/protovisUtil.js
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Utility functions for protovis in ComfyUI.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get correct local coordinates for protovis in transformed containers.
|
||||
* Uses getBoundingClientRect() which properly accounts for CSS transforms.
|
||||
*
|
||||
* This fixes coordinate calculation issues when protovis widgets are rendered
|
||||
* inside ComfyUI's vueNodes mode, which uses CSS transforms for panning/zooming.
|
||||
*
|
||||
* @param {pv.Mark} mark - The protovis mark instance
|
||||
* @returns {{x: number, y: number}} Local coordinates relative to the canvas
|
||||
*/
|
||||
export function getLocalMouse(mark) {
|
||||
const e = pv.event
|
||||
if (!e) return { x: 0, y: 0 }
|
||||
const canvas = mark.root.canvas()
|
||||
if (!canvas) return { x: 0, y: 0 }
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
return {
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top
|
||||
}
|
||||
}
|
||||
567
custom_nodes/ComfyUI-KJNodes/web/js/setgetnodes.js
Normal file
567
custom_nodes/ComfyUI-KJNodes/web/js/setgetnodes.js
Normal file
@@ -0,0 +1,567 @@
|
||||
const { app } = window.comfyAPI.app;
|
||||
|
||||
//based on diffus3's SetGet: https://github.com/diffus3/ComfyUI-extensions
|
||||
|
||||
// Nodes that allow you to tunnel connections for cleaner graphs
|
||||
function setColorAndBgColor(type) {
|
||||
const colorMap = {
|
||||
"DEFAULT": LGraphCanvas.node_colors.gray,
|
||||
"MODEL": LGraphCanvas.node_colors.blue,
|
||||
"LATENT": LGraphCanvas.node_colors.purple,
|
||||
"VAE": LGraphCanvas.node_colors.red,
|
||||
"WANVAE": LGraphCanvas.node_colors.red,
|
||||
"CONDITIONING": LGraphCanvas.node_colors.brown,
|
||||
"IMAGE": LGraphCanvas.node_colors.pale_blue,
|
||||
"CLIP": LGraphCanvas.node_colors.yellow,
|
||||
"FLOAT": LGraphCanvas.node_colors.green,
|
||||
"MASK": { color: "#1c5715", bgcolor: "#1f401b"},
|
||||
"INT": { color: "#1b4669", bgcolor: "#29699c"},
|
||||
"CONTROL_NET": { color: "#156653", bgcolor: "#1c453b"},
|
||||
"NOISE": { color: "#2e2e2e", bgcolor: "#242121"},
|
||||
"GUIDER": { color: "#3c7878", bgcolor: "#1c453b"},
|
||||
"SAMPLER": { color: "#614a4a", bgcolor: "#3b2c2c"},
|
||||
"SIGMAS": { color: "#485248", bgcolor: "#272e27"},
|
||||
|
||||
};
|
||||
console.log("Setting color for type:", colorMap[type]);
|
||||
const colors = colorMap[type];
|
||||
if (colors) {
|
||||
this.color = colors.color;
|
||||
this.bgcolor = colors.bgcolor;
|
||||
}
|
||||
else{
|
||||
// Default color
|
||||
this.color = LGraphCanvas.node_colors.gray;
|
||||
this.bgcolor = LGraphCanvas.node_colors.gray;
|
||||
}
|
||||
}
|
||||
let disablePrefix = app.ui.settings.getSettingValue("KJNodes.disablePrefix")
|
||||
const LGraphNode = LiteGraph.LGraphNode
|
||||
|
||||
function showAlert(message) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: 'warn',
|
||||
summary: "KJ Get/Set",
|
||||
detail: `${message}. Most likely you're missing custom nodes`,
|
||||
life: 5000,
|
||||
})
|
||||
}
|
||||
app.registerExtension({
|
||||
name: "SetNode",
|
||||
registerCustomNodes() {
|
||||
class SetNode extends LGraphNode {
|
||||
defaultVisibility = true;
|
||||
serialize_widgets = true;
|
||||
drawConnection = false;
|
||||
currentGetters = null;
|
||||
slotColor = "#FFF";
|
||||
canvas = app.canvas;
|
||||
menuEntry = "Show connections";
|
||||
|
||||
constructor(title) {
|
||||
super(title)
|
||||
if (!this.properties) {
|
||||
this.properties = {
|
||||
"previousName": ""
|
||||
};
|
||||
}
|
||||
this.properties.showOutputText = SetNode.defaultVisibility;
|
||||
|
||||
const node = this;
|
||||
|
||||
this.addWidget(
|
||||
"text",
|
||||
"Constant",
|
||||
'',
|
||||
(s, t, u, v, x) => {
|
||||
node.validateName(node.graph);
|
||||
if(this.widgets[0].value !== ''){
|
||||
this.title = (!disablePrefix ? "Set_" : "") + this.widgets[0].value;
|
||||
}
|
||||
this.update();
|
||||
this.properties.previousName = this.widgets[0].value;
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
this.addInput("*", "*");
|
||||
this.addOutput("*", '*');
|
||||
|
||||
this.onConnectionsChange = function(
|
||||
slotType, //1 = input, 2 = output
|
||||
slot,
|
||||
isChangeConnect,
|
||||
link_info,
|
||||
output
|
||||
) {
|
||||
//On Disconnect
|
||||
if (slotType == 1 && !isChangeConnect) {
|
||||
if(this.inputs[slot].name === ''){
|
||||
this.inputs[slot].type = '*';
|
||||
this.inputs[slot].name = '*';
|
||||
this.title = "Set"
|
||||
}
|
||||
}
|
||||
if (slotType == 2 && !isChangeConnect) {
|
||||
if (this.outputs && this.outputs[slot]) {
|
||||
this.outputs[slot].type = '*';
|
||||
this.outputs[slot].name = '*';
|
||||
}
|
||||
}
|
||||
//On Connect
|
||||
if (link_info && node.graph && slotType == 1 && isChangeConnect) {
|
||||
const resolve = link_info.resolve(node.graph)
|
||||
const type = (resolve?.subgraphInput ?? resolve?.output)?.type
|
||||
if (type) {
|
||||
if (this.title === "Set"){
|
||||
this.title = (!disablePrefix ? "Set_" : "") + type;
|
||||
}
|
||||
if (this.widgets[0].value === '*'){
|
||||
this.widgets[0].value = type
|
||||
}
|
||||
|
||||
this.validateName(node.graph);
|
||||
this.inputs[0].type = type;
|
||||
this.inputs[0].name = type;
|
||||
|
||||
if (app.ui.settings.getSettingValue("KJNodes.nodeAutoColor")){
|
||||
setColorAndBgColor.call(this, type);
|
||||
}
|
||||
} else {
|
||||
showAlert("node input undefined.")
|
||||
}
|
||||
}
|
||||
if (link_info && node.graph && slotType == 2 && isChangeConnect) {
|
||||
const fromNode = node.graph._nodes.find((otherNode) => otherNode.id == link_info.origin_id);
|
||||
|
||||
if (fromNode && fromNode.inputs && fromNode.inputs[link_info.origin_slot]) {
|
||||
const type = fromNode.inputs[link_info.origin_slot].type;
|
||||
|
||||
this.outputs[0].type = type;
|
||||
this.outputs[0].name = type;
|
||||
} else {
|
||||
showAlert('node output undefined');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Update either way
|
||||
this.update();
|
||||
}
|
||||
|
||||
this.validateName = function(graph) {
|
||||
let widgetValue = node.widgets[0].value;
|
||||
|
||||
if (widgetValue !== '') {
|
||||
let tries = 0;
|
||||
const existingValues = new Set();
|
||||
|
||||
graph._nodes.forEach(otherNode => {
|
||||
if (otherNode !== this && otherNode.type === 'SetNode') {
|
||||
existingValues.add(otherNode.widgets[0].value);
|
||||
}
|
||||
});
|
||||
|
||||
while (existingValues.has(widgetValue)) {
|
||||
widgetValue = node.widgets[0].value + "_" + tries;
|
||||
tries++;
|
||||
}
|
||||
|
||||
node.widgets[0].value = widgetValue;
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
this.clone = function () {
|
||||
const cloned = SetNode.prototype.clone.apply(this);
|
||||
cloned.inputs[0].name = '*';
|
||||
cloned.inputs[0].type = '*';
|
||||
cloned.value = '';
|
||||
cloned.properties.previousName = '';
|
||||
cloned.size = cloned.computeSize();
|
||||
return cloned;
|
||||
};
|
||||
|
||||
this.onAdded = function(graph) {
|
||||
this.validateName(graph);
|
||||
}
|
||||
|
||||
|
||||
this.update = function() {
|
||||
if (!node.graph) {
|
||||
return;
|
||||
}
|
||||
|
||||
const getters = this.findGetters(node.graph);
|
||||
getters.forEach(getter => {
|
||||
getter.setType(this.inputs[0].type);
|
||||
});
|
||||
|
||||
if (this.widgets[0].value) {
|
||||
const gettersWithPreviousName = this.findGetters(node.graph, true);
|
||||
gettersWithPreviousName.forEach(getter => {
|
||||
getter.setName(this.widgets[0].value);
|
||||
});
|
||||
}
|
||||
|
||||
const allGetters = node.graph._nodes.filter(otherNode => otherNode.type === "GetNode");
|
||||
allGetters.forEach(otherNode => {
|
||||
if (otherNode.setComboValues) {
|
||||
otherNode.setComboValues();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
this.findGetters = function(graph, checkForPreviousName) {
|
||||
const name = checkForPreviousName ? this.properties.previousName : this.widgets[0].value;
|
||||
return graph._nodes.filter(otherNode => otherNode.type === 'GetNode' && otherNode.widgets[0].value === name && name !== '');
|
||||
}
|
||||
|
||||
|
||||
// This node is purely frontend and does not impact the resulting prompt so should not be serialized
|
||||
this.isVirtualNode = true;
|
||||
}
|
||||
|
||||
|
||||
onRemoved() {
|
||||
const allGetters = this.graph._nodes.filter((otherNode) => otherNode.type == "GetNode");
|
||||
allGetters.forEach((otherNode) => {
|
||||
if (otherNode.setComboValues) {
|
||||
otherNode.setComboValues([this]);
|
||||
}
|
||||
})
|
||||
}
|
||||
getExtraMenuOptions(_, options) {
|
||||
this.menuEntry = this.drawConnection ? "Hide connections" : "Show connections";
|
||||
options.unshift(
|
||||
{
|
||||
content: this.menuEntry,
|
||||
callback: () => {
|
||||
this.currentGetters = this.findGetters(this.graph);
|
||||
if (this.currentGetters.length == 0) return;
|
||||
let linkType = (this.currentGetters[0].outputs[0].type);
|
||||
this.slotColor = this.canvas.default_connection_color_byType[linkType]
|
||||
this.menuEntry = this.drawConnection ? "Hide connections" : "Show connections";
|
||||
this.drawConnection = !this.drawConnection;
|
||||
this.canvas.setDirty(true, true);
|
||||
|
||||
},
|
||||
has_submenu: true,
|
||||
submenu: {
|
||||
title: "Color",
|
||||
options: [
|
||||
{
|
||||
content: "Highlight",
|
||||
callback: () => {
|
||||
this.slotColor = "orange"
|
||||
this.canvas.setDirty(true, true);
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "Hide all connections",
|
||||
callback: () => {
|
||||
const allGetters = this.graph._nodes.filter(otherNode => otherNode.type === "GetNode" || otherNode.type === "SetNode");
|
||||
allGetters.forEach(otherNode => {
|
||||
otherNode.drawConnection = false;
|
||||
console.log(otherNode);
|
||||
});
|
||||
|
||||
this.menuEntry = "Show connections";
|
||||
this.drawConnection = false
|
||||
this.canvas.setDirty(true, true);
|
||||
|
||||
},
|
||||
|
||||
},
|
||||
);
|
||||
// Dynamically add a submenu for all getters
|
||||
this.currentGetters = this.findGetters(this.graph);
|
||||
if (this.currentGetters) {
|
||||
|
||||
let gettersSubmenu = this.currentGetters.map(getter => ({
|
||||
|
||||
content: `${getter.title} id: ${getter.id}`,
|
||||
callback: () => {
|
||||
this.canvas.centerOnNode(getter);
|
||||
this.canvas.selectNode(getter, false);
|
||||
this.canvas.setDirty(true, true);
|
||||
|
||||
},
|
||||
}));
|
||||
|
||||
options.unshift({
|
||||
content: "Getters",
|
||||
has_submenu: true,
|
||||
submenu: {
|
||||
title: "GetNodes",
|
||||
options: gettersSubmenu,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onDrawForeground(ctx, lGraphCanvas) {
|
||||
if (this.drawConnection) {
|
||||
this._drawVirtualLinks(lGraphCanvas, ctx);
|
||||
}
|
||||
}
|
||||
// onDrawCollapsed(ctx, lGraphCanvas) {
|
||||
// if (this.drawConnection) {
|
||||
// this._drawVirtualLinks(lGraphCanvas, ctx);
|
||||
// }
|
||||
// }
|
||||
_drawVirtualLinks(lGraphCanvas, ctx) {
|
||||
if (!this.currentGetters?.length) return;
|
||||
var title = this.getTitle ? this.getTitle() : this.title;
|
||||
var title_width = ctx.measureText(title).width;
|
||||
if (!this.flags.collapsed) {
|
||||
var start_node_slotpos = [
|
||||
this.size[0],
|
||||
LiteGraph.NODE_TITLE_HEIGHT * 0.5,
|
||||
];
|
||||
}
|
||||
else {
|
||||
|
||||
var start_node_slotpos = [
|
||||
title_width + 55,
|
||||
-15,
|
||||
|
||||
];
|
||||
}
|
||||
// Provide a default link object with necessary properties, to avoid errors as link can't be null anymore
|
||||
const defaultLink = { type: 'default', color: this.slotColor };
|
||||
|
||||
for (const getter of this.currentGetters) {
|
||||
if (!this.flags.collapsed) {
|
||||
var end_node_slotpos = this.getConnectionPos(false, 0);
|
||||
end_node_slotpos = [
|
||||
getter.pos[0] - end_node_slotpos[0] + this.size[0],
|
||||
getter.pos[1] - end_node_slotpos[1]
|
||||
];
|
||||
}
|
||||
else {
|
||||
var end_node_slotpos = this.getConnectionPos(false, 0);
|
||||
end_node_slotpos = [
|
||||
getter.pos[0] - end_node_slotpos[0] + title_width + 50,
|
||||
getter.pos[1] - end_node_slotpos[1] - 30
|
||||
];
|
||||
}
|
||||
lGraphCanvas.renderLink(
|
||||
ctx,
|
||||
start_node_slotpos,
|
||||
end_node_slotpos,
|
||||
defaultLink,
|
||||
false,
|
||||
null,
|
||||
this.slotColor,
|
||||
LiteGraph.RIGHT,
|
||||
LiteGraph.LEFT
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType(
|
||||
"SetNode",
|
||||
Object.assign(SetNode, {
|
||||
title: "Set",
|
||||
})
|
||||
);
|
||||
|
||||
SetNode.category = "KJNodes";
|
||||
},
|
||||
});
|
||||
|
||||
app.registerExtension({
|
||||
name: "GetNode",
|
||||
registerCustomNodes() {
|
||||
class GetNode extends LGraphNode {
|
||||
|
||||
defaultVisibility = true;
|
||||
serialize_widgets = true;
|
||||
drawConnection = false;
|
||||
slotColor = "#FFF";
|
||||
currentSetter = null;
|
||||
canvas = app.canvas;
|
||||
|
||||
constructor(title) {
|
||||
super(title)
|
||||
if (!this.properties) {
|
||||
this.properties = {};
|
||||
}
|
||||
this.properties.showOutputText = GetNode.defaultVisibility;
|
||||
const node = this;
|
||||
this.addWidget(
|
||||
"combo",
|
||||
"Constant",
|
||||
"",
|
||||
(e) => {
|
||||
this.onRename();
|
||||
},
|
||||
{
|
||||
values: () => {
|
||||
const setterNodes = node.graph._nodes.filter((otherNode) => otherNode.type == 'SetNode');
|
||||
return setterNodes.map((otherNode) => otherNode.widgets[0].value).sort();
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
this.addOutput("*", '*');
|
||||
this.onConnectionsChange = function(
|
||||
slotType, //0 = output, 1 = input
|
||||
slot, //self-explanatory
|
||||
isChangeConnect,
|
||||
link_info,
|
||||
output
|
||||
) {
|
||||
this.validateLinks();
|
||||
}
|
||||
|
||||
this.setName = function(name) {
|
||||
node.widgets[0].value = name;
|
||||
node.onRename();
|
||||
node.serialize();
|
||||
}
|
||||
|
||||
this.onRename = function() {
|
||||
const setter = this.findSetter(node.graph);
|
||||
if (setter) {
|
||||
let linkType = (setter.inputs[0].type);
|
||||
|
||||
this.setType(linkType);
|
||||
this.title = (!disablePrefix ? "Get_" : "") + setter.widgets[0].value;
|
||||
|
||||
if (app.ui.settings.getSettingValue("KJNodes.nodeAutoColor")){
|
||||
setColorAndBgColor.call(this, linkType);
|
||||
}
|
||||
|
||||
} else {
|
||||
this.setType('*');
|
||||
}
|
||||
}
|
||||
|
||||
this.clone = function () {
|
||||
const cloned = GetNode.prototype.clone.apply(this);
|
||||
cloned.size = cloned.computeSize();
|
||||
return cloned;
|
||||
};
|
||||
|
||||
this.validateLinks = function() {
|
||||
if (this.outputs[0].type !== '*' && this.outputs[0].links) {
|
||||
this.outputs[0].links.filter(linkId => {
|
||||
const link = node.graph.links[linkId];
|
||||
return link && (!link.type.split(",").includes(this.outputs[0].type) && link.type !== '*');
|
||||
}).forEach(linkId => {
|
||||
node.graph.removeLink(linkId);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.setType = function(type) {
|
||||
this.outputs[0].name = type;
|
||||
this.outputs[0].type = type;
|
||||
this.validateLinks();
|
||||
}
|
||||
|
||||
this.findSetter = function(graph) {
|
||||
const name = this.widgets[0].value;
|
||||
const foundNode = graph._nodes.find(otherNode => otherNode.type === 'SetNode' && otherNode.widgets[0].value === name && name !== '');
|
||||
return foundNode;
|
||||
};
|
||||
|
||||
this.goToSetter = function() {
|
||||
this.canvas.centerOnNode(this.currentSetter);
|
||||
this.canvas.selectNode(this.currentSetter, false);
|
||||
};
|
||||
|
||||
// This node is purely frontend and does not impact the resulting prompt so should not be serialized
|
||||
this.isVirtualNode = true;
|
||||
}
|
||||
|
||||
getInputLink(slot) {
|
||||
const setter = this.findSetter(this.graph);
|
||||
|
||||
if (setter) {
|
||||
const slotInfo = setter.inputs[slot];
|
||||
const link = this.graph.links[slotInfo.link];
|
||||
return link;
|
||||
} else {
|
||||
const errorMessage = "No SetNode found for " + this.widgets[0].value + "(" + this.type + ")";
|
||||
showAlert(errorMessage);
|
||||
//throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
onAdded(graph) {
|
||||
}
|
||||
getExtraMenuOptions(_, options) {
|
||||
let menuEntry = this.drawConnection ? "Hide connections" : "Show connections";
|
||||
this.currentSetter = this.findSetter(this.graph)
|
||||
if (!this.currentSetter) return
|
||||
options.unshift(
|
||||
{
|
||||
content: "Go to setter",
|
||||
callback: () => {
|
||||
this.goToSetter();
|
||||
},
|
||||
},
|
||||
{
|
||||
content: menuEntry,
|
||||
callback: () => {
|
||||
let linkType = (this.currentSetter.inputs[0].type);
|
||||
this.drawConnection = !this.drawConnection;
|
||||
this.slotColor = this.canvas.default_connection_color_byType[linkType]
|
||||
this.canvas.setDirty(true, true);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
onDrawForeground(ctx, lGraphCanvas) {
|
||||
if (this.drawConnection) {
|
||||
this._drawVirtualLink(lGraphCanvas, ctx);
|
||||
}
|
||||
}
|
||||
// onDrawCollapsed(ctx, lGraphCanvas) {
|
||||
// if (this.drawConnection) {
|
||||
// this._drawVirtualLink(lGraphCanvas, ctx);
|
||||
// }
|
||||
// }
|
||||
_drawVirtualLink(lGraphCanvas, ctx) {
|
||||
if (!this.currentSetter) return;
|
||||
|
||||
// Provide a default link object with necessary properties, to avoid errors as link can't be null anymore
|
||||
const defaultLink = { type: 'default', color: this.slotColor };
|
||||
|
||||
let start_node_slotpos = this.currentSetter.getConnectionPos(false, 0);
|
||||
start_node_slotpos = [
|
||||
start_node_slotpos[0] - this.pos[0],
|
||||
start_node_slotpos[1] - this.pos[1],
|
||||
];
|
||||
let end_node_slotpos = [0, -LiteGraph.NODE_TITLE_HEIGHT * 0.5];
|
||||
lGraphCanvas.renderLink(
|
||||
ctx,
|
||||
start_node_slotpos,
|
||||
end_node_slotpos,
|
||||
defaultLink,
|
||||
false,
|
||||
null,
|
||||
this.slotColor
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType(
|
||||
"GetNode",
|
||||
Object.assign(GetNode, {
|
||||
title: "Get",
|
||||
})
|
||||
);
|
||||
|
||||
GetNode.category = "KJNodes";
|
||||
},
|
||||
});
|
||||
1386
custom_nodes/ComfyUI-KJNodes/web/js/spline_editor.js
Normal file
1386
custom_nodes/ComfyUI-KJNodes/web/js/spline_editor.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user