Parse template, refactor layout panes

This commit is contained in:
space-nuko
2023-05-24 14:35:58 -05:00
parent b30ecd3166
commit f793630064
22 changed files with 1028 additions and 656 deletions

View File

@@ -72,6 +72,7 @@
"@litegraph-ts/tsconfig": "workspace:*", "@litegraph-ts/tsconfig": "workspace:*",
"@sveltejs/vite-plugin-svelte": "^2.1.1", "@sveltejs/vite-plugin-svelte": "^2.1.1",
"@tsconfig/svelte": "^4.0.1", "@tsconfig/svelte": "^4.0.1",
"@types/dompurify": "^3.0.2",
"@zerodevx/svelte-json-view": "^1.0.5", "@zerodevx/svelte-json-view": "^1.0.5",
"canvas-to-svg": "^1.0.3", "canvas-to-svg": "^1.0.3",
"cm6-theme-basic-dark": "^0.2.0", "cm6-theme-basic-dark": "^0.2.0",
@@ -79,6 +80,7 @@
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"csv": "^6.3.0", "csv": "^6.3.0",
"csv-parse": "^5.3.10", "csv-parse": "^5.3.10",
"dompurify": "^3.0.3",
"events": "^3.3.0", "events": "^3.3.0",
"framework7": "^8.0.3", "framework7": "^8.0.3",
"framework7-svelte": "^8.0.3", "framework7-svelte": "^8.0.3",
@@ -88,7 +90,6 @@
"radix-icons-svelte": "^1.2.1", "radix-icons-svelte": "^1.2.1",
"style-mod": "^4.0.3", "style-mod": "^4.0.3",
"svelte-bootstrap-icons": "^2.3.1", "svelte-bootstrap-icons": "^2.3.1",
"svelte-codemirror-editor": "^1.1.0",
"svelte-feather-icons": "^4.0.0", "svelte-feather-icons": "^4.0.0",
"svelte-preprocess": "^5.0.3", "svelte-preprocess": "^5.0.3",
"svelte-select": "^5.5.3", "svelte-select": "^5.5.3",

31
pnpm-lock.yaml generated
View File

