From 4dfa66530364025aa5a7705de196afe3f4f74157 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Wed, 24 May 2023 17:38:37 -0500 Subject: [PATCH] Templates view and store --- src/global.d.ts | 8 + src/lib/ComfyBoxTemplate.ts | 86 ++++---- src/lib/ComfyGraphCanvas.ts | 16 +- src/lib/components/AccordionContainer.svelte | 12 +- src/lib/components/BlockContainer.svelte | 13 +- src/lib/components/ComfyApp.ts | 6 +- .../components/ComfyBoxWorkflowView.svelte | 4 +- .../components/ComfyBoxWorkflowsView.svelte | 11 +- src/lib/components/ComfyPaneView.svelte | 8 +- src/lib/components/ComfyTemplates.svelte | 186 ++++++++++++++++++ src/lib/components/TabsContainer.svelte | 12 +- .../components/modal/EditTemplateModal.svelte | 6 +- src/lib/components/utils.ts | 19 ++ src/lib/stores/layoutStates.ts | 22 ++- src/lib/stores/templateState.ts | 101 ++++++++++ src/scss/global.scss | 5 + 16 files changed, 444 insertions(+), 71 deletions(-) create mode 100644 src/global.d.ts create mode 100644 src/lib/components/ComfyTemplates.svelte create mode 100644 src/lib/components/utils.ts create mode 100644 src/lib/stores/templateState.ts diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 0000000..a72ab10 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,8 @@ +declare type Item = import('svelte-dnd-action').Item; +declare type DndEvent = import('svelte-dnd-action').DndEvent; +declare namespace svelte.JSX { + interface HTMLAttributes { + onconsider?: (event: CustomEvent> & { target: EventTarget & T }) => void; + onfinalize?: (event: CustomEvent> & { target: EventTarget & T }) => void; + } +} diff --git a/src/lib/ComfyBoxTemplate.ts b/src/lib/ComfyBoxTemplate.ts index 07e52ea..fdb23f4 100644 --- a/src/lib/ComfyBoxTemplate.ts +++ b/src/lib/ComfyBoxTemplate.ts @@ -1,9 +1,10 @@ -import { Subgraph, type LGraphNode, type LLink, type SerializedLGraphNode, type SerializedLLink, LGraph, type NodeID } from "@litegraph-ts/core" +import { Subgraph, type LGraphNode, type LLink, type SerializedLGraphNode, type SerializedLLink, LGraph, type NodeID, type UUID } 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 { v4 as uuidv4 } from "uuid"; /* * In ComfyBox a template contains a subset of nodes in the graph and the set of @@ -11,6 +12,7 @@ import { download } from "./utils"; */ export type ComfyBoxTemplate = { version: 1, + id: UUID, metadata: ComfyBoxTemplateMetadata, nodes: LGraphNode[], links: LLink[], @@ -19,6 +21,17 @@ export type ComfyBoxTemplate = { 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. @@ -26,6 +39,7 @@ export type SerializedTemplateLink = [NodeID, number, NodeID, number]; export type SerializedComfyBoxTemplate = { isComfyBoxTemplate: true, version: 1, + id: UUID, /* * Serialized metadata @@ -46,29 +60,23 @@ export type SerializedComfyBoxTemplate = { * Serialized container type drag item */ layout?: SerializedLayoutState + + /* + * SVG of the graph ndoes + */ + svg?: string } function isSerializedComfyBoxTemplate(param: any): param is SerializedComfyBoxTemplate { return param && param.isComfyBoxTemplate; } -export type SerializedComfyBoxTemplateAndSVG = { - template: SerializedComfyBoxTemplate, - svg: string, -} - -export type ComfyBoxTemplateMetadata = { - title: string, - author: string, - tags: string[], - - // TODO required/optional python extensions -} - const DEFAULT_TEMPLATE_METADATA = { title: "New Template", author: "Anonymous", - tags: [] + description: "A brand-new ComfyBox template", + tags: [], + category: "general" } export type ComfyBoxTemplateError = { @@ -145,7 +153,7 @@ function unescapeXml(safe) { const TEMPLATE_SVG_PADDING: number = 50; -function renderSvg(canvas: ComfyGraphCanvas, graph: LGraph, extraData: SerializedComfyBoxTemplate, 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) => { @@ -247,11 +255,16 @@ function renderSvg(canvas: ComfyGraphCanvas, graph: LGraph, extraData: Serialize 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("", `${escapeXml(json)}`); + let svg = svgCtx.getSerializedSvg(true) + return svg +} +export function embedTemplateInSvg(template: SerializedComfyBoxTemplate): string { + let oldSvg = template.svg; + template.svg = undefined; + const json = JSON.stringify(template); + const svg = oldSvg.replace("", `${escapeXml(json)}`); + template.svg = oldSvg; return svg } @@ -285,7 +298,7 @@ function convLinkForTemplate(link: LLink): SerializedTemplateLink { return [link.origin_id, link.origin_slot, link.target_id, link.target_slot]; } -export function serializeTemplate(canvas: ComfyGraphCanvas, template: ComfyBoxTemplate): SerializedComfyBoxTemplateAndSVG { +export function serializeTemplate(canvas: ComfyGraphCanvas, template: ComfyBoxTemplate): SerializedComfyBoxTemplate { let graph: LGraph if (template.nodes.length === 1 && template.nodes[0].is(Subgraph)) { graph = template.nodes[0].subgraph @@ -305,21 +318,23 @@ export function serializeTemplate(canvas: ComfyGraphCanvas, template: ComfyBoxTe [nodes, links] = pruneDetachedLinks(nodes, links); - let serTemplate: SerializedComfyBoxTemplate = { + const svg = renderSvg(canvas, graph, TEMPLATE_SVG_PADDING); + + const serTemplate: SerializedComfyBoxTemplate = { isComfyBoxTemplate: true, version: 1, + id: template.id, metadata, nodes, links, layout, + svg } - const svg = renderSvg(canvas, graph, serTemplate, TEMPLATE_SVG_PADDING) - - return { svg, template: serTemplate } + return serTemplate; } -export function deserializeTemplate(file: File): Promise { +export function deserializeTemplateFromSVG(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = async () => { @@ -340,10 +355,8 @@ export function deserializeTemplate(file: File): Promise 0) { // Find the highest-level container that contains all these nodes and // contains no other widgets @@ -386,7 +404,8 @@ export function createTemplate(nodes: LGraphNode[]): ComfyBoxTemplateResult { return { version: 1, - metadata: { ...DEFAULT_TEMPLATE_METADATA }, + id: uuidv4(), + metadata: { ...DEFAULT_TEMPLATE_METADATA, title, createdAt: Date.now() }, nodes: nodes, links: links, container: container @@ -396,7 +415,8 @@ export function createTemplate(nodes: LGraphNode[]): ComfyBoxTemplateResult { // No UI to serialize. return { version: 1, - metadata: { ...DEFAULT_TEMPLATE_METADATA }, + id: uuidv4(), + metadata: { ...DEFAULT_TEMPLATE_METADATA, title, createdAt: Date.now() }, nodes: nodes, links: links, } diff --git a/src/lib/ComfyGraphCanvas.ts b/src/lib/ComfyGraphCanvas.ts index 1bc57fc..af302c9 100644 --- a/src/lib/ComfyGraphCanvas.ts +++ b/src/lib/ComfyGraphCanvas.ts @@ -6,6 +6,7 @@ import { ComfyReroute } from "./nodes"; import layoutStates from "./stores/layoutStates"; import queueState from "./stores/queueState"; import selectionState from "./stores/selectionState"; +import templateState from "./stores/templateState"; import { createTemplate, type ComfyBoxTemplate, serializeTemplate, type SerializedComfyBoxTemplate } from "./ComfyBoxTemplate"; import notify from "./notify"; import { v4 as uuidv4 } from "uuid" @@ -523,11 +524,20 @@ export default class ComfyGraphCanvas extends LGraphCanvas { const template = result as ComfyBoxTemplate; - console.warn("TEMPLATEFOUND", template) - const serialized = serializeTemplate(this, template); - download("template.svg", serialized.svg, "image/svg+xml"); + 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[] { diff --git a/src/lib/components/AccordionContainer.svelte b/src/lib/components/AccordionContainer.svelte index 6dbda8c..b9ed311 100644 --- a/src/lib/components/AccordionContainer.svelte +++ b/src/lib/components/AccordionContainer.svelte @@ -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>) { + 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>) { + children = handleContainerFinalize(layoutState, container, evt) }; function handleClick(e: CustomEvent) { @@ -85,6 +84,7 @@ class:empty={children.length === 0} class:edit={edit} use:dndzone="{{ + type: "layout", items: children, flipDurationMs, centreDraggedOnCursor: true, diff --git a/src/lib/components/BlockContainer.svelte b/src/lib/components/BlockContainer.svelte index 1fb5072..913c274 100644 --- a/src/lib/components/BlockContainer.svelte +++ b/src/lib/components/BlockContainer.svelte @@ -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>) { + 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>) { + 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, diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index 27e5be7..b0b1b8e 100644 --- a/src/lib/components/ComfyApp.ts +++ b/src/lib/components/ComfyApp.ts @@ -36,7 +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 { deserializeTemplate } from "$lib/ComfyBoxTemplate"; +import { deserializeTemplateFromSVG } from "$lib/ComfyBoxTemplate"; +import templateState from "$lib/stores/templateState"; export const COMFYBOX_SERIAL_VERSION = 1; @@ -234,6 +235,7 @@ export default class ComfyApp { this.addKeyboardHandler(); await this.updateHistoryAndQueue(); + templateState.load(); await this.initFrontendFeatures(); @@ -1004,7 +1006,7 @@ export default class ComfyApp { }; reader.readAsText(file); } else if (file.type === "image/svg+xml" || file.name.endsWith(".svg")) { - const templateAndSvg = await deserializeTemplate(file); + const templateAndSvg = await deserializeTemplateFromSVG(file); modalState.pushModal({ title: "ComfyBox Template Preview", svelteComponent: EditTemplateModal, diff --git a/src/lib/components/ComfyBoxWorkflowView.svelte b/src/lib/components/ComfyBoxWorkflowView.svelte index d766cd2..d0846dc 100644 --- a/src/lib/components/ComfyBoxWorkflowView.svelte +++ b/src/lib/components/ComfyBoxWorkflowView.svelte @@ -140,7 +140,7 @@ {#if workflow != null} {#if layoutState != null} -
+
{/if} @@ -185,7 +185,7 @@ {/if} diff --git a/src/lib/components/TabsContainer.svelte b/src/lib/components/TabsContainer.svelte index 5d71a34..162ed46 100644 --- a/src/lib/components/TabsContainer.svelte +++ b/src/lib/components/TabsContainer.svelte @@ -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>) { + 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>) { + 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, diff --git a/src/lib/components/modal/EditTemplateModal.svelte b/src/lib/components/modal/EditTemplateModal.svelte index 555026f..ff11a5c 100644 --- a/src/lib/components/modal/EditTemplateModal.svelte +++ b/src/lib/components/modal/EditTemplateModal.svelte @@ -1,5 +1,5 @@