Merge pull request #72 from space-nuko/builtin-templates
Builtin templates
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,3 +9,4 @@ node_modules
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
/dist
|
||||
/stats.html
|
||||
|
||||
Submodule litegraph updated: 8ca6cca777...d335948703
@@ -5059,14 +5059,15 @@
|
||||
"\n",
|
||||
"![ ! -d $WORKSPACE ] && echo -= Initial setup ComfyUI =- && git clone https://github.com/comfyanonymous/ComfyUI\n",
|
||||
"%cd $WORKSPACE\n",
|
||||
"![ ! -d \"ComfyBox\" ] && echo -= Initial setup ComfyBox =- && git clone --recursive https://github.com/space-nuko/ComfyBox -b subgraph-templates3 && cd ComfyBox && pnpm install && pnpm prebuild && pnpm build && cd ..\n",
|
||||
"![ ! -d \"ComfyBox\" ] && echo -= Initial setup ComfyBox =- && git clone --recursive https://github.com/space-nuko/ComfyBox && cd ComfyBox && pnpm install && pnpm prebuild && pnpm build && cd ..\n",
|
||||
"\n",
|
||||
"if OPTIONS['UPDATE_COMFY_UI']:\n",
|
||||
" !echo -= Updating ComfyUI =-\n",
|
||||
" !git pull\n",
|
||||
" !echo -= Updating ComfyBox =-\n",
|
||||
" %cd ComfyBox\n",
|
||||
" !git pull\n",
|
||||
" !git fetch -a\n",
|
||||
" !git reset --hard origin/master\n",
|
||||
" !pnpm install\n",
|
||||
" !pnpm prebuild\n",
|
||||
" !pnpm build\n",
|
||||
@@ -5585,4 +5586,4 @@
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"jsdom": "^22.0.0",
|
||||
"prettier": "^2.8.7",
|
||||
"prettier-plugin-svelte": "^2.10.0",
|
||||
"rollup-plugin-visualizer": "^5.9.0",
|
||||
"sass": "^1.61.0",
|
||||
"svelte": "^3.58.0",
|
||||
"svelte-check": "^3.2.0",
|
||||
@@ -73,7 +74,6 @@
|
||||
"@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",
|
||||
"cm6-theme-basic-light": "^0.2.0",
|
||||
|
||||
57
pnpm-lock.yaml
generated
57
pnpm-lock.yaml
generated
@@ -97,9 +97,6 @@ importers:
|
||||
'@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)
|
||||
canvas-to-svg:
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3
|
||||
@@ -212,6 +209,9 @@ importers:
|
||||
prettier-plugin-svelte:
|
||||
specifier: ^2.10.0
|
||||
version: 2.10.0(prettier@2.8.7)(svelte@3.58.0)
|
||||
rollup-plugin-visualizer:
|
||||
specifier: ^5.9.0
|
||||
version: 5.9.0
|
||||
sass:
|
||||
specifier: ^1.61.0
|
||||
version: 1.61.0
|
||||
@@ -3455,14 +3455,6 @@ packages:
|
||||
pretty-format: 27.5.1
|
||||
dev: false
|
||||
|
||||
/@zerodevx/svelte-json-view@1.0.5(svelte@3.58.0):
|
||||
resolution: {integrity: sha512-oQDI9v0dJEte6PYVDVjLOjU58AOoWLYRXjghKggFpZXrglWJJqoMeDe14Jrd0cs6NPcPogT/aR/LtkuW2Z1GkQ==}
|
||||
peerDependencies:
|
||||
svelte: ^3.55.1
|
||||
dependencies:
|
||||
svelte: 3.58.0
|
||||
dev: false
|
||||
|
||||
/@zerodevx/svelte-toast@0.9.3(svelte@3.58.0):
|
||||
resolution: {integrity: sha512-VPKWR4A9y01fyXRscu9HiTj7tV2hFrpRKZvGwMmaPXfHIXR1D9+NNsz0HXcQ7qZ0C5UaHS3n9uNtPtIcAXT7RQ==}
|
||||
peerDependencies:
|
||||
@@ -4487,6 +4479,11 @@ packages:
|
||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
/define-lazy-prop@2.0.0:
|
||||
resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/delaunator@5.0.0:
|
||||
resolution: {integrity: sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==}
|
||||
dependencies:
|
||||
@@ -5806,6 +5803,12 @@ packages:
|
||||
dependencies:
|
||||
has: 1.0.3
|
||||
|
||||
/is-docker@2.2.1:
|
||||
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/is-extglob@2.1.1:
|
||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -5852,6 +5855,13 @@ packages:
|
||||
resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==}
|
||||
dev: false
|
||||
|
||||
/is-wsl@2.2.0:
|
||||
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
is-docker: 2.2.1
|
||||
dev: true
|
||||
|
||||
/isarray@0.0.1:
|
||||
resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==}
|
||||
dev: false
|
||||
@@ -7065,6 +7075,15 @@ packages:
|
||||
mimic-fn: 2.1.0
|
||||
dev: true
|
||||
|
||||
/open@8.4.2:
|
||||
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
define-lazy-prop: 2.0.0
|
||||
is-docker: 2.2.1
|
||||
is-wsl: 2.2.0
|
||||
dev: true
|
||||
|
||||
/optionator@0.9.1:
|
||||
resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -7656,6 +7675,22 @@ packages:
|
||||
resolution: {integrity: sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==}
|
||||
dev: false
|
||||
|
||||
/rollup-plugin-visualizer@5.9.0:
|
||||
resolution: {integrity: sha512-bbDOv47+Bw4C/cgs0czZqfm8L82xOZssk4ayZjG40y9zbXclNk7YikrZTDao6p7+HDiGxrN0b65SgZiVm9k1Cg==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
rollup: 2.x || 3.x
|
||||
peerDependenciesMeta:
|
||||
rollup:
|
||||
optional: true
|
||||
dependencies:
|
||||
open: 8.4.2
|
||||
picomatch: 2.3.1
|
||||
source-map: 0.7.4
|
||||
yargs: 17.7.1
|
||||
dev: true
|
||||
|
||||
/rollup@2.79.1:
|
||||
resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"comfyUIHostname": "localhost",
|
||||
"comfyUIPort": 8188,
|
||||
"alwaysStripUserState": false,
|
||||
"promptForWorkflowName": false,
|
||||
"confirmWhenUnloadingUnsavedChanges": true
|
||||
"comfyUIHostname": "localhost",
|
||||
"comfyUIPort": 8188,
|
||||
"alwaysStripUserState": false,
|
||||
"promptForWorkflowName": false,
|
||||
"confirmWhenUnloadingUnsavedChanges": true,
|
||||
"builtInTemplates": ["ControlNet", "LoRA x5", "Model Loader", "Positive_Negative", "Seed Randomizer"],
|
||||
"cacheBuiltInResources": true
|
||||
}
|
||||
|
||||
1
public/templates/ControlNet.svg
Normal file
1
public/templates/ControlNet.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 156 KiB |
1
public/templates/LoRA x5.svg
Normal file
1
public/templates/LoRA x5.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 319 KiB |
1
public/templates/Model Loader.svg
Normal file
1
public/templates/Model Loader.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 75 KiB |
1
public/templates/Positive_Negative.svg
Normal file
1
public/templates/Positive_Negative.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 38 KiB |
1
public/templates/Seed Randomizer.svg
Normal file
1
public/templates/Seed Randomizer.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 50 KiB |
File diff suppressed because it is too large
Load Diff
@@ -707,7 +707,7 @@
|
||||
"buttonVariant": "primary",
|
||||
"buttonSize": "large",
|
||||
"tags": [],
|
||||
"destroyChildOnCLose": false
|
||||
"destroyChildOnClose": false
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
@@ -733,7 +733,7 @@
|
||||
"buttonVariant": "primary",
|
||||
"buttonSize": "large",
|
||||
"tags": [],
|
||||
"destroyChildOnCLose": false
|
||||
"destroyChildOnClose": false
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
@@ -760,7 +760,7 @@
|
||||
"buttonVariant": "primary",
|
||||
"buttonSize": "large",
|
||||
"tags": [],
|
||||
"destroyChildOnCLose": false
|
||||
"destroyChildOnClose": false
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
@@ -788,7 +788,7 @@
|
||||
"buttonVariant": "primary",
|
||||
"buttonSize": "large",
|
||||
"tags": [],
|
||||
"destroyChildOnCLose": false
|
||||
"destroyChildOnClose": false
|
||||
}
|
||||
},
|
||||
"children": [],
|
||||
@@ -813,7 +813,7 @@
|
||||
"buttonVariant": "primary",
|
||||
"buttonSize": "large",
|
||||
"tags": [],
|
||||
"destroyChildOnCLose": false
|
||||
"destroyChildOnClose": false
|
||||
}
|
||||
},
|
||||
"children": [],
|
||||
@@ -837,7 +837,7 @@
|
||||
"buttonVariant": "primary",
|
||||
"buttonSize": "large",
|
||||
"tags": [],
|
||||
"destroyChildOnCLose": false
|
||||
"destroyChildOnClose": false
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
@@ -864,7 +864,7 @@
|
||||
"buttonVariant": "primary",
|
||||
"buttonSize": "large",
|
||||
"tags": [],
|
||||
"destroyChildOnCLose": false
|
||||
"destroyChildOnClose": false
|
||||
}
|
||||
},
|
||||
"children": [],
|
||||
@@ -888,7 +888,7 @@
|
||||
"buttonVariant": "primary",
|
||||
"buttonSize": "large",
|
||||
"tags": [],
|
||||
"destroyChildOnCLose": false
|
||||
"destroyChildOnClose": false
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
@@ -915,7 +915,7 @@
|
||||
"buttonVariant": "primary",
|
||||
"buttonSize": "large",
|
||||
"tags": [],
|
||||
"destroyChildOnCLose": false
|
||||
"destroyChildOnClose": false
|
||||
}
|
||||
},
|
||||
"children": [],
|
||||
@@ -930,4 +930,4 @@
|
||||
],
|
||||
"scale": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ export type SerializedComfyBoxTemplate = {
|
||||
version: 1,
|
||||
id: UUID,
|
||||
commitHash: string,
|
||||
isBuiltIn?: boolean,
|
||||
|
||||
/*
|
||||
* Serialized metadata
|
||||
@@ -356,35 +357,40 @@ export function serializeTemplate(canvas: ComfyGraphCanvas, template: ComfyBoxTe
|
||||
return serTemplate;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Extract embedded workflow from desc tags
|
||||
*/
|
||||
export function extractTemplateJSONFromSVG(svg: string): string | null {
|
||||
const descEnd = svg.lastIndexOf("</desc>");
|
||||
if (descEnd !== -1) {
|
||||
const descStart = svg.lastIndexOf("<desc>", descEnd);
|
||||
if (descStart !== -1) {
|
||||
const json = svg.substring(descStart + 6, descEnd);
|
||||
return unescapeXml(json);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSerializedComfyBoxTemplate(template)) {
|
||||
reject("Invalid template format!")
|
||||
}
|
||||
else {
|
||||
template.svg = svg;
|
||||
resolve(template)
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Credit goes to pythongosssss for this format
|
||||
*/
|
||||
export function deserializeTemplateFromSVG(svg: string): SerializedComfyBoxTemplate | null {
|
||||
let template = null;
|
||||
let templateJSON = extractTemplateJSONFromSVG(svg);
|
||||
if (templateJSON)
|
||||
template = JSON.parse(templateJSON);
|
||||
|
||||
if (!isSerializedComfyBoxTemplate(template)) {
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
template.svg = svg;
|
||||
return template;
|
||||
}
|
||||
}
|
||||
|
||||
export function createTemplate(nodes: LGraphNode[]): ComfyBoxTemplateResult {
|
||||
if (nodes.length === 0) {
|
||||
|
||||
@@ -489,7 +489,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
||||
const serialized = serializeTemplate(this, template);
|
||||
|
||||
try {
|
||||
if (templateState.add(serialized)) {
|
||||
if (templateState.addTemplate(serialized)) {
|
||||
notify("Template saved!", { type: "success" })
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -85,10 +85,8 @@ export default class DanbooruTags {
|
||||
}
|
||||
|
||||
async load(force: boolean = false) {
|
||||
console.log("Parsing danbooru tags CSV...")
|
||||
|
||||
if (this.tags.length > 0 && !force) {
|
||||
console.info("Danbooru tags already parsed")
|
||||
console.warn("Danbooru tags already parsed")
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,14 +29,14 @@ import queueState from "$lib/stores/queueState";
|
||||
import selectionState from "$lib/stores/selectionState";
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import workflowState, { ComfyBoxWorkflow, type WorkflowAttributes, type WorkflowInstID } from "$lib/stores/workflowState";
|
||||
import type { SerializedPromptOutput } from "$lib/utils";
|
||||
import { readFileToText, type SerializedPromptOutput } from "$lib/utils";
|
||||
import { basename, capitalize, download, graphToGraphVis, jsonToJsObject, promptToGraphVis, range } from "$lib/utils";
|
||||
import { tick } from "svelte";
|
||||
import { type SvelteComponentDev } from "svelte/internal";
|
||||
import { get, writable, type Writable } from "svelte/store";
|
||||
import ComfyPromptSerializer, { isActiveBackendNode, UpstreamNodeLocator } from "./ComfyPromptSerializer";
|
||||
import ComfyPromptSerializer, { isActiveBackendNode, nodeHasTag, UpstreamNodeLocator } from "./ComfyPromptSerializer";
|
||||
import DanbooruTags from "$lib/DanbooruTags";
|
||||
import { deserializeTemplateFromSVG } from "$lib/ComfyBoxTemplate";
|
||||
import { deserializeTemplateFromSVG, type SerializedComfyBoxTemplate } from "$lib/ComfyBoxTemplate";
|
||||
import templateState from "$lib/stores/templateState";
|
||||
|
||||
export const COMFYBOX_SERIAL_VERSION = 1;
|
||||
@@ -240,7 +240,9 @@ export default class ComfyApp {
|
||||
this.addKeyboardHandler();
|
||||
|
||||
await this.updateHistoryAndQueue();
|
||||
templateState.load();
|
||||
|
||||
const builtInTemplates = await this.loadBuiltInTemplates();
|
||||
templateState.load(builtInTemplates);
|
||||
|
||||
await this.initFrontendFeatures();
|
||||
|
||||
@@ -262,15 +264,60 @@ export default class ComfyApp {
|
||||
*/
|
||||
async loadConfig() {
|
||||
try {
|
||||
const config = await fetch(`/config.json`);
|
||||
const state = await config.json() as ConfigState;
|
||||
configState.set(state);
|
||||
const config = await fetch(`/config.json`, { cache: "no-store" });
|
||||
const newConfig = await config.json() as ConfigState;
|
||||
configState.set({ ...get(configState), ...newConfig });
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`Failed to load config`, error)
|
||||
}
|
||||
}
|
||||
|
||||
async loadBuiltInTemplates(): Promise<SerializedComfyBoxTemplate[]> {
|
||||
const builtInTemplates = get(configState).builtInTemplates
|
||||
const options: RequestInit = get(configState).cacheBuiltInResources ? {} : { cache: "no-store" }
|
||||
const promises = builtInTemplates.map(basename => {
|
||||
return fetch(`/templates/${basename}.svg`, options)
|
||||
.then(res => res.text())
|
||||
.catch(error => error)
|
||||
})
|
||||
|
||||
const [templates, error] = await Promise.all(promises).then((results) => {
|
||||
const templates: SerializedComfyBoxTemplate[] = []
|
||||
const errors: string[] = []
|
||||
|
||||
for (const r of results) {
|
||||
if (r instanceof Error) {
|
||||
errors.push(r.toString())
|
||||
}
|
||||
else {
|
||||
// bare filename of image
|
||||
const svg = r as string;
|
||||
const templateAndSvg = deserializeTemplateFromSVG(svg)
|
||||
if (templateAndSvg == null) {
|
||||
errors.push("Invalid SVG template format")
|
||||
}
|
||||
else {
|
||||
templates.push(templateAndSvg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let error = null;
|
||||
if (errors && errors.length > 0)
|
||||
error = "Error(s) loading builtin templates:\n" + errors.join("\n");
|
||||
|
||||
console.log(`Loaded {templates.length} builtin templates.`);
|
||||
|
||||
return [templates, error]
|
||||
})
|
||||
|
||||
if (error)
|
||||
notify(error, { type: "error" })
|
||||
|
||||
return templates;
|
||||
}
|
||||
|
||||
resizeCanvas() {
|
||||
if (!this.canvasEl)
|
||||
return;
|
||||
@@ -606,10 +653,12 @@ export default class ComfyApp {
|
||||
// Queue prompt using ctrl or command + enter
|
||||
if ((e.ctrlKey || e.metaKey) && (e.key === "Enter" || e.code === "Enter" || e.keyCode === 10)) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
this.runDefaultQueueAction();
|
||||
}
|
||||
else if ((e.ctrlKey) && (e.key === "s" || e.code === "KeyS")) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
this.saveStateToLocalStorage();
|
||||
}
|
||||
});
|
||||
@@ -780,7 +829,8 @@ export default class ComfyApp {
|
||||
async initDefaultWorkflow(name: string = "defaultWorkflow", options?: OpenWorkflowOptions) {
|
||||
let state = null;
|
||||
try {
|
||||
const graphResponse = await fetch(`/workflows/${name}.json`);
|
||||
const options: RequestInit = get(configState).cacheBuiltInResources ? {} : { cache: "no-store" }
|
||||
const graphResponse = await fetch(`/workflows/${name}.json`, options);
|
||||
state = await graphResponse.json() as SerializedAppState;
|
||||
}
|
||||
catch (error) {
|
||||
@@ -918,10 +968,7 @@ export default class ComfyApp {
|
||||
|
||||
const thumbnails = []
|
||||
for (const node of workflow.graph.iterateNodesInOrderRecursive()) {
|
||||
if (node.mode !== NodeMode.ALWAYS
|
||||
|| (tag != null
|
||||
&& Array.isArray(node.properties.tags)
|
||||
&& node.properties.tags.indexOf(tag) === -1))
|
||||
if (node.mode !== NodeMode.ALWAYS || (tag != null && !nodeHasTag(node, tag)))
|
||||
continue;
|
||||
|
||||
if ("getPromptThumbnails" in node) {
|
||||
@@ -1058,11 +1105,16 @@ export default class ComfyApp {
|
||||
};
|
||||
reader.readAsText(file);
|
||||
} else if (file.type === "image/svg+xml" || file.name.endsWith(".svg")) {
|
||||
const templateAndSvg = await deserializeTemplateFromSVG(file);
|
||||
const svg = await readFileToText(file);
|
||||
const templateAndSvg = deserializeTemplateFromSVG(svg);
|
||||
if (templateAndSvg == null) {
|
||||
notify("Invalid SVG template format!", { type: "error" })
|
||||
return;
|
||||
}
|
||||
|
||||
const importTemplate = () => {
|
||||
try {
|
||||
if (templateState.add(templateAndSvg)) {
|
||||
if (templateState.addTemplate(templateAndSvg)) {
|
||||
notify("Template imported successfully!", { type: "success" })
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -4,21 +4,42 @@ import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
|
||||
import { GraphInput, GraphOutput, LGraph, LGraphNode, LLink, NodeMode, Subgraph, type SlotIndex } from "@litegraph-ts/core";
|
||||
import type { SerializedPrompt, SerializedPromptInput, SerializedPromptInputsForNode, SerializedPromptInputsAll, SerializedPromptInputs } from "./ComfyApp";
|
||||
import type IComfyInputSlot from "$lib/IComfyInputSlot";
|
||||
import { Reroute } from "@litegraph-ts/nodes-basic";
|
||||
import { ComfyReroute } from "$lib/nodes";
|
||||
|
||||
function hasTag(node: LGraphNode, tag: string): boolean {
|
||||
return "tags" in node.properties && node.properties.tags.indexOf(tag) !== -1
|
||||
function isReroute(node: LGraphNode): boolean {
|
||||
return node.is(Reroute) || node.is(ComfyReroute)
|
||||
}
|
||||
|
||||
function isGraphInputOutput(node: LGraphNode): boolean {
|
||||
return node.is(GraphInput) || node.is(GraphOutput)
|
||||
}
|
||||
|
||||
export function nodeHasTag(node: LGraphNode, tag: string): boolean {
|
||||
// Ignore tags on reroutes since they're just movable wires and it defeats
|
||||
// the convenience gains to have to set tags for all them
|
||||
if (isReroute(node))
|
||||
return true;
|
||||
|
||||
while (node != null) {
|
||||
if ("tags" in node.properties) {
|
||||
if (node.properties.tags.indexOf(tag) !== -1)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Count parent subgraphs having the tag also.
|
||||
node = node.graph?._subgraph_node;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isActiveNode(node: LGraphNode, tag: string | null = null): boolean {
|
||||
if (!node)
|
||||
return false;
|
||||
|
||||
// Check tags but not on graph inputs/outputs
|
||||
if (!isGraphInputOutput(node) && (tag && !hasTag(node, tag))) {
|
||||
if (!isGraphInputOutput(node) && (tag && !nodeHasTag(node, tag))) {
|
||||
console.debug("Skipping tagged node", tag, node.properties.tags, node)
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { embedTemplateInSvg, type SerializedComfyBoxTemplate } from "$lib/ComfyBoxTemplate";
|
||||
import templateState from "$lib/stores/templateState";
|
||||
import templateState, { type TemplateState, type WritableTemplateStateStore } from "$lib/stores/templateState";
|
||||
import { BoxSeam, Hdd } from "svelte-bootstrap-icons";
|
||||
import uiState from "$lib/stores/uiState";
|
||||
import { download, truncateString } from "$lib/utils";
|
||||
import { download, getLocalStorageUsedMB, MAX_LOCAL_STORAGE_MB, truncateString } from "$lib/utils";
|
||||
import type ComfyApp from "./ComfyApp";
|
||||
import { flip } from 'svelte/animate';
|
||||
import {fade} from 'svelte/transition';
|
||||
@@ -26,12 +27,26 @@
|
||||
source?: string
|
||||
}
|
||||
|
||||
let storageUsedPercent = 0;
|
||||
let storageUsedMB = 0;
|
||||
|
||||
$: {
|
||||
if ($templateState) {
|
||||
storageUsedMB = getLocalStorageUsedMB()
|
||||
storageUsedPercent = (storageUsedMB / MAX_LOCAL_STORAGE_MB) * 100;
|
||||
}
|
||||
else {
|
||||
storageUsedPercent = 0;
|
||||
storageUsedMB = 0;
|
||||
}
|
||||
}
|
||||
|
||||
let _sorted: TemplateLayout[] = []
|
||||
|
||||
$: rebuildTemplates($templateState.templates);
|
||||
$: $templateState && rebuildTemplates(templateState.getAllTemplates());
|
||||
|
||||
function rebuildTemplates(templates: SerializedComfyBoxTemplate[]) {
|
||||
_sorted = Array.from(templates).map(t => {
|
||||
function rebuildTemplates(allTemplates: SerializedComfyBoxTemplate[]): void {
|
||||
_sorted = Array.from(allTemplates).map(t => {
|
||||
return {
|
||||
type: "template", id: uuidv4(), template: t, attrs: {...defaultWidgetAttributes}, attrsChanged: writable(0)
|
||||
}
|
||||
@@ -88,7 +103,7 @@
|
||||
const saveTemplate = (modal: ModalData) => {
|
||||
updateTemplate(modal);
|
||||
try {
|
||||
templateState.update(layout.template);
|
||||
templateState.updateTemplate(layout.template);
|
||||
notify("Saved template!", { type: "success" })
|
||||
}
|
||||
catch (error) {
|
||||
@@ -101,7 +116,7 @@
|
||||
return false;
|
||||
|
||||
try {
|
||||
if (templateState.remove(layout.template.id)) {
|
||||
if (templateState.removeTemplate(layout.template.id)) {
|
||||
notify("Template deleted!", { type: "success" })
|
||||
}
|
||||
else {
|
||||
@@ -131,7 +146,8 @@
|
||||
{
|
||||
name: "Save",
|
||||
variant: "primary",
|
||||
onClick: saveTemplate
|
||||
onClick: saveTemplate,
|
||||
hidden: layout.template.isBuiltIn
|
||||
},
|
||||
{
|
||||
name: "Download",
|
||||
@@ -142,13 +158,13 @@
|
||||
{
|
||||
name: "Delete",
|
||||
variant: "secondary",
|
||||
onClick: deleteTemplate
|
||||
onClick: deleteTemplate,
|
||||
hidden: layout.template.isBuiltIn
|
||||
},
|
||||
{
|
||||
name: "Close",
|
||||
variant: "secondary",
|
||||
onClick: () => {
|
||||
}
|
||||
onClick: () => {}
|
||||
},
|
||||
]
|
||||
})
|
||||
@@ -159,35 +175,52 @@
|
||||
<div class="template-entries">
|
||||
{#if _sorted.length > 0}
|
||||
{@const draggable = $uiState.uiUnlocked}
|
||||
<div class="template-category-group">
|
||||
<div class="template-category-header">
|
||||
General
|
||||
<div class="template-list">
|
||||
<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:built-in={item.template.isBuiltIn} 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>
|
||||
<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 class="storage-used">
|
||||
<div class="storage-used-icon">
|
||||
<Hdd width="100%" height="2rem" />
|
||||
</div>
|
||||
<div class="storage-used-bar-wrapper">
|
||||
<div class="storage-used-bar" style="width: {storageUsedPercent}%;">
|
||||
<span class="storage-used-label">{storageUsedMB.toFixed(2)} / {MAX_LOCAL_STORAGE_MB} MB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="no-templates">
|
||||
<span>(No templates)</span>
|
||||
<div class="no-templates-container">
|
||||
<div class="no-templates-icon">
|
||||
<BoxSeam width="100%" height="8rem" />
|
||||
</div>
|
||||
<div class="no-templates-message">
|
||||
(No templates)
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -200,9 +233,13 @@
|
||||
|
||||
.template-entries {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.template-list {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.template-category-header {
|
||||
@@ -214,6 +251,7 @@
|
||||
}
|
||||
|
||||
.template-entry {
|
||||
min-width: 12rem;
|
||||
padding: 1.0rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -227,6 +265,16 @@
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
&.built-in {
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
var(--neutral-800),
|
||||
var(--neutral-800) 15px,
|
||||
#1f293780 15px,
|
||||
#1f293780 30px,
|
||||
);
|
||||
}
|
||||
|
||||
font-size: 13pt;
|
||||
.template-desc {
|
||||
opacity: 65%;
|
||||
@@ -253,15 +301,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.no-templates {
|
||||
.no-templates-container {
|
||||
display: flex;
|
||||
color: var(--comfy-accent-soft);
|
||||
flex-direction: row;
|
||||
flex-direction: column;
|
||||
margin: auto;
|
||||
height: 100%;
|
||||
color: var(--comfy-accent-soft);
|
||||
|
||||
span {
|
||||
.no-templates-icon {
|
||||
margin: auto;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.no-templates-message {
|
||||
margin: auto;
|
||||
font-size: 32px;
|
||||
font-weight: bolder;
|
||||
@@ -277,4 +329,45 @@
|
||||
opacity: 0.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.storage-used {
|
||||
height: 2.5rem;
|
||||
margin: 5px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
padding: 0.1rem 0.5rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--spacing-lg);
|
||||
|
||||
.storage-used-icon {
|
||||
color: var(--neutral-500);
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.storage-used-bar-wrapper {
|
||||
width: 100%;
|
||||
background: var(--panel-background-fill);
|
||||
border: 1px solid var(--neutral-700);
|
||||
position: relative;
|
||||
|
||||
.storage-used-bar {
|
||||
height: 100%;
|
||||
background: var(--neutral-700);
|
||||
|
||||
.storage-used-label {
|
||||
width: 100%;
|
||||
color: var(--neutral-200);
|
||||
font-size: 12pt;
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
}
|
||||
|
||||
function onButtonClicked(modal: ModalData, button: ModalButton, closeDialog: Function) {
|
||||
if (button.disabled || button.hidden)
|
||||
return;
|
||||
|
||||
if (button.onClick(modal) === false)
|
||||
return
|
||||
|
||||
@@ -39,9 +42,11 @@
|
||||
<div slot="buttons" class="buttons" let:closeDialog>
|
||||
{#if modal != null && modal.buttons?.length > 0}
|
||||
{#each modal.buttons as button}
|
||||
<Button variant={button.variant} on:click={() => onButtonClicked(modal, button, closeDialog)}>
|
||||
{button.name}
|
||||
</Button>
|
||||
{#if !button.hidden}
|
||||
<Button variant={button.variant} disabled={button.disabled} on:click={() => onButtonClicked(modal, button, closeDialog)}>
|
||||
{button.name}
|
||||
</Button>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
{#if modal.showCloseButton}
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
|
||||
for (const r of results) {
|
||||
if (r instanceof Error) {
|
||||
errors.push(r.cause)
|
||||
errors.push(r.toString())
|
||||
}
|
||||
else {
|
||||
// bare filename of image
|
||||
|
||||
141
src/lib/components/JsonView.svelte
Normal file
141
src/lib/components/JsonView.svelte
Normal file
@@ -0,0 +1,141 @@
|
||||
<script lang="ts">
|
||||
/*
|
||||
* Modified from @zerodevx/svelte-json-view
|
||||
*/
|
||||
|
||||
/** @type {*} - object or array to display */
|
||||
export let json: any
|
||||
/** @type {number} - initial expansion depth */
|
||||
export let depth: number = Infinity
|
||||
export let collapseByDefault: boolean | ((v: any) => boolean) | null = false;
|
||||
export let _cur: number = 0
|
||||
export let _last: boolean = true
|
||||
|
||||
/** @type {*[]} */
|
||||
let items: any[]
|
||||
let isArray: boolean = false
|
||||
let brackets: [string, string] = ['', '']
|
||||
export let collapsed: boolean | null = null
|
||||
|
||||
/**
|
||||
* @param {*} i
|
||||
* @returns {string}
|
||||
*/
|
||||
function getType(i: any): string {
|
||||
if (i === null) return 'null'
|
||||
return typeof i
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*} i
|
||||
* @returns {string}
|
||||
*/
|
||||
function format(i: any): string {
|
||||
const t = getType(i)
|
||||
if (t === 'string') return `"${i}"`
|
||||
if (t === 'function') return 'f () {...}'
|
||||
if (t === 'symbol') return i.toString()
|
||||
return i
|
||||
}
|
||||
|
||||
function clicked() {
|
||||
collapsed = !collapsed
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Event} e
|
||||
*/
|
||||
function pressed(e) {
|
||||
if (e instanceof KeyboardEvent && ['Enter', ' '].includes(e.key)) clicked()
|
||||
}
|
||||
|
||||
$: {
|
||||
items = getType(json) === 'object' ? Object.keys(json) : []
|
||||
isArray = Array.isArray(json)
|
||||
brackets = isArray ? ['[', ']'] : ['{', '}']
|
||||
}
|
||||
|
||||
$: {
|
||||
if (collapsed === null)
|
||||
collapsed = depth < _cur;
|
||||
}
|
||||
|
||||
function calcCollapsed(json: any): boolean | null {
|
||||
if (typeof collapseByDefault === "function")
|
||||
return collapseByDefault(json)
|
||||
else if (typeof collapseByDefault === "boolean")
|
||||
return collapseByDefault
|
||||
return null;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !items.length}
|
||||
<span class="_jsonBkt empty">{brackets[0]}{brackets[1]}</span>{#if !_last}<span class="_jsonSep"
|
||||
>,</span
|
||||
>{/if}
|
||||
{:else if collapsed}
|
||||
<span class="_jsonBkt _jsonCollapsed" role="button" tabindex="0" on:click={clicked} on:keydown={pressed}
|
||||
>{brackets[0]}...{brackets[1]}</span
|
||||
>{#if !_last && collapsed}<span class="_jsonSep">,</span>{/if}
|
||||
{:else}
|
||||
<span class="_jsonBkt" role="button" tabindex="0" on:click={clicked} on:keydown={pressed}
|
||||
>{brackets[0]}</span
|
||||
>
|
||||
<ul class="_jsonList">
|
||||
{#each items as i, idx}
|
||||
<li>
|
||||
{#if !isArray}
|
||||
<span class="_jsonKey">"{i}"</span><span class="_jsonSep">:</span>
|
||||
{/if}
|
||||
{#if getType(json[i]) === 'object'}
|
||||
<svelte:self json={json[i]} {collapseByDefault} collapsed={calcCollapsed(json[i])} {depth} _cur={_cur + 1} _last={idx === items.length - 1} />
|
||||
{:else}
|
||||
<span class="_jsonVal {getType(json[i])}">{format(json[i])}</span
|
||||
>{#if idx < items.length - 1}<span class="_jsonSep">,</span>{/if}
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<span class="_jsonBkt" role="button" tabindex="0" on:click={clicked} on:keydown={pressed}
|
||||
>{brackets[1]}</span
|
||||
>{#if !_last}<span class="_jsonSep">,</span>{/if}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
._jsonList {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-left: var(--jsonPaddingLeft, 1rem);
|
||||
border-left: var(--jsonBorderLeft, 1px dotted);
|
||||
}
|
||||
._jsonBkt {
|
||||
color: var(--jsonBracketColor, currentcolor);
|
||||
}
|
||||
._jsonBkt:not(.empty):hover {
|
||||
cursor: pointer;
|
||||
background: var(--jsonBracketHoverBackground, #e5e7eb);
|
||||
}
|
||||
._jsonSep {
|
||||
color: var(--jsonSeparatorColor, currentcolor);
|
||||
}
|
||||
._jsonKey {
|
||||
color: var(--jsonKeyColor, currentcolor);
|
||||
}
|
||||
._jsonVal {
|
||||
color: var(--jsonValColor, #9ca3af);
|
||||
}
|
||||
._jsonVal.string {
|
||||
color: var(--jsonValStringColor, #059669);
|
||||
}
|
||||
._jsonVal.number {
|
||||
color: var(--jsonValNumberColor, #d97706);
|
||||
}
|
||||
._jsonVal.boolean {
|
||||
color: var(--jsonValBooleanColor, #2563eb);
|
||||
}
|
||||
._jsonCollapsed {
|
||||
color: var(--jsonCollapsedColor, currentcolor);
|
||||
background: var(--jsonCollapsedBackground, currentcolor);
|
||||
}
|
||||
</style>
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { A1111ParsedInfotext } from "$lib/parseA1111";
|
||||
import { Block, BlockTitle } from "@gradio/atoms";
|
||||
import { TextBox } from "@gradio/form";
|
||||
import { JsonView } from '@zerodevx/svelte-json-view'
|
||||
import JsonView from '$lib/components/JsonView.svelte'
|
||||
import type { A1111PromptAndInfo } from "$lib/components/ComfyApp";
|
||||
import { StaticImage } from "$lib/components/gradio/image";
|
||||
|
||||
@@ -65,17 +65,6 @@
|
||||
height: 70vh;
|
||||
color: none;
|
||||
|
||||
--jsonPaddingLeft: 1rem;
|
||||
--jsonBorderLeft: 1px dotted var(--neutral-600);
|
||||
--jsonBracketColor: currentcolor;
|
||||
--jsonBracketHoverBackground: var(--neutral-100);
|
||||
--jsonSeparatorColor: currentcolor;
|
||||
--jsonKeyColor: var(--body-text-color);
|
||||
--jsonValColor: var(--body-text-color-subdued);
|
||||
--jsonValStringColor: var(--color-green-500);
|
||||
--jsonValNumberColor: var(--color-blue-500);
|
||||
--jsonValBooleanColor: var(--color-red-500);
|
||||
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
overflow-y: none;
|
||||
@@ -87,7 +76,7 @@
|
||||
overflow: auto;
|
||||
|
||||
.json {
|
||||
font-family: monospace;
|
||||
@include json-view;
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
import type { ComfyBoxTemplate, SerializedComfyBoxTemplate } from "$lib/ComfyBoxTemplate";
|
||||
import type { SerializedDragEntry, SerializedLayoutState } from "$lib/stores/layoutStates";
|
||||
import { Block, BlockTitle } from "@gradio/atoms";
|
||||
import { Tabs, TabItem } from "@gradio/tabs";
|
||||
import { JSON as JSONIcon } from "@gradio/icons";
|
||||
import JsonView from '$lib/components/JsonView.svelte'
|
||||
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";
|
||||
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";
|
||||
import { negmod } from "$lib/utils";
|
||||
const DOMPurify = createDOMPurify(window);
|
||||
|
||||
export let templateAndSvg: SerializedComfyBoxTemplate;
|
||||
@@ -18,6 +22,49 @@
|
||||
let layout: SerializedLayoutState | null
|
||||
let root: SerializedDragEntry | null
|
||||
let state: Writable<any> = writable({})
|
||||
let rawTemplate: SerializedComfyBoxTemplate | null
|
||||
let showJSON = false;
|
||||
let showAllJSON: number = 0;
|
||||
let createdAt = "";
|
||||
|
||||
let isEditable = true;
|
||||
|
||||
$: isEditable = editable && templateAndSvg && !templateAndSvg.isBuiltIn;
|
||||
|
||||
$: {
|
||||
rawTemplate = { ...templateAndSvg };
|
||||
rawTemplate.svg = undefined;
|
||||
}
|
||||
|
||||
function collapseByDefault(json: any): boolean {
|
||||
switch (showAllJSON) {
|
||||
case 0:
|
||||
return typeof json["id"] === "string";
|
||||
case 1:
|
||||
return typeof json["nodes"] === "object"
|
||||
case 2:
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function expandJSON() {
|
||||
showAllJSON = negmod(showAllJSON + 1, 3)
|
||||
}
|
||||
|
||||
$: {
|
||||
let options: Intl.DateTimeFormatOptions = {
|
||||
weekday: 'short',
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric'
|
||||
};
|
||||
const date = new Date(templateAndSvg.metadata.createdAt);
|
||||
createdAt = date.toLocaleString('en-US', options);
|
||||
}
|
||||
|
||||
$: {
|
||||
state = _modal.state;
|
||||
@@ -31,9 +78,9 @@
|
||||
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\")'")
|
||||
: "";
|
||||
? 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;
|
||||
@@ -57,9 +104,13 @@
|
||||
<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} />
|
||||
<Textbox label="Name" disabled={!isEditable} bind:value={$state.name} lines={1} max_lines={1} />
|
||||
<Textbox label="Author" disabled={!isEditable} bind:value={$state.author} lines={1} max_lines={1} />
|
||||
<Textbox label="Description" disabled={!isEditable} bind:value={$state.description} lines={5} max_lines={5} />
|
||||
<Row>
|
||||
<Textbox label="Created At" disabled={true} bind:value={createdAt} lines={1} max_lines={1} />
|
||||
<Textbox label="Size" disabled={true} value="{(templateAndSvg.svg.length/1024).toFixed(2)} KB" lines={1} max_lines={1} />
|
||||
</Row>
|
||||
</div>
|
||||
</Block>
|
||||
</div>
|
||||
@@ -76,15 +127,31 @@
|
||||
{/if}
|
||||
</Row>
|
||||
<div class="template-graph-preview">
|
||||
<Block>
|
||||
<Accordion label="Graph">
|
||||
<Tabs selected="graph">
|
||||
<TabItem name="Graph" id="graph">
|
||||
<Block>
|
||||
<div class="template-graph-wrapper">
|
||||
{@html saneSvg}
|
||||
</div>
|
||||
<Accordion label="Graph">
|
||||
<Block>
|
||||
<div class="template-graph-wrapper">
|
||||
{@html saneSvg}
|
||||
</div>
|
||||
</Block>
|
||||
</Accordion>
|
||||
</Block>
|
||||
</Accordion>
|
||||
</Block>
|
||||
</TabItem>
|
||||
<TabItem name="Raw JSON" id="json" on:select={() => (showJSON = true)}>
|
||||
{#key showAllJSON}
|
||||
{#if showJSON}
|
||||
<button class="json-button" on:click={expandJSON}>
|
||||
<JSONIcon />
|
||||
</button>
|
||||
<div class="json">
|
||||
<JsonView json={rawTemplate} {collapseByDefault} />
|
||||
</div>
|
||||
{/if}
|
||||
{/key}
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -120,10 +187,34 @@
|
||||
:global(> .block) {
|
||||
background: var(--panel-background-fill);
|
||||
}
|
||||
|
||||
.json {
|
||||
@include json-view;
|
||||
}
|
||||
}
|
||||
|
||||
.template-graph-wrapper {
|
||||
overflow: auto;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
|
||||
.json-button {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: var(--block-label-margin);
|
||||
right: var(--block-label-margin);
|
||||
align-items: center;
|
||||
box-shadow: var(--shadow-drop);
|
||||
border: 1px solid var(--border-color-primary);
|
||||
border-radius: var(--block-label-right-radius);
|
||||
background: var(--block-label-background-fill);
|
||||
padding: 5px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
overflow: hidden;
|
||||
color: var(--block-label-text-color);
|
||||
font: var(--font);
|
||||
font-size: var(--button-small-text-size);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ComfyWidgetNode } from "$lib/nodes/widgets";
|
||||
import { BuiltInSlotType, LiteGraph, NodeMode, type ITextWidget, type IToggleWidget, type SlotLayout } from "@litegraph-ts/core";
|
||||
import { get } from "svelte/store";
|
||||
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "../ComfyGraphNode";
|
||||
import { nodeHasTag } from "$lib/components/ComfyPromptSerializer";
|
||||
|
||||
export interface ComfySetNodeModeActionProperties extends ComfyGraphNodeProperties {
|
||||
targetTags: string,
|
||||
@@ -52,7 +53,7 @@ export default class ComfySetNodeModeAction extends ComfyGraphNode {
|
||||
for (const node of this.graph._nodes) {
|
||||
if ("tags" in node.properties) {
|
||||
const comfyNode = node as ComfyGraphNode;
|
||||
const hasTag = tags.some(t => comfyNode.properties.tags.indexOf(t) != -1);
|
||||
const hasTag = tags.some(t => nodeHasTag(comfyNode, t));
|
||||
if (hasTag) {
|
||||
let newMode: NodeMode;
|
||||
if (enabled) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { type DragItemID } from "$lib/stores/layoutStates";
|
||||
import { BuiltInSlotType, LiteGraph, NodeMode, type ITextWidget, type IToggleWidget, type PropertyLayout, type SlotLayout } from "@litegraph-ts/core";
|
||||
import { get } from "svelte/store";
|
||||
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "../ComfyGraphNode";
|
||||
import { nodeHasTag } from "$lib/components/ComfyPromptSerializer";
|
||||
|
||||
export type TagAction = {
|
||||
tag: string,
|
||||
@@ -68,7 +69,7 @@ export default class ComfySetNodeModeAdvancedAction extends ComfyGraphNode {
|
||||
for (const node of this.graph.iterateNodesInOrderRecursive()) {
|
||||
if ("tags" in node.properties) {
|
||||
const comfyNode = node as ComfyGraphNode;
|
||||
const hasTag = comfyNode.properties.tags.indexOf(action.tag) != -1;
|
||||
const hasTag = nodeHasTag(comfyNode, action.tag);
|
||||
|
||||
if (hasTag) {
|
||||
let newMode: NodeMode;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { parseWhateverIntoImageMetadata, type ComfyBoxImageMetadata, type ComfyUploadImageType } from "$lib/utils";
|
||||
import { BuiltInSlotType, LiteGraph, type IComboWidget, type ITextWidget, type PropertyLayout, type SlotLayout, type INumberWidget, clamp } from "@litegraph-ts/core";
|
||||
import { BuiltInSlotType, LiteGraph, type IComboWidget, type ITextWidget, type PropertyLayout, type SlotLayout, type INumberWidget, clamp, type SerializedLGraphNode } from "@litegraph-ts/core";
|
||||
import { get, writable, type Writable } from "svelte/store";
|
||||
|
||||
import GalleryWidget from "$lib/widgets/GalleryWidget.svelte";
|
||||
@@ -58,6 +58,7 @@ export default class ComfyGalleryNode extends ComfyWidgetNode<ComfyBoxImageMetad
|
||||
this.selectedIndexWidget = this.addWidget("text", "Selected", String(get(this.selectedImage)))
|
||||
this.selectedIndexWidget.disabled = true;
|
||||
this.modeWidget = this.addWidget("combo", "Mode", this.properties.updateMode, null, { property: "updateMode", values: ["replace", "append"] })
|
||||
this.defaultValue = []
|
||||
}
|
||||
|
||||
override onPropertyChanged(property: any, value: any) {
|
||||
@@ -103,6 +104,12 @@ export default class ComfyGalleryNode extends ComfyWidgetNode<ComfyBoxImageMetad
|
||||
|
||||
private _newSelectedIndex: number | null = null;
|
||||
|
||||
override stripUserState(o: SerializedLGraphNode) {
|
||||
super.stripUserState(o);
|
||||
o.properties.defaultValue = [];
|
||||
(o as any).comfyValue = [];
|
||||
}
|
||||
|
||||
override parseValue(param: any): ComfyBoxImageMetadata[] {
|
||||
if (param == null)
|
||||
return []
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { parseWhateverIntoImageMetadata, type ComfyBoxImageMetadata } from "$lib/utils";
|
||||
import type { FileData as GradioFileData } from "@gradio/upload";
|
||||
import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core";
|
||||
import { BuiltInSlotType, LiteGraph, type SerializedLGraphNode, type SlotLayout } from "@litegraph-ts/core";
|
||||
|
||||
import ImageUploadWidget from "$lib/widgets/ImageUploadWidget.svelte";
|
||||
import type { ComfyWidgetProperties } from "./ComfyWidgetNode";
|
||||
@@ -55,6 +55,12 @@ export default class ComfyImageUploadNode extends ComfyWidgetNode<ComfyBoxImageM
|
||||
override formatValue(value: GradioFileData[]): string {
|
||||
return `Images: ${value?.length || 0}`
|
||||
}
|
||||
|
||||
override stripUserState(o: SerializedLGraphNode) {
|
||||
super.stripUserState(o);
|
||||
o.properties.defaultValue = [];
|
||||
(o as any).comfyValue = [];
|
||||
}
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType({
|
||||
|
||||
@@ -355,6 +355,6 @@ export default abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||
|
||||
override stripUserState(o: SerializedLGraphNode) {
|
||||
super.stripUserState(o);
|
||||
(o as any).comfyValue = this.properties.defaultValue;
|
||||
(o as any).comfyValue = LiteGraph.cloneObject(this.properties.defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,12 @@ export type ConfigState = {
|
||||
|
||||
/** When closing the tab, open the confirmation window if there's unsaved changes */
|
||||
confirmWhenUnloadingUnsavedChanges: boolean,
|
||||
|
||||
/** Basenames of templates that can be loaded from public/templates. Saves LocalStorage space. */
|
||||
builtInTemplates: string[],
|
||||
|
||||
/** Cache loading of built-in resources to save network use */
|
||||
cacheBuiltInResources: boolean
|
||||
}
|
||||
|
||||
type ConfigStateOps = {
|
||||
@@ -30,7 +36,9 @@ const store: Writable<ConfigState> = writable(
|
||||
comfyUIPort: 8188,
|
||||
alwaysStripUserState: false,
|
||||
promptForWorkflowName: false,
|
||||
confirmWhenUnloadingUnsavedChanges: true
|
||||
confirmWhenUnloadingUnsavedChanges: true,
|
||||
builtInTemplates: [],
|
||||
cacheBuiltInResources: true,
|
||||
})
|
||||
|
||||
function getBackendURL(): string {
|
||||
|
||||
@@ -8,6 +8,8 @@ export type ModalButton = {
|
||||
name: string,
|
||||
variant: "primary" | "secondary",
|
||||
onClick: (state: ModalData) => boolean | void,
|
||||
disabled?: boolean,
|
||||
hidden?: boolean,
|
||||
closeOnClick?: boolean
|
||||
}
|
||||
export interface ModalData {
|
||||
|
||||
@@ -5,33 +5,48 @@ import type { Readable, Writable } from 'svelte/store';
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export type TemplateState = {
|
||||
templates: SerializedComfyBoxTemplate[]
|
||||
builtInTemplates: SerializedComfyBoxTemplate[]
|
||||
userTemplates: SerializedComfyBoxTemplate[]
|
||||
templatesByID: Record<UUID, SerializedComfyBoxTemplate>
|
||||
}
|
||||
|
||||
type TemplateStateOps = {
|
||||
getAllTemplates: () => SerializedComfyBoxTemplate[],
|
||||
addTemplate: (template: SerializedComfyBoxTemplate) => boolean,
|
||||
updateTemplate: (template: SerializedComfyBoxTemplate) => boolean,
|
||||
removeTemplate: (templateID: UUID) => boolean,
|
||||
save: () => void,
|
||||
load: () => void,
|
||||
add: (template: SerializedComfyBoxTemplate) => boolean,
|
||||
update: (template: SerializedComfyBoxTemplate) => boolean,
|
||||
remove: (templateID: UUID) => boolean,
|
||||
load: (builtInTemplates: SerializedComfyBoxTemplate[]) => void,
|
||||
}
|
||||
|
||||
export type WritableTemplateStateStore = Writable<TemplateState> & TemplateStateOps;
|
||||
const store: Writable<TemplateState> = writable(
|
||||
{
|
||||
templates: [],
|
||||
builtInTemplates: [],
|
||||
userTemplates: [],
|
||||
templatesByID: {}
|
||||
})
|
||||
|
||||
function add(template: SerializedComfyBoxTemplate): boolean {
|
||||
function getTemplateList(template: SerializedComfyBoxTemplate, state: TemplateState): SerializedComfyBoxTemplate[] {
|
||||
if (template.isBuiltIn)
|
||||
return state.builtInTemplates
|
||||
return state.userTemplates;
|
||||
}
|
||||
|
||||
function getAllTemplates(): SerializedComfyBoxTemplate[] {
|
||||
const state = get(store);
|
||||
return state.builtInTemplates.concat(state.userTemplates);
|
||||
}
|
||||
|
||||
function addTemplate(template: SerializedComfyBoxTemplate): boolean {
|
||||
const state = get(store);
|
||||
if (state.templatesByID[template.id]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
store.update(s => {
|
||||
s.templates.push(template);
|
||||
const templateList = getTemplateList(template, s)
|
||||
templateList.push(template)
|
||||
s.templatesByID[template.id] = template;
|
||||
return s;
|
||||
})
|
||||
@@ -41,15 +56,16 @@ function add(template: SerializedComfyBoxTemplate): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
function remove(templateID: UUID): boolean {
|
||||
function removeTemplate(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);
|
||||
const templateList = getTemplateList(s.templatesByID[templateID], s)
|
||||
const index = templateList.findIndex(t => t.id === templateID)
|
||||
templateList.splice(index, 1);
|
||||
delete s.templatesByID[templateID];
|
||||
return s;
|
||||
})
|
||||
@@ -59,7 +75,7 @@ function remove(templateID: UUID): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
function update(template: SerializedComfyBoxTemplate): boolean {
|
||||
function updateTemplate(template: SerializedComfyBoxTemplate): boolean {
|
||||
const state = get(store);
|
||||
if (!state.templatesByID[template.id]) {
|
||||
return false;
|
||||
@@ -67,13 +83,14 @@ function update(template: SerializedComfyBoxTemplate): boolean {
|
||||
|
||||
store.update(s => {
|
||||
const oldId = template.id
|
||||
const index = s.templates.findIndex(t => t.id === oldId)
|
||||
s.templates.splice(index, 1);
|
||||
const templateList = getTemplateList(template, s)
|
||||
const index = templateList.findIndex(t => t.id === oldId)
|
||||
templateList.splice(index, 1);
|
||||
delete s.templatesByID[oldId];
|
||||
|
||||
template.id = uuidv4();
|
||||
|
||||
s.templates.push(template);
|
||||
templateList.push(template);
|
||||
s.templatesByID[template.id] = template;
|
||||
return s;
|
||||
})
|
||||
@@ -84,12 +101,26 @@ function update(template: SerializedComfyBoxTemplate): boolean {
|
||||
}
|
||||
|
||||
function save() {
|
||||
const json = JSON.stringify(get(store).templates)
|
||||
const json = JSON.stringify(get(store).userTemplates)
|
||||
localStorage.setItem("templates", json)
|
||||
store.set(get(store))
|
||||
}
|
||||
|
||||
function load() {
|
||||
function load(builtInTemplates: SerializedComfyBoxTemplate[]) {
|
||||
store.update(s => {
|
||||
s.userTemplates = []
|
||||
s.templatesByID = {}
|
||||
|
||||
for (const t of builtInTemplates) {
|
||||
t.isBuiltIn = true;
|
||||
s.templatesByID[t.id] = t;
|
||||
}
|
||||
|
||||
s.builtInTemplates = builtInTemplates;
|
||||
|
||||
return s
|
||||
})
|
||||
|
||||
const json = localStorage.getItem("templates")
|
||||
if (!json) {
|
||||
console.info("No templates in local storage, creating store")
|
||||
@@ -100,18 +131,15 @@ function load() {
|
||||
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), {})
|
||||
data.map(t => {
|
||||
t.isBuiltIn = false;
|
||||
return [t.id, t]
|
||||
}).reduce((dict, el: [UUID, SerializedComfyBoxTemplate]) => (dict[el[0]] = el[1], dict), {})
|
||||
|
||||
store.set({
|
||||
templates: data,
|
||||
templatesByID
|
||||
})
|
||||
}
|
||||
else {
|
||||
store.set({
|
||||
templates: [],
|
||||
templatesByID: {}
|
||||
store.update(s => {
|
||||
s.userTemplates = data
|
||||
s.templatesByID = { ...s.templatesByID, ...templatesByID }
|
||||
return s;
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -119,10 +147,11 @@ function load() {
|
||||
const templateStateStore: WritableTemplateStateStore =
|
||||
{
|
||||
...store,
|
||||
add,
|
||||
remove,
|
||||
update,
|
||||
getAllTemplates,
|
||||
addTemplate,
|
||||
removeTemplate,
|
||||
updateTemplate,
|
||||
save,
|
||||
load,
|
||||
load
|
||||
}
|
||||
export default templateStateStore;
|
||||
|
||||
@@ -75,7 +75,9 @@ export function download(filename: string, text: string, type: string = "text/pl
|
||||
}, 0);
|
||||
}
|
||||
|
||||
export function getLocalStorageUsed(): number {
|
||||
export const MAX_LOCAL_STORAGE_MB = 5;
|
||||
|
||||
export function getLocalStorageUsedMB(): number {
|
||||
var total = 0;
|
||||
for (const x in localStorage) {
|
||||
// Value is multiplied by 2 due to data being stored in `utf-16` format, which requires twice the space.
|
||||
@@ -598,3 +600,13 @@ export function calcNodesBoundingBox(nodes: SerializedLGraphNode[]): Vector4 {
|
||||
|
||||
return [min_x, min_y, max_x, max_y];
|
||||
}
|
||||
|
||||
export async function readFileToText(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async () => {
|
||||
resolve(reader.result as string);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -146,6 +146,23 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
@mixin json-view {
|
||||
--jsonPaddingLeft: 1rem;
|
||||
--jsonBorderLeft: 1px dotted var(--neutral-600);
|
||||
--jsonBracketColor: currentcolor;
|
||||
--jsonBracketHoverBackground: var(--primary-200);
|
||||
--jsonSeparatorColor: currentcolor;
|
||||
--jsonKeyColor: var(--body-text-color);
|
||||
--jsonValColor: var(--body-text-color-subdued);
|
||||
--jsonValStringColor: var(--color-green-500);
|
||||
--jsonValNumberColor: var(--color-blue-500);
|
||||
--jsonValBooleanColor: var(--color-red-500);
|
||||
--jsonCollapsedColor: var(--neutral-100);
|
||||
--jsonCollapsedBackground: var(--primary-400);
|
||||
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
hr {
|
||||
color: var(--panel-border-color);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import { LGraph, LiteGraph, Subgraph, type SlotLayout } from "@litegraph-ts/core"
|
||||
import { Watch } from "@litegraph-ts/nodes-basic"
|
||||
import { expect } from 'vitest'
|
||||
import UnitTest from "./UnitTest"
|
||||
import ComfyGraph from "$lib/ComfyGraph";
|
||||
import ComfyPromptSerializer from "$lib/components/ComfyPromptSerializer";
|
||||
import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
|
||||
import ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
|
||||
import { graphToGraphVis } from "$lib/utils";
|
||||
import { ComfyNumberNode } from "$lib/nodes/widgets";
|
||||
import { get } from "svelte/store";
|
||||
import layoutStates from "$lib/stores/layoutStates";
|
||||
import { ComfyBoxWorkflow } from "$lib/stores/workflowState";
|
||||
import { LiteGraph, Subgraph } from "@litegraph-ts/core";
|
||||
import { get } from "svelte/store";
|
||||
import { expect } from 'vitest';
|
||||
import UnitTest from "./UnitTest";
|
||||
import { Watch } from "@litegraph-ts/nodes-basic";
|
||||
|
||||
export default class ComfyGraphTests extends UnitTest {
|
||||
test__onNodeAdded__updatesLayoutState() {
|
||||
@@ -92,4 +87,24 @@ export default class ComfyGraphTests extends UnitTest {
|
||||
expect(Object.keys(state.allItems)).toHaveLength(3)
|
||||
expect(Object.keys(state.allItemsByNode)).toHaveLength(0)
|
||||
}
|
||||
|
||||
test__serialize__stripsLinkData() {
|
||||
const [{ graph }, layoutState] = ComfyBoxWorkflow.create()
|
||||
layoutState.initDefaultLayout()
|
||||
|
||||
const widget = LiteGraph.createNode(ComfyNumberNode);
|
||||
const watch = LiteGraph.createNode(Watch);
|
||||
graph.add(widget)
|
||||
graph.add(watch)
|
||||
|
||||
widget.connect(0, watch, 0)
|
||||
const link = widget.getOutputLinks(0)[0]
|
||||
widget.setOutputData(0, 42);
|
||||
|
||||
const result = graph.serialize();
|
||||
|
||||
const serNode = result.nodes.find(n => n.id === widget.id);
|
||||
|
||||
expect(serNode.outputs[0]._data).toBeUndefined()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ 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"
|
||||
import { visualizer } from "rollup-plugin-visualizer";
|
||||
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
console.log("Production build: " + isProduction)
|
||||
@@ -30,6 +31,7 @@ export default defineConfig({
|
||||
isProduction && removeConsole(),
|
||||
glsl(),
|
||||
svelte(),
|
||||
visualizer(),
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user