commit f323a35e3e006c22e4c4da654062dfa16c2e24d0 Author: uihp Date: Fri Feb 7 02:25:44 2025 +0800 init: first available diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/README.md b/README.md new file mode 100644 index 0000000..1c40ce0 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# ComfyUI StringChain diff --git a/StringChain.py b/StringChain.py new file mode 100644 index 0000000..aff8302 --- /dev/null +++ b/StringChain.py @@ -0,0 +1,15 @@ +class StringChain: + @classmethod + def INPUT_TYPES(cls) -> dict: + return { + 'optional': { 'previous': ('STRING', {'forceInput': True, 'default': '', 'multiline': True}) }, + 'required': { + 'separator': ('STRING', {'default': ', '}), + 'string': ('STRING', {'default': '', 'multiline': True}), + } + } + RETURN_TYPES = ('STRING',) + FUNCTION = 'combine' + CATEGORY = 'utils' + def combine(self, string: str, separator: str, previous: str = '') -> tuple[str]: + return (f'{previous}{separator}{string}',) if previous and string else (previous or string,) diff --git a/StringConcat.py b/StringConcat.py new file mode 100644 index 0000000..d3b1905 --- /dev/null +++ b/StringConcat.py @@ -0,0 +1,19 @@ +class StringConcat: + @classmethod + def INPUT_TYPES(cls) -> dict: + return { + 'optional': { + 'previous': ('STRING', {'forceInput': True, 'default': '', 'multiline': True}), + 'string': ('STRING', {'default': '', 'multiline': True}) + }, + 'required': { + 'separator': ('STRING', {'default': ', '}), + 'reverse': ('BOOLEAN', {'default': False}) + } + } + RETURN_TYPES = ('STRING',) + FUNCTION = 'concat' + CATEGORY = 'utils' + def concat(self, separator: str, reverse: bool, previous: str = '', string: str = '') -> tuple[str]: + if reverse: previous, string = string, previous + return (f'{previous}{separator}{string}',) if previous and string else (previous or string,) diff --git a/StringToggle.py b/StringToggle.py new file mode 100644 index 0000000..846af01 --- /dev/null +++ b/StringToggle.py @@ -0,0 +1,27 @@ +from .common import FlexibleOptionalInputType, AnyType + +optional = FlexibleOptionalInputType(AnyType('STRING')) +optional.update({ + 'previous': ('STRING', {'forceInput': True, 'default': '', 'multiline': True}) +}) + +class StringToggle: + @classmethod + def INPUT_TYPES(cls) -> dict: + return { + 'required': { + 'separator': ('STRING', {'default': ', '}), + 'reverse': ('BOOLEAN', {'default': False}) + }, + 'optional': optional + } + RETURN_TYPES = ('STRING',) + FUNCTION = 'join' + CATEGORY = 'utils' + def join(self, separator: str, reverse: bool, previous: str = '', **kwargs) -> tuple[str]: + arguments = {} + for (argtype, unique), value in [(k.split('_'),v) for k,v in kwargs.items() if '_' in k]: + arguments.setdefault(argtype, {})[unique] = value + strings = ([previous] if previous else []) + [text for unique, text in arguments['text'].items() if arguments['append'][unique]] + if reverse: strings = strings[::-1] + return (separator.join(strings),) diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..cc45f3c --- /dev/null +++ b/__init__.py @@ -0,0 +1,14 @@ +from .StringChain import StringChain +from .StringConcat import StringConcat +from .StringToggle import StringToggle + +WEB_DIRECTORY = "./js" + +NODE_CLASS_MAPPINGS = { + 'String Chain': StringChain, + 'String Concat': StringConcat, + 'String Toggle': StringToggle, + 'String Toggle (Multiline)': StringToggle +} + +__all__ = ['NODE_CLASS_MAPPINGS', 'WEB_DIRECTORY'] diff --git a/common.py b/common.py new file mode 100644 index 0000000..3782b0d --- /dev/null +++ b/common.py @@ -0,0 +1,31 @@ +''' Credit to rgthree (https://github.com/rgthree/rgthree-comfy) below ''' + +class AnyType(str): + """A special class that is always equal in not equal comparisons. Credit to pythongosssss""" + + def __ne__(self, __value: object) -> bool: + return False + +class FlexibleOptionalInputType(dict): + """A special class to make flexible nodes that pass data to our python handlers. + + Enables both flexible/dynamic input types (like for Any Switch) or a dynamic number of inputs + (like for Any Switch, Context Switch, Context Merge, Power Lora Loader, etc). + + Note, for ComfyUI, all that's needed is the `__contains__` override below, which tells ComfyUI + that our node will handle the input, regardless of what it is. + + However, with https://github.com/comfyanonymous/ComfyUI/pull/2666 a large change would occur + requiring more details on the input itself. There, we need to return a list/tuple where the first + item is the type. This can be a real type, or use the AnyType for additional flexibility. + + This should be forwards compatible unless more changes occur in the PR. + """ + def __init__(self, type): + self.type = type + + def __getitem__(self, key): + return (self.type, ) + + def __contains__(self, key): + return True diff --git a/js/StringToggle.js b/js/StringToggle.js new file mode 100644 index 0000000..99f8609 --- /dev/null +++ b/js/StringToggle.js @@ -0,0 +1,46 @@ +import { app } from "../../scripts/app.js" +import { DividerWidget } from "./common.js" +import { OVERRIDDEN_SERVER_NODES } from './common.js' + +const upgradeNodeType = (nodeType) => class extends nodeType { + constructor() { + super(...arguments) + this.units = [] + this.nameUniqueCounter = 0 + this.addWidget('button', 'Add', null, () => this.addTextUnit()) + this.addWidget('button', 'Remove', null, () => this.removeTextUnit(this.units.pop()?.id)) + } + configure(info) { + super.configure(info) + const { widgets_values } = info + const units_values = widgets_values.slice(3) + units_values.filter((each, index) => { + if (typeof each !== 'string') return + this.addTextUnit(each, units_values[index-1] ?? true) + }) + } + addTextUnit(text='', append=true) { + const id = Symbol() + const unit = { id, enabled: true, text } + const [width,] = this.size + Object.assign(this.addCustomWidget(new DividerWidget({ marginTop: 2, marginBottom: 2, thickness: 2 })), { id }) + Object.assign(this.addWidget('toggle', `append_${this.nameUniqueCounter}`, append, () => unit.enabled = !unit.enabled), { id }) + Object.assign(this.addWidget('text', `text_${this.nameUniqueCounter++}`, text), { id }) + this.units.push(unit) + this.setSize([width, this.computeSize()[1]]) + return id + } + removeTextUnit(id) { + this.units = this.units.filter(each => each.id !== id) + this.widgets = this.widgets.filter(each => each.id !== id) + this.setSize(this.size) + } +} + +app.registerExtension({ + name: "string-master.StringToggle", + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData.name === "String Toggle") + OVERRIDDEN_SERVER_NODES.set(nodeType, upgradeNodeType(nodeType)) + } +}) diff --git a/js/StringToggleMultiline.js b/js/StringToggleMultiline.js new file mode 100644 index 0000000..6088ad5 --- /dev/null +++ b/js/StringToggleMultiline.js @@ -0,0 +1,51 @@ +import { app } from "../../scripts/app.js" +import { ComfyWidgets } from "../../scripts/widgets.js" +import { DividerWidget } from "./common.js" +import { OVERRIDDEN_SERVER_NODES } from './common.js' + +const upgradeNodeType = (nodeType) => class extends nodeType { + constructor() { + super(...arguments) + this.units = [] + this.nameUniqueCounter = 0 + this.addWidget('button', 'Add', null, () => this.addTextUnit()) + this.addWidget('button', 'Remove', null, () => this.removeTextUnit(this.units.pop()?.id)) + } + configure(info) { + super.configure(info) + const { widgets_values } = info + const units_values = widgets_values.slice(3) + units_values.filter((each, index) => { + if (typeof each !== 'string') return + this.addTextUnit(each, units_values[index-1] ?? true) + }) + } + addTextUnit(text='', append=true) { + const id = Symbol() + const unit = { id, enabled: true, text } + const [width, height] = this.size + Object.assign(this.addCustomWidget(new DividerWidget({ marginTop: 2, marginBottom: 2, thickness: 2 })), { id }) + Object.assign(this.addWidget('toggle', `append_${this.nameUniqueCounter}`, append, () => unit.enabled = !unit.enabled), { id }) + Object.assign(ComfyWidgets.STRING(this, `text_${this.nameUniqueCounter++}`, [ 'STRING', { default: unit.text, multiline: true } ], app).widget, { id, isDOMWidget: true }) + this.units.push(unit) + this.setSize([width, height]) + return id + } + removeTextUnit(id) { + this.units = this.units.filter(each => each.id !== id) + this.widgets = this.widgets.filter(each => { + if (each.id === id) { + if (each.isDOMWidget) each.element.remove() + } else return true + }) + this.setSize(this.size) + } +} + +app.registerExtension({ + name: "string-master.StringToggleMultiline", + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData.name === "String Toggle (Multiline)") + OVERRIDDEN_SERVER_NODES.set(nodeType, upgradeNodeType(nodeType)) + } +}) diff --git a/js/common.js b/js/common.js new file mode 100644 index 0000000..064d3a5 --- /dev/null +++ b/js/common.js @@ -0,0 +1,40 @@ +export const OVERRIDDEN_SERVER_NODES = new Map() + +const registerNodeType = LiteGraph.registerNodeType +LiteGraph.registerNodeType = async (nodeId, baseClass) => registerNodeType.call(LiteGraph, nodeId, OVERRIDDEN_SERVER_NODES.get(baseClass) || baseClass) + +export class DividerWidget { // Credit to rgthree + options = { serialize: false } + value = null + name = "divider" + + widgetOptions = { + marginTop: 7, + marginBottom: 7, + marginLeft: 15, + marginRight: 15, + color: LiteGraph.WIDGET_OUTLINE_COLOR, + thickness: 1 + } + + constructor(widgetOptions) { + Object.assign(this.widgetOptions, widgetOptions || {}) + } + + draw(ctx, node, width, posY, h) { + if (this.widgetOptions.thickness) { + ctx.strokeStyle = this.widgetOptions.color + const x = this.widgetOptions.marginLeft + const y = posY + this.widgetOptions.marginTop + const w = width - this.widgetOptions.marginLeft - this.widgetOptions.marginRight + ctx.stroke(new Path2D(`M ${x} ${y} h ${w}`)) + } + } + + computeSize(width) { + return [ + width, + this.widgetOptions.marginTop + this.widgetOptions.marginBottom + this.widgetOptions.thickness + ] + } +}