Merge branch 'main' into show-guide-lines
This commit is contained in:
@@ -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 }}
|
||||
@@ -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
|
||||

|
||||

|
||||
|
||||
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
|
||||

|
||||
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
@@ -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
@@ -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
@@ -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 = {
|
||||
|
||||
@@ -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 🐍",
|
||||
}
|
||||
@@ -132,6 +132,8 @@ class LoadText(TextFileNode):
|
||||
|
||||
|
||||
class SaveText(TextFileNode):
|
||||
OUTPUT_NODE = True
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(self, **kwargs):
|
||||
return float("nan")
|
||||
|
||||
@@ -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 = ""
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user