From 9c3cf86381fcc0fa3783f6b9098d668afc535b3e Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Thu, 26 Sep 2024 00:47:41 -0500 Subject: [PATCH 1/3] POC for an annotated number widget --- src/extensions/core/annotatedNumber.ts | 241 +++++++++++++++++++++++++ src/extensions/core/index.ts | 1 + 2 files changed, 242 insertions(+) create mode 100644 src/extensions/core/annotatedNumber.ts diff --git a/src/extensions/core/annotatedNumber.ts b/src/extensions/core/annotatedNumber.ts new file mode 100644 index 000000000..0a4c6be5f --- /dev/null +++ b/src/extensions/core/annotatedNumber.ts @@ -0,0 +1,241 @@ +import { LiteGraph, LGraphCanvas } from '@comfyorg/litegraph' +import { app } from '../../scripts/app.js' +import { LGraphNode } from '@comfyorg/litegraph' +//import { ComfyWidgets } from '../../scripts/widgets' + +function inner_value_change(widget, value) { + widget.value = value + if ( + widget.options && + widget.options.property && + node.properties[widget.options.property] !== undefined + ) { + node.setProperty(widget.options.property, value) + } +} + +function button_action(widget) { + if ( + widget.options?.reset == undefined && + widget.options?.disable == undefined + ) { + return 'None' + } + if ( + widget.options.reset != undefined && + widget.value != widget.options.reset + ) { + return 'Reset' + } + if ( + widget.options.disable != undefined && + widget.value != widget.options.disable + ) { + return 'Disable' + } + if (widget.options.reset) { + return 'No Reset' + } + return 'No Disable' +} + +function draw(ctx, node, widget_width, y, H) { + const show_text = app.canvas.ds.scale > 0.5 + const margin = 15 + ctx.textAlign = 'left' + ctx.strokeStyle = LiteGraph.WIDGET_OUTLINE_COLOR + ctx.fillStyle = LiteGraph.WIDGET_BGCOLOR + ctx.beginPath() + if (show_text) + ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5]) + else ctx.rect(margin, y, widget_width - margin * 2, H) + ctx.fill() + if (show_text) { + if (!this.disabled) ctx.stroke() + const button = button_action(this) + const padding = button == 'None' ? 0 : 20 + if (button != 'None') { + ctx.save() + ctx.font = ctx.font.split(' ')[0] + ' monospace' + if (button.startsWith('No ')) { + ctx.fillStyle = LiteGraph.WIDGET_OUTLINE_COLOR + } else { + ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR + } + if (button.endsWith('Reset')) { + ctx.fillText('\u21ba', margin + 6, y + H * 0.7) + } else { + ctx.fillText('\u2298', margin + 6, y + H * 0.7) + } + ctx.restore() + } + ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR + if (!this.disabled) { + ctx.beginPath() + ctx.moveTo(margin + 16 + padding, y + 5) + ctx.lineTo(margin + 6 + padding, y + H * 0.5) + ctx.lineTo(margin + 16 + padding, y + H - 5) + ctx.fill() + ctx.beginPath() + ctx.moveTo(widget_width - margin - 16, y + 5) + ctx.lineTo(widget_width - margin - 6, y + H * 0.5) + ctx.lineTo(widget_width - margin - 16, y + H - 5) + ctx.fill() + } + ctx.fillStyle = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR + ctx.fillText(this.label || this.name, margin * 2 + 5 + padding, y + H * 0.7) + ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR + ctx.textAlign = 'right' + const text = Number(this.value).toFixed( + this.options.precision !== undefined ? this.options.precision : 3 + ) + ctx.fillText(text, widget_width - margin * 2 - 20, y + H * 0.7) + if (this.options.mappedValues && this.value in this.options.mappedValues) { + //TODO: measure this text + ctx.fillStyle = LiteGraph.WIDGET_OUTLINE_COLOR + const value_width = ctx.measureText(text).width + ctx.fillText( + this.options.mappedValues[this.value], + widget_width - margin * 2 - 25 - value_width, + y + H * 0.7 + ) + } + } +} +function mouse(event, [x, y], node) { + const button = button_action(this) + const padding = button == 'None' ? 0 : 20 + const widget_width = this.width || node.size[0] + const old_value = this.value + const delta = x < 40 + padding ? -1 : x > widget_width - 40 ? 1 : 0 + const margin = 15 + var allow_scroll = true + if (delta) { + if (x > -3 && x < widget_width + 3) { + allow_scroll = false + } + } + if (allow_scroll && event.type == LiteGraph.pointerevents_method + 'move') { + if (event.deltaX) + this.value += event.deltaX * 0.1 * (this.options.step || 1) + if (this.options.min != null && this.value < this.options.min) { + this.value = this.options.min + } + if (this.options.max != null && this.value > this.options.max) { + this.value = this.options.max + } + } else if (event.type == LiteGraph.pointerevents_method + 'down') { + if (x < padding + margin) { + if (button == 'Reset') { + this.value = this.options.reset + } else if (button == 'Disable') { + this.value = this.options.disable + } + } else { + this.value += delta * 0.1 * (this.options.step || 1) + if (this.options.min != null && this.value < this.options.min) { + this.value = this.options.min + } + if (this.options.max != null && this.value > this.options.max) { + this.value = this.options.max + } + } + } //end mousedown + else if (event.type == LiteGraph.pointerevents_method + 'up') { + if (event.click_time < 200 && delta == 0) { + app.canvas.prompt( + 'Value', + this.value, + function (v) { + //NOTE: Original code uses eval here. This will not be reproduced + this.value = Number(v) + if (this.callback) { + this.callback(this.value, app.canvas, node, [x, y], event) + } + }.bind(this), + event + ) + } + } + + if (old_value != this.value) + setTimeout( + function () { + if (this.callback) { + this.callback(this.value, app.canvas, node, [x, y], event) + } + }.bind(this), + 20 + ) + this.dirty_canvas = true +} + +app.registerExtension({ + name: 'Comfy.MappedNumber', + async getCustomWidgets(app) { + return { + MAPPEDNUMBER(node, inputName, inputData) { + let w = { + name: inputName, + type: 'MAPPEDNUMBER', + value: 0, + draw: draw, + mouse: mouse, + computeSize: undefined, //TODO: calculate minimum width + options: {} + } + if (inputData.length > 1) { + w.options = inputData[1] + for (let k of ['default', 'min', 'max']) { + if (inputData[1][k] != undefined) { + w.value = inputData[1][k] + break + } + } + } + if (!node.widgets) { + node.widgets = [] + } + node.widgets.push(w) + return w + } + } + }, + registerCustomNodes() { + class TestNum extends LGraphNode { + static category = 'utils' + + color = LGraphCanvas.node_colors.yellow.color + bgcolor = LGraphCanvas.node_colors.yellow.bgcolor + groupcolor = LGraphCanvas.node_colors.yellow.groupcolor + isVirtualNode = true + collapsable = true + title_mode = LiteGraph.NORMAL_TITLE + title = 'testNum' + + constructor(title?: string) { + super(title) + app.widgets.MAPPEDNUMBER( + // Should we extends LGraphNode? Yesss + this, + 'x', + [ + 'MAPPEDNUMBER', + { + default: 5, + reset: 5, + disable: 0, + mappedValues: { 6: 'def+1', 5: 'default', 0: 'disabled' }, + step: 10 + } + ], + app + ) + } + } + + // Load default visibility + + LiteGraph.registerNodeType('TestNum', TestNum) + } +}) diff --git a/src/extensions/core/index.ts b/src/extensions/core/index.ts index 772dd5f3d..0be54d904 100644 --- a/src/extensions/core/index.ts +++ b/src/extensions/core/index.ts @@ -22,3 +22,4 @@ import './webcamCapture' import './widgetInputs' import './uploadAudio' import './nodeBadge' +import './annotatedNumber' From 330633355ac0c44a16de0477d73b07db048bbdff Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Thu, 26 Sep 2024 03:16:40 -0500 Subject: [PATCH 2/3] Type correctness, widget conversion Type checking is satisfied aside from the forced widget type Widgets defined in python code as INT or FLOAT are now converted if the options includes relevant keys. This grants functionality similar to the multiline option on strings Colors now correctly follow themes --- src/extensions/core/annotatedNumber.ts | 131 ++++++++++++++++--------- 1 file changed, 86 insertions(+), 45 deletions(-) diff --git a/src/extensions/core/annotatedNumber.ts b/src/extensions/core/annotatedNumber.ts index 0a4c6be5f..cb7960eb8 100644 --- a/src/extensions/core/annotatedNumber.ts +++ b/src/extensions/core/annotatedNumber.ts @@ -1,9 +1,11 @@ import { LiteGraph, LGraphCanvas } from '@comfyorg/litegraph' import { app } from '../../scripts/app.js' +import { getColorPalette } from './colorPalette' +import { ComfyWidgets } from '../../scripts/widgets' import { LGraphNode } from '@comfyorg/litegraph' -//import { ComfyWidgets } from '../../scripts/widgets' +import type { IWidget, widgetTypes } from '@comfyorg/litegraph' -function inner_value_change(widget, value) { +function inner_value_change(widget, value, node, pos) { widget.value = value if ( widget.options && @@ -12,6 +14,9 @@ function inner_value_change(widget, value) { ) { node.setProperty(widget.options.property, value) } + if (widget.callback) { + widget.callback(this.value, app.canvas, node, event) + } } function button_action(widget) { @@ -40,11 +45,12 @@ function button_action(widget) { } function draw(ctx, node, widget_width, y, H) { + const litegraph_base = getColorPalette().colors.litegraph_base const show_text = app.canvas.ds.scale > 0.5 const margin = 15 ctx.textAlign = 'left' - ctx.strokeStyle = LiteGraph.WIDGET_OUTLINE_COLOR - ctx.fillStyle = LiteGraph.WIDGET_BGCOLOR + ctx.strokeStyle = litegraph_base.WIDGET_OUTLINE_COLOR + ctx.fillStyle = litegraph_base.WIDGET_BGCOLOR ctx.beginPath() if (show_text) ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5]) @@ -58,9 +64,9 @@ function draw(ctx, node, widget_width, y, H) { ctx.save() ctx.font = ctx.font.split(' ')[0] + ' monospace' if (button.startsWith('No ')) { - ctx.fillStyle = LiteGraph.WIDGET_OUTLINE_COLOR + ctx.fillStyle = litegraph_base.WIDGET_OUTLINE_COLOR } else { - ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR + ctx.fillStyle = litegraph_base.WIDGET_TEXT_COLOR } if (button.endsWith('Reset')) { ctx.fillText('\u21ba', margin + 6, y + H * 0.7) @@ -69,7 +75,7 @@ function draw(ctx, node, widget_width, y, H) { } ctx.restore() } - ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR + ctx.fillStyle = litegraph_base.WIDGET_TEXT_COLOR if (!this.disabled) { ctx.beginPath() ctx.moveTo(margin + 16 + padding, y + 5) @@ -82,9 +88,9 @@ function draw(ctx, node, widget_width, y, H) { ctx.lineTo(widget_width - margin - 16, y + H - 5) ctx.fill() } - ctx.fillStyle = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR + ctx.fillStyle = litegraph_base.WIDGET_SECONDARY_TEXT_COLOR ctx.fillText(this.label || this.name, margin * 2 + 5 + padding, y + H * 0.7) - ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR + ctx.fillStyle = litegraph_base.WIDGET_TEXT_COLOR ctx.textAlign = 'right' const text = Number(this.value).toFixed( this.options.precision !== undefined ? this.options.precision : 3 @@ -92,7 +98,7 @@ function draw(ctx, node, widget_width, y, H) { ctx.fillText(text, widget_width - margin * 2 - 20, y + H * 0.7) if (this.options.mappedValues && this.value in this.options.mappedValues) { //TODO: measure this text - ctx.fillStyle = LiteGraph.WIDGET_OUTLINE_COLOR + ctx.fillStyle = litegraph_base.WIDGET_OUTLINE_COLOR const value_width = ctx.measureText(text).width ctx.fillText( this.options.mappedValues[this.value], @@ -115,7 +121,7 @@ function mouse(event, [x, y], node) { allow_scroll = false } } - if (allow_scroll && event.type == LiteGraph.pointerevents_method + 'move') { + if (allow_scroll && event.type == 'pointermove') { if (event.deltaX) this.value += event.deltaX * 0.1 * (this.options.step || 1) if (this.options.min != null && this.value < this.options.min) { @@ -124,7 +130,7 @@ function mouse(event, [x, y], node) { if (this.options.max != null && this.value > this.options.max) { this.value = this.options.max } - } else if (event.type == LiteGraph.pointerevents_method + 'down') { + } else if (event.type == 'pointerdown') { if (x < padding + margin) { if (button == 'Reset') { this.value = this.options.reset @@ -141,7 +147,7 @@ function mouse(event, [x, y], node) { } } } //end mousedown - else if (event.type == LiteGraph.pointerevents_method + 'up') { + else if (event.type == 'pointerup') { if (event.click_time < 200 && delta == 0) { app.canvas.prompt( 'Value', @@ -149,9 +155,7 @@ function mouse(event, [x, y], node) { function (v) { //NOTE: Original code uses eval here. This will not be reproduced this.value = Number(v) - if (this.callback) { - this.callback(this.value, app.canvas, node, [x, y], event) - } + inner_value_change(this, this.value, node, [x, y]) }.bind(this), event ) @@ -161,44 +165,81 @@ function mouse(event, [x, y], node) { if (old_value != this.value) setTimeout( function () { - if (this.callback) { - this.callback(this.value, app.canvas, node, [x, y], event) - } + inner_value_change(this, this.value, node, [x, y]) }.bind(this), 20 ) - this.dirty_canvas = true + return true +} +function mappednumber(node, inputName, inputData, app): { widget: IWidget } { + // @ts-expect-error We are defining a new type + const type: widgetType = 'MAPPEDNUMBER' + let w = { + name: inputName, + type: type, + value: 0, + draw: draw, + mouse: mouse, + computeSize: undefined, //TODO: calculate minimum width + options: {}, + linkedWidgets: [] + } + if (inputData.length > 1) { + w.options = inputData[1] + for (let k of ['default', 'min', 'max']) { + if (inputData[1][k] != undefined) { + w.value = inputData[1][k] + break + } + } + } + if (!node.widgets) { + node.widgets = [] + } + node.widgets.push(w) + return { widget: w } +} +const originalFLOAT = ComfyWidgets.FLOAT +ComfyWidgets.FLOAT = function ( + node, + inputName, + inputData, + app +): { widget: IWidget } { + if ( + inputData[1]?.reset == undefined && + inputData[1]?.disable == undefined && + inputData[1]?.mappedValues == undefined + ) { + return originalFLOAT(node, inputName, inputData, app) + } + if (inputData[1]['display'] === 'slider') { + return originalFLOAT(node, inputName, inputData, app) + } + return mappednumber(node, inputName, inputData, app) +} +const originalINT = ComfyWidgets.INT +ComfyWidgets.INT = function ( + node, + inputName, + inputData, + app +): { widget: IWidget } { + if ( + inputData[1]?.reset || + inputData[1]?.disable || + inputData[1]?.mappedValues + ) { + return mappednumber(node, inputName, inputData, app) + } + return originalINT(node, inputName, inputData, app) } app.registerExtension({ name: 'Comfy.MappedNumber', async getCustomWidgets(app) { return { - MAPPEDNUMBER(node, inputName, inputData) { - let w = { - name: inputName, - type: 'MAPPEDNUMBER', - value: 0, - draw: draw, - mouse: mouse, - computeSize: undefined, //TODO: calculate minimum width - options: {} - } - if (inputData.length > 1) { - w.options = inputData[1] - for (let k of ['default', 'min', 'max']) { - if (inputData[1][k] != undefined) { - w.value = inputData[1][k] - break - } - } - } - if (!node.widgets) { - node.widgets = [] - } - node.widgets.push(w) - return w - } + MAPPEDNUMBER: mappednumber } }, registerCustomNodes() { From 72535eb88d673cb4c85dd0f039233069386a23dd Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Thu, 26 Sep 2024 15:17:06 -0500 Subject: [PATCH 3/3] Type cleanup, annotation as function, fix import Naming now uniformly refers to the widget as an Annotated Number and the widget is now implemented as a proper typescript class annotatedNumber widgets can optionally have an annotation function set. This allows for extensions to set annotations on ranges of values. Fixed the app import incorrectly having an extension --- src/extensions/core/annotatedNumber.ts | 87 +++++++++++++++----------- 1 file changed, 52 insertions(+), 35 deletions(-) diff --git a/src/extensions/core/annotatedNumber.ts b/src/extensions/core/annotatedNumber.ts index cb7960eb8..5bc035d75 100644 --- a/src/extensions/core/annotatedNumber.ts +++ b/src/extensions/core/annotatedNumber.ts @@ -1,5 +1,5 @@ import { LiteGraph, LGraphCanvas } from '@comfyorg/litegraph' -import { app } from '../../scripts/app.js' +import { app } from '../../scripts/app' import { getColorPalette } from './colorPalette' import { ComfyWidgets } from '../../scripts/widgets' import { LGraphNode } from '@comfyorg/litegraph' @@ -96,12 +96,21 @@ function draw(ctx, node, widget_width, y, H) { this.options.precision !== undefined ? this.options.precision : 3 ) ctx.fillText(text, widget_width - margin * 2 - 20, y + H * 0.7) - if (this.options.mappedValues && this.value in this.options.mappedValues) { + let annotation = '' + if (this.annotation) { + annotation = this.annotation(this.value) + } else if ( + this.options.annotation && + this.value in this.options.annotation + ) { + annotation = this.options.annotation[this.value] + } + if (annotation) { //TODO: measure this text ctx.fillStyle = litegraph_base.WIDGET_OUTLINE_COLOR const value_width = ctx.measureText(text).width ctx.fillText( - this.options.mappedValues[this.value], + annotation, widget_width - margin * 2 - 25 - value_width, y + H * 0.7 ) @@ -171,28 +180,35 @@ function mouse(event, [x, y], node) { ) return true } -function mappednumber(node, inputName, inputData, app): { widget: IWidget } { - // @ts-expect-error We are defining a new type - const type: widgetType = 'MAPPEDNUMBER' - let w = { - name: inputName, - type: type, - value: 0, - draw: draw, - mouse: mouse, - computeSize: undefined, //TODO: calculate minimum width - options: {}, - linkedWidgets: [] +class AnnotatedNumber implements IWidget { + // @ts-expect-error We must forcibly set a type here to allow custom mouse and draw + type: widgetTypes = 'annotatedNumber' + draw = draw + mouse = mouse + options = {} + linkedWidgets = [] + name: string + value: number + annotation: (value: number) => string + computeSize(width: number): [number, number] { + return [width, 20] } - if (inputData.length > 1) { - w.options = inputData[1] - for (let k of ['default', 'min', 'max']) { - if (inputData[1][k] != undefined) { - w.value = inputData[1][k] - break + constructor(inputName, inputData) { + this.name = inputName + if (inputData.length > 1) { + this.options = inputData[1] + for (let k of ['default', 'min', 'max']) { + if (inputData[1][k] != undefined) { + this.value = inputData[1][k] + break + } } } } +} + +function annotatedNumber(node, inputName, inputData, app): { widget: IWidget } { + let w = new AnnotatedNumber(inputName, inputData) if (!node.widgets) { node.widgets = [] } @@ -209,14 +225,14 @@ ComfyWidgets.FLOAT = function ( if ( inputData[1]?.reset == undefined && inputData[1]?.disable == undefined && - inputData[1]?.mappedValues == undefined + inputData[1]?.annotation == undefined ) { return originalFLOAT(node, inputName, inputData, app) } if (inputData[1]['display'] === 'slider') { return originalFLOAT(node, inputName, inputData, app) } - return mappednumber(node, inputName, inputData, app) + return annotatedNumber(node, inputName, inputData, app) } const originalINT = ComfyWidgets.INT ComfyWidgets.INT = function ( @@ -228,27 +244,23 @@ ComfyWidgets.INT = function ( if ( inputData[1]?.reset || inputData[1]?.disable || - inputData[1]?.mappedValues + inputData[1]?.annotation ) { - return mappednumber(node, inputName, inputData, app) + return annotatedNumber(node, inputName, inputData, app) } return originalINT(node, inputName, inputData, app) } app.registerExtension({ - name: 'Comfy.MappedNumber', + name: 'Comfy.AnnotatedNumber', async getCustomWidgets(app) { return { - MAPPEDNUMBER: mappednumber + ANNOTATEDNUMBER: annotatedNumber } }, registerCustomNodes() { class TestNum extends LGraphNode { static category = 'utils' - - color = LGraphCanvas.node_colors.yellow.color - bgcolor = LGraphCanvas.node_colors.yellow.bgcolor - groupcolor = LGraphCanvas.node_colors.yellow.groupcolor isVirtualNode = true collapsable = true title_mode = LiteGraph.NORMAL_TITLE @@ -256,25 +268,30 @@ app.registerExtension({ constructor(title?: string) { super(title) - app.widgets.MAPPEDNUMBER( + app.widgets.ANNOTATEDNUMBER( // Should we extends LGraphNode? Yesss this, 'x', [ - 'MAPPEDNUMBER', + 'ANNOTATEDNUMBER', { default: 5, reset: 5, disable: 0, - mappedValues: { 6: 'def+1', 5: 'default', 0: 'disabled' }, + annotation: { 6: 'def+1', 5: 'default', 0: 'disabled' }, step: 10 } ], app ) + let annotatedWidget = this.widgets[0] as AnnotatedNumber + annotatedWidget.annotation = function (value) { + return ['smol', 'medium', 'big', 'real big'][ + Math.floor(Math.log10(value)) + ] + } } } - // Load default visibility LiteGraph.registerNodeType('TestNum', TestNum)