Parse template, refactor layout panes
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Subgraph, type LGraphNode, type LLink, type SerializedLGraphNode, type SerializedLLink, LGraph } from "@litegraph-ts/core"
|
||||
import { Subgraph, type LGraphNode, type LLink, type SerializedLGraphNode, type SerializedLLink, LGraph, type NodeID } from "@litegraph-ts/core"
|
||||
import layoutStates, { isComfyWidgetNode, type ContainerLayout, type SerializedDragEntry, type WidgetLayout, type DragItemID, type WritableLayoutStateStore, type DragItemEntry, type SerializedLayoutState } from "./stores/layoutStates"
|
||||
import type { ComfyWidgetNode } from "./nodes/widgets"
|
||||
import type ComfyGraphCanvas from "./ComfyGraphCanvas"
|
||||
@@ -11,18 +11,27 @@ import { download } from "./utils";
|
||||
*/
|
||||
export type ComfyBoxTemplate = {
|
||||
version: 1,
|
||||
metadata: ComfyBoxTemplateMetadata,
|
||||
nodes: LGraphNode[],
|
||||
links: LLink[],
|
||||
container?: DragItemEntry
|
||||
}
|
||||
|
||||
export type SerializedTemplateLink = [NodeID, number, NodeID, number];
|
||||
|
||||
/*
|
||||
* In ComfyBox a template contains a subset of nodes in the graph and the set of
|
||||
* components they represent in the UI.
|
||||
*/
|
||||
export type SerializedComfyBoxTemplate = {
|
||||
isComfyBoxTemplate: true,
|
||||
version: 1,
|
||||
|
||||
/*
|
||||
* Serialized metadata
|
||||
*/
|
||||
metadata: ComfyBoxTemplateMetadata,
|
||||
|
||||
/*
|
||||
* Serialized nodes
|
||||
*/
|
||||
@@ -31,7 +40,7 @@ export type SerializedComfyBoxTemplate = {
|
||||
/*
|
||||
* Serialized inner links
|
||||
*/
|
||||
links: SerializedLLink[],
|
||||
links: SerializedTemplateLink[],
|
||||
|
||||
/*
|
||||
* Serialized container type drag item
|
||||
@@ -39,8 +48,27 @@ export type SerializedComfyBoxTemplate = {
|
||||
layout?: SerializedLayoutState
|
||||
}
|
||||
|
||||
export type SerializedComfyBoxTemplateData = {
|
||||
comfyBoxTemplate: SerializedComfyBoxTemplate
|
||||
function isSerializedComfyBoxTemplate(param: any): param is SerializedComfyBoxTemplate {
|
||||
return param && param.isComfyBoxTemplate;
|
||||
}
|
||||
|
||||
export type SerializedComfyBoxTemplateAndSVG = {
|
||||
template: SerializedComfyBoxTemplate,
|
||||
svg: string,
|
||||
}
|
||||
|
||||
export type ComfyBoxTemplateMetadata = {
|
||||
title: string,
|
||||
author: string,
|
||||
tags: string[],
|
||||
|
||||
// TODO required/optional python extensions
|
||||
}
|
||||
|
||||
const DEFAULT_TEMPLATE_METADATA = {
|
||||
title: "New Template",
|
||||
author: "Anonymous",
|
||||
tags: []
|
||||
}
|
||||
|
||||
export type ComfyBoxTemplateError = {
|
||||
@@ -117,7 +145,7 @@ function unescapeXml(safe) {
|
||||
|
||||
const TEMPLATE_SVG_PADDING: number = 50;
|
||||
|
||||
function renderSvg(canvas: ComfyGraphCanvas, graph: LGraph, extraData: any, padding: number): string {
|
||||
function renderSvg(canvas: ComfyGraphCanvas, graph: LGraph, extraData: SerializedComfyBoxTemplate, padding: number): string {
|
||||
// Calculate the min max bounds for the nodes on the graph
|
||||
const bounds = graph._nodes.reduce(
|
||||
(p, n) => {
|
||||
@@ -227,7 +255,7 @@ function renderSvg(canvas: ComfyGraphCanvas, graph: LGraph, extraData: any, padd
|
||||
return svg
|
||||
}
|
||||
|
||||
function pruneDetachedLinks(nodes: SerializedLGraphNode[], links: SerializedLLink[]): [SerializedLGraphNode[], SerializedLLink[]] {
|
||||
function pruneDetachedLinks(nodes: SerializedLGraphNode[], links: SerializedTemplateLink[]): [SerializedLGraphNode[], SerializedTemplateLink[]] {
|
||||
const nodeIds = new Set(nodes.map(n => n.id));
|
||||
|
||||
for (const node of nodes) {
|
||||
@@ -247,13 +275,17 @@ function pruneDetachedLinks(nodes: SerializedLGraphNode[], links: SerializedLLin
|
||||
}
|
||||
|
||||
links = links.filter(l => {
|
||||
return nodeIds.has(l[1]) && nodeIds.has(l[3]);
|
||||
return nodeIds.has(l[0]) && nodeIds.has(l[2]);
|
||||
})
|
||||
|
||||
return [nodes, links]
|
||||
}
|
||||
|
||||
export function serializeTemplate(canvas: ComfyGraphCanvas, template: ComfyBoxTemplate): SerializedComfyBoxTemplate {
|
||||
function convLinkForTemplate(link: LLink): SerializedTemplateLink {
|
||||
return [link.origin_id, link.origin_slot, link.target_id, link.target_slot];
|
||||
}
|
||||
|
||||
export function serializeTemplate(canvas: ComfyGraphCanvas, template: ComfyBoxTemplate): SerializedComfyBoxTemplateAndSVG {
|
||||
let graph: LGraph
|
||||
if (template.nodes.length === 1 && template.nodes[0].is(Subgraph)) {
|
||||
graph = template.nodes[0].subgraph
|
||||
@@ -266,27 +298,56 @@ export function serializeTemplate(canvas: ComfyGraphCanvas, template: ComfyBoxTe
|
||||
if (layoutState == null)
|
||||
throw "Couldn't find layout for template being serialized!"
|
||||
|
||||
const metadata = template.metadata;
|
||||
let nodes = template.nodes.map(n => n.serialize());
|
||||
let links = template.links.map(l => l.serialize());
|
||||
let links = template.links.map(convLinkForTemplate);
|
||||
const layout = layoutState.serializeAtRoot(template.container.dragItem.id);
|
||||
|
||||
[nodes, links] = pruneDetachedLinks(nodes, links);
|
||||
|
||||
let comfyBoxTemplate: SerializedComfyBoxTemplate = {
|
||||
let serTemplate: SerializedComfyBoxTemplate = {
|
||||
isComfyBoxTemplate: true,
|
||||
version: 1,
|
||||
nodes: nodes,
|
||||
links: links,
|
||||
layout: layout
|
||||
metadata,
|
||||
nodes,
|
||||
links,
|
||||
layout,
|
||||
}
|
||||
|
||||
let templateData: SerializedComfyBoxTemplateData = {
|
||||
comfyBoxTemplate
|
||||
}
|
||||
const svg = renderSvg(canvas, graph, serTemplate, TEMPLATE_SVG_PADDING)
|
||||
|
||||
const svg = renderSvg(canvas, graph, templateData, TEMPLATE_SVG_PADDING)
|
||||
download("workflow.svg", svg, "image/svg+xml");
|
||||
return { svg, template: serTemplate }
|
||||
}
|
||||
|
||||
return comfyBoxTemplate
|
||||
export function deserializeTemplate(file: File): Promise<SerializedComfyBoxTemplateAndSVG> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async () => {
|
||||
const svg = reader.result as string;
|
||||
let template = null;
|
||||
|
||||
// Extract embedded workflow from desc tags
|
||||
const descEnd = svg.lastIndexOf("</desc>");
|
||||
if (descEnd !== -1) {
|
||||
const descStart = svg.lastIndexOf("<desc>", descEnd);
|
||||
if (descStart !== -1) {
|
||||
const json = svg.substring(descStart + 6, descEnd);
|
||||
template = JSON.parse(unescapeXml(json));
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSerializedComfyBoxTemplate(template)) {
|
||||
reject("Invalid template format!")
|
||||
}
|
||||
else {
|
||||
const result: SerializedComfyBoxTemplateAndSVG = {
|
||||
svg, template
|
||||
}
|
||||
resolve(result)
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -325,6 +386,7 @@ export function createTemplate(nodes: LGraphNode[]): ComfyBoxTemplateResult {
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
metadata: { ...DEFAULT_TEMPLATE_METADATA },
|
||||
nodes: nodes,
|
||||
links: links,
|
||||
container: container
|
||||
@@ -334,6 +396,7 @@ export function createTemplate(nodes: LGraphNode[]): ComfyBoxTemplateResult {
|
||||
// No UI to serialize.
|
||||
return {
|
||||
version: 1,
|
||||
metadata: { ...DEFAULT_TEMPLATE_METADATA },
|
||||
nodes: nodes,
|
||||
links: links,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LConnectionKind, LGraph, LGraphNode, type INodeSlot, type SlotIndex, LiteGraph, getStaticProperty, type LGraphAddNodeOptions, LGraphCanvas, type LGraphRemoveNodeOptions, Subgraph, type LGraphAddNodeMode } from "@litegraph-ts/core";
|
||||
import { LConnectionKind, LGraph, LGraphNode, type INodeSlot, type SlotIndex, LiteGraph, getStaticProperty, type LGraphAddNodeOptions, LGraphCanvas, type LGraphRemoveNodeOptions, Subgraph, type LGraphAddNodeMode, type SerializedLGraphNode, type Vector2 } from "@litegraph-ts/core";
|
||||
import GraphSync from "./GraphSync";
|
||||
import EventEmitter from "events";
|
||||
import type TypedEmitter from "typed-emitter";
|
||||
@@ -13,6 +13,7 @@ import type { WritableLayoutStateStore } from "./stores/layoutStates";
|
||||
import layoutStates from "./stores/layoutStates";
|
||||
import type { ComfyBoxWorkflow, WorkflowInstID } from "./stores/workflowState";
|
||||
import workflowState from "./stores/workflowState";
|
||||
import type { SerializedComfyBoxTemplate } from "./ComfyBoxTemplate";
|
||||
|
||||
type ComfyGraphEvents = {
|
||||
configured: (graph: LGraph) => void
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BuiltInSlotShape, LGraphCanvas, LGraphNode, LLink, LiteGraph, NodeMode, Subgraph, TitleMode, type ContextMenuItem, type IContextMenuItem, type NodeID, type Vector2, type Vector4, type MouseEventExt, ContextMenu } from "@litegraph-ts/core";
|
||||
import { BuiltInSlotShape, LGraphCanvas, LGraphNode, LLink, LiteGraph, NodeMode, Subgraph, TitleMode, type ContextMenuItem, type IContextMenuItem, type NodeID, type Vector2, type Vector4, type MouseEventExt, ContextMenu, type SerializedLGraphNode } from "@litegraph-ts/core";
|
||||
import { get, type Unsubscriber } from "svelte/store";
|
||||
import type ComfyGraph from "./ComfyGraph";
|
||||
import type ComfyApp from "./components/ComfyApp";
|
||||
@@ -6,14 +6,40 @@ import { ComfyReroute } from "./nodes";
|
||||
import layoutStates from "./stores/layoutStates";
|
||||
import queueState from "./stores/queueState";
|
||||
import selectionState from "./stores/selectionState";
|
||||
import { createTemplate, type ComfyBoxTemplate, serializeTemplate } from "./ComfyBoxTemplate";
|
||||
import { createTemplate, type ComfyBoxTemplate, serializeTemplate, type SerializedComfyBoxTemplate } from "./ComfyBoxTemplate";
|
||||
import notify from "./notify";
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { download } from "./utils";
|
||||
|
||||
export type SerializedGraphCanvasState = {
|
||||
offset: Vector2,
|
||||
scale: number
|
||||
}
|
||||
|
||||
function getMinPos(nodes: SerializedLGraphNode[]): Vector2 {
|
||||
var posMin: Vector2 = [0, 0]
|
||||
var posMinIndexes: [number, number] | null = null;
|
||||
|
||||
for (var i = 0; i < nodes.length; ++i) {
|
||||
if (posMin) {
|
||||
if (posMin[0] > nodes[i].pos[0]) {
|
||||
posMin[0] = nodes[i].pos[0];
|
||||
posMinIndexes[0] = i;
|
||||
}
|
||||
if (posMin[1] > nodes[i].pos[1]) {
|
||||
posMin[1] = nodes[i].pos[1];
|
||||
posMinIndexes[1] = i;
|
||||
}
|
||||
}
|
||||
else {
|
||||
posMin = [nodes[i].pos[0], nodes[i].pos[1]];
|
||||
posMinIndexes = [i, i];
|
||||
}
|
||||
}
|
||||
|
||||
return posMin;
|
||||
}
|
||||
|
||||
export default class ComfyGraphCanvas extends LGraphCanvas {
|
||||
app: ComfyApp | null;
|
||||
private _unsubscribe: Unsubscriber;
|
||||
@@ -439,19 +465,49 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
||||
this.graph.add(subgraph)
|
||||
}
|
||||
|
||||
override getCanvasMenuOptions(): ContextMenuItem[] {
|
||||
const options = super.getCanvasMenuOptions();
|
||||
/*
|
||||
* Inserts a ComfyBox template. Logic is similar to pasting from the
|
||||
* clipboard in vanilla litegraph.
|
||||
*/
|
||||
insertTemplate(template: SerializedComfyBoxTemplate, pos: Vector2) {
|
||||
const minPos = getMinPos(template.nodes);
|
||||
|
||||
options.push(
|
||||
{
|
||||
content: "Convert to Subgraph",
|
||||
has_submenu: false,
|
||||
disabled: Object.keys(this.selected_nodes).length === 0,
|
||||
callback: this.convertToSubgraph.bind(this)
|
||||
},
|
||||
)
|
||||
const templateNodeIDToNewNode: Record<NodeID, LGraphNode> = {}
|
||||
|
||||
return options
|
||||
var nodes = [];
|
||||
for (var i = 0; i < template.nodes.length; ++i) {
|
||||
var node_data = template.nodes[i];
|
||||
var node = LiteGraph.createNode(node_data.type);
|
||||
if (node) {
|
||||
const prevNodeId = node_data.id;
|
||||
node_data.id = uuidv4();
|
||||
templateNodeIDToNewNode[prevNodeId] = node
|
||||
|
||||
node.configure(node_data);
|
||||
|
||||
node.pos[0] += pos[0] - minPos[0]; //+= 5;
|
||||
node.pos[1] += pos[1] - minPos[1]; //+= 5;
|
||||
|
||||
this.graph.add(node, { doProcessChange: false, addedBy: "template" as any });
|
||||
|
||||
nodes.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
//create links
|
||||
for (var i = 0; i < template.links.length; ++i) {
|
||||
var link_info = template.links[i];
|
||||
var origin_node = templateNodeIDToNewNode[link_info[0]];
|
||||
var target_node = templateNodeIDToNewNode[link_info[2]];
|
||||
if (origin_node && target_node)
|
||||
origin_node.connect(link_info[1], target_node, link_info[3]);
|
||||
else
|
||||
console.error("[ComfyGraphCanvas] nodes missing on template insertion!", link_info);
|
||||
}
|
||||
|
||||
this.selectNodes(nodes);
|
||||
|
||||
this.graph.afterChange();
|
||||
}
|
||||
|
||||
saveAsTemplate(_value: IContextMenuItem, _options, mouseEvent, prevMenu, node?: LGraphNode) {
|
||||
@@ -470,6 +526,23 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
||||
console.warn("TEMPLATEFOUND", template)
|
||||
|
||||
const serialized = serializeTemplate(this, template);
|
||||
|
||||
download("template.svg", serialized.svg, "image/svg+xml");
|
||||
}
|
||||
|
||||
override getCanvasMenuOptions(): ContextMenuItem[] {
|
||||
const options = super.getCanvasMenuOptions();
|
||||
|
||||
options.push(
|
||||
{
|
||||
content: "Convert to Subgraph",
|
||||
has_submenu: false,
|
||||
disabled: Object.keys(this.selected_nodes).length === 0,
|
||||
callback: this.convertToSubgraph.bind(this)
|
||||
},
|
||||
)
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
override getNodeMenuOptions(node: LGraphNode): ContextMenuItem[] {
|
||||
|
||||
@@ -116,7 +116,6 @@ export default class DanbooruTags {
|
||||
})
|
||||
|
||||
console.log(`Parsed ${this.tags.length} tags in ${time / 1000}ms.`)
|
||||
console.error(this.tags[0])
|
||||
}
|
||||
|
||||
autocomplete(context: CompletionContext): CompletionResult {
|
||||
|
||||
@@ -219,8 +219,7 @@
|
||||
|
||||
.animation-wrapper {
|
||||
position: relative;
|
||||
flex-grow: 100;
|
||||
flex-basis: 0;
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
|
||||
.handle-widget:hover {
|
||||
|
||||
@@ -261,7 +261,9 @@
|
||||
|
||||
.animation-wrapper {
|
||||
position: relative;
|
||||
flex-basis: 0%;
|
||||
flex-grow: 100;
|
||||
flex-shrink: 100;
|
||||
}
|
||||
|
||||
.handle-hidden {
|
||||
|
||||
@@ -5,6 +5,7 @@ import A1111PromptModal from "./modal/A1111PromptModal.svelte";
|
||||
import ConfirmConvertWithMissingNodeTypesModal from "./modal/ConfirmConvertWithMissingNodeTypesModal.svelte";
|
||||
import MissingNodeTypesModal from "./modal/MissingNodeTypesModal.svelte";
|
||||
import WorkflowLoadErrorModal from "./modal/WorkflowLoadErrorModal.svelte";
|
||||
import EditTemplateModal from "./modal/EditTemplateModal.svelte";
|
||||
|
||||
import * as nodes from "$lib/nodes/index";
|
||||
|
||||
@@ -35,6 +36,7 @@ import { type SvelteComponentDev } from "svelte/internal";
|
||||
import { get, writable, type Writable } from "svelte/store";
|
||||
import ComfyPromptSerializer, { isActiveBackendNode, UpstreamNodeLocator } from "./ComfyPromptSerializer";
|
||||
import DanbooruTags from "$lib/DanbooruTags";
|
||||
import { deserializeTemplate } from "$lib/ComfyBoxTemplate";
|
||||
|
||||
export const COMFYBOX_SERIAL_VERSION = 1;
|
||||
|
||||
@@ -1001,6 +1003,24 @@ export default class ComfyApp {
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
} else if (file.type === "image/svg+xml" || file.name.endsWith(".svg")) {
|
||||
const templateAndSvg = await deserializeTemplate(file);
|
||||
modalState.pushModal({
|
||||
title: "ComfyBox Template Preview",
|
||||
svelteComponent: EditTemplateModal,
|
||||
closeOnClick: false,
|
||||
showCloseButton: false,
|
||||
svelteProps: {
|
||||
templateAndSvg
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
name: "Close",
|
||||
variant: "secondary",
|
||||
onClick: () => { }
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import type { ComfyBoxWorkflow } from "$lib/stores/workflowState";
|
||||
|
||||
export let app: ComfyApp;
|
||||
export let workflow: ComfyBoxWorkflow;
|
||||
export let workflow: ComfyBoxWorkflow | null = null;
|
||||
|
||||
let layoutState: WritableLayoutStateStore | null;
|
||||
|
||||
@@ -111,9 +111,9 @@
|
||||
let menuPos = { x: 0, y: 0 };
|
||||
let showMenu = false;
|
||||
|
||||
$: $layoutState.isMenuOpen = showMenu;
|
||||
$: if (layoutState) $layoutState.isMenuOpen = showMenu;
|
||||
|
||||
$: if ($layoutState.root) {
|
||||
$: if (layoutState && $layoutState.root) {
|
||||
root = $layoutState.root
|
||||
} else {
|
||||
root = null;
|
||||
@@ -138,44 +138,50 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if layoutState != null}
|
||||
<div id="comfy-workflow-view" on:contextmenu={onRightClick}>
|
||||
<WidgetContainer bind:dragItem={root} classes={["root-container"]} {layoutState} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if workflow != null}
|
||||
{#if layoutState != null}
|
||||
<div id="comfy-workflow-view" on:contextmenu={onRightClick}>
|
||||
<WidgetContainer bind:dragItem={root} classes={["root-container"]} {layoutState} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showMenu}
|
||||
<Menu {...menuPos} on:click={closeMenu} on:clickoutside={closeMenu}>
|
||||
<MenuOption
|
||||
isDisabled={$selectionState.currentSelection.length !== 1}
|
||||
on:click={() => moveUp()}
|
||||
text="Move Up" />
|
||||
<MenuOption
|
||||
isDisabled={$selectionState.currentSelection.length !== 1}
|
||||
on:click={() => moveDown()}
|
||||
text="Move Down" />
|
||||
<MenuOption
|
||||
isDisabled={$selectionState.currentSelection.length !== 1}
|
||||
on:click={() => sendToTop()}
|
||||
text="Send to Top" />
|
||||
<MenuOption
|
||||
isDisabled={$selectionState.currentSelection.length !== 1}
|
||||
on:click={() => sendToBottom()}
|
||||
text="Send to Bottom" />
|
||||
<MenuDivider/>
|
||||
<MenuOption
|
||||
isDisabled={$selectionState.currentSelection.length === 0}
|
||||
on:click={() => groupWidgets(false)}
|
||||
text="Group" />
|
||||
<MenuOption
|
||||
isDisabled={$selectionState.currentSelection.length === 0}
|
||||
on:click={() => groupWidgets(true)}
|
||||
text="Group Horizontally" />
|
||||
<MenuOption
|
||||
isDisabled={!canUngroup}
|
||||
on:click={ungroup}
|
||||
text={isDeleteGroup ? "Delete Group" : "Ungroup"} />
|
||||
</Menu>
|
||||
{#if showMenu}
|
||||
<Menu {...menuPos} on:click={closeMenu} on:clickoutside={closeMenu}>
|
||||
<MenuOption
|
||||
isDisabled={$selectionState.currentSelection.length !== 1}
|
||||
on:click={() => moveUp()}
|
||||
text="Move Up" />
|
||||
<MenuOption
|
||||
isDisabled={$selectionState.currentSelection.length !== 1}
|
||||
on:click={() => moveDown()}
|
||||
text="Move Down" />
|
||||
<MenuOption
|
||||
isDisabled={$selectionState.currentSelection.length !== 1}
|
||||
on:click={() => sendToTop()}
|
||||
text="Send to Top" />
|
||||
<MenuOption
|
||||
isDisabled={$selectionState.currentSelection.length !== 1}
|
||||
on:click={() => sendToBottom()}
|
||||
text="Send to Bottom" />
|
||||
<MenuDivider/>
|
||||
<MenuOption
|
||||
isDisabled={$selectionState.currentSelection.length === 0}
|
||||
on:click={() => groupWidgets(false)}
|
||||
text="Group" />
|
||||
<MenuOption
|
||||
isDisabled={$selectionState.currentSelection.length === 0}
|
||||
on:click={() => groupWidgets(true)}
|
||||
text="Group Horizontally" />
|
||||
<MenuOption
|
||||
isDisabled={!canUngroup}
|
||||
on:click={ungroup}
|
||||
text={isDeleteGroup ? "Delete Group" : "Ungroup"} />
|
||||
</Menu>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="no-workflows">
|
||||
<span>No workflow loaded</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
@@ -185,4 +191,17 @@
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.no-workflows {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
position: relative;
|
||||
color: var(--body-text-color);
|
||||
|
||||
> span {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "@gradio/button";
|
||||
import type ComfyApp from "./ComfyApp";
|
||||
import DropZone from "./DropZone.svelte";
|
||||
import DropZone from "./DropZone.svelte";
|
||||
import interfaceState from "$lib/stores/interfaceState";
|
||||
|
||||
export let app: ComfyApp;
|
||||
export let transitioning: boolean = false;
|
||||
|
||||
function doRecenter(): void {
|
||||
app?.lCanvas?.recenter();
|
||||
@@ -17,7 +17,7 @@
|
||||
<DropZone {app} />
|
||||
</div>
|
||||
<div class="bar">
|
||||
{#if !transitioning}
|
||||
{#if !$interfaceState.graphTransitioning}
|
||||
<span class="left">
|
||||
<button on:click={doRecenter}>Recenter</button>
|
||||
</span>
|
||||
|
||||
43
src/lib/components/ComfyPaneView.svelte
Normal file
43
src/lib/components/ComfyPaneView.svelte
Normal file
@@ -0,0 +1,43 @@
|
||||
<script context="module" lang="ts">
|
||||
export type ComfyPaneMode = "none" | "activeWorkflow" | "graph" | "properties" | "queue"
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
/*
|
||||
* A panel/sidebar that can be switched between different modes.
|
||||
*/
|
||||
import workflowState from "$lib/stores/workflowState";
|
||||
import type ComfyApp from "./ComfyApp";
|
||||
|
||||
import ComfyBoxWorkflowView from "./ComfyBoxWorkflowView.svelte";
|
||||
import ComfyGraphView from "./ComfyGraphView.svelte";
|
||||
import ComfyProperties from "./ComfyProperties.svelte";
|
||||
import ComfyQueue from "./ComfyQueue.svelte";
|
||||
|
||||
export let app: ComfyApp
|
||||
export let mode: ComfyPaneMode = "none";
|
||||
|
||||
</script>
|
||||
|
||||
<div class="pane-wrapper">
|
||||
{#if mode === "activeWorkflow"}
|
||||
<ComfyBoxWorkflowView {app} workflow={$workflowState.activeWorkflow} />
|
||||
{:else if mode === "graph"}
|
||||
<ComfyGraphView {app} />
|
||||
{:else if mode === "properties"}
|
||||
<ComfyProperties workflow={$workflowState.activeWorkflow} />
|
||||
{:else if mode === "queue"}
|
||||
<ComfyQueue {app} />
|
||||
{:else}
|
||||
<div class="blank-panel">(Blank)</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.pane-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -211,7 +211,7 @@
|
||||
await tick(); // Wait for list size to be recalculated
|
||||
queueList.scroll({ top: queueList.scrollHeight })
|
||||
}
|
||||
console.warn("[ComfyQueue] BUILDQUEUE", _entries, $queuePending, $queueRunning)
|
||||
console.warn("[ComfyQueue] BUILDQUEUE", _entries.length, $queuePending.length, $queueRunning.length)
|
||||
}
|
||||
|
||||
async function updateFromHistory() {
|
||||
@@ -219,7 +219,7 @@
|
||||
if (queueList) {
|
||||
queueList.scrollTo(0, 0);
|
||||
}
|
||||
console.warn("[ComfyQueue] BUILDHISTORY", _entries, $queueCompleted)
|
||||
console.warn("[ComfyQueue] BUILDHISTORY", _entries.length, $queueCompleted.length)
|
||||
}
|
||||
|
||||
async function interrupt() {
|
||||
|
||||
@@ -174,8 +174,6 @@
|
||||
uploaded = true;
|
||||
}
|
||||
|
||||
$: console.warn(imgWidth, imgHeight, "IMGSIZE!!")
|
||||
|
||||
function handle_clear(_e: CustomEvent<null>) {
|
||||
_value = null;
|
||||
value = [];
|
||||
|
||||
@@ -222,8 +222,7 @@
|
||||
|
||||
.animation-wrapper {
|
||||
position: relative;
|
||||
flex-grow: 100;
|
||||
flex-basis: 0;
|
||||
flex: 1 100 0%;
|
||||
}
|
||||
|
||||
.handle-widget:hover {
|
||||
|
||||
117
src/lib/components/modal/EditTemplateModal.svelte
Normal file
117
src/lib/components/modal/EditTemplateModal.svelte
Normal file
@@ -0,0 +1,117 @@
|
||||
<script lang="ts">
|
||||
import type { ComfyBoxTemplate, SerializedComfyBoxTemplateAndSVG } from "$lib/ComfyBoxTemplate";
|
||||
import type { SerializedDragEntry, SerializedLayoutState } from "$lib/stores/layoutStates";
|
||||
import { Block, BlockTitle } from "@gradio/atoms";
|
||||
import SerializedLayoutPreviewNode from "./SerializedLayoutPreviewNode.svelte";
|
||||
import Row from "../gradio/app/Row.svelte";
|
||||
import createDOMPurify from "dompurify"
|
||||
import Column from "../gradio/app/Column.svelte";
|
||||
import Accordion from "../gradio/app/Accordion.svelte";
|
||||
import Textbox from "@gradio/form/src/Textbox.svelte";
|
||||
const DOMPurify = createDOMPurify(window);
|
||||
|
||||
export let templateAndSvg: SerializedComfyBoxTemplateAndSVG;
|
||||
let layout: SerializedLayoutState | null
|
||||
let root: SerializedDragEntry | null
|
||||
|
||||
let saneSvg: string = "";
|
||||
|
||||
$: saneSvg = templateAndSvg
|
||||
? DOMPurify.sanitize(templateAndSvg.svg, { USE_PROFILES: { svg: true, svgFilters: true } })
|
||||
: "";
|
||||
|
||||
$: if (templateAndSvg) {
|
||||
layout = templateAndSvg.template.layout;
|
||||
if (layout) {
|
||||
root = layout.allItems[layout.root];
|
||||
}
|
||||
else {
|
||||
root = null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
layout = null;
|
||||
root = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="template-preview">
|
||||
<Row>
|
||||
<Column>
|
||||
<div class="template-metadata">
|
||||
<Block>
|
||||
<BlockTitle>Metadata</BlockTitle>
|
||||
<div>
|
||||
<Textbox label="Name" value="Text" lines={1} max_lines={1} />
|
||||
<Textbox label="Author" value="Text" lines={1} max_lines={1} />
|
||||
<Textbox label="Description" value="Text" lines={5} max_lines={5} />
|
||||
</div>
|
||||
</Block>
|
||||
</div>
|
||||
</Column>
|
||||
{#if root}
|
||||
<Column>
|
||||
<div class="template-layout-preview">
|
||||
<Block>
|
||||
<BlockTitle>Layout</BlockTitle>
|
||||
<SerializedLayoutPreviewNode {layout} entry={root} entryID={root.dragItem.id} />
|
||||
</Block>
|
||||
</div>
|
||||
</Column>
|
||||
{/if}
|
||||
</Row>
|
||||
<div class="template-graph-preview">
|
||||
<Block>
|
||||
<Accordion label="Graph">
|
||||
<Block>
|
||||
<div class="template-graph-wrapper">
|
||||
{@html saneSvg}
|
||||
</div>
|
||||
</Block>
|
||||
</Accordion>
|
||||
</Block>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.template-preview {
|
||||
width: 60vw;
|
||||
height: 70vh;
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
gap: var(--layout-gap);
|
||||
}
|
||||
|
||||
.template-metadata {
|
||||
position: relative;
|
||||
flex: 1 1 0%;
|
||||
:global(> .block) {
|
||||
background: var(--panel-background-fill);
|
||||
}
|
||||
}
|
||||
|
||||
.template-rows {
|
||||
}
|
||||
|
||||
.template-layout-preview {
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
:global(> .block) {
|
||||
background: var(--panel-background-fill);
|
||||
}
|
||||
}
|
||||
|
||||
.template-graph-preview {
|
||||
min-width: 0;
|
||||
:global(> .block) {
|
||||
background: var(--panel-background-fill);
|
||||
}
|
||||
}
|
||||
|
||||
.template-graph-wrapper {
|
||||
overflow: auto;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
48
src/lib/components/modal/SerializedLayoutPreviewNode.svelte
Normal file
48
src/lib/components/modal/SerializedLayoutPreviewNode.svelte
Normal file
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { type DragItemID, type SerializedDragEntry, type SerializedLayoutState } from "$lib/stores/layoutStates";
|
||||
import { Block, BlockTitle } from "@gradio/atoms";
|
||||
import Accordion from "../gradio/app/Accordion.svelte";
|
||||
|
||||
export let layout: SerializedLayoutState
|
||||
export let entryID: DragItemID
|
||||
export let entry: SerializedDragEntry
|
||||
</script>
|
||||
|
||||
{#if entry}
|
||||
{#if entry.dragItem.type === "container"}
|
||||
<div class="layout-container">
|
||||
<Block>
|
||||
<Accordion label={entry.dragItem.attrs.title || "(Container)"} open={true}>
|
||||
{#each entry.children as childID}
|
||||
{@const child = layout.allItems[childID]}
|
||||
<svelte:self {layout} entry={child} entryID={childID} />
|
||||
{/each}
|
||||
</Accordion>
|
||||
</Block>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="layout-widget">
|
||||
<Block>
|
||||
<BlockTitle>{entry.dragItem.attrs.title}</BlockTitle>
|
||||
</Block>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Block>
|
||||
Missing drag entry! {entryID}
|
||||
</Block>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.layout-container {
|
||||
:global(> .block) {
|
||||
background: var(--panel-background-fill);
|
||||
}
|
||||
}
|
||||
|
||||
.layout-widget {
|
||||
:global(> .block) {
|
||||
background: var(--block-background-fill);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -9,6 +9,8 @@ export type InterfaceState = {
|
||||
pointerNearLeft: boolean,
|
||||
showIndicator: boolean,
|
||||
indicatorValue: any,
|
||||
|
||||
graphTransitioning: boolean
|
||||
}
|
||||
|
||||
type InterfaceStateOps = {
|
||||
@@ -22,6 +24,8 @@ const store: Writable<InterfaceState> = writable(
|
||||
pointerNearLeft: false,
|
||||
showIndicator: false,
|
||||
indicatorValue: null,
|
||||
|
||||
graphTransitioning: false
|
||||
})
|
||||
|
||||
const debounceDrag = debounce(() => { store.update(s => { s.showIndicator = false; return s }) }, 1000)
|
||||
|
||||
@@ -29,7 +29,7 @@ const store: Writable<UIState> = writable(
|
||||
uiEditMode: "widgets",
|
||||
|
||||
reconnecting: false,
|
||||
isSavingToLocalStorage: false
|
||||
isSavingToLocalStorage: false,
|
||||
})
|
||||
|
||||
function reconnecting() {
|
||||
|
||||
Reference in New Issue
Block a user