From fac7529d1f7b6fc4b3b2e7f6022ebb23ec71169d Mon Sep 17 00:00:00 2001 From: Mel Massadian Date: Fri, 22 Mar 2024 06:35:16 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20poc=20of=20the=20doc=20widg?= =?UTF-8?q?et=20idea?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __init__.py | 13 ++- nodes/animation.py | 32 ++++++- nodes/generate.py | 7 ++ web/comfy_shared.js | 225 +++++++++++++++++++++++++++++++++++++++++++- web/mtb_widgets.js | 6 +- web/note_plus.js | 12 +-- 6 files changed, 278 insertions(+), 17 deletions(-) diff --git a/__init__.py b/__init__.py index 588c9be..9529429 100644 --- a/__init__.py +++ b/__init__.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding:utf-8 -*- ### # File: __init__.py # Project: comfy_mtb @@ -43,7 +42,7 @@ __version__ = "0.2.0" def extract_nodes_from_source(filename): source_code = "" - with open(filename, "r", encoding="utf8") as file: + with open(filename, encoding="utf8") as file: source_code = file.read() nodes = [] @@ -81,7 +80,7 @@ def load_nodes(): module = importlib.import_module( f".nodes.{module_name}", package=__package__ ) - _nodes = getattr(module, "__nodes__") + _nodes = module.__nodes__ nodes.extend(_nodes) log.debug(f"Imported {module_name} nodes") @@ -97,7 +96,7 @@ def load_nodes(): if errors: log.debug( - f"Some nodes failed to load:\n\t" + "Some nodes failed to load:\n\t" + "\n\t".join(errors) + "\n\n" + "Check that you properly installed the dependencies.\n" @@ -127,6 +126,10 @@ if web_mtb.exists() and hasattr(nodes, "EXTENSION_WEB_DIRS"): nodes, failed = load_nodes() for node_class in nodes: class_name = node_class.__name__ + # fallback to __doc__ + if not hasattr(node_class, "DESCRIPTION") and node_class.__doc__: + node_class.DESCRIPTION = node_class.__doc__ + node_label = f"{get_label(class_name)} (mtb)" NODE_CLASS_MAPPINGS[node_label] = node_class NODE_DISPLAY_NAME_MAPPINGS[class_name] = node_label @@ -147,7 +150,7 @@ for node_class in nodes: ) log.debug( - f"Loaded the following nodes:\n\t" + "Loaded the following nodes:\n\t" + "\n\t".join( f"{cyan_text(k)}: {blue_text(get_summary(doc)) if doc else '-'}" for k, doc in NODE_CLASS_MAPPINGS_DEBUG.items() diff --git a/nodes/animation.py b/nodes/animation.py index 194282e..437fd41 100644 --- a/nodes/animation.py +++ b/nodes/animation.py @@ -2,7 +2,7 @@ from ..log import log class AnimationBuilder: - """Simple maths for animation.""" + """Node built around the idea of values over a queue of frames.""" @classmethod def INPUT_TYPES(cls): @@ -21,6 +21,36 @@ class AnimationBuilder: RETURN_NAMES = ("frame", "0-1 (scaled)", "count", "loop_ended") CATEGORY = "mtb/animation" FUNCTION = "build_animation" + DESCRIPTION = """ +# Animation Builder + +Check the +[wiki page](https://github.com/melMass/comfy_mtb/wiki/nodes-animation-builder) +for more info. + + +- This basic example should help to understand the meaning of +its inputs and outputs thanks to the [debug](nodes-debug) node. + +![](https://github.com/melMass/comfy_mtb/assets/7041726/2b5c7e4f-372d-4494-9e73-abb2daa7cb36) + +- In this other example Animation Builder is used in combination with +[Batch From History](https://github.com/melMass/comfy_mtb/wiki/nodes-batch-from-history) +to create a zoom-in animation on a static image + +![](https://github.com/melMass/comfy_mtb/assets/7041726/77d37da1-0a8e-4519-a493-dfdef7f755ea) + +## Inputs + +| name | description | +| ---- | :----------:| +| total_frames | The number of frame to queue (this is multiplied by the `loop_count`)| +| scale_float | Convenience input to scale the normalized `current value` (a float between 0 and 1 lerp over the current queue length) | +| loop_count | The number of loops to queue | +| **Reset Button** | resets the internal counters, although the node is though around using its queue button it should still work fine when using the regular queue button of comfy | +| **Queue Button** | Convenience button to run the queues (`total_frames` * `loop_count`) | + +""" def build_animation( self, diff --git a/nodes/generate.py b/nodes/generate.py index ecc7734..85f24f8 100644 --- a/nodes/generate.py +++ b/nodes/generate.py @@ -200,6 +200,13 @@ class TextToImage: """ fonts = {} + DESCRIPTION = """# Text to Image + +This node look for any font files in comfy_dir/fonts. +by default it fallsback to a default font. + +![img](https://i.imgur.com/3GT92hy.gif) +""" def __init__(self): # - This is executed when the graph is executed, diff --git a/web/comfy_shared.js b/web/comfy_shared.js index 3ab6522..c611467 100644 --- a/web/comfy_shared.js +++ b/web/comfy_shared.js @@ -13,7 +13,7 @@ import { app } from '../../scripts/app.js' 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 + const r = ((dt + Math.random() * 16) % 16) | 0 dt = Math.floor(dt / 16) return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16) }) @@ -65,8 +65,8 @@ function createLogger(emoji, color, consoleMethod = 'log') { } } -export const infoLogger = createLogger('ℹ️', 'yellow') -export const warnLogger = createLogger('⚠️', 'orange', 'warn') +export const infoLogger = createLogger('i', 'yellow') +export const warnLogger = createLogger('!', 'orange', 'warn') export const errorLogger = createLogger('🔥', 'red', 'error') export const successLogger = createLogger('✅', 'green') @@ -495,8 +495,225 @@ export const addDeprecation = (nodeType, reason) => { reason: 'font-size:1.2em', } console.log( - `%c⚠️ ${title} is deprecated:%c ${reason}`, + `%c! ${title} is deprecated:%c ${reason}`, styles.title, styles.reason, ) } + +const create_documentation_stylesheet = () => { + const tag = 'mtb-documentation-stylesheet' + + let styleTag = document.head.querySelector(tag) + + if (!styleTag) { + styleTag = document.createElement('style') + styleTag.type = 'text/css' + styleTag.id = tag + + styleTag.innerHTML = ` + .documentation-popup { + background: var(--bg-color); + position: absolute; + color: var(--fg-color); + font: 12px monospace; + line-height: 1.5em; + padding: 3px; + border-radius: 4px; + pointer-events: "inherit"; + z-index: 5; + overflow:scroll; + } + + .documentation-popup img { + max-width: 100%; + } + .documentation-popup table { + border-collapse: collapse; + border: 1px var(--border-color) solid; + } + .documentation-popup th, + .documentation-popup td { + border: 1px var(--border-color) solid; + } + .documentation-popup th { + background-color: var(--comfy-input-bg); + } + ` + document.head.appendChild(styleTag) + } +} +let documentationConverter + +/** Add documentation widget to the selected node */ +export const addDocumentation = ( + nodeData, + nodeType, + opts = { icon_size: 14, icon_margin: 4 }, +) => { + if (!documentationConverter) { + documentationConverter = new showdown.Converter({ + tables: true, + strikethrough: true, + emoji: true, + ghCodeBlocks: true, + tasklists: true, + ghMentions: true, + smoothLivePreview: true, + simplifiedAutoLink: true, + parseImgDimensions: true, + openLinksInNewWindow: true, + }) + } + + opts = opts || {} + const iconSize = opts.icon_size ? opts.icon_size : 14 + const iconMargin = opts.icon_margin ? opts.icon_margin : 4 + let docElement = null + let offsetX = 0 + let offsetY = 0 + + if (!nodeData.description) { + return + } + const drawFg = nodeType.prototype.onDrawForeground + nodeType.prototype.onDrawForeground = function (ctx, canvas) { + const r = drawFg ? drawFg.apply(this, arguments) : undefined + if (this.flags.collapsed) return r + + // icon position + const x = this.size[0] - iconSize - iconMargin + // const y = iconMargin * 1.5 + + // const questionMark = new Path2D( + // 'm15.901 25.36h3.84v-3.84h-3.84v3.84zm1.92-15.36c-2.88 0-5.76 2.88-5.76 5.76h3.84c0-.96.96-1.92 1.92-1.92s1.92.96 1.92 1.92c0 1.92-3.84 1.92-3.84 3.84h3.84c1.92-.66 3.84-1.92 3.84-4.8s-2.88-4.8-5.76-4.8zm0-7.68c-8.49 0-15.36 6.87-15.36 15.36s6.87 15.36 15.36 15.36 15.36-6.87 15.36-15.36-6.87-15.36-15.36-15.36zm0 26.88c-6.36 0-11.52-5.16-11.52-11.52s5.16-11.52 11.52-11.52 11.52 5.16 11.52 11.52-5.16 11.52-11.52 11.52z', + // ) + // + // ctx.save() + + if (this.show_doc && docElement === null) { + create_documentation_stylesheet() + docElement = document.createElement('div') + docElement.classList.add('documentation-popup') + docElement.innerHTML = documentationConverter.makeHtml( + nodeData.description, + ) + // resize handle + const resizeHandle = document.createElement('div') + resizeHandle.style.width = '10px' + resizeHandle.style.height = '10px' + resizeHandle.style.background = 'gray' + resizeHandle.style.position = 'absolute' + resizeHandle.style.bottom = '0' + resizeHandle.style.right = '0' + resizeHandle.style.cursor = 'se-resize' + + // TODO: fix resize logic + docElement.appendChild(resizeHandle) + let isResizing = false + let startX, startY, startWidth, startHeight + + resizeHandle.addEventListener('mousedown', function (e) { + 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, + ) + }) + + document.addEventListener('mousemove', function (e) { + if (!isResizing) return + const newWidth = startWidth + e.clientX - startX + const newHeight = startHeight + e.clientY - startY + offsetX += newWidth - startWidth + offsetY += newHeight - startHeight + + startWidth = newWidth + startHeight = newHeight + }) + + document.addEventListener('mouseup', function () { + isResizing = false + }) + document.body.appendChild(docElement) + } else if (!this.show_doc && docElement !== null) { + docElement.parentNode.removeChild(docElement) + docElement = null + } + + 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, 0) + .translateSelf(10, -32) + const scale = new DOMMatrix().scaleSelf(transform.a, transform.d) + Object.assign(docElement.style, { + transformOrigin: '0 0', + transform: scale, + left: `${transform.a + transform.e}px`, + top: `${transform.d + transform.f}px`, + width: `${this.size[0] * 2}px`, + // height: `${(widget.parent?.inputHeight || 32) - (margin * 2)}px`, + height: `${this.size[1] || this.parent?.inputHeight || 32}px`, + + // background: !node.color ? "" : node.color, + // color: "blue", //!node.color ? "" : "white", + }) + // docElement.style.left = 140 - rect.right + "px"; + // docElement.style.top = rect.top + "px"; + } + ctx.translate(x, iconSize - 34) // Position the icon on the canvas + ctx.scale(iconSize / 32, iconSize / 32) // Scale the icon to the desired size + ctx.strokeStyle = 'rgba(255,255,255,0.3)' + + ctx.lineCap = 'round' + ctx.lineJoin = 'round' + + ctx.lineWidth = 2.4 + // ctx.stroke(questionMark); + ctx.font = '36px monospace' + ctx.fillText('?', 0, 24) + ctx.restore() + + return r + } + 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 + ) { + // Pencil icon was clicked, open the editor + // this.openEditorDialog(); + if (this.show_doc === undefined) { + this.show_doc = true + } else { + this.show_doc = !this.show_doc + } + return true // Return true to indicate the event was handled + } + + return r // Return false to let the event propagate + + // return r; + } +} diff --git a/web/mtb_widgets.js b/web/mtb_widgets.js index 2962d90..c3fcff4 100644 --- a/web/mtb_widgets.js +++ b/web/mtb_widgets.js @@ -17,7 +17,7 @@ import * as shared from './comfy_shared.js' import { log } from './comfy_shared.js' // NOTE: new widget types registered by MTB Widgets -const newTypes = [, /*'BOOL'*/ 'COLOR', 'BBOX'] +const newTypes = [/*'BOOL'*/ , 'COLOR', 'BBOX'] const deprecated_nodes = { // 'Animation Builder': @@ -572,6 +572,10 @@ const mtb_widgets = { if (!nodeData.name.endsWith('(mtb)')) { return } + // console.log('MTB Node', { description: nodeData.description, nodeType }) + + shared.addDocumentation(nodeData, nodeType) + const deprecation = deprecated_nodes[nodeData.name.replace(' (mtb)', '')] if (deprecation) { diff --git a/web/note_plus.js b/web/note_plus.js index e6064bd..2650a5d 100644 --- a/web/note_plus.js +++ b/web/note_plus.js @@ -171,10 +171,10 @@ class NotePlus extends LiteGraph.LGraphNode { // Create a new Path2D object from SVG path data const pencilPath = new Path2D( - 'M21.28 6.4l-9.54 9.54c-.95.95-3.77 1.39-4.4.76-.63-.63-.2-3.45.75-4.4l9.55-9.55a2.58 2.58 0 1 1 3.64 3.65z' + 'M21.28 6.4l-9.54 9.54c-.95.95-3.77 1.39-4.4.76-.63-.63-.2-3.45.75-4.4l9.55-9.55a2.58 2.58 0 1 1 3.64 3.65z', ) const folderPath = new Path2D( - 'M11 4H6a4 4 0 0 0-4 4v10a4 4 0 0 0 4 4h11c2.21 0 3-1.8 3-4v-5' + 'M11 4H6a4 4 0 0 0-4 4v10a4 4 0 0 0 4 4h11c2.21 0 3-1.8 3-4v-5', ) // Draw the paths @@ -222,7 +222,7 @@ class NotePlus extends LiteGraph.LGraphNode { (me) => successLogger(`Updating edit_mode`, me), { values: ['html', 'markdown', 'raw'], - } + }, ) this.css_widget = this.addWidget('text', 'CSS', DEFAULT_CSS, (val) => { @@ -409,7 +409,7 @@ class NotePlus extends LiteGraph.LGraphNode { loadAceEditor() { shared .loadScript( - 'https://cdn.jsdelivr.net/npm/ace-builds@1.16.0/src-min-noconflict/ace.min.js' + 'https://cdn.jsdelivr.net/npm/ace-builds@1.16.0/src-min-noconflict/ace.min.js', ) .catch((e) => { errorLogger(e) @@ -557,7 +557,7 @@ class NotePlus extends LiteGraph.LGraphNode { } calculateHeight() { this.calculated_height = shared.calculateTotalChildrenHeight( - this.html_widget.element + this.html_widget.element, ) this.setDirtyCanvas(true, true) } @@ -566,7 +566,7 @@ class NotePlus extends LiteGraph.LGraphNode { // this.html_widget.element.style = css const scopedCss = this.scopeCss( `${CSS_RESET}\n${css}`, - `note-plus-${this.uuid}` + `note-plus-${this.uuid}`, ) const cssDom = this.getCssDom()