Refactor for built-in templates
This commit is contained in:
Submodule litegraph updated: 8ca6cca777...d335948703
@@ -42,6 +42,7 @@ export type SerializedComfyBoxTemplate = {
|
|||||||
version: 1,
|
version: 1,
|
||||||
id: UUID,
|
id: UUID,
|
||||||
commitHash: string,
|
commitHash: string,
|
||||||
|
isBuiltIn?: boolean,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Serialized metadata
|
* Serialized metadata
|
||||||
|
|||||||
@@ -489,7 +489,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
|||||||
const serialized = serializeTemplate(this, template);
|
const serialized = serializeTemplate(this, template);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (templateState.add(serialized)) {
|
if (templateState.addTemplate(serialized)) {
|
||||||
notify("Template saved!", { type: "success" })
|
notify("Template saved!", { type: "success" })
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import { type SvelteComponentDev } from "svelte/internal";
|
|||||||
import { get, writable, type Writable } from "svelte/store";
|
import { get, writable, type Writable } from "svelte/store";
|
||||||
import ComfyPromptSerializer, { isActiveBackendNode, UpstreamNodeLocator } from "./ComfyPromptSerializer";
|
import ComfyPromptSerializer, { isActiveBackendNode, UpstreamNodeLocator } from "./ComfyPromptSerializer";
|
||||||
import DanbooruTags from "$lib/DanbooruTags";
|
import DanbooruTags from "$lib/DanbooruTags";
|
||||||
import { deserializeTemplateFromSVG } from "$lib/ComfyBoxTemplate";
|
import { deserializeTemplateFromSVG, type SerializedComfyBoxTemplate } from "$lib/ComfyBoxTemplate";
|
||||||
import templateState from "$lib/stores/templateState";
|
import templateState from "$lib/stores/templateState";
|
||||||
|
|
||||||
export const COMFYBOX_SERIAL_VERSION = 1;
|
export const COMFYBOX_SERIAL_VERSION = 1;
|
||||||
@@ -240,7 +240,9 @@ export default class ComfyApp {
|
|||||||
this.addKeyboardHandler();
|
this.addKeyboardHandler();
|
||||||
|
|
||||||
await this.updateHistoryAndQueue();
|
await this.updateHistoryAndQueue();
|
||||||
templateState.load();
|
|
||||||
|
const builtInTemplates = await this.loadBuiltInTemplates();
|
||||||
|
templateState.load(builtInTemplates);
|
||||||
|
|
||||||
await this.initFrontendFeatures();
|
await this.initFrontendFeatures();
|
||||||
|
|
||||||
@@ -271,6 +273,10 @@ export default class ComfyApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async loadBuiltInTemplates(): Promise<SerializedComfyBoxTemplate[]> {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
resizeCanvas() {
|
resizeCanvas() {
|
||||||
if (!this.canvasEl)
|
if (!this.canvasEl)
|
||||||
return;
|
return;
|
||||||
@@ -1062,7 +1068,7 @@ export default class ComfyApp {
|
|||||||
|
|
||||||
const importTemplate = () => {
|
const importTemplate = () => {
|
||||||
try {
|
try {
|
||||||
if (templateState.add(templateAndSvg)) {
|
if (templateState.addTemplate(templateAndSvg)) {
|
||||||
notify("Template imported successfully!", { type: "success" })
|
notify("Template imported successfully!", { type: "success" })
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { embedTemplateInSvg, type SerializedComfyBoxTemplate } from "$lib/ComfyBoxTemplate";
|
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 } from "svelte-bootstrap-icons";
|
||||||
import uiState from "$lib/stores/uiState";
|
import uiState from "$lib/stores/uiState";
|
||||||
import { download, truncateString } from "$lib/utils";
|
import { download, truncateString } from "$lib/utils";
|
||||||
import type ComfyApp from "./ComfyApp";
|
import type ComfyApp from "./ComfyApp";
|
||||||
@@ -28,10 +29,10 @@
|
|||||||
|
|
||||||
let _sorted: TemplateLayout[] = []
|
let _sorted: TemplateLayout[] = []
|
||||||
|
|
||||||
$: rebuildTemplates($templateState.templates);
|
$: $templateState && rebuildTemplates(templateState.getAllTemplates());
|
||||||
|
|
||||||
function rebuildTemplates(templates: SerializedComfyBoxTemplate[]) {
|
function rebuildTemplates(allTemplates: SerializedComfyBoxTemplate[]): void {
|
||||||
_sorted = Array.from(templates).map(t => {
|
_sorted = Array.from(allTemplates).map(t => {
|
||||||
return {
|
return {
|
||||||
type: "template", id: uuidv4(), template: t, attrs: {...defaultWidgetAttributes}, attrsChanged: writable(0)
|
type: "template", id: uuidv4(), template: t, attrs: {...defaultWidgetAttributes}, attrsChanged: writable(0)
|
||||||
}
|
}
|
||||||
@@ -88,7 +89,7 @@
|
|||||||
const saveTemplate = (modal: ModalData) => {
|
const saveTemplate = (modal: ModalData) => {
|
||||||
updateTemplate(modal);
|
updateTemplate(modal);
|
||||||
try {
|
try {
|
||||||
templateState.update(layout.template);
|
templateState.updateTemplate(layout.template);
|
||||||
notify("Saved template!", { type: "success" })
|
notify("Saved template!", { type: "success" })
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
@@ -101,7 +102,7 @@
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (templateState.remove(layout.template.id)) {
|
if (templateState.removeTemplate(layout.template.id)) {
|
||||||
notify("Template deleted!", { type: "success" })
|
notify("Template deleted!", { type: "success" })
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -165,17 +166,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="template-entries-wrapper"
|
<div class="template-entries-wrapper"
|
||||||
use:dndzone={{
|
use:dndzone={{
|
||||||
type: "layout",
|
type: "layout",
|
||||||
items: _sorted,
|
items: _sorted,
|
||||||
flipDurationMs,
|
flipDurationMs,
|
||||||
dragDisabled: !draggable,
|
dragDisabled: !draggable,
|
||||||
dropFromOthersDisabled: true
|
dropFromOthersDisabled: true
|
||||||
}}
|
}}
|
||||||
on:consider={handleDndConsider}
|
on:consider={handleDndConsider}
|
||||||
on:finalize={handleDndFinalize}>
|
on:finalize={handleDndFinalize}>
|
||||||
{#each _sorted.filter(i => i.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)}
|
{#each _sorted.filter(i => i.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)}
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div class="template-entry" class:draggable on:click={() => handleClick(item)}>
|
<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-name">{item.template.metadata.title}</div>
|
||||||
<div class="template-desc">{item.template.metadata.description}</div>
|
<div class="template-desc">{item.template.metadata.description}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -186,8 +187,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="no-templates">
|
<div class="no-templates-container">
|
||||||
<span>(No templates)</span>
|
<div class="no-templates-icon">
|
||||||
|
<BoxSeam width="100%" height="8rem" />
|
||||||
|
</div>
|
||||||
|
<div class="no-templates-message">
|
||||||
|
(No templates)
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -253,15 +259,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-templates {
|
.no-templates-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
color: var(--comfy-accent-soft);
|
color: var(--comfy-accent-soft);
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
height: 100%;
|
|
||||||
color: var(--comfy-accent-soft);
|
color: var(--comfy-accent-soft);
|
||||||
|
|
||||||
span {
|
.no-templates-icon {
|
||||||
|
margin: auto;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-templates-message {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
|
|||||||
@@ -5,33 +5,48 @@ import type { Readable, Writable } from 'svelte/store';
|
|||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
export type TemplateState = {
|
export type TemplateState = {
|
||||||
templates: SerializedComfyBoxTemplate[]
|
builtInTemplates: SerializedComfyBoxTemplate[]
|
||||||
|
userTemplates: SerializedComfyBoxTemplate[]
|
||||||
templatesByID: Record<UUID, SerializedComfyBoxTemplate>
|
templatesByID: Record<UUID, SerializedComfyBoxTemplate>
|
||||||
}
|
}
|
||||||
|
|
||||||
type TemplateStateOps = {
|
type TemplateStateOps = {
|
||||||
|
getAllTemplates: () => SerializedComfyBoxTemplate[],
|
||||||
|
addTemplate: (template: SerializedComfyBoxTemplate) => boolean,
|
||||||
|
updateTemplate: (template: SerializedComfyBoxTemplate) => boolean,
|
||||||
|
removeTemplate: (templateID: UUID) => boolean,
|
||||||
save: () => void,
|
save: () => void,
|
||||||
load: () => void,
|
load: (builtInTemplates: SerializedComfyBoxTemplate[]) => void,
|
||||||
add: (template: SerializedComfyBoxTemplate) => boolean,
|
|
||||||
update: (template: SerializedComfyBoxTemplate) => boolean,
|
|
||||||
remove: (templateID: UUID) => boolean,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WritableTemplateStateStore = Writable<TemplateState> & TemplateStateOps;
|
export type WritableTemplateStateStore = Writable<TemplateState> & TemplateStateOps;
|
||||||
const store: Writable<TemplateState> = writable(
|
const store: Writable<TemplateState> = writable(
|
||||||
{
|
{
|
||||||
templates: [],
|
builtInTemplates: [],
|
||||||
|
userTemplates: [],
|
||||||
templatesByID: {}
|
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);
|
const state = get(store);
|
||||||
if (state.templatesByID[template.id]) {
|
if (state.templatesByID[template.id]) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
store.update(s => {
|
store.update(s => {
|
||||||
s.templates.push(template);
|
const templateList = getTemplateList(template, s)
|
||||||
|
templateList.push(template)
|
||||||
s.templatesByID[template.id] = template;
|
s.templatesByID[template.id] = template;
|
||||||
return s;
|
return s;
|
||||||
})
|
})
|
||||||
@@ -41,15 +56,16 @@ function add(template: SerializedComfyBoxTemplate): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove(templateID: UUID): boolean {
|
function removeTemplate(templateID: UUID): boolean {
|
||||||
const state = get(store);
|
const state = get(store);
|
||||||
if (!state.templatesByID[templateID]) {
|
if (!state.templatesByID[templateID]) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
store.update(s => {
|
store.update(s => {
|
||||||
const index = s.templates.findIndex(t => t.id === templateID)
|
const templateList = getTemplateList(s.templatesByID[templateID], s)
|
||||||
s.templates.splice(index, 1);
|
const index = templateList.findIndex(t => t.id === templateID)
|
||||||
|
templateList.splice(index, 1);
|
||||||
delete s.templatesByID[templateID];
|
delete s.templatesByID[templateID];
|
||||||
return s;
|
return s;
|
||||||
})
|
})
|
||||||
@@ -59,7 +75,7 @@ function remove(templateID: UUID): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function update(template: SerializedComfyBoxTemplate): boolean {
|
function updateTemplate(template: SerializedComfyBoxTemplate): boolean {
|
||||||
const state = get(store);
|
const state = get(store);
|
||||||
if (!state.templatesByID[template.id]) {
|
if (!state.templatesByID[template.id]) {
|
||||||
return false;
|
return false;
|
||||||
@@ -67,13 +83,14 @@ function update(template: SerializedComfyBoxTemplate): boolean {
|
|||||||
|
|
||||||
store.update(s => {
|
store.update(s => {
|
||||||
const oldId = template.id
|
const oldId = template.id
|
||||||
const index = s.templates.findIndex(t => t.id === oldId)
|
const templateList = getTemplateList(template, s)
|
||||||
s.templates.splice(index, 1);
|
const index = templateList.findIndex(t => t.id === oldId)
|
||||||
|
templateList.splice(index, 1);
|
||||||
delete s.templatesByID[oldId];
|
delete s.templatesByID[oldId];
|
||||||
|
|
||||||
template.id = uuidv4();
|
template.id = uuidv4();
|
||||||
|
|
||||||
s.templates.push(template);
|
templateList.push(template);
|
||||||
s.templatesByID[template.id] = template;
|
s.templatesByID[template.id] = template;
|
||||||
return s;
|
return s;
|
||||||
})
|
})
|
||||||
@@ -84,12 +101,26 @@ function update(template: SerializedComfyBoxTemplate): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
const json = JSON.stringify(get(store).templates)
|
const json = JSON.stringify(get(store).userTemplates)
|
||||||
localStorage.setItem("templates", json)
|
localStorage.setItem("templates", json)
|
||||||
store.set(get(store))
|
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")
|
const json = localStorage.getItem("templates")
|
||||||
if (!json) {
|
if (!json) {
|
||||||
console.info("No templates in local storage, creating store")
|
console.info("No templates in local storage, creating store")
|
||||||
@@ -100,18 +131,15 @@ function load() {
|
|||||||
const data = JSON.parse(json) as SerializedComfyBoxTemplate[];
|
const data = JSON.parse(json) as SerializedComfyBoxTemplate[];
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
const templatesByID: Record<UUID, SerializedComfyBoxTemplate> =
|
const templatesByID: Record<UUID, SerializedComfyBoxTemplate> =
|
||||||
data.map(d => [d.id, d])
|
data.map(t => {
|
||||||
.reduce((dict, el: [UUID, SerializedComfyBoxTemplate]) => (dict[el[0]] = el[1], dict), {})
|
t.isBuiltIn = false;
|
||||||
|
return [t.id, t]
|
||||||
|
}).reduce((dict, el: [UUID, SerializedComfyBoxTemplate]) => (dict[el[0]] = el[1], dict), {})
|
||||||
|
|
||||||
store.set({
|
store.update(s => {
|
||||||
templates: data,
|
s.userTemplates = data
|
||||||
templatesByID
|
s.templatesByID = { ...s.templatesByID, ...templatesByID }
|
||||||
})
|
return s;
|
||||||
}
|
|
||||||
else {
|
|
||||||
store.set({
|
|
||||||
templates: [],
|
|
||||||
templatesByID: {}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,10 +147,11 @@ function load() {
|
|||||||
const templateStateStore: WritableTemplateStateStore =
|
const templateStateStore: WritableTemplateStateStore =
|
||||||
{
|
{
|
||||||
...store,
|
...store,
|
||||||
add,
|
getAllTemplates,
|
||||||
remove,
|
addTemplate,
|
||||||
update,
|
removeTemplate,
|
||||||
|
updateTemplate,
|
||||||
save,
|
save,
|
||||||
load,
|
load
|
||||||
}
|
}
|
||||||
export default templateStateStore;
|
export default templateStateStore;
|
||||||
|
|||||||
@@ -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 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 { ComfyNumberNode } from "$lib/nodes/widgets";
|
||||||
import { get } from "svelte/store";
|
|
||||||
import layoutStates from "$lib/stores/layoutStates";
|
|
||||||
import { ComfyBoxWorkflow } from "$lib/stores/workflowState";
|
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 {
|
export default class ComfyGraphTests extends UnitTest {
|
||||||
test__onNodeAdded__updatesLayoutState() {
|
test__onNodeAdded__updatesLayoutState() {
|
||||||
@@ -92,4 +87,24 @@ export default class ComfyGraphTests extends UnitTest {
|
|||||||
expect(Object.keys(state.allItems)).toHaveLength(3)
|
expect(Object.keys(state.allItems)).toHaveLength(3)
|
||||||
expect(Object.keys(state.allItemsByNode)).toHaveLength(0)
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user