@@ -94,6 +94,9 @@ importers:
'@tsconfig/svelte': '@tsconfig/svelte':
specifier: ^4.0.1 specifier: ^4.0.1
version: 4.0.1 version: 4.0.1
'@types/dompurify':
specifier: ^3.0.2
version: 3.0.2
'@zerodevx/svelte-json-view': '@zerodevx/svelte-json-view':
specifier: ^1.0.5 specifier: ^1.0.5
version: 1.0.5(svelte@3.58.0) version: 1.0.5(svelte@3.58.0)
@@ -115,6 +118,9 @@ importers:
csv-parse: csv-parse:
specifier: ^5.3.10 specifier: ^5.3.10
version: 5.3.10 version: 5.3.10
dompurify:
specifier: ^3.0.3
version: 3.0.3
events: events:
specifier: ^3.3.0 specifier: ^3.3.0
version: 3.3.0 version: 3.3.0
@@ -142,9 +148,6 @@ importers:
svelte-bootstrap-icons: svelte-bootstrap-icons:
specifier: ^2.3.1 specifier: ^2.3.1
version: 2.3.1 version: 2.3.1
svelte-codemirror-editor:
specifier: ^1.1.0
version: 1.1.0(codemirror@6.0.1)
svelte-feather-icons: svelte-feather-icons:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.0 version: 4.0.0
@@ -3235,6 +3238,12 @@ packages:
resolution: {integrity: sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==} resolution: {integrity: sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==}
dev: true dev: true
/@types/dompurify@3.0.2:
resolution: {integrity: sha512-YBL4ziFebbbfQfH5mlC+QTJsvh0oJUrWbmxKMyEdL7emlHJqGR2Qb34TEFKj+VCayBvjKy3xczMFNhugThUsfQ==}
dependencies:
'@types/trusted-types': 2.0.3
dev: false
/@types/estree@1.0.1: /@types/estree@1.0.1:
resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==}
@@ -3318,6 +3327,10 @@ packages:
resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==}
dev: true dev: true
/@types/trusted-types@2.0.3:
resolution: {integrity: sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==}
dev: false
/@types/uuid@9.0.1: /@types/uuid@9.0.1:
resolution: {integrity: sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==} resolution: {integrity: sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==}
dev: false dev: false
@@ -4562,6 +4575,10 @@ packages:
domelementtype: 2.3.0 domelementtype: 2.3.0
dev: false dev: false
/dompurify@3.0.3:
resolution: {integrity: sha512-axQ9zieHLnAnHh0sfAamKYiqXMJAVwu+LM/alQ7WDagoWessyWvMSFyW65CqF3owufNu8HBcE4cM2Vflu7YWcQ==}
dev: false
/domutils@2.8.0: /domutils@2.8.0:
resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
dependencies: dependencies:
@@ -8097,14 +8114,6 @@ packages:
- sugarss - sugarss
dev: true dev: true
/svelte-codemirror-editor@1.1.0(codemirror@6.0.1):
resolution: {integrity: sha512-wFdMIsZds5qzn3x2NbFUxDVU6Cn3rwFdq0035ypaFVgzTjJ90bnPm6IbrFA4OJz1ngIyfbIuPAPDjm7rJIr0gg==}
peerDependencies:
codemirror: ^6.0.0
dependencies:
codemirror: 6.0.1
dev: false
/svelte-dnd-action@0.9.22(svelte@3.58.0): /svelte-dnd-action@0.9.22(svelte@3.58.0):
resolution: {integrity: sha512-lOQJsNLM1QWv5mdxIkCVtk6k4lHCtLgfE59y8rs7iOM6erchbLC9hMEFYSveZz7biJV0mpg7yDSs4bj/RT/YkA==} resolution: {integrity: sha512-lOQJsNLM1QWv5mdxIkCVtk6k4lHCtLgfE59y8rs7iOM6erchbLC9hMEFYSveZz7biJV0mpg7yDSs4bj/RT/YkA==}
peerDependencies: peerDependencies:

View File

