Parse template, refactor layout panes
This commit is contained in:
@@ -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
31
pnpm-lock.yaml
generated
@@ -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:
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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[] {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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: () => { }
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
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
|
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() {
|
||||||
|
|||||||
@@ -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 = [];
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
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,
|
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)
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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/*"]
|
||||||
|
|||||||
Reference in New Issue
Block a user