Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
chenpangpang
ComfyUI
Commits
c92f3dca
Unverified
Commit
c92f3dca
authored
Dec 02, 2023
by
Jairo Correa
Committed by
GitHub
Dec 02, 2023
Browse files
Merge branch 'master' into image-cache
parents
006b24cc
2995a247
Changes
57
Hide whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
2516 additions
and
556 deletions
+2516
-556
tests-ui/utils/ezgraph.js
tests-ui/utils/ezgraph.js
+34
-12
tests-ui/utils/index.js
tests-ui/utils/index.js
+49
-14
tests-ui/utils/setup.js
tests-ui/utils/setup.js
+12
-8
web/extensions/core/colorPalette.js
web/extensions/core/colorPalette.js
+207
-0
web/extensions/core/groupNode.js
web/extensions/core/groupNode.js
+1053
-0
web/extensions/core/nodeTemplates.js
web/extensions/core/nodeTemplates.js
+40
-17
web/extensions/core/undoRedo.js
web/extensions/core/undoRedo.js
+150
-0
web/extensions/core/widgetInputs.js
web/extensions/core/widgetInputs.js
+122
-109
web/lib/litegraph.core.js
web/lib/litegraph.core.js
+9
-8
web/scripts/api.js
web/scripts/api.js
+2
-2
web/scripts/app.js
web/scripts/app.js
+247
-186
web/scripts/domWidget.js
web/scripts/domWidget.js
+322
-0
web/scripts/pnginfo.js
web/scripts/pnginfo.js
+4
-2
web/scripts/ui.js
web/scripts/ui.js
+5
-5
web/scripts/ui/imagePreview.js
web/scripts/ui/imagePreview.js
+97
-0
web/scripts/widgets.js
web/scripts/widgets.js
+148
-193
web/style.css
web/style.css
+15
-0
No files found.
tests-ui/utils/ezgraph.js
View file @
c92f3dca
...
@@ -150,7 +150,7 @@ export class EzNodeMenuItem {
...
@@ -150,7 +150,7 @@ export class EzNodeMenuItem {
if (selectNode) {
if (selectNode) {
this.node.select();
this.node.select();
}
}
this.item.callback.call(this.node.node, undefined, undefined, undefined, undefined, this.node.node);
return
this.item.callback.call(this.node.node, undefined, undefined, undefined, undefined, this.node.node);
}
}
}
}
...
@@ -240,8 +240,12 @@ export class EzNode {
...
@@ -240,8 +240,12 @@ export class EzNode {
return this.#makeLookupArray(() => this.app.canvas.getNodeMenuOptions(this.node), "content", EzNodeMenuItem);
return this.#makeLookupArray(() => this.app.canvas.getNodeMenuOptions(this.node), "content", EzNodeMenuItem);
}
}
select() {
get isRemoved() {
this.app.canvas.selectNode(this.node);
return !this.app.graph.getNodeById(this.id);
}
select(addToSelection = false) {
this.app.canvas.selectNode(this.node, addToSelection);
}
}
// /**
// /**
...
@@ -275,12 +279,17 @@ export class EzNode {
...
@@ -275,12 +279,17 @@ export class EzNode {
if (!s) return p;
if (!s) return p;
const name = s[nameProperty];
const name = s[nameProperty];
const item = new ctor(this, i, s);
// @ts-ignore
// @ts-ignore
if (!name || name in p) {
p.push(item);
throw new Error(`
Unable
to
store
$
{
nodeProperty
}
$
{
name
}
on
array
as
name
conflicts
.
`);
if (name) {
// @ts-ignore
if (name in p) {
throw new Error(`
Unable
to
store
$
{
nodeProperty
}
$
{
name
}
on
array
as
name
conflicts
.
`);
}
}
}
// @ts-ignore
// @ts-ignore
p.push((
p[name] =
new ctor(this, i, s)))
;
p[name] =
item
;
return p;
return p;
}, Object.assign([], { $: this }));
}, Object.assign([], { $: this }));
}
}
...
@@ -348,6 +357,19 @@ export class EzGraph {
...
@@ -348,6 +357,19 @@ export class EzGraph {
}, 10);
}, 10);
});
});
}
}
/**
* @returns { Promise<{
* workflow: {},
* output: Record<string, {
* class_name: string,
* inputs: Record<string, [string, number] | unknown>
* }>}> }
*/
toPrompt() {
// @ts-ignore
return this.app.graphToPrompt();
}
}
}
export const Ez = {
export const Ez = {
...
@@ -356,12 +378,12 @@ export const Ez = {
...
@@ -356,12 +378,12 @@ export const Ez = {
* @example
* @example
* const { ez, graph } = Ez.graph(app);
* const { ez, graph } = Ez.graph(app);
* graph.clear();
* graph.clear();
* const [model, clip, vae] = ez.CheckpointLoaderSimple();
* const [model, clip, vae] = ez.CheckpointLoaderSimple()
.outputs
;
* const [pos] = ez.CLIPTextEncode(clip, { text: "positive" });
* const [pos] = ez.CLIPTextEncode(clip, { text: "positive" })
.outputs
;
* const [neg] = ez.CLIPTextEncode(clip, { text: "negative" });
* const [neg] = ez.CLIPTextEncode(clip, { text: "negative" })
.outputs
;
* const [latent] = ez.KSampler(model, pos, neg, ...ez.EmptyLatentImage()
)
;
* const [latent] = ez.KSampler(model, pos, neg, ...ez.EmptyLatentImage()
.outputs).outputs
;
* const [image] = ez.VAEDecode(latent, vae);
* const [image] = ez.VAEDecode(latent, vae)
.outputs
;
* const saveNode = ez.SaveImage(image)
.node
;
* const saveNode = ez.SaveImage(image);
* console.log(saveNode);
* console.log(saveNode);
* graph.arrange();
* graph.arrange();
* @param { app } app
* @param { app } app
...
...
tests-ui/utils/index.js
View file @
c92f3dca
const
{
mockApi
}
=
require
(
"
./setup
"
);
const
{
mockApi
}
=
require
(
"
./setup
"
);
const
{
Ez
}
=
require
(
"
./ezgraph
"
);
const
{
Ez
}
=
require
(
"
./ezgraph
"
);
const
lg
=
require
(
"
./litegraph
"
);
/**
/**
*
*
* @param { Parameters<mockApi>[0] } config
* @param { Parameters<mockApi>[0]
& { resetEnv?: boolean, preSetup?(app): Promise<void> }
} config
* @returns
* @returns
*/
*/
export
async
function
start
(
config
=
undefined
)
{
export
async
function
start
(
config
=
{})
{
if
(
config
.
resetEnv
)
{
jest
.
resetModules
();
jest
.
resetAllMocks
();
lg
.
setup
(
global
);
}
mockApi
(
config
);
mockApi
(
config
);
const
{
app
}
=
require
(
"
../../web/scripts/app
"
);
const
{
app
}
=
require
(
"
../../web/scripts/app
"
);
config
.
preSetup
?.(
app
);
await
app
.
setup
();
await
app
.
setup
();
return
Ez
.
graph
(
app
,
global
[
"
LiteGraph
"
],
global
[
"
LGraphCanvas
"
]);
return
{
...
Ez
.
graph
(
app
,
global
[
"
LiteGraph
"
],
global
[
"
LGraphCanvas
"
])
,
app
}
;
}
}
/**
/**
* @param { ReturnType<Ez["graph"]>["graph"] } graph
* @param { ReturnType<Ez["graph"]>["graph"] } graph
* @param { (hasReloaded: boolean) => (Promise<void> | void) } cb
* @param { (hasReloaded: boolean) => (Promise<void> | void) } cb
*/
*/
export
async
function
checkBeforeAndAfterReload
(
graph
,
cb
)
{
export
async
function
checkBeforeAndAfterReload
(
graph
,
cb
)
{
await
cb
(
false
);
await
cb
(
false
);
...
@@ -24,10 +32,10 @@ export async function checkBeforeAndAfterReload(graph, cb) {
...
@@ -24,10 +32,10 @@ export async function checkBeforeAndAfterReload(graph, cb) {
}
}
/**
/**
* @param { string } name
* @param { string } name
* @param { Record<string, string | [string | string[], any]> } input
* @param { Record<string, string | [string | string[], any]> } input
* @param { (string | string[])[] | Record<string, string | string[]> } output
* @param { (string | string[])[] | Record<string, string | string[]> } output
* @returns { Record<string, import("../../web/types/comfy").ComfyObjectInfo> }
* @returns { Record<string, import("../../web/types/comfy").ComfyObjectInfo> }
*/
*/
export
function
makeNodeDef
(
name
,
input
,
output
=
{})
{
export
function
makeNodeDef
(
name
,
input
,
output
=
{})
{
const
nodeDef
=
{
const
nodeDef
=
{
...
@@ -37,19 +45,19 @@ export function makeNodeDef(name, input, output = {}) {
...
@@ -37,19 +45,19 @@ export function makeNodeDef(name, input, output = {}) {
output_name
:
[],
output_name
:
[],
output_is_list
:
[],
output_is_list
:
[],
input
:
{
input
:
{
required
:
{}
required
:
{}
,
},
},
};
};
for
(
const
k
in
input
)
{
for
(
const
k
in
input
)
{
nodeDef
.
input
.
required
[
k
]
=
typeof
input
[
k
]
===
"
string
"
?
[
input
[
k
],
{}]
:
[...
input
[
k
]];
nodeDef
.
input
.
required
[
k
]
=
typeof
input
[
k
]
===
"
string
"
?
[
input
[
k
],
{}]
:
[...
input
[
k
]];
}
}
if
(
output
instanceof
Array
)
{
if
(
output
instanceof
Array
)
{
output
=
output
.
reduce
((
p
,
c
)
=>
{
output
=
output
.
reduce
((
p
,
c
)
=>
{
p
[
c
]
=
c
;
p
[
c
]
=
c
;
return
p
;
return
p
;
},
{})
},
{})
;
}
}
for
(
const
k
in
output
)
{
for
(
const
k
in
output
)
{
nodeDef
.
output
.
push
(
output
[
k
]);
nodeDef
.
output
.
push
(
output
[
k
]);
nodeDef
.
output_name
.
push
(
k
);
nodeDef
.
output_name
.
push
(
k
);
nodeDef
.
output_is_list
.
push
(
false
);
nodeDef
.
output_is_list
.
push
(
false
);
...
@@ -68,4 +76,31 @@ export function assertNotNullOrUndefined(x) {
...
@@ -68,4 +76,31 @@ export function assertNotNullOrUndefined(x) {
expect
(
x
).
not
.
toEqual
(
null
);
expect
(
x
).
not
.
toEqual
(
null
);
expect
(
x
).
not
.
toEqual
(
undefined
);
expect
(
x
).
not
.
toEqual
(
undefined
);
return
true
;
return
true
;
}
}
\ No newline at end of file
/**
*
* @param { ReturnType<Ez["graph"]>["ez"] } ez
* @param { ReturnType<Ez["graph"]>["graph"] } graph
*/
export
function
createDefaultWorkflow
(
ez
,
graph
)
{
graph
.
clear
();
const
ckpt
=
ez
.
CheckpointLoaderSimple
();
const
pos
=
ez
.
CLIPTextEncode
(
ckpt
.
outputs
.
CLIP
,
{
text
:
"
positive
"
});
const
neg
=
ez
.
CLIPTextEncode
(
ckpt
.
outputs
.
CLIP
,
{
text
:
"
negative
"
});
const
empty
=
ez
.
EmptyLatentImage
();
const
sampler
=
ez
.
KSampler
(
ckpt
.
outputs
.
MODEL
,
pos
.
outputs
.
CONDITIONING
,
neg
.
outputs
.
CONDITIONING
,
empty
.
outputs
.
LATENT
);
const
decode
=
ez
.
VAEDecode
(
sampler
.
outputs
.
LATENT
,
ckpt
.
outputs
.
VAE
);
const
save
=
ez
.
SaveImage
(
decode
.
outputs
.
IMAGE
);
graph
.
arrange
();
return
{
ckpt
,
pos
,
neg
,
empty
,
sampler
,
decode
,
save
};
}
tests-ui/utils/setup.js
View file @
c92f3dca
...
@@ -30,16 +30,20 @@ export function mockApi({ mockExtensions, mockNodeDefs } = {}) {
...
@@ -30,16 +30,20 @@ export function mockApi({ mockExtensions, mockNodeDefs } = {}) {
mockNodeDefs
=
JSON
.
parse
(
fs
.
readFileSync
(
path
.
resolve
(
"
./data/object_info.json
"
)));
mockNodeDefs
=
JSON
.
parse
(
fs
.
readFileSync
(
path
.
resolve
(
"
./data/object_info.json
"
)));
}
}
const
events
=
new
EventTarget
();
const
mockApi
=
{
addEventListener
:
events
.
addEventListener
.
bind
(
events
),
removeEventListener
:
events
.
removeEventListener
.
bind
(
events
),
dispatchEvent
:
events
.
dispatchEvent
.
bind
(
events
),
getSystemStats
:
jest
.
fn
(),
getExtensions
:
jest
.
fn
(()
=>
mockExtensions
),
getNodeDefs
:
jest
.
fn
(()
=>
mockNodeDefs
),
init
:
jest
.
fn
(),
apiURL
:
jest
.
fn
((
x
)
=>
"
../../web/
"
+
x
),
};
jest
.
mock
(
"
../../web/scripts/api
"
,
()
=>
({
jest
.
mock
(
"
../../web/scripts/api
"
,
()
=>
({
get
api
()
{
get
api
()
{
return
{
return
mockApi
;
addEventListener
:
jest
.
fn
(),
getSystemStats
:
jest
.
fn
(),
getExtensions
:
jest
.
fn
(()
=>
mockExtensions
),
getNodeDefs
:
jest
.
fn
(()
=>
mockNodeDefs
),
init
:
jest
.
fn
(),
apiURL
:
jest
.
fn
((
x
)
=>
"
../../web/
"
+
x
),
};
},
},
}));
}));
}
}
web/extensions/core/colorPalette.js
View file @
c92f3dca
...
@@ -174,6 +174,213 @@ const colorPalettes = {
...
@@ -174,6 +174,213 @@ const colorPalettes = {
"
tr-odd-bg-color
"
:
"
#073642
"
,
"
tr-odd-bg-color
"
:
"
#073642
"
,
}
}
},
},
},
"
arc
"
:
{
"
id
"
:
"
arc
"
,
"
name
"
:
"
Arc
"
,
"
colors
"
:
{
"
node_slot
"
:
{
"
BOOLEAN
"
:
""
,
"
CLIP
"
:
"
#eacb8b
"
,
"
CLIP_VISION
"
:
"
#A8DADC
"
,
"
CLIP_VISION_OUTPUT
"
:
"
#ad7452
"
,
"
CONDITIONING
"
:
"
#cf876f
"
,
"
CONTROL_NET
"
:
"
#00d78d
"
,
"
CONTROL_NET_WEIGHTS
"
:
""
,
"
FLOAT
"
:
""
,
"
GLIGEN
"
:
""
,
"
IMAGE
"
:
"
#80a1c0
"
,
"
IMAGEUPLOAD
"
:
""
,
"
INT
"
:
""
,
"
LATENT
"
:
"
#b38ead
"
,
"
LATENT_KEYFRAME
"
:
""
,
"
MASK
"
:
"
#a3bd8d
"
,
"
MODEL
"
:
"
#8978a7
"
,
"
SAMPLER
"
:
""
,
"
SIGMAS
"
:
""
,
"
STRING
"
:
""
,
"
STYLE_MODEL
"
:
"
#C2FFAE
"
,
"
T2I_ADAPTER_WEIGHTS
"
:
""
,
"
TAESD
"
:
"
#DCC274
"
,
"
TIMESTEP_KEYFRAME
"
:
""
,
"
UPSCALE_MODEL
"
:
""
,
"
VAE
"
:
"
#be616b
"
},
"
litegraph_base
"
:
{
"
BACKGROUND_IMAGE
"
:
"
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAACXBIWXMAAAsTAAALEwEAmpwYAAABcklEQVR4nO3YMUoDARgF4RfxBqZI6/0vZqFn0MYtrLIQMFN8U6V4LAtD+Jm9XG/v30OGl2e/AP7yevz4+vx45nvgF/+QGITEICQGITEIiUFIjNNC3q43u3/YnRJyPOzeQ+0e220nhRzReC8e7R7bbdvl+Jal1Bs46jEIiUFIDEJiEBKDkBhKPbZT6qHdptRTu02p53DUYxASg5AYhMQgJAYhMZR6bKfUQ7tNqad2m1LP4ajHICQGITEIiUFIDEJiKPXYTqmHdptST+02pZ7DUY9BSAxCYhASg5AYhMRQ6rGdUg/tNqWe2m1KPYejHoOQGITEICQGITEIiaHUYzulHtptSj2125R6Dkc9BiExCIlBSAxCYhASQ6nHdko9tNuUemq3KfUcjnoMQmIQEoOQGITEICSGUo/tlHpotyn11G5T6jkc9RiExCAkBiExCIlBSAylHtsp9dBuU+qp3abUczjqMQiJQUgMQmIQEoOQGITE+AHFISNQrFTGuwAAAABJRU5ErkJggg==
"
,
"
CLEAR_BACKGROUND_COLOR
"
:
"
#2b2f38
"
,
"
NODE_TITLE_COLOR
"
:
"
#b2b7bd
"
,
"
NODE_SELECTED_TITLE_COLOR
"
:
"
#FFF
"
,
"
NODE_TEXT_SIZE
"
:
14
,
"
NODE_TEXT_COLOR
"
:
"
#AAA
"
,
"
NODE_SUBTEXT_SIZE
"
:
12
,
"
NODE_DEFAULT_COLOR
"
:
"
#2b2f38
"
,
"
NODE_DEFAULT_BGCOLOR
"
:
"
#242730
"
,
"
NODE_DEFAULT_BOXCOLOR
"
:
"
#6e7581
"
,
"
NODE_DEFAULT_SHAPE
"
:
"
box
"
,
"
NODE_BOX_OUTLINE_COLOR
"
:
"
#FFF
"
,
"
DEFAULT_SHADOW_COLOR
"
:
"
rgba(0,0,0,0.5)
"
,
"
DEFAULT_GROUP_FONT
"
:
22
,
"
WIDGET_BGCOLOR
"
:
"
#2b2f38
"
,
"
WIDGET_OUTLINE_COLOR
"
:
"
#6e7581
"
,
"
WIDGET_TEXT_COLOR
"
:
"
#DDD
"
,
"
WIDGET_SECONDARY_TEXT_COLOR
"
:
"
#b2b7bd
"
,
"
LINK_COLOR
"
:
"
#9A9
"
,
"
EVENT_LINK_COLOR
"
:
"
#A86
"
,
"
CONNECTING_LINK_COLOR
"
:
"
#AFA
"
},
"
comfy_base
"
:
{
"
fg-color
"
:
"
#fff
"
,
"
bg-color
"
:
"
#2b2f38
"
,
"
comfy-menu-bg
"
:
"
#242730
"
,
"
comfy-input-bg
"
:
"
#2b2f38
"
,
"
input-text
"
:
"
#ddd
"
,
"
descrip-text
"
:
"
#b2b7bd
"
,
"
drag-text
"
:
"
#ccc
"
,
"
error-text
"
:
"
#ff4444
"
,
"
border-color
"
:
"
#6e7581
"
,
"
tr-even-bg-color
"
:
"
#2b2f38
"
,
"
tr-odd-bg-color
"
:
"
#242730
"
}
},
},
"
nord
"
:
{
"
id
"
:
"
nord
"
,
"
name
"
:
"
Nord
"
,
"
colors
"
:
{
"
node_slot
"
:
{
"
BOOLEAN
"
:
""
,
"
CLIP
"
:
"
#eacb8b
"
,
"
CLIP_VISION
"
:
"
#A8DADC
"
,
"
CLIP_VISION_OUTPUT
"
:
"
#ad7452
"
,
"
CONDITIONING
"
:
"
#cf876f
"
,
"
CONTROL_NET
"
:
"
#00d78d
"
,
"
CONTROL_NET_WEIGHTS
"
:
""
,
"
FLOAT
"
:
""
,
"
GLIGEN
"
:
""
,
"
IMAGE
"
:
"
#80a1c0
"
,
"
IMAGEUPLOAD
"
:
""
,
"
INT
"
:
""
,
"
LATENT
"
:
"
#b38ead
"
,
"
LATENT_KEYFRAME
"
:
""
,
"
MASK
"
:
"
#a3bd8d
"
,
"
MODEL
"
:
"
#8978a7
"
,
"
SAMPLER
"
:
""
,
"
SIGMAS
"
:
""
,
"
STRING
"
:
""
,
"
STYLE_MODEL
"
:
"
#C2FFAE
"
,
"
T2I_ADAPTER_WEIGHTS
"
:
""
,
"
TAESD
"
:
"
#DCC274
"
,
"
TIMESTEP_KEYFRAME
"
:
""
,
"
UPSCALE_MODEL
"
:
""
,
"
VAE
"
:
"
#be616b
"
},
"
litegraph_base
"
:
{
"
BACKGROUND_IMAGE
"
:
"
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFu2lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDEgNzkuMTQ2Mjg5OSwgMjAyMy8wNi8yNS0yMDowMTo1NSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjMtMTEtMTVUMDE6MjA6NDUrMDE6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMTEtMTVUMDE6MjA6NDUrMDE6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjUwNDFhMmZjLTEzNzQtMTk0ZC1hZWY4LTYxMzM1MTVmNjUwMCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoyMzFiMTBiMC1iNGZiLTAyNGUtYjEyZS0zMDUzMDNjZDA3YzgiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDoyMzFiMTBiMC1iNGZiLTAyNGUtYjEyZS0zMDUzMDNjZDA3YzgiPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjIzMWIxMGIwLWI0ZmItMDI0ZS1iMTJlLTMwNTMwM2NkMDdjOCIgc3RFdnQ6d2hlbj0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo1MDQxYTJmYy0xMzc0LTE5NGQtYWVmOC02MTMzNTE1ZjY1MDAiIHN0RXZ0OndoZW49IjIwMjMtMTEtMTVUMDE6MjA6NDUrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNS4xIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz73jWg/AAAAyUlEQVR42u3WKwoAIBRFQRdiMb1idv9Lsxn9gEFw4Dbb8JCTojbbXEJwjJVL2HKwYMGCBQuWLbDmjr+9zrBGjHl1WVcvy2DBggULFizTWQpewSt4HzwsgwULFiwFr7MUvMtS8D54WLBgGSxYCl7BK3iXZbBgwYIFC5bpLAWv4BW8Dx6WwYIFC5aC11kK3mUpeB88LFiwDBYsBa/gFbzLMliwYMGCBct0loJX8AreBw/LYMGCBUvB6ywF77IUvA8eFixYBgsWrNfWAZPltufdad+1AAAAAElFTkSuQmCC
"
,
"
CLEAR_BACKGROUND_COLOR
"
:
"
#212732
"
,
"
NODE_TITLE_COLOR
"
:
"
#999
"
,
"
NODE_SELECTED_TITLE_COLOR
"
:
"
#e5eaf0
"
,
"
NODE_TEXT_SIZE
"
:
14
,
"
NODE_TEXT_COLOR
"
:
"
#bcc2c8
"
,
"
NODE_SUBTEXT_SIZE
"
:
12
,
"
NODE_DEFAULT_COLOR
"
:
"
#2e3440
"
,
"
NODE_DEFAULT_BGCOLOR
"
:
"
#161b22
"
,
"
NODE_DEFAULT_BOXCOLOR
"
:
"
#545d70
"
,
"
NODE_DEFAULT_SHAPE
"
:
"
box
"
,
"
NODE_BOX_OUTLINE_COLOR
"
:
"
#e5eaf0
"
,
"
DEFAULT_SHADOW_COLOR
"
:
"
rgba(0,0,0,0.5)
"
,
"
DEFAULT_GROUP_FONT
"
:
24
,
"
WIDGET_BGCOLOR
"
:
"
#2e3440
"
,
"
WIDGET_OUTLINE_COLOR
"
:
"
#545d70
"
,
"
WIDGET_TEXT_COLOR
"
:
"
#bcc2c8
"
,
"
WIDGET_SECONDARY_TEXT_COLOR
"
:
"
#999
"
,
"
LINK_COLOR
"
:
"
#9A9
"
,
"
EVENT_LINK_COLOR
"
:
"
#A86
"
,
"
CONNECTING_LINK_COLOR
"
:
"
#AFA
"
},
"
comfy_base
"
:
{
"
fg-color
"
:
"
#e5eaf0
"
,
"
bg-color
"
:
"
#2e3440
"
,
"
comfy-menu-bg
"
:
"
#161b22
"
,
"
comfy-input-bg
"
:
"
#2e3440
"
,
"
input-text
"
:
"
#bcc2c8
"
,
"
descrip-text
"
:
"
#999
"
,
"
drag-text
"
:
"
#ccc
"
,
"
error-text
"
:
"
#ff4444
"
,
"
border-color
"
:
"
#545d70
"
,
"
tr-even-bg-color
"
:
"
#2e3440
"
,
"
tr-odd-bg-color
"
:
"
#161b22
"
}
},
},
"
github
"
:
{
"
id
"
:
"
github
"
,
"
name
"
:
"
Github
"
,
"
colors
"
:
{
"
node_slot
"
:
{
"
BOOLEAN
"
:
""
,
"
CLIP
"
:
"
#eacb8b
"
,
"
CLIP_VISION
"
:
"
#A8DADC
"
,
"
CLIP_VISION_OUTPUT
"
:
"
#ad7452
"
,
"
CONDITIONING
"
:
"
#cf876f
"
,
"
CONTROL_NET
"
:
"
#00d78d
"
,
"
CONTROL_NET_WEIGHTS
"
:
""
,
"
FLOAT
"
:
""
,
"
GLIGEN
"
:
""
,
"
IMAGE
"
:
"
#80a1c0
"
,
"
IMAGEUPLOAD
"
:
""
,
"
INT
"
:
""
,
"
LATENT
"
:
"
#b38ead
"
,
"
LATENT_KEYFRAME
"
:
""
,
"
MASK
"
:
"
#a3bd8d
"
,
"
MODEL
"
:
"
#8978a7
"
,
"
SAMPLER
"
:
""
,
"
SIGMAS
"
:
""
,
"
STRING
"
:
""
,
"
STYLE_MODEL
"
:
"
#C2FFAE
"
,
"
T2I_ADAPTER_WEIGHTS
"
:
""
,
"
TAESD
"
:
"
#DCC274
"
,
"
TIMESTEP_KEYFRAME
"
:
""
,
"
UPSCALE_MODEL
"
:
""
,
"
VAE
"
:
"
#be616b
"
},
"
litegraph_base
"
:
{
"
BACKGROUND_IMAGE
"
:
"
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGlmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDEgNzkuMTQ2Mjg5OSwgMjAyMy8wNi8yNS0yMDowMTo1NSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOmIyYzRhNjA5LWJmYTctYTg0MC1iOGFlLTk3MzE2ZjM1ZGIyNyIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjk0ZmNlZGU4LTE1MTctZmQ0MC04ZGU3LWYzOTgxM2E3ODk5ZiIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjIzMWIxMGIwLWI0ZmItMDI0ZS1iMTJlLTMwNTMwM2NkMDdjOCI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MjMxYjEwYjAtYjRmYi0wMjRlLWIxMmUtMzA1MzAzY2QwN2M4IiBzdEV2dDp3aGVuPSIyMDIzLTExLTEzVDAwOjE4OjAyKzAxOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjUuMSAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjQ4OWY1NzlmLTJkNjUtZWQ0Zi04OTg0LTA4NGE2MGE1ZTMzNSIgc3RFdnQ6d2hlbj0iMjAyMy0xMS0xNVQwMjowNDo1OSswMTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpiMmM0YTYwOS1iZmE3LWE4NDAtYjhhZS05NzMxNmYzNWRiMjciIHN0RXZ0OndoZW49IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNS4xIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4OTe6GAAAAx0lEQVR42u3WMQoAIQxFwRzJys77X8vSLiRgITif7bYbgrwYc/mKXyBoY4VVBgsWLFiwYFmOlTv+9jfDOjHmr8u6eVkGCxYsWLBgmc5S8ApewXvgYRksWLBgKXidpeBdloL3wMOCBctgwVLwCl7BuyyDBQsWLFiwTGcpeAWv4D3wsAwWLFiwFLzOUvAuS8F74GHBgmWwYCl4Ba/gXZbBggULFixYprMUvIJX8B54WAYLFixYCl5nKXiXpeA98LBgwTJYsGC9tg1o8f4TTtqzNQAAAABJRU5ErkJggg==
"
,
"
CLEAR_BACKGROUND_COLOR
"
:
"
#040506
"
,
"
NODE_TITLE_COLOR
"
:
"
#999
"
,
"
NODE_SELECTED_TITLE_COLOR
"
:
"
#e5eaf0
"
,
"
NODE_TEXT_SIZE
"
:
14
,
"
NODE_TEXT_COLOR
"
:
"
#bcc2c8
"
,
"
NODE_SUBTEXT_SIZE
"
:
12
,
"
NODE_DEFAULT_COLOR
"
:
"
#161b22
"
,
"
NODE_DEFAULT_BGCOLOR
"
:
"
#13171d
"
,
"
NODE_DEFAULT_BOXCOLOR
"
:
"
#30363d
"
,
"
NODE_DEFAULT_SHAPE
"
:
"
box
"
,
"
NODE_BOX_OUTLINE_COLOR
"
:
"
#e5eaf0
"
,
"
DEFAULT_SHADOW_COLOR
"
:
"
rgba(0,0,0,0.5)
"
,
"
DEFAULT_GROUP_FONT
"
:
24
,
"
WIDGET_BGCOLOR
"
:
"
#161b22
"
,
"
WIDGET_OUTLINE_COLOR
"
:
"
#30363d
"
,
"
WIDGET_TEXT_COLOR
"
:
"
#bcc2c8
"
,
"
WIDGET_SECONDARY_TEXT_COLOR
"
:
"
#999
"
,
"
LINK_COLOR
"
:
"
#9A9
"
,
"
EVENT_LINK_COLOR
"
:
"
#A86
"
,
"
CONNECTING_LINK_COLOR
"
:
"
#AFA
"
},
"
comfy_base
"
:
{
"
fg-color
"
:
"
#e5eaf0
"
,
"
bg-color
"
:
"
#161b22
"
,
"
comfy-menu-bg
"
:
"
#13171d
"
,
"
comfy-input-bg
"
:
"
#161b22
"
,
"
input-text
"
:
"
#bcc2c8
"
,
"
descrip-text
"
:
"
#999
"
,
"
drag-text
"
:
"
#ccc
"
,
"
error-text
"
:
"
#ff4444
"
,
"
border-color
"
:
"
#30363d
"
,
"
tr-even-bg-color
"
:
"
#161b22
"
,
"
tr-odd-bg-color
"
:
"
#13171d
"
}
},
}
}
};
};
...
...
web/extensions/core/groupNode.js
0 → 100644
View file @
c92f3dca
import
{
app
}
from
"
../../scripts/app.js
"
;
import
{
api
}
from
"
../../scripts/api.js
"
;
import
{
mergeIfValid
}
from
"
./widgetInputs.js
"
;
const
GROUP
=
Symbol
();
const
Workflow
=
{
InUse
:
{
Free
:
0
,
Registered
:
1
,
InWorkflow
:
2
,
},
isInUseGroupNode
(
name
)
{
const
id
=
`workflow/
${
name
}
`
;
// Check if lready registered/in use in this workflow
if
(
app
.
graph
.
extra
?.
groupNodes
?.[
name
])
{
if
(
app
.
graph
.
_nodes
.
find
((
n
)
=>
n
.
type
===
id
))
{
return
Workflow
.
InUse
.
InWorkflow
;
}
else
{
return
Workflow
.
InUse
.
Registered
;
}
}
return
Workflow
.
InUse
.
Free
;
},
storeGroupNode
(
name
,
data
)
{
let
extra
=
app
.
graph
.
extra
;
if
(
!
extra
)
app
.
graph
.
extra
=
extra
=
{};
let
groupNodes
=
extra
.
groupNodes
;
if
(
!
groupNodes
)
extra
.
groupNodes
=
groupNodes
=
{};
groupNodes
[
name
]
=
data
;
},
};
class
GroupNodeBuilder
{
constructor
(
nodes
)
{
this
.
nodes
=
nodes
;
}
build
()
{
const
name
=
this
.
getName
();
if
(
!
name
)
return
;
// Sort the nodes so they are in execution order
// this allows for widgets to be in the correct order when reconstructing
this
.
sortNodes
();
this
.
nodeData
=
this
.
getNodeData
();
Workflow
.
storeGroupNode
(
name
,
this
.
nodeData
);
return
{
name
,
nodeData
:
this
.
nodeData
};
}
getName
()
{
const
name
=
prompt
(
"
Enter group name
"
);
if
(
!
name
)
return
;
const
used
=
Workflow
.
isInUseGroupNode
(
name
);
switch
(
used
)
{
case
Workflow
.
InUse
.
InWorkflow
:
alert
(
"
An in use group node with this name already exists embedded in this workflow, please remove any instances or use a new name.
"
);
return
;
case
Workflow
.
InUse
.
Registered
:
if
(
!
confirm
(
"
An group node with this name already exists embedded in this workflow, are you sure you want to overwrite it?
"
)
)
{
return
;
}
break
;
}
return
name
;
}
sortNodes
()
{
// Gets the builders nodes in graph execution order
const
nodesInOrder
=
app
.
graph
.
computeExecutionOrder
(
false
);
this
.
nodes
=
this
.
nodes
.
map
((
node
)
=>
({
index
:
nodesInOrder
.
indexOf
(
node
),
node
}))
.
sort
((
a
,
b
)
=>
a
.
index
-
b
.
index
||
a
.
node
.
id
-
b
.
node
.
id
)
.
map
(({
node
})
=>
node
);
}
getNodeData
()
{
const
storeLinkTypes
=
(
config
)
=>
{
// Store link types for dynamically typed nodes e.g. reroutes
for
(
const
link
of
config
.
links
)
{
const
origin
=
app
.
graph
.
getNodeById
(
link
[
4
]);
const
type
=
origin
.
outputs
[
link
[
1
]].
type
;
link
.
push
(
type
);
}
};
const
storeExternalLinks
=
(
config
)
=>
{
// Store any external links to the group in the config so when rebuilding we add extra slots
config
.
external
=
[];
for
(
let
i
=
0
;
i
<
this
.
nodes
.
length
;
i
++
)
{
const
node
=
this
.
nodes
[
i
];
if
(
!
node
.
outputs
?.
length
)
continue
;
for
(
let
slot
=
0
;
slot
<
node
.
outputs
.
length
;
slot
++
)
{
let
hasExternal
=
false
;
const
output
=
node
.
outputs
[
slot
];
let
type
=
output
.
type
;
if
(
!
output
.
links
?.
length
)
continue
;
for
(
const
l
of
output
.
links
)
{
const
link
=
app
.
graph
.
links
[
l
];
if
(
!
link
)
continue
;
if
(
type
===
"
*
"
)
type
=
link
.
type
;
if
(
!
app
.
canvas
.
selected_nodes
[
link
.
target_id
])
{
hasExternal
=
true
;
break
;
}
}
if
(
hasExternal
)
{
config
.
external
.
push
([
i
,
slot
,
type
]);
}
}
}
};
// Use the built in copyToClipboard function to generate the node data we need
const
backup
=
localStorage
.
getItem
(
"
litegrapheditor_clipboard
"
);
try
{
app
.
canvas
.
copyToClipboard
(
this
.
nodes
);
const
config
=
JSON
.
parse
(
localStorage
.
getItem
(
"
litegrapheditor_clipboard
"
));
storeLinkTypes
(
config
);
storeExternalLinks
(
config
);
return
config
;
}
finally
{
localStorage
.
setItem
(
"
litegrapheditor_clipboard
"
,
backup
);
}
}
}
export
class
GroupNodeConfig
{
constructor
(
name
,
nodeData
)
{
this
.
name
=
name
;
this
.
nodeData
=
nodeData
;
this
.
getLinks
();
this
.
inputCount
=
0
;
this
.
oldToNewOutputMap
=
{};
this
.
newToOldOutputMap
=
{};
this
.
oldToNewInputMap
=
{};
this
.
oldToNewWidgetMap
=
{};
this
.
newToOldWidgetMap
=
{};
this
.
primitiveDefs
=
{};
this
.
widgetToPrimitive
=
{};
this
.
primitiveToWidget
=
{};
}
async
registerType
(
source
=
"
workflow
"
)
{
this
.
nodeDef
=
{
output
:
[],
output_name
:
[],
output_is_list
:
[],
name
:
source
+
"
/
"
+
this
.
name
,
display_name
:
this
.
name
,
category
:
"
group nodes
"
+
(
"
/
"
+
source
),
input
:
{
required
:
{}
},
[
GROUP
]:
this
,
};
this
.
inputs
=
[];
const
seenInputs
=
{};
const
seenOutputs
=
{};
for
(
let
i
=
0
;
i
<
this
.
nodeData
.
nodes
.
length
;
i
++
)
{
const
node
=
this
.
nodeData
.
nodes
[
i
];
node
.
index
=
i
;
this
.
processNode
(
node
,
seenInputs
,
seenOutputs
);
}
await
app
.
registerNodeDef
(
"
workflow/
"
+
this
.
name
,
this
.
nodeDef
);
}
getLinks
()
{
this
.
linksFrom
=
{};
this
.
linksTo
=
{};
this
.
externalFrom
=
{};
// Extract links for easy lookup
for
(
const
l
of
this
.
nodeData
.
links
)
{
const
[
sourceNodeId
,
sourceNodeSlot
,
targetNodeId
,
targetNodeSlot
]
=
l
;
// Skip links outside the copy config
if
(
sourceNodeId
==
null
)
continue
;
if
(
!
this
.
linksFrom
[
sourceNodeId
])
{
this
.
linksFrom
[
sourceNodeId
]
=
{};
}
this
.
linksFrom
[
sourceNodeId
][
sourceNodeSlot
]
=
l
;
if
(
!
this
.
linksTo
[
targetNodeId
])
{
this
.
linksTo
[
targetNodeId
]
=
{};
}
this
.
linksTo
[
targetNodeId
][
targetNodeSlot
]
=
l
;
}
if
(
this
.
nodeData
.
external
)
{
for
(
const
ext
of
this
.
nodeData
.
external
)
{
if
(
!
this
.
externalFrom
[
ext
[
0
]])
{
this
.
externalFrom
[
ext
[
0
]]
=
{
[
ext
[
1
]]:
ext
[
2
]
};
}
else
{
this
.
externalFrom
[
ext
[
0
]][
ext
[
1
]]
=
ext
[
2
];
}
}
}
}
processNode
(
node
,
seenInputs
,
seenOutputs
)
{
const
def
=
this
.
getNodeDef
(
node
);
if
(
!
def
)
return
;
const
inputs
=
{
...
def
.
input
?.
required
,
...
def
.
input
?.
optional
};
this
.
inputs
.
push
(
this
.
processNodeInputs
(
node
,
seenInputs
,
inputs
));
if
(
def
.
output
?.
length
)
this
.
processNodeOutputs
(
node
,
seenOutputs
,
def
);
}
getNodeDef
(
node
)
{
const
def
=
globalDefs
[
node
.
type
];
if
(
def
)
return
def
;
const
linksFrom
=
this
.
linksFrom
[
node
.
index
];
if
(
node
.
type
===
"
PrimitiveNode
"
)
{
// Skip as its not linked
if
(
!
linksFrom
)
return
;
let
type
=
linksFrom
[
"
0
"
][
5
];
if
(
type
===
"
COMBO
"
)
{
// Use the array items
const
source
=
node
.
outputs
[
0
].
widget
.
name
;
const
fromTypeName
=
this
.
nodeData
.
nodes
[
linksFrom
[
"
0
"
][
2
]].
type
;
const
fromType
=
globalDefs
[
fromTypeName
];
const
input
=
fromType
.
input
.
required
[
source
]
??
fromType
.
input
.
optional
[
source
];
type
=
input
[
0
];
}
const
def
=
(
this
.
primitiveDefs
[
node
.
index
]
=
{
input
:
{
required
:
{
value
:
[
type
,
{}],
},
},
output
:
[
type
],
output_name
:
[],
output_is_list
:
[],
});
return
def
;
}
else
if
(
node
.
type
===
"
Reroute
"
)
{
const
linksTo
=
this
.
linksTo
[
node
.
index
];
if
(
linksTo
&&
linksFrom
&&
!
this
.
externalFrom
[
node
.
index
]?.[
0
])
{
// Being used internally
return
null
;
}
let
rerouteType
=
"
*
"
;
if
(
linksFrom
)
{
const
[,
,
id
,
slot
]
=
linksFrom
[
"
0
"
];
rerouteType
=
this
.
nodeData
.
nodes
[
id
].
inputs
[
slot
].
type
;
}
else
if
(
linksTo
)
{
const
[
id
,
slot
]
=
linksTo
[
"
0
"
];
rerouteType
=
this
.
nodeData
.
nodes
[
id
].
outputs
[
slot
].
type
;
}
else
{
// Reroute used as a pipe
for
(
const
l
of
this
.
nodeData
.
links
)
{
if
(
l
[
2
]
===
node
.
index
)
{
rerouteType
=
l
[
5
];
break
;
}
}
if
(
rerouteType
===
"
*
"
)
{
// Check for an external link
const
t
=
this
.
externalFrom
[
node
.
index
]?.[
0
];
if
(
t
)
{
rerouteType
=
t
;
}
}
}
return
{
input
:
{
required
:
{
[
rerouteType
]:
[
rerouteType
,
{}],
},
},
output
:
[
rerouteType
],
output_name
:
[],
output_is_list
:
[],
};
}
console
.
warn
(
"
Skipping virtual node
"
+
node
.
type
+
"
when building group node
"
+
this
.
name
);
}
getInputConfig
(
node
,
inputName
,
seenInputs
,
config
,
extra
)
{
let
name
=
node
.
inputs
?.
find
((
inp
)
=>
inp
.
name
===
inputName
)?.
label
??
inputName
;
let
prefix
=
""
;
// Special handling for primitive to include the title if it is set rather than just "value"
if
((
node
.
type
===
"
PrimitiveNode
"
&&
node
.
title
)
||
name
in
seenInputs
)
{
prefix
=
`
${
node
.
title
??
node
.
type
}
`;
name = `
$
{
prefix
}
$
{
inputName
}
`;
if (name in seenInputs) {
name = `
$
{
prefix
}
$
{
seenInputs
[
name
]}
$
{
inputName
}
`;
}
}
seenInputs[name] = (seenInputs[name] ?? 1) + 1;
if (inputName === "seed" || inputName === "noise_seed") {
if (!extra) extra = {};
extra.control_after_generate = `
$
{
prefix
}
control_after_generate
`;
}
if (config[0] === "IMAGEUPLOAD") {
if (!extra) extra = {};
extra.widget = `
$
{
prefix
}
$
{
config
[
1
]?.
widget
??
"
image
"
}
`;
}
if (extra) {
config = [config[0], { ...config[1], ...extra }];
}
return { name, config };
}
processWidgetInputs(inputs, node, inputNames, seenInputs) {
const slots = [];
const converted = new Map();
const widgetMap = (this.oldToNewWidgetMap[node.index] = {});
for (const inputName of inputNames) {
let widgetType = app.getWidgetType(inputs[inputName], inputName);
if (widgetType) {
const convertedIndex = node.inputs?.findIndex(
(inp) => inp.name === inputName && inp.widget?.name === inputName
);
if (convertedIndex > -1) {
// This widget has been converted to a widget
// We need to store this in the correct position so link ids line up
converted.set(convertedIndex, inputName);
widgetMap[inputName] = null;
} else {
// Normal widget
const { name, config } = this.getInputConfig(node, inputName, seenInputs, inputs[inputName]);
this.nodeDef.input.required[name] = config;
widgetMap[inputName] = name;
this.newToOldWidgetMap[name] = { node, inputName };
}
} else {
// Normal input
slots.push(inputName);
}
}
return { converted, slots };
}
checkPrimitiveConnection(link, inputName, inputs) {
const sourceNode = this.nodeData.nodes[link[0]];
if (sourceNode.type === "PrimitiveNode") {
// Merge link configurations
const [sourceNodeId, _, targetNodeId, __] = link;
const primitiveDef = this.primitiveDefs[sourceNodeId];
const targetWidget = inputs[inputName];
const primitiveConfig = primitiveDef.input.required.value;
const output = { widget: primitiveConfig };
const config = mergeIfValid(output, targetWidget, false, null, primitiveConfig);
primitiveConfig[1] = config?.customConfig ?? inputs[inputName][1] ? { ...inputs[inputName][1] } : {};
let name = this.oldToNewWidgetMap[sourceNodeId]["value"];
name = name.substr(0, name.length - 6);
primitiveConfig[1].control_after_generate = true;
primitiveConfig[1].control_prefix = name;
let toPrimitive = this.widgetToPrimitive[targetNodeId];
if (!toPrimitive) {
toPrimitive = this.widgetToPrimitive[targetNodeId] = {};
}
if (toPrimitive[inputName]) {
toPrimitive[inputName].push(sourceNodeId);
}
toPrimitive[inputName] = sourceNodeId;
let toWidget = this.primitiveToWidget[sourceNodeId];
if (!toWidget) {
toWidget = this.primitiveToWidget[sourceNodeId] = [];
}
toWidget.push({ nodeId: targetNodeId, inputName });
}
}
processInputSlots(inputs, node, slots, linksTo, inputMap, seenInputs) {
for (let i = 0; i < slots.length; i++) {
const inputName = slots[i];
if (linksTo[i]) {
this.checkPrimitiveConnection(linksTo[i], inputName, inputs);
// This input is linked so we can skip it
continue;
}
const { name, config } = this.getInputConfig(node, inputName, seenInputs, inputs[inputName]);
this.nodeDef.input.required[name] = config;
inputMap[i] = this.inputCount++;
}
}
processConvertedWidgets(inputs, node, slots, converted, linksTo, inputMap, seenInputs) {
// Add converted widgets sorted into their index order (ordered as they were converted) so link ids match up
const convertedSlots = [...converted.keys()].sort().map((k) => converted.get(k));
for (let i = 0; i < convertedSlots.length; i++) {
const inputName = convertedSlots[i];
if (linksTo[slots.length + i]) {
this.checkPrimitiveConnection(linksTo[slots.length + i], inputName, inputs);
// This input is linked so we can skip it
continue;
}
const { name, config } = this.getInputConfig(node, inputName, seenInputs, inputs[inputName], {
defaultInput: true,
});
this.nodeDef.input.required[name] = config;
inputMap[slots.length + i] = this.inputCount++;
}
}
processNodeInputs(node, seenInputs, inputs) {
const inputMapping = [];
const inputNames = Object.keys(inputs);
if (!inputNames.length) return;
const { converted, slots } = this.processWidgetInputs(inputs, node, inputNames, seenInputs);
const linksTo = this.linksTo[node.index] ?? {};
const inputMap = (this.oldToNewInputMap[node.index] = {});
this.processInputSlots(inputs, node, slots, linksTo, inputMap, seenInputs);
this.processConvertedWidgets(inputs, node, slots, converted, linksTo, inputMap, seenInputs);
return inputMapping;
}
processNodeOutputs(node, seenOutputs, def) {
const oldToNew = (this.oldToNewOutputMap[node.index] = {});
// Add outputs
for (let outputId = 0; outputId < def.output.length; outputId++) {
const linksFrom = this.linksFrom[node.index];
if (linksFrom?.[outputId] && !this.externalFrom[node.index]?.[outputId]) {
// This output is linked internally so we can skip it
continue;
}
oldToNew[outputId] = this.nodeDef.output.length;
this.newToOldOutputMap[this.nodeDef.output.length] = { node, slot: outputId };
this.nodeDef.output.push(def.output[outputId]);
this.nodeDef.output_is_list.push(def.output_is_list[outputId]);
let label = def.output_name?.[outputId] ?? def.output[outputId];
const output = node.outputs.find((o) => o.name === label);
if (output?.label) {
label = output.label;
}
let name = label;
if (name in seenOutputs) {
const prefix = `
$
{
node
.
title
??
node
.
type
}
`;
name = `
$
{
prefix
}
$
{
label
}
`;
if (name in seenOutputs) {
name = `
$
{
prefix
}
$
{
node
.
index
}
$
{
label
}
`;
}
}
seenOutputs[name] = 1;
this.nodeDef.output_name.push(name);
}
}
static async registerFromWorkflow(groupNodes, missingNodeTypes) {
for (const g in groupNodes) {
const groupData = groupNodes[g];
let hasMissing = false;
for (const n of groupData.nodes) {
// Find missing node types
if (!(n.type in LiteGraph.registered_node_types)) {
missingNodeTypes.push(n.type);
hasMissing = true;
}
}
if (hasMissing) continue;
const config = new GroupNodeConfig(g, groupData);
await config.registerType();
}
}
}
export class GroupNodeHandler {
node;
groupData;
constructor(node) {
this.node = node;
this.groupData = node.constructor?.nodeData?.[GROUP];
this.node.setInnerNodes = (innerNodes) => {
this.innerNodes = innerNodes;
for (let innerNodeIndex = 0; innerNodeIndex < this.innerNodes.length; innerNodeIndex++) {
const innerNode = this.innerNodes[innerNodeIndex];
for (const w of innerNode.widgets ?? []) {
if (w.type === "converted-widget") {
w.serializeValue = w.origSerializeValue;
}
}
innerNode.index = innerNodeIndex;
innerNode.getInputNode = (slot) => {
// Check if this input is internal or external
const externalSlot = this.groupData.oldToNewInputMap[innerNode.index]?.[slot];
if (externalSlot != null) {
return this.node.getInputNode(externalSlot);
}
// Internal link
const innerLink = this.groupData.linksTo[innerNode.index]?.[slot];
if (!innerLink) return null;
const inputNode = innerNodes[innerLink[0]];
// Primitives will already apply their values
if (inputNode.type === "PrimitiveNode") return null;
return inputNode;
};
innerNode.getInputLink = (slot) => {
const externalSlot = this.groupData.oldToNewInputMap[innerNode.index]?.[slot];
if (externalSlot != null) {
// The inner node is connected via the group node inputs
const linkId = this.node.inputs[externalSlot].link;
let link = app.graph.links[linkId];
// Use the outer link, but update the target to the inner node
link = {
...link,
target_id: innerNode.id,
target_slot: +slot,
};
return link;
}
let link = this.groupData.linksTo[innerNode.index]?.[slot];
if (!link) return null;
// Use the inner link, but update the origin node to be inner node id
link = {
origin_id: innerNodes[link[0]].id,
origin_slot: link[1],
target_id: innerNode.id,
target_slot: +slot,
};
return link;
};
}
};
this.node.updateLink = (link) => {
// Replace the group node reference with the internal node
link = { ...link };
const output = this.groupData.newToOldOutputMap[link.origin_slot];
let innerNode = this.innerNodes[output.node.index];
let l;
while (innerNode.type === "Reroute") {
l = innerNode.getInputLink(0);
innerNode = innerNode.getInputNode(0);
}
link.origin_id = innerNode.id;
link.origin_slot = l?.origin_slot ?? output.slot;
return link;
};
this.node.getInnerNodes = () => {
if (!this.innerNodes) {
this.node.setInnerNodes(
this.groupData.nodeData.nodes.map((n, i) => {
const innerNode = LiteGraph.createNode(n.type);
innerNode.configure(n);
innerNode.id = `
$
{
this
.
node
.
id
}:
$
{
i
}
`;
return innerNode;
})
);
}
this.updateInnerWidgets();
return this.innerNodes;
};
this.node.convertToNodes = () => {
const addInnerNodes = () => {
const backup = localStorage.getItem("litegrapheditor_clipboard");
// Clone the node data so we dont mutate it for other nodes
const c = { ...this.groupData.nodeData };
c.nodes = [...c.nodes];
const innerNodes = this.node.getInnerNodes();
let ids = [];
for (let i = 0; i < c.nodes.length; i++) {
let id = innerNodes?.[i]?.id;
// Use existing IDs if they are set on the inner nodes
if (id == null || isNaN(id)) {
id = undefined;
} else {
ids.push(id);
}
c.nodes[i] = { ...c.nodes[i], id };
}
localStorage.setItem("litegrapheditor_clipboard", JSON.stringify(c));
app.canvas.pasteFromClipboard();
localStorage.setItem("litegrapheditor_clipboard", backup);
const [x, y] = this.node.pos;
let top;
let left;
// Configure nodes with current widget data
const selectedIds = ids.length ? ids : Object.keys(app.canvas.selected_nodes);
const newNodes = [];
for (let i = 0; i < selectedIds.length; i++) {
const id = selectedIds[i];
const newNode = app.graph.getNodeById(id);
const innerNode = innerNodes[i];
newNodes.push(newNode);
if (left == null || newNode.pos[0] < left) {
left = newNode.pos[0];
}
if (top == null || newNode.pos[1] < top) {
top = newNode.pos[1];
}
const map = this.groupData.oldToNewWidgetMap[innerNode.index];
if (map) {
const widgets = Object.keys(map);
for (const oldName of widgets) {
const newName = map[oldName];
if (!newName) continue;
const widgetIndex = this.node.widgets.findIndex((w) => w.name === newName);
if (widgetIndex === -1) continue;
// Populate the main and any linked widgets
if (innerNode.type === "PrimitiveNode") {
for (let i = 0; i < newNode.widgets.length; i++) {
newNode.widgets[i].value = this.node.widgets[widgetIndex + i].value;
}
} else {
const outerWidget = this.node.widgets[widgetIndex];
const newWidget = newNode.widgets.find((w) => w.name === oldName);
if (!newWidget) continue;
newWidget.value = outerWidget.value;
for (let w = 0; w < outerWidget.linkedWidgets?.length; w++) {
newWidget.linkedWidgets[w].value = outerWidget.linkedWidgets[w].value;
}
}
}
}
}
// Shift each node
for (const newNode of newNodes) {
newNode.pos = [newNode.pos[0] - (left - x), newNode.pos[1] - (top - y)];
}
return { newNodes, selectedIds };
};
const reconnectInputs = (selectedIds) => {
for (const innerNodeIndex in this.groupData.oldToNewInputMap) {
const id = selectedIds[innerNodeIndex];
const newNode = app.graph.getNodeById(id);
const map = this.groupData.oldToNewInputMap[innerNodeIndex];
for (const innerInputId in map) {
const groupSlotId = map[innerInputId];
if (groupSlotId == null) continue;
const slot = node.inputs[groupSlotId];
if (slot.link == null) continue;
const link = app.graph.links[slot.link];
// connect this node output to the input of another node
const originNode = app.graph.getNodeById(link.origin_id);
originNode.connect(link.origin_slot, newNode, +innerInputId);
}
}
};
const reconnectOutputs = () => {
for (let groupOutputId = 0; groupOutputId < node.outputs?.length; groupOutputId++) {
const output = node.outputs[groupOutputId];
if (!output.links) continue;
const links = [...output.links];
for (const l of links) {
const slot = this.groupData.newToOldOutputMap[groupOutputId];
const link = app.graph.links[l];
const targetNode = app.graph.getNodeById(link.target_id);
const newNode = app.graph.getNodeById(selectedIds[slot.node.index]);
newNode.connect(slot.slot, targetNode, link.target_slot);
}
}
};
const { newNodes, selectedIds } = addInnerNodes();
reconnectInputs(selectedIds);
reconnectOutputs(selectedIds);
app.graph.remove(this.node);
return newNodes;
};
const getExtraMenuOptions = this.node.getExtraMenuOptions;
this.node.getExtraMenuOptions = function (_, options) {
getExtraMenuOptions?.apply(this, arguments);
let optionIndex = options.findIndex((o) => o.content === "Outputs");
if (optionIndex === -1) optionIndex = options.length;
else optionIndex++;
options.splice(optionIndex, 0, null, {
content: "Convert to nodes",
callback: () => {
return this.convertToNodes();
},
});
};
// Draw custom collapse icon to identity this as a group
const onDrawTitleBox = this.node.onDrawTitleBox;
this.node.onDrawTitleBox = function (ctx, height, size, scale) {
onDrawTitleBox?.apply(this, arguments);
const fill = ctx.fillStyle;
ctx.beginPath();
ctx.rect(11, -height + 11, 2, 2);
ctx.rect(14, -height + 11, 2, 2);
ctx.rect(17, -height + 11, 2, 2);
ctx.rect(11, -height + 14, 2, 2);
ctx.rect(14, -height + 14, 2, 2);
ctx.rect(17, -height + 14, 2, 2);
ctx.rect(11, -height + 17, 2, 2);
ctx.rect(14, -height + 17, 2, 2);
ctx.rect(17, -height + 17, 2, 2);
ctx.fillStyle = this.boxcolor || LiteGraph.NODE_DEFAULT_BOXCOLOR;
ctx.fill();
ctx.fillStyle = fill;
};
// Draw progress label
const onDrawForeground = node.onDrawForeground;
const groupData = this.groupData.nodeData;
node.onDrawForeground = function (ctx) {
const r = onDrawForeground?.apply?.(this, arguments);
if (+app.runningNodeId === this.id && this.runningInternalNodeId !== null) {
const n = groupData.nodes[this.runningInternalNodeId];
const message = `
Running
$
{
n
.
title
||
n
.
type
}
(
$
{
this
.
runningInternalNodeId
}
/${groupData.nodes.length}
)
`
;
ctx
.
save
();
ctx
.
font
=
"
12px sans-serif
"
;
const
sz
=
ctx
.
measureText
(
message
);
ctx
.
fillStyle
=
node
.
boxcolor
||
LiteGraph
.
NODE_DEFAULT_BOXCOLOR
;
ctx
.
beginPath
();
ctx
.
roundRect
(
0
,
-
LiteGraph
.
NODE_TITLE_HEIGHT
-
20
,
sz
.
width
+
12
,
20
,
5
);
ctx
.
fill
();
ctx
.
fillStyle
=
"
#fff
"
;
ctx
.
fillText
(
message
,
6
,
-
LiteGraph
.
NODE_TITLE_HEIGHT
-
6
);
ctx
.
restore
();
}
};
// Flag this node as needing to be reset
const
onExecutionStart
=
this
.
node
.
onExecutionStart
;
this
.
node
.
onExecutionStart
=
function
()
{
this
.
resetExecution
=
true
;
return
onExecutionStart
?.
apply
(
this
,
arguments
);
};
function
handleEvent
(
type
,
getId
,
getEvent
)
{
const
handler
=
({
detail
})
=>
{
const
id
=
getId
(
detail
);
if
(
!
id
)
return
;
const
node
=
app
.
graph
.
getNodeById
(
id
);
if
(
node
)
return
;
const
innerNodeIndex
=
this
.
innerNodes
?.
findIndex
((
n
)
=>
n
.
id
==
id
);
if
(
innerNodeIndex
>
-
1
)
{
this
.
node
.
runningInternalNodeId
=
innerNodeIndex
;
api
.
dispatchEvent
(
new
CustomEvent
(
type
,
{
detail
:
getEvent
(
detail
,
this
.
node
.
id
+
""
,
this
.
node
)
}));
}
};
api
.
addEventListener
(
type
,
handler
);
return
handler
;
}
const
executing
=
handleEvent
.
call
(
this
,
"
executing
"
,
(
d
)
=>
d
,
(
d
,
id
,
node
)
=>
id
);
const
executed
=
handleEvent
.
call
(
this
,
"
executed
"
,
(
d
)
=>
d
?.
node
,
(
d
,
id
,
node
)
=>
({
...
d
,
node
:
id
,
merge
:
!
node
.
resetExecution
})
);
const
onRemoved
=
node
.
onRemoved
;
this
.
node
.
onRemoved
=
function
()
{
onRemoved
?.
apply
(
this
,
arguments
);
api
.
removeEventListener
(
"
executing
"
,
executing
);
api
.
removeEventListener
(
"
executed
"
,
executed
);
};
}
updateInnerWidgets
()
{
for
(
const
newWidgetName
in
this
.
groupData
.
newToOldWidgetMap
)
{
const
newWidget
=
this
.
node
.
widgets
.
find
((
w
)
=>
w
.
name
===
newWidgetName
);
if
(
!
newWidget
)
continue
;
const
newValue
=
newWidget
.
value
;
const
old
=
this
.
groupData
.
newToOldWidgetMap
[
newWidgetName
];
let
innerNode
=
this
.
innerNodes
[
old
.
node
.
index
];
if
(
innerNode
.
type
===
"
PrimitiveNode
"
)
{
innerNode
.
primitiveValue
=
newValue
;
const
primitiveLinked
=
this
.
groupData
.
primitiveToWidget
[
old
.
node
.
index
];
for
(
const
linked
of
primitiveLinked
)
{
const
node
=
this
.
innerNodes
[
linked
.
nodeId
];
const
widget
=
node
.
widgets
.
find
((
w
)
=>
w
.
name
===
linked
.
inputName
);
if
(
widget
)
{
widget
.
value
=
newValue
;
}
}
continue
;
}
const
widget
=
innerNode
.
widgets
?.
find
((
w
)
=>
w
.
name
===
old
.
inputName
);
if
(
widget
)
{
widget
.
value
=
newValue
;
}
}
}
populatePrimitive
(
node
,
nodeId
,
oldName
,
i
,
linkedShift
)
{
// Converted widget, populate primitive if linked
const
primitiveId
=
this
.
groupData
.
widgetToPrimitive
[
nodeId
]?.[
oldName
];
if
(
primitiveId
==
null
)
return
;
const
targetWidgetName
=
this
.
groupData
.
oldToNewWidgetMap
[
primitiveId
][
"
value
"
];
const
targetWidgetIndex
=
this
.
node
.
widgets
.
findIndex
((
w
)
=>
w
.
name
===
targetWidgetName
);
if
(
targetWidgetIndex
>
-
1
)
{
const
primitiveNode
=
this
.
innerNodes
[
primitiveId
];
let
len
=
primitiveNode
.
widgets
.
length
;
if
(
len
-
1
!==
this
.
node
.
widgets
[
targetWidgetIndex
].
linkedWidgets
?.
length
)
{
// Fallback handling for if some reason the primitive has a different number of widgets
// we dont want to overwrite random widgets, better to leave blank
len
=
1
;
}
for
(
let
i
=
0
;
i
<
len
;
i
++
)
{
this
.
node
.
widgets
[
targetWidgetIndex
+
i
].
value
=
primitiveNode
.
widgets
[
i
].
value
;
}
}
}
populateWidgets
()
{
for
(
let
nodeId
=
0
;
nodeId
<
this
.
groupData
.
nodeData
.
nodes
.
length
;
nodeId
++
)
{
const
node
=
this
.
groupData
.
nodeData
.
nodes
[
nodeId
];
if
(
!
node
.
widgets_values
?.
length
)
continue
;
const
map
=
this
.
groupData
.
oldToNewWidgetMap
[
nodeId
];
const
widgets
=
Object
.
keys
(
map
);
let
linkedShift
=
0
;
for
(
let
i
=
0
;
i
<
widgets
.
length
;
i
++
)
{
const
oldName
=
widgets
[
i
];
const
newName
=
map
[
oldName
];
const
widgetIndex
=
this
.
node
.
widgets
.
findIndex
((
w
)
=>
w
.
name
===
newName
);
const
mainWidget
=
this
.
node
.
widgets
[
widgetIndex
];
if
(
!
newName
)
{
// New name will be null if its a converted widget
this
.
populatePrimitive
(
node
,
nodeId
,
oldName
,
i
,
linkedShift
);
// Find the inner widget and shift by the number of linked widgets as they will have been removed too
const
innerWidget
=
this
.
innerNodes
[
nodeId
].
widgets
?.
find
((
w
)
=>
w
.
name
===
oldName
);
linkedShift
+=
innerWidget
.
linkedWidgets
?.
length
??
0
;
continue
;
}
if
(
widgetIndex
===
-
1
)
{
continue
;
}
// Populate the main and any linked widget
mainWidget
.
value
=
node
.
widgets_values
[
i
+
linkedShift
];
for
(
let
w
=
0
;
w
<
mainWidget
.
linkedWidgets
?.
length
;
w
++
)
{
this
.
node
.
widgets
[
widgetIndex
+
w
+
1
].
value
=
node
.
widgets_values
[
i
+
++
linkedShift
];
}
}
}
}
replaceNodes
(
nodes
)
{
let
top
;
let
left
;
for
(
let
i
=
0
;
i
<
nodes
.
length
;
i
++
)
{
const
node
=
nodes
[
i
];
if
(
left
==
null
||
node
.
pos
[
0
]
<
left
)
{
left
=
node
.
pos
[
0
];
}
if
(
top
==
null
||
node
.
pos
[
1
]
<
top
)
{
top
=
node
.
pos
[
1
];
}
this
.
linkOutputs
(
node
,
i
);
app
.
graph
.
remove
(
node
);
}
this
.
linkInputs
();
this
.
node
.
pos
=
[
left
,
top
];
}
linkOutputs
(
originalNode
,
nodeId
)
{
if
(
!
originalNode
.
outputs
)
return
;
for
(
const
output
of
originalNode
.
outputs
)
{
if
(
!
output
.
links
)
continue
;
// Clone the links as they'll be changed if we reconnect
const
links
=
[...
output
.
links
];
for
(
const
l
of
links
)
{
const
link
=
app
.
graph
.
links
[
l
];
if
(
!
link
)
continue
;
const
targetNode
=
app
.
graph
.
getNodeById
(
link
.
target_id
);
const
newSlot
=
this
.
groupData
.
oldToNewOutputMap
[
nodeId
]?.[
link
.
origin_slot
];
if
(
newSlot
!=
null
)
{
this
.
node
.
connect
(
newSlot
,
targetNode
,
link
.
target_slot
);
}
}
}
}
linkInputs
()
{
for
(
const
link
of
this
.
groupData
.
nodeData
.
links
??
[])
{
const
[,
originSlot
,
targetId
,
targetSlot
,
actualOriginId
]
=
link
;
const
originNode
=
app
.
graph
.
getNodeById
(
actualOriginId
);
if
(
!
originNode
)
continue
;
// this node is in the group
originNode
.
connect
(
originSlot
,
this
.
node
.
id
,
this
.
groupData
.
oldToNewInputMap
[
targetId
][
targetSlot
]);
}
}
static
getGroupData
(
node
)
{
return
node
.
constructor
?.
nodeData
?.[
GROUP
];
}
static
isGroupNode
(
node
)
{
return
!!
node
.
constructor
?.
nodeData
?.[
GROUP
];
}
static
async
fromNodes
(
nodes
)
{
// Process the nodes into the stored workflow group node data
const
builder
=
new
GroupNodeBuilder
(
nodes
);
const
res
=
builder
.
build
();
if
(
!
res
)
return
;
const
{
name
,
nodeData
}
=
res
;
// Convert this data into a LG node definition and register it
const
config
=
new
GroupNodeConfig
(
name
,
nodeData
);
await
config
.
registerType
();
const
groupNode
=
LiteGraph
.
createNode
(
`workflow/
${
name
}
`
);
// Reuse the existing nodes for this instance
groupNode
.
setInnerNodes
(
builder
.
nodes
);
groupNode
[
GROUP
].
populateWidgets
();
app
.
graph
.
add
(
groupNode
);
// Remove all converted nodes and relink them
groupNode
[
GROUP
].
replaceNodes
(
builder
.
nodes
);
return
groupNode
;
}
}
function
addConvertToGroupOptions
()
{
function
addOption
(
options
,
index
)
{
const
selected
=
Object
.
values
(
app
.
canvas
.
selected_nodes
??
{});
const
disabled
=
selected
.
length
<
2
||
selected
.
find
((
n
)
=>
GroupNodeHandler
.
isGroupNode
(
n
));
options
.
splice
(
index
+
1
,
null
,
{
content
:
`Convert to Group Node`
,
disabled
,
callback
:
async
()
=>
{
return
await
GroupNodeHandler
.
fromNodes
(
selected
);
},
});
}
// Add to canvas
const
getCanvasMenuOptions
=
LGraphCanvas
.
prototype
.
getCanvasMenuOptions
;
LGraphCanvas
.
prototype
.
getCanvasMenuOptions
=
function
()
{
const
options
=
getCanvasMenuOptions
.
apply
(
this
,
arguments
);
const
index
=
options
.
findIndex
((
o
)
=>
o
?.
content
===
"
Add Group
"
)
+
1
||
options
.
length
;
addOption
(
options
,
index
);
return
options
;
};
// Add to nodes
const
getNodeMenuOptions
=
LGraphCanvas
.
prototype
.
getNodeMenuOptions
;
LGraphCanvas
.
prototype
.
getNodeMenuOptions
=
function
(
node
)
{
const
options
=
getNodeMenuOptions
.
apply
(
this
,
arguments
);
if
(
!
GroupNodeHandler
.
isGroupNode
(
node
))
{
const
index
=
options
.
findIndex
((
o
)
=>
o
?.
content
===
"
Outputs
"
)
+
1
||
options
.
length
-
1
;
addOption
(
options
,
index
);
}
return
options
;
};
}
const
id
=
"
Comfy.GroupNode
"
;
let
globalDefs
;
const
ext
=
{
name
:
id
,
setup
()
{
addConvertToGroupOptions
();
},
async
beforeConfigureGraph
(
graphData
,
missingNodeTypes
)
{
const
nodes
=
graphData
?.
extra
?.
groupNodes
;
if
(
nodes
)
{
await
GroupNodeConfig
.
registerFromWorkflow
(
nodes
,
missingNodeTypes
);
}
},
addCustomNodeDefs
(
defs
)
{
// Store this so we can mutate it later with group nodes
globalDefs
=
defs
;
},
nodeCreated
(
node
)
{
if
(
GroupNodeHandler
.
isGroupNode
(
node
))
{
node
[
GROUP
]
=
new
GroupNodeHandler
(
node
);
}
},
};
app
.
registerExtension
(
ext
);
web/extensions/core/nodeTemplates.js
View file @
c92f3dca
import
{
app
}
from
"
../../scripts/app.js
"
;
import
{
app
}
from
"
../../scripts/app.js
"
;
import
{
ComfyDialog
,
$el
}
from
"
../../scripts/ui.js
"
;
import
{
ComfyDialog
,
$el
}
from
"
../../scripts/ui.js
"
;
import
{
GroupNodeConfig
,
GroupNodeHandler
}
from
"
./groupNode.js
"
;
// Adds the ability to save and add multiple nodes as a template
// Adds the ability to save and add multiple nodes as a template
// To save:
// To save:
...
@@ -34,7 +35,7 @@ class ManageTemplates extends ComfyDialog {
...
@@ -34,7 +35,7 @@ class ManageTemplates extends ComfyDialog {
type
:
"
file
"
,
type
:
"
file
"
,
accept
:
"
.json
"
,
accept
:
"
.json
"
,
multiple
:
true
,
multiple
:
true
,
style
:
{
display
:
"
none
"
},
style
:
{
display
:
"
none
"
},
parent
:
document
.
body
,
parent
:
document
.
body
,
onchange
:
()
=>
this
.
importAll
(),
onchange
:
()
=>
this
.
importAll
(),
});
});
...
@@ -109,13 +110,13 @@ class ManageTemplates extends ComfyDialog {
...
@@ -109,13 +110,13 @@ class ManageTemplates extends ComfyDialog {
return
;
return
;
}
}
const
json
=
JSON
.
stringify
({
templates
:
this
.
templates
},
null
,
2
);
// convert the data to a JSON string
const
json
=
JSON
.
stringify
({
templates
:
this
.
templates
},
null
,
2
);
// convert the data to a JSON string
const
blob
=
new
Blob
([
json
],
{
type
:
"
application/json
"
});
const
blob
=
new
Blob
([
json
],
{
type
:
"
application/json
"
});
const
url
=
URL
.
createObjectURL
(
blob
);
const
url
=
URL
.
createObjectURL
(
blob
);
const
a
=
$el
(
"
a
"
,
{
const
a
=
$el
(
"
a
"
,
{
href
:
url
,
href
:
url
,
download
:
"
node_templates.json
"
,
download
:
"
node_templates.json
"
,
style
:
{
display
:
"
none
"
},
style
:
{
display
:
"
none
"
},
parent
:
document
.
body
,
parent
:
document
.
body
,
});
});
a
.
click
();
a
.
click
();
...
@@ -291,11 +292,11 @@ app.registerExtension({
...
@@ -291,11 +292,11 @@ app.registerExtension({
setup
()
{
setup
()
{
const
manage
=
new
ManageTemplates
();
const
manage
=
new
ManageTemplates
();
const
clipboardAction
=
(
cb
)
=>
{
const
clipboardAction
=
async
(
cb
)
=>
{
// We use the clipboard functions but dont want to overwrite the current user clipboard
// We use the clipboard functions but dont want to overwrite the current user clipboard
// Restore it after we've run our callback
// Restore it after we've run our callback
const
old
=
localStorage
.
getItem
(
"
litegrapheditor_clipboard
"
);
const
old
=
localStorage
.
getItem
(
"
litegrapheditor_clipboard
"
);
cb
();
await
cb
();
localStorage
.
setItem
(
"
litegrapheditor_clipboard
"
,
old
);
localStorage
.
setItem
(
"
litegrapheditor_clipboard
"
,
old
);
};
};
...
@@ -309,13 +310,31 @@ app.registerExtension({
...
@@ -309,13 +310,31 @@ app.registerExtension({
disabled
:
!
Object
.
keys
(
app
.
canvas
.
selected_nodes
||
{}).
length
,
disabled
:
!
Object
.
keys
(
app
.
canvas
.
selected_nodes
||
{}).
length
,
callback
:
()
=>
{
callback
:
()
=>
{
const
name
=
prompt
(
"
Enter name
"
);
const
name
=
prompt
(
"
Enter name
"
);
if
(
!
name
||
!
name
.
trim
())
return
;
if
(
!
name
?
.
trim
())
return
;
clipboardAction
(()
=>
{
clipboardAction
(()
=>
{
app
.
canvas
.
copyToClipboard
();
app
.
canvas
.
copyToClipboard
();
let
data
=
localStorage
.
getItem
(
"
litegrapheditor_clipboard
"
);
data
=
JSON
.
parse
(
data
);
const
nodeIds
=
Object
.
keys
(
app
.
canvas
.
selected_nodes
);
for
(
let
i
=
0
;
i
<
nodeIds
.
length
;
i
++
)
{
const
node
=
app
.
graph
.
getNodeById
(
nodeIds
[
i
]);
const
nodeData
=
node
?.
constructor
.
nodeData
;
let
groupData
=
GroupNodeHandler
.
getGroupData
(
node
);
if
(
groupData
)
{
groupData
=
groupData
.
nodeData
;
if
(
!
data
.
groupNodes
)
{
data
.
groupNodes
=
{};
}
data
.
groupNodes
[
nodeData
.
name
]
=
groupData
;
data
.
nodes
[
i
].
type
=
nodeData
.
name
;
}
}
manage
.
templates
.
push
({
manage
.
templates
.
push
({
name
,
name
,
data
:
localStorage
.
getItem
(
"
litegrapheditor_clipboard
"
),
data
:
JSON
.
stringify
(
data
),
});
});
manage
.
store
();
manage
.
store
();
});
});
...
@@ -323,15 +342,19 @@ app.registerExtension({
...
@@ -323,15 +342,19 @@ app.registerExtension({
});
});
// Map each template to a menu item
// Map each template to a menu item
const
subItems
=
manage
.
templates
.
map
((
t
)
=>
({
const
subItems
=
manage
.
templates
.
map
((
t
)
=>
{
content
:
t
.
name
,
return
{
callback
:
()
=>
{
content
:
t
.
name
,
clipboardAction
(()
=>
{
callback
:
()
=>
{
localStorage
.
setItem
(
"
litegrapheditor_clipboard
"
,
t
.
data
);
clipboardAction
(
async
()
=>
{
app
.
canvas
.
pasteFromClipboard
();
const
data
=
JSON
.
parse
(
t
.
data
);
});
await
GroupNodeConfig
.
registerFromWorkflow
(
data
.
groupNodes
,
{});
},
localStorage
.
setItem
(
"
litegrapheditor_clipboard
"
,
t
.
data
);
}));
app
.
canvas
.
pasteFromClipboard
();
});
},
};
});
subItems
.
push
(
null
,
{
subItems
.
push
(
null
,
{
content
:
"
Manage
"
,
content
:
"
Manage
"
,
...
...
web/extensions/core/undoRedo.js
0 → 100644
View file @
c92f3dca
import
{
app
}
from
"
../../scripts/app.js
"
;
const
MAX_HISTORY
=
50
;
let
undo
=
[];
let
redo
=
[];
let
activeState
=
null
;
let
isOurLoad
=
false
;
function
checkState
()
{
const
currentState
=
app
.
graph
.
serialize
();
if
(
!
graphEqual
(
activeState
,
currentState
))
{
undo
.
push
(
activeState
);
if
(
undo
.
length
>
MAX_HISTORY
)
{
undo
.
shift
();
}
activeState
=
clone
(
currentState
);
redo
.
length
=
0
;
}
}
const
loadGraphData
=
app
.
loadGraphData
;
app
.
loadGraphData
=
async
function
()
{
const
v
=
await
loadGraphData
.
apply
(
this
,
arguments
);
if
(
isOurLoad
)
{
isOurLoad
=
false
;
}
else
{
checkState
();
}
return
v
;
};
function
clone
(
obj
)
{
try
{
if
(
typeof
structuredClone
!==
"
undefined
"
)
{
return
structuredClone
(
obj
);
}
}
catch
(
error
)
{
// structuredClone is stricter than using JSON.parse/stringify so fallback to that
}
return
JSON
.
parse
(
JSON
.
stringify
(
obj
));
}
function
graphEqual
(
a
,
b
,
root
=
true
)
{
if
(
a
===
b
)
return
true
;
if
(
typeof
a
==
"
object
"
&&
a
&&
typeof
b
==
"
object
"
&&
b
)
{
const
keys
=
Object
.
getOwnPropertyNames
(
a
);
if
(
keys
.
length
!=
Object
.
getOwnPropertyNames
(
b
).
length
)
{
return
false
;
}
for
(
const
key
of
keys
)
{
let
av
=
a
[
key
];
let
bv
=
b
[
key
];
if
(
root
&&
key
===
"
nodes
"
)
{
// Nodes need to be sorted as the order changes when selecting nodes
av
=
[...
av
].
sort
((
a
,
b
)
=>
a
.
id
-
b
.
id
);
bv
=
[...
bv
].
sort
((
a
,
b
)
=>
a
.
id
-
b
.
id
);
}
if
(
!
graphEqual
(
av
,
bv
,
false
))
{
return
false
;
}
}
return
true
;
}
return
false
;
}
const
undoRedo
=
async
(
e
)
=>
{
if
(
e
.
ctrlKey
||
e
.
metaKey
)
{
if
(
e
.
key
===
"
y
"
)
{
const
prevState
=
redo
.
pop
();
if
(
prevState
)
{
undo
.
push
(
activeState
);
isOurLoad
=
true
;
await
app
.
loadGraphData
(
prevState
);
activeState
=
prevState
;
}
return
true
;
}
else
if
(
e
.
key
===
"
z
"
)
{
const
prevState
=
undo
.
pop
();
if
(
prevState
)
{
redo
.
push
(
activeState
);
isOurLoad
=
true
;
await
app
.
loadGraphData
(
prevState
);
activeState
=
prevState
;
}
return
true
;
}
}
};
const
bindInput
=
(
activeEl
)
=>
{
if
(
activeEl
?.
tagName
!==
"
CANVAS
"
&&
activeEl
?.
tagName
!==
"
BODY
"
)
{
for
(
const
evt
of
[
"
change
"
,
"
input
"
,
"
blur
"
])
{
if
(
`on
${
evt
}
`
in
activeEl
)
{
const
listener
=
()
=>
{
checkState
();
activeEl
.
removeEventListener
(
evt
,
listener
);
};
activeEl
.
addEventListener
(
evt
,
listener
);
return
true
;
}
}
}
};
window
.
addEventListener
(
"
keydown
"
,
(
e
)
=>
{
requestAnimationFrame
(
async
()
=>
{
const
activeEl
=
document
.
activeElement
;
if
(
activeEl
?.
tagName
===
"
INPUT
"
||
activeEl
?.
type
===
"
textarea
"
)
{
// Ignore events on inputs, they have their native history
return
;
}
// Check if this is a ctrl+z ctrl+y
if
(
await
undoRedo
(
e
))
return
;
// If our active element is some type of input then handle changes after they're done
if
(
bindInput
(
activeEl
))
return
;
checkState
();
});
},
true
);
// Handle clicking DOM elements (e.g. widgets)
window
.
addEventListener
(
"
mouseup
"
,
()
=>
{
checkState
();
});
// Handle litegraph clicks
const
processMouseUp
=
LGraphCanvas
.
prototype
.
processMouseUp
;
LGraphCanvas
.
prototype
.
processMouseUp
=
function
(
e
)
{
const
v
=
processMouseUp
.
apply
(
this
,
arguments
);
checkState
();
return
v
;
};
const
processMouseDown
=
LGraphCanvas
.
prototype
.
processMouseDown
;
LGraphCanvas
.
prototype
.
processMouseDown
=
function
(
e
)
{
const
v
=
processMouseDown
.
apply
(
this
,
arguments
);
checkState
();
return
v
;
};
web/extensions/core/widgetInputs.js
View file @
c92f3dca
import
{
ComfyWidgets
,
addValueControlWidget
}
from
"
../../scripts/widgets.js
"
;
import
{
ComfyWidgets
,
addValueControlWidget
s
}
from
"
../../scripts/widgets.js
"
;
import
{
app
}
from
"
../../scripts/app.js
"
;
import
{
app
}
from
"
../../scripts/app.js
"
;
const
CONVERTED_TYPE
=
"
converted-widget
"
;
const
CONVERTED_TYPE
=
"
converted-widget
"
;
...
@@ -121,6 +121,110 @@ function isValidCombo(combo, obj) {
...
@@ -121,6 +121,110 @@ function isValidCombo(combo, obj) {
return
true
;
return
true
;
}
}
export
function
mergeIfValid
(
output
,
config2
,
forceUpdate
,
recreateWidget
,
config1
)
{
if
(
!
config1
)
{
config1
=
output
.
widget
[
CONFIG
]
??
output
.
widget
[
GET_CONFIG
]();
}
if
(
config1
[
0
]
instanceof
Array
)
{
if
(
!
isValidCombo
(
config1
[
0
],
config2
[
0
]))
return
false
;
}
else
if
(
config1
[
0
]
!==
config2
[
0
])
{
// Types dont match
console
.
log
(
`connection rejected: types dont match`
,
config1
[
0
],
config2
[
0
]);
return
false
;
}
const
keys
=
new
Set
([...
Object
.
keys
(
config1
[
1
]
??
{}),
...
Object
.
keys
(
config2
[
1
]
??
{})]);
let
customConfig
;
const
getCustomConfig
=
()
=>
{
if
(
!
customConfig
)
{
if
(
typeof
structuredClone
===
"
undefined
"
)
{
customConfig
=
JSON
.
parse
(
JSON
.
stringify
(
config1
[
1
]
??
{}));
}
else
{
customConfig
=
structuredClone
(
config1
[
1
]
??
{});
}
}
return
customConfig
;
};
const
isNumber
=
config1
[
0
]
===
"
INT
"
||
config1
[
0
]
===
"
FLOAT
"
;
for
(
const
k
of
keys
.
values
())
{
if
(
k
!==
"
default
"
&&
k
!==
"
forceInput
"
&&
k
!==
"
defaultInput
"
)
{
let
v1
=
config1
[
1
][
k
];
let
v2
=
config2
[
1
]?.[
k
];
if
(
v1
===
v2
||
(
!
v1
&&
!
v2
))
continue
;
if
(
isNumber
)
{
if
(
k
===
"
min
"
)
{
const
theirMax
=
config2
[
1
]?.[
"
max
"
];
if
(
theirMax
!=
null
&&
v1
>
theirMax
)
{
console
.
log
(
"
connection rejected: min > max
"
,
v1
,
theirMax
);
return
false
;
}
getCustomConfig
()[
k
]
=
v1
==
null
?
v2
:
v2
==
null
?
v1
:
Math
.
max
(
v1
,
v2
);
continue
;
}
else
if
(
k
===
"
max
"
)
{
const
theirMin
=
config2
[
1
]?.[
"
min
"
];
if
(
theirMin
!=
null
&&
v1
<
theirMin
)
{
console
.
log
(
"
connection rejected: max < min
"
,
v1
,
theirMin
);
return
false
;
}
getCustomConfig
()[
k
]
=
v1
==
null
?
v2
:
v2
==
null
?
v1
:
Math
.
min
(
v1
,
v2
);
continue
;
}
else
if
(
k
===
"
step
"
)
{
let
step
;
if
(
v1
==
null
)
{
// No current step
step
=
v2
;
}
else
if
(
v2
==
null
)
{
// No new step
step
=
v1
;
}
else
{
if
(
v1
<
v2
)
{
// Ensure v1 is larger for the mod
const
a
=
v2
;
v2
=
v1
;
v1
=
a
;
}
if
(
v1
%
v2
)
{
console
.
log
(
"
connection rejected: steps not divisible
"
,
"
current:
"
,
v1
,
"
new:
"
,
v2
);
return
false
;
}
step
=
v1
;
}
getCustomConfig
()[
k
]
=
step
;
continue
;
}
}
console
.
log
(
`connection rejected: config
${
k
}
values dont match`
,
v1
,
v2
);
return
false
;
}
}
if
(
customConfig
||
forceUpdate
)
{
if
(
customConfig
)
{
output
.
widget
[
CONFIG
]
=
[
config1
[
0
],
customConfig
];
}
const
widget
=
recreateWidget
?.
call
(
this
);
// When deleting a node this can be null
if
(
widget
)
{
const
min
=
widget
.
options
.
min
;
const
max
=
widget
.
options
.
max
;
if
(
min
!=
null
&&
widget
.
value
<
min
)
widget
.
value
=
min
;
if
(
max
!=
null
&&
widget
.
value
>
max
)
widget
.
value
=
max
;
widget
.
callback
(
widget
.
value
);
}
}
return
{
customConfig
};
}
app
.
registerExtension
({
app
.
registerExtension
({
name
:
"
Comfy.WidgetInputs
"
,
name
:
"
Comfy.WidgetInputs
"
,
async
beforeRegisterNodeDef
(
nodeType
,
nodeData
,
app
)
{
async
beforeRegisterNodeDef
(
nodeType
,
nodeData
,
app
)
{
...
@@ -308,7 +412,7 @@ app.registerExtension({
...
@@ -308,7 +412,7 @@ app.registerExtension({
this
.
isVirtualNode
=
true
;
this
.
isVirtualNode
=
true
;
}
}
applyToGraph
()
{
applyToGraph
(
extraLinks
=
[]
)
{
if
(
!
this
.
outputs
[
0
].
links
?.
length
)
return
;
if
(
!
this
.
outputs
[
0
].
links
?.
length
)
return
;
function
get_links
(
node
)
{
function
get_links
(
node
)
{
...
@@ -325,10 +429,9 @@ app.registerExtension({
...
@@ -325,10 +429,9 @@ app.registerExtension({
return
links
;
return
links
;
}
}
let
links
=
get_links
(
this
);
let
links
=
[...
get_links
(
this
)
.
map
((
l
)
=>
app
.
graph
.
links
[
l
]),
...
extraLinks
]
;
// For each output link copy our value over the original widget value
// For each output link copy our value over the original widget value
for
(
const
l
of
links
)
{
for
(
const
linkInfo
of
links
)
{
const
linkInfo
=
app
.
graph
.
links
[
l
];
const
node
=
this
.
graph
.
getNodeById
(
linkInfo
.
target_id
);
const
node
=
this
.
graph
.
getNodeById
(
linkInfo
.
target_id
);
const
input
=
node
.
inputs
[
linkInfo
.
target_slot
];
const
input
=
node
.
inputs
[
linkInfo
.
target_slot
];
const
widgetName
=
input
.
widget
.
name
;
const
widgetName
=
input
.
widget
.
name
;
...
@@ -405,7 +508,12 @@ app.registerExtension({
...
@@ -405,7 +508,12 @@ app.registerExtension({
}
}
if
(
this
.
outputs
[
slot
].
links
?.
length
)
{
if
(
this
.
outputs
[
slot
].
links
?.
length
)
{
return
this
.
#
isValidConnection
(
input
);
const
valid
=
this
.
#
isValidConnection
(
input
);
if
(
valid
)
{
// On connect of additional outputs, copy our value to their widget
this
.
applyToGraph
([{
target_id
:
target_node
.
id
,
target_slot
}]);
}
return
valid
;
}
}
}
}
...
@@ -462,12 +570,16 @@ app.registerExtension({
...
@@ -462,12 +570,16 @@ app.registerExtension({
}
}
}
}
if
(
widget
.
type
===
"
number
"
||
widget
.
type
===
"
combo
"
)
{
if
(
!
inputData
?.[
1
]?.
control_after_generate
&&
(
widget
.
type
===
"
number
"
||
widget
.
type
===
"
combo
"
)
)
{
let
control_value
=
this
.
widgets_values
?.[
1
];
let
control_value
=
this
.
widgets_values
?.[
1
];
if
(
!
control_value
)
{
if
(
!
control_value
)
{
control_value
=
"
fixed
"
;
control_value
=
"
fixed
"
;
}
}
addValueControlWidget
(
this
,
widget
,
control_value
);
addValueControlWidgets
(
this
,
widget
,
control_value
,
undefined
,
inputData
);
let
filter
=
this
.
widgets_values
?.[
2
];
if
(
filter
&&
this
.
widgets
.
length
===
3
)
{
this
.
widgets
[
2
].
value
=
filter
;
}
}
}
// When our value changes, update other widgets to reflect our changes
// When our value changes, update other widgets to reflect our changes
...
@@ -503,6 +615,7 @@ app.registerExtension({
...
@@ -503,6 +615,7 @@ app.registerExtension({
this
.
#
removeWidgets
();
this
.
#
removeWidgets
();
this
.
#
onFirstConnection
(
true
);
this
.
#
onFirstConnection
(
true
);
for
(
let
i
=
0
;
i
<
this
.
widgets
?.
length
;
i
++
)
this
.
widgets
[
i
].
value
=
values
[
i
];
for
(
let
i
=
0
;
i
<
this
.
widgets
?.
length
;
i
++
)
this
.
widgets
[
i
].
value
=
values
[
i
];
return
this
.
widgets
[
0
];
}
}
#
mergeWidgetConfig
()
{
#
mergeWidgetConfig
()
{
...
@@ -543,108 +656,8 @@ app.registerExtension({
...
@@ -543,108 +656,8 @@ app.registerExtension({
#
isValidConnection
(
input
,
forceUpdate
)
{
#
isValidConnection
(
input
,
forceUpdate
)
{
// Only allow connections where the configs match
// Only allow connections where the configs match
const
output
=
this
.
outputs
[
0
];
const
output
=
this
.
outputs
[
0
];
const
config1
=
output
.
widget
[
CONFIG
]
??
output
.
widget
[
GET_CONFIG
]();
const
config2
=
input
.
widget
[
GET_CONFIG
]();
const
config2
=
input
.
widget
[
GET_CONFIG
]();
return
!!
mergeIfValid
.
call
(
this
,
output
,
config2
,
forceUpdate
,
this
.
#
recreateWidget
);
if
(
config1
[
0
]
instanceof
Array
)
{
if
(
!
isValidCombo
(
config1
[
0
],
config2
[
0
]))
return
false
;
}
else
if
(
config1
[
0
]
!==
config2
[
0
])
{
// Types dont match
console
.
log
(
`connection rejected: types dont match`
,
config1
[
0
],
config2
[
0
]);
return
false
;
}
const
keys
=
new
Set
([...
Object
.
keys
(
config1
[
1
]
??
{}),
...
Object
.
keys
(
config2
[
1
]
??
{})]);
let
customConfig
;
const
getCustomConfig
=
()
=>
{
if
(
!
customConfig
)
{
if
(
typeof
structuredClone
===
"
undefined
"
)
{
customConfig
=
JSON
.
parse
(
JSON
.
stringify
(
config1
[
1
]
??
{}));
}
else
{
customConfig
=
structuredClone
(
config1
[
1
]
??
{});
}
}
return
customConfig
;
};
const
isNumber
=
config1
[
0
]
===
"
INT
"
||
config1
[
0
]
===
"
FLOAT
"
;
for
(
const
k
of
keys
.
values
())
{
if
(
k
!==
"
default
"
&&
k
!==
"
forceInput
"
&&
k
!==
"
defaultInput
"
)
{
let
v1
=
config1
[
1
][
k
];
let
v2
=
config2
[
1
][
k
];
if
(
v1
===
v2
||
(
!
v1
&&
!
v2
))
continue
;
if
(
isNumber
)
{
if
(
k
===
"
min
"
)
{
const
theirMax
=
config2
[
1
][
"
max
"
];
if
(
theirMax
!=
null
&&
v1
>
theirMax
)
{
console
.
log
(
"
connection rejected: min > max
"
,
v1
,
theirMax
);
return
false
;
}
getCustomConfig
()[
k
]
=
v1
==
null
?
v2
:
v2
==
null
?
v1
:
Math
.
max
(
v1
,
v2
);
continue
;
}
else
if
(
k
===
"
max
"
)
{
const
theirMin
=
config2
[
1
][
"
min
"
];
if
(
theirMin
!=
null
&&
v1
<
theirMin
)
{
console
.
log
(
"
connection rejected: max < min
"
,
v1
,
theirMin
);
return
false
;
}
getCustomConfig
()[
k
]
=
v1
==
null
?
v2
:
v2
==
null
?
v1
:
Math
.
min
(
v1
,
v2
);
continue
;
}
else
if
(
k
===
"
step
"
)
{
let
step
;
if
(
v1
==
null
)
{
// No current step
step
=
v2
;
}
else
if
(
v2
==
null
)
{
// No new step
step
=
v1
;
}
else
{
if
(
v1
<
v2
)
{
// Ensure v1 is larger for the mod
const
a
=
v2
;
v2
=
v1
;
v1
=
a
;
}
if
(
v1
%
v2
)
{
console
.
log
(
"
connection rejected: steps not divisible
"
,
"
current:
"
,
v1
,
"
new:
"
,
v2
);
return
false
;
}
step
=
v1
;
}
getCustomConfig
()[
k
]
=
step
;
continue
;
}
}
console
.
log
(
`connection rejected: config
${
k
}
values dont match`
,
v1
,
v2
);
return
false
;
}
}
if
(
customConfig
||
forceUpdate
)
{
if
(
customConfig
)
{
output
.
widget
[
CONFIG
]
=
[
config1
[
0
],
customConfig
];
}
this
.
#
recreateWidget
();
const
widget
=
this
.
widgets
[
0
];
// When deleting a node this can be null
if
(
widget
)
{
const
min
=
widget
.
options
.
min
;
const
max
=
widget
.
options
.
max
;
if
(
min
!=
null
&&
widget
.
value
<
min
)
widget
.
value
=
min
;
if
(
max
!=
null
&&
widget
.
value
>
max
)
widget
.
value
=
max
;
widget
.
callback
(
widget
.
value
);
}
}
return
true
;
}
}
#
removeWidgets
()
{
#
removeWidgets
()
{
...
...
web/lib/litegraph.core.js
View file @
c92f3dca
...
@@ -2533,7 +2533,7 @@
...
@@ -2533,7 +2533,7 @@
var
w
=
this
.
widgets
[
i
];
var
w
=
this
.
widgets
[
i
];
if
(
!
w
)
if
(
!
w
)
continue
;
continue
;
if
(
w
.
options
&&
w
.
options
.
property
&&
this
.
properties
[
w
.
options
.
property
])
if
(
w
.
options
&&
w
.
options
.
property
&&
(
this
.
properties
[
w
.
options
.
property
]
!=
undefined
)
)
w
.
value
=
JSON
.
parse
(
JSON
.
stringify
(
this
.
properties
[
w
.
options
.
property
]
)
);
w
.
value
=
JSON
.
parse
(
JSON
.
stringify
(
this
.
properties
[
w
.
options
.
property
]
)
);
}
}
if
(
info
.
widgets_values
)
{
if
(
info
.
widgets_values
)
{
...
@@ -5714,10 +5714,10 @@ LGraphNode.prototype.executeAction = function(action)
...
@@ -5714,10 +5714,10 @@ LGraphNode.prototype.executeAction = function(action)
* @method enableWebGL
* @method enableWebGL
**/
**/
LGraphCanvas
.
prototype
.
enableWebGL
=
function
()
{
LGraphCanvas
.
prototype
.
enableWebGL
=
function
()
{
if
(
typeof
GL
===
undefined
)
{
if
(
typeof
GL
===
"
undefined
"
)
{
throw
"
litegl.js must be included to use a WebGL canvas
"
;
throw
"
litegl.js must be included to use a WebGL canvas
"
;
}
}
if
(
typeof
enableWebGLCanvas
===
undefined
)
{
if
(
typeof
enableWebGLCanvas
===
"
undefined
"
)
{
throw
"
webglCanvas.js must be included to use this feature
"
;
throw
"
webglCanvas.js must be included to use this feature
"
;
}
}
...
@@ -7110,15 +7110,16 @@ LGraphNode.prototype.executeAction = function(action)
...
@@ -7110,15 +7110,16 @@ LGraphNode.prototype.executeAction = function(action)
}
}
};
};
LGraphCanvas
.
prototype
.
copyToClipboard
=
function
()
{
LGraphCanvas
.
prototype
.
copyToClipboard
=
function
(
nodes
)
{
var
clipboard_info
=
{
var
clipboard_info
=
{
nodes
:
[],
nodes
:
[],
links
:
[]
links
:
[]
};
};
var
index
=
0
;
var
index
=
0
;
var
selected_nodes_array
=
[];
var
selected_nodes_array
=
[];
for
(
var
i
in
this
.
selected_nodes
)
{
if
(
!
nodes
)
nodes
=
this
.
selected_nodes
;
var
node
=
this
.
selected_nodes
[
i
];
for
(
var
i
in
nodes
)
{
var
node
=
nodes
[
i
];
if
(
node
.
clonable
===
false
)
if
(
node
.
clonable
===
false
)
continue
;
continue
;
node
.
_relative_id
=
index
;
node
.
_relative_id
=
index
;
...
@@ -11702,7 +11703,7 @@ LGraphNode.prototype.executeAction = function(action)
...
@@ -11702,7 +11703,7 @@ LGraphNode.prototype.executeAction = function(action)
default
:
default
:
iS
=
0
;
// try with first if no name set
iS
=
0
;
// try with first if no name set
}
}
if
(
typeof
options
.
node_from
.
outputs
[
iS
]
!==
undefined
){
if
(
typeof
options
.
node_from
.
outputs
[
iS
]
!==
"
undefined
"
){
if
(
iS
!==
false
&&
iS
>-
1
){
if
(
iS
!==
false
&&
iS
>-
1
){
options
.
node_from
.
connectByType
(
iS
,
node
,
options
.
node_from
.
outputs
[
iS
].
type
);
options
.
node_from
.
connectByType
(
iS
,
node
,
options
.
node_from
.
outputs
[
iS
].
type
);
}
}
...
@@ -11730,7 +11731,7 @@ LGraphNode.prototype.executeAction = function(action)
...
@@ -11730,7 +11731,7 @@ LGraphNode.prototype.executeAction = function(action)
default
:
default
:
iS
=
0
;
// try with first if no name set
iS
=
0
;
// try with first if no name set
}
}
if
(
typeof
options
.
node_to
.
inputs
[
iS
]
!==
undefined
){
if
(
typeof
options
.
node_to
.
inputs
[
iS
]
!==
"
undefined
"
){
if
(
iS
!==
false
&&
iS
>-
1
){
if
(
iS
!==
false
&&
iS
>-
1
){
// try connection
// try connection
options
.
node_to
.
connectByTypeOutput
(
iS
,
node
,
options
.
node_to
.
inputs
[
iS
].
type
);
options
.
node_to
.
connectByTypeOutput
(
iS
,
node
,
options
.
node_to
.
inputs
[
iS
].
type
);
...
...
web/scripts/api.js
View file @
c92f3dca
...
@@ -254,9 +254,9 @@ class ComfyApi extends EventTarget {
...
@@ -254,9 +254,9 @@ class ComfyApi extends EventTarget {
* Gets the prompt execution history
* Gets the prompt execution history
* @returns Prompt history including node outputs
* @returns Prompt history including node outputs
*/
*/
async
getHistory
()
{
async
getHistory
(
max_items
=
200
)
{
try
{
try
{
const
res
=
await
this
.
fetchApi
(
"
/history
"
);
const
res
=
await
this
.
fetchApi
(
`
/history
?max_items=
${
max_items
}
`
);
return
{
History
:
Object
.
values
(
await
res
.
json
())
};
return
{
History
:
Object
.
values
(
await
res
.
json
())
};
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
error
(
error
);
console
.
error
(
error
);
...
...
web/scripts/app.js
View file @
c92f3dca
...
@@ -4,7 +4,10 @@ import { ComfyUI, $el } from "./ui.js";
...
@@ -4,7 +4,10 @@ import { ComfyUI, $el } from "./ui.js";
import
{
api
}
from
"
./api.js
"
;
import
{
api
}
from
"
./api.js
"
;
import
{
defaultGraph
}
from
"
./defaultGraph.js
"
;
import
{
defaultGraph
}
from
"
./defaultGraph.js
"
;
import
{
getPngMetadata
,
getWebpMetadata
,
importA1111
,
getLatentMetadata
}
from
"
./pnginfo.js
"
;
import
{
getPngMetadata
,
getWebpMetadata
,
importA1111
,
getLatentMetadata
}
from
"
./pnginfo.js
"
;
import
{
addDomClippingSetting
}
from
"
./domWidget.js
"
;
import
{
createImageHost
,
calculateImageGrid
}
from
"
./ui/imagePreview.js
"
export
const
ANIM_PREVIEW_WIDGET
=
"
$$comfy_animation_preview
"
function
sanitizeNodeName
(
string
)
{
function
sanitizeNodeName
(
string
)
{
let
entityMap
=
{
let
entityMap
=
{
...
@@ -409,7 +412,9 @@ export class ComfyApp {
...
@@ -409,7 +412,9 @@ export class ComfyApp {
return
shiftY
;
return
shiftY
;
}
}
node
.
prototype
.
setSizeForImage
=
function
()
{
node
.
prototype
.
setSizeForImage
=
function
(
force
)
{
if
(
!
force
&&
this
.
animatedImages
)
return
;
if
(
this
.
inputHeight
)
{
if
(
this
.
inputHeight
)
{
this
.
setSize
(
this
.
size
);
this
.
setSize
(
this
.
size
);
return
;
return
;
...
@@ -426,13 +431,20 @@ export class ComfyApp {
...
@@ -426,13 +431,20 @@ export class ComfyApp {
let
imagesChanged
=
false
let
imagesChanged
=
false
const
output
=
app
.
nodeOutputs
[
this
.
id
+
""
];
const
output
=
app
.
nodeOutputs
[
this
.
id
+
""
];
if
(
output
&&
output
.
images
)
{
if
(
output
?.
images
)
{
this
.
animatedImages
=
output
?.
animated
?.
find
(
Boolean
);
if
(
this
.
images
!==
output
.
images
)
{
if
(
this
.
images
!==
output
.
images
)
{
this
.
images
=
output
.
images
;
this
.
images
=
output
.
images
;
imagesChanged
=
true
;
imagesChanged
=
true
;
imgURLs
=
imgURLs
.
concat
(
output
.
images
.
map
(
params
=>
{
imgURLs
=
imgURLs
.
concat
(
return
api
.
apiURL
(
"
/view?
"
+
new
URLSearchParams
(
params
).
toString
()
+
app
.
getPreviewFormatParam
()
+
app
.
getRandParam
());
output
.
images
.
map
((
params
)
=>
{
}))
return
api
.
apiURL
(
"
/view?
"
+
new
URLSearchParams
(
params
).
toString
()
+
(
this
.
animatedImages
?
""
:
app
.
getPreviewFormatParam
())
+
app
.
getRandParam
()
);
})
);
}
}
}
}
...
@@ -511,7 +523,35 @@ export class ComfyApp {
...
@@ -511,7 +523,35 @@ export class ComfyApp {
return
true
;
return
true
;
}
}
if
(
this
.
imgs
&&
this
.
imgs
.
length
)
{
if
(
this
.
imgs
?.
length
)
{
const
widgetIdx
=
this
.
widgets
?.
findIndex
((
w
)
=>
w
.
name
===
ANIM_PREVIEW_WIDGET
);
if
(
this
.
animatedImages
)
{
// Instead of using the canvas we'll use a IMG
if
(
widgetIdx
>
-
1
)
{
// Replace content
const
widget
=
this
.
widgets
[
widgetIdx
];
widget
.
options
.
host
.
updateImages
(
this
.
imgs
);
}
else
{
const
host
=
createImageHost
(
this
);
this
.
setSizeForImage
(
true
);
const
widget
=
this
.
addDOMWidget
(
ANIM_PREVIEW_WIDGET
,
"
img
"
,
host
.
el
,
{
host
,
getHeight
:
host
.
getHeight
,
onDraw
:
host
.
onDraw
,
hideOnZoom
:
false
});
widget
.
serializeValue
=
()
=>
undefined
;
widget
.
options
.
host
.
updateImages
(
this
.
imgs
);
}
return
;
}
if
(
widgetIdx
>
-
1
)
{
this
.
widgets
[
widgetIdx
].
onRemove
?.();
this
.
widgets
.
splice
(
widgetIdx
,
1
);
}
const
canvas
=
app
.
graph
.
list_of_graphcanvas
[
0
];
const
canvas
=
app
.
graph
.
list_of_graphcanvas
[
0
];
const
mouse
=
canvas
.
graph_mouse
;
const
mouse
=
canvas
.
graph_mouse
;
if
(
!
canvas
.
pointer_is_down
&&
this
.
pointerDown
)
{
if
(
!
canvas
.
pointer_is_down
&&
this
.
pointerDown
)
{
...
@@ -551,31 +591,7 @@ export class ComfyApp {
...
@@ -551,31 +591,7 @@ export class ComfyApp {
}
}
else
{
else
{
cell_padding
=
0
;
cell_padding
=
0
;
let
best
=
0
;
({
cellWidth
,
cellHeight
,
cols
,
shiftX
}
=
calculateImageGrid
(
this
.
imgs
,
dw
,
dh
));
let
w
=
this
.
imgs
[
0
].
naturalWidth
;
let
h
=
this
.
imgs
[
0
].
naturalHeight
;
// compact style
for
(
let
c
=
1
;
c
<=
numImages
;
c
++
)
{
const
rows
=
Math
.
ceil
(
numImages
/
c
);
const
cW
=
dw
/
c
;
const
cH
=
dh
/
rows
;
const
scaleX
=
cW
/
w
;
const
scaleY
=
cH
/
h
;
const
scale
=
Math
.
min
(
scaleX
,
scaleY
,
1
);
const
imageW
=
w
*
scale
;
const
imageH
=
h
*
scale
;
const
area
=
imageW
*
imageH
*
numImages
;
if
(
area
>
best
)
{
best
=
area
;
cellWidth
=
imageW
;
cellHeight
=
imageH
;
cols
=
c
;
shiftX
=
c
*
((
cW
-
imageW
)
/
2
);
}
}
}
}
let
anyHovered
=
false
;
let
anyHovered
=
false
;
...
@@ -767,7 +783,7 @@ export class ComfyApp {
...
@@ -767,7 +783,7 @@ export class ComfyApp {
* Adds a handler on paste that extracts and loads images or workflows from pasted JSON data
* Adds a handler on paste that extracts and loads images or workflows from pasted JSON data
*/
*/
#
addPasteHandler
()
{
#
addPasteHandler
()
{
document
.
addEventListener
(
"
paste
"
,
(
e
)
=>
{
document
.
addEventListener
(
"
paste
"
,
async
(
e
)
=>
{
// ctrl+shift+v is used to paste nodes with connections
// ctrl+shift+v is used to paste nodes with connections
// this is handled by litegraph
// this is handled by litegraph
if
(
this
.
shiftDown
)
return
;
if
(
this
.
shiftDown
)
return
;
...
@@ -815,7 +831,7 @@ export class ComfyApp {
...
@@ -815,7 +831,7 @@ export class ComfyApp {
}
}
if
(
workflow
&&
workflow
.
version
&&
workflow
.
nodes
&&
workflow
.
extra
)
{
if
(
workflow
&&
workflow
.
version
&&
workflow
.
nodes
&&
workflow
.
extra
)
{
this
.
loadGraphData
(
workflow
);
await
this
.
loadGraphData
(
workflow
);
}
}
else
{
else
{
if
(
e
.
target
.
type
===
"
text
"
||
e
.
target
.
type
===
"
textarea
"
)
{
if
(
e
.
target
.
type
===
"
text
"
||
e
.
target
.
type
===
"
textarea
"
)
{
...
@@ -1165,7 +1181,19 @@ export class ComfyApp {
...
@@ -1165,7 +1181,19 @@ export class ComfyApp {
});
});
api
.
addEventListener
(
"
executed
"
,
({
detail
})
=>
{
api
.
addEventListener
(
"
executed
"
,
({
detail
})
=>
{
this
.
nodeOutputs
[
detail
.
node
]
=
detail
.
output
;
const
output
=
this
.
nodeOutputs
[
detail
.
node
];
if
(
detail
.
merge
&&
output
)
{
for
(
const
k
in
detail
.
output
??
{})
{
const
v
=
output
[
k
];
if
(
v
instanceof
Array
)
{
output
[
k
]
=
v
.
concat
(
detail
.
output
[
k
]);
}
else
{
output
[
k
]
=
detail
.
output
[
k
];
}
}
}
else
{
this
.
nodeOutputs
[
detail
.
node
]
=
detail
.
output
;
}
const
node
=
this
.
graph
.
getNodeById
(
detail
.
node
);
const
node
=
this
.
graph
.
getNodeById
(
detail
.
node
);
if
(
node
)
{
if
(
node
)
{
if
(
node
.
onExecuted
)
if
(
node
.
onExecuted
)
...
@@ -1276,9 +1304,11 @@ export class ComfyApp {
...
@@ -1276,9 +1304,11 @@ export class ComfyApp {
canvasEl
.
tabIndex
=
"
1
"
;
canvasEl
.
tabIndex
=
"
1
"
;
document
.
body
.
prepend
(
canvasEl
);
document
.
body
.
prepend
(
canvasEl
);
addDomClippingSetting
();
this
.
#
addProcessMouseHandler
();
this
.
#
addProcessMouseHandler
();
this
.
#
addProcessKeyHandler
();
this
.
#
addProcessKeyHandler
();
this
.
#
addConfigureHandler
();
this
.
#
addConfigureHandler
();
this
.
#
addApiUpdateHandlers
();
this
.
graph
=
new
LGraph
();
this
.
graph
=
new
LGraph
();
...
@@ -1315,7 +1345,7 @@ export class ComfyApp {
...
@@ -1315,7 +1345,7 @@ export class ComfyApp {
const
json
=
localStorage
.
getItem
(
"
workflow
"
);
const
json
=
localStorage
.
getItem
(
"
workflow
"
);
if
(
json
)
{
if
(
json
)
{
const
workflow
=
JSON
.
parse
(
json
);
const
workflow
=
JSON
.
parse
(
json
);
this
.
loadGraphData
(
workflow
);
await
this
.
loadGraphData
(
workflow
);
restored
=
true
;
restored
=
true
;
}
}
}
catch
(
err
)
{
}
catch
(
err
)
{
...
@@ -1324,7 +1354,7 @@ export class ComfyApp {
...
@@ -1324,7 +1354,7 @@ export class ComfyApp {
// We failed to restore a workflow so load the default
// We failed to restore a workflow so load the default
if
(
!
restored
)
{
if
(
!
restored
)
{
this
.
loadGraphData
();
await
this
.
loadGraphData
();
}
}
// Save current workflow automatically
// Save current workflow automatically
...
@@ -1332,7 +1362,6 @@ export class ComfyApp {
...
@@ -1332,7 +1362,6 @@ export class ComfyApp {
this
.
#
addDrawNodeHandler
();
this
.
#
addDrawNodeHandler
();
this
.
#
addDrawGroupsHandler
();
this
.
#
addDrawGroupsHandler
();
this
.
#
addApiUpdateHandlers
();
this
.
#
addDropHandler
();
this
.
#
addDropHandler
();
this
.
#
addCopyHandler
();
this
.
#
addCopyHandler
();
this
.
#
addPasteHandler
();
this
.
#
addPasteHandler
();
...
@@ -1352,11 +1381,95 @@ export class ComfyApp {
...
@@ -1352,11 +1381,95 @@ export class ComfyApp {
await
this
.
#
invokeExtensionsAsync
(
"
registerCustomNodes
"
);
await
this
.
#
invokeExtensionsAsync
(
"
registerCustomNodes
"
);
}
}
getWidgetType
(
inputData
,
inputName
)
{
const
type
=
inputData
[
0
];
if
(
Array
.
isArray
(
type
))
{
return
"
COMBO
"
;
}
else
if
(
`
${
type
}
:
${
inputName
}
`
in
this
.
widgets
)
{
return
`
${
type
}
:
${
inputName
}
`
;
}
else
if
(
type
in
this
.
widgets
)
{
return
type
;
}
else
{
return
null
;
}
}
async
registerNodeDef
(
nodeId
,
nodeData
)
{
const
self
=
this
;
const
node
=
Object
.
assign
(
function
ComfyNode
()
{
var
inputs
=
nodeData
[
"
input
"
][
"
required
"
];
if
(
nodeData
[
"
input
"
][
"
optional
"
]
!=
undefined
)
{
inputs
=
Object
.
assign
({},
nodeData
[
"
input
"
][
"
required
"
],
nodeData
[
"
input
"
][
"
optional
"
]);
}
const
config
=
{
minWidth
:
1
,
minHeight
:
1
};
for
(
const
inputName
in
inputs
)
{
const
inputData
=
inputs
[
inputName
];
const
type
=
inputData
[
0
];
let
widgetCreated
=
true
;
const
widgetType
=
self
.
getWidgetType
(
inputData
,
inputName
);
if
(
widgetType
)
{
if
(
widgetType
===
"
COMBO
"
)
{
Object
.
assign
(
config
,
self
.
widgets
.
COMBO
(
this
,
inputName
,
inputData
,
app
)
||
{});
}
else
{
Object
.
assign
(
config
,
self
.
widgets
[
widgetType
](
this
,
inputName
,
inputData
,
app
)
||
{});
}
}
else
{
// Node connection inputs
this
.
addInput
(
inputName
,
type
);
widgetCreated
=
false
;
}
if
(
widgetCreated
&&
inputData
[
1
]?.
forceInput
&&
config
?.
widget
)
{
if
(
!
config
.
widget
.
options
)
config
.
widget
.
options
=
{};
config
.
widget
.
options
.
forceInput
=
inputData
[
1
].
forceInput
;
}
if
(
widgetCreated
&&
inputData
[
1
]?.
defaultInput
&&
config
?.
widget
)
{
if
(
!
config
.
widget
.
options
)
config
.
widget
.
options
=
{};
config
.
widget
.
options
.
defaultInput
=
inputData
[
1
].
defaultInput
;
}
}
for
(
const
o
in
nodeData
[
"
output
"
])
{
let
output
=
nodeData
[
"
output
"
][
o
];
if
(
output
instanceof
Array
)
output
=
"
COMBO
"
;
const
outputName
=
nodeData
[
"
output_name
"
][
o
]
||
output
;
const
outputShape
=
nodeData
[
"
output_is_list
"
][
o
]
?
LiteGraph
.
GRID_SHAPE
:
LiteGraph
.
CIRCLE_SHAPE
;
this
.
addOutput
(
outputName
,
output
,
{
shape
:
outputShape
});
}
const
s
=
this
.
computeSize
();
s
[
0
]
=
Math
.
max
(
config
.
minWidth
,
s
[
0
]
*
1.5
);
s
[
1
]
=
Math
.
max
(
config
.
minHeight
,
s
[
1
]);
this
.
size
=
s
;
this
.
serialize_widgets
=
true
;
app
.
#
invokeExtensionsAsync
(
"
nodeCreated
"
,
this
);
},
{
title
:
nodeData
.
display_name
||
nodeData
.
name
,
comfyClass
:
nodeData
.
name
,
nodeData
}
);
node
.
prototype
.
comfyClass
=
nodeData
.
name
;
this
.
#
addNodeContextMenuHandler
(
node
);
this
.
#
addDrawBackgroundHandler
(
node
,
app
);
this
.
#
addNodeKeyHandler
(
node
);
await
this
.
#
invokeExtensionsAsync
(
"
beforeRegisterNodeDef
"
,
node
,
nodeData
);
LiteGraph
.
registerNodeType
(
nodeId
,
node
);
node
.
category
=
nodeData
.
category
;
}
async
registerNodesFromDefs
(
defs
)
{
async
registerNodesFromDefs
(
defs
)
{
await
this
.
#
invokeExtensionsAsync
(
"
addCustomNodeDefs
"
,
defs
);
await
this
.
#
invokeExtensionsAsync
(
"
addCustomNodeDefs
"
,
defs
);
// Generate list of known widgets
// Generate list of known widgets
const
widgets
=
Object
.
assign
(
this
.
widgets
=
Object
.
assign
(
{},
{},
ComfyWidgets
,
ComfyWidgets
,
...(
await
this
.
#
invokeExtensionsAsync
(
"
getCustomWidgets
"
)).
filter
(
Boolean
)
...(
await
this
.
#
invokeExtensionsAsync
(
"
getCustomWidgets
"
)).
filter
(
Boolean
)
...
@@ -1364,75 +1477,7 @@ export class ComfyApp {
...
@@ -1364,75 +1477,7 @@ export class ComfyApp {
// Register a node for each definition
// Register a node for each definition
for
(
const
nodeId
in
defs
)
{
for
(
const
nodeId
in
defs
)
{
const
nodeData
=
defs
[
nodeId
];
this
.
registerNodeDef
(
nodeId
,
defs
[
nodeId
]);
const
node
=
Object
.
assign
(
function
ComfyNode
()
{
var
inputs
=
nodeData
[
"
input
"
][
"
required
"
];
if
(
nodeData
[
"
input
"
][
"
optional
"
]
!=
undefined
){
inputs
=
Object
.
assign
({},
nodeData
[
"
input
"
][
"
required
"
],
nodeData
[
"
input
"
][
"
optional
"
])
}
const
config
=
{
minWidth
:
1
,
minHeight
:
1
};
for
(
const
inputName
in
inputs
)
{
const
inputData
=
inputs
[
inputName
];
const
type
=
inputData
[
0
];
let
widgetCreated
=
true
;
if
(
Array
.
isArray
(
type
))
{
// Enums
Object
.
assign
(
config
,
widgets
.
COMBO
(
this
,
inputName
,
inputData
,
app
)
||
{});
}
else
if
(
`
${
type
}
:
${
inputName
}
`
in
widgets
)
{
// Support custom widgets by Type:Name
Object
.
assign
(
config
,
widgets
[
`
${
type
}
:
${
inputName
}
`
](
this
,
inputName
,
inputData
,
app
)
||
{});
}
else
if
(
type
in
widgets
)
{
// Standard type widgets
Object
.
assign
(
config
,
widgets
[
type
](
this
,
inputName
,
inputData
,
app
)
||
{});
}
else
{
// Node connection inputs
this
.
addInput
(
inputName
,
type
);
widgetCreated
=
false
;
}
if
(
widgetCreated
&&
inputData
[
1
]?.
forceInput
&&
config
?.
widget
)
{
if
(
!
config
.
widget
.
options
)
config
.
widget
.
options
=
{};
config
.
widget
.
options
.
forceInput
=
inputData
[
1
].
forceInput
;
}
if
(
widgetCreated
&&
inputData
[
1
]?.
defaultInput
&&
config
?.
widget
)
{
if
(
!
config
.
widget
.
options
)
config
.
widget
.
options
=
{};
config
.
widget
.
options
.
defaultInput
=
inputData
[
1
].
defaultInput
;
}
}
for
(
const
o
in
nodeData
[
"
output
"
])
{
let
output
=
nodeData
[
"
output
"
][
o
];
if
(
output
instanceof
Array
)
output
=
"
COMBO
"
;
const
outputName
=
nodeData
[
"
output_name
"
][
o
]
||
output
;
const
outputShape
=
nodeData
[
"
output_is_list
"
][
o
]
?
LiteGraph
.
GRID_SHAPE
:
LiteGraph
.
CIRCLE_SHAPE
;
this
.
addOutput
(
outputName
,
output
,
{
shape
:
outputShape
});
}
const
s
=
this
.
computeSize
();
s
[
0
]
=
Math
.
max
(
config
.
minWidth
,
s
[
0
]
*
1.5
);
s
[
1
]
=
Math
.
max
(
config
.
minHeight
,
s
[
1
]);
this
.
size
=
s
;
this
.
serialize_widgets
=
true
;
app
.
#
invokeExtensionsAsync
(
"
nodeCreated
"
,
this
);
},
{
title
:
nodeData
.
display_name
||
nodeData
.
name
,
comfyClass
:
nodeData
.
name
,
nodeData
}
);
node
.
prototype
.
comfyClass
=
nodeData
.
name
;
this
.
#
addNodeContextMenuHandler
(
node
);
this
.
#
addDrawBackgroundHandler
(
node
,
app
);
this
.
#
addNodeKeyHandler
(
node
);
await
this
.
#
invokeExtensionsAsync
(
"
beforeRegisterNodeDef
"
,
node
,
nodeData
);
LiteGraph
.
registerNodeType
(
nodeId
,
node
);
node
.
category
=
nodeData
.
category
;
}
}
}
}
...
@@ -1475,9 +1520,14 @@ export class ComfyApp {
...
@@ -1475,9 +1520,14 @@ export class ComfyApp {
showMissingNodesError
(
missingNodeTypes
,
hasAddedNodes
=
true
)
{
showMissingNodesError
(
missingNodeTypes
,
hasAddedNodes
=
true
)
{
this
.
ui
.
dialog
.
show
(
this
.
ui
.
dialog
.
show
(
`When loading the graph, the following node types were not found: <ul>
${
Array
.
from
(
new
Set
(
missingNodeTypes
)).
map
(
$el
(
"
div
"
,
[
(
t
)
=>
`<li>
${
t
}
</li>`
$el
(
"
span
"
,
{
textContent
:
"
When loading the graph, the following node types were not found:
"
}),
).
join
(
""
)}
</ul>
${
hasAddedNodes
?
"
Nodes that have failed to load will show as red on the graph.
"
:
""
}
`
$el
(
"
ul
"
,
Array
.
from
(
new
Set
(
missingNodeTypes
)).
map
((
t
)
=>
$el
(
"
li
"
,
{
textContent
:
t
}))
),
...(
hasAddedNodes
?
[
$el
(
"
span
"
,
{
textContent
:
"
Nodes that have failed to load will show as red on the graph.
"
})]
:
[]),
])
);
);
this
.
logging
.
addEntry
(
"
Comfy.App
"
,
"
warn
"
,
{
this
.
logging
.
addEntry
(
"
Comfy.App
"
,
"
warn
"
,
{
MissingNodes
:
missingNodeTypes
,
MissingNodes
:
missingNodeTypes
,
...
@@ -1488,31 +1538,35 @@ export class ComfyApp {
...
@@ -1488,31 +1538,35 @@ export class ComfyApp {
* Populates the graph with the specified workflow data
* Populates the graph with the specified workflow data
* @param {*} graphData A serialized graph object
* @param {*} graphData A serialized graph object
*/
*/
loadGraphData
(
graphData
)
{
async
loadGraphData
(
graphData
)
{
this
.
clean
();
this
.
clean
();
let
reset_invalid_values
=
false
;
let
reset_invalid_values
=
false
;
if
(
!
graphData
)
{
if
(
!
graphData
)
{
if
(
typeof
structuredClone
===
"
undefined
"
)
graphData
=
defaultGraph
;
{
graphData
=
JSON
.
parse
(
JSON
.
stringify
(
defaultGraph
));
}
else
{
graphData
=
structuredClone
(
defaultGraph
);
}
reset_invalid_values
=
true
;
reset_invalid_values
=
true
;
}
}
if
(
typeof
structuredClone
===
"
undefined
"
)
{
graphData
=
JSON
.
parse
(
JSON
.
stringify
(
graphData
));
}
else
{
graphData
=
structuredClone
(
graphData
);
}
const
missingNodeTypes
=
[];
const
missingNodeTypes
=
[];
await
this
.
#
invokeExtensionsAsync
(
"
beforeConfigureGraph
"
,
graphData
,
missingNodeTypes
);
for
(
let
n
of
graphData
.
nodes
)
{
for
(
let
n
of
graphData
.
nodes
)
{
// Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
// Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
if
(
n
.
type
==
"
T2IAdapterLoader
"
)
n
.
type
=
"
ControlNetLoader
"
;
if
(
n
.
type
==
"
T2IAdapterLoader
"
)
n
.
type
=
"
ControlNetLoader
"
;
if
(
n
.
type
==
"
ConditioningAverage
"
)
n
.
type
=
"
ConditioningAverage
"
;
//typo fix
if
(
n
.
type
==
"
ConditioningAverage
"
)
n
.
type
=
"
ConditioningAverage
"
;
//typo fix
if
(
n
.
type
==
"
SDV_img2vid_Conditioning
"
)
n
.
type
=
"
SVD_img2vid_Conditioning
"
;
//typo fix
// Find missing node types
// Find missing node types
if
(
!
(
n
.
type
in
LiteGraph
.
registered_node_types
))
{
if
(
!
(
n
.
type
in
LiteGraph
.
registered_node_types
))
{
n
.
type
=
sanitizeNodeName
(
n
.
type
);
missingNodeTypes
.
push
(
n
.
type
);
missingNodeTypes
.
push
(
n
.
type
);
n
.
type
=
sanitizeNodeName
(
n
.
type
);
}
}
}
}
...
@@ -1604,6 +1658,7 @@ export class ComfyApp {
...
@@ -1604,6 +1658,7 @@ export class ComfyApp {
if
(
missingNodeTypes
.
length
)
{
if
(
missingNodeTypes
.
length
)
{
this
.
showMissingNodesError
(
missingNodeTypes
);
this
.
showMissingNodesError
(
missingNodeTypes
);
}
}
await
this
.
#
invokeExtensionsAsync
(
"
afterConfigureGraph
"
,
missingNodeTypes
);
}
}
/**
/**
...
@@ -1611,92 +1666,98 @@ export class ComfyApp {
...
@@ -1611,92 +1666,98 @@ export class ComfyApp {
* @returns The workflow and node links
* @returns The workflow and node links
*/
*/
async
graphToPrompt
()
{
async
graphToPrompt
()
{
for
(
const
node
of
this
.
graph
.
computeExecutionOrder
(
false
))
{
for
(
const
outerNode
of
this
.
graph
.
computeExecutionOrder
(
false
))
{
if
(
node
.
isVirtualNode
)
{
const
innerNodes
=
outerNode
.
getInnerNodes
?
outerNode
.
getInnerNodes
()
:
[
outerNode
];
// Don't serialize frontend only nodes but let them make changes
for
(
const
node
of
innerNodes
)
{
if
(
node
.
applyToGraph
)
{
if
(
node
.
isVirtualNode
)
{
node
.
applyToGraph
();
// Don't serialize frontend only nodes but let them make changes
if
(
node
.
applyToGraph
)
{
node
.
applyToGraph
();
}
}
}
continue
;
}
}
}
}
const
workflow
=
this
.
graph
.
serialize
();
const
workflow
=
this
.
graph
.
serialize
();
const
output
=
{};
const
output
=
{};
// Process nodes in order of execution
// Process nodes in order of execution
for
(
const
n
ode
of
this
.
graph
.
computeExecutionOrder
(
false
))
{
for
(
const
outerN
ode
of
this
.
graph
.
computeExecutionOrder
(
false
))
{
const
n
=
workflow
.
nodes
.
find
((
n
)
=>
n
.
id
===
node
.
id
)
;
const
innerNodes
=
outerNode
.
getInnerNodes
?
outerNode
.
getInnerNodes
()
:
[
outerNode
]
;
for
(
const
node
of
innerNodes
)
{
if
(
node
.
isVirtualNode
)
{
if
(
node
.
isVirtualNode
)
{
continue
;
continue
;
}
}
if
(
node
.
mode
===
2
||
node
.
mode
===
4
)
{
if
(
node
.
mode
===
2
||
node
.
mode
===
4
)
{
// Don't serialize muted nodes
// Don't serialize muted nodes
continue
;
continue
;
}
}
const
inputs
=
{};
const
inputs
=
{};
const
widgets
=
node
.
widgets
;
const
widgets
=
node
.
widgets
;
// Store all widget values
// Store all widget values
if
(
widgets
)
{
if
(
widgets
)
{
for
(
const
i
in
widgets
)
{
for
(
const
i
in
widgets
)
{
const
widget
=
widgets
[
i
];
const
widget
=
widgets
[
i
];
if
(
!
widget
.
options
||
widget
.
options
.
serialize
!==
false
)
{
if
(
!
widget
.
options
||
widget
.
options
.
serialize
!==
false
)
{
inputs
[
widget
.
name
]
=
widget
.
serializeValue
?
await
widget
.
serializeValue
(
n
,
i
)
:
widget
.
value
;
inputs
[
widget
.
name
]
=
widget
.
serializeValue
?
await
widget
.
serializeValue
(
node
,
i
)
:
widget
.
value
;
}
}
}
}
}
}
// Store all node links
// Store all node links
for
(
let
i
in
node
.
inputs
)
{
for
(
let
i
in
node
.
inputs
)
{
let
parent
=
node
.
getInputNode
(
i
);
let
parent
=
node
.
getInputNode
(
i
);
if
(
parent
)
{
if
(
parent
)
{
let
link
=
node
.
getInputLink
(
i
);
let
link
=
node
.
getInputLink
(
i
);
while
(
parent
.
mode
===
4
||
parent
.
isVirtualNode
)
{
while
(
parent
.
mode
===
4
||
parent
.
isVirtualNode
)
{
let
found
=
false
;
let
found
=
false
;
if
(
parent
.
isVirtualNode
)
{
if
(
parent
.
isVirtualNode
)
{
link
=
parent
.
getInputLink
(
link
.
origin_slot
);
link
=
parent
.
getInputLink
(
link
.
origin_slot
);
if
(
link
)
{
if
(
link
)
{
parent
=
parent
.
getInputNode
(
link
.
target_slot
);
parent
=
parent
.
getInputNode
(
link
.
target_slot
);
if
(
parent
)
{
if
(
parent
)
{
found
=
true
;
found
=
true
;
}
}
}
}
}
else
if
(
link
&&
parent
.
mode
===
4
)
{
}
else
if
(
link
&&
parent
.
mode
===
4
)
{
let
all_inputs
=
[
link
.
origin_slot
];
let
all_inputs
=
[
link
.
origin_slot
];
if
(
parent
.
inputs
)
{
if
(
parent
.
inputs
)
{
all_inputs
=
all_inputs
.
concat
(
Object
.
keys
(
parent
.
inputs
))
all_inputs
=
all_inputs
.
concat
(
Object
.
keys
(
parent
.
inputs
))
for
(
let
parent_input
in
all_inputs
)
{
for
(
let
parent_input
in
all_inputs
)
{
parent_input
=
all_inputs
[
parent_input
];
parent_input
=
all_inputs
[
parent_input
];
if
(
parent
.
inputs
[
parent_input
]?.
type
===
node
.
inputs
[
i
].
type
)
{
if
(
parent
.
inputs
[
parent_input
]?.
type
===
node
.
inputs
[
i
].
type
)
{
link
=
parent
.
getInputLink
(
parent_input
);
link
=
parent
.
getInputLink
(
parent_input
);
if
(
link
)
{
if
(
link
)
{
parent
=
parent
.
getInputNode
(
parent_input
);
parent
=
parent
.
getInputNode
(
parent_input
);
}
found
=
true
;
break
;
}
}
found
=
true
;
break
;
}
}
}
}
}
}
}
if
(
!
found
)
{
if
(
!
found
)
{
break
;
break
;
}
}
}
}
if
(
link
)
{
if
(
link
)
{
inputs
[
node
.
inputs
[
i
].
name
]
=
[
String
(
link
.
origin_id
),
parseInt
(
link
.
origin_slot
)];
if
(
parent
?.
updateLink
)
{
link
=
parent
.
updateLink
(
link
);
}
inputs
[
node
.
inputs
[
i
].
name
]
=
[
String
(
link
.
origin_id
),
parseInt
(
link
.
origin_slot
)];
}
}
}
}
}
}
output
[
String
(
node
.
id
)]
=
{
output
[
String
(
node
.
id
)]
=
{
inputs
,
inputs
,
class_type
:
node
.
comfyClass
,
class_type
:
node
.
comfyClass
,
};
};
}
}
}
// Remove inputs connected to removed nodes
// Remove inputs connected to removed nodes
...
@@ -1816,7 +1877,7 @@ export class ComfyApp {
...
@@ -1816,7 +1877,7 @@ export class ComfyApp {
const
pngInfo
=
await
getPngMetadata
(
file
);
const
pngInfo
=
await
getPngMetadata
(
file
);
if
(
pngInfo
)
{
if
(
pngInfo
)
{
if
(
pngInfo
.
workflow
)
{
if
(
pngInfo
.
workflow
)
{
this
.
loadGraphData
(
JSON
.
parse
(
pngInfo
.
workflow
));
await
this
.
loadGraphData
(
JSON
.
parse
(
pngInfo
.
workflow
));
}
else
if
(
pngInfo
.
parameters
)
{
}
else
if
(
pngInfo
.
parameters
)
{
importA1111
(
this
.
graph
,
pngInfo
.
parameters
);
importA1111
(
this
.
graph
,
pngInfo
.
parameters
);
}
}
...
@@ -1832,21 +1893,21 @@ export class ComfyApp {
...
@@ -1832,21 +1893,21 @@ export class ComfyApp {
}
}
}
else
if
(
file
.
type
===
"
application/json
"
||
file
.
name
?.
endsWith
(
"
.json
"
))
{
}
else
if
(
file
.
type
===
"
application/json
"
||
file
.
name
?.
endsWith
(
"
.json
"
))
{
const
reader
=
new
FileReader
();
const
reader
=
new
FileReader
();
reader
.
onload
=
()
=>
{
reader
.
onload
=
async
()
=>
{
const
jsonContent
=
JSON
.
parse
(
reader
.
result
);
const
jsonContent
=
JSON
.
parse
(
reader
.
result
);
if
(
jsonContent
?.
templates
)
{
if
(
jsonContent
?.
templates
)
{
this
.
loadTemplateData
(
jsonContent
);
this
.
loadTemplateData
(
jsonContent
);
}
else
if
(
this
.
isApiJson
(
jsonContent
))
{
}
else
if
(
this
.
isApiJson
(
jsonContent
))
{
this
.
loadApiJson
(
jsonContent
);
this
.
loadApiJson
(
jsonContent
);
}
else
{
}
else
{
this
.
loadGraphData
(
jsonContent
);
await
this
.
loadGraphData
(
jsonContent
);
}
}
};
};
reader
.
readAsText
(
file
);
reader
.
readAsText
(
file
);
}
else
if
(
file
.
name
?.
endsWith
(
"
.latent
"
)
||
file
.
name
?.
endsWith
(
"
.safetensors
"
))
{
}
else
if
(
file
.
name
?.
endsWith
(
"
.latent
"
)
||
file
.
name
?.
endsWith
(
"
.safetensors
"
))
{
const
info
=
await
getLatentMetadata
(
file
);
const
info
=
await
getLatentMetadata
(
file
);
if
(
info
.
workflow
)
{
if
(
info
.
workflow
)
{
this
.
loadGraphData
(
JSON
.
parse
(
info
.
workflow
));
await
this
.
loadGraphData
(
JSON
.
parse
(
info
.
workflow
));
}
}
}
}
}
}
...
@@ -1867,7 +1928,7 @@ export class ComfyApp {
...
@@ -1867,7 +1928,7 @@ export class ComfyApp {
for
(
const
id
of
ids
)
{
for
(
const
id
of
ids
)
{
const
data
=
apiData
[
id
];
const
data
=
apiData
[
id
];
const
node
=
LiteGraph
.
createNode
(
data
.
class_type
);
const
node
=
LiteGraph
.
createNode
(
data
.
class_type
);
node
.
id
=
id
;
node
.
id
=
isNaN
(
+
id
)
?
id
:
+
id
;
graph
.
add
(
node
);
graph
.
add
(
node
);
}
}
...
...
web/scripts/domWidget.js
0 → 100644
View file @
c92f3dca
import
{
app
,
ANIM_PREVIEW_WIDGET
}
from
"
./app.js
"
;
const
SIZE
=
Symbol
();
function
intersect
(
a
,
b
)
{
const
x
=
Math
.
max
(
a
.
x
,
b
.
x
);
const
num1
=
Math
.
min
(
a
.
x
+
a
.
width
,
b
.
x
+
b
.
width
);
const
y
=
Math
.
max
(
a
.
y
,
b
.
y
);
const
num2
=
Math
.
min
(
a
.
y
+
a
.
height
,
b
.
y
+
b
.
height
);
if
(
num1
>=
x
&&
num2
>=
y
)
return
[
x
,
y
,
num1
-
x
,
num2
-
y
];
else
return
null
;
}
function
getClipPath
(
node
,
element
,
elRect
)
{
const
selectedNode
=
Object
.
values
(
app
.
canvas
.
selected_nodes
)[
0
];
if
(
selectedNode
&&
selectedNode
!==
node
)
{
const
MARGIN
=
7
;
const
scale
=
app
.
canvas
.
ds
.
scale
;
const
bounding
=
selectedNode
.
getBounding
();
const
intersection
=
intersect
(
{
x
:
elRect
.
x
/
scale
,
y
:
elRect
.
y
/
scale
,
width
:
elRect
.
width
/
scale
,
height
:
elRect
.
height
/
scale
},
{
x
:
selectedNode
.
pos
[
0
]
+
app
.
canvas
.
ds
.
offset
[
0
]
-
MARGIN
,
y
:
selectedNode
.
pos
[
1
]
+
app
.
canvas
.
ds
.
offset
[
1
]
-
LiteGraph
.
NODE_TITLE_HEIGHT
-
MARGIN
,
width
:
bounding
[
2
]
+
MARGIN
+
MARGIN
,
height
:
bounding
[
3
]
+
MARGIN
+
MARGIN
,
}
);
if
(
!
intersection
)
{
return
""
;
}
const
widgetRect
=
element
.
getBoundingClientRect
();
const
clipX
=
intersection
[
0
]
-
widgetRect
.
x
/
scale
+
"
px
"
;
const
clipY
=
intersection
[
1
]
-
widgetRect
.
y
/
scale
+
"
px
"
;
const
clipWidth
=
intersection
[
2
]
+
"
px
"
;
const
clipHeight
=
intersection
[
3
]
+
"
px
"
;
const
path
=
`polygon(0% 0%, 0% 100%,
${
clipX
}
100%,
${
clipX
}
${
clipY
}
, calc(
${
clipX
}
+
${
clipWidth
}
)
${
clipY
}
, calc(
${
clipX
}
+
${
clipWidth
}
) calc(
${
clipY
}
+
${
clipHeight
}
),
${
clipX
}
calc(
${
clipY
}
+
${
clipHeight
}
),
${
clipX
}
100%, 100% 100%, 100% 0%)`
;
return
path
;
}
return
""
;
}
function
computeSize
(
size
)
{
if
(
this
.
widgets
?.[
0
]?.
last_y
==
null
)
return
;
let
y
=
this
.
widgets
[
0
].
last_y
;
let
freeSpace
=
size
[
1
]
-
y
;
let
widgetHeight
=
0
;
let
dom
=
[];
for
(
const
w
of
this
.
widgets
)
{
if
(
w
.
type
===
"
converted-widget
"
)
{
// Ignore
delete
w
.
computedHeight
;
}
else
if
(
w
.
computeSize
)
{
widgetHeight
+=
w
.
computeSize
()[
1
]
+
4
;
}
else
if
(
w
.
element
)
{
// Extract DOM widget size info
const
styles
=
getComputedStyle
(
w
.
element
);
let
minHeight
=
w
.
options
.
getMinHeight
?.()
??
parseInt
(
styles
.
getPropertyValue
(
"
--comfy-widget-min-height
"
));
let
maxHeight
=
w
.
options
.
getMaxHeight
?.()
??
parseInt
(
styles
.
getPropertyValue
(
"
--comfy-widget-max-height
"
));
let
prefHeight
=
w
.
options
.
getHeight
?.()
??
styles
.
getPropertyValue
(
"
--comfy-widget-height
"
);
if
(
prefHeight
.
endsWith
?.(
"
%
"
))
{
prefHeight
=
size
[
1
]
*
(
parseFloat
(
prefHeight
.
substring
(
0
,
prefHeight
.
length
-
1
))
/
100
);
}
else
{
prefHeight
=
parseInt
(
prefHeight
);
if
(
isNaN
(
minHeight
))
{
minHeight
=
prefHeight
;
}
}
if
(
isNaN
(
minHeight
))
{
minHeight
=
50
;
}
if
(
!
isNaN
(
maxHeight
))
{
if
(
!
isNaN
(
prefHeight
))
{
prefHeight
=
Math
.
min
(
prefHeight
,
maxHeight
);
}
else
{
prefHeight
=
maxHeight
;
}
}
dom
.
push
({
minHeight
,
prefHeight
,
w
,
});
}
else
{
widgetHeight
+=
LiteGraph
.
NODE_WIDGET_HEIGHT
+
4
;
}
}
freeSpace
-=
widgetHeight
;
// Calculate sizes with all widgets at their min height
const
prefGrow
=
[];
// Nodes that want to grow to their prefd size
const
canGrow
=
[];
// Nodes that can grow to auto size
let
growBy
=
0
;
for
(
const
d
of
dom
)
{
freeSpace
-=
d
.
minHeight
;
if
(
isNaN
(
d
.
prefHeight
))
{
canGrow
.
push
(
d
);
d
.
w
.
computedHeight
=
d
.
minHeight
;
}
else
{
const
diff
=
d
.
prefHeight
-
d
.
minHeight
;
if
(
diff
>
0
)
{
prefGrow
.
push
(
d
);
growBy
+=
diff
;
d
.
diff
=
diff
;
}
else
{
d
.
w
.
computedHeight
=
d
.
minHeight
;
}
}
}
if
(
this
.
imgs
&&
!
this
.
widgets
.
find
((
w
)
=>
w
.
name
===
ANIM_PREVIEW_WIDGET
))
{
// Allocate space for image
freeSpace
-=
220
;
}
if
(
freeSpace
<
0
)
{
// Not enough space for all widgets so we need to grow
size
[
1
]
-=
freeSpace
;
this
.
graph
.
setDirtyCanvas
(
true
);
}
else
{
// Share the space between each
const
growDiff
=
freeSpace
-
growBy
;
if
(
growDiff
>
0
)
{
// All pref sizes can be fulfilled
freeSpace
=
growDiff
;
for
(
const
d
of
prefGrow
)
{
d
.
w
.
computedHeight
=
d
.
prefHeight
;
}
}
else
{
// We need to grow evenly
const
shared
=
-
growDiff
/
prefGrow
.
length
;
for
(
const
d
of
prefGrow
)
{
d
.
w
.
computedHeight
=
d
.
prefHeight
-
shared
;
}
freeSpace
=
0
;
}
if
(
freeSpace
>
0
&&
canGrow
.
length
)
{
// Grow any that are auto height
const
shared
=
freeSpace
/
canGrow
.
length
;
for
(
const
d
of
canGrow
)
{
d
.
w
.
computedHeight
+=
shared
;
}
}
}
// Position each of the widgets
for
(
const
w
of
this
.
widgets
)
{
w
.
y
=
y
;
if
(
w
.
computedHeight
)
{
y
+=
w
.
computedHeight
;
}
else
if
(
w
.
computeSize
)
{
y
+=
w
.
computeSize
()[
1
]
+
4
;
}
else
{
y
+=
LiteGraph
.
NODE_WIDGET_HEIGHT
+
4
;
}
}
}
// Override the compute visible nodes function to allow us to hide/show DOM elements when the node goes offscreen
const
elementWidgets
=
new
Set
();
const
computeVisibleNodes
=
LGraphCanvas
.
prototype
.
computeVisibleNodes
;
LGraphCanvas
.
prototype
.
computeVisibleNodes
=
function
()
{
const
visibleNodes
=
computeVisibleNodes
.
apply
(
this
,
arguments
);
for
(
const
node
of
app
.
graph
.
_nodes
)
{
if
(
elementWidgets
.
has
(
node
))
{
const
hidden
=
visibleNodes
.
indexOf
(
node
)
===
-
1
;
for
(
const
w
of
node
.
widgets
)
{
if
(
w
.
element
)
{
w
.
element
.
hidden
=
hidden
;
if
(
hidden
)
{
w
.
options
.
onHide
?.(
w
);
}
}
}
}
}
return
visibleNodes
;
};
let
enableDomClipping
=
true
;
export
function
addDomClippingSetting
()
{
app
.
ui
.
settings
.
addSetting
({
id
:
"
Comfy.DOMClippingEnabled
"
,
name
:
"
Enable DOM element clipping (enabling may reduce performance)
"
,
type
:
"
boolean
"
,
defaultValue
:
enableDomClipping
,
onChange
(
value
)
{
enableDomClipping
=
!!
value
;
},
});
}
LGraphNode
.
prototype
.
addDOMWidget
=
function
(
name
,
type
,
element
,
options
)
{
options
=
{
hideOnZoom
:
true
,
selectOn
:
[
"
focus
"
,
"
click
"
],
...
options
};
if
(
!
element
.
parentElement
)
{
document
.
body
.
append
(
element
);
}
let
mouseDownHandler
;
if
(
element
.
blur
)
{
mouseDownHandler
=
(
event
)
=>
{
if
(
!
element
.
contains
(
event
.
target
))
{
element
.
blur
();
}
};
document
.
addEventListener
(
"
mousedown
"
,
mouseDownHandler
);
}
const
widget
=
{
type
,
name
,
get
value
()
{
return
options
.
getValue
?.()
??
undefined
;
},
set
value
(
v
)
{
options
.
setValue
?.(
v
);
widget
.
callback
?.(
widget
.
value
);
},
draw
:
function
(
ctx
,
node
,
widgetWidth
,
y
,
widgetHeight
)
{
if
(
widget
.
computedHeight
==
null
)
{
computeSize
.
call
(
node
,
node
.
size
);
}
const
hidden
=
node
.
flags
?.
collapsed
||
(
!!
options
.
hideOnZoom
&&
app
.
canvas
.
ds
.
scale
<
0.5
)
||
widget
.
computedHeight
<=
0
||
widget
.
type
===
"
converted-widget
"
;
element
.
hidden
=
hidden
;
element
.
style
.
display
=
hidden
?
"
none
"
:
null
;
if
(
hidden
)
{
widget
.
options
.
onHide
?.(
widget
);
return
;
}
const
margin
=
10
;
const
elRect
=
ctx
.
canvas
.
getBoundingClientRect
();
const
transform
=
new
DOMMatrix
()
.
scaleSelf
(
elRect
.
width
/
ctx
.
canvas
.
width
,
elRect
.
height
/
ctx
.
canvas
.
height
)
.
multiplySelf
(
ctx
.
getTransform
())
.
translateSelf
(
margin
,
margin
+
y
);
const
scale
=
new
DOMMatrix
().
scaleSelf
(
transform
.
a
,
transform
.
d
);
Object
.
assign
(
element
.
style
,
{
transformOrigin
:
"
0 0
"
,
transform
:
scale
,
left
:
`
${
transform
.
a
+
transform
.
e
}
px`
,
top
:
`
${
transform
.
d
+
transform
.
f
}
px`
,
width
:
`
${
widgetWidth
-
margin
*
2
}
px`
,
height
:
`
${(
widget
.
computedHeight
??
50
)
-
margin
*
2
}
px
`,
position: "absolute",
zIndex: app.graph._nodes.indexOf(node),
});
if (enableDomClipping) {
element.style.clipPath = getClipPath(node, element, elRect);
element.style.willChange = "clip-path";
}
this.options.onDraw?.(widget);
},
element,
options,
onRemove() {
if (mouseDownHandler) {
document.removeEventListener("mousedown", mouseDownHandler);
}
element.remove();
},
};
for (const evt of options.selectOn) {
element.addEventListener(evt, () => {
app.canvas.selectNode(this);
app.canvas.bringToFront(this);
});
}
this.addCustomWidget(widget);
elementWidgets.add(this);
const collapse = this.collapse;
this.collapse = function() {
collapse.apply(this, arguments);
if(this.flags?.collapsed) {
element.hidden = true;
element.style.display = "none";
}
}
const onRemoved = this.onRemoved;
this.onRemoved = function () {
element.remove();
elementWidgets.delete(this);
onRemoved?.apply(this, arguments);
};
if (!this[SIZE]) {
this[SIZE] = true;
const onResize = this.onResize;
this.onResize = function (size) {
options.beforeResize?.call(widget, this);
computeSize.call(this, size);
onResize?.apply(this, arguments);
options.afterResize?.call(widget, this);
};
}
return widget;
};
web/scripts/pnginfo.js
View file @
c92f3dca
...
@@ -24,7 +24,7 @@ export function getPngMetadata(file) {
...
@@ -24,7 +24,7 @@ export function getPngMetadata(file) {
const
length
=
dataView
.
getUint32
(
offset
);
const
length
=
dataView
.
getUint32
(
offset
);
// Get the chunk type
// Get the chunk type
const
type
=
String
.
fromCharCode
(...
pngData
.
slice
(
offset
+
4
,
offset
+
8
));
const
type
=
String
.
fromCharCode
(...
pngData
.
slice
(
offset
+
4
,
offset
+
8
));
if
(
type
===
"
tEXt
"
)
{
if
(
type
===
"
tEXt
"
||
type
==
"
comf
"
)
{
// Get the keyword
// Get the keyword
let
keyword_end
=
offset
+
8
;
let
keyword_end
=
offset
+
8
;
while
(
pngData
[
keyword_end
]
!==
0
)
{
while
(
pngData
[
keyword_end
]
!==
0
)
{
...
@@ -50,7 +50,6 @@ export function getPngMetadata(file) {
...
@@ -50,7 +50,6 @@ export function getPngMetadata(file) {
function
parseExifData
(
exifData
)
{
function
parseExifData
(
exifData
)
{
// Check for the correct TIFF header (0x4949 for little-endian or 0x4D4D for big-endian)
// Check for the correct TIFF header (0x4949 for little-endian or 0x4D4D for big-endian)
const
isLittleEndian
=
new
Uint16Array
(
exifData
.
slice
(
0
,
2
))[
0
]
===
0x4949
;
const
isLittleEndian
=
new
Uint16Array
(
exifData
.
slice
(
0
,
2
))[
0
]
===
0x4949
;
console
.
log
(
exifData
);
// Function to read 16-bit and 32-bit integers from binary data
// Function to read 16-bit and 32-bit integers from binary data
function
readInt
(
offset
,
isLittleEndian
,
length
)
{
function
readInt
(
offset
,
isLittleEndian
,
length
)
{
...
@@ -126,6 +125,9 @@ export function getWebpMetadata(file) {
...
@@ -126,6 +125,9 @@ export function getWebpMetadata(file) {
const
chunk_length
=
dataView
.
getUint32
(
offset
+
4
,
true
);
const
chunk_length
=
dataView
.
getUint32
(
offset
+
4
,
true
);
const
chunk_type
=
String
.
fromCharCode
(...
webp
.
slice
(
offset
,
offset
+
4
));
const
chunk_type
=
String
.
fromCharCode
(...
webp
.
slice
(
offset
,
offset
+
4
));
if
(
chunk_type
===
"
EXIF
"
)
{
if
(
chunk_type
===
"
EXIF
"
)
{
if
(
String
.
fromCharCode
(...
webp
.
slice
(
offset
+
8
,
offset
+
8
+
6
))
==
"
Exif
\
0
\
0
"
)
{
offset
+=
6
;
}
let
data
=
parseExifData
(
webp
.
slice
(
offset
+
8
,
offset
+
8
+
chunk_length
));
let
data
=
parseExifData
(
webp
.
slice
(
offset
+
8
,
offset
+
8
+
chunk_length
));
for
(
var
key
in
data
)
{
for
(
var
key
in
data
)
{
var
value
=
data
[
key
];
var
value
=
data
[
key
];
...
...
web/scripts/ui.js
View file @
c92f3dca
...
@@ -462,8 +462,8 @@ class ComfyList {
...
@@ -462,8 +462,8 @@ class ComfyList {
return
$el
(
"
div
"
,
{
textContent
:
item
.
prompt
[
0
]
+
"
:
"
},
[
return
$el
(
"
div
"
,
{
textContent
:
item
.
prompt
[
0
]
+
"
:
"
},
[
$el
(
"
button
"
,
{
$el
(
"
button
"
,
{
textContent
:
"
Load
"
,
textContent
:
"
Load
"
,
onclick
:
()
=>
{
onclick
:
async
()
=>
{
app
.
loadGraphData
(
item
.
prompt
[
3
].
extra_pnginfo
.
workflow
);
await
app
.
loadGraphData
(
item
.
prompt
[
3
].
extra_pnginfo
.
workflow
);
if
(
item
.
outputs
)
{
if
(
item
.
outputs
)
{
app
.
nodeOutputs
=
item
.
outputs
;
app
.
nodeOutputs
=
item
.
outputs
;
}
}
...
@@ -599,7 +599,7 @@ export class ComfyUI {
...
@@ -599,7 +599,7 @@ export class ComfyUI {
const
fileInput
=
$el
(
"
input
"
,
{
const
fileInput
=
$el
(
"
input
"
,
{
id
:
"
comfy-file-input
"
,
id
:
"
comfy-file-input
"
,
type
:
"
file
"
,
type
:
"
file
"
,
accept
:
"
.json,image/png,.latent,.safetensors
"
,
accept
:
"
.json,image/png,.latent,.safetensors
,image/webp
"
,
style
:
{
display
:
"
none
"
},
style
:
{
display
:
"
none
"
},
parent
:
document
.
body
,
parent
:
document
.
body
,
onchange
:
()
=>
{
onchange
:
()
=>
{
...
@@ -784,9 +784,9 @@ export class ComfyUI {
...
@@ -784,9 +784,9 @@ export class ComfyUI {
}
}
}),
}),
$el
(
"
button
"
,
{
$el
(
"
button
"
,
{
id
:
"
comfy-load-default-button
"
,
textContent
:
"
Load Default
"
,
onclick
:
()
=>
{
id
:
"
comfy-load-default-button
"
,
textContent
:
"
Load Default
"
,
onclick
:
async
()
=>
{
if
(
!
confirmClear
.
value
||
confirm
(
"
Load default workflow?
"
))
{
if
(
!
confirmClear
.
value
||
confirm
(
"
Load default workflow?
"
))
{
app
.
loadGraphData
()
await
app
.
loadGraphData
()
}
}
}
}
}),
}),
...
...
web/scripts/ui/imagePreview.js
0 → 100644
View file @
c92f3dca
import
{
$el
}
from
"
../ui.js
"
;
export
function
calculateImageGrid
(
imgs
,
dw
,
dh
)
{
let
best
=
0
;
let
w
=
imgs
[
0
].
naturalWidth
;
let
h
=
imgs
[
0
].
naturalHeight
;
const
numImages
=
imgs
.
length
;
let
cellWidth
,
cellHeight
,
cols
,
rows
,
shiftX
;
// compact style
for
(
let
c
=
1
;
c
<=
numImages
;
c
++
)
{
const
r
=
Math
.
ceil
(
numImages
/
c
);
const
cW
=
dw
/
c
;
const
cH
=
dh
/
r
;
const
scaleX
=
cW
/
w
;
const
scaleY
=
cH
/
h
;
const
scale
=
Math
.
min
(
scaleX
,
scaleY
,
1
);
const
imageW
=
w
*
scale
;
const
imageH
=
h
*
scale
;
const
area
=
imageW
*
imageH
*
numImages
;
if
(
area
>
best
)
{
best
=
area
;
cellWidth
=
imageW
;
cellHeight
=
imageH
;
cols
=
c
;
rows
=
r
;
shiftX
=
c
*
((
cW
-
imageW
)
/
2
);
}
}
return
{
cellWidth
,
cellHeight
,
cols
,
rows
,
shiftX
};
}
export
function
createImageHost
(
node
)
{
const
el
=
$el
(
"
div.comfy-img-preview
"
);
let
currentImgs
;
let
first
=
true
;
function
updateSize
()
{
let
w
=
null
;
let
h
=
null
;
if
(
currentImgs
)
{
let
elH
=
el
.
clientHeight
;
if
(
first
)
{
first
=
false
;
// On first run, if we are small then grow a bit
if
(
elH
<
190
)
{
elH
=
190
;
}
el
.
style
.
setProperty
(
"
--comfy-widget-min-height
"
,
elH
);
}
else
{
el
.
style
.
setProperty
(
"
--comfy-widget-min-height
"
,
null
);
}
const
nw
=
node
.
size
[
0
];
({
cellWidth
:
w
,
cellHeight
:
h
}
=
calculateImageGrid
(
currentImgs
,
nw
-
20
,
elH
));
w
+=
"
px
"
;
h
+=
"
px
"
;
el
.
style
.
setProperty
(
"
--comfy-img-preview-width
"
,
w
);
el
.
style
.
setProperty
(
"
--comfy-img-preview-height
"
,
h
);
}
}
return
{
el
,
updateImages
(
imgs
)
{
if
(
imgs
!==
currentImgs
)
{
if
(
currentImgs
==
null
)
{
requestAnimationFrame
(()
=>
{
updateSize
();
});
}
el
.
replaceChildren
(...
imgs
);
currentImgs
=
imgs
;
node
.
onResize
(
node
.
size
);
node
.
graph
.
setDirtyCanvas
(
true
,
true
);
}
},
getHeight
()
{
updateSize
();
},
onDraw
()
{
// Element from point uses a hittest find elements so we need to toggle pointer events
el
.
style
.
pointerEvents
=
"
all
"
;
const
over
=
document
.
elementFromPoint
(
app
.
canvas
.
mouse
[
0
],
app
.
canvas
.
mouse
[
1
]);
el
.
style
.
pointerEvents
=
"
none
"
;
if
(
!
over
)
return
;
// Set the overIndex so Open Image etc work
const
idx
=
currentImgs
.
indexOf
(
over
);
node
.
overIndex
=
idx
;
},
};
}
web/scripts/widgets.js
View file @
c92f3dca
import
{
api
}
from
"
./api.js
"
import
{
api
}
from
"
./api.js
"
import
"
./domWidget.js
"
;
function
getNumberDefaults
(
inputData
,
defaultStep
,
precision
,
enable_rounding
)
{
function
getNumberDefaults
(
inputData
,
defaultStep
,
precision
,
enable_rounding
)
{
let
defaultVal
=
inputData
[
1
][
"
default
"
];
let
defaultVal
=
inputData
[
1
][
"
default
"
];
...
@@ -22,18 +23,89 @@ function getNumberDefaults(inputData, defaultStep, precision, enable_rounding) {
...
@@ -22,18 +23,89 @@ function getNumberDefaults(inputData, defaultStep, precision, enable_rounding) {
return
{
val
:
defaultVal
,
config
:
{
min
,
max
,
step
:
10.0
*
step
,
round
,
precision
}
};
return
{
val
:
defaultVal
,
config
:
{
min
,
max
,
step
:
10.0
*
step
,
round
,
precision
}
};
}
}
export
function
addValueControlWidget
(
node
,
targetWidget
,
defaultValue
=
"
randomize
"
,
values
)
{
export
function
addValueControlWidget
(
node
,
targetWidget
,
defaultValue
=
"
randomize
"
,
values
,
widgetName
,
inputData
)
{
const
valueControl
=
node
.
addWidget
(
"
combo
"
,
"
control_after_generate
"
,
defaultValue
,
function
(
v
)
{
},
{
let
name
=
inputData
[
1
]?.
control_after_generate
;
values
:
[
"
fixed
"
,
"
increment
"
,
"
decrement
"
,
"
randomize
"
],
if
(
typeof
name
!==
"
string
"
)
{
serialize
:
false
,
// Don't include this in prompt.
name
=
widgetName
;
});
}
valueControl
.
afterQueued
=
()
=>
{
const
widgets
=
addValueControlWidgets
(
node
,
targetWidget
,
defaultValue
,
{
addFilterList
:
false
,
controlAfterGenerateName
:
name
},
inputData
);
return
widgets
[
0
];
}
export
function
addValueControlWidgets
(
node
,
targetWidget
,
defaultValue
=
"
randomize
"
,
options
,
inputData
)
{
if
(
!
defaultValue
)
defaultValue
=
"
randomize
"
;
if
(
!
options
)
options
=
{};
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
;
}
const
widgets
=
[];
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.
}
);
widgets
.
push
(
valueControl
);
const
isCombo
=
targetWidget
.
type
===
"
combo
"
;
let
comboFilter
;
if
(
isCombo
&&
options
.
addFilterList
!==
false
)
{
comboFilter
=
node
.
addWidget
(
"
string
"
,
getName
(
"
control_filter_list
"
,
"
controlFilterListName
"
),
""
,
function
()
{},
{
serialize
:
false
,
// Don't include this in prompt.
}
);
widgets
.
push
(
comboFilter
);
}
valueControl
.
afterQueued
=
()
=>
{
var
v
=
valueControl
.
value
;
var
v
=
valueControl
.
value
;
if
(
targetWidget
.
type
==
"
combo
"
&&
v
!==
"
fixed
"
)
{
if
(
isCombo
&&
v
!==
"
fixed
"
)
{
let
current_index
=
targetWidget
.
options
.
values
.
indexOf
(
targetWidget
.
value
);
let
values
=
targetWidget
.
options
.
values
;
let
current_length
=
targetWidget
.
options
.
values
.
length
;
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
;
switch
(
v
)
{
switch
(
v
)
{
case
"
increment
"
:
case
"
increment
"
:
...
@@ -50,11 +122,12 @@ export function addValueControlWidget(node, targetWidget, defaultValue = "random
...
@@ -50,11 +122,12 @@ export function addValueControlWidget(node, targetWidget, defaultValue = "random
current_index
=
Math
.
max
(
0
,
current_index
);
current_index
=
Math
.
max
(
0
,
current_index
);
current_index
=
Math
.
min
(
current_length
-
1
,
current_index
);
current_index
=
Math
.
min
(
current_length
-
1
,
current_index
);
if
(
current_index
>=
0
)
{
if
(
current_index
>=
0
)
{
let
value
=
targetWidget
.
options
.
values
[
current_index
];
let
value
=
values
[
current_index
];
targetWidget
.
value
=
value
;
targetWidget
.
value
=
value
;
targetWidget
.
callback
(
value
);
targetWidget
.
callback
(
value
);
}
}
}
else
{
//number
}
else
{
//number
let
min
=
targetWidget
.
options
.
min
;
let
min
=
targetWidget
.
options
.
min
;
let
max
=
targetWidget
.
options
.
max
;
let
max
=
targetWidget
.
options
.
max
;
// limit to something that javascript can handle
// limit to something that javascript can handle
...
@@ -77,186 +150,68 @@ export function addValueControlWidget(node, targetWidget, defaultValue = "random
...
@@ -77,186 +150,68 @@ export function addValueControlWidget(node, targetWidget, defaultValue = "random
default
:
default
:
break
;
break
;
}
}
/*check if values are over or under their respective
/*check if values are over or under their respective
* ranges and set them to min or max.*/
* ranges and set them to min or max.*/
if
(
targetWidget
.
value
<
min
)
if
(
targetWidget
.
value
<
min
)
targetWidget
.
value
=
min
;
targetWidget
.
value
=
min
;
if
(
targetWidget
.
value
>
max
)
if
(
targetWidget
.
value
>
max
)
targetWidget
.
value
=
max
;
targetWidget
.
value
=
max
;
targetWidget
.
callback
(
targetWidget
.
value
);
targetWidget
.
callback
(
targetWidget
.
value
);
}
}
}
}
;
return
valueControl
;
return
widgets
;
};
};
function
seedWidget
(
node
,
inputName
,
inputData
,
app
)
{
function
seedWidget
(
node
,
inputName
,
inputData
,
app
,
widgetName
)
{
const
seed
=
Comfy
Widget
s
.
INT
(
node
,
inputName
,
inputData
,
app
);
const
seed
=
createInt
Widget
(
node
,
inputName
,
inputData
,
app
,
true
);
const
seedControl
=
addValueControlWidget
(
node
,
seed
.
widget
,
"
randomize
"
);
const
seedControl
=
addValueControlWidget
(
node
,
seed
.
widget
,
"
randomize
"
,
undefined
,
widgetName
,
inputData
);
seed
.
widget
.
linkedWidgets
=
[
seedControl
];
seed
.
widget
.
linkedWidgets
=
[
seedControl
];
return
seed
;
return
seed
;
}
}
const
MultilineSymbol
=
Symbol
();
function
createIntWidget
(
node
,
inputName
,
inputData
,
app
,
isSeedInput
)
{
const
MultilineResizeSymbol
=
Symbol
();
const
control
=
inputData
[
1
]?.
control_after_generate
;
if
(
!
isSeedInput
&&
control
)
{
function
addMultilineWidget
(
node
,
name
,
opts
,
app
)
{
return
seedWidget
(
node
,
inputName
,
inputData
,
app
,
typeof
control
===
"
string
"
?
control
:
undefined
);
const
MIN_SIZE
=
50
;
function
computeSize
(
size
)
{
if
(
node
.
widgets
[
0
].
last_y
==
null
)
return
;
let
y
=
node
.
widgets
[
0
].
last_y
;
let
freeSpace
=
size
[
1
]
-
y
;
// Compute the height of all non customtext widgets
let
widgetHeight
=
0
;
const
multi
=
[];
for
(
let
i
=
0
;
i
<
node
.
widgets
.
length
;
i
++
)
{
const
w
=
node
.
widgets
[
i
];
if
(
w
.
type
===
"
customtext
"
)
{
multi
.
push
(
w
);
}
else
{
if
(
w
.
computeSize
)
{
widgetHeight
+=
w
.
computeSize
()[
1
]
+
4
;
}
else
{
widgetHeight
+=
LiteGraph
.
NODE_WIDGET_HEIGHT
+
4
;
}
}
}
// See how large each text input can be
freeSpace
-=
widgetHeight
;
freeSpace
/=
multi
.
length
+
(
!!
node
.
imgs
?.
length
);
if
(
freeSpace
<
MIN_SIZE
)
{
// There isnt enough space for all the widgets, increase the size of the node
freeSpace
=
MIN_SIZE
;
node
.
size
[
1
]
=
y
+
widgetHeight
+
freeSpace
*
(
multi
.
length
+
(
!!
node
.
imgs
?.
length
));
node
.
graph
.
setDirtyCanvas
(
true
);
}
// Position each of the widgets
for
(
const
w
of
node
.
widgets
)
{
w
.
y
=
y
;
if
(
w
.
type
===
"
customtext
"
)
{
y
+=
freeSpace
;
w
.
computedHeight
=
freeSpace
-
multi
.
length
*
4
;
}
else
if
(
w
.
computeSize
)
{
y
+=
w
.
computeSize
()[
1
]
+
4
;
}
else
{
y
+=
LiteGraph
.
NODE_WIDGET_HEIGHT
+
4
;
}
}
node
.
inputHeight
=
freeSpace
;
}
}
const
widget
=
{
let
widgetType
=
isSlider
(
inputData
[
1
][
"
display
"
],
app
);
type
:
"
customtext
"
,
const
{
val
,
config
}
=
getNumberDefaults
(
inputData
,
1
,
0
,
true
);
name
,
Object
.
assign
(
config
,
{
precision
:
0
});
get
value
()
{
return
{
return
this
.
inputEl
.
value
;
widget
:
node
.
addWidget
(
},
widgetType
,
set
value
(
x
)
{
inputName
,
this
.
inputEl
.
value
=
x
;
val
,
function
(
v
)
{
const
s
=
this
.
options
.
step
/
10
;
this
.
value
=
Math
.
round
(
v
/
s
)
*
s
;
},
config
),
};
}
function
addMultilineWidget
(
node
,
name
,
opts
,
app
)
{
const
inputEl
=
document
.
createElement
(
"
textarea
"
);
inputEl
.
className
=
"
comfy-multiline-input
"
;
inputEl
.
value
=
opts
.
defaultVal
;
inputEl
.
placeholder
=
opts
.
placeholder
||
name
;
const
widget
=
node
.
addDOMWidget
(
name
,
"
customtext
"
,
inputEl
,
{
getValue
()
{
return
inputEl
.
value
;
},
},
draw
:
function
(
ctx
,
_
,
widgetWidth
,
y
,
widgetHeight
)
{
setValue
(
v
)
{
if
(
!
this
.
parent
.
inputHeight
)
{
inputEl
.
value
=
v
;
// If we are initially offscreen when created we wont have received a resize event
// Calculate it here instead
computeSize
(
node
.
size
);
}
const
visible
=
app
.
canvas
.
ds
.
scale
>
0.5
&&
this
.
type
===
"
customtext
"
;
const
margin
=
10
;
const
elRect
=
ctx
.
canvas
.
getBoundingClientRect
();
const
transform
=
new
DOMMatrix
()
.
scaleSelf
(
elRect
.
width
/
ctx
.
canvas
.
width
,
elRect
.
height
/
ctx
.
canvas
.
height
)
.
multiplySelf
(
ctx
.
getTransform
())
.
translateSelf
(
margin
,
margin
+
y
);
const
scale
=
new
DOMMatrix
().
scaleSelf
(
transform
.
a
,
transform
.
d
)
Object
.
assign
(
this
.
inputEl
.
style
,
{
transformOrigin
:
"
0 0
"
,
transform
:
scale
,
left
:
`
${
transform
.
a
+
transform
.
e
}
px`
,
top
:
`
${
transform
.
d
+
transform
.
f
}
px`
,
width
:
`
${
widgetWidth
-
(
margin
*
2
)}
px`
,
height
:
`
${
this
.
parent
.
inputHeight
-
(
margin
*
2
)}
px`
,
position
:
"
absolute
"
,
background
:
(
!
node
.
color
)?
''
:
node
.
color
,
color
:
(
!
node
.
color
)?
''
:
'
white
'
,
zIndex
:
app
.
graph
.
_nodes
.
indexOf
(
node
),
});
this
.
inputEl
.
hidden
=
!
visible
;
},
},
};
widget
.
inputEl
=
document
.
createElement
(
"
textarea
"
);
widget
.
inputEl
.
className
=
"
comfy-multiline-input
"
;
widget
.
inputEl
.
value
=
opts
.
defaultVal
;
widget
.
inputEl
.
placeholder
=
opts
.
placeholder
||
""
;
document
.
addEventListener
(
"
mousedown
"
,
function
(
event
)
{
if
(
!
widget
.
inputEl
.
contains
(
event
.
target
))
{
widget
.
inputEl
.
blur
();
}
});
});
widget
.
parent
=
node
;
widget
.
inputEl
=
inputEl
;
document
.
body
.
appendChild
(
widget
.
inputEl
);
node
.
addCustomWidget
(
widget
);
app
.
canvas
.
onDrawBackground
=
function
()
{
// Draw node isnt fired once the node is off the screen
// if it goes off screen quickly, the input may not be removed
// this shifts it off screen so it can be moved back if the node is visible.
for
(
let
n
in
app
.
graph
.
_nodes
)
{
n
=
graph
.
_nodes
[
n
];
for
(
let
w
in
n
.
widgets
)
{
let
wid
=
n
.
widgets
[
w
];
if
(
Object
.
hasOwn
(
wid
,
"
inputEl
"
))
{
wid
.
inputEl
.
style
.
left
=
-
8000
+
"
px
"
;
wid
.
inputEl
.
style
.
position
=
"
absolute
"
;
}
}
}
};
node
.
onRemoved
=
function
()
{
// When removing this node we need to remove the input from the DOM
for
(
let
y
in
this
.
widgets
)
{
if
(
this
.
widgets
[
y
].
inputEl
)
{
this
.
widgets
[
y
].
inputEl
.
remove
();
}
}
};
widget
.
onRemove
=
()
=>
{
inputEl
.
addEventListener
(
"
input
"
,
()
=>
{
widget
.
inputEl
?.
remove
();
widget
.
callback
?.(
widget
.
value
);
});
// Restore original size handler if we are the last
if
(
!--
node
[
MultilineSymbol
])
{
node
.
onResize
=
node
[
MultilineResizeSymbol
];
delete
node
[
MultilineSymbol
];
delete
node
[
MultilineResizeSymbol
];
}
};
if
(
node
[
MultilineSymbol
])
{
node
[
MultilineSymbol
]
++
;
}
else
{
node
[
MultilineSymbol
]
=
1
;
const
onResize
=
(
node
[
MultilineResizeSymbol
]
=
node
.
onResize
);
node
.
onResize
=
function
(
size
)
{
computeSize
(
size
);
// Call original resizer handler
if
(
onResize
)
{
onResize
.
apply
(
this
,
arguments
);
}
};
}
return
{
minWidth
:
400
,
minHeight
:
200
,
widget
};
return
{
minWidth
:
400
,
minHeight
:
200
,
widget
};
}
}
...
@@ -288,31 +243,26 @@ export const ComfyWidgets = {
...
@@ -288,31 +243,26 @@ export const ComfyWidgets = {
},
config
)
};
},
config
)
};
},
},
INT
(
node
,
inputName
,
inputData
,
app
)
{
INT
(
node
,
inputName
,
inputData
,
app
)
{
let
widgetType
=
isSlider
(
inputData
[
1
][
"
display
"
],
app
);
return
createIntWidget
(
node
,
inputName
,
inputData
,
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
;
this
.
value
=
Math
.
round
(
v
/
s
)
*
s
;
},
config
),
};
},
},
BOOLEAN
(
node
,
inputName
,
inputData
)
{
BOOLEAN
(
node
,
inputName
,
inputData
)
{
let
defaultVal
=
inputData
[
1
][
"
default
"
];
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
;
}
return
{
return
{
widget
:
node
.
addWidget
(
widget
:
node
.
addWidget
(
"
toggle
"
,
"
toggle
"
,
inputName
,
inputName
,
defaultVal
,
defaultVal
,
()
=>
{},
()
=>
{},
{
"
on
"
:
inputData
[
1
].
label_on
,
"
off
"
:
inputData
[
1
].
label_off
}
options
,
)
)
};
};
},
},
...
@@ -338,10 +288,14 @@ export const ComfyWidgets = {
...
@@ -338,10 +288,14 @@ export const ComfyWidgets = {
if
(
inputData
[
1
]
&&
inputData
[
1
].
default
)
{
if
(
inputData
[
1
]
&&
inputData
[
1
].
default
)
{
defaultValue
=
inputData
[
1
].
default
;
defaultValue
=
inputData
[
1
].
default
;
}
}
return
{
widget
:
node
.
addWidget
(
"
combo
"
,
inputName
,
defaultValue
,
()
=>
{},
{
values
:
type
})
};
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
;
},
},
IMAGEUPLOAD
(
node
,
inputName
,
inputData
,
app
)
{
IMAGEUPLOAD
(
node
,
inputName
,
inputData
,
app
)
{
const
imageWidget
=
node
.
widgets
.
find
((
w
)
=>
w
.
name
===
"
image
"
);
const
imageWidget
=
node
.
widgets
.
find
((
w
)
=>
w
.
name
===
(
inputData
[
1
]?.
widget
??
"
image
"
)
)
;
let
uploadWidget
;
let
uploadWidget
;
function
showImage
(
name
)
{
function
showImage
(
name
)
{
...
@@ -455,9 +409,10 @@ export const ComfyWidgets = {
...
@@ -455,9 +409,10 @@ export const ComfyWidgets = {
document
.
body
.
append
(
fileInput
);
document
.
body
.
append
(
fileInput
);
// Create the button widget for selecting the files
// Create the button widget for selecting the files
uploadWidget
=
node
.
addWidget
(
"
button
"
,
"
choose file to upload
"
,
"
image
"
,
()
=>
{
uploadWidget
=
node
.
addWidget
(
"
button
"
,
inputName
,
"
image
"
,
()
=>
{
fileInput
.
click
();
fileInput
.
click
();
});
});
uploadWidget
.
label
=
"
choose file to upload
"
;
uploadWidget
.
serialize
=
false
;
uploadWidget
.
serialize
=
false
;
// Add handler to check if an image is being dragged over our node
// Add handler to check if an image is being dragged over our node
...
...
web/style.css
View file @
c92f3dca
...
@@ -409,6 +409,21 @@ dialog::backdrop {
...
@@ -409,6 +409,21 @@ dialog::backdrop {
width
:
calc
(
100%
-
10px
);
width
:
calc
(
100%
-
10px
);
}
}
.comfy-img-preview
{
pointer-events
:
none
;
overflow
:
hidden
;
display
:
flex
;
flex-wrap
:
wrap
;
align-content
:
flex-start
;
justify-content
:
center
;
}
.comfy-img-preview
img
{
object-fit
:
contain
;
width
:
var
(
--comfy-img-preview-width
);
height
:
var
(
--comfy-img-preview-height
);
}
/* Search box */
/* Search box */
.litegraph.litesearchbox
{
.litegraph.litesearchbox
{
...
...
Prev
1
2
3
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment