Merge pull request #71 from space-nuko/subgraph-templates3
Subgraph templates
This commit is contained in:
Submodule litegraph updated: a1bf4cb511...8ca6cca777
5588
notebooks/ComfyBox_Colab.ipynb
Normal file
5588
notebooks/ComfyBox_Colab.ipynb
Normal file
File diff suppressed because it is too large
Load Diff
@@ -72,6 +72,7 @@
|
||||
"@litegraph-ts/tsconfig": "workspace:*",
|
||||
"@sveltejs/vite-plugin-svelte": "^2.1.1",
|
||||
"@tsconfig/svelte": "^4.0.1",
|
||||
"@types/dompurify": "^3.0.2",
|
||||
"@zerodevx/svelte-json-view": "^1.0.5",
|
||||
"canvas-to-svg": "^1.0.3",
|
||||
"cm6-theme-basic-dark": "^0.2.0",
|
||||
@@ -79,6 +80,7 @@
|
||||
"codemirror": "^6.0.1",
|
||||
"csv": "^6.3.0",
|
||||
"csv-parse": "^5.3.10",
|
||||
"dompurify": "^3.0.3",
|
||||
"events": "^3.3.0",
|
||||
"framework7": "^8.0.3",
|
||||
"framework7-svelte": "^8.0.3",
|
||||
@@ -88,8 +90,8 @@
|
||||
"radix-icons-svelte": "^1.2.1",
|
||||
"style-mod": "^4.0.3",
|
||||
"svelte-bootstrap-icons": "^2.3.1",
|
||||
"svelte-codemirror-editor": "^1.1.0",
|
||||
"svelte-feather-icons": "^4.0.0",
|
||||
"svelte-floating-ui": "^1.5.2",
|
||||
"svelte-preprocess": "^5.0.3",
|
||||
"svelte-select": "^5.5.3",
|
||||
"svelte-splitpanes": "^0.7.13",
|
||||
|
||||
41
pnpm-lock.yaml
generated
41
pnpm-lock.yaml
generated
@@ -94,6 +94,9 @@ importers:
|
||||
'@tsconfig/svelte':
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
'@types/dompurify':
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2
|
||||
'@zerodevx/svelte-json-view':
|
||||
specifier: ^1.0.5
|
||||
version: 1.0.5(svelte@3.58.0)
|
||||
@@ -115,6 +118,9 @@ importers:
|
||||
csv-parse:
|
||||
specifier: ^5.3.10
|
||||
version: 5.3.10
|
||||
dompurify:
|
||||
specifier: ^3.0.3
|
||||
version: 3.0.3
|
||||
events:
|
||||
specifier: ^3.3.0
|
||||
version: 3.3.0
|
||||
@@ -142,12 +148,12 @@ importers:
|
||||
svelte-bootstrap-icons:
|
||||
specifier: ^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:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
svelte-floating-ui:
|
||||
specifier: ^1.5.2
|
||||
version: 1.5.2
|
||||
svelte-preprocess:
|
||||
specifier: ^5.0.3
|
||||
version: 5.0.3(sass@1.61.0)(svelte@3.58.0)(typescript@5.0.3)
|
||||
@@ -3235,6 +3241,12 @@ packages:
|
||||
resolution: {integrity: sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==}
|
||||
|
||||
@@ -3318,6 +3330,10 @@ packages:
|
||||
resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==}
|
||||
dev: true
|
||||
|
||||
/@types/trusted-types@2.0.3:
|
||||
resolution: {integrity: sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==}
|
||||
dev: false
|
||||
|
||||
/@types/uuid@9.0.1:
|
||||
resolution: {integrity: sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==}
|
||||
dev: false
|
||||
@@ -4562,6 +4578,10 @@ packages:
|
||||
domelementtype: 2.3.0
|
||||
dev: false
|
||||
|
||||
/dompurify@3.0.3:
|
||||
resolution: {integrity: sha512-axQ9zieHLnAnHh0sfAamKYiqXMJAVwu+LM/alQ7WDagoWessyWvMSFyW65CqF3owufNu8HBcE4cM2Vflu7YWcQ==}
|
||||
dev: false
|
||||
|
||||
/domutils@2.8.0:
|
||||
resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
|
||||
dependencies:
|
||||
@@ -8097,14 +8117,6 @@ packages:
|
||||
- sugarss
|
||||
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):
|
||||
resolution: {integrity: sha512-lOQJsNLM1QWv5mdxIkCVtk6k4lHCtLgfE59y8rs7iOM6erchbLC9hMEFYSveZz7biJV0mpg7yDSs4bj/RT/YkA==}
|
||||
peerDependencies:
|
||||
@@ -8126,6 +8138,13 @@ packages:
|
||||
'@floating-ui/dom': 1.2.8
|
||||
dev: false
|
||||
|
||||
/svelte-floating-ui@1.5.2:
|
||||
resolution: {integrity: sha512-nV50eno74CEsFfFJ6iyN/oNYDEOck1TZjGV3lmJksVRbiiUAVF6bHspyAhR7GZ7c/4qbRWp9UyX24J+UXdEpag==}
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.2.6
|
||||
'@floating-ui/dom': 1.2.8
|
||||
dev: false
|
||||
|
||||
/svelte-hmr@0.15.1(svelte@3.58.0):
|
||||
resolution: {integrity: sha512-BiKB4RZ8YSwRKCNVdNxK/GfY+r4Kjgp9jCLEy0DuqAKfmQtpL38cQK3afdpjw4sqSs4PLi3jIPJIFp259NkZtA==}
|
||||
engines: {node: ^12.20 || ^14.13.1 || >= 16}
|
||||
|
||||
7
public/config.json
Normal file
7
public/config.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"comfyUIHostname": "localhost",
|
||||
"comfyUIPort": 8188,
|
||||
"alwaysStripUserState": false,
|
||||
"promptForWorkflowName": false,
|
||||
"confirmWhenUnloadingUnsavedChanges": true
|
||||
}
|
||||
BIN
public/image/graph-bg.png
Normal file
BIN
public/image/graph-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 350 B |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
"comfyBoxWorkflow": true,
|
||||
"createdBy": "ComfyBox",
|
||||
"version": 1,
|
||||
"commitHash": "test",
|
||||
"commitHash": "574d3170a4e829df366dc12c3aaa049121052d8f\n",
|
||||
"workflow": {
|
||||
"last_node_id": 0,
|
||||
"last_link_id": 0,
|
||||
@@ -176,7 +176,7 @@
|
||||
"widgets_values": [],
|
||||
"color": "#223",
|
||||
"bgColor": "#335",
|
||||
"comfyValue": "A",
|
||||
"comfyValue": null,
|
||||
"shownOutputProperties": {},
|
||||
"saveUserState": false
|
||||
},
|
||||
@@ -391,7 +391,20 @@
|
||||
"title": "UI.Gallery",
|
||||
"properties": {
|
||||
"tags": [],
|
||||
"defaultValue": null,
|
||||
"defaultValue": [
|
||||
{
|
||||
"isComfyBoxImageMetadata": true,
|
||||
"comfyUIFile": {
|
||||
"filename": "ComfyUI_04712_.png",
|
||||
"subfolder": "",
|
||||
"type": "output"
|
||||
},
|
||||
"name": "File",
|
||||
"tags": [],
|
||||
"width": 6656,
|
||||
"height": 4096
|
||||
}
|
||||
],
|
||||
"index": 0,
|
||||
"updateMode": "replace",
|
||||
"autoSelectOnUpdate": true
|
||||
@@ -399,7 +412,20 @@
|
||||
"widgets_values": [],
|
||||
"color": "#223",
|
||||
"bgColor": "#335",
|
||||
"comfyValue": [],
|
||||
"comfyValue": [
|
||||
{
|
||||
"isComfyBoxImageMetadata": true,
|
||||
"comfyUIFile": {
|
||||
"filename": "ComfyUI_04712_.png",
|
||||
"subfolder": "",
|
||||
"type": "output"
|
||||
},
|
||||
"name": "File",
|
||||
"tags": [],
|
||||
"width": 6656,
|
||||
"height": 4096
|
||||
}
|
||||
],
|
||||
"shownOutputProperties": {},
|
||||
"saveUserState": false
|
||||
},
|
||||
@@ -502,13 +528,39 @@
|
||||
],
|
||||
"title": "UI.ImageUpload",
|
||||
"properties": {
|
||||
"defaultValue": null,
|
||||
"defaultValue": [
|
||||
{
|
||||
"isComfyBoxImageMetadata": true,
|
||||
"comfyUIFile": {
|
||||
"filename": "ComfyUI_05835_.png",
|
||||
"type": "output",
|
||||
"subfolder": ""
|
||||
},
|
||||
"name": "File",
|
||||
"tags": [],
|
||||
"width": 640,
|
||||
"height": 768
|
||||
}
|
||||
],
|
||||
"tags": []
|
||||
},
|
||||
"widgets_values": [],
|
||||
"color": "#223",
|
||||
"bgColor": "#335",
|
||||
"comfyValue": [],
|
||||
"comfyValue": [
|
||||
{
|
||||
"isComfyBoxImageMetadata": true,
|
||||
"comfyUIFile": {
|
||||
"filename": "ComfyUI_05835_.png",
|
||||
"type": "output",
|
||||
"subfolder": ""
|
||||
},
|
||||
"name": "File",
|
||||
"tags": [],
|
||||
"width": 640,
|
||||
"height": 768
|
||||
}
|
||||
],
|
||||
"shownOutputProperties": {},
|
||||
"saveUserState": false
|
||||
},
|
||||
@@ -654,7 +706,8 @@
|
||||
"openOnStartup": false,
|
||||
"buttonVariant": "primary",
|
||||
"buttonSize": "large",
|
||||
"tags": []
|
||||
"tags": [],
|
||||
"destroyChildOnCLose": false
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
@@ -679,7 +732,8 @@
|
||||
"openOnStartup": false,
|
||||
"buttonVariant": "primary",
|
||||
"buttonSize": "large",
|
||||
"tags": []
|
||||
"tags": [],
|
||||
"destroyChildOnCLose": false
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
@@ -705,7 +759,8 @@
|
||||
"openOnStartup": false,
|
||||
"buttonVariant": "primary",
|
||||
"buttonSize": "large",
|
||||
"tags": []
|
||||
"tags": [],
|
||||
"destroyChildOnCLose": false
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
@@ -732,7 +787,8 @@
|
||||
"openOnStartup": false,
|
||||
"buttonVariant": "primary",
|
||||
"buttonSize": "large",
|
||||
"tags": []
|
||||
"tags": [],
|
||||
"destroyChildOnCLose": false
|
||||
}
|
||||
},
|
||||
"children": [],
|
||||
@@ -744,7 +800,7 @@
|
||||
"id": "2f0371e8-559e-4a58-a5d1-0a50117675fc",
|
||||
"nodeId": "578edfae-2767-4b23-9a3b-6edd8ccad1dd",
|
||||
"attrs": {
|
||||
"title": "model_name",
|
||||
"title": "Modal Name",
|
||||
"hidden": false,
|
||||
"disabled": false,
|
||||
"direction": "vertical",
|
||||
@@ -756,7 +812,8 @@
|
||||
"openOnStartup": false,
|
||||
"buttonVariant": "primary",
|
||||
"buttonSize": "large",
|
||||
"tags": []
|
||||
"tags": [],
|
||||
"destroyChildOnCLose": false
|
||||
}
|
||||
},
|
||||
"children": [],
|
||||
@@ -779,7 +836,8 @@
|
||||
"openOnStartup": false,
|
||||
"buttonVariant": "primary",
|
||||
"buttonSize": "large",
|
||||
"tags": []
|
||||
"tags": [],
|
||||
"destroyChildOnCLose": false
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
@@ -793,7 +851,7 @@
|
||||
"id": "db12215f-fd9c-463f-9fbd-26b79e994e0e",
|
||||
"nodeId": "59feacdf-de02-4b1e-b8ba-e219ba5126b7",
|
||||
"attrs": {
|
||||
"title": "filename_prefix",
|
||||
"title": "Filename Prefix",
|
||||
"hidden": false,
|
||||
"disabled": false,
|
||||
"direction": "vertical",
|
||||
@@ -805,7 +863,8 @@
|
||||
"openOnStartup": false,
|
||||
"buttonVariant": "primary",
|
||||
"buttonSize": "large",
|
||||
"tags": []
|
||||
"tags": [],
|
||||
"destroyChildOnCLose": false
|
||||
}
|
||||
},
|
||||
"children": [],
|
||||
@@ -828,7 +887,8 @@
|
||||
"openOnStartup": false,
|
||||
"buttonVariant": "primary",
|
||||
"buttonSize": "large",
|
||||
"tags": []
|
||||
"tags": [],
|
||||
"destroyChildOnCLose": false
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
@@ -849,12 +909,13 @@
|
||||
"classes": "",
|
||||
"style": "",
|
||||
"nodeDisabledState": "hidden",
|
||||
"variant": "gallery",
|
||||
"variant": "image",
|
||||
"containerVariant": "hidden",
|
||||
"openOnStartup": false,
|
||||
"buttonVariant": "primary",
|
||||
"buttonSize": "large",
|
||||
"tags": []
|
||||
"tags": [],
|
||||
"destroyChildOnCLose": false
|
||||
}
|
||||
},
|
||||
"children": [],
|
||||
8
src/global.d.ts
vendored
Normal file
8
src/global.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
declare type Item = import('svelte-dnd-action').Item;
|
||||
declare type DndEvent<ItemType = Item> = import('svelte-dnd-action').DndEvent<ItemType>;
|
||||
declare namespace svelte.JSX {
|
||||
interface HTMLAttributes<T> {
|
||||
onconsider?: (event: CustomEvent<DndEvent<ItemType>> & { target: EventTarget & T }) => void;
|
||||
onfinalize?: (event: CustomEvent<DndEvent<ItemType>> & { target: EventTarget & T }) => void;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
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, type UUID, type Vector2 } 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"
|
||||
import C2S from "canvas-to-svg";
|
||||
import { download } from "./utils";
|
||||
import { calcNodesBoundingBox, download } from "./utils";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import uiState from "./stores/uiState";
|
||||
|
||||
/*
|
||||
* In ComfyBox a template contains a subset of nodes in the graph and the set of
|
||||
@@ -11,17 +13,40 @@ import { download } from "./utils";
|
||||
*/
|
||||
export type ComfyBoxTemplate = {
|
||||
version: 1,
|
||||
id: UUID,
|
||||
metadata: ComfyBoxTemplateMetadata,
|
||||
nodes: LGraphNode[],
|
||||
links: LLink[],
|
||||
container?: DragItemEntry
|
||||
}
|
||||
|
||||
export type SerializedTemplateLink = [NodeID, number, NodeID, number];
|
||||
|
||||
export type ComfyBoxTemplateMetadata = {
|
||||
title: string,
|
||||
author: string,
|
||||
description: string,
|
||||
tags: string[],
|
||||
category: string,
|
||||
createdAt: number
|
||||
|
||||
// TODO required/optional python extensions
|
||||
}
|
||||
|
||||
/*
|
||||
* 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,
|
||||
id: UUID,
|
||||
commitHash: string,
|
||||
|
||||
/*
|
||||
* Serialized metadata
|
||||
*/
|
||||
metadata: ComfyBoxTemplateMetadata,
|
||||
|
||||
/*
|
||||
* Serialized nodes
|
||||
@@ -31,16 +56,29 @@ export type SerializedComfyBoxTemplate = {
|
||||
/*
|
||||
* Serialized inner links
|
||||
*/
|
||||
links: SerializedLLink[],
|
||||
links: SerializedTemplateLink[],
|
||||
|
||||
/*
|
||||
* Serialized container type drag item
|
||||
*/
|
||||
layout?: SerializedLayoutState
|
||||
|
||||
/*
|
||||
* SVG of the graph ndoes
|
||||
*/
|
||||
svg?: string
|
||||
}
|
||||
|
||||
export type SerializedComfyBoxTemplateData = {
|
||||
comfyBoxTemplate: SerializedComfyBoxTemplate
|
||||
function isSerializedComfyBoxTemplate(param: any): param is SerializedComfyBoxTemplate {
|
||||
return param && param.isComfyBoxTemplate;
|
||||
}
|
||||
|
||||
const DEFAULT_TEMPLATE_METADATA = {
|
||||
title: "New Template",
|
||||
author: "Anonymous",
|
||||
description: "A brand-new ComfyBox template",
|
||||
tags: [],
|
||||
category: "general"
|
||||
}
|
||||
|
||||
export type ComfyBoxTemplateError = {
|
||||
@@ -117,7 +155,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, padding: number, extraData?: SerializedComfyBoxTemplate | null): string {
|
||||
// Calculate the min max bounds for the nodes on the graph
|
||||
const bounds = graph._nodes.reduce(
|
||||
(p, n) => {
|
||||
@@ -146,6 +184,7 @@ function renderSvg(canvas: ComfyGraphCanvas, graph: LGraph, extraData: any, padd
|
||||
const offset = canvas.ds.offset;
|
||||
const show_info = canvas.show_info;
|
||||
const background_image = canvas.background_image;
|
||||
const clear_background = canvas.clear_background;
|
||||
const render_canvas_border = canvas.render_canvas_border;
|
||||
const render_subgraph_panels = canvas.render_subgraph_panels
|
||||
const render_subgraph_stack_header = canvas.render_subgraph_stack_header
|
||||
@@ -153,6 +192,7 @@ function renderSvg(canvas: ComfyGraphCanvas, graph: LGraph, extraData: any, padd
|
||||
canvas.openSubgraph(graph)
|
||||
canvas.show_info = false;
|
||||
canvas.background_image = null;
|
||||
canvas.clear_background = false;
|
||||
canvas.render_canvas_border = false;
|
||||
canvas.render_subgraph_panels = false;
|
||||
canvas.render_subgraph_stack_header = false;
|
||||
@@ -197,12 +237,10 @@ function renderSvg(canvas: ComfyGraphCanvas, graph: LGraph, extraData: any, padd
|
||||
canvas.ds.offset = [-bounds[0], -bounds[1]];
|
||||
canvas.ctx = svgCtx;
|
||||
|
||||
let saving = false;
|
||||
|
||||
// Trigger saving
|
||||
saving = true;
|
||||
canvas.isExportingSVG = true;
|
||||
canvas.draw(true, true);
|
||||
saving = false;
|
||||
canvas.isExportingSVG = false;
|
||||
|
||||
// Restore original settings
|
||||
canvas.closeSubgraph();
|
||||
@@ -212,6 +250,7 @@ function renderSvg(canvas: ComfyGraphCanvas, graph: LGraph, extraData: any, padd
|
||||
canvas.ds.offset = offset;
|
||||
canvas.ctx = ctx;
|
||||
canvas.show_info = show_info;
|
||||
canvas.clear_background = clear_background;
|
||||
canvas.background_image = background_image;
|
||||
canvas.render_canvas_border = render_canvas_border;
|
||||
canvas.render_subgraph_panels = render_subgraph_panels;
|
||||
@@ -219,15 +258,33 @@ function renderSvg(canvas: ComfyGraphCanvas, graph: LGraph, extraData: any, padd
|
||||
|
||||
canvas.draw(true, true);
|
||||
|
||||
// Convert to SVG, embed graph and save
|
||||
// const json = JSON.stringify(app.graph.serialize());
|
||||
const json = JSON.stringify(extraData);
|
||||
const svg = svgCtx.getSerializedSvg(true).replace("</svg>", `<desc>${escapeXml(json)}</desc></svg>`);
|
||||
|
||||
let svg = svgCtx.getSerializedSvg(true)
|
||||
return svg
|
||||
}
|
||||
|
||||
function pruneDetachedLinks(nodes: SerializedLGraphNode[], links: SerializedLLink[]): [SerializedLGraphNode[], SerializedLLink[]] {
|
||||
export function embedTemplateInSvg(template: SerializedComfyBoxTemplate): string {
|
||||
let oldSvg = template.svg;
|
||||
template.svg = undefined;
|
||||
const json = JSON.stringify(template);
|
||||
const svg = oldSvg.replace("</svg>", `<desc>${escapeXml(json)}</desc></svg>`);
|
||||
template.svg = oldSvg;
|
||||
return svg
|
||||
}
|
||||
|
||||
/*
|
||||
* Moves nodes so their origin is at (0, 0)
|
||||
*/
|
||||
function relocateNodes(nodes: SerializedLGraphNode[]): SerializedLGraphNode[] {
|
||||
let [min_x, min_y, max_x, max_y] = calcNodesBoundingBox(nodes);
|
||||
|
||||
for (const node of nodes) {
|
||||
node.pos = [node.pos[0] - min_x, node.pos[1] - min_y];
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function pruneDetachedLinks(nodes: SerializedLGraphNode[], links: SerializedTemplateLink[]): [SerializedLGraphNode[], SerializedTemplateLink[]] {
|
||||
const nodeIds = new Set(nodes.map(n => n.id));
|
||||
|
||||
for (const node of nodes) {
|
||||
@@ -247,12 +304,16 @@ 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]
|
||||
}
|
||||
|
||||
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): SerializedComfyBoxTemplate {
|
||||
let graph: LGraph
|
||||
if (template.nodes.length === 1 && template.nodes[0].is(Subgraph)) {
|
||||
@@ -266,27 +327,62 @@ export function serializeTemplate(canvas: ComfyGraphCanvas, template: ComfyBoxTe
|
||||
if (layoutState == null)
|
||||
throw "Couldn't find layout for template being serialized!"
|
||||
|
||||
uiState.update(s => { s.forceSaveUserState = false; return s; });
|
||||
|
||||
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);
|
||||
|
||||
uiState.update(s => { s.forceSaveUserState = null; return s; });
|
||||
|
||||
nodes = relocateNodes(nodes);
|
||||
[nodes, links] = pruneDetachedLinks(nodes, links);
|
||||
|
||||
let comfyBoxTemplate: SerializedComfyBoxTemplate = {
|
||||
const svg = renderSvg(canvas, graph, TEMPLATE_SVG_PADDING);
|
||||
|
||||
const serTemplate: SerializedComfyBoxTemplate = {
|
||||
isComfyBoxTemplate: true,
|
||||
version: 1,
|
||||
nodes: nodes,
|
||||
links: links,
|
||||
layout: layout
|
||||
commitHash: __GIT_COMMIT_HASH__,
|
||||
id: template.id,
|
||||
metadata,
|
||||
nodes,
|
||||
links,
|
||||
layout,
|
||||
svg
|
||||
}
|
||||
|
||||
let templateData: SerializedComfyBoxTemplateData = {
|
||||
comfyBoxTemplate
|
||||
}
|
||||
return serTemplate;
|
||||
}
|
||||
|
||||
const svg = renderSvg(canvas, graph, templateData, TEMPLATE_SVG_PADDING)
|
||||
download("workflow.svg", svg, "image/svg+xml");
|
||||
export function deserializeTemplateFromSVG(file: File): Promise<SerializedComfyBoxTemplate> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async () => {
|
||||
const svg = reader.result as string;
|
||||
let template = null;
|
||||
|
||||
return comfyBoxTemplate
|
||||
// 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 {
|
||||
template.svg = svg;
|
||||
resolve(template)
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -303,10 +399,15 @@ export function createTemplate(nodes: LGraphNode[]): ComfyBoxTemplateResult {
|
||||
const layout = layoutStates.getLayoutByNode(nodes[0])
|
||||
if (layout == null) {
|
||||
return {
|
||||
error: "Subgraph not contained in a layout!"
|
||||
error: "Node(s) not contained in a layout!"
|
||||
}
|
||||
}
|
||||
|
||||
let title = "New Template";
|
||||
if (nodes.length === 1) {
|
||||
title = nodes[0].title || title;
|
||||
}
|
||||
|
||||
if (widgetNodes.length > 0) {
|
||||
// Find the highest-level container that contains all these nodes and
|
||||
// contains no other widgets
|
||||
@@ -325,6 +426,8 @@ export function createTemplate(nodes: LGraphNode[]): ComfyBoxTemplateResult {
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
id: uuidv4(),
|
||||
metadata: { ...DEFAULT_TEMPLATE_METADATA, title, createdAt: Date.now() },
|
||||
nodes: nodes,
|
||||
links: links,
|
||||
container: container
|
||||
@@ -334,6 +437,8 @@ export function createTemplate(nodes: LGraphNode[]): ComfyBoxTemplateResult {
|
||||
// No UI to serialize.
|
||||
return {
|
||||
version: 1,
|
||||
id: uuidv4(),
|
||||
metadata: { ...DEFAULT_TEMPLATE_METADATA, title, createdAt: Date.now() },
|
||||
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, type NodeID, reassignGraphIDs, type GraphIDMapping, type SerializedLGraph } from "@litegraph-ts/core";
|
||||
import GraphSync from "./GraphSync";
|
||||
import EventEmitter from "events";
|
||||
import type TypedEmitter from "typed-emitter";
|
||||
@@ -11,8 +11,10 @@ import type { ComfyComboNode, ComfyWidgetNode } from "./nodes/widgets";
|
||||
import selectionState from "./stores/selectionState";
|
||||
import type { WritableLayoutStateStore } from "./stores/layoutStates";
|
||||
import layoutStates from "./stores/layoutStates";
|
||||
import type { ComfyWorkflow, WorkflowInstID } from "./stores/workflowState";
|
||||
import type { ComfyBoxWorkflow, WorkflowInstID } from "./stores/workflowState";
|
||||
import workflowState from "./stores/workflowState";
|
||||
import type { SerializedComfyBoxTemplate } from "./ComfyBoxTemplate";
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
|
||||
type ComfyGraphEvents = {
|
||||
configured: (graph: LGraph) => void
|
||||
@@ -30,13 +32,17 @@ export default class ComfyGraph extends LGraph {
|
||||
|
||||
workflowID: WorkflowInstID | null = null;
|
||||
|
||||
get workflow(): ComfyWorkflow | null {
|
||||
get workflow(): ComfyBoxWorkflow | null {
|
||||
const workflowID = (this.getRootGraph() as ComfyGraph)?.workflowID;
|
||||
if (workflowID == null)
|
||||
return null;
|
||||
return workflowState.getWorkflow(workflowID)
|
||||
}
|
||||
|
||||
get layout(): WritableLayoutStateStore | null {
|
||||
return this.workflow?.layout;
|
||||
}
|
||||
|
||||
constructor(workflowID?: WorkflowInstID) {
|
||||
super();
|
||||
this.workflowID = workflowID;
|
||||
@@ -217,4 +223,63 @@ export default class ComfyGraph extends LGraph {
|
||||
// console.debug("ConnectionChange", node);
|
||||
this.eventBus.emit("nodeConnectionChanged", kind, node, slot, targetNode, targetSlot);
|
||||
}
|
||||
|
||||
/*
|
||||
* Inserts a template.
|
||||
* Layout deserialization must be handled afterwards.
|
||||
* NOTE: Modifies the template in-place, be sure you cloned it beforehand!
|
||||
*/
|
||||
insertTemplate(template: SerializedComfyBoxTemplate, pos: Vector2): Record<NodeID, LGraphNode> {
|
||||
const minPos = [0, 0]
|
||||
|
||||
const templateNodeIDToNewNode: Record<NodeID, LGraphNode> = {}
|
||||
|
||||
var nodes = [];
|
||||
for (var i = 0; i < template.nodes.length; ++i) {
|
||||
var node_data = template.nodes[i];
|
||||
var node = LiteGraph.createNode(node_data.type);
|
||||
|
||||
let mapping: GraphIDMapping = null;
|
||||
if (node_data.type === "graph/subgraph") {
|
||||
mapping = reassignGraphIDs((node_data as any).subgraph as SerializedLGraph);
|
||||
}
|
||||
|
||||
if (node) {
|
||||
const prevNodeId = node_data.id;
|
||||
node_data.id = uuidv4();
|
||||
templateNodeIDToNewNode[prevNodeId] = node
|
||||
|
||||
node.configure(node_data);
|
||||
|
||||
if (mapping) {
|
||||
for (const subnode of (node as Subgraph).subgraph.iterateNodesInOrderRecursive()) {
|
||||
const oldNodeID = mapping.nodeIDs[subnode.id];
|
||||
templateNodeIDToNewNode[oldNodeID] = subnode;
|
||||
}
|
||||
}
|
||||
|
||||
node.pos[0] += pos[0] - minPos[0]; //+= 5;
|
||||
node.pos[1] += pos[1] - minPos[1]; //+= 5;
|
||||
|
||||
this.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.afterChange();
|
||||
|
||||
return templateNodeIDToNewNode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
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";
|
||||
import { ComfyReroute } from "./nodes";
|
||||
import layoutStates from "./stores/layoutStates";
|
||||
import layoutStates, { type ContainerLayout } from "./stores/layoutStates";
|
||||
import queueState from "./stores/queueState";
|
||||
import selectionState from "./stores/selectionState";
|
||||
import { createTemplate, type ComfyBoxTemplate, serializeTemplate } from "./ComfyBoxTemplate";
|
||||
import templateState from "./stores/templateState";
|
||||
import { createTemplate, type ComfyBoxTemplate, serializeTemplate, type SerializedComfyBoxTemplate } from "./ComfyBoxTemplate";
|
||||
import notify from "./notify";
|
||||
import { calcNodesBoundingBox } from "./utils";
|
||||
|
||||
export type SerializedGraphCanvasState = {
|
||||
offset: Vector2,
|
||||
@@ -17,6 +19,7 @@ export type SerializedGraphCanvasState = {
|
||||
export default class ComfyGraphCanvas extends LGraphCanvas {
|
||||
app: ComfyApp | null;
|
||||
private _unsubscribe: Unsubscriber;
|
||||
isExportingSVG: boolean = false;
|
||||
|
||||
get comfyGraph(): ComfyGraph | null {
|
||||
return this.graph as ComfyGraph;
|
||||
@@ -439,19 +442,35 @@ 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, container: ContainerLayout, containerIndex: number): [LGraphNode[], IDragItem] {
|
||||
const comfyGraph = this.graph as ComfyGraph;
|
||||
|
||||
options.push(
|
||||
{
|
||||
content: "Convert to Subgraph",
|
||||
has_submenu: false,
|
||||
disabled: Object.keys(this.selected_nodes).length === 0,
|
||||
callback: this.convertToSubgraph.bind(this)
|
||||
},
|
||||
)
|
||||
let [min_x, min_y, max_x, max_y] = calcNodesBoundingBox(template.nodes);
|
||||
|
||||
return options
|
||||
const width = max_x - min_x
|
||||
const height = max_y - min_y
|
||||
|
||||
pos[0] -= width / 2
|
||||
pos[1] -= height / 2
|
||||
|
||||
const layout = comfyGraph.layout;
|
||||
if (layout == null) {
|
||||
console.error("[ComfyGraphCanvas] graph has no layout!", comfyGraph)
|
||||
return;
|
||||
}
|
||||
|
||||
// The following operations modify the template in-place, so be sure it's been cloned first
|
||||
const cloned = LiteGraph.cloneObject(template)
|
||||
const nodeMapping = comfyGraph.insertTemplate(cloned, pos);
|
||||
const templateLayoutRoot = layout.insertTemplate(cloned, comfyGraph, nodeMapping, container, containerIndex);
|
||||
|
||||
this.selectNodes(Object.values(nodeMapping).filter(n => n.graph === this.graph));
|
||||
|
||||
return [Object.values(nodeMapping), templateLayoutRoot]
|
||||
}
|
||||
|
||||
saveAsTemplate(_value: IContextMenuItem, _options, mouseEvent, prevMenu, node?: LGraphNode) {
|
||||
@@ -467,9 +486,35 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
||||
|
||||
const template = result as ComfyBoxTemplate;
|
||||
|
||||
console.warn("TEMPLATEFOUND", template)
|
||||
|
||||
const serialized = serializeTemplate(this, template);
|
||||
|
||||
try {
|
||||
if (templateState.add(serialized)) {
|
||||
notify("Template saved!", { type: "success" })
|
||||
}
|
||||
else {
|
||||
notify("Failed to save template: already exists in LocalStorage", { type: "error" })
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
// Quota exceeded?
|
||||
notify(`Failed to save template: ${error}`, { type: "error", timeout: 10000 })
|
||||
}
|
||||
}
|
||||
|
||||
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[] {
|
||||
@@ -510,4 +555,17 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
override onRenderBackground(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D): boolean {
|
||||
if (this.isExportingSVG) {
|
||||
ctx.clearRect(
|
||||
this.visible_area[0],
|
||||
this.visible_area[1],
|
||||
this.visible_area[2],
|
||||
this.visible_area[3]
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { parse } from 'csv-parse/browser/esm/sync';
|
||||
import { timeExecutionMs } from './utils';
|
||||
import { insertCompletionText, type Completion, type CompletionContext, type CompletionResult, type CompletionSource, type CompletionConfig, autocompletion } from '@codemirror/autocomplete';
|
||||
import { insertCompletionText, type Completion, type CompletionContext, type CompletionResult, type CompletionSource, autocompletion } from '@codemirror/autocomplete';
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
import type { Extension, TransactionSpec } from '@codemirror/state';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import type { EditorView } from '@codemirror/view';
|
||||
import type { StyleSpec } from "style-mod"
|
||||
|
||||
@@ -43,7 +43,7 @@ export const TAG_CATEGORY_COLORS: StyleSpec = Object.values(TAG_CATEGORY_DATA)
|
||||
[`.cm-autocompletion-${d.name}`, { color: d.color + " !important" }],
|
||||
]
|
||||
})
|
||||
.reduce((dict, el) => (dict[el[0]] = el[1], dict), {})
|
||||
.reduce((dict: StyleSpec, el: [string, any]) => (dict[el[0]] = el[1], dict), {})
|
||||
|
||||
export type DanbooruTag = {
|
||||
text: string,
|
||||
@@ -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 {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import { startDrag, stopDrag } from "$lib/utils"
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
import { isHidden } from "$lib/widgets/utils";
|
||||
import { handleContainerConsider, handleContainerFinalize } from "./utils";
|
||||
|
||||
export let layoutState: WritableLayoutStateStore;
|
||||
export let container: ContainerLayout | null = null;
|
||||
@@ -46,14 +47,12 @@
|
||||
isOpen = container.isOpen
|
||||
}
|
||||
|
||||
function handleConsider(evt: any) {
|
||||
children = layoutState.updateChildren(container, evt.detail.items)
|
||||
// console.log(dragItems);
|
||||
function handleConsider(evt: CustomEvent<DndEvent<IDragItem>>) {
|
||||
children = handleContainerConsider(layoutState, container, evt)
|
||||
};
|
||||
|
||||
function handleFinalize(evt: any) {
|
||||
children = layoutState.updateChildren(container, evt.detail.items)
|
||||
// Ensure dragging is stopped on drag finish
|
||||
function handleFinalize(evt: CustomEvent<DndEvent<IDragItem>>) {
|
||||
children = handleContainerFinalize(layoutState, container, evt)
|
||||
};
|
||||
|
||||
function handleClick(e: CustomEvent<boolean>) {
|
||||
@@ -85,6 +84,7 @@
|
||||
class:empty={children.length === 0}
|
||||
class:edit={edit}
|
||||
use:dndzone="{{
|
||||
type: "layout",
|
||||
items: children,
|
||||
flipDurationMs,
|
||||
centreDraggedOnCursor: true,
|
||||
@@ -219,8 +219,7 @@
|
||||
|
||||
.animation-wrapper {
|
||||
position: relative;
|
||||
flex-grow: 100;
|
||||
flex-basis: 0;
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
|
||||
.handle-widget:hover {
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
||||
|
||||
import {fade} from 'svelte/transition';
|
||||
// notice - fade in works fine but don't add svelte's fade-out (known issue)
|
||||
import {cubicIn} from 'svelte/easing';
|
||||
import { flip } from 'svelte/animate';
|
||||
import { type ContainerLayout, type WidgetLayout, type IDragItem, type WritableLayoutStateStore } from "$lib/stores/layoutStates";
|
||||
import { startDrag, stopDrag } from "$lib/utils"
|
||||
import type { Writable } from "svelte/store";
|
||||
import { isHidden } from "$lib/widgets/utils";
|
||||
import { handleContainerConsider, handleContainerFinalize } from "./utils";
|
||||
|
||||
export let layoutState: WritableLayoutStateStore;
|
||||
export let container: ContainerLayout | null = null;
|
||||
@@ -45,14 +45,12 @@
|
||||
attrsChanged = null
|
||||
}
|
||||
|
||||
function handleConsider(evt: any) {
|
||||
children = layoutState.updateChildren(container, evt.detail.items)
|
||||
// console.log(dragItems);
|
||||
function handleConsider(evt: CustomEvent<DndEvent<IDragItem>>) {
|
||||
children = handleContainerConsider(layoutState, container, evt);
|
||||
};
|
||||
|
||||
function handleFinalize(evt: any) {
|
||||
children = layoutState.updateChildren(container, evt.detail.items)
|
||||
// Ensure dragging is stopped on drag finish
|
||||
function handleFinalize(evt: CustomEvent<DndEvent<IDragItem>>) {
|
||||
children = handleContainerFinalize(layoutState, container, evt);
|
||||
};
|
||||
|
||||
function _startDrag(e: MouseEvent | TouchEvent) {
|
||||
@@ -83,6 +81,7 @@
|
||||
class:empty={children.length === 0}
|
||||
class:edit
|
||||
use:dndzone="{{
|
||||
type: "layout",
|
||||
items: children,
|
||||
flipDurationMs,
|
||||
centreDraggedOnCursor: true,
|
||||
@@ -261,7 +260,9 @@
|
||||
|
||||
.animation-wrapper {
|
||||
position: relative;
|
||||
flex-basis: 0%;
|
||||
flex-grow: 100;
|
||||
flex-shrink: 100;
|
||||
}
|
||||
|
||||
.handle-hidden {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import Sidebar from "./Sidebar.svelte";
|
||||
import SidebarItem from "./SidebarItem.svelte";
|
||||
import notify from "$lib/notify";
|
||||
import ComfyWorkflowsView from "./ComfyWorkflowsView.svelte";
|
||||
import ComfyBoxWorkflowsView from "./ComfyBoxWorkflowsView.svelte";
|
||||
import GlobalModal from "./GlobalModal.svelte";
|
||||
|
||||
export let app: ComfyApp = undefined;
|
||||
@@ -60,7 +60,7 @@
|
||||
<div id="container">
|
||||
<Sidebar selected="generate">
|
||||
<SidebarItem id="generate" name="Generate" icon={Image}>
|
||||
<ComfyWorkflowsView {app} {uiTheme} />
|
||||
<ComfyBoxWorkflowsView {app} {uiTheme} />
|
||||
</SidebarItem>
|
||||
<SidebarItem id="settings" name="Settings" icon={Gear}>
|
||||
</SidebarItem>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -21,13 +22,13 @@ import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
|
||||
import { ComfyComboNode } from "$lib/nodes/widgets";
|
||||
import notify from "$lib/notify";
|
||||
import parseA1111, { type A1111ParsedInfotext } from "$lib/parseA1111";
|
||||
import configState from "$lib/stores/configState";
|
||||
import layoutStates, { defaultWorkflowAttributes, type SerializedLayoutState } from "$lib/stores/layoutStates";
|
||||
import configState, { type ConfigState } from "$lib/stores/configState";
|
||||
import layoutStates, { defaultWorkflowAttributes, isComfyWidgetNode, type SerializedLayoutState } from "$lib/stores/layoutStates";
|
||||
import modalState from "$lib/stores/modalState";
|
||||
import queueState from "$lib/stores/queueState";
|
||||
import selectionState from "$lib/stores/selectionState";
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import workflowState, { ComfyWorkflow, type WorkflowAttributes, type WorkflowInstID } from "$lib/stores/workflowState";
|
||||
import workflowState, { ComfyBoxWorkflow, type WorkflowAttributes, type WorkflowInstID } from "$lib/stores/workflowState";
|
||||
import type { SerializedPromptOutput } from "$lib/utils";
|
||||
import { basename, capitalize, download, graphToGraphVis, jsonToJsObject, promptToGraphVis, range } from "$lib/utils";
|
||||
import { tick } from "svelte";
|
||||
@@ -35,6 +36,8 @@ 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 { deserializeTemplateFromSVG } from "$lib/ComfyBoxTemplate";
|
||||
import templateState from "$lib/stores/templateState";
|
||||
|
||||
export const COMFYBOX_SERIAL_VERSION = 1;
|
||||
|
||||
@@ -55,7 +58,7 @@ export type OpenWorkflowOptions = {
|
||||
type PromptQueueItem = {
|
||||
num: number,
|
||||
batchCount: number
|
||||
workflow: ComfyWorkflow
|
||||
workflow: ComfyBoxWorkflow
|
||||
}
|
||||
|
||||
export type A1111PromptAndInfo = {
|
||||
@@ -189,6 +192,11 @@ export default class ComfyApp {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.loadConfig();
|
||||
|
||||
this.api.hostname = get(configState).comfyUIHostname
|
||||
this.api.port = get(configState).comfyUIPort
|
||||
|
||||
this.setupColorScheme()
|
||||
|
||||
this.rootEl = document.getElementById("app-root") as HTMLDivElement;
|
||||
@@ -220,7 +228,7 @@ export default class ComfyApp {
|
||||
setActive: false
|
||||
}
|
||||
await this.initDefaultWorkflow("defaultWorkflow", options);
|
||||
await this.initDefaultWorkflow("upscale", options);
|
||||
await this.initDefaultWorkflow("upscaleByModel", options);
|
||||
await this.initDefaultWorkflow("conditioningRegions", options);
|
||||
}
|
||||
|
||||
@@ -232,6 +240,7 @@ export default class ComfyApp {
|
||||
this.addKeyboardHandler();
|
||||
|
||||
await this.updateHistoryAndQueue();
|
||||
templateState.load();
|
||||
|
||||
await this.initFrontendFeatures();
|
||||
|
||||
@@ -248,7 +257,24 @@ export default class ComfyApp {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO
|
||||
*/
|
||||
async loadConfig() {
|
||||
try {
|
||||
const config = await fetch(`/config.json`);
|
||||
const state = await config.json() as ConfigState;
|
||||
configState.set(state);
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`Failed to load config`, error)
|
||||
}
|
||||
}
|
||||
|
||||
resizeCanvas() {
|
||||
if (!this.canvasEl)
|
||||
return;
|
||||
|
||||
this.canvasEl.width = this.canvasEl.parentElement.offsetWidth;
|
||||
this.canvasEl.height = this.canvasEl.parentElement.offsetHeight;
|
||||
this.canvasEl.style.width = ""
|
||||
@@ -256,7 +282,7 @@ export default class ComfyApp {
|
||||
this.lCanvas.draw(true, true);
|
||||
}
|
||||
|
||||
serialize(workflow: ComfyWorkflow, canvas?: SerializedGraphCanvasState): SerializedAppState {
|
||||
serialize(workflow: ComfyBoxWorkflow, canvas?: SerializedGraphCanvasState): SerializedAppState {
|
||||
const layoutState = layoutStates.getLayout(workflow.id);
|
||||
if (layoutState == null)
|
||||
throw new Error("Workflow has no layout!")
|
||||
@@ -278,7 +304,7 @@ export default class ComfyApp {
|
||||
|
||||
saveStateToLocalStorage() {
|
||||
try {
|
||||
uiState.update(s => { s.isSavingToLocalStorage = true; return s; })
|
||||
uiState.update(s => { s.forceSaveUserState = true; return s; })
|
||||
const state = get(workflowState)
|
||||
const workflows = state.openedWorkflows
|
||||
const savedWorkflows = workflows.map(w => this.serialize(w));
|
||||
@@ -294,7 +320,7 @@ export default class ComfyApp {
|
||||
notify(`Failed saving to local storage:\n${err}`, { type: "error" })
|
||||
}
|
||||
finally {
|
||||
uiState.update(s => { s.isSavingToLocalStorage = false; return s; })
|
||||
uiState.update(s => { s.forceSaveUserState = null; return s; })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,12 +485,16 @@ export default class ComfyApp {
|
||||
} catch (error) { }
|
||||
}
|
||||
|
||||
if (workflow && typeof workflow.createdBy === "string") {
|
||||
this.openWorkflow(workflow);
|
||||
}
|
||||
else {
|
||||
// TODO handle vanilla workflows
|
||||
throw new Error("Workflow was not in ComfyBox format!")
|
||||
if (workflow == null)
|
||||
return;
|
||||
|
||||
if (typeof workflow === "object") {
|
||||
if (typeof workflow.createdBy === "string")
|
||||
this.openWorkflow(workflow);
|
||||
else {
|
||||
// TODO handle vanilla workflows
|
||||
throw new Error("Workflow was not in ComfyBox format!")
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -631,7 +661,7 @@ export default class ComfyApp {
|
||||
refreshCombos: true,
|
||||
warnMissingNodeTypes: true
|
||||
}
|
||||
): Promise<ComfyWorkflow> {
|
||||
): Promise<ComfyBoxWorkflow> {
|
||||
if (data.version !== COMFYBOX_SERIAL_VERSION) {
|
||||
const mes = `Invalid ComfyBox saved data format: ${data.version} `
|
||||
notify(mes, { type: "error" })
|
||||
@@ -640,7 +670,7 @@ export default class ComfyApp {
|
||||
|
||||
this.clean();
|
||||
|
||||
let workflow: ComfyWorkflow;
|
||||
let workflow: ComfyBoxWorkflow;
|
||||
try {
|
||||
workflow = workflowState.openWorkflow(this.lCanvas, data, options.setActive);
|
||||
}
|
||||
@@ -761,6 +791,30 @@ export default class ComfyApp {
|
||||
await this.openWorkflow(state, options)
|
||||
}
|
||||
|
||||
saveWorkflowStateAsDefault(workflow: ComfyBoxWorkflow | null) {
|
||||
workflow ||= workflowState.getActiveWorkflow();
|
||||
if (workflow == null)
|
||||
return;
|
||||
|
||||
for (const node of workflow.graph.iterateNodesInOrderRecursive()) {
|
||||
if (isComfyWidgetNode(node)) {
|
||||
node.properties.defaultValue = node.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resetCurrentWorkflow() {
|
||||
const workflow = workflowState.getActiveWorkflow();
|
||||
if (workflow == null)
|
||||
return;
|
||||
|
||||
for (const node of workflow.graph.iterateNodesInOrderRecursive()) {
|
||||
if (isComfyWidgetNode(node)) {
|
||||
node.setValue(node.properties.defaultValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.clean();
|
||||
|
||||
@@ -798,6 +852,8 @@ export default class ComfyApp {
|
||||
return;
|
||||
}
|
||||
|
||||
this.saveWorkflowStateAsDefault(workflow);
|
||||
|
||||
const promptFilename = get(configState).promptForWorkflowName;
|
||||
|
||||
const title = workflow.attrs.title.trim() || "workflow"
|
||||
@@ -831,7 +887,7 @@ export default class ComfyApp {
|
||||
* Converts the current graph workflow for sending to the API
|
||||
* @returns The workflow and node links
|
||||
*/
|
||||
graphToPrompt(workflow: ComfyWorkflow, tag: string | null = null): SerializedPrompt {
|
||||
graphToPrompt(workflow: ComfyBoxWorkflow, tag: string | null = null): SerializedPrompt {
|
||||
return this.promptSerializer.serialize(workflow.graph, tag)
|
||||
}
|
||||
|
||||
@@ -853,7 +909,7 @@ export default class ComfyApp {
|
||||
tag = null;
|
||||
|
||||
this.processingQueue = true;
|
||||
let workflow: ComfyWorkflow;
|
||||
let workflow: ComfyBoxWorkflow;
|
||||
|
||||
try {
|
||||
while (this.queueItems.length) {
|
||||
@@ -1001,6 +1057,45 @@ export default class ComfyApp {
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
} else if (file.type === "image/svg+xml" || file.name.endsWith(".svg")) {
|
||||
const templateAndSvg = await deserializeTemplateFromSVG(file);
|
||||
|
||||
const importTemplate = () => {
|
||||
try {
|
||||
if (templateState.add(templateAndSvg)) {
|
||||
notify("Template imported successfully!", { type: "success" })
|
||||
}
|
||||
else {
|
||||
notify("Template already exists in saved list.", { type: "warning" })
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
notify(`Error importing template: ${error}`, { type: "error", timeout: 10000 })
|
||||
}
|
||||
}
|
||||
|
||||
modalState.pushModal({
|
||||
title: "ComfyBox Template Preview",
|
||||
svelteComponent: EditTemplateModal,
|
||||
closeOnClick: false,
|
||||
showCloseButton: false,
|
||||
svelteProps: {
|
||||
templateAndSvg,
|
||||
editable: false
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
name: "Import",
|
||||
variant: "primary",
|
||||
onClick: importTemplate
|
||||
},
|
||||
{
|
||||
name: "Close",
|
||||
variant: "secondary",
|
||||
onClick: () => { }
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1017,7 +1112,7 @@ export default class ComfyApp {
|
||||
/**
|
||||
* Refresh combo list on whole nodes
|
||||
*/
|
||||
async refreshComboInNodes(workflow?: ComfyWorkflow, defs?: Record<string, ComfyNodeDef>, flashUI: boolean = false) {
|
||||
async refreshComboInNodes(workflow?: ComfyBoxWorkflow, defs?: Record<string, ComfyNodeDef>, flashUI: boolean = false) {
|
||||
workflow ||= workflowState.getActiveWorkflow();
|
||||
if (workflow == null) {
|
||||
notify("No active workflow!", { type: "error" })
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
import Menu from './menu/Menu.svelte';
|
||||
import MenuOption from './menu/MenuOption.svelte';
|
||||
import MenuDivider from './menu/MenuDivider.svelte';
|
||||
import type { ComfyWorkflow } from "$lib/stores/workflowState";
|
||||
import type { ComfyBoxWorkflow } from "$lib/stores/workflowState";
|
||||
|
||||
export let app: ComfyApp;
|
||||
export let workflow: ComfyWorkflow;
|
||||
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,51 +138,70 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if layoutState != null}
|
||||
<div id="comfy-workflow-view" on:contextmenu={onRightClick}>
|
||||
<WidgetContainer bind:dragItem={root} classes={["root-container"]} {layoutState} />
|
||||
{#if workflow != null}
|
||||
{#if layoutState != null}
|
||||
<div class="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}
|
||||
{:else}
|
||||
<div class="no-workflows">
|
||||
<span>No workflow loaded</span>
|
||||
</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}
|
||||
|
||||
<style lang="scss">
|
||||
#comfy-workflow-view {
|
||||
.comfy-workflow-view {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
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>
|
||||
|
||||
108
src/lib/components/ComfyPaneView.svelte
Normal file
108
src/lib/components/ComfyPaneView.svelte
Normal file
@@ -0,0 +1,108 @@
|
||||
<script context="module" lang="ts">
|
||||
export type ComfyPaneMode = "none" | "activeWorkflow" | "graph" | "properties" | "templates" | "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 { Sliders2, BoxSeam, LayoutTextSidebarReverse } from "svelte-bootstrap-icons";
|
||||
|
||||
import ComfyBoxWorkflowView from "./ComfyBoxWorkflowView.svelte";
|
||||
import ComfyGraphView from "./ComfyGraphView.svelte";
|
||||
import ComfyProperties from "./ComfyProperties.svelte";
|
||||
import ComfyQueue from "./ComfyQueue.svelte";
|
||||
import ComfyTemplates from "./ComfyTemplates.svelte";
|
||||
import { SvelteComponent } from "svelte";
|
||||
|
||||
export let app: ComfyApp
|
||||
export let mode: ComfyPaneMode = "none";
|
||||
export let showSwitcher: boolean = false;
|
||||
|
||||
const MODES: [ComfyPaneMode, typeof SvelteComponent][] = [
|
||||
["properties", Sliders2],
|
||||
["templates", BoxSeam],
|
||||
["queue", LayoutTextSidebarReverse]
|
||||
]
|
||||
|
||||
function switchMode(newMode: ComfyPaneMode) {
|
||||
console.warn("switch", mode, newMode)
|
||||
mode = newMode;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="pane">
|
||||
<div class="pane-wrapper" class:has-switcher={showSwitcher}>
|
||||
{#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 === "templates"}
|
||||
<ComfyTemplates {app} />
|
||||
{:else if mode === "queue"}
|
||||
<ComfyQueue {app} />
|
||||
{:else}
|
||||
<div class="blank-panel">(Blank)</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if showSwitcher}
|
||||
<div class="switcher">
|
||||
{#each MODES as [theMode, icon]}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<button class="mode-button ternary"
|
||||
disabled={mode === theMode}
|
||||
class:selected={mode === theMode}
|
||||
on:click={() => switchMode(theMode)}>
|
||||
<svelte:component this={icon} width="100%" height="100%" />
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
$button-height: 2.5rem;
|
||||
|
||||
.pane {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.pane-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
&.has-switcher {
|
||||
height: calc(100% - $button-height);
|
||||
}
|
||||
}
|
||||
|
||||
.switcher {
|
||||
height: $button-height;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
color: var(--comfy-accent-soft);
|
||||
|
||||
.mode-button {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
|
||||
@include square-button;
|
||||
|
||||
&:hover {
|
||||
color: var(--body-text-color);
|
||||
}
|
||||
&.selected {
|
||||
color: var(--body-text-color);
|
||||
background-color: var(--panel-background-fill);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -11,9 +11,9 @@
|
||||
import ComfyNumberProperty from "./ComfyNumberProperty.svelte";
|
||||
import ComfyComboProperty from "./ComfyComboProperty.svelte";
|
||||
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
|
||||
import type { ComfyWorkflow } from "$lib/stores/workflowState";
|
||||
import type { ComfyBoxWorkflow } from "$lib/stores/workflowState";
|
||||
|
||||
export let workflow: ComfyWorkflow | null;
|
||||
export let workflow: ComfyBoxWorkflow | null;
|
||||
|
||||
let layoutState: WritableLayoutStateStore | null = null
|
||||
|
||||
|
||||
@@ -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() {
|
||||
@@ -354,10 +354,11 @@
|
||||
<style lang="scss">
|
||||
$pending-height: 200px;
|
||||
$display-mode-buttons-height: 2rem;
|
||||
$pane-mode-buttons-height: 2.5rem;
|
||||
$bottom-bar-height: 70px;
|
||||
$workflow-tabs-height: 2.5rem;
|
||||
$mode-buttons-height: 30px;
|
||||
$queue-height: calc(100vh - #{$pending-height} - #{$mode-buttons-height} - #{$bottom-bar-height} - #{$workflow-tabs-height} - 0.9rem);
|
||||
$queue-height: calc(100vh - #{$pending-height} - #{$pane-mode-buttons-height} - #{$mode-buttons-height} - #{$bottom-bar-height} - #{$workflow-tabs-height} - 0.9rem);
|
||||
$queue-height-history: calc(#{$queue-height} - #{$display-mode-buttons-height});
|
||||
|
||||
.prompt-modal-header {
|
||||
|
||||
280
src/lib/components/ComfyTemplates.svelte
Normal file
280
src/lib/components/ComfyTemplates.svelte
Normal file
@@ -0,0 +1,280 @@
|
||||
<script lang="ts">
|
||||
import { embedTemplateInSvg, type SerializedComfyBoxTemplate } from "$lib/ComfyBoxTemplate";
|
||||
import templateState from "$lib/stores/templateState";
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import { download, truncateString } from "$lib/utils";
|
||||
import type ComfyApp from "./ComfyApp";
|
||||
import { flip } from 'svelte/animate';
|
||||
import {fade} from 'svelte/transition';
|
||||
import {cubicIn} from 'svelte/easing';
|
||||
import { dndzone, TRIGGERS, SHADOW_PLACEHOLDER_ITEM_ID, SHADOW_ITEM_MARKER_PROPERTY_NAME } from 'svelte-dnd-action';
|
||||
import { defaultWidgetAttributes, type TemplateLayout } from "$lib/stores/layoutStates";
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { get, writable } from "svelte/store";
|
||||
import EditTemplateModal from "./modal/EditTemplateModal.svelte";
|
||||
import modalState, { type ModalData } from "$lib/stores/modalState";
|
||||
import notify from "$lib/notify";
|
||||
|
||||
export let app: ComfyApp
|
||||
|
||||
type DNDConsiderOrFinalizeEvent<T> = {
|
||||
items: T[],
|
||||
info: any,
|
||||
el: Node,
|
||||
id: string,
|
||||
trigger?: string,
|
||||
source?: string
|
||||
}
|
||||
|
||||
let _sorted: TemplateLayout[] = []
|
||||
|
||||
$: rebuildTemplates($templateState.templates);
|
||||
|
||||
function rebuildTemplates(templates: SerializedComfyBoxTemplate[]) {
|
||||
_sorted = Array.from(templates).map(t => {
|
||||
return {
|
||||
type: "template", id: uuidv4(), template: t, attrs: {...defaultWidgetAttributes}, attrsChanged: writable(0)
|
||||
}
|
||||
});
|
||||
_sorted.sort(t => t.template.metadata.createdAt || 0);
|
||||
}
|
||||
|
||||
const flipDurationMs = 200;
|
||||
let shouldIgnoreDndEvents = false;
|
||||
|
||||
function handleDndConsider(e: CustomEvent<DNDConsiderOrFinalizeEvent<TemplateLayout>>) {
|
||||
// console.warn(`got consider ${JSON.stringify(e.detail, null, 2)}`);
|
||||
const {trigger, id} = e.detail.info;
|
||||
if (trigger === TRIGGERS.DRAG_STARTED) {
|
||||
// console.warn(`copying ${id}`);
|
||||
const idx = _sorted.findIndex(item => item.id === id);
|
||||
const newId = `${id}_copy_${Math.round(Math.random()*1000000)}`;
|
||||
|
||||
e.detail.items = e.detail.items.filter(item => !item[SHADOW_ITEM_MARKER_PROPERTY_NAME]);
|
||||
e.detail.items.splice(idx, 0, {..._sorted[idx], id: newId});
|
||||
|
||||
_sorted = e.detail.items;
|
||||
shouldIgnoreDndEvents = true;
|
||||
}
|
||||
else if (!shouldIgnoreDndEvents) {
|
||||
_sorted = e.detail.items;
|
||||
}
|
||||
else {
|
||||
_sorted = _sorted;
|
||||
}
|
||||
}
|
||||
|
||||
function handleDndFinalize(e: CustomEvent<DNDConsiderOrFinalizeEvent<TemplateLayout>>) {
|
||||
if (!shouldIgnoreDndEvents) {
|
||||
_sorted = e.detail.items;
|
||||
}
|
||||
else {
|
||||
_sorted = _sorted;
|
||||
shouldIgnoreDndEvents = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleClick(layout: TemplateLayout) {
|
||||
if ($uiState.uiUnlocked)
|
||||
return;
|
||||
|
||||
const updateTemplate = (modal: ModalData) => {
|
||||
const state = get(modal.state);
|
||||
layout.template.metadata.title = state.name || layout.template.metadata.title
|
||||
layout.template.metadata.author = state.author || layout.template.metadata.author
|
||||
layout.template.metadata.description = state.description || layout.template.metadata.description
|
||||
}
|
||||
|
||||
const saveTemplate = (modal: ModalData) => {
|
||||
updateTemplate(modal);
|
||||
try {
|
||||
templateState.update(layout.template);
|
||||
notify("Saved template!", { type: "success" })
|
||||
}
|
||||
catch (error) {
|
||||
notify(`Failed to save template: ${error}`, { type: "error", timeout: 10000 })
|
||||
}
|
||||
}
|
||||
|
||||
const deleteTemplate = (modal: ModalData) => {
|
||||
if (!confirm("Are you sure you want to delete this template?"))
|
||||
return false;
|
||||
|
||||
try {
|
||||
if (templateState.remove(layout.template.id)) {
|
||||
notify("Template deleted!", { type: "success" })
|
||||
}
|
||||
else {
|
||||
notify("Failed to delete template: not saved to local storage.", { type: "warning" })
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
notify(`Failed to delete template: ${error}`, { type: "error", timeout: 10000 })
|
||||
}
|
||||
}
|
||||
|
||||
const downloadTemplate = (modal: ModalData) => {
|
||||
updateTemplate(modal);
|
||||
const svg = embedTemplateInSvg(layout.template);
|
||||
const title = layout.template.metadata.title || "template";
|
||||
download(`${title}.svg`, svg, "image/svg+xml");
|
||||
}
|
||||
|
||||
modalState.pushModal({
|
||||
svelteComponent: EditTemplateModal,
|
||||
svelteProps: {
|
||||
templateAndSvg: layout.template
|
||||
},
|
||||
showCloseButton: false,
|
||||
closeOnClick: false,
|
||||
buttons: [
|
||||
{
|
||||
name: "Save",
|
||||
variant: "primary",
|
||||
onClick: saveTemplate
|
||||
},
|
||||
{
|
||||
name: "Download",
|
||||
variant: "secondary",
|
||||
onClick: downloadTemplate,
|
||||
closeOnClick: false
|
||||
},
|
||||
{
|
||||
name: "Delete",
|
||||
variant: "secondary",
|
||||
onClick: deleteTemplate
|
||||
},
|
||||
{
|
||||
name: "Close",
|
||||
variant: "secondary",
|
||||
onClick: () => {
|
||||
}
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="template-list">
|
||||
<div class="template-entries">
|
||||
{#if _sorted.length > 0}
|
||||
{@const draggable = $uiState.uiUnlocked}
|
||||
<div class="template-category-group">
|
||||
<div class="template-category-header">
|
||||
General
|
||||
</div>
|
||||
<div class="template-entries-wrapper"
|
||||
use:dndzone={{
|
||||
type: "layout",
|
||||
items: _sorted,
|
||||
flipDurationMs,
|
||||
dragDisabled: !draggable,
|
||||
dropFromOthersDisabled: true
|
||||
}}
|
||||
on:consider={handleDndConsider}
|
||||
on:finalize={handleDndFinalize}>
|
||||
{#each _sorted.filter(i => i.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="template-entry" class:draggable on:click={() => handleClick(item)}>
|
||||
<div class="template-name">{item.template.metadata.title}</div>
|
||||
<div class="template-desc">{item.template.metadata.description}</div>
|
||||
</div>
|
||||
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
|
||||
<div in:fade={{duration:200, easing: cubicIn}} class='template-drag-item-shadow'/>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="no-templates">
|
||||
<span>(No templates)</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.template-list {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.template-entries {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
|
||||
.template-category-header {
|
||||
color: var(--body-text-color);
|
||||
background: var(--block-background-fill);
|
||||
border-color: var(--panel-border-color);
|
||||
padding: 0.8rem 1.0rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.template-entry {
|
||||
padding: 1.0rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-bottom: 1px solid var(--block-border-color);
|
||||
border-top: 1px solid var(--table-border-color);
|
||||
color: var(--body-text-color);
|
||||
background: var(--panel-background-fill);
|
||||
max-height: 14rem;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
font-size: 13pt;
|
||||
.template-desc {
|
||||
opacity: 65%;
|
||||
font-size: 11pt;
|
||||
}
|
||||
|
||||
&.draggable {
|
||||
border: 5px dashed var(--secondary-500);
|
||||
margin: 0.2em;
|
||||
}
|
||||
|
||||
&:hover:not(:has(img:hover)):not(:has(button:hover)) {
|
||||
cursor: pointer;
|
||||
background: var(--block-background-fill);
|
||||
|
||||
&.draggable {
|
||||
cursor: grab;
|
||||
background: var(--secondary-700);
|
||||
}
|
||||
|
||||
&.running {
|
||||
background: var(--comfy-accent-soft);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-templates {
|
||||
display: flex;
|
||||
color: var(--comfy-accent-soft);
|
||||
flex-direction: row;
|
||||
margin: auto;
|
||||
height: 100%;
|
||||
color: var(--comfy-accent-soft);
|
||||
|
||||
span {
|
||||
margin: auto;
|
||||
font-size: 32px;
|
||||
font-weight: bolder;
|
||||
}
|
||||
}
|
||||
|
||||
.template-drag-item-shadow {
|
||||
position: absolute;
|
||||
top: 0; left:0; right: 0; bottom: 0;
|
||||
visibility: visible;
|
||||
border: 1px dashed grey;
|
||||
background: lightblue;
|
||||
opacity: 0.5;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -13,7 +13,8 @@
|
||||
}
|
||||
|
||||
function onButtonClicked(modal: ModalData, button: ModalButton, closeDialog: Function) {
|
||||
button.onClick(modal);
|
||||
if (button.onClick(modal) === false)
|
||||
return
|
||||
|
||||
if (button.closeOnClick !== false) {
|
||||
closeDialog()
|
||||
@@ -29,9 +30,11 @@
|
||||
{/if}
|
||||
</div>
|
||||
<svelte:fragment>
|
||||
{#if modal != null && modal.svelteComponent != null}
|
||||
<svelte:component this={modal.svelteComponent} {...modal.svelteProps} _modal={modal}/>
|
||||
{/if}
|
||||
<div class="modal-body">
|
||||
{#if modal != null && modal.svelteComponent != null}
|
||||
<svelte:component this={modal.svelteComponent} {...modal.svelteProps} _modal={modal}/>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<div slot="buttons" class="buttons" let:closeDialog>
|
||||
{#if modal != null && modal.buttons?.length > 0}
|
||||
@@ -52,6 +55,12 @@
|
||||
|
||||
<style lang="scss">
|
||||
.buttons {
|
||||
gap: var(--spacing-sm);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import UploadText from "$lib/components/gradio/app/UploadText.svelte";
|
||||
import type { ComfyImageLocation } from "$lib/nodes/ComfyWidgetNodes";
|
||||
import notify from "$lib/notify";
|
||||
import configState from "$lib/stores/configState";
|
||||
import { convertComfyOutputEntryToGradio, convertComfyOutputToComfyURL, type ComfyUploadImageAPIResponse } from "$lib/utils";
|
||||
import { Block, BlockLabel } from "@gradio/atoms";
|
||||
import { File as FileIcon } from "@gradio/icons";
|
||||
@@ -68,7 +69,7 @@
|
||||
|
||||
dispatch("uploading")
|
||||
|
||||
const url = `http://${location.hostname}:8188` // TODO make configurable
|
||||
const url = configState.getBackendURL();
|
||||
|
||||
const requests = files.map(async (file) => {
|
||||
const formData = new FormData();
|
||||
@@ -174,8 +175,6 @@
|
||||
uploaded = true;
|
||||
}
|
||||
|
||||
$: console.warn(imgWidth, imgHeight, "IMGSIZE!!")
|
||||
|
||||
function handle_clear(_e: CustomEvent<null>) {
|
||||
_value = null;
|
||||
value = [];
|
||||
|
||||
@@ -94,9 +94,6 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
|
||||
.button-row, .buttons {
|
||||
gap: var(--spacing-sm);
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import type { Styles } from "@gradio/utils";
|
||||
import { comfyFileToComfyBoxMetadata, comfyURLToComfyFile, countNewLines } from "$lib/utils";
|
||||
import ReceiveOutputTargets from "./modal/ReceiveOutputTargets.svelte";
|
||||
import workflowState, { type ComfyWorkflow, type WorkflowReceiveOutputTargets } from "$lib/stores/workflowState";
|
||||
import workflowState, { type ComfyBoxWorkflow, type WorkflowReceiveOutputTargets } from "$lib/stores/workflowState";
|
||||
import type { ComfyReceiveOutputNode } from "$lib/nodes/actions";
|
||||
import type ComfyApp from "./ComfyApp";
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
// ImageViewer.instance.showLightbox(e.detail)
|
||||
}
|
||||
|
||||
function sendOutput(workflow: ComfyWorkflow, targetNode: ComfyReceiveOutputNode) {
|
||||
function sendOutput(workflow: ComfyBoxWorkflow, targetNode: ComfyReceiveOutputNode) {
|
||||
if (workflow == null || targetNode == null)
|
||||
return
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import { startDrag, stopDrag } from "$lib/utils"
|
||||
import type { Writable } from "svelte/store";
|
||||
import { isHidden } from "$lib/widgets/utils";
|
||||
import { handleContainerConsider, handleContainerFinalize } from "./utils";
|
||||
|
||||
export let layoutState: WritableLayoutStateStore;
|
||||
export let container: ContainerLayout | null = null;
|
||||
@@ -38,14 +39,12 @@
|
||||
// attrsChanged = writable(0)
|
||||
}
|
||||
|
||||
function handleConsider(evt: any) {
|
||||
children = layoutState.updateChildren(container, evt.detail.items)
|
||||
// console.log(dragItems);
|
||||
function handleConsider(evt: CustomEvent<DndEvent<IDragItem>>) {
|
||||
children = handleContainerConsider(layoutState, container, evt)
|
||||
};
|
||||
|
||||
function handleFinalize(evt: any) {
|
||||
children = layoutState.updateChildren(container, evt.detail.items)
|
||||
// Ensure dragging is stopped on drag finish
|
||||
function handleFinalize(evt: CustomEvent<DndEvent<IDragItem>>) {
|
||||
children = handleContainerFinalize(layoutState, container, evt)
|
||||
};
|
||||
|
||||
function getTabName(container: ContainerLayout, i: number): string {
|
||||
@@ -89,6 +88,7 @@
|
||||
class:empty={children.length === 0}
|
||||
class:edit={edit}
|
||||
use:dndzone="{{
|
||||
type: "layout",
|
||||
items: children,
|
||||
flipDurationMs,
|
||||
centreDraggedOnCursor: true,
|
||||
@@ -222,8 +222,7 @@
|
||||
|
||||
.animation-wrapper {
|
||||
position: relative;
|
||||
flex-grow: 100;
|
||||
flex-basis: 0;
|
||||
flex: 1 100 0%;
|
||||
}
|
||||
|
||||
.handle-widget:hover {
|
||||
|
||||
@@ -1,35 +1,73 @@
|
||||
<script>
|
||||
import { setContext, createEventDispatcher } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { key } from './menu.ts';
|
||||
<script lang="ts">
|
||||
import { setContext, createEventDispatcher } from 'svelte';
|
||||
import { key } from './menu.ts';
|
||||
|
||||
export let x;
|
||||
export let y;
|
||||
import { offset, flip, shift } from "svelte-floating-ui/dom";
|
||||
import { createFloatingActions, type ClientRectObject, type VirtualElement } from "svelte-floating-ui";
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
|
||||
// whenever x and y is changed, restrict box to be within bounds
|
||||
$: (() => {
|
||||
if (!menuEl) return;
|
||||
const [floatingRef, floatingContent] = createFloatingActions({
|
||||
placement: "right-start",
|
||||
strategy: "fixed",
|
||||
middleware: [
|
||||
offset({ mainAxis: 5, alignmentAxis: 4 }),
|
||||
flip({
|
||||
fallbackPlacements: ["left-start"]
|
||||
}),
|
||||
shift({ padding: 10 })
|
||||
],
|
||||
});
|
||||
|
||||
const rect = menuEl.getBoundingClientRect();
|
||||
x = Math.min(window.innerWidth - rect.width, x);
|
||||
if (y > window.innerHeight - rect.height) y -= rect.height;
|
||||
})(x, y);
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
export let x;
|
||||
export let y;
|
||||
|
||||
setContext(key, {
|
||||
dispatchClick: () => dispatch('click')
|
||||
});
|
||||
// whenever x and y is changed, restrict box to be within bounds
|
||||
$: (() => {
|
||||
if (!menuEl) return;
|
||||
|
||||
let menuEl;
|
||||
function onPageClick(e) {
|
||||
if (e.target === menuEl || menuEl.contains(e.target)) return;
|
||||
dispatch('clickoutside');
|
||||
}
|
||||
const rect = menuEl.getBoundingClientRect();
|
||||
x = Math.min(window.innerWidth - rect.width, x);
|
||||
if (y > window.innerHeight - rect.height) y -= rect.height;
|
||||
})();
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
setContext(key, {
|
||||
dispatchClick: () => dispatch('click')
|
||||
});
|
||||
|
||||
let menuEl;
|
||||
function onPageClick(e) {
|
||||
if (e.target === menuEl || menuEl.contains(e.target)) return;
|
||||
dispatch('clickoutside');
|
||||
}
|
||||
|
||||
|
||||
let getBoundingClientRect: () => ClientRectObject;
|
||||
|
||||
$: getBoundingClientRect = (): ClientRectObject => {
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
top: y,
|
||||
left: x,
|
||||
bottom: y,
|
||||
right: x,
|
||||
width: 0,
|
||||
height: 0
|
||||
}
|
||||
}
|
||||
|
||||
const virtualElement: Writable<VirtualElement> = writable({ getBoundingClientRect })
|
||||
|
||||
$: virtualElement.set({ getBoundingClientRect })
|
||||
|
||||
floatingRef(virtualElement)
|
||||
</script>
|
||||
|
||||
<svelte:body on:click={onPageClick} />
|
||||
<div class="menu" bind:this={menuEl} style="top: {y}px; left: {x}px;">
|
||||
<div class="menu" bind:this={menuEl} style="top: {y}px; left: {x}px;" use:floatingContent>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
|
||||
129
src/lib/components/modal/EditTemplateModal.svelte
Normal file
129
src/lib/components/modal/EditTemplateModal.svelte
Normal file
@@ -0,0 +1,129 @@
|
||||
<script lang="ts">
|
||||
import type { ComfyBoxTemplate, SerializedComfyBoxTemplate } 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";
|
||||
import type { ModalData } from "$lib/stores/modalState";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
const DOMPurify = createDOMPurify(window);
|
||||
|
||||
export let templateAndSvg: SerializedComfyBoxTemplate;
|
||||
export let editable: boolean = true;
|
||||
export let _modal: ModalData;
|
||||
let layout: SerializedLayoutState | null
|
||||
let root: SerializedDragEntry | null
|
||||
let state: Writable<any> = writable({})
|
||||
|
||||
$: {
|
||||
state = _modal.state;
|
||||
if (!("name" in $state)) {
|
||||
$state.name = templateAndSvg.metadata.title;
|
||||
$state.author = templateAndSvg.metadata.author;
|
||||
$state.description = templateAndSvg.metadata.description;
|
||||
}
|
||||
}
|
||||
|
||||
let saneSvg: string = "";
|
||||
|
||||
$: saneSvg = templateAndSvg
|
||||
? DOMPurify.sanitize(templateAndSvg.svg, { USE_PROFILES: { svg: true, svgFilters: true } })
|
||||
.replace("<svg", "<svg style='background: url(\"image/graph-bg.png\")'")
|
||||
: "";
|
||||
|
||||
$: if (templateAndSvg) {
|
||||
layout = templateAndSvg.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" disabled={!editable} bind:value={$state.name} lines={1} max_lines={1} />
|
||||
<Textbox label="Author" disabled={!editable} bind:value={$state.author} lines={1} max_lines={1} />
|
||||
<Textbox label="Description" disabled={!editable} bind:value={$state.description} 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: 70vw;
|
||||
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-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>
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import type { ComfyReceiveOutputNode } from "$lib/nodes/actions";
|
||||
import type { ComfyWorkflow, WorkflowReceiveOutputTargets } from "$lib/stores/workflowState";
|
||||
import type { ComfyBoxWorkflow, WorkflowReceiveOutputTargets } from "$lib/stores/workflowState";
|
||||
import { Block, BlockTitle } from "@gradio/atoms";
|
||||
import { Button } from "@gradio/button";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
select: { workflow: ComfyWorkflow, targetNode: ComfyReceiveOutputNode };
|
||||
select: { workflow: ComfyBoxWorkflow, targetNode: ComfyReceiveOutputNode };
|
||||
}>();
|
||||
|
||||
export let receiveTargets: WorkflowReceiveOutputTargets[] = [];
|
||||
|
||||
function onSelected( workflow: ComfyWorkflow, targetNode: ComfyReceiveOutputNode ) {
|
||||
function onSelected( workflow: ComfyBoxWorkflow, targetNode: ComfyReceiveOutputNode ) {
|
||||
dispatch("select", {
|
||||
workflow,
|
||||
targetNode
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" context="module">
|
||||
export type SendOutputModalResult = {
|
||||
workflow?: ComfyWorkflow,
|
||||
workflow?: ComfyBoxWorkflow,
|
||||
targetNode?: ComfyReceiveOutputNode,
|
||||
}
|
||||
</script>
|
||||
@@ -11,7 +11,7 @@
|
||||
import type { SlotType } from "@litegraph-ts/core";
|
||||
import type { Writable } from "svelte/store";
|
||||
import { StaticImage } from "$lib/components/gradio/image";
|
||||
import type { ComfyWorkflow, WorkflowReceiveOutputTargets } from "$lib/stores/workflowState";
|
||||
import type { ComfyBoxWorkflow, WorkflowReceiveOutputTargets } from "$lib/stores/workflowState";
|
||||
import { comfyBoxImageToComfyURL } from "$lib/utils";
|
||||
import { Button } from "@gradio/button";
|
||||
import type { ComfyReceiveOutputNode } from "$lib/nodes/actions";
|
||||
@@ -29,7 +29,7 @@
|
||||
images = [comfyBoxImageToComfyURL(value)];
|
||||
}
|
||||
|
||||
function sendOutput(workflow: ComfyWorkflow, targetNode: ComfyReceiveOutputNode) {
|
||||
function sendOutput(workflow: ComfyBoxWorkflow, targetNode: ComfyReceiveOutputNode) {
|
||||
const result: SendOutputModalResult = {
|
||||
workflow,
|
||||
targetNode
|
||||
|
||||
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={false}>
|
||||
{#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>
|
||||
47
src/lib/components/utils.ts
Normal file
47
src/lib/components/utils.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type ComfyGraphCanvas from "$lib/ComfyGraphCanvas";
|
||||
import { type ContainerLayout, type IDragItem, type TemplateLayout, type WritableLayoutStateStore } from "$lib/stores/layoutStates"
|
||||
import type { LGraphCanvas, Vector2 } from "@litegraph-ts/core";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
export function handleContainerConsider(layoutState: WritableLayoutStateStore, container: ContainerLayout, evt: CustomEvent<DndEvent<IDragItem>>): IDragItem[] {
|
||||
return layoutState.updateChildren(container, evt.detail.items)
|
||||
};
|
||||
|
||||
export function handleContainerFinalize(layoutState: WritableLayoutStateStore, container: ContainerLayout, evt: CustomEvent<DndEvent<IDragItem>>): IDragItem[] {
|
||||
const dnd = evt.detail
|
||||
const info = dnd.info;
|
||||
const droppedItem = dnd.items.find(i => i.id === info.id);
|
||||
const isDroppingTemplate = droppedItem?.type === "template"
|
||||
|
||||
if (isDroppingTemplate) {
|
||||
return doInsertTemplate(layoutState, droppedItem as TemplateLayout, container, dnd.items)
|
||||
}
|
||||
else {
|
||||
return layoutState.updateChildren(container, dnd.items)
|
||||
}
|
||||
};
|
||||
|
||||
function isComfyGraphCanvas(canvas: LGraphCanvas): canvas is ComfyGraphCanvas {
|
||||
return "insertTemplate" in canvas;
|
||||
}
|
||||
|
||||
function doInsertTemplate(layoutState: WritableLayoutStateStore, droppedTemplate: TemplateLayout, container: ContainerLayout, items: IDragItem[]): IDragItem[] {
|
||||
const workflow = layoutState.workflow;
|
||||
const templateItemIndex = items.findIndex(i => i.id === droppedTemplate.id)
|
||||
|
||||
const newChildren = items.filter(i => i.id !== droppedTemplate.id);
|
||||
|
||||
const canvas = workflow.canvases["app"]?.canvas
|
||||
if (canvas == null || !isComfyGraphCanvas(canvas) || canvas.graph !== workflow.graph) {
|
||||
console.error("Couldn't get main graph canvas!")
|
||||
return newChildren;
|
||||
}
|
||||
|
||||
layoutState.updateChildren(container, newChildren);
|
||||
|
||||
const newPos: Vector2 = [canvas.visible_area[0] + canvas.visible_area[2] / 2, canvas.visible_area[1] + canvas.visible_area[3] / 2]
|
||||
|
||||
canvas.insertTemplate(droppedTemplate.template, newPos, container, templateItemIndex);
|
||||
|
||||
return get(layoutState).allItems[container.id].children;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LGraph, type INodeInputSlot, type SerializedLGraph, type LinkID, type UUID, type NodeID, LiteGraph, BuiltInSlotType, type SerializedLGraphNode, type Vector2, BuiltInSlotShape, type INodeOutputSlot, type SlotType } from "@litegraph-ts/core";
|
||||
import type { SerializedAppState } from "./components/ComfyApp";
|
||||
import layoutStates, { defaultWorkflowAttributes, type ContainerLayout, type DragItemID, type SerializedDragEntry, type SerializedLayoutState, type WritableLayoutStateStore } from "./stores/layoutStates";
|
||||
import { ComfyWorkflow, type WorkflowAttributes } from "./stores/workflowState";
|
||||
import { ComfyBoxWorkflow, type WorkflowAttributes } from "./stores/workflowState";
|
||||
import type { SerializedGraphCanvasState } from "./ComfyGraphCanvas";
|
||||
import ComfyApp from "./components/ComfyApp";
|
||||
import { iterateNodeDefInputs, type ComfyNodeDefInputType, type ComfyNodeDefInputOptions } from "./ComfyNodeDef";
|
||||
@@ -346,8 +346,8 @@ function removeSerializedNode(vanillaWorkflow: SerializedLGraph, node: Serialize
|
||||
* Converts a workflow saved with vanilla ComfyUI into a ComfyBox workflow,
|
||||
* adding UI nodes for each widget.
|
||||
*/
|
||||
export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWorkflow, attrs: WorkflowAttributes): [ComfyWorkflow, WritableLayoutStateStore] {
|
||||
const [comfyBoxWorkflow, layoutState] = ComfyWorkflow.create();
|
||||
export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWorkflow, attrs: WorkflowAttributes): [ComfyBoxWorkflow, WritableLayoutStateStore] {
|
||||
const [comfyBoxWorkflow, layoutState] = ComfyBoxWorkflow.create();
|
||||
const { root, left, right } = layoutState.initDefaultLayout();
|
||||
|
||||
// TODO will need to convert IDs to UUIDs
|
||||
|
||||
@@ -10,7 +10,7 @@ import { get } from "svelte/store";
|
||||
import configState from "$lib/stores/configState";
|
||||
import type { WidgetLayout, WritableLayoutStateStore } from "$lib/stores/layoutStates";
|
||||
import layoutStates from "$lib/stores/layoutStates";
|
||||
import workflowStateStore, { ComfyWorkflow } from "$lib/stores/workflowState";
|
||||
import workflowStateStore, { ComfyBoxWorkflow } from "$lib/stores/workflowState";
|
||||
|
||||
export type DefaultWidgetSpec = {
|
||||
defaultWidgetNode: new (name?: string) => ComfyWidgetNode,
|
||||
@@ -111,7 +111,7 @@ export default class ComfyGraphNode extends LGraphNode {
|
||||
return layoutStates.getDragItemByNode(this);
|
||||
}
|
||||
|
||||
get workflow(): ComfyWorkflow | null {
|
||||
get workflow(): ComfyBoxWorkflow | null {
|
||||
return workflowStateStore.getWorkflowByNode(this);
|
||||
}
|
||||
|
||||
@@ -312,7 +312,13 @@ export default class ComfyGraphNode extends LGraphNode {
|
||||
}
|
||||
|
||||
(o as any).saveUserState = this.saveUserState
|
||||
if (!this.saveUserState && (!get(uiState).isSavingToLocalStorage || get(configState).alwaysStripUserState)) {
|
||||
|
||||
let saveUserState = this.saveUserState || get(configState).alwaysStripUserState;
|
||||
const forceSaveUserState = get(uiState).forceSaveUserState;
|
||||
if (forceSaveUserState !== null)
|
||||
saveUserState = forceSaveUserState;
|
||||
|
||||
if (!saveUserState) {
|
||||
this.stripUserState(o)
|
||||
console.debug("[ComfyGraphNode] stripUserState", this, o)
|
||||
}
|
||||
|
||||
@@ -162,6 +162,8 @@ export default class ComfyComboNode extends ComfyWidgetNode<string> {
|
||||
override stripUserState(o: SerializedLGraphNode) {
|
||||
super.stripUserState(o);
|
||||
o.properties.values = []
|
||||
o.properties.defaultValue = null;
|
||||
(o as any).comfyValue = null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -297,6 +297,9 @@ export default abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||
}
|
||||
|
||||
notifyPropsChanged() {
|
||||
if (!this.layoutState)
|
||||
return;
|
||||
|
||||
const layoutEntry = this.layoutState.findLayoutEntryForNode(this.id as ComfyNodeID)
|
||||
if (layoutEntry && layoutEntry.parent) {
|
||||
layoutEntry.parent.attrsChanged.set(get(layoutEntry.parent.attrsChanged) + 1)
|
||||
@@ -352,7 +355,6 @@ export default abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||
|
||||
override stripUserState(o: SerializedLGraphNode) {
|
||||
super.stripUserState(o);
|
||||
(o as any).comfyValue = this.defaultValue;
|
||||
o.properties.defaultValue = null;
|
||||
(o as any).comfyValue = this.properties.defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@ import { get, writable } from 'svelte/store';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
export type ConfigState = {
|
||||
/** Backend domain for ComfyUI */
|
||||
comfyUIHostname: string,
|
||||
|
||||
/** Backend port for ComfyUI */
|
||||
comfyUIPort: number,
|
||||
|
||||
/** Strip user state even if saving to local storage */
|
||||
alwaysStripUserState: boolean,
|
||||
|
||||
@@ -14,18 +20,27 @@ export type ConfigState = {
|
||||
}
|
||||
|
||||
type ConfigStateOps = {
|
||||
getBackendURL: () => string
|
||||
}
|
||||
|
||||
export type WritableConfigStateStore = Writable<ConfigState> & ConfigStateOps;
|
||||
const store: Writable<ConfigState> = writable(
|
||||
{
|
||||
comfyUIHostname: "localhost",
|
||||
comfyUIPort: 8188,
|
||||
alwaysStripUserState: false,
|
||||
promptForWorkflowName: false,
|
||||
confirmWhenUnloadingUnsavedChanges: true
|
||||
})
|
||||
|
||||
function getBackendURL(): string {
|
||||
const state = get(store);
|
||||
return `${window.location.protocol}//${state.comfyUIHostname}:${state.comfyUIPort}`
|
||||
}
|
||||
|
||||
const configStateStore: WritableConfigStateStore =
|
||||
{
|
||||
...store
|
||||
...store,
|
||||
getBackendURL
|
||||
}
|
||||
export default configStateStore;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -7,8 +7,9 @@ import type { ComfyNodeID } from '$lib/api';
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import type { ComfyWidgetNode } from '$lib/nodes/widgets';
|
||||
import type ComfyGraph from '$lib/ComfyGraph';
|
||||
import type { ComfyWorkflow, WorkflowAttributes, WorkflowInstID } from './workflowState';
|
||||
import type { ComfyBoxWorkflow, WorkflowAttributes, WorkflowInstID } from './workflowState';
|
||||
import workflowState from './workflowState';
|
||||
import type { SerializedComfyBoxTemplate } from '$lib/ComfyBoxTemplate';
|
||||
|
||||
export function isComfyWidgetNode(node: LGraphNode): node is ComfyWidgetNode {
|
||||
return "svelteComponentType" in node
|
||||
@@ -524,7 +525,37 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
||||
canShow: (di: IDragItem) => di.type === "container"
|
||||
},
|
||||
|
||||
// Combo
|
||||
{
|
||||
name: "defaultValue",
|
||||
type: "string",
|
||||
location: "nodeProps",
|
||||
editable: true,
|
||||
defaultValue: "A",
|
||||
validNodeTypes: ["ui/combo"],
|
||||
},
|
||||
|
||||
// Checkbox
|
||||
{
|
||||
name: "defaultValue",
|
||||
type: "boolean",
|
||||
location: "nodeProps",
|
||||
editable: true,
|
||||
defaultValue: true,
|
||||
validNodeTypes: ["ui/checkbox"],
|
||||
},
|
||||
|
||||
// Range
|
||||
{
|
||||
name: "defaultValue",
|
||||
type: "number",
|
||||
location: "nodeProps",
|
||||
editable: true,
|
||||
defaultValue: 0,
|
||||
min: -2 ^ 16,
|
||||
max: 2 ^ 16,
|
||||
validNodeTypes: ["ui/number"],
|
||||
},
|
||||
{
|
||||
name: "min",
|
||||
type: "number",
|
||||
@@ -586,6 +617,14 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
||||
},
|
||||
|
||||
// Radio
|
||||
{
|
||||
name: "defaultValue",
|
||||
type: "string",
|
||||
location: "nodeProps",
|
||||
editable: true,
|
||||
defaultValue: "Choice A",
|
||||
validNodeTypes: ["ui/radio"],
|
||||
},
|
||||
{
|
||||
name: "choices",
|
||||
type: "string",
|
||||
@@ -597,6 +636,16 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
||||
deserialize: deserializeStringArray,
|
||||
},
|
||||
|
||||
// Text
|
||||
{
|
||||
name: "defaultValue",
|
||||
type: "string",
|
||||
location: "nodeProps",
|
||||
editable: true,
|
||||
defaultValue: "",
|
||||
validNodeTypes: ["ui/text"],
|
||||
},
|
||||
|
||||
// Workflow
|
||||
{
|
||||
name: "title",
|
||||
@@ -635,7 +684,7 @@ for (const cat of Object.values(ALL_ATTRIBUTES)) {
|
||||
export { ALL_ATTRIBUTES };
|
||||
|
||||
// TODO Should be nested by category for name uniqueness?
|
||||
const defaultWidgetAttributes: Attributes = {} as any
|
||||
export const defaultWidgetAttributes: Attributes = {} as any
|
||||
export const defaultWorkflowAttributes: WorkflowAttributes = {} as any
|
||||
for (const cat of Object.values(ALL_ATTRIBUTES)) {
|
||||
for (const spec of Object.values(cat.specs)) {
|
||||
@@ -657,7 +706,7 @@ export interface IDragItem {
|
||||
/*
|
||||
* Type of the item.
|
||||
*/
|
||||
type: "container" | "widget",
|
||||
type: "container" | "widget" | "template",
|
||||
|
||||
/*
|
||||
* Unique ID of the item.
|
||||
@@ -707,6 +756,21 @@ export interface WidgetLayout extends IDragItem {
|
||||
node: ComfyWidgetNode
|
||||
}
|
||||
|
||||
/*
|
||||
* A template being dragged into the workflow UI.
|
||||
*
|
||||
* These will never be saved, instead they will be expanded inside
|
||||
* updateChildren() and then removed.
|
||||
*/
|
||||
export interface TemplateLayout extends IDragItem {
|
||||
type: "template",
|
||||
|
||||
/*
|
||||
* Template to expand
|
||||
*/
|
||||
template: SerializedComfyBoxTemplate
|
||||
}
|
||||
|
||||
export type DefaultLayout = {
|
||||
root: ContainerLayout,
|
||||
left: ContainerLayout,
|
||||
@@ -716,7 +780,7 @@ export type DefaultLayout = {
|
||||
export type DragItemID = UUID;
|
||||
|
||||
type LayoutStateOps = {
|
||||
workflow: ComfyWorkflow | null,
|
||||
workflow: ComfyBoxWorkflow | null,
|
||||
|
||||
addContainer: (parent: ContainerLayout | null, attrs?: Partial<Attributes>, index?: number) => ContainerLayout,
|
||||
addWidget: (parent: ContainerLayout, node: ComfyWidgetNode, attrs?: Partial<Attributes>, index?: number) => WidgetLayout,
|
||||
@@ -724,6 +788,7 @@ type LayoutStateOps = {
|
||||
updateChildren: (parent: IDragItem, children: IDragItem[]) => IDragItem[],
|
||||
nodeAdded: (node: LGraphNode, options: LGraphAddNodeOptions) => void,
|
||||
nodeRemoved: (node: LGraphNode, options: LGraphRemoveNodeOptions) => void,
|
||||
insertTemplate: (template: SerializedComfyBoxTemplate, graph: LGraph, templateNodeIDToNode: Record<NodeID, LGraphNode>, container: ContainerLayout, childIndex: number) => IDragItem,
|
||||
moveItem: (target: IDragItem, to: ContainerLayout, index?: number) => void,
|
||||
groupItems: (dragItemIDs: DragItemID[], attrs?: Partial<Attributes>) => ContainerLayout,
|
||||
ungroup: (container: ContainerLayout) => void,
|
||||
@@ -752,13 +817,13 @@ export type SerializedDragEntry = {
|
||||
export type SerializedDragItem = {
|
||||
type: string,
|
||||
id: DragItemID,
|
||||
nodeId: UUID | null,
|
||||
nodeId: NodeID | null,
|
||||
attrs: Attributes
|
||||
}
|
||||
|
||||
export type WritableLayoutStateStore = Writable<LayoutState> & LayoutStateOps;
|
||||
|
||||
function createRaw(workflow: ComfyWorkflow | null = null): WritableLayoutStateStore {
|
||||
function createRaw(workflow: ComfyBoxWorkflow | null = null): WritableLayoutStateStore {
|
||||
const store: Writable<LayoutState> = writable({
|
||||
root: null,
|
||||
allItems: {},
|
||||
@@ -874,7 +939,7 @@ function createRaw(workflow: ComfyWorkflow | null = null): WritableLayoutStateSt
|
||||
if (newChildren)
|
||||
state.allItems[parent.id].children = newChildren;
|
||||
for (const child of state.allItems[parent.id].children) {
|
||||
if (child.id === SHADOW_PLACEHOLDER_ITEM_ID)
|
||||
if (child.id === SHADOW_PLACEHOLDER_ITEM_ID || child.type === "template")
|
||||
continue;
|
||||
state.allItems[child.id].parent = parent;
|
||||
}
|
||||
@@ -912,7 +977,11 @@ function createRaw(workflow: ComfyWorkflow | null = null): WritableLayoutStateSt
|
||||
|
||||
let attrs: Partial<Attributes> = {}
|
||||
|
||||
if (options.addedBy === "moveIntoSubgraph" || options.addedBy === "moveOutOfSubgraph") {
|
||||
if ((options.addedBy as any) === "template") {
|
||||
// Template layout will be deserialized shortly
|
||||
return;
|
||||
}
|
||||
else if (options.addedBy === "moveIntoSubgraph" || options.addedBy === "moveOutOfSubgraph") {
|
||||
// All we need to do is update the nodeID linked to this node.
|
||||
const item = state.allItemsByNode[options.prevNodeID]
|
||||
delete state.allItemsByNode[options.prevNodeID]
|
||||
@@ -984,6 +1053,56 @@ function createRaw(workflow: ComfyWorkflow | null = null): WritableLayoutStateSt
|
||||
store.set(state)
|
||||
}
|
||||
|
||||
/*
|
||||
* NOTE: Modifies the template in-place, be sure you cloned it beforehand!
|
||||
*/
|
||||
function insertTemplate(template: SerializedComfyBoxTemplate, graph: LGraph, templateNodeIDToNode: Record<NodeID, LGraphNode>, container: ContainerLayout, childIndex: number): IDragItem {
|
||||
const idMapping: Record<DragItemID, DragItemID> = {};
|
||||
|
||||
const getDragItemID = (id: DragItemID): DragItemID => {
|
||||
idMapping[id] ||= uuidv4();
|
||||
return idMapping[id];
|
||||
}
|
||||
|
||||
// Ensure all IDs are unique, and rewrite node IDs in widgets to point
|
||||
// to newly created nodes
|
||||
for (const [id, entry] of Object.entries(template.layout.allItems)) {
|
||||
const newId = getDragItemID(id);
|
||||
template.layout.allItems[newId] = entry;
|
||||
entry.dragItem.id = newId;
|
||||
|
||||
if (entry.parent)
|
||||
entry.parent = getDragItemID(entry.parent)
|
||||
entry.children = entry.children.map(getDragItemID);
|
||||
|
||||
if (entry.dragItem.type === "widget") {
|
||||
entry.dragItem.nodeId = templateNodeIDToNode[entry.dragItem.nodeId].id;
|
||||
}
|
||||
}
|
||||
|
||||
if (template.layout.root) {
|
||||
template.layout.root = getDragItemID(template.layout.root)
|
||||
|
||||
// make sure the new root doesn't have a parent since that parent
|
||||
// was detached from the serialized layout and won't be found in
|
||||
// template.layout.allItems
|
||||
template.layout.allItems[template.layout.root].parent = null;
|
||||
}
|
||||
|
||||
const raw = deserializeRaw(template.layout, graph);
|
||||
|
||||
// merge the template's detached layout tree into this layout
|
||||
store.update(s => {
|
||||
s.allItems = { ...s.allItems, ...raw.allItems }
|
||||
s.allItemsByNode = { ...s.allItemsByNode, ...raw.allItemsByNode }
|
||||
return s;
|
||||
})
|
||||
|
||||
moveItem(raw.root, container, childIndex);
|
||||
|
||||
return raw.root
|
||||
}
|
||||
|
||||
function moveItem(target: IDragItem, to: ContainerLayout, index?: number) {
|
||||
const state = get(store)
|
||||
const entry = state.allItems[target.id]
|
||||
@@ -1148,6 +1267,13 @@ function createRaw(workflow: ComfyWorkflow | null = null): WritableLayoutStateSt
|
||||
const queue = [state.allItems[rootID]]
|
||||
while (queue.length > 0) {
|
||||
const entry = queue.shift();
|
||||
|
||||
if (entry.dragItem.type === "template") {
|
||||
// If this happens then there's a bug somewhere
|
||||
console.error("[layoutState] Found template drag item in current layout, skipping!")
|
||||
continue;
|
||||
}
|
||||
|
||||
allItems[entry.dragItem.id] = {
|
||||
dragItem: {
|
||||
type: entry.dragItem.type,
|
||||
@@ -1177,6 +1303,13 @@ function createRaw(workflow: ComfyWorkflow | null = null): WritableLayoutStateSt
|
||||
const allItems: Record<DragItemID, SerializedDragEntry> = {}
|
||||
for (const pair of Object.entries(state.allItems)) {
|
||||
const [id, entry] = pair;
|
||||
|
||||
if (entry.dragItem.type === "template") {
|
||||
// If this happens then there's a bug somewhere
|
||||
console.error("[layoutState] Found template drag item in current layout, skipping!")
|
||||
continue;
|
||||
}
|
||||
|
||||
allItems[id] = {
|
||||
dragItem: {
|
||||
type: entry.dragItem.type,
|
||||
@@ -1195,12 +1328,19 @@ function createRaw(workflow: ComfyWorkflow | null = null): WritableLayoutStateSt
|
||||
}
|
||||
}
|
||||
|
||||
function deserialize(data: SerializedLayoutState, graph: LGraph) {
|
||||
function deserializeRaw(data: SerializedLayoutState, graph: LGraph): LayoutState {
|
||||
const allItems: Record<DragItemID, DragItemEntry> = {}
|
||||
const allItemsByNode: Record<number, DragItemEntry> = {}
|
||||
|
||||
for (const pair of Object.entries(data.allItems)) {
|
||||
const [id, entry] = pair;
|
||||
|
||||
if (entry.dragItem.type === "template") {
|
||||
// If this happens then there's a bug somewhere
|
||||
console.error("[layoutState] Found template drag item in serialized layout, skipping!")
|
||||
continue;
|
||||
}
|
||||
|
||||
const dragItem: IDragItem = {
|
||||
type: entry.dragItem.type,
|
||||
id: entry.dragItem.id,
|
||||
@@ -1249,6 +1389,12 @@ function createRaw(workflow: ComfyWorkflow | null = null): WritableLayoutStateSt
|
||||
isConfiguring: false,
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
function deserialize(data: SerializedLayoutState, graph: LGraph) {
|
||||
const state = deserializeRaw(data, graph);
|
||||
|
||||
console.debug("[layoutState] deserialize", data, state, defaultWorkflowAttributes)
|
||||
|
||||
store.set(state)
|
||||
@@ -1282,6 +1428,7 @@ function createRaw(workflow: ComfyWorkflow | null = null): WritableLayoutStateSt
|
||||
updateChildren,
|
||||
nodeAdded,
|
||||
nodeRemoved,
|
||||
insertTemplate,
|
||||
moveItem,
|
||||
groupItems,
|
||||
findLayoutEntryForNode,
|
||||
@@ -1299,7 +1446,7 @@ function createRaw(workflow: ComfyWorkflow | null = null): WritableLayoutStateSt
|
||||
return layoutStateStore
|
||||
}
|
||||
|
||||
function create(workflow: ComfyWorkflow): WritableLayoutStateStore {
|
||||
function create(workflow: ComfyBoxWorkflow): WritableLayoutStateStore {
|
||||
if (get(layoutStates).all[workflow.id] != null) {
|
||||
throw new Error(`Layout state already created! ${id}`)
|
||||
}
|
||||
@@ -1369,8 +1516,8 @@ export type LayoutStateStores = {
|
||||
}
|
||||
|
||||
export type LayoutStateStoresOps = {
|
||||
create: (workflow: ComfyWorkflow) => WritableLayoutStateStore,
|
||||
createRaw: (workflow?: ComfyWorkflow | null) => WritableLayoutStateStore,
|
||||
create: (workflow: ComfyBoxWorkflow) => WritableLayoutStateStore,
|
||||
createRaw: (workflow?: ComfyBoxWorkflow | null) => WritableLayoutStateStore,
|
||||
remove: (workflowID: WorkflowInstID) => void,
|
||||
getLayout: (workflowID: WorkflowInstID) => WritableLayoutStateStore | null,
|
||||
getLayoutByGraph: (graph: LGraph) => WritableLayoutStateStore | null,
|
||||
|
||||
@@ -7,7 +7,7 @@ export type ModalState = Record<string, any>;
|
||||
export type ModalButton = {
|
||||
name: string,
|
||||
variant: "primary" | "secondary",
|
||||
onClick: (state: ModalData) => void,
|
||||
onClick: (state: ModalData) => boolean | void,
|
||||
closeOnClick?: boolean
|
||||
}
|
||||
export interface ModalData {
|
||||
|
||||
128
src/lib/stores/templateState.ts
Normal file
128
src/lib/stores/templateState.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import type { SerializedComfyBoxTemplate } from '$lib/ComfyBoxTemplate';
|
||||
import type { UUID } from '@litegraph-ts/core';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import type { Readable, Writable } from 'svelte/store';
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export type TemplateState = {
|
||||
templates: SerializedComfyBoxTemplate[]
|
||||
templatesByID: Record<UUID, SerializedComfyBoxTemplate>
|
||||
}
|
||||
|
||||
type TemplateStateOps = {
|
||||
save: () => void,
|
||||
load: () => void,
|
||||
add: (template: SerializedComfyBoxTemplate) => boolean,
|
||||
update: (template: SerializedComfyBoxTemplate) => boolean,
|
||||
remove: (templateID: UUID) => boolean,
|
||||
}
|
||||
|
||||
export type WritableTemplateStateStore = Writable<TemplateState> & TemplateStateOps;
|
||||
const store: Writable<TemplateState> = writable(
|
||||
{
|
||||
templates: [],
|
||||
templatesByID: {}
|
||||
})
|
||||
|
||||
function add(template: SerializedComfyBoxTemplate): boolean {
|
||||
const state = get(store);
|
||||
if (state.templatesByID[template.id]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
store.update(s => {
|
||||
s.templates.push(template);
|
||||
s.templatesByID[template.id] = template;
|
||||
return s;
|
||||
})
|
||||
|
||||
save();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function remove(templateID: UUID): boolean {
|
||||
const state = get(store);
|
||||
if (!state.templatesByID[templateID]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
store.update(s => {
|
||||
const index = s.templates.findIndex(t => t.id === templateID)
|
||||
s.templates.splice(index, 1);
|
||||
delete s.templatesByID[templateID];
|
||||
return s;
|
||||
})
|
||||
|
||||
save();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function update(template: SerializedComfyBoxTemplate): boolean {
|
||||
const state = get(store);
|
||||
if (!state.templatesByID[template.id]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
store.update(s => {
|
||||
const oldId = template.id
|
||||
const index = s.templates.findIndex(t => t.id === oldId)
|
||||
s.templates.splice(index, 1);
|
||||
delete s.templatesByID[oldId];
|
||||
|
||||
template.id = uuidv4();
|
||||
|
||||
s.templates.push(template);
|
||||
s.templatesByID[template.id] = template;
|
||||
return s;
|
||||
})
|
||||
|
||||
save();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function save() {
|
||||
const json = JSON.stringify(get(store).templates)
|
||||
localStorage.setItem("templates", json)
|
||||
store.set(get(store))
|
||||
}
|
||||
|
||||
function load() {
|
||||
const json = localStorage.getItem("templates")
|
||||
if (!json) {
|
||||
console.info("No templates in local storage, creating store")
|
||||
save();
|
||||
return;
|
||||
}
|
||||
|
||||
const data = JSON.parse(json) as SerializedComfyBoxTemplate[];
|
||||
if (Array.isArray(data)) {
|
||||
const templatesByID: Record<UUID, SerializedComfyBoxTemplate> =
|
||||
data.map(d => [d.id, d])
|
||||
.reduce((dict, el: [UUID, SerializedComfyBoxTemplate]) => (dict[el[0]] = el[1], dict), {})
|
||||
|
||||
store.set({
|
||||
templates: data,
|
||||
templatesByID
|
||||
})
|
||||
}
|
||||
else {
|
||||
store.set({
|
||||
templates: [],
|
||||
templatesByID: {}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const templateStateStore: WritableTemplateStateStore =
|
||||
{
|
||||
...store,
|
||||
add,
|
||||
remove,
|
||||
update,
|
||||
save,
|
||||
load,
|
||||
}
|
||||
export default templateStateStore;
|
||||
@@ -11,7 +11,7 @@ export type UIState = {
|
||||
uiEditMode: UIEditMode,
|
||||
|
||||
reconnecting: boolean,
|
||||
isSavingToLocalStorage: boolean
|
||||
forceSaveUserState: boolean | null
|
||||
}
|
||||
|
||||
type UIStateOps = {
|
||||
@@ -29,7 +29,7 @@ const store: Writable<UIState> = writable(
|
||||
uiEditMode: "widgets",
|
||||
|
||||
reconnecting: false,
|
||||
isSavingToLocalStorage: false
|
||||
forceSaveUserState: null,
|
||||
})
|
||||
|
||||
function reconnecting() {
|
||||
|
||||
@@ -56,7 +56,7 @@ export type WorkflowAttributes = {
|
||||
queuePromptButtonRunWorkflow: boolean,
|
||||
}
|
||||
|
||||
export class ComfyWorkflow {
|
||||
export class ComfyBoxWorkflow {
|
||||
/*
|
||||
* Used for uniquely identifying the instance of the opened workflow in the frontend.
|
||||
*/
|
||||
@@ -176,8 +176,8 @@ export class ComfyWorkflow {
|
||||
* will not. If you change your mind later be sure to call
|
||||
* layoutStates.remove(workflow.id)!
|
||||
*/
|
||||
static create(title: string = "New Workflow"): [ComfyWorkflow, WritableLayoutStateStore] {
|
||||
const workflow = new ComfyWorkflow(title);
|
||||
static create(title: string = "New Workflow"): [ComfyBoxWorkflow, WritableLayoutStateStore] {
|
||||
const workflow = new ComfyBoxWorkflow(title);
|
||||
const layoutState = layoutStates.create(workflow);
|
||||
return [workflow, layoutState]
|
||||
}
|
||||
@@ -227,29 +227,29 @@ export class ComfyWorkflow {
|
||||
}
|
||||
|
||||
export type WorkflowState = {
|
||||
openedWorkflows: ComfyWorkflow[],
|
||||
openedWorkflowsByID: Record<WorkflowInstID, ComfyWorkflow>,
|
||||
openedWorkflows: ComfyBoxWorkflow[],
|
||||
openedWorkflowsByID: Record<WorkflowInstID, ComfyBoxWorkflow>,
|
||||
activeWorkflowID: WorkflowInstID | null,
|
||||
activeWorkflow: ComfyWorkflow | null,
|
||||
activeWorkflow: ComfyBoxWorkflow | null,
|
||||
}
|
||||
|
||||
export type WorkflowReceiveOutputTargets = {
|
||||
workflow: ComfyWorkflow,
|
||||
workflow: ComfyBoxWorkflow,
|
||||
targetNodes: ComfyReceiveOutputNode[]
|
||||
}
|
||||
|
||||
type WorkflowStateOps = {
|
||||
getWorkflow: (id: WorkflowInstID) => ComfyWorkflow | null
|
||||
getWorkflowByGraph: (graph: LGraph) => ComfyWorkflow | null
|
||||
getWorkflowByNode: (node: LGraphNode) => ComfyWorkflow | null
|
||||
getWorkflowByNodeID: (id: NodeID) => ComfyWorkflow | null
|
||||
getActiveWorkflow: () => ComfyWorkflow | null
|
||||
createNewWorkflow: (canvas: ComfyGraphCanvas, title?: string, setActive?: boolean) => ComfyWorkflow,
|
||||
openWorkflow: (canvas: ComfyGraphCanvas, data: SerializedAppState, setActive?: boolean) => ComfyWorkflow,
|
||||
addWorkflow: (canvas: ComfyGraphCanvas, data: ComfyWorkflow, setActive?: boolean) => void,
|
||||
getWorkflow: (id: WorkflowInstID) => ComfyBoxWorkflow | null
|
||||
getWorkflowByGraph: (graph: LGraph) => ComfyBoxWorkflow | null
|
||||
getWorkflowByNode: (node: LGraphNode) => ComfyBoxWorkflow | null
|
||||
getWorkflowByNodeID: (id: NodeID) => ComfyBoxWorkflow | null
|
||||
getActiveWorkflow: () => ComfyBoxWorkflow | null
|
||||
createNewWorkflow: (canvas: ComfyGraphCanvas, title?: string, setActive?: boolean) => ComfyBoxWorkflow,
|
||||
openWorkflow: (canvas: ComfyGraphCanvas, data: SerializedAppState, setActive?: boolean) => ComfyBoxWorkflow,
|
||||
addWorkflow: (canvas: ComfyGraphCanvas, data: ComfyBoxWorkflow, setActive?: boolean) => void,
|
||||
closeWorkflow: (canvas: ComfyGraphCanvas, index: number) => void,
|
||||
closeAllWorkflows: (canvas: ComfyGraphCanvas) => void,
|
||||
setActiveWorkflow: (canvas: ComfyGraphCanvas, index: number | WorkflowInstID) => ComfyWorkflow | null,
|
||||
setActiveWorkflow: (canvas: ComfyGraphCanvas, index: number | WorkflowInstID) => ComfyBoxWorkflow | null,
|
||||
findReceiveOutputTargets: (type: SlotType | SlotType[]) => WorkflowReceiveOutputTargets[]
|
||||
}
|
||||
|
||||
@@ -262,36 +262,36 @@ const store: Writable<WorkflowState> = writable(
|
||||
activeWorkflow: null
|
||||
})
|
||||
|
||||
function getWorkflow(id: WorkflowInstID): ComfyWorkflow | null {
|
||||
function getWorkflow(id: WorkflowInstID): ComfyBoxWorkflow | null {
|
||||
return get(store).openedWorkflowsByID[id];
|
||||
}
|
||||
|
||||
function getWorkflowByGraph(graph: LGraph): ComfyWorkflow | null {
|
||||
function getWorkflowByGraph(graph: LGraph): ComfyBoxWorkflow | null {
|
||||
const workflowID = (graph.getRootGraph() as ComfyGraph)?.workflowID;
|
||||
if (workflowID == null)
|
||||
return null;
|
||||
return getWorkflow(workflowID);
|
||||
}
|
||||
|
||||
function getWorkflowByNode(node: LGraphNode): ComfyWorkflow | null {
|
||||
function getWorkflowByNode(node: LGraphNode): ComfyBoxWorkflow | null {
|
||||
return getWorkflowByGraph(node.graph);
|
||||
}
|
||||
|
||||
function getWorkflowByNodeID(id: NodeID): ComfyWorkflow | null {
|
||||
function getWorkflowByNodeID(id: NodeID): ComfyBoxWorkflow | null {
|
||||
return Object.values(get(store).openedWorkflows).find(w => {
|
||||
return w.graph.getNodeByIdRecursive(id) != null
|
||||
})
|
||||
}
|
||||
|
||||
function getActiveWorkflow(): ComfyWorkflow | null {
|
||||
function getActiveWorkflow(): ComfyBoxWorkflow | null {
|
||||
const state = get(store);
|
||||
if (state.activeWorkflowID == null)
|
||||
return null;
|
||||
return state.openedWorkflowsByID[state.activeWorkflowID];
|
||||
}
|
||||
|
||||
function createNewWorkflow(canvas: ComfyGraphCanvas, title: string = "New Workflow", setActive: boolean = false): ComfyWorkflow {
|
||||
const workflow = new ComfyWorkflow(title);
|
||||
function createNewWorkflow(canvas: ComfyGraphCanvas, title: string = "New Workflow", setActive: boolean = false): ComfyBoxWorkflow {
|
||||
const workflow = new ComfyBoxWorkflow(title);
|
||||
const layoutState = layoutStates.create(workflow);
|
||||
layoutState.initDefaultLayout();
|
||||
|
||||
@@ -307,8 +307,8 @@ function createNewWorkflow(canvas: ComfyGraphCanvas, title: string = "New Workfl
|
||||
return workflow;
|
||||
}
|
||||
|
||||
function openWorkflow(canvas: ComfyGraphCanvas, data: SerializedAppState, setActive: boolean = true): ComfyWorkflow {
|
||||
const [workflow, layoutState] = ComfyWorkflow.create("Workflow")
|
||||
function openWorkflow(canvas: ComfyGraphCanvas, data: SerializedAppState, setActive: boolean = true): ComfyBoxWorkflow {
|
||||
const [workflow, layoutState] = ComfyBoxWorkflow.create("Workflow")
|
||||
workflow.deserialize(layoutState, { graph: data.workflow, layout: data.layout, attrs: data.attrs })
|
||||
|
||||
addWorkflow(canvas, workflow, setActive);
|
||||
@@ -316,7 +316,7 @@ function openWorkflow(canvas: ComfyGraphCanvas, data: SerializedAppState, setAct
|
||||
return workflow;
|
||||
}
|
||||
|
||||
function addWorkflow(canvas: ComfyGraphCanvas, workflow: ComfyWorkflow, setActive: boolean = true) {
|
||||
function addWorkflow(canvas: ComfyGraphCanvas, workflow: ComfyBoxWorkflow, setActive: boolean = true) {
|
||||
const state = get(store);
|
||||
state.openedWorkflows.push(workflow);
|
||||
state.openedWorkflowsByID[workflow.id] = workflow;
|
||||
@@ -354,7 +354,7 @@ function closeAllWorkflows(canvas: ComfyGraphCanvas) {
|
||||
closeWorkflow(canvas, 0)
|
||||
}
|
||||
|
||||
function setActiveWorkflow(canvas: ComfyGraphCanvas, index: number | WorkflowInstID): ComfyWorkflow | null {
|
||||
function setActiveWorkflow(canvas: ComfyGraphCanvas, index: number | WorkflowInstID): ComfyBoxWorkflow | null {
|
||||
const state = get(store);
|
||||
|
||||
if (state.openedWorkflows.length === 0) {
|
||||
|
||||
1171
src/lib/utils.ts
1171
src/lib/utils.ts
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@
|
||||
import "klecks/style/style.scss";
|
||||
import ImageUpload from "$lib/components/ImageUpload.svelte";
|
||||
import { uploadImageToComfyUI, type ComfyBoxImageMetadata, comfyFileToComfyBoxMetadata, comfyBoxImageToComfyURL, comfyBoxImageToComfyFile, type ComfyUploadImageType, type ComfyImageLocation } from "$lib/utils";
|
||||
import configState from "$lib/stores/configState";
|
||||
import notify from "$lib/notify";
|
||||
import NumberInput from "$lib/components/NumberInput.svelte";
|
||||
import type { ComfyImageEditorNode } from "$lib/nodes/widgets";
|
||||
@@ -102,7 +103,7 @@
|
||||
|
||||
showModal = true;
|
||||
|
||||
const url = `http://${location.hostname}:8188` // TODO make configurable
|
||||
const url = configState.getBackendURL();
|
||||
|
||||
kl = new Klecks({
|
||||
embedUrl: url,
|
||||
|
||||
@@ -86,7 +86,7 @@ const COLOR_MAP: [string, string][] = [
|
||||
_node ||= node;
|
||||
bboxes ||= $nodeValue
|
||||
|
||||
console.debug("[MultiRegionWidget] Recreate!", bboxes, imageElem, _node)
|
||||
// console.debug("[MultiRegionWidget] Recreate!", bboxes, _node)
|
||||
|
||||
if (_node != null && imageElem != null && imageContainer != null) {
|
||||
selectedIndex = clamp(selectedIndex, 0, bboxes.length - 1);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import { basicSetup } from "./TextWidgetCodeVariant";
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
import { TAG_CATEGORY_COLORS } from "$lib/DanbooruTags";
|
||||
import { Block, BlockTitle } from "@gradio/atoms";
|
||||
|
||||
export let widget: WidgetLayout;
|
||||
export let node: ComfyTextNode;
|
||||
@@ -176,7 +177,7 @@
|
||||
function getExtensions(): Extension[] {
|
||||
// TODO
|
||||
const readonly = false;
|
||||
const placeholder = "Placeholder..."
|
||||
const placeholder = ""
|
||||
const dark_mode = true;
|
||||
|
||||
const stateExtensions: Extension[] = [
|
||||
@@ -206,12 +207,22 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrap">
|
||||
<div class="codemirror-wrapper {classNames}" bind:this={element} />
|
||||
<div class="code-editor-wrapper">
|
||||
<Block>
|
||||
<BlockTitle>{widget.attrs.title}</BlockTitle>
|
||||
<div class="wrap">
|
||||
<div class="codemirror-wrapper {classNames}" bind:this={element} />
|
||||
</div>
|
||||
</Block>
|
||||
</div>
|
||||
<!-- <CodeMirror bind:value={$nodeValue} {styles} /> -->
|
||||
|
||||
<style lang="scss">
|
||||
.code-editor-wrapper {
|
||||
:global(> .block) {
|
||||
background: var(--panel-background-fill) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
|
||||
import queueState from "$lib/stores/queueState";
|
||||
import workflowState, { ComfyWorkflow } from "$lib/stores/workflowState";
|
||||
import workflowState, { ComfyBoxWorkflow } from "$lib/stores/workflowState";
|
||||
import { getNodeInfo } from "$lib/utils"
|
||||
|
||||
import { Link, Toolbar } from "framework7-svelte"
|
||||
@@ -14,7 +14,7 @@
|
||||
export let app: ComfyApp = undefined;
|
||||
let layoutState: WritableLayoutStateStore = null;
|
||||
let fileInput: HTMLInputElement = undefined;
|
||||
let workflow: ComfyWorkflow | null = null;
|
||||
let workflow: ComfyBoxWorkflow | null = null;
|
||||
|
||||
$: workflow = $workflowState.activeWorkflow;
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
import type ComfyApp from "$lib/components/ComfyApp";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
import type { WritableLayoutStateStore } from "$lib/stores/layoutStates";
|
||||
import workflowState, { type ComfyWorkflow } from "$lib/stores/workflowState";
|
||||
import workflowState, { type ComfyBoxWorkflow } from "$lib/stores/workflowState";
|
||||
|
||||
export let subworkflowID: number = -1;
|
||||
export let app: ComfyApp
|
||||
|
||||
// TODO move
|
||||
let workflow: ComfyWorkflow | null = null
|
||||
let workflow: ComfyBoxWorkflow | null = null
|
||||
let layoutState: WritableLayoutStateStore | null = null;
|
||||
|
||||
$: workflow = $workflowState.activeWorkflow;
|
||||
|
||||
@@ -6,10 +6,13 @@ body {
|
||||
|
||||
// Disable pull to refresh
|
||||
overscroll-behavior-y: contain;
|
||||
}
|
||||
|
||||
#app-root {
|
||||
background: var(--body-background-fill);
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0px;
|
||||
font-family: Arial;
|
||||
}
|
||||
|
||||
:root {
|
||||
@@ -125,6 +128,7 @@ body {
|
||||
border-color: var(--comfy-disabled-textbox-border-color);
|
||||
box-shadow: 0 0 0 var(--shadow-spread) transparent, rgba(0, 0, 0, 0.08) 0px 2px 4px 0px inset;
|
||||
cursor: not-allowed;
|
||||
color: var(--comfy-disabled-label-color);
|
||||
}
|
||||
|
||||
@mixin disable-inputs {
|
||||
@@ -216,3 +220,8 @@ button {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// fixes drop item shadow not appearing when dragging template items onto workflow
|
||||
:global([data-is-dnd-shadow-item]) {
|
||||
min-height: 5rem;
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ import { graphToGraphVis } from "$lib/utils";
|
||||
import { ComfyNumberNode } from "$lib/nodes/widgets";
|
||||
import { get } from "svelte/store";
|
||||
import layoutStates from "$lib/stores/layoutStates";
|
||||
import { ComfyWorkflow } from "$lib/stores/workflowState";
|
||||
import { ComfyBoxWorkflow } from "$lib/stores/workflowState";
|
||||
|
||||
export default class ComfyGraphTests extends UnitTest {
|
||||
test__onNodeAdded__updatesLayoutState() {
|
||||
const [{ graph }, layoutState] = ComfyWorkflow.create()
|
||||
const [{ graph }, layoutState] = ComfyBoxWorkflow.create()
|
||||
layoutState.initDefaultLayout() // adds 3 containers
|
||||
|
||||
const state = get(layoutState)
|
||||
@@ -39,7 +39,7 @@ export default class ComfyGraphTests extends UnitTest {
|
||||
}
|
||||
|
||||
test__onNodeAdded__handlesNodesAddedInSubgraphs() {
|
||||
const [{ graph }, layoutState] = ComfyWorkflow.create()
|
||||
const [{ graph }, layoutState] = ComfyBoxWorkflow.create()
|
||||
layoutState.initDefaultLayout()
|
||||
|
||||
const subgraph = LiteGraph.createNode(Subgraph);
|
||||
@@ -58,7 +58,7 @@ export default class ComfyGraphTests extends UnitTest {
|
||||
}
|
||||
|
||||
test__onNodeAdded__handlesSubgraphsWithNodes() {
|
||||
const [{ graph }, layoutState] = ComfyWorkflow.create()
|
||||
const [{ graph }, layoutState] = ComfyBoxWorkflow.create()
|
||||
layoutState.initDefaultLayout()
|
||||
|
||||
const state = get(layoutState)
|
||||
@@ -76,7 +76,7 @@ export default class ComfyGraphTests extends UnitTest {
|
||||
}
|
||||
|
||||
test__onNodeRemoved__updatesLayoutState() {
|
||||
const [{ graph }, layoutState] = ComfyWorkflow.create()
|
||||
const [{ graph }, layoutState] = ComfyBoxWorkflow.create()
|
||||
layoutState.initDefaultLayout()
|
||||
|
||||
const widget = LiteGraph.createNode(ComfyNumberNode);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"checkJs": true,
|
||||
"strict": false,
|
||||
"baseUrl": "./src",
|
||||
"typeRoots": ["./node_modules/@types/", "./src"],
|
||||
"paths": {
|
||||
"$lib": ["lib"],
|
||||
"$lib/*": ["lib/*"]
|
||||
|
||||
@@ -6,13 +6,17 @@ import FullReload from 'vite-plugin-full-reload';
|
||||
import { viteStaticCopy } from 'vite-plugin-static-copy'
|
||||
import removeConsole from 'vite-plugin-svelte-console-remover';
|
||||
import glsl from 'vite-plugin-glsl';
|
||||
import { execSync } from "child_process"
|
||||
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
console.log("Production build: " + isProduction)
|
||||
|
||||
const commitHash = execSync('git rev-parse HEAD').toString();
|
||||
console.log("Commit: " + commitHash)
|
||||
|
||||
export default defineConfig({
|
||||
define: {
|
||||
"__GIT_COMMIT_HASH__": '"test"'
|
||||
"__GIT_COMMIT_HASH__": JSON.stringify(commitHash)
|
||||
},
|
||||
clearScreen: false,
|
||||
base: "./",
|
||||
|
||||
Reference in New Issue
Block a user