2023-07-14 04:46:25 +00:00
import { api } from "./api.js"
2023-11-21 06:33:58 +00:00
import "./domWidget.js" ;
2023-07-14 04:46:25 +00:00
2024-01-13 18:57:47 +00:00
let controlValueRunBefore = false ;
2024-01-13 18:57:59 +00:00
export function updateControlWidgetLabel ( widget ) {
2024-01-13 18:57:47 +00:00
let replacement = "after" ;
let find = "before" ;
if ( controlValueRunBefore ) {
[ find , replacement ] = [ replacement , find ]
}
widget . label = ( widget . label ? ? widget . name ) . replace ( find , replacement ) ;
}
const IS _CONTROL _WIDGET = Symbol ( ) ;
const HAS _EXECUTED = Symbol ( ) ;
2023-09-19 03:33:19 +00:00
function getNumberDefaults ( inputData , defaultStep , precision , enable _rounding ) {
2023-03-02 20:00:06 +00:00
let defaultVal = inputData [ 1 ] [ "default" ] ;
2023-09-17 16:49:06 +00:00
let { min , max , step , round } = inputData [ 1 ] ;
2023-03-02 20:00:06 +00:00
if ( defaultVal == undefined ) defaultVal = 0 ;
if ( min == undefined ) min = 0 ;
if ( max == undefined ) max = 2048 ;
if ( step == undefined ) step = defaultStep ;
2023-09-17 16:49:06 +00:00
// precision is the number of decimal places to show.
// by default, display the the smallest number of decimal places such that changes of size step are visible.
2023-09-19 03:33:19 +00:00
if ( precision == undefined ) {
precision = Math . max ( - Math . floor ( Math . log10 ( step ) ) , 0 ) ;
2023-09-19 03:20:00 +00:00
}
2023-09-17 16:49:06 +00:00
2023-09-19 03:33:19 +00:00
if ( enable _rounding && ( round == undefined || round === true ) ) {
2023-09-17 16:49:06 +00:00
// by default, round the value to those decimal places shown.
round = Math . round ( 1000000 * Math . pow ( 0.1 , precision ) ) / 1000000 ;
}
2023-09-09 05:21:38 +00:00
return { val : defaultVal , config : { min , max , step : 10.0 * step , round , precision } } ;
2023-03-02 20:00:06 +00:00
}
2023-11-30 19:13:27 +00:00
export function addValueControlWidget ( node , targetWidget , defaultValue = "randomize" , values , widgetName , inputData ) {
let name = inputData [ 1 ] ? . control _after _generate ;
if ( typeof name !== "string" ) {
name = widgetName ;
}
const widgets = addValueControlWidgets ( node , targetWidget , defaultValue , {
2023-11-22 17:52:20 +00:00
addFilterList : false ,
2023-11-30 19:13:27 +00:00
controlAfterGenerateName : name
} , inputData ) ;
2023-11-22 17:52:20 +00:00
return widgets [ 0 ] ;
}
2023-11-30 19:13:27 +00:00
export function addValueControlWidgets ( node , targetWidget , defaultValue = "randomize" , options , inputData ) {
if ( ! defaultValue ) defaultValue = "randomize" ;
2023-11-22 17:52:20 +00:00
if ( ! options ) options = { } ;
2023-11-30 19:13:27 +00:00
const getName = ( defaultName , optionName ) => {
let name = defaultName ;
if ( options [ optionName ] ) {
name = options [ optionName ] ;
} else if ( typeof inputData ? . [ 1 ] ? . [ defaultName ] === "string" ) {
name = inputData ? . [ 1 ] ? . [ defaultName ] ;
} else if ( inputData ? . [ 1 ] ? . control _prefix ) {
name = inputData ? . [ 1 ] ? . control _prefix + " " + name
}
return name ;
}
2023-11-22 17:52:20 +00:00
const widgets = [ ] ;
2023-11-30 19:13:27 +00:00
const valueControl = node . addWidget (
"combo" ,
getName ( "control_after_generate" , "controlAfterGenerateName" ) ,
defaultValue ,
function ( ) { } ,
{
values : [ "fixed" , "increment" , "decrement" , "randomize" ] ,
serialize : false , // Don't include this in prompt.
}
) ;
2024-08-14 05:22:10 +00:00
valueControl . tooltip = "Allows the linked widget to be changed automatically, for example randomizing the noise seed." ;
2024-01-13 18:57:47 +00:00
valueControl [ IS _CONTROL _WIDGET ] = true ;
updateControlWidgetLabel ( valueControl ) ;
2023-11-22 17:52:20 +00:00
widgets . push ( valueControl ) ;
2023-04-13 00:57:13 +00:00
2023-11-22 17:52:20 +00:00
const isCombo = targetWidget . type === "combo" ;
let comboFilter ;
2024-02-01 00:14:50 +00:00
if ( isCombo ) {
valueControl . options . values . push ( "increment-wrap" ) ;
}
2023-11-22 17:52:20 +00:00
if ( isCombo && options . addFilterList !== false ) {
2023-11-30 19:13:27 +00:00
comboFilter = node . addWidget (
"string" ,
getName ( "control_filter_list" , "controlFilterListName" ) ,
"" ,
function ( ) { } ,
{
serialize : false , // Don't include this in prompt.
}
) ;
2024-01-13 18:57:47 +00:00
updateControlWidgetLabel ( comboFilter ) ;
2024-08-14 05:22:10 +00:00
comboFilter . tooltip = "Allows for filtering the list of values when changing the value via the control generate mode. Allows for RegEx matches in the format /abc/ to only filter to values containing 'abc'."
2024-01-13 18:57:47 +00:00
2023-11-22 17:52:20 +00:00
widgets . push ( comboFilter ) ;
}
2023-04-13 00:57:13 +00:00
2024-01-13 18:57:47 +00:00
const applyWidgetControl = ( ) => {
2023-04-13 00:57:13 +00:00
var v = valueControl . value ;
2023-11-22 17:52:20 +00:00
if ( isCombo && v !== "fixed" ) {
let values = targetWidget . options . values ;
const filter = comboFilter ? . value ;
if ( filter ) {
let check ;
if ( filter . startsWith ( "/" ) && filter . endsWith ( "/" ) ) {
try {
const regex = new RegExp ( filter . substring ( 1 , filter . length - 1 ) ) ;
check = ( item ) => regex . test ( item ) ;
} catch ( error ) {
console . error ( "Error constructing RegExp filter for node " + node . id , filter , error ) ;
}
}
if ( ! check ) {
const lower = filter . toLocaleLowerCase ( ) ;
check = ( item ) => item . toLocaleLowerCase ( ) . includes ( lower ) ;
}
values = values . filter ( item => check ( item ) ) ;
if ( ! values . length && targetWidget . options . values . length ) {
console . warn ( "Filter for node " + node . id + " has filtered out all items" , filter ) ;
}
}
let current _index = values . indexOf ( targetWidget . value ) ;
let current _length = values . length ;
2023-05-16 07:18:11 +00:00
switch ( v ) {
case "increment" :
current _index += 1 ;
break ;
2024-02-01 00:14:50 +00:00
case "increment-wrap" :
current _index += 1 ;
if ( current _index >= current _length ) {
current _index = 0 ;
}
break ;
2023-05-16 07:18:11 +00:00
case "decrement" :
current _index -= 1 ;
break ;
case "randomize" :
current _index = Math . floor ( Math . random ( ) * current _length ) ;
default :
break ;
}
current _index = Math . max ( 0 , current _index ) ;
current _index = Math . min ( current _length - 1 , current _index ) ;
if ( current _index >= 0 ) {
2023-11-22 17:52:20 +00:00
let value = values [ current _index ] ;
2023-05-16 07:18:11 +00:00
targetWidget . value = value ;
targetWidget . callback ( value ) ;
}
2023-11-30 19:13:27 +00:00
} else {
//number
2023-05-16 07:18:11 +00:00
let min = targetWidget . options . min ;
let max = targetWidget . options . max ;
// limit to something that javascript can handle
max = Math . min ( 1125899906842624 , max ) ;
min = Math . max ( - 1125899906842624 , min ) ;
let range = ( max - min ) / ( targetWidget . options . step / 10 ) ;
2023-04-13 00:57:13 +00:00
2023-05-16 07:18:11 +00:00
//adjust values based on valueControl Behaviour
switch ( v ) {
case "fixed" :
break ;
case "increment" :
targetWidget . value += targetWidget . options . step / 10 ;
break ;
case "decrement" :
targetWidget . value -= targetWidget . options . step / 10 ;
break ;
case "randomize" :
targetWidget . value = Math . floor ( Math . random ( ) * range ) * ( targetWidget . options . step / 10 ) + min ;
default :
break ;
}
2023-11-30 19:13:27 +00:00
/ * c h e c k i f v a l u e s a r e o v e r o r u n d e r t h e i r r e s p e c t i v e
* ranges and set them to min or max . * /
if ( targetWidget . value < min ) targetWidget . value = min ;
2023-05-16 07:18:11 +00:00
if ( targetWidget . value > max )
targetWidget . value = max ;
2023-10-23 03:38:18 +00:00
targetWidget . callback ( targetWidget . value ) ;
2023-05-16 07:18:11 +00:00
}
2023-11-30 19:13:27 +00:00
} ;
2024-01-13 18:57:47 +00:00
valueControl . beforeQueued = ( ) => {
if ( controlValueRunBefore ) {
// Don't run on first execution
if ( valueControl [ HAS _EXECUTED ] ) {
applyWidgetControl ( ) ;
}
}
valueControl [ HAS _EXECUTED ] = true ;
} ;
valueControl . afterQueued = ( ) => {
if ( ! controlValueRunBefore ) {
applyWidgetControl ( ) ;
}
} ;
2023-11-22 17:52:20 +00:00
return widgets ;
2023-04-13 00:57:13 +00:00
} ;
2023-03-23 21:37:19 +00:00
2023-11-30 19:13:27 +00:00
function seedWidget ( node , inputName , inputData , app , widgetName ) {
const seed = createIntWidget ( node , inputName , inputData , app , true ) ;
const seedControl = addValueControlWidget ( node , seed . widget , "randomize" , undefined , widgetName , inputData ) ;
2023-03-02 20:00:06 +00:00
2023-04-13 00:57:13 +00:00
seed . widget . linkedWidgets = [ seedControl ] ;
return seed ;
2023-03-02 20:00:06 +00:00
}
2023-11-30 19:13:27 +00:00
function createIntWidget ( node , inputName , inputData , app , isSeedInput ) {
const control = inputData [ 1 ] ? . control _after _generate ;
if ( ! isSeedInput && control ) {
return seedWidget ( node , inputName , inputData , app , typeof control === "string" ? control : undefined ) ;
2023-03-22 18:43:43 +00:00
}
2023-11-30 19:13:27 +00:00
let widgetType = isSlider ( inputData [ 1 ] [ "display" ] , app ) ;
const { val , config } = getNumberDefaults ( inputData , 1 , 0 , true ) ;
Object . assign ( config , { precision : 0 } ) ;
return {
widget : node . addWidget (
widgetType ,
inputName ,
val ,
function ( v ) {
const s = this . options . step / 10 ;
2024-05-03 09:49:21 +00:00
let sh = this . options . min % s ;
if ( isNaN ( sh ) ) {
sh = 0 ;
}
this . value = Math . round ( ( v - sh ) / s ) * s + sh ;
2023-11-30 19:13:27 +00:00
} ,
config
) ,
} ;
}
2023-03-21 21:34:00 +00:00
function addMultilineWidget ( node , name , opts , app ) {
2023-11-21 06:33:58 +00:00
const inputEl = document . createElement ( "textarea" ) ;
inputEl . className = "comfy-multiline-input" ;
inputEl . value = opts . defaultVal ;
2023-11-30 19:13:27 +00:00
inputEl . placeholder = opts . placeholder || name ;
2023-11-21 06:33:58 +00:00
const widget = node . addDOMWidget ( name , "customtext" , inputEl , {
getValue ( ) {
return inputEl . value ;
2023-03-02 20:00:06 +00:00
} ,
2023-11-21 06:33:58 +00:00
setValue ( v ) {
inputEl . value = v ;
2023-03-02 20:00:06 +00:00
} ,
} ) ;
2023-11-21 06:33:58 +00:00
widget . inputEl = inputEl ;
2023-03-02 20:00:06 +00:00
2023-11-30 19:13:27 +00:00
inputEl . addEventListener ( "input" , ( ) => {
widget . callback ? . ( widget . value ) ;
} ) ;
2023-03-21 21:34:00 +00:00
2023-03-02 20:00:06 +00:00
return { minWidth : 400 , minHeight : 200 , widget } ;
}
2023-08-01 22:50:06 +00:00
function isSlider ( display , app ) {
if ( app . ui . settings . getSettingValue ( "Comfy.DisableSliders" ) ) {
return "number"
}
2023-08-01 02:38:11 +00:00
return ( display === "slider" ) ? "slider" : "number"
2023-07-31 06:48:44 +00:00
}
2024-01-13 18:57:47 +00:00
export function initWidgets ( app ) {
app . ui . settings . addSetting ( {
id : "Comfy.WidgetControlMode" ,
name : "Widget Value Control Mode" ,
type : "combo" ,
defaultValue : "after" ,
options : [ "before" , "after" ] ,
tooltip : "Controls when widget values are updated (randomize/increment/decrement), either before the prompt is queued or after." ,
onChange ( value ) {
controlValueRunBefore = value === "before" ;
for ( const n of app . graph . _nodes ) {
if ( ! n . widgets ) continue ;
for ( const w of n . widgets ) {
if ( w [ IS _CONTROL _WIDGET ] ) {
updateControlWidgetLabel ( w ) ;
if ( w . linkedWidgets ) {
for ( const l of w . linkedWidgets ) {
updateControlWidgetLabel ( l ) ;
}
}
}
}
}
app . graph . setDirtyCanvas ( true ) ;
} ,
} ) ;
}
2023-03-02 20:00:06 +00:00
export const ComfyWidgets = {
"INT:seed" : seedWidget ,
"INT:noise_seed" : seedWidget ,
2023-08-01 22:50:06 +00:00
FLOAT ( node , inputName , inputData , app ) {
let widgetType = isSlider ( inputData [ 1 ] [ "display" ] , app ) ;
2023-09-19 03:33:19 +00:00
let precision = app . ui . settings . getSettingValue ( "Comfy.FloatRoundingPrecision" ) ;
let disable _rounding = app . ui . settings . getSettingValue ( "Comfy.DisableFloatRounding" )
if ( precision == 0 ) precision = undefined ;
const { val , config } = getNumberDefaults ( inputData , 0.5 , precision , ! disable _rounding ) ;
2024-02-01 00:14:50 +00:00
return { widget : node . addWidget ( widgetType , inputName , val ,
2023-09-08 10:37:55 +00:00
function ( v ) {
2023-09-17 16:49:06 +00:00
if ( config . round ) {
2024-04-17 21:34:02 +00:00
this . value = Math . round ( ( v + Number . EPSILON ) / config . round ) * config . round ;
if ( this . value > config . max ) this . value = config . max ;
if ( this . value < config . min ) this . value = config . min ;
2023-09-17 16:49:06 +00:00
} else {
this . value = v ;
}
2023-09-08 10:37:55 +00:00
} , config ) } ;
2023-03-02 20:00:06 +00:00
} ,
2023-08-01 22:50:06 +00:00
INT ( node , inputName , inputData , app ) {
2023-11-30 19:13:27 +00:00
return createIntWidget ( node , inputName , inputData , app ) ;
2023-07-29 12:48:00 +00:00
} ,
2023-08-01 07:08:35 +00:00
BOOLEAN ( node , inputName , inputData ) {
2023-11-13 05:42:34 +00:00
let defaultVal = false ;
let options = { } ;
if ( inputData [ 1 ] ) {
if ( inputData [ 1 ] . default )
defaultVal = inputData [ 1 ] . default ;
if ( inputData [ 1 ] . label _on )
options [ "on" ] = inputData [ 1 ] . label _on ;
if ( inputData [ 1 ] . label _off )
options [ "off" ] = inputData [ 1 ] . label _off ;
}
2023-07-29 12:48:00 +00:00
return {
widget : node . addWidget (
"toggle" ,
inputName ,
defaultVal ,
( ) => { } ,
2023-11-13 05:42:34 +00:00
options ,
2023-07-29 12:48:00 +00:00
)
} ;
} ,
2023-03-02 20:00:06 +00:00
STRING ( node , inputName , inputData , app ) {
const defaultVal = inputData [ 1 ] . default || "" ;
const multiline = ! ! inputData [ 1 ] . multiline ;
2023-08-27 16:34:24 +00:00
let res ;
2023-03-02 20:00:06 +00:00
if ( multiline ) {
2023-08-27 16:34:24 +00:00
res = addMultilineWidget ( node , inputName , { defaultVal , ... inputData [ 1 ] } , app ) ;
2023-03-02 20:00:06 +00:00
} else {
2023-08-27 16:34:24 +00:00
res = { widget : node . addWidget ( "text" , inputName , defaultVal , ( ) => { } , { } ) } ;
2023-03-02 20:00:06 +00:00
}
2023-08-27 16:34:24 +00:00
if ( inputData [ 1 ] . dynamicPrompts != undefined )
res . widget . dynamicPrompts = inputData [ 1 ] . dynamicPrompts ;
return res ;
2023-03-02 20:00:06 +00:00
} ,
2023-03-23 21:37:19 +00:00
COMBO ( node , inputName , inputData ) {
const type = inputData [ 0 ] ;
let defaultValue = type [ 0 ] ;
if ( inputData [ 1 ] && inputData [ 1 ] . default ) {
defaultValue = inputData [ 1 ] . default ;
}
2023-11-30 19:13:27 +00:00
const res = { widget : node . addWidget ( "combo" , inputName , defaultValue , ( ) => { } , { values : type } ) } ;
if ( inputData [ 1 ] ? . control _after _generate ) {
res . widget . linkedWidgets = addValueControlWidgets ( node , res . widget , undefined , undefined , inputData ) ;
}
return res ;
2023-03-23 21:37:19 +00:00
} ,
2023-03-08 22:07:44 +00:00
IMAGEUPLOAD ( node , inputName , inputData , app ) {
2023-11-30 19:13:27 +00:00
const imageWidget = node . widgets . find ( ( w ) => w . name === ( inputData [ 1 ] ? . widget ? ? "image" ) ) ;
2023-03-08 22:07:44 +00:00
let uploadWidget ;
function showImage ( name ) {
const img = new Image ( ) ;
img . onload = ( ) => {
node . imgs = [ img ] ;
app . graph . setDirtyCanvas ( true ) ;
} ;
2023-05-08 18:13:06 +00:00
let folder _separator = name . lastIndexOf ( "/" ) ;
let subfolder = "" ;
if ( folder _separator > - 1 ) {
subfolder = name . substring ( 0 , folder _separator ) ;
name = name . substring ( folder _separator + 1 ) ;
}
2023-11-11 18:56:14 +00:00
img . src = api . apiURL ( ` /view?filename= ${ encodeURIComponent ( name ) } &type=input&subfolder= ${ subfolder } ${ app . getPreviewFormatParam ( ) } ${ app . getRandParam ( ) } ` ) ;
2023-05-04 18:42:07 +00:00
node . setSizeForImage ? . ( ) ;
2023-03-08 22:07:44 +00:00
}
2023-05-08 18:13:06 +00:00
var default _value = imageWidget . value ;
Object . defineProperty ( imageWidget , "value" , {
set : function ( value ) {
this . _real _value = value ;
} ,
get : function ( ) {
let value = "" ;
if ( this . _real _value ) {
value = this . _real _value ;
} else {
return default _value ;
}
if ( value . filename ) {
let real _value = value ;
value = "" ;
if ( real _value . subfolder ) {
value = real _value . subfolder + "/" ;
}
value += real _value . filename ;
if ( real _value . type && real _value . type !== "input" )
value += ` [ ${ real _value . type } ] ` ;
}
return value ;
}
} ) ;
2023-03-08 22:07:44 +00:00
// Add our own callback to the combo widget to render an image when it changes
const cb = node . callback ;
imageWidget . callback = function ( ) {
showImage ( imageWidget . value ) ;
if ( cb ) {
return cb . apply ( this , arguments ) ;
}
} ;
// On load if we have a value then render the image
// The value isnt set immediately so we need to wait a moment
// No change callbacks seem to be fired on initial setting of the value
requestAnimationFrame ( ( ) => {
if ( imageWidget . value ) {
showImage ( imageWidget . value ) ;
}
} ) ;
2023-09-03 15:51:50 +00:00
async function uploadFile ( file , updateNode , pasted = false ) {
2023-03-14 21:13:29 +00:00
try {
// Wrap file in formdata so it includes filename
const body = new FormData ( ) ;
body . append ( "image" , file ) ;
2023-09-03 15:51:50 +00:00
if ( pasted ) body . append ( "subfolder" , "pasted" ) ;
2023-07-14 04:46:25 +00:00
const resp = await api . fetchApi ( "/upload/image" , {
2023-03-14 21:13:29 +00:00
method : "POST" ,
body ,
} ) ;
if ( resp . status === 200 ) {
const data = await resp . json ( ) ;
2023-09-03 15:51:50 +00:00
// Add the file to the dropdown list and update the widget value
let path = data . name ;
if ( data . subfolder ) path = data . subfolder + "/" + path ;
if ( ! imageWidget . options . values . includes ( path ) ) {
imageWidget . options . values . push ( path ) ;
2023-03-14 21:13:29 +00:00
}
if ( updateNode ) {
2023-09-03 15:51:50 +00:00
showImage ( path ) ;
imageWidget . value = path ;
2023-03-14 21:13:29 +00:00
}
} else {
alert ( resp . status + " - " + resp . statusText ) ;
}
} catch ( error ) {
alert ( error ) ;
}
}
2023-03-08 22:07:44 +00:00
const fileInput = document . createElement ( "input" ) ;
Object . assign ( fileInput , {
type : "file" ,
2023-04-07 02:22:59 +00:00
accept : "image/jpeg,image/png,image/webp" ,
2023-03-08 22:07:44 +00:00
style : "display: none" ,
onchange : async ( ) => {
if ( fileInput . files . length ) {
2023-03-14 21:13:29 +00:00
await uploadFile ( fileInput . files [ 0 ] , true ) ;
2023-03-08 22:07:44 +00:00
}
} ,
} ) ;
document . body . append ( fileInput ) ;
// Create the button widget for selecting the files
2023-11-30 19:13:27 +00:00
uploadWidget = node . addWidget ( "button" , inputName , "image" , ( ) => {
2023-03-08 22:07:44 +00:00
fileInput . click ( ) ;
} ) ;
2023-11-30 19:13:27 +00:00
uploadWidget . label = "choose file to upload" ;
2023-03-08 22:07:44 +00:00
uploadWidget . serialize = false ;
2023-03-14 21:13:29 +00:00
// Add handler to check if an image is being dragged over our node
node . onDragOver = function ( e ) {
if ( e . dataTransfer && e . dataTransfer . items ) {
2023-08-09 15:31:27 +00:00
const image = [ ... e . dataTransfer . items ] . find ( ( f ) => f . kind === "file" ) ;
2023-03-14 21:13:29 +00:00
return ! ! image ;
}
return false ;
} ;
// On drop upload files
node . onDragDrop = function ( e ) {
console . log ( "onDragDrop called" ) ;
let handled = false ;
for ( const file of e . dataTransfer . files ) {
if ( file . type . startsWith ( "image/" ) ) {
uploadFile ( file , ! handled ) ; // Dont await these, any order is fine, only update on first one
handled = true ;
}
}
return handled ;
} ;
2023-09-03 15:51:50 +00:00
node . pasteFile = function ( file ) {
if ( file . type . startsWith ( "image/" ) ) {
const is _pasted = ( file . name === "image.png" ) &&
( file . lastModified - Date . now ( ) < 2000 ) ;
uploadFile ( file , true , is _pasted ) ;
return true ;
}
return false ;
}
2023-03-08 22:07:44 +00:00
return { widget : uploadWidget } ;
} ,
2023-03-02 20:00:06 +00:00
} ;