Merge branch 'main' into show-guide-lines

This commit is contained in:
pythongosssss
2024-07-13 17:10:18 +01:00
committed by GitHub
16 changed files with 257 additions and 37 deletions
+21
View File
@@ -0,0 +1,21 @@
name: Publish to Comfy registry
on:
workflow_dispatch:
push:
branches:
- main
paths:
- "pyproject.toml"
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main
with:
## Add your own personal access token to your Github Repository secrets and reference it here.
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
+8
View File
@@ -1,5 +1,7 @@
# ComfyUI-Custom-Scripts
### ⚠️ While these extensions work for the most part, i'm very busy at the moment and so unable to keep on top of everything here, thanks for your patience!
# Installation
1. Clone the repository:
@@ -103,6 +105,12 @@ Adds various menu items to some nodes for quickly setting up common parts of gra
Plays a sound when the node is executed, either after each prompt or only when the queue is empty for queuing multiple prompts.
You can customize the sound by replacing the mp3 file `web/extensions/pysssss/CustomScripts/assets\notify.mp3`
## System Notification
![image](https://github.com/pythongosssss/ComfyUI-Custom-Scripts/assets/30354775/993fd783-5cd6-4779-aa97-173bc06cc405)
![image](https://github.com/pythongosssss/ComfyUI-Custom-Scripts/assets/30354775/e45227fb-5714-4f45-b96b-6601902ef6e2)
Sends a system notification via the browser when the node is executed, either after each prompt or only when the queue is empty for queuing multiple prompts.
## [WIP] Repeater
![image](https://github.com/pythongosssss/ComfyUI-Custom-Scripts/assets/125205205/ec0dac25-14e4-4d44-b975-52193656709d)
Node allows you to either create a list of N repeats of the input node, or create N outputs from the input node.
+1 -1
View File
@@ -85,7 +85,7 @@ def populate_items(names, type):
file_path_no_ext = os.path.splitext(file_path)[0]
for ext in ["png", "jpg", "jpeg", "preview.png"]:
for ext in ["png", "jpg", "jpeg", "preview.png", "preview.jpeg"]:
has_image = os.path.isfile(file_path_no_ext + "." + ext)
if has_image:
item_image = f"{file_name}.{ext}"
+50 -3
View File
@@ -12,7 +12,13 @@ operators = {
ast.Pow: op.pow,
ast.BitXor: op.xor,
ast.USub: op.neg,
ast.Mod: op.mod
ast.Mod: op.mod,
ast.BitAnd: op.and_,
ast.BitOr: op.or_,
ast.Invert: op.invert,
ast.And: lambda a, b: 1 if a and b else 0,
ast.Or: lambda a, b: 1 if a or b else 0,
ast.Not: lambda a: 0 if a else 1,
}
# TODO: restructure args to provide more info, generate hint based on args to save duplication
@@ -52,6 +58,21 @@ functions = {
"call": lambda *args: random.choice(args),
"hint": "...numbers"
},
"sqrt": {
"args": (1, 1),
"call": lambda a: math.sqrt(a),
"hint": "number"
},
"int": {
"args": (1, 1),
"call": lambda a = None: int(a),
"hint": "number"
},
"iif": {
"args": (3, 3),
"call": lambda a, b, c = None: b if a else c,
"hint": "value, truepart, falsepart"
},
}
autocompleteWords = list({
@@ -136,11 +157,20 @@ class MathExpression:
lookup = {"a": a, "b": b, "c": c}
def eval_op(node, l, r):
l = eval_expr(l)
r = eval_expr(r)
l = l if isinstance(l, int) else float(l)
r = r if isinstance(r, int) else float(r)
return operators[type(node.op)](l, r)
def eval_expr(node):
if isinstance(node, ast.Num):
if isinstance(node, ast.Constant) or isinstance(node, ast.Num):
return node.n
elif isinstance(node, ast.BinOp):
return operators[type(node.op)](float(eval_expr(node.left)), float(eval_expr(node.right)))
return eval_op(node, node.left, node.right)
elif isinstance(node, ast.BoolOp):
return eval_op(node, node.values[0], node.values[1])
elif isinstance(node, ast.UnaryOp):
return operators[type(node.op)](eval_expr(node.operand))
elif isinstance(node, ast.Attribute):
@@ -174,6 +204,23 @@ class MathExpression:
args.append(eval_expr(arg))
return fn["call"](*args)
raise NameError(f"Invalid function call: {node.func.id}")
elif isinstance(node, ast.Compare):
l = eval_expr(node.left)
r = eval_expr(node.comparators[0])
if isinstance(node.ops[0], ast.Eq):
return 1 if l == r else 0
if isinstance(node.ops[0], ast.NotEq):
return 1 if l != r else 0
if isinstance(node.ops[0], ast.Gt):
return 1 if l > r else 0
if isinstance(node.ops[0], ast.GtE):
return 1 if l >= r else 0
if isinstance(node.ops[0], ast.Lt):
return 1 if l < r else 0
if isinstance(node.ops[0], ast.LtE):
return 1 if l <= r else 0
raise NotImplementedError(
"Operator " + node.ops[0].__class__.__name__ + " not supported.")
else:
raise TypeError(node)
+7 -12
View File
@@ -7,10 +7,10 @@ class StringFunction:
"required": {
"action": (["append", "replace"], {}),
"tidy_tags": (["yes", "no"], {}),
"text_a": ("STRING", {"multiline": True, "dynamicPrompts": False}),
"text_b": ("STRING", {"multiline": True, "dynamicPrompts": False}),
},
"optional": {
"text_a": ("STRING", {"multiline": True, "dynamicPrompts": False}),
"text_b": ("STRING", {"multiline": True, "dynamicPrompts": False}),
"text_c": ("STRING", {"multiline": True, "dynamicPrompts": False})
}
}
@@ -20,15 +20,7 @@ class StringFunction:
CATEGORY = "utils"
OUTPUT_NODE = True
def exec(self, action, tidy_tags, text_a, text_b, text_c=""):
# Converted inputs are sent as the string of 'undefined' if not connected
if text_a == "undefined":
text_a = ""
if text_b == "undefined":
text_b = ""
if text_c == "undefined":
text_c = ""
def exec(self, action, tidy_tags, text_a="", text_b="", text_c=""):
tidy_tags = tidy_tags == "yes"
out = ""
if action == "append":
@@ -42,7 +34,10 @@ class StringFunction:
else:
out = text_a.replace(text_b, text_c)
if tidy_tags:
out = out.replace(" ", " ").replace(" ,", ",").replace(",,", ",").replace(",,", ",")
out = re.sub(r"\s{2,}", " ", out)
out = out.replace(" ,", ",")
out = re.sub(r",{2,}", ",", out)
out = out.strip()
return {"ui": {"text": (out,)}, "result": (out,)}
NODE_CLASS_MAPPINGS = {
+41
View File
@@ -0,0 +1,41 @@
# Hack: string type that is always equal in not equal comparisons
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False
# Our any instance wants to be a wildcard string
any = AnyType("*")
class SystemNotification:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"message": ("STRING", {"default": "Your notification has triggered."}),
"any": (any, {}),
"mode": (["always", "on empty queue"], {}),
}}
FUNCTION = "nop"
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
OUTPUT_NODE = True
RETURN_TYPES = (any,)
CATEGORY = "utils"
def IS_CHANGED(self, **kwargs):
return float("NaN")
def nop(self, any, **kwargs):
return {"ui": {"a": []}, "result": (any,)}
NODE_CLASS_MAPPINGS = {
"SystemNotification|pysssss": SystemNotification,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"SystemNotification|pysssss": "SystemNotification 🐍",
}
+2
View File
@@ -132,6 +132,8 @@ class LoadText(TextFileNode):
class SaveText(TextFileNode):
OUTPUT_NODE = True
@classmethod
def IS_CHANGED(self, **kwargs):
return float("nan")
+13
View File
@@ -0,0 +1,13 @@
[project]
name = "comfyui-custom-scripts"
description = "Enhancements & experiments for ComfyUI, mostly focusing on UI features"
version = "1.0.0"
license = "LICENSE"
[project.urls]
Repository = "https://github.com/pythongosssss/ComfyUI-Custom-Scripts"
[tool.comfy]
PublisherId = "pythongosssss"
DisplayName = "ComfyUI-Custom-Scripts"
Icon = ""
+1 -1
View File
@@ -471,7 +471,7 @@ export class TextAreaAutoComplete {
this.#hide();
break;
}
} else if (e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "ArrowLeft" || e.key === "ArrowRight") {
} else if (e.key.length > 1 && e.key != "Delete" && e.key != "Backspace") {
return;
}
if (!e.defaultPrevented) {
+7
View File
@@ -62,6 +62,13 @@ export class Lightbox {
onclick: (e) => {
e.stopImmediatePropagation();
},
onwheel: (e) => {
if (!(e instanceof WheelEvent) || e.ctrlKey) {
return;
}
const direction = Math.sign(e.deltaY);
this.update(direction);
},
});
}
+3 -2
View File
@@ -249,8 +249,9 @@ export class ModelInfoDialog extends ComfyDialog {
})
);
if (info.images?.length) {
this.img.src = info.images[0].url;
const preview = info.images.find((i) => i.type === "image");
if (preview) {
this.img.src = preview.url;
this.img.style.display = "";
this.imgSave = $el("button", {
+5
View File
@@ -37,6 +37,9 @@ $el("style", {
min-width: 200px;
max-width: calc(var(--max-size, 10) * 1vw);
}
.comfyui-body-left .pysssss-image-feed--left, .comfyui-body-right .pysssss-image-feed--right {
height: 100%;
}
.pysssss-image-feed--left {
left: 0;
}
@@ -405,6 +408,8 @@ Recommended: "enabled (max performance)" uness images are erroneously deduplicat
),
])
);
// If lightbox is open, update it with new image
lightbox.updateWithNewImage(href, feedDirection.value);
}
imageFeed.append(
+17 -1
View File
@@ -5,6 +5,7 @@ import { app } from "../../../scripts/app.js";
let replaceRegex;
const id = "pysssss.PresetText.Presets";
const MISSING = Symbol();
const getPresets = () => {
let items;
@@ -47,6 +48,19 @@ app.registerExtension({
}
},
});
const drawNodeWidgets = LGraphCanvas.prototype.drawNodeWidgets
LGraphCanvas.prototype.drawNodeWidgets = function(node) {
const c = LiteGraph.WIDGET_BGCOLOR;
try {
if(node[MISSING]) {
LiteGraph.WIDGET_BGCOLOR = "red"
}
return drawNodeWidgets.apply(this, arguments);
} finally {
LiteGraph.WIDGET_BGCOLOR = c;
}
}
},
registerCustomNodes() {
class PresetTextNode {
@@ -153,10 +167,12 @@ app.registerExtension({
if (!w) continue;
const preset = presets.find((p) => p.name === widget.value);
if (!preset) {
this[MISSING] = true;
app.graph.setDirtyCanvas(true, true);
const msg = `Preset text '${widget.value}' not found. Please fix this and queue again.`;
alert(msg);
throw new Error(msg);
}
delete this[MISSING];
w.value = preset.value;
}
}
+30 -17
View File
@@ -1,5 +1,5 @@
import { app } from "../../../scripts/app.js";
import { $el, ComfyDialog } from "../../../scripts/ui.js";
import { $el } from "../../../scripts/ui.js";
let setting, guide_setting, guide_config;
const id = "pysssss.SnapToGrid";
@@ -13,6 +13,17 @@ const guide_config_default = {
enabled: false,
fillStyle: "rgba(0, 0, 255, 0.5)",
},
/** Wraps the provided function call to set/reset shiftDown when setting is enabled. */
function wrapCallInSettingCheck(fn) {
if (setting?.value) {
const shift = app.shiftDown;
app.shiftDown = true;
const r = fn();
app.shiftDown = shift;
return r;
}
return fn();
}
const ext = {
@@ -115,14 +126,7 @@ const ext = {
// Override drawNode to draw the drop position
const drawNode = LGraphCanvas.prototype.drawNode;
LGraphCanvas.prototype.drawNode = function () {
if (setting?.value) {
const shift = app.shiftDown;
app.shiftDown = true;
const r = drawNode.apply(this, arguments);
app.shiftDown = shift;
return r;
}
return drawNode.apply(this, arguments);
wrapCallInSettingCheck(() => drawNode.apply(this, arguments));
};
// Override node added to add a resize handler to force grid alignment
@@ -131,18 +135,27 @@ const ext = {
const r = onNodeAdded?.apply(this, arguments);
const onResize = node.onResize;
node.onResize = function () {
if (setting?.value) {
const shift = app.shiftDown;
app.shiftDown = true;
const r = onResize?.apply(this, arguments);
app.shiftDown = shift;
return r;
}
return onResize?.apply(this, arguments);
wrapCallInSettingCheck(() => onResize?.apply(this, arguments));
};
return r;
};
const groupMove = LGraphGroup.prototype.move;
LGraphGroup.prototype.move = function(deltax, deltay, ignore_nodes) {
wrapCallInSettingCheck(() => groupMove.apply(this, arguments));
}
const canvasDrawGroups = LGraphCanvas.prototype.drawGroups;
LGraphCanvas.prototype.drawGroups = function (canvas, ctx) {
wrapCallInSettingCheck(() => canvasDrawGroups.apply(this, arguments));
}
const canvasOnGroupAdd = LGraphCanvas.onGroupAdd;
LGraphCanvas.onGroupAdd = function() {
wrapCallInSettingCheck(() => canvasOnGroupAdd.apply(this, arguments));
}
return configure.apply(this, arguments);
};
+49
View File
@@ -0,0 +1,49 @@
import { app } from "../../../scripts/app.js";
const notificationSetup = () => {
if (!("Notification" in window)) {
console.log("This browser does not support notifications.");
alert("This browser does not support notifications.");
return;
}
if (Notification.permission === "denied") {
console.log("Notifications are blocked. Please enable them in your browser settings.");
alert("Notifications are blocked. Please enable them in your browser settings.");
return;
}
if (Notification.permission !== "granted") {
Notification.requestPermission();
}
return true;
};
app.registerExtension({
name: "pysssss.SystemNotification",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "SystemNotification|pysssss") {
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = async function () {
onExecuted?.apply(this, arguments);
const mode = this.widgets.find((w) => w.name === "mode");
const message = this.widgets.find((w) => w.name === "message");
if (mode.value === "on empty queue") {
if (app.ui.lastQueueSize !== 0) {
await new Promise((r) => setTimeout(r, 500));
}
if (app.ui.lastQueueSize !== 0) {
return;
}
}
if (!notificationSetup()) return;
const notification = new Notification("ComfyUI", { body: message.value ?? "Your notification has triggered." });
};
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
onNodeCreated?.apply(this, arguments);
notificationSetup();
};
}
},
});
+2
View File
@@ -37,6 +37,7 @@ class WorkflowImage {
width: app.canvas.canvas.width,
height: app.canvas.canvas.height,
offset: app.canvas.ds.offset,
transform: app.canvas.canvas.getContext('2d').getTransform(), // Save the original transformation matrix
};
}
@@ -45,6 +46,7 @@ class WorkflowImage {
app.canvas.canvas.width = this.state.width;
app.canvas.canvas.height = this.state.height;
app.canvas.ds.offset = this.state.offset;
app.canvas.canvas.getContext('2d').setTransform(this.state.transform); // Reapply the original transformation matrix
}
updateView(bounds) {