@@ -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 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 { ComfyWidgetNode } from "./nodes/widgets"
import type ComfyGraphCanvas from "./ComfyGraphCanvas" import type ComfyGraphCanvas from "./ComfyGraphCanvas"
@@ -11,18 +11,27 @@ import { download } from "./utils";
*/ */
export type ComfyBoxTemplate = { export type ComfyBoxTemplate = {
version: 1, version: 1,
metadata: ComfyBoxTemplateMetadata,
nodes: LGraphNode[], nodes: LGraphNode[],
links: LLink[], links: LLink[],
container?: DragItemEntry 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 * In ComfyBox a template contains a subset of nodes in the graph and the set of
* components they represent in the UI. * components they represent in the UI.
*/ */
export type SerializedComfyBoxTemplate = { export type SerializedComfyBoxTemplate = {
isComfyBoxTemplate: true,
version: 1, version: 1,
/*
* Serialized metadata
*/
metadata: ComfyBoxTemplateMetadata,
/* /*
* Serialized nodes * Serialized nodes
*/ */
@@ -31,7 +40,7 @@ export type SerializedComfyBoxTemplate = {
/* /*
* Serialized inner links * Serialized inner links
*/ */
links: SerializedLLink[], links: SerializedTemplateLink[],
/* /*
* Serialized container type drag item * Serialized container type drag item
@@ -39,8 +48,27 @@ export type SerializedComfyBoxTemplate = {
layout?: SerializedLayoutState layout?: SerializedLayoutState
} }
export type SerializedComfyBoxTemplateData = { function isSerializedComfyBoxTemplate(param: any): param is SerializedComfyBoxTemplate {
comfyBoxTemplate: 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 = { export type ComfyBoxTemplateError = {
@@ -117,7 +145,7 @@ function unescapeXml(safe) {
const TEMPLATE_SVG_PADDING: number = 50; 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 // Calculate the min max bounds for the nodes on the graph
const bounds = graph._nodes.reduce( const bounds = graph._nodes.reduce(
(p, n) => { (p, n) => {
@@ -227,7 +255,7 @@ function renderSvg(canvas: ComfyGraphCanvas, graph: LGraph, extraData: any, padd
return svg 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)); const nodeIds = new Set(nodes.map(n => n.id));
for (const node of nodes) { for (const node of nodes) {
@@ -247,13 +275,17 @@ function pruneDetachedLinks(nodes: SerializedLGraphNode[], links: SerializedLLin
} }
links = links.filter(l => { 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] 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 let graph: LGraph
if (template.nodes.length === 1 && template.nodes[0].is(Subgraph)) { if (template.nodes.length === 1 && template.nodes[0].is(Subgraph)) {
graph = template.nodes[0].subgraph graph = template.nodes[0].subgraph
@@ -266,27 +298,56 @@ export function serializeTemplate(canvas: ComfyGraphCanvas, template: ComfyBoxTe
if (layoutState == null) if (layoutState == null)
throw "Couldn't find layout for template being serialized!" throw "Couldn't find layout for template being serialized!"
const metadata = template.metadata;
let nodes = template.nodes.map(n => n.serialize()); 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); const layout = layoutState.serializeAtRoot(template.container.dragItem.id);
[nodes, links] = pruneDetachedLinks(nodes, links); [nodes, links] = pruneDetachedLinks(nodes, links);
let comfyBoxTemplate: SerializedComfyBoxTemplate = { let serTemplate: SerializedComfyBoxTemplate = {
isComfyBoxTemplate: true,
version: 1, version: 1,
nodes: nodes, metadata,
links: links, nodes,
layout: layout links,
layout,
} }
let templateData: SerializedComfyBoxTemplateData = { const svg = renderSvg(canvas, graph, serTemplate, TEMPLATE_SVG_PADDING)
comfyBoxTemplate
return { svg, template: serTemplate }
}
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));
}
} }
const svg = renderSvg(canvas, graph, templateData, TEMPLATE_SVG_PADDING) if (!isSerializedComfyBoxTemplate(template)) {
download("workflow.svg", svg, "image/svg+xml"); reject("Invalid template format!")
}
return comfyBoxTemplate else {
const result: SerializedComfyBoxTemplateAndSVG = {
svg, template
}
resolve(result)
}
};
reader.readAsText(file);
});
} }
@@ -325,6 +386,7 @@ export function createTemplate(nodes: LGraphNode[]): ComfyBoxTemplateResult {
return { return {
version: 1, version: 1,
metadata: { ...DEFAULT_TEMPLATE_METADATA },
nodes: nodes, nodes: nodes,
links: links, links: links,
container: container container: container
@@ -334,6 +396,7 @@ export function createTemplate(nodes: LGraphNode[]): ComfyBoxTemplateResult {
// No UI to serialize. // No UI to serialize.
return { return {
version: 1, version: 1,
metadata: { ...DEFAULT_TEMPLATE_METADATA },
nodes: nodes, nodes: nodes,
links: links, links: links,
} }

View File

@@ -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 GraphSync from "./GraphSync";
import EventEmitter from "events"; import EventEmitter from "events";
import type TypedEmitter from "typed-emitter"; import type TypedEmitter from "typed-emitter";
@@ -13,6 +13,7 @@ import type { WritableLayoutStateStore } from "./stores/layoutStates";
import layoutStates from "./stores/layoutStates"; import layoutStates from "./stores/layoutStates";
import type { ComfyBoxWorkflow, WorkflowInstID } from "./stores/workflowState"; import type { ComfyBoxWorkflow, WorkflowInstID } from "./stores/workflowState";
import workflowState from "./stores/workflowState"; import workflowState from "./stores/workflowState";
import type { SerializedComfyBoxTemplate } from "./ComfyBoxTemplate";
type ComfyGraphEvents = { type ComfyGraphEvents = {
configured: (graph: LGraph) => void configured: (graph: LGraph) => void

View File

@@ -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 { get, type Unsubscriber } from "svelte/store";
import type ComfyGraph from "./ComfyGraph"; import type ComfyGraph from "./ComfyGraph";
import type ComfyApp from "./components/ComfyApp"; import type ComfyApp from "./components/ComfyApp";
@@ -6,14 +6,40 @@ import { ComfyReroute } from "./nodes";
import layoutStates from "./stores/layoutStates"; import layoutStates from "./stores/layoutStates";
import queueState from "./stores/queueState"; import queueState from "./stores/queueState";
import selectionState from "./stores/selectionState"; 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 notify from "./notify";
import { v4 as uuidv4 } from "uuid"
import { download } from "./utils";
export type SerializedGraphCanvasState = { export type SerializedGraphCanvasState = {
offset: Vector2, offset: Vector2,
scale: number 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 { export default class ComfyGraphCanvas extends LGraphCanvas {
app: ComfyApp | null; app: ComfyApp | null;
private _unsubscribe: Unsubscriber; private _unsubscribe: Unsubscriber;
@@ -439,19 +465,49 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
this.graph.add(subgraph) 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( const templateNodeIDToNewNode: Record<NodeID, LGraphNode> = {}
{
content: "Convert to Subgraph",
has_submenu: false,
disabled: Object.keys(this.selected_nodes).length === 0,
callback: this.convertToSubgraph.bind(this)
},
)
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) { saveAsTemplate(_value: IContextMenuItem, _options, mouseEvent, prevMenu, node?: LGraphNode) {
@@ -470,6 +526,23 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
console.warn("TEMPLATEFOUND", template) console.warn("TEMPLATEFOUND", template)
const serialized = serializeTemplate(this, 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[] { override getNodeMenuOptions(node: LGraphNode): ContextMenuItem[] {

View File

@@ -116,7 +116,6 @@ export default class DanbooruTags {
}) })
console.log(`Parsed ${this.tags.length} tags in ${time / 1000}ms.`) console.log(`Parsed ${this.tags.length} tags in ${time / 1000}ms.`)
console.error(this.tags[0])
} }
autocomplete(context: CompletionContext): CompletionResult { autocomplete(context: CompletionContext): CompletionResult {

View File

@@ -219,8 +219,7 @@
.animation-wrapper { .animation-wrapper {
position: relative; position: relative;
flex-grow: 100; flex: 1 1 0%;
flex-basis: 0;
} }
.handle-widget:hover { .handle-widget:hover {

View File

@@ -261,7 +261,9 @@
.animation-wrapper { .animation-wrapper {
position: relative; position: relative;
flex-basis: 0%;
flex-grow: 100; flex-grow: 100;
flex-shrink: 100;
} }
.handle-hidden { .handle-hidden {

View File

@@ -5,6 +5,7 @@ import A1111PromptModal from "./modal/A1111PromptModal.svelte";
import ConfirmConvertWithMissingNodeTypesModal from "./modal/ConfirmConvertWithMissingNodeTypesModal.svelte"; import ConfirmConvertWithMissingNodeTypesModal from "./modal/ConfirmConvertWithMissingNodeTypesModal.svelte";
import MissingNodeTypesModal from "./modal/MissingNodeTypesModal.svelte"; import MissingNodeTypesModal from "./modal/MissingNodeTypesModal.svelte";
import WorkflowLoadErrorModal from "./modal/WorkflowLoadErrorModal.svelte"; import WorkflowLoadErrorModal from "./modal/WorkflowLoadErrorModal.svelte";
import EditTemplateModal from "./modal/EditTemplateModal.svelte";
import * as nodes from "$lib/nodes/index"; 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 { get, writable, type Writable } from "svelte/store";
import ComfyPromptSerializer, { isActiveBackendNode, UpstreamNodeLocator } from "./ComfyPromptSerializer"; import ComfyPromptSerializer, { isActiveBackendNode, UpstreamNodeLocator } from "./ComfyPromptSerializer";
import DanbooruTags from "$lib/DanbooruTags"; import DanbooruTags from "$lib/DanbooruTags";
import { deserializeTemplate } from "$lib/ComfyBoxTemplate";
export const COMFYBOX_SERIAL_VERSION = 1; export const COMFYBOX_SERIAL_VERSION = 1;
@@ -1001,6 +1003,24 @@ export default class ComfyApp {
} }
}; };
reader.readAsText(file); 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: () => { }
},
]
})
} }
} }

View File

@@ -11,7 +11,7 @@
import type { ComfyBoxWorkflow } from "$lib/stores/workflowState"; import type { ComfyBoxWorkflow } from "$lib/stores/workflowState";
export let app: ComfyApp; export let app: ComfyApp;
export let workflow: ComfyBoxWorkflow; export let workflow: ComfyBoxWorkflow | null = null;
let layoutState: WritableLayoutStateStore | null; let layoutState: WritableLayoutStateStore | null;
@@ -111,9 +111,9 @@
let menuPos = { x: 0, y: 0 }; let menuPos = { x: 0, y: 0 };
let showMenu = false; let showMenu = false;
$: $layoutState.isMenuOpen = showMenu; $: if (layoutState) $layoutState.isMenuOpen = showMenu;
$: if ($layoutState.root) { $: if (layoutState && $layoutState.root) {
root = $layoutState.root root = $layoutState.root
} else { } else {
root = null; root = null;
@@ -138,13 +138,14 @@
} }
</script> </script>
{#if layoutState != null} {#if workflow != null}
{#if layoutState != null}
<div id="comfy-workflow-view" on:contextmenu={onRightClick}> <div id="comfy-workflow-view" on:contextmenu={onRightClick}>
<WidgetContainer bind:dragItem={root} classes={["root-container"]} {layoutState} /> <WidgetContainer bind:dragItem={root} classes={["root-container"]} {layoutState} />
</div> </div>
{/if} {/if}
{#if showMenu} {#if showMenu}
<Menu {...menuPos} on:click={closeMenu} on:clickoutside={closeMenu}> <Menu {...menuPos} on:click={closeMenu} on:clickoutside={closeMenu}>
<MenuOption <MenuOption
isDisabled={$selectionState.currentSelection.length !== 1} isDisabled={$selectionState.currentSelection.length !== 1}
@@ -176,6 +177,11 @@
on:click={ungroup} on:click={ungroup}
text={isDeleteGroup ? "Delete Group" : "Ungroup"} /> text={isDeleteGroup ? "Delete Group" : "Ungroup"} />
</Menu> </Menu>
{/if}
{:else}
<div class="no-workflows">
<span>No workflow loaded</span>
</div>
{/if} {/if}
<style lang="scss"> <style lang="scss">
@@ -185,4 +191,17 @@
overflow: auto; 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> </style>

View File

@@ -3,14 +3,12 @@
import { PlusSquareDotted } from 'svelte-bootstrap-icons'; import { PlusSquareDotted } from 'svelte-bootstrap-icons';
import { Button } from "@gradio/button"; import { Button } from "@gradio/button";
import { BlockTitle } from "@gradio/atoms"; import { BlockTitle } from "@gradio/atoms";
import ComfyBoxWorkflowView from "./ComfyBoxWorkflowView.svelte";
import { Checkbox, TextBox } from "@gradio/form" import { Checkbox, TextBox } from "@gradio/form"
import ComfyQueue from "./ComfyQueue.svelte"; import ComfyQueue from "./ComfyQueue.svelte";
import ComfyUnlockUIButton from "./ComfyUnlockUIButton.svelte"; import ComfyUnlockUIButton from "./ComfyUnlockUIButton.svelte";
import ComfyGraphView from "./ComfyGraphView.svelte";
import { get, writable, type Writable } from "svelte/store"; import { get, writable, type Writable } from "svelte/store";
import ComfyProperties from "./ComfyProperties.svelte";
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";
import interfaceState from "$lib/stores/interfaceState";
import workflowState, { ComfyBoxWorkflow } from "$lib/stores/workflowState"; import workflowState, { ComfyBoxWorkflow } from "$lib/stores/workflowState";
import selectionState from "$lib/stores/selectionState"; import selectionState from "$lib/stores/selectionState";
import type ComfyApp from './ComfyApp'; import type ComfyApp from './ComfyApp';
@@ -19,6 +17,7 @@
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { cubicIn } from 'svelte/easing'; import { cubicIn } from 'svelte/easing';
import { truncateString } from '$lib/utils'; import { truncateString } from '$lib/utils';
import ComfyPaneView from './ComfyPaneView.svelte';
export let app: ComfyApp; export let app: ComfyApp;
export let uiTheme: string = "gradio-dark" // TODO config export let uiTheme: string = "gradio-dark" // TODO config
@@ -28,7 +27,6 @@
let containerElem: HTMLDivElement; let containerElem: HTMLDivElement;
let resizeTimeout: NodeJS.Timeout | null; let resizeTimeout: NodeJS.Timeout | null;
let alreadySetup: Writable<boolean> = writable(false);
let fileInput: HTMLInputElement = undefined; let fileInput: HTMLInputElement = undefined;
let loading = true; let loading = true;
@@ -46,10 +44,6 @@
refreshView(); refreshView();
}) })
$: if (app) {
alreadySetup = app.alreadySetup;
}
async function doRefreshCombos() { async function doRefreshCombos() {
await app.refreshComboInNodes(undefined, undefined, true) await app.refreshComboInNodes(undefined, undefined, true)
} }
@@ -73,7 +67,6 @@
$selectionState.currentSelection = [] $selectionState.currentSelection = []
let graphSize = 0; let graphSize = 0;
let graphTransitioning = false;
$: if (containerElem) { $: if (containerElem) {
const canvas = containerElem.querySelector<HTMLDivElement>("#graph-canvas") const canvas = containerElem.querySelector<HTMLDivElement>("#graph-canvas")
@@ -81,10 +74,10 @@
const paneNode = canvas.closest(".splitpanes__pane") const paneNode = canvas.closest(".splitpanes__pane")
if (paneNode) { if (paneNode) {
(paneNode as HTMLElement).ontransitionstart = () => { (paneNode as HTMLElement).ontransitionstart = () => {
graphTransitioning = true $interfaceState.graphTransitioning = true
} }
(paneNode as HTMLElement).ontransitionend = () => { (paneNode as HTMLElement).ontransitionend = () => {
graphTransitioning = false $interfaceState.graphTransitioning = false
app.resizeCanvas() app.resizeCanvas()
} }
} }
@@ -194,28 +187,20 @@
<div id="comfy-content" bind:this={containerElem} class:loading> <div id="comfy-content" bind:this={containerElem} class:loading>
<Splitpanes theme="comfy" on:resize={refreshView}> <Splitpanes theme="comfy" on:resize={refreshView}>
<Pane bind:size={propsSidebarSize}> <Pane bind:size={propsSidebarSize}>
<div class="sidebar-wrapper pane-wrapper"> <ComfyPaneView {app} mode="properties"/>
<ComfyProperties workflow={$workflowState.activeWorkflow} />
</div>
</Pane> </Pane>
<Pane> <Pane>
<Splitpanes theme="comfy" on:resize={refreshView} horizontal="{true}"> <Splitpanes theme="comfy" on:resize={refreshView} horizontal="{true}">
<Pane> <Pane>
{#if $workflowState.activeWorkflow != null} <ComfyPaneView {app} mode="activeWorkflow"/>
<ComfyBoxWorkflowView {app} workflow={$workflowState.activeWorkflow} />
{:else}
<span style:color="var(--body-text-color)">No workflow loaded</span>
{/if}
</Pane> </Pane>
<Pane bind:size={graphSize}> <Pane bind:size={graphSize}>
<ComfyGraphView {app} transitioning={graphTransitioning} /> <ComfyPaneView {app} mode="graph"/>
</Pane> </Pane>
</Splitpanes> </Splitpanes>
</Pane> </Pane>
<Pane bind:size={queueSidebarSize}> <Pane bind:size={queueSidebarSize}>
<div class="sidebar-wrapper pane-wrapper"> <ComfyPaneView {app} mode="queue"/>
<ComfyQueue {app} />
</div>
</Pane> </Pane>
</Splitpanes> </Splitpanes>
<div id="workflow-tabs"> <div id="workflow-tabs">
@@ -260,32 +245,32 @@
<div class="bottombar-content"> <div class="bottombar-content">
<div class="left"> <div class="left">
{#if workflow != null && workflow.attrs.queuePromptButtonName != ""} {#if workflow != null && workflow.attrs.queuePromptButtonName != ""}
<Button variant="primary" disabled={!$alreadySetup} on:click={queuePrompt}> <Button variant="primary" disabled={loading} on:click={queuePrompt}>
{workflow.attrs.queuePromptButtonName} {workflow.attrs.queuePromptButtonName}
</Button> </Button>
{/if} {/if}
<Button variant="secondary" disabled={!$alreadySetup} on:click={toggleGraph}> <Button variant="secondary" disabled={loading} on:click={toggleGraph}>
Toggle Graph Toggle Graph
</Button> </Button>
<Button variant="secondary" disabled={!$alreadySetup} on:click={toggleProps}> <Button variant="secondary" disabled={loading} on:click={toggleProps}>
Toggle Props Toggle Props
</Button> </Button>
<Button variant="secondary" disabled={!$alreadySetup} on:click={toggleQueue}> <Button variant="secondary" disabled={loading} on:click={toggleQueue}>
Toggle Queue Toggle Queue
</Button> </Button>
<Button variant="secondary" disabled={!$alreadySetup} on:click={doSave}> <Button variant="secondary" disabled={loading} on:click={doSave}>
Save Save
</Button> </Button>
<Button variant="secondary" disabled={!$alreadySetup} on:click={doSaveLocal}> <Button variant="secondary" disabled={loading} on:click={doSaveLocal}>
Save Local Save Local
</Button> </Button>
<Button variant="secondary" disabled={!$alreadySetup} on:click={doLoad}> <Button variant="secondary" disabled={loading} on:click={doLoad}>
Load Load
</Button> </Button>
<Button variant="secondary" disabled={!$alreadySetup} on:click={doLoadDefault}> <Button variant="secondary" disabled={loading} on:click={doLoadDefault}>
Load Default Load Default
</Button> </Button>
<Button variant="secondary" disabled={!$alreadySetup} on:click={doRefreshCombos}> <Button variant="secondary" disabled={loading} on:click={doRefreshCombos}>
🔄 🔄
</Button> </Button>
<!-- <Checkbox label="Lock Nodes" bind:value={$uiState.nodesLocked}/> <!-- <Checkbox label="Lock Nodes" bind:value={$uiState.nodesLocked}/>
@@ -486,18 +471,6 @@
} }
} }
.sidebar-wrapper {
width: 100%;
height: 100%;
}
:global(html, body) {
width: 100%;
height: 100%;
margin: 0px;
font-family: Arial;
}
:global(.splitpanes.comfy>.splitpanes__splitter) { :global(.splitpanes.comfy>.splitpanes__splitter) {
background: var(--comfy-splitpanes-background-fill); background: var(--comfy-splitpanes-background-fill);

View File

@@ -2,9 +2,9 @@
import { Button } from "@gradio/button"; import { Button } from "@gradio/button";
import type ComfyApp from "./ComfyApp"; 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 app: ComfyApp;
export let transitioning: boolean = false;
function doRecenter(): void { function doRecenter(): void {
app?.lCanvas?.recenter(); app?.lCanvas?.recenter();
@@ -17,7 +17,7 @@
<DropZone {app} /> <DropZone {app} />
</div> </div>
<div class="bar"> <div class="bar">
{#if !transitioning} {#if !$interfaceState.graphTransitioning}
<span class="left"> <span class="left">
<button on:click={doRecenter}>Recenter</button> <button on:click={doRecenter}>Recenter</button>
</span> </span>

View 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>

View File

@@ -211,7 +211,7 @@
await tick(); // Wait for list size to be recalculated await tick(); // Wait for list size to be recalculated
queueList.scroll({ top: queueList.scrollHeight }) 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() { async function updateFromHistory() {
@@ -219,7 +219,7 @@
if (queueList) { if (queueList) {
queueList.scrollTo(0, 0); queueList.scrollTo(0, 0);
} }
console.warn("[ComfyQueue] BUILDHISTORY", _entries, $queueCompleted) console.warn("[ComfyQueue] BUILDHISTORY", _entries.length, $queueCompleted.length)
} }
async function interrupt() { async function interrupt() {

View File

@@ -174,8 +174,6 @@
uploaded = true; uploaded = true;
} }
$: console.warn(imgWidth, imgHeight, "IMGSIZE!!")
function handle_clear(_e: CustomEvent<null>) { function handle_clear(_e: CustomEvent<null>) {
_value = null; _value = null;
value = []; value = [];

View File

@@ -222,8 +222,7 @@
.animation-wrapper { .animation-wrapper {
position: relative; position: relative;
flex-grow: 100; flex: 1 100 0%;
flex-basis: 0;
} }
.handle-widget:hover { .handle-widget:hover {

View 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>

View 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>

View File

@@ -9,6 +9,8 @@ export type InterfaceState = {
pointerNearLeft: boolean, pointerNearLeft: boolean,
showIndicator: boolean, showIndicator: boolean,
indicatorValue: any, indicatorValue: any,
graphTransitioning: boolean
} }
type InterfaceStateOps = { type InterfaceStateOps = {
@@ -22,6 +24,8 @@ const store: Writable<InterfaceState> = writable(
pointerNearLeft: false, pointerNearLeft: false,
showIndicator: false, showIndicator: false,
indicatorValue: null, indicatorValue: null,
graphTransitioning: false
}) })
const debounceDrag = debounce(() => { store.update(s => { s.showIndicator = false; return s }) }, 1000) const debounceDrag = debounce(() => { store.update(s => { s.showIndicator = false; return s }) }, 1000)

View File

@@ -29,7 +29,7 @@ const store: Writable<UIState> = writable(
uiEditMode: "widgets", uiEditMode: "widgets",
reconnecting: false, reconnecting: false,
isSavingToLocalStorage: false isSavingToLocalStorage: false,
}) })
function reconnecting() { function reconnecting() {

View File

@@ -6,10 +6,13 @@ body {
// Disable pull to refresh // Disable pull to refresh
overscroll-behavior-y: contain; overscroll-behavior-y: contain;
}
#app-root {
background: var(--body-background-fill); background: var(--body-background-fill);
width: 100%;
height: 100%;
margin: 0px;
font-family: Arial;
} }
:root { :root {

View File

@@ -10,6 +10,7 @@
"checkJs": true, "checkJs": true,
"strict": false, "strict": false,
"baseUrl": "./src", "baseUrl": "./src",
"typeRoots": ["./node_modules/@types/", "./src"],
"paths": { "paths": {
"$lib": ["lib"], "$lib": ["lib"],
"$lib/*": ["lib/*"] "$lib/*": ["lib/*"]