A whole lotta things
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes';
|
||||
import { Tabs } from '@svelteuidev/core';
|
||||
import { Backpack, Gear } from 'radix-icons-svelte';
|
||||
import ComfyUIPane from "./ComfyUIPane.svelte";
|
||||
import ComfyApp from "./ComfyApp";
|
||||
|
||||
import { LGraphNode } from "litegraph.js";
|
||||
|
||||
let app: ComfyApp = undefined;
|
||||
let uiPane: ComfyUIPane = undefined;
|
||||
|
||||
function refreshView(event) {
|
||||
app.resizeCanvas();
|
||||
@@ -13,6 +16,23 @@
|
||||
|
||||
onMount(async () => {
|
||||
app = new ComfyApp();
|
||||
|
||||
app.eventBus.on("nodeAdded", (node: LGraphNode) => {
|
||||
uiPane.addNodeUI(node);
|
||||
});
|
||||
|
||||
app.eventBus.on("nodeRemoved", (node: LGraphNode) => {
|
||||
uiPane.removeNodeUI(node);
|
||||
});
|
||||
|
||||
app.eventBus.on("configured", (graph: LGraph) => {
|
||||
uiPane.configureFinished(graph);
|
||||
});
|
||||
|
||||
app.eventBus.on("cleared", () => {
|
||||
uiPane.clear();
|
||||
});
|
||||
|
||||
await app.setup();
|
||||
refreshView();
|
||||
|
||||
@@ -20,25 +40,26 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<Tabs>
|
||||
<Tabs.Tab label="Workspace" icon={Backpack}>
|
||||
<div id="container">
|
||||
<Splitpanes on:resize={refreshView}>
|
||||
<Pane class="sidebar" size={20} minSize={20}>I have a min width of 20%</Pane>
|
||||
<Pane>
|
||||
<Splitpanes on:resize={refreshView} horizontal="{true}">
|
||||
<Pane minSize={15}>I have a min height of 15%</Pane>
|
||||
<Pane minSize={10}>
|
||||
<canvas id="graph-canvas" />
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
<div id="dropzone" class="dropzone"></div>
|
||||
<div id="container">
|
||||
<Splitpanes theme="comfy" on:resize={refreshView}>
|
||||
<Pane size={20} minSize={10}>
|
||||
<div>
|
||||
Sidebar
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane>
|
||||
<Splitpanes theme="comfy" on:resize={refreshView} horizontal="{true}">
|
||||
<Pane minSize={15}>
|
||||
<ComfyUIPane bind:this={uiPane} {app} />
|
||||
</Pane>
|
||||
<Pane minSize={10}>
|
||||
<canvas id="graph-canvas" />
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</div>
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab label="Settings" icon={Gear}>
|
||||
</Tabs.Tab>
|
||||
</Tabs>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#container {
|
||||
@@ -47,24 +68,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.panel {
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.resizer[data-direction='horizontal'] {
|
||||
background-color: #cbd5e0;
|
||||
cursor: ew-resize;
|
||||
height: 100%;
|
||||
width: 2px;
|
||||
}
|
||||
.resizer[data-direction='vertical'] {
|
||||
background-color: #cbd5e0;
|
||||
cursor: ns-resize;
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#comfy-content {
|
||||
grid-area: content;
|
||||
height: 100vh;
|
||||
@@ -81,6 +84,19 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dropzone {
|
||||
box-sizing: border-box;
|
||||
display: none;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 99999;
|
||||
background: #60a7dc80;
|
||||
border: 4px dashed #60a7dc;
|
||||
}
|
||||
|
||||
:global(html, body) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -88,6 +104,20 @@
|
||||
font-family: Arial;
|
||||
}
|
||||
|
||||
:global(.splitpanes.comfy.splitpanes--horizontal>.splitpanes__splitter) {
|
||||
min-height: 20px;
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
:global(.splitpanes.comfy.splitpanes--vertical>.splitpanes__splitter) {
|
||||
min-width: 20px;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
:global(.splitpanes.comfy) {
|
||||
max-height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
:global(.splitpanes__pane) {
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, .2) inset;
|
||||
justify-content: center;
|
||||
|
||||
@@ -1,19 +1,31 @@
|
||||
import { LiteGraph, LGraph, LGraphCanvas, LGraphNode } from "litegraph.js";
|
||||
import type { LGraphNodeBase } from "litegraph.js";
|
||||
import type { LGraphNodeBase, LConnectionKind, INodeSlot } from "litegraph.js";
|
||||
import ComfyAPI from "$lib/api"
|
||||
import { ComfyWidgets } from "$lib/widgets"
|
||||
import defaultGraph from "$lib/defaultGraph"
|
||||
import { getPngMetadata, importA1111 } from "$lib/pnginfo";
|
||||
import EventEmitter from "events";
|
||||
import type TypedEmitter from "typed-emitter";
|
||||
|
||||
type QueueItem = { num: number, batchCount: number }
|
||||
|
||||
type ComfyAppEvents = {
|
||||
configured: (graph: LGraph) => void
|
||||
nodeAdded: (node: LGraphNode) => void
|
||||
nodeRemoved: (node: LGraphNode) => void
|
||||
nodeConnectionChanged: (kind: LConnectionKind, node: LGraphNode, slot: INodeSlot, targetNode: LGraphNode, targetSlot: INodeSlot) => void
|
||||
cleared: () => void
|
||||
}
|
||||
|
||||
export default class ComfyApp {
|
||||
api: ComfyAPI;
|
||||
canvasEl: HTMLCanvasElement | null = null;
|
||||
canvasCtx: CanvasRenderingContext2D | null = null;
|
||||
lGraph: LGraph | null = null;
|
||||
lCanvas: LGraphCanvas | null = null;
|
||||
dropZone: HTMLElement | null = null;
|
||||
nodeOutputs: Record<string, any> = {};
|
||||
eventBus: TypedEmitter<ComfyAppEvents> = new EventEmitter() as TypedEmitter<ComfyAppEvents>;
|
||||
|
||||
private queueItems: QueueItem[] = [];
|
||||
private processingQueue: boolean = false;
|
||||
@@ -31,6 +43,8 @@ export default class ComfyApp {
|
||||
this.lCanvas = new LGraphCanvas(this.canvasEl, this.lGraph);
|
||||
this.canvasCtx = this.canvasEl.getContext("2d");
|
||||
|
||||
this.addGraphLifecycleHooks();
|
||||
|
||||
LiteGraph.release_link_on_empty_shows_menu = true;
|
||||
LiteGraph.alt_drag_do_clone_nodes = true;
|
||||
|
||||
@@ -63,7 +77,7 @@ export default class ComfyApp {
|
||||
// this.#addDrawNodeHandler();
|
||||
// this.#addDrawGroupsHandler();
|
||||
// this.#addApiUpdateHandlers();
|
||||
// this.#addDropHandler();
|
||||
this.addDropHandler();
|
||||
// this.#addPasteHandler();
|
||||
// this.#addKeyboardHandler();
|
||||
|
||||
@@ -101,6 +115,52 @@ export default class ComfyApp {
|
||||
|
||||
}
|
||||
|
||||
private graphOnConfigure() {
|
||||
console.log("Configured");
|
||||
this.eventBus.emit("configured", this.lGraph);
|
||||
}
|
||||
|
||||
private graphOnBeforeChange(graph: LGraph, info: any) {
|
||||
console.log("BeforeChange", info);
|
||||
this.eventBus.emit("beforeChange", graph, info);
|
||||
}
|
||||
|
||||
private graphOnAfterChange(graph: LGraph, info: any) {
|
||||
console.log("AfterChange", info);
|
||||
this.eventBus.emit("afterChange", graph, info);
|
||||
}
|
||||
|
||||
private graphOnNodeAdded(node: LGraphNode) {
|
||||
console.log("Added", node);
|
||||
this.eventBus.emit("nodeAdded", node);
|
||||
}
|
||||
|
||||
private graphOnNodeRemoved(node: LGraphNode) {
|
||||
console.log("Removed", node);
|
||||
this.eventBus.emit("nodeRemoved", node);
|
||||
}
|
||||
|
||||
private graphOnNodeConnectionChange(kind: LConnectionKind, node: LGraphNode, slot: INodeSlot, targetNode: LGraphNode, targetSlot: INodeSlot) {
|
||||
console.log("ConnectionChange", node);
|
||||
this.eventBus.emit("nodeConnectionChanged", kind, node, slot, targetNode, targetSlot);
|
||||
}
|
||||
|
||||
private canvasOnClear() {
|
||||
console.log("CanvasClear");
|
||||
this.eventBus.emit("cleared");
|
||||
}
|
||||
|
||||
private addGraphLifecycleHooks() {
|
||||
this.lGraph.onConfigure = this.graphOnConfigure.bind(this);
|
||||
this.lGraph.onBeforeChange = this.graphOnBeforeChange.bind(this);
|
||||
this.lGraph.onAfterChange = this.graphOnAfterChange.bind(this);
|
||||
this.lGraph.onNodeAdded = this.graphOnNodeAdded.bind(this);
|
||||
this.lGraph.onNodeRemoved = this.graphOnNodeRemoved.bind(this);
|
||||
this.lGraph.onNodeConnectionChange = this.graphOnNodeConnectionChange.bind(this);
|
||||
|
||||
this.lCanvas.onClear = this.canvasOnClear.bind(this);
|
||||
}
|
||||
|
||||
private async registerNodes() {
|
||||
const app = this;
|
||||
|
||||
@@ -183,235 +243,277 @@ export default class ComfyApp {
|
||||
// await this.#invokeExtensionsAsync("registerCustomNodes");
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the graph with the specified workflow data
|
||||
* @param {*} graphData A serialized graph object
|
||||
*/
|
||||
loadGraphData(graphData: any = null) {
|
||||
this.clean();
|
||||
private showDropZone() {
|
||||
this.dropZone.style.display = "block";
|
||||
}
|
||||
|
||||
if (!graphData) {
|
||||
graphData = structuredClone(defaultGraph);
|
||||
}
|
||||
private hideDropZone() {
|
||||
this.dropZone.style.display = "none";
|
||||
}
|
||||
|
||||
// Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
|
||||
for (let n of graphData.nodes) {
|
||||
if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader";
|
||||
}
|
||||
private allowDrag(event: DragEvent) {
|
||||
if (event.dataTransfer.items?.length > 0) {
|
||||
event.dataTransfer.dropEffect = 'copy';
|
||||
this.showDropZone();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
this.lGraph.configure(graphData);
|
||||
private async handleDrop(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.hideDropZone();
|
||||
|
||||
for (const node of this.lGraph._nodes) {
|
||||
const size = node.computeSize();
|
||||
size[0] = Math.max(node.size[0], size[0]);
|
||||
size[1] = Math.max(node.size[1], size[1]);
|
||||
node.size = size;
|
||||
if (event.dataTransfer.files.length > 0) {
|
||||
await this.handleFile(event.dataTransfer.files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
if (node.widgets) {
|
||||
// If you break something in the backend and want to patch workflows in the frontend
|
||||
// This is the place to do this
|
||||
for (let widget of node.widgets) {
|
||||
if (node.type == "KSampler" || node.type == "KSamplerAdvanced") {
|
||||
if (widget.name == "sampler_name") {
|
||||
if (widget.value.constructor === String && widget.value.startsWith("sample_")) {
|
||||
widget.value = widget.value.slice(7);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private addDropHandler() {
|
||||
this.dropZone = document.getElementById("dropzone");
|
||||
|
||||
// this.#invokeExtensions("loadedGraphNode", node);
|
||||
}
|
||||
}
|
||||
window.addEventListener('dragenter', this.allowDrag.bind(this));
|
||||
this.dropZone.addEventListener('dragover', this.allowDrag.bind(this));
|
||||
this.dropZone.addEventListener('dragleave', this.hideDropZone.bind(this));
|
||||
this.dropZone.addEventListener('drop', this.handleDrop.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the current graph workflow for sending to the API
|
||||
* @returns The workflow and node links
|
||||
*/
|
||||
async graphToPrompt() {
|
||||
const workflow = this.lGraph.serialize();
|
||||
const output = {};
|
||||
// Process nodes in order of execution
|
||||
for (const node of this.lGraph.computeExecutionOrder(false, null)) {
|
||||
const n = workflow.nodes.find((n) => n.id === node.id);
|
||||
/**
|
||||
* Populates the graph with the specified workflow data
|
||||
* @param {*} graphData A serialized graph object
|
||||
*/
|
||||
loadGraphData(graphData: any = null) {
|
||||
this.clean();
|
||||
|
||||
if (node.isVirtualNode) {
|
||||
// Don't serialize frontend only nodes but let them make changes
|
||||
if (node.applyToGraph) {
|
||||
node.applyToGraph(workflow);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!graphData) {
|
||||
graphData = structuredClone(defaultGraph);
|
||||
}
|
||||
|
||||
if (node.mode === 2) {
|
||||
// Don't serialize muted nodes
|
||||
continue;
|
||||
}
|
||||
// Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
|
||||
for (let n of graphData.nodes) {
|
||||
if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader";
|
||||
}
|
||||
|
||||
const inputs = {};
|
||||
const widgets = node.widgets;
|
||||
this.lGraph.configure(graphData);
|
||||
|
||||
// Store all widget values
|
||||
if (widgets) {
|
||||
for (const i in widgets) {
|
||||
const widget = widgets[i];
|
||||
if (!widget.options || widget.options.serialize !== false) {
|
||||
inputs[widget.name] = widget.serializeValue ? await widget.serializeValue(n, i) : widget.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const node of this.lGraph._nodes) {
|
||||
const size = node.computeSize();
|
||||
size[0] = Math.max(node.size[0], size[0]);
|
||||
size[1] = Math.max(node.size[1], size[1]);
|
||||
node.size = size;
|
||||
|
||||
// Store all node links
|
||||
for (let i in node.inputs) {
|
||||
let parent = node.getInputNode(i);
|
||||
if (parent) {
|
||||
let link = node.getInputLink(i);
|
||||
while (parent && parent.isVirtualNode) {
|
||||
link = parent.getInputLink(link.origin_slot);
|
||||
if (link) {
|
||||
parent = parent.getInputNode(link.origin_slot);
|
||||
} else {
|
||||
parent = null;
|
||||
}
|
||||
}
|
||||
if (node.widgets) {
|
||||
// If you break something in the backend and want to patch workflows in the frontend
|
||||
// This is the place to do this
|
||||
for (let widget of node.widgets) {
|
||||
if (node.type == "KSampler" || node.type == "KSamplerAdvanced") {
|
||||
if (widget.name == "sampler_name") {
|
||||
if (widget.value.constructor === String && widget.value.startsWith("sample_")) {
|
||||
widget.value = widget.value.slice(7);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (link) {
|
||||
inputs[node.inputs[i].name] = [String(link.origin_id), parseInt(link.origin_slot)];
|
||||
}
|
||||
}
|
||||
}
|
||||
// this.#invokeExtensions("loadedGraphNode", node);
|
||||
}
|
||||
}
|
||||
|
||||
output[String(node.id)] = {
|
||||
inputs,
|
||||
class_type: node.comfyClass,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Converts the current graph workflow for sending to the API
|
||||
* @returns The workflow and node links
|
||||
*/
|
||||
async graphToPrompt(frontendState: Record<number, any[]> = {}) {
|
||||
const workflow = this.lGraph.serialize();
|
||||
|
||||
// Remove inputs connected to removed nodes
|
||||
const output = {};
|
||||
// Process nodes in order of execution
|
||||
for (const node of this.lGraph.computeExecutionOrder(false, null)) {
|
||||
const fromFrontend = frontendState[node.id];
|
||||
if (fromFrontend) {
|
||||
console.log("Set values!", node, fromFrontend)
|
||||
node.widgets_values = fromFrontend;
|
||||
}
|
||||
|
||||
for (const o in output) {
|
||||
for (const i in output[o].inputs) {
|
||||
if (Array.isArray(output[o].inputs[i])
|
||||
&& output[o].inputs[i].length === 2
|
||||
&& !output[output[o].inputs[i][0]]) {
|
||||
delete output[o].inputs[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
const n = workflow.nodes.find((n) => n.id === node.id);
|
||||
|
||||
return { workflow, output };
|
||||
}
|
||||
if (node.isVirtualNode) {
|
||||
// Don't serialize frontend only nodes but let them make changes
|
||||
if (node.applyToGraph) {
|
||||
node.applyToGraph(workflow);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
async queuePrompt(num: number, batchCount: number = 1) {
|
||||
this.queueItems.push({ num, batchCount });
|
||||
if (node.mode === 2) {
|
||||
// Don't serialize muted nodes
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only have one action process the items so each one gets a unique seed correctly
|
||||
if (this.processingQueue) {
|
||||
return;
|
||||
}
|
||||
const inputs = {};
|
||||
const widgets = node.widgets;
|
||||
|
||||
this.processingQueue = true;
|
||||
try {
|
||||
while (this.queueItems.length) {
|
||||
({ num, batchCount } = this.queueItems.pop());
|
||||
// Store all widget values
|
||||
if (widgets) {
|
||||
for (const i in widgets) {
|
||||
const widget = widgets[i];
|
||||
if (!widget.options || widget.options.serialize !== false) {
|
||||
inputs[widget.name] = widget.serializeValue ? await widget.serializeValue(n, i) : widget.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store all node links
|
||||
for (let i in node.inputs) {
|
||||
let parent = node.getInputNode(i);
|
||||
if (parent) {
|
||||
let link = node.getInputLink(i);
|
||||
while (parent && parent.isVirtualNode) {
|
||||
link = parent.getInputLink(link.origin_slot);
|
||||
if (link) {
|
||||
parent = parent.getInputNode(link.origin_slot);
|
||||
} else {
|
||||
parent = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (link) {
|
||||
inputs[node.inputs[i].name] = [String(link.origin_id), parseInt(link.origin_slot)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output[String(node.id)] = {
|
||||
inputs,
|
||||
class_type: node.comfyClass,
|
||||
};
|
||||
}
|
||||
|
||||
// Remove inputs connected to removed nodes
|
||||
|
||||
for (const o in output) {
|
||||
for (const i in output[o].inputs) {
|
||||
if (Array.isArray(output[o].inputs[i])
|
||||
&& output[o].inputs[i].length === 2
|
||||
&& !output[output[o].inputs[i][0]]) {
|
||||
delete output[o].inputs[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { workflow, output };
|
||||
}
|
||||
|
||||
async queuePrompt(num: number, batchCount: number = 1, frontendState: Record<number, any[]> = {}) {
|
||||
this.queueItems.push({ num, batchCount });
|
||||
|
||||
// Only have one action process the items so each one gets a unique seed correctly
|
||||
if (this.processingQueue) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.processingQueue = true;
|
||||
try {
|
||||
while (this.queueItems.length) {
|
||||
({ num, batchCount } = this.queueItems.pop());
|
||||
console.log(`Queue get! ${num} ${batchCount}`);
|
||||
|
||||
for (let i = 0; i < batchCount; i++) {
|
||||
const p = await this.graphToPrompt();
|
||||
for (let i = 0; i < batchCount; i++) {
|
||||
const p = await this.graphToPrompt(frontendState);
|
||||
|
||||
try {
|
||||
await this.api.queuePrompt(num, p);
|
||||
} catch (error) {
|
||||
// this.ui.dialog.show(error.response || error.toString());
|
||||
try {
|
||||
await this.api.queuePrompt(num, p);
|
||||
} catch (error) {
|
||||
// this.ui.dialog.show(error.response || error.toString());
|
||||
console.error(error.response || error.toString())
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
for (const n of p.workflow.nodes) {
|
||||
const node = this.lGraph.getNodeById(n.id);
|
||||
if (node.widgets) {
|
||||
for (const widget of node.widgets) {
|
||||
// Allow widgets to run callbacks after a prompt has been queued
|
||||
// e.g. random seed after every gen
|
||||
// if (widget.afterQueued) {
|
||||
// widget.afterQueued();
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const n of p.workflow.nodes) {
|
||||
const node = this.lGraph.getNodeById(n.id);
|
||||
if (node.widgets) {
|
||||
for (const widget of node.widgets) {
|
||||
// Allow widgets to run callbacks after a prompt has been queued
|
||||
// e.g. random seed after every gen
|
||||
// if (widget.afterQueued) {
|
||||
// widget.afterQueued();
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.lCanvas.draw(true, true);
|
||||
// await this.ui.queue.update();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.lCanvas.draw(true, true);
|
||||
// await this.ui.queue.update();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
console.log("Queue finished!");
|
||||
this.processingQueue = false;
|
||||
}
|
||||
}
|
||||
this.processingQueue = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads workflow data from the specified file
|
||||
*/
|
||||
async handleFile(file: File) {
|
||||
if (file.type === "image/png") {
|
||||
const pngInfo = await getPngMetadata(file);
|
||||
if (pngInfo) {
|
||||
if (pngInfo.workflow) {
|
||||
this.loadGraphData(JSON.parse(pngInfo.workflow));
|
||||
} else if (pngInfo.parameters) {
|
||||
importA1111(this.lGraph, pngInfo.parameters);
|
||||
}
|
||||
}
|
||||
} else if (file.type === "application/json" || file.name.endsWith(".json")) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
this.loadGraphData(JSON.parse(reader.result));
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Loads workflow data from the specified file
|
||||
*/
|
||||
async handleFile(file: File) {
|
||||
if (file.type === "image/png") {
|
||||
const pngInfo = await getPngMetadata(file);
|
||||
if (pngInfo) {
|
||||
if (pngInfo.workflow) {
|
||||
this.loadGraphData(JSON.parse(pngInfo.workflow));
|
||||
} else if (pngInfo.parameters) {
|
||||
importA1111(this.lGraph, pngInfo.parameters);
|
||||
}
|
||||
}
|
||||
} else if (file.type === "application/json" || file.name.endsWith(".json")) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
this.loadGraphData(JSON.parse(reader.result as string));
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
}
|
||||
|
||||
// registerExtension(extension) {
|
||||
// if (!extension.name) {
|
||||
// throw new Error("Extensions must have a 'name' property.");
|
||||
// }
|
||||
// if (this.extensions.find((ext) => ext.name === extension.name)) {
|
||||
// throw new Error(`Extension named '${extension.name}' already registered.`);
|
||||
// }
|
||||
// this.extensions.push(extension);
|
||||
// }
|
||||
// registerExtension(extension) {
|
||||
// if (!extension.name) {
|
||||
// throw new Error("Extensions must have a 'name' property.");
|
||||
// }
|
||||
// if (this.extensions.find((ext) => ext.name === extension.name)) {
|
||||
// throw new Error(`Extension named '${extension.name}' already registered.`);
|
||||
// }
|
||||
// this.extensions.push(extension);
|
||||
// }
|
||||
|
||||
/**
|
||||
* Refresh combo list on whole nodes
|
||||
*/
|
||||
async refreshComboInNodes() {
|
||||
const defs = await this.api.getNodeDefs();
|
||||
/**
|
||||
* Refresh combo list on whole nodes
|
||||
*/
|
||||
async refreshComboInNodes() {
|
||||
const defs = await this.api.getNodeDefs();
|
||||
|
||||
for(let nodeNum in this.lGraph._nodes) {
|
||||
const node = this.lGraph._nodes[nodeNum];
|
||||
for(let nodeNum in this.lGraph._nodes) {
|
||||
const node = this.lGraph._nodes[nodeNum];
|
||||
|
||||
const def = defs[node.type];
|
||||
const def = defs[node.type];
|
||||
|
||||
for(const widgetNum in node.widgets) {
|
||||
const widget = node.widgets[widgetNum]
|
||||
for(const widgetNum in node.widgets) {
|
||||
const widget = node.widgets[widgetNum]
|
||||
|
||||
if(widget.type == "combo" && def["input"]["required"][widget.name] !== undefined) {
|
||||
widget.options.values = def["input"]["required"][widget.name][0];
|
||||
if(widget.type == "combo" && def["input"]["required"][widget.name] !== undefined) {
|
||||
widget.options.values = def["input"]["required"][widget.name][0];
|
||||
|
||||
if(!widget.options.values.includes(widget.value)) {
|
||||
widget.value = widget.options.values[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!widget.options.values.includes(widget.value)) {
|
||||
widget.value = widget.options.values[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean current state
|
||||
*/
|
||||
clean() {
|
||||
this.nodeOutputs = {};
|
||||
}
|
||||
/**
|
||||
* Clean current state
|
||||
*/
|
||||
clean() {
|
||||
this.nodeOutputs = {};
|
||||
}
|
||||
}
|
||||
|
||||
165
src/lib/components/ComfyUIPane.svelte
Normal file
165
src/lib/components/ComfyUIPane.svelte
Normal file
@@ -0,0 +1,165 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "@gradio/button";
|
||||
import { Block, BlockTitle } from "@gradio/atoms";
|
||||
import { Dropdown, Range, TextBox } from "@gradio/form";
|
||||
import { LGraphNode, LGraph } from "litegraph.js";
|
||||
import type { IWidget } from "litegraph.js";
|
||||
import ComfyApp from "./ComfyApp";
|
||||
|
||||
export let app: ComfyApp;
|
||||
|
||||
export function clear() {
|
||||
nodes = {};
|
||||
items = {};
|
||||
state = {};
|
||||
}
|
||||
|
||||
export function addNodeUI(node: LGraphNode) {
|
||||
if (node.widgets) {
|
||||
for (const [i, widget] of node.widgets.entries()) {
|
||||
nodes[node.id] = node;
|
||||
|
||||
node.onPropertyChanged = (k, v) => {
|
||||
console.log("PROPCHANGE", k, v)
|
||||
};
|
||||
|
||||
if (!items[node.id]) {
|
||||
items[node.id] = []
|
||||
}
|
||||
items[node.id].push({ node, widget })
|
||||
|
||||
if (!state[node.id]) {
|
||||
state[node.id] = []
|
||||
}
|
||||
state[node.id].push(widget.value);
|
||||
}
|
||||
}
|
||||
|
||||
nodes = nodes;
|
||||
items = items;
|
||||
state = state;
|
||||
}
|
||||
|
||||
export function removeNodeUI(node: LGraphNode) {
|
||||
delete nodes[node.id]
|
||||
delete state[node.id]
|
||||
delete items[node.id]
|
||||
|
||||
nodes = nodes;
|
||||
items = items;
|
||||
state = state;
|
||||
}
|
||||
|
||||
export function configureFinished(graph: LGraph) {
|
||||
for (const node of graph.computeExecutionOrder(false, null)) {
|
||||
if (node.widgets_values) {
|
||||
for (const [j, value] of node.widgets_values.entries()) {
|
||||
state[node.id][j] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nodes = nodes;
|
||||
state = state;
|
||||
}
|
||||
|
||||
function getState() {
|
||||
|
||||
}
|
||||
|
||||
function queuePrompt() {
|
||||
console.log("Queuing!", state);
|
||||
app.queuePrompt(0, 1, state);
|
||||
}
|
||||
|
||||
let nodes: Record<number, LGraphNode> = {};
|
||||
let items: Record<number, { node: LGraphNode, widget: IWidget }[]> = {};
|
||||
let state: Record<number, any[]> = {};
|
||||
</script>
|
||||
|
||||
<div id="comfy-ui-panes">
|
||||
<div class="v-pane">
|
||||
{#each Object.keys(items) as id}
|
||||
{@const node = nodes[id]}
|
||||
<Block>
|
||||
<label for={id}>
|
||||
<BlockTitle>{node.title}</BlockTitle>
|
||||
</label>
|
||||
{#each items[id] as item, i}
|
||||
{#if item.widget.type == "combo"}
|
||||
<div class="wrapper">
|
||||
<Dropdown
|
||||
bind:value={state[id][i]}
|
||||
choices={item.widget.options.values}
|
||||
multiselect={false}
|
||||
max_choices={1},
|
||||
label={item.widget.name}
|
||||
show_label={true}
|
||||
disabled={item.widget.options.values.length === 0}
|
||||
on:change
|
||||
on:select
|
||||
on:blur
|
||||
/>
|
||||
</div>
|
||||
{:else if item.widget.type == "number"}
|
||||
<div class="wrapper">
|
||||
<Range
|
||||
bind:value={state[id][i]}
|
||||
minimum={item.widget.options.min}
|
||||
maximum={item.widget.options.max}
|
||||
step={item.widget.options.step}
|
||||
label={item.widget.name}
|
||||
show_label={true}
|
||||
on:change
|
||||
on:release
|
||||
/>
|
||||
</div>
|
||||
{:else if item.widget.type == "text"}
|
||||
<div class="wrapper">
|
||||
<TextBox
|
||||
bind:value={state[id][i]}
|
||||
label={item.widget.name}
|
||||
lines={item.widget.options.multiline ? 5 : 1}
|
||||
show_label={true}
|
||||
on:change
|
||||
on:submit
|
||||
on:blur
|
||||
on:select
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</Block>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="v-pane">
|
||||
</div>
|
||||
<div class="v-pane">
|
||||
<div class="wrapper">
|
||||
<Button variant="primary" on:click={queuePrompt}>
|
||||
Run
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#comfy-ui-panes {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.v-pane {
|
||||
border: 1px solid grey;
|
||||
float: left;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { IWidget, LGraphNode } from "litegraph.js";
|
||||
import type ComfyApp from "$lib/components/ComfyApp";
|
||||
|
||||
interface WidgetData {
|
||||
export interface WidgetData {
|
||||
widget: IWidget,
|
||||
minWidth?: number,
|
||||
minHeight?: number
|
||||
@@ -55,7 +55,7 @@ const STRING: WidgetFactory = (node: LGraphNode, inputName: string, inputData: a
|
||||
// if (multiline) {
|
||||
// return addMultilineWidget(node, inputName, { defaultVal, ...inputData[1] }, app);
|
||||
// } else {
|
||||
return { widget: node.addWidget("text", inputName, defaultVal, () => {}, {}) };
|
||||
return { widget: node.addWidget("text", inputName, defaultVal, () => {}, { multiline }) };
|
||||
// }
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user