Template saving/loading
This commit is contained in:
Submodule litegraph updated: a1bf4cb511...fd0c428237
@@ -91,6 +91,7 @@
|
|||||||
"style-mod": "^4.0.3",
|
"style-mod": "^4.0.3",
|
||||||
"svelte-bootstrap-icons": "^2.3.1",
|
"svelte-bootstrap-icons": "^2.3.1",
|
||||||
"svelte-feather-icons": "^4.0.0",
|
"svelte-feather-icons": "^4.0.0",
|
||||||
|
"svelte-floating-ui": "^1.5.2",
|
||||||
"svelte-preprocess": "^5.0.3",
|
"svelte-preprocess": "^5.0.3",
|
||||||
"svelte-select": "^5.5.3",
|
"svelte-select": "^5.5.3",
|
||||||
"svelte-splitpanes": "^0.7.13",
|
"svelte-splitpanes": "^0.7.13",
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -151,6 +151,9 @@ importers:
|
|||||||
svelte-feather-icons:
|
svelte-feather-icons:
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
|
svelte-floating-ui:
|
||||||
|
specifier: ^1.5.2
|
||||||
|
version: 1.5.2
|
||||||
svelte-preprocess:
|
svelte-preprocess:
|
||||||
specifier: ^5.0.3
|
specifier: ^5.0.3
|
||||||
version: 5.0.3(sass@1.61.0)(svelte@3.58.0)(typescript@5.0.3)
|
version: 5.0.3(sass@1.61.0)(svelte@3.58.0)(typescript@5.0.3)
|
||||||
@@ -8135,6 +8138,13 @@ packages:
|
|||||||
'@floating-ui/dom': 1.2.8
|
'@floating-ui/dom': 1.2.8
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/svelte-floating-ui@1.5.2:
|
||||||
|
resolution: {integrity: sha512-nV50eno74CEsFfFJ6iyN/oNYDEOck1TZjGV3lmJksVRbiiUAVF6bHspyAhR7GZ7c/4qbRWp9UyX24J+UXdEpag==}
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/core': 1.2.6
|
||||||
|
'@floating-ui/dom': 1.2.8
|
||||||
|
dev: false
|
||||||
|
|
||||||
/svelte-hmr@0.15.1(svelte@3.58.0):
|
/svelte-hmr@0.15.1(svelte@3.58.0):
|
||||||
resolution: {integrity: sha512-BiKB4RZ8YSwRKCNVdNxK/GfY+r4Kjgp9jCLEy0DuqAKfmQtpL38cQK3afdpjw4sqSs4PLi3jIPJIFp259NkZtA==}
|
resolution: {integrity: sha512-BiKB4RZ8YSwRKCNVdNxK/GfY+r4Kjgp9jCLEy0DuqAKfmQtpL38cQK3afdpjw4sqSs4PLi3jIPJIFp259NkZtA==}
|
||||||
engines: {node: ^12.20 || ^14.13.1 || >= 16}
|
engines: {node: ^12.20 || ^14.13.1 || >= 16}
|
||||||
|
|||||||
BIN
public/image/graph-bg.png
Normal file
BIN
public/image/graph-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 350 B |
@@ -40,6 +40,7 @@ export type SerializedComfyBoxTemplate = {
|
|||||||
isComfyBoxTemplate: true,
|
isComfyBoxTemplate: true,
|
||||||
version: 1,
|
version: 1,
|
||||||
id: UUID,
|
id: UUID,
|
||||||
|
commitHash: string,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Serialized metadata
|
* Serialized metadata
|
||||||
@@ -182,6 +183,7 @@ function renderSvg(canvas: ComfyGraphCanvas, graph: LGraph, padding: number, ext
|
|||||||
const offset = canvas.ds.offset;
|
const offset = canvas.ds.offset;
|
||||||
const show_info = canvas.show_info;
|
const show_info = canvas.show_info;
|
||||||
const background_image = canvas.background_image;
|
const background_image = canvas.background_image;
|
||||||
|
const clear_background = canvas.clear_background;
|
||||||
const render_canvas_border = canvas.render_canvas_border;
|
const render_canvas_border = canvas.render_canvas_border;
|
||||||
const render_subgraph_panels = canvas.render_subgraph_panels
|
const render_subgraph_panels = canvas.render_subgraph_panels
|
||||||
const render_subgraph_stack_header = canvas.render_subgraph_stack_header
|
const render_subgraph_stack_header = canvas.render_subgraph_stack_header
|
||||||
@@ -189,6 +191,7 @@ function renderSvg(canvas: ComfyGraphCanvas, graph: LGraph, padding: number, ext
|
|||||||
canvas.openSubgraph(graph)
|
canvas.openSubgraph(graph)
|
||||||
canvas.show_info = false;
|
canvas.show_info = false;
|
||||||
canvas.background_image = null;
|
canvas.background_image = null;
|
||||||
|
canvas.clear_background = false;
|
||||||
canvas.render_canvas_border = false;
|
canvas.render_canvas_border = false;
|
||||||
canvas.render_subgraph_panels = false;
|
canvas.render_subgraph_panels = false;
|
||||||
canvas.render_subgraph_stack_header = false;
|
canvas.render_subgraph_stack_header = false;
|
||||||
@@ -233,12 +236,10 @@ function renderSvg(canvas: ComfyGraphCanvas, graph: LGraph, padding: number, ext
|
|||||||
canvas.ds.offset = [-bounds[0], -bounds[1]];
|
canvas.ds.offset = [-bounds[0], -bounds[1]];
|
||||||
canvas.ctx = svgCtx;
|
canvas.ctx = svgCtx;
|
||||||
|
|
||||||
let saving = false;
|
|
||||||
|
|
||||||
// Trigger saving
|
// Trigger saving
|
||||||
saving = true;
|
canvas.isExportingSVG = true;
|
||||||
canvas.draw(true, true);
|
canvas.draw(true, true);
|
||||||
saving = false;
|
canvas.isExportingSVG = false;
|
||||||
|
|
||||||
// Restore original settings
|
// Restore original settings
|
||||||
canvas.closeSubgraph();
|
canvas.closeSubgraph();
|
||||||
@@ -248,6 +249,7 @@ function renderSvg(canvas: ComfyGraphCanvas, graph: LGraph, padding: number, ext
|
|||||||
canvas.ds.offset = offset;
|
canvas.ds.offset = offset;
|
||||||
canvas.ctx = ctx;
|
canvas.ctx = ctx;
|
||||||
canvas.show_info = show_info;
|
canvas.show_info = show_info;
|
||||||
|
canvas.clear_background = clear_background;
|
||||||
canvas.background_image = background_image;
|
canvas.background_image = background_image;
|
||||||
canvas.render_canvas_border = render_canvas_border;
|
canvas.render_canvas_border = render_canvas_border;
|
||||||
canvas.render_subgraph_panels = render_subgraph_panels;
|
canvas.render_subgraph_panels = render_subgraph_panels;
|
||||||
@@ -323,6 +325,7 @@ export function serializeTemplate(canvas: ComfyGraphCanvas, template: ComfyBoxTe
|
|||||||
const serTemplate: SerializedComfyBoxTemplate = {
|
const serTemplate: SerializedComfyBoxTemplate = {
|
||||||
isComfyBoxTemplate: true,
|
isComfyBoxTemplate: true,
|
||||||
version: 1,
|
version: 1,
|
||||||
|
commitHash: __GIT_COMMIT_HASH__,
|
||||||
id: template.id,
|
id: template.id,
|
||||||
metadata,
|
metadata,
|
||||||
nodes,
|
nodes,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LConnectionKind, LGraph, LGraphNode, type INodeSlot, type SlotIndex, LiteGraph, getStaticProperty, type LGraphAddNodeOptions, LGraphCanvas, type LGraphRemoveNodeOptions, Subgraph, type LGraphAddNodeMode, type SerializedLGraphNode, type Vector2 } from "@litegraph-ts/core";
|
import { LConnectionKind, LGraph, LGraphNode, type INodeSlot, type SlotIndex, LiteGraph, getStaticProperty, type LGraphAddNodeOptions, LGraphCanvas, type LGraphRemoveNodeOptions, Subgraph, type LGraphAddNodeMode, type SerializedLGraphNode, type Vector2, type NodeID, reassignGraphIDs, type GraphIDMapping, type SerializedLGraph } from "@litegraph-ts/core";
|
||||||
import GraphSync from "./GraphSync";
|
import GraphSync from "./GraphSync";
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import type TypedEmitter from "typed-emitter";
|
import type TypedEmitter from "typed-emitter";
|
||||||
@@ -14,6 +14,31 @@ import layoutStates from "./stores/layoutStates";
|
|||||||
import type { ComfyBoxWorkflow, WorkflowInstID } from "./stores/workflowState";
|
import type { ComfyBoxWorkflow, WorkflowInstID } from "./stores/workflowState";
|
||||||
import workflowState from "./stores/workflowState";
|
import workflowState from "./stores/workflowState";
|
||||||
import type { SerializedComfyBoxTemplate } from "./ComfyBoxTemplate";
|
import type { SerializedComfyBoxTemplate } from "./ComfyBoxTemplate";
|
||||||
|
import { v4 as uuidv4 } from "uuid"
|
||||||
|
|
||||||
|
function calculateMinPosOfNodes(nodes: SerializedLGraphNode[]): Vector2 {
|
||||||
|
var posMin: Vector2 = [0, 0]
|
||||||
|
var posMinIndexes: [number, number] | null = null;
|
||||||
|
|
||||||
|
for (var i = 0; i < nodes.length; ++i) {
|
||||||
|
if (posMin) {
|
||||||
|
if (posMin[0] > nodes[i].pos[0]) {
|
||||||
|
posMin[0] = nodes[i].pos[0];
|
||||||
|
posMinIndexes[0] = i;
|
||||||
|
}
|
||||||
|
if (posMin[1] > nodes[i].pos[1]) {
|
||||||
|
posMin[1] = nodes[i].pos[1];
|
||||||
|
posMinIndexes[1] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
posMin = [nodes[i].pos[0], nodes[i].pos[1]];
|
||||||
|
posMinIndexes = [i, i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return posMin;
|
||||||
|
}
|
||||||
|
|
||||||
type ComfyGraphEvents = {
|
type ComfyGraphEvents = {
|
||||||
configured: (graph: LGraph) => void
|
configured: (graph: LGraph) => void
|
||||||
@@ -38,6 +63,10 @@ export default class ComfyGraph extends LGraph {
|
|||||||
return workflowState.getWorkflow(workflowID)
|
return workflowState.getWorkflow(workflowID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get layout(): WritableLayoutStateStore | null {
|
||||||
|
return this.workflow?.layout;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(workflowID?: WorkflowInstID) {
|
constructor(workflowID?: WorkflowInstID) {
|
||||||
super();
|
super();
|
||||||
this.workflowID = workflowID;
|
this.workflowID = workflowID;
|
||||||
@@ -218,4 +247,62 @@ export default class ComfyGraph extends LGraph {
|
|||||||
// console.debug("ConnectionChange", node);
|
// console.debug("ConnectionChange", node);
|
||||||
this.eventBus.emit("nodeConnectionChanged", kind, node, slot, targetNode, targetSlot);
|
this.eventBus.emit("nodeConnectionChanged", kind, node, slot, targetNode, targetSlot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Inserts a template.
|
||||||
|
* Layout deserialization must be handled afterwards!
|
||||||
|
*/
|
||||||
|
insertTemplate(template: SerializedComfyBoxTemplate, pos: Vector2): Record<NodeID, LGraphNode> {
|
||||||
|
const minPos = calculateMinPosOfNodes(template.nodes);
|
||||||
|
|
||||||
|
const templateNodeIDToNewNode: Record<NodeID, LGraphNode> = {}
|
||||||
|
|
||||||
|
var nodes = [];
|
||||||
|
for (var i = 0; i < template.nodes.length; ++i) {
|
||||||
|
var node_data = template.nodes[i];
|
||||||
|
var node = LiteGraph.createNode(node_data.type);
|
||||||
|
|
||||||
|
let mapping: GraphIDMapping = null;
|
||||||
|
if (node_data.type === "graph/subgraph") {
|
||||||
|
mapping = reassignGraphIDs((node_data as any).subgraph as SerializedLGraph);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node) {
|
||||||
|
const prevNodeId = node_data.id;
|
||||||
|
node_data.id = uuidv4();
|
||||||
|
templateNodeIDToNewNode[prevNodeId] = node
|
||||||
|
|
||||||
|
node.configure(node_data);
|
||||||
|
|
||||||
|
if (mapping) {
|
||||||
|
for (const subnode of (node as Subgraph).subgraph.iterateNodesInOrderRecursive()) {
|
||||||
|
const oldNodeID = mapping.nodeIDs[subnode.id];
|
||||||
|
templateNodeIDToNewNode[oldNodeID] = subnode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node.pos[0] += pos[0] - minPos[0]; //+= 5;
|
||||||
|
node.pos[1] += pos[1] - minPos[1]; //+= 5;
|
||||||
|
|
||||||
|
this.add(node, { doProcessChange: false, addedBy: "template" as any });
|
||||||
|
|
||||||
|
nodes.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//create links
|
||||||
|
for (var i = 0; i < template.links.length; ++i) {
|
||||||
|
var link_info = template.links[i];
|
||||||
|
var origin_node = templateNodeIDToNewNode[link_info[0]];
|
||||||
|
var target_node = templateNodeIDToNewNode[link_info[2]];
|
||||||
|
if (origin_node && target_node)
|
||||||
|
origin_node.connect(link_info[1], target_node, link_info[3]);
|
||||||
|
else
|
||||||
|
console.error("[ComfyGraphCanvas] nodes missing on template insertion!", link_info);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.afterChange();
|
||||||
|
|
||||||
|
return templateNodeIDToNewNode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,47 +3,22 @@ import { get, type Unsubscriber } from "svelte/store";
|
|||||||
import type ComfyGraph from "./ComfyGraph";
|
import type ComfyGraph from "./ComfyGraph";
|
||||||
import type ComfyApp from "./components/ComfyApp";
|
import type ComfyApp from "./components/ComfyApp";
|
||||||
import { ComfyReroute } from "./nodes";
|
import { ComfyReroute } from "./nodes";
|
||||||
import layoutStates from "./stores/layoutStates";
|
import layoutStates, { type ContainerLayout } from "./stores/layoutStates";
|
||||||
import queueState from "./stores/queueState";
|
import queueState from "./stores/queueState";
|
||||||
import selectionState from "./stores/selectionState";
|
import selectionState from "./stores/selectionState";
|
||||||
import templateState from "./stores/templateState";
|
import templateState from "./stores/templateState";
|
||||||
import { createTemplate, type ComfyBoxTemplate, serializeTemplate, type SerializedComfyBoxTemplate } from "./ComfyBoxTemplate";
|
import { createTemplate, type ComfyBoxTemplate, serializeTemplate, type SerializedComfyBoxTemplate } from "./ComfyBoxTemplate";
|
||||||
import notify from "./notify";
|
import notify from "./notify";
|
||||||
import { v4 as uuidv4 } from "uuid"
|
|
||||||
import { download } from "./utils";
|
|
||||||
|
|
||||||
export type SerializedGraphCanvasState = {
|
export type SerializedGraphCanvasState = {
|
||||||
offset: Vector2,
|
offset: Vector2,
|
||||||
scale: number
|
scale: number
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMinPos(nodes: SerializedLGraphNode[]): Vector2 {
|
|
||||||
var posMin: Vector2 = [0, 0]
|
|
||||||
var posMinIndexes: [number, number] | null = null;
|
|
||||||
|
|
||||||
for (var i = 0; i < nodes.length; ++i) {
|
|
||||||
if (posMin) {
|
|
||||||
if (posMin[0] > nodes[i].pos[0]) {
|
|
||||||
posMin[0] = nodes[i].pos[0];
|
|
||||||
posMinIndexes[0] = i;
|
|
||||||
}
|
|
||||||
if (posMin[1] > nodes[i].pos[1]) {
|
|
||||||
posMin[1] = nodes[i].pos[1];
|
|
||||||
posMinIndexes[1] = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
posMin = [nodes[i].pos[0], nodes[i].pos[1]];
|
|
||||||
posMinIndexes = [i, i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return posMin;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ComfyGraphCanvas extends LGraphCanvas {
|
export default class ComfyGraphCanvas extends LGraphCanvas {
|
||||||
app: ComfyApp | null;
|
app: ComfyApp | null;
|
||||||
private _unsubscribe: Unsubscriber;
|
private _unsubscribe: Unsubscriber;
|
||||||
|
isExportingSVG: boolean = false;
|
||||||
|
|
||||||
get comfyGraph(): ComfyGraph | null {
|
get comfyGraph(): ComfyGraph | null {
|
||||||
return this.graph as ComfyGraph;
|
return this.graph as ComfyGraph;
|
||||||
@@ -470,45 +445,21 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
|||||||
* Inserts a ComfyBox template. Logic is similar to pasting from the
|
* Inserts a ComfyBox template. Logic is similar to pasting from the
|
||||||
* clipboard in vanilla litegraph.
|
* clipboard in vanilla litegraph.
|
||||||
*/
|
*/
|
||||||
insertTemplate(template: SerializedComfyBoxTemplate, pos: Vector2) {
|
insertTemplate(template: SerializedComfyBoxTemplate, pos: Vector2, container: ContainerLayout, containerIndex: number): [LGraphNode[], IDragItem] {
|
||||||
const minPos = getMinPos(template.nodes);
|
const comfyGraph = this.graph as ComfyGraph;
|
||||||
|
|
||||||
const templateNodeIDToNewNode: Record<NodeID, LGraphNode> = {}
|
const layout = comfyGraph.layout;
|
||||||
|
if (layout == null) {
|
||||||
var nodes = [];
|
console.error("[ComfyGraphCanvas] graph has no layout!", comfyGraph)
|
||||||
for (var i = 0; i < template.nodes.length; ++i) {
|
return;
|
||||||
var node_data = template.nodes[i];
|
|
||||||
var node = LiteGraph.createNode(node_data.type);
|
|
||||||
if (node) {
|
|
||||||
const prevNodeId = node_data.id;
|
|
||||||
node_data.id = uuidv4();
|
|
||||||
templateNodeIDToNewNode[prevNodeId] = node
|
|
||||||
|
|
||||||
node.configure(node_data);
|
|
||||||
|
|
||||||
node.pos[0] += pos[0] - minPos[0]; //+= 5;
|
|
||||||
node.pos[1] += pos[1] - minPos[1]; //+= 5;
|
|
||||||
|
|
||||||
this.graph.add(node, { doProcessChange: false, addedBy: "template" as any });
|
|
||||||
|
|
||||||
nodes.push(node);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//create links
|
const nodeMapping = comfyGraph.insertTemplate(template, pos);
|
||||||
for (var i = 0; i < template.links.length; ++i) {
|
const templateLayoutRoot = layout.insertTemplate(template, comfyGraph, nodeMapping, container, containerIndex);
|
||||||
var link_info = template.links[i];
|
|
||||||
var origin_node = templateNodeIDToNewNode[link_info[0]];
|
|
||||||
var target_node = templateNodeIDToNewNode[link_info[2]];
|
|
||||||
if (origin_node && target_node)
|
|
||||||
origin_node.connect(link_info[1], target_node, link_info[3]);
|
|
||||||
else
|
|
||||||
console.error("[ComfyGraphCanvas] nodes missing on template insertion!", link_info);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectNodes(nodes);
|
this.selectNodes(Object.values(nodeMapping).filter(n => n.graph === this.graph));
|
||||||
|
|
||||||
this.graph.afterChange();
|
return [Object.values(nodeMapping), templateLayoutRoot]
|
||||||
}
|
}
|
||||||
|
|
||||||
saveAsTemplate(_value: IContextMenuItem, _options, mouseEvent, prevMenu, node?: LGraphNode) {
|
saveAsTemplate(_value: IContextMenuItem, _options, mouseEvent, prevMenu, node?: LGraphNode) {
|
||||||
@@ -593,4 +544,17 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
|||||||
|
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override onRenderBackground(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D): boolean {
|
||||||
|
if (this.isExportingSVG) {
|
||||||
|
ctx.clearRect(
|
||||||
|
this.visible_area[0],
|
||||||
|
this.visible_area[1],
|
||||||
|
this.visible_area[2],
|
||||||
|
this.visible_area[3]
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1013,7 +1013,8 @@ export default class ComfyApp {
|
|||||||
closeOnClick: false,
|
closeOnClick: false,
|
||||||
showCloseButton: false,
|
showCloseButton: false,
|
||||||
svelteProps: {
|
svelteProps: {
|
||||||
templateAndSvg
|
templateAndSvg,
|
||||||
|
editable: false
|
||||||
},
|
},
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { SerializedComfyBoxTemplate } from "$lib/ComfyBoxTemplate";
|
import { embedTemplateInSvg, type SerializedComfyBoxTemplate } from "$lib/ComfyBoxTemplate";
|
||||||
import templateState from "$lib/stores/templateState";
|
import templateState from "$lib/stores/templateState";
|
||||||
import uiState from "$lib/stores/uiState";
|
import uiState from "$lib/stores/uiState";
|
||||||
import { truncateString } from "$lib/utils";
|
import { download, truncateString } from "$lib/utils";
|
||||||
import type ComfyApp from "./ComfyApp";
|
import type ComfyApp from "./ComfyApp";
|
||||||
import { flip } from 'svelte/animate';
|
import { flip } from 'svelte/animate';
|
||||||
import {fade} from 'svelte/transition';
|
import {fade} from 'svelte/transition';
|
||||||
@@ -10,7 +10,10 @@
|
|||||||
import { dndzone, TRIGGERS, SHADOW_PLACEHOLDER_ITEM_ID, SHADOW_ITEM_MARKER_PROPERTY_NAME } from 'svelte-dnd-action';
|
import { dndzone, TRIGGERS, SHADOW_PLACEHOLDER_ITEM_ID, SHADOW_ITEM_MARKER_PROPERTY_NAME } from 'svelte-dnd-action';
|
||||||
import { defaultWidgetAttributes, type TemplateLayout } from "$lib/stores/layoutStates";
|
import { defaultWidgetAttributes, type TemplateLayout } from "$lib/stores/layoutStates";
|
||||||
import { v4 as uuidv4 } from "uuid"
|
import { v4 as uuidv4 } from "uuid"
|
||||||
import { writable } from "svelte/store";
|
import { get, writable } from "svelte/store";
|
||||||
|
import EditTemplateModal from "./modal/EditTemplateModal.svelte";
|
||||||
|
import modalState, { type ModalData } from "$lib/stores/modalState";
|
||||||
|
import notify from "$lib/notify";
|
||||||
|
|
||||||
export let app: ComfyApp
|
export let app: ComfyApp
|
||||||
|
|
||||||
@@ -71,6 +74,61 @@
|
|||||||
shouldIgnoreDndEvents = false;
|
shouldIgnoreDndEvents = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleClick(layout: TemplateLayout) {
|
||||||
|
const updateTemplate = (modal: ModalData) => {
|
||||||
|
const state = get(modal.state);
|
||||||
|
layout.template.metadata.title = state.name || layout.template.metadata.title
|
||||||
|
layout.template.metadata.author = state.author || layout.template.metadata.author
|
||||||
|
layout.template.metadata.description = state.description || layout.template.metadata.description
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveTemplate = (modal: ModalData) => {
|
||||||
|
updateTemplate(modal);
|
||||||
|
try {
|
||||||
|
templateState.update(layout.template);
|
||||||
|
notify("Saved template!", { type: "success" })
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
notify(`Failed to save template: ${error}`, { type: "error", timeout: 10000 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadTemplate = (modal: ModalData) => {
|
||||||
|
updateTemplate(modal);
|
||||||
|
const svg = embedTemplateInSvg(layout.template);
|
||||||
|
const title = layout.template.metadata.title || "template";
|
||||||
|
download(`${title}.svg`, svg, "image/svg+xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
modalState.pushModal({
|
||||||
|
svelteComponent: EditTemplateModal,
|
||||||
|
svelteProps: {
|
||||||
|
templateAndSvg: layout.template
|
||||||
|
},
|
||||||
|
showCloseButton: false,
|
||||||
|
closeOnClick: false,
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
name: "Save",
|
||||||
|
variant: "primary",
|
||||||
|
onClick: saveTemplate
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Download",
|
||||||
|
variant: "secondary",
|
||||||
|
onClick: downloadTemplate,
|
||||||
|
closeOnClick: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Close",
|
||||||
|
variant: "secondary",
|
||||||
|
onClick: () => {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="template-list">
|
<div class="template-list">
|
||||||
@@ -92,9 +150,10 @@
|
|||||||
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)}
|
||||||
<div class="template-entry" class:draggable>
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div class="template-name">{truncateString(item.template.metadata.title, 16)}</div>
|
<div class="template-entry" class:draggable on:click={() => handleClick(item)}>
|
||||||
<div class="template-desc">{truncateString(item.template.metadata.description, 24)}</div>
|
<div class="template-name">{item.template.metadata.title}</div>
|
||||||
|
<div class="template-desc">{item.template.metadata.description}</div>
|
||||||
</div>
|
</div>
|
||||||
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
|
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
|
||||||
<div in:fade={{duration:200, easing: cubicIn}} class='template-drag-item-shadow'/>
|
<div in:fade={{duration:200, easing: cubicIn}} class='template-drag-item-shadow'/>
|
||||||
@@ -140,6 +199,9 @@
|
|||||||
background: var(--panel-background-fill);
|
background: var(--panel-background-fill);
|
||||||
max-height: 14rem;
|
max-height: 14rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
font-size: 13pt;
|
font-size: 13pt;
|
||||||
.template-desc {
|
.template-desc {
|
||||||
@@ -147,11 +209,19 @@
|
|||||||
font-size: 11pt;
|
font-size: 11pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.draggable {
|
||||||
|
border: 5px dashed var(--secondary-500);
|
||||||
|
margin: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover:not(:has(img:hover)):not(:has(button:hover)) {
|
&:hover:not(:has(img:hover)):not(:has(button:hover)) {
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--block-background-fill);
|
||||||
|
|
||||||
&.draggable {
|
&.draggable {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
|
background: var(--secondary-700);
|
||||||
}
|
}
|
||||||
background: var(--block-background-fill);
|
|
||||||
|
|
||||||
&.running {
|
&.running {
|
||||||
background: var(--comfy-accent-soft);
|
background: var(--comfy-accent-soft);
|
||||||
|
|||||||
@@ -29,9 +29,11 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<svelte:fragment>
|
<svelte:fragment>
|
||||||
{#if modal != null && modal.svelteComponent != null}
|
<div class="modal-body">
|
||||||
<svelte:component this={modal.svelteComponent} {...modal.svelteProps} _modal={modal}/>
|
{#if modal != null && modal.svelteComponent != null}
|
||||||
{/if}
|
<svelte:component this={modal.svelteComponent} {...modal.svelteProps} _modal={modal}/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
<div slot="buttons" class="buttons" let:closeDialog>
|
<div slot="buttons" class="buttons" let:closeDialog>
|
||||||
{#if modal != null && modal.buttons?.length > 0}
|
{#if modal != null && modal.buttons?.length > 0}
|
||||||
@@ -52,6 +54,12 @@
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.buttons {
|
.buttons {
|
||||||
gap: var(--spacing-sm);
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -94,9 +94,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
padding-top: 0.5em;
|
padding-top: 0.5em;
|
||||||
}
|
gap: var(--spacing-md);
|
||||||
|
|
||||||
.button-row, .buttons {
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,35 +1,73 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import { setContext, createEventDispatcher } from 'svelte';
|
import { setContext, createEventDispatcher } from 'svelte';
|
||||||
import { fade } from 'svelte/transition';
|
import { key } from './menu.ts';
|
||||||
import { key } from './menu.ts';
|
|
||||||
|
|
||||||
export let x;
|
import { offset, flip, shift } from "svelte-floating-ui/dom";
|
||||||
export let y;
|
import { createFloatingActions, type ClientRectObject, type VirtualElement } from "svelte-floating-ui";
|
||||||
|
import { writable, type Writable } from 'svelte/store';
|
||||||
|
|
||||||
// whenever x and y is changed, restrict box to be within bounds
|
const [floatingRef, floatingContent] = createFloatingActions({
|
||||||
$: (() => {
|
placement: "right-start",
|
||||||
if (!menuEl) return;
|
strategy: "fixed",
|
||||||
|
middleware: [
|
||||||
|
offset({ mainAxis: 5, alignmentAxis: 4 }),
|
||||||
|
flip({
|
||||||
|
fallbackPlacements: ["left-start"]
|
||||||
|
}),
|
||||||
|
shift({ padding: 10 })
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
const rect = menuEl.getBoundingClientRect();
|
|
||||||
x = Math.min(window.innerWidth - rect.width, x);
|
|
||||||
if (y > window.innerHeight - rect.height) y -= rect.height;
|
|
||||||
})(x, y);
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
export let x;
|
||||||
|
export let y;
|
||||||
|
|
||||||
setContext(key, {
|
// whenever x and y is changed, restrict box to be within bounds
|
||||||
dispatchClick: () => dispatch('click')
|
$: (() => {
|
||||||
});
|
if (!menuEl) return;
|
||||||
|
|
||||||
let menuEl;
|
const rect = menuEl.getBoundingClientRect();
|
||||||
function onPageClick(e) {
|
x = Math.min(window.innerWidth - rect.width, x);
|
||||||
if (e.target === menuEl || menuEl.contains(e.target)) return;
|
if (y > window.innerHeight - rect.height) y -= rect.height;
|
||||||
dispatch('clickoutside');
|
})();
|
||||||
}
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
setContext(key, {
|
||||||
|
dispatchClick: () => dispatch('click')
|
||||||
|
});
|
||||||
|
|
||||||
|
let menuEl;
|
||||||
|
function onPageClick(e) {
|
||||||
|
if (e.target === menuEl || menuEl.contains(e.target)) return;
|
||||||
|
dispatch('clickoutside');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let getBoundingClientRect: () => ClientRectObject;
|
||||||
|
|
||||||
|
$: getBoundingClientRect = (): ClientRectObject => {
|
||||||
|
return {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
top: y,
|
||||||
|
left: x,
|
||||||
|
bottom: y,
|
||||||
|
right: x,
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const virtualElement: Writable<VirtualElement> = writable({ getBoundingClientRect })
|
||||||
|
|
||||||
|
$: virtualElement.set({ getBoundingClientRect })
|
||||||
|
|
||||||
|
floatingRef(virtualElement)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:body on:click={onPageClick} />
|
<svelte:body on:click={onPageClick} />
|
||||||
<div class="menu" bind:this={menuEl} style="top: {y}px; left: {x}px;">
|
<div class="menu" bind:this={menuEl} style="top: {y}px; left: {x}px;" use:floatingContent>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -8,16 +8,31 @@
|
|||||||
import Column from "../gradio/app/Column.svelte";
|
import Column from "../gradio/app/Column.svelte";
|
||||||
import Accordion from "../gradio/app/Accordion.svelte";
|
import Accordion from "../gradio/app/Accordion.svelte";
|
||||||
import Textbox from "@gradio/form/src/Textbox.svelte";
|
import Textbox from "@gradio/form/src/Textbox.svelte";
|
||||||
|
import type { ModalData } from "$lib/stores/modalState";
|
||||||
|
import { writable, type Writable } from "svelte/store";
|
||||||
const DOMPurify = createDOMPurify(window);
|
const DOMPurify = createDOMPurify(window);
|
||||||
|
|
||||||
export let templateAndSvg: SerializedComfyBoxTemplate;
|
export let templateAndSvg: SerializedComfyBoxTemplate;
|
||||||
|
export let editable: boolean = true;
|
||||||
|
export let _modal: ModalData;
|
||||||
let layout: SerializedLayoutState | null
|
let layout: SerializedLayoutState | null
|
||||||
let root: SerializedDragEntry | null
|
let root: SerializedDragEntry | null
|
||||||
|
let state: Writable<any> = writable({})
|
||||||
|
|
||||||
|
$: {
|
||||||
|
state = _modal.state;
|
||||||
|
if (!("name" in $state)) {
|
||||||
|
$state.name = templateAndSvg.metadata.title;
|
||||||
|
$state.author = templateAndSvg.metadata.author;
|
||||||
|
$state.description = templateAndSvg.metadata.description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let saneSvg: string = "";
|
let saneSvg: string = "";
|
||||||
|
|
||||||
$: saneSvg = templateAndSvg
|
$: saneSvg = templateAndSvg
|
||||||
? DOMPurify.sanitize(templateAndSvg.svg, { USE_PROFILES: { svg: true, svgFilters: true } })
|
? DOMPurify.sanitize(templateAndSvg.svg, { USE_PROFILES: { svg: true, svgFilters: true } })
|
||||||
|
.replace("<svg", "<svg style='background: url(\"image/graph-bg.png\")'")
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
$: if (templateAndSvg) {
|
$: if (templateAndSvg) {
|
||||||
@@ -42,9 +57,9 @@
|
|||||||
<Block>
|
<Block>
|
||||||
<BlockTitle>Metadata</BlockTitle>
|
<BlockTitle>Metadata</BlockTitle>
|
||||||
<div>
|
<div>
|
||||||
<Textbox label="Name" value="Text" lines={1} max_lines={1} />
|
<Textbox label="Name" disabled={!editable} bind:value={$state.name} lines={1} max_lines={1} />
|
||||||
<Textbox label="Author" value="Text" lines={1} max_lines={1} />
|
<Textbox label="Author" disabled={!editable} bind:value={$state.author} lines={1} max_lines={1} />
|
||||||
<Textbox label="Description" value="Text" lines={5} max_lines={5} />
|
<Textbox label="Description" disabled={!editable} bind:value={$state.description} lines={5} max_lines={5} />
|
||||||
</div>
|
</div>
|
||||||
</Block>
|
</Block>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,7 +90,7 @@
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.template-preview {
|
.template-preview {
|
||||||
width: 60vw;
|
width: 70vw;
|
||||||
height: 70vh;
|
height: 70vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
@@ -92,9 +107,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-rows {
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-layout-preview {
|
.template-layout-preview {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
{#if entry.dragItem.type === "container"}
|
{#if entry.dragItem.type === "container"}
|
||||||
<div class="layout-container">
|
<div class="layout-container">
|
||||||
<Block>
|
<Block>
|
||||||
<Accordion label={entry.dragItem.attrs.title || "(Container)"} open={true}>
|
<Accordion label={entry.dragItem.attrs.title || "(Container)"} open={false}>
|
||||||
{#each entry.children as childID}
|
{#each entry.children as childID}
|
||||||
{@const child = layout.allItems[childID]}
|
{@const child = layout.allItems[childID]}
|
||||||
<svelte:self {layout} entry={child} entryID={childID} />
|
<svelte:self {layout} entry={child} entryID={childID} />
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { type ContainerLayout, type IDragItem, type WritableLayoutStateStore } from "$lib/stores/layoutStates"
|
import type ComfyGraphCanvas from "$lib/ComfyGraphCanvas";
|
||||||
|
import { type ContainerLayout, type IDragItem, type TemplateLayout, type WritableLayoutStateStore } from "$lib/stores/layoutStates"
|
||||||
|
import type { LGraphCanvas } from "@litegraph-ts/core";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
export function handleContainerConsider(layoutState: WritableLayoutStateStore, container: ContainerLayout, evt: CustomEvent<DndEvent<IDragItem>>): IDragItem[] {
|
export function handleContainerConsider(layoutState: WritableLayoutStateStore, container: ContainerLayout, evt: CustomEvent<DndEvent<IDragItem>>): IDragItem[] {
|
||||||
return layoutState.updateChildren(container, evt.detail.items)
|
return layoutState.updateChildren(container, evt.detail.items)
|
||||||
@@ -11,9 +14,37 @@ export function handleContainerFinalize(layoutState: WritableLayoutStateStore, c
|
|||||||
const isDroppingTemplate = droppedItem?.type === "template"
|
const isDroppingTemplate = droppedItem?.type === "template"
|
||||||
|
|
||||||
if (isDroppingTemplate) {
|
if (isDroppingTemplate) {
|
||||||
return layoutState.updateChildren(container, dnd.items.filter(i => i.id !== info.id));
|
return doInsertTemplate(layoutState, droppedItem as TemplateLayout, container, dnd.items)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return layoutState.updateChildren(container, dnd.items)
|
return layoutState.updateChildren(container, dnd.items)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function isComfyGraphCanvas(canvas: LGraphCanvas): canvas is ComfyGraphCanvas {
|
||||||
|
return "insertTemplate" in canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
function doInsertTemplate(layoutState: WritableLayoutStateStore, droppedTemplate: TemplateLayout, container: ContainerLayout, items: IDragItem[]): IDragItem[] {
|
||||||
|
const workflow = layoutState.workflow;
|
||||||
|
const templateItemIndex = items.findIndex(i => i.id === droppedTemplate.id)
|
||||||
|
|
||||||
|
const newChildren = items.filter(i => i.id !== droppedTemplate.id);
|
||||||
|
|
||||||
|
const canvas = workflow.canvases["app"]?.canvas
|
||||||
|
if (canvas == null || !isComfyGraphCanvas(canvas) || canvas.graph !== workflow.graph) {
|
||||||
|
console.error("Couldn't get main graph canvas!")
|
||||||
|
return newChildren;
|
||||||
|
}
|
||||||
|
|
||||||
|
layoutState.updateChildren(container, newChildren);
|
||||||
|
|
||||||
|
const rect = canvas.ds.element.getBoundingClientRect();
|
||||||
|
const width = rect?.width || 1;
|
||||||
|
const height = rect?.height || 1;
|
||||||
|
const center = canvas.convertOffsetToCanvas([width * 0.5, height * 0.5]);
|
||||||
|
|
||||||
|
canvas.insertTemplate(droppedTemplate.template, center, container, templateItemIndex);
|
||||||
|
|
||||||
|
return get(layoutState).allItems[container.id].children;
|
||||||
|
}
|
||||||
|
|||||||
@@ -297,6 +297,9 @@ export default abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
notifyPropsChanged() {
|
notifyPropsChanged() {
|
||||||
|
if (!this.layoutState)
|
||||||
|
return;
|
||||||
|
|
||||||
const layoutEntry = this.layoutState.findLayoutEntryForNode(this.id as ComfyNodeID)
|
const layoutEntry = this.layoutState.findLayoutEntryForNode(this.id as ComfyNodeID)
|
||||||
if (layoutEntry && layoutEntry.parent) {
|
if (layoutEntry && layoutEntry.parent) {
|
||||||
layoutEntry.parent.attrsChanged.set(get(layoutEntry.parent.attrsChanged) + 1)
|
layoutEntry.parent.attrsChanged.set(get(layoutEntry.parent.attrsChanged) + 1)
|
||||||
|
|||||||
@@ -740,6 +740,7 @@ type LayoutStateOps = {
|
|||||||
updateChildren: (parent: IDragItem, children: IDragItem[]) => IDragItem[],
|
updateChildren: (parent: IDragItem, children: IDragItem[]) => IDragItem[],
|
||||||
nodeAdded: (node: LGraphNode, options: LGraphAddNodeOptions) => void,
|
nodeAdded: (node: LGraphNode, options: LGraphAddNodeOptions) => void,
|
||||||
nodeRemoved: (node: LGraphNode, options: LGraphRemoveNodeOptions) => void,
|
nodeRemoved: (node: LGraphNode, options: LGraphRemoveNodeOptions) => void,
|
||||||
|
insertTemplate: (template: SerializedComfyBoxTemplate, graph: LGraph, templateNodeIDToNode: Record<NodeID, LGraphNode>, container: ContainerLayout, childIndex: number) => IDragItem,
|
||||||
moveItem: (target: IDragItem, to: ContainerLayout, index?: number) => void,
|
moveItem: (target: IDragItem, to: ContainerLayout, index?: number) => void,
|
||||||
groupItems: (dragItemIDs: DragItemID[], attrs?: Partial<Attributes>) => ContainerLayout,
|
groupItems: (dragItemIDs: DragItemID[], attrs?: Partial<Attributes>) => ContainerLayout,
|
||||||
ungroup: (container: ContainerLayout) => void,
|
ungroup: (container: ContainerLayout) => void,
|
||||||
@@ -768,7 +769,7 @@ export type SerializedDragEntry = {
|
|||||||
export type SerializedDragItem = {
|
export type SerializedDragItem = {
|
||||||
type: string,
|
type: string,
|
||||||
id: DragItemID,
|
id: DragItemID,
|
||||||
nodeId: UUID | null,
|
nodeId: NodeID | null,
|
||||||
attrs: Attributes
|
attrs: Attributes
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -928,7 +929,11 @@ function createRaw(workflow: ComfyBoxWorkflow | null = null): WritableLayoutStat
|
|||||||
|
|
||||||
let attrs: Partial<Attributes> = {}
|
let attrs: Partial<Attributes> = {}
|
||||||
|
|
||||||
if (options.addedBy === "moveIntoSubgraph" || options.addedBy === "moveOutOfSubgraph") {
|
if ((options.addedBy as any) === "template") {
|
||||||
|
// Template layout will be deserialized shortly
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (options.addedBy === "moveIntoSubgraph" || options.addedBy === "moveOutOfSubgraph") {
|
||||||
// All we need to do is update the nodeID linked to this node.
|
// All we need to do is update the nodeID linked to this node.
|
||||||
const item = state.allItemsByNode[options.prevNodeID]
|
const item = state.allItemsByNode[options.prevNodeID]
|
||||||
delete state.allItemsByNode[options.prevNodeID]
|
delete state.allItemsByNode[options.prevNodeID]
|
||||||
@@ -1000,6 +1005,53 @@ function createRaw(workflow: ComfyBoxWorkflow | null = null): WritableLayoutStat
|
|||||||
store.set(state)
|
store.set(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function insertTemplate(template: SerializedComfyBoxTemplate, graph: LGraph, templateNodeIDToNode: Record<NodeID, LGraphNode>, container: ContainerLayout, childIndex: number): IDragItem {
|
||||||
|
const idMapping: Record<DragItemID, DragItemID> = {};
|
||||||
|
|
||||||
|
const getDragItemID = (id: DragItemID): DragItemID => {
|
||||||
|
idMapping[id] ||= uuidv4();
|
||||||
|
return idMapping[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure all IDs are unique, and rewrite node IDs in widgets to point
|
||||||
|
// to newly created nodes
|
||||||
|
for (const [id, entry] of Object.entries(template.layout.allItems)) {
|
||||||
|
const newId = getDragItemID(id);
|
||||||
|
template.layout.allItems[newId] = entry;
|
||||||
|
entry.dragItem.id = newId;
|
||||||
|
|
||||||
|
if (entry.parent)
|
||||||
|
entry.parent = getDragItemID(entry.parent)
|
||||||
|
entry.children = entry.children.map(getDragItemID);
|
||||||
|
|
||||||
|
if (entry.dragItem.type === "widget") {
|
||||||
|
entry.dragItem.nodeId = templateNodeIDToNode[entry.dragItem.nodeId].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (template.layout.root) {
|
||||||
|
template.layout.root = getDragItemID(template.layout.root)
|
||||||
|
|
||||||
|
// make sure the new root doesn't have a parent since that parent
|
||||||
|
// was detached from the serialized layout and won't be found in
|
||||||
|
// template.layout.allItems
|
||||||
|
template.layout.allItems[template.layout.root].parent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = deserializeRaw(template.layout, graph);
|
||||||
|
|
||||||
|
// merge the template's detached layout tree into this layout
|
||||||
|
store.update(s => {
|
||||||
|
s.allItems = { ...s.allItems, ...raw.allItems }
|
||||||
|
s.allItemsByNode = { ...s.allItemsByNode, ...raw.allItemsByNode }
|
||||||
|
return s;
|
||||||
|
})
|
||||||
|
|
||||||
|
moveItem(raw.root, container, childIndex);
|
||||||
|
|
||||||
|
return raw.root
|
||||||
|
}
|
||||||
|
|
||||||
function moveItem(target: IDragItem, to: ContainerLayout, index?: number) {
|
function moveItem(target: IDragItem, to: ContainerLayout, index?: number) {
|
||||||
const state = get(store)
|
const state = get(store)
|
||||||
const entry = state.allItems[target.id]
|
const entry = state.allItems[target.id]
|
||||||
@@ -1164,6 +1216,13 @@ function createRaw(workflow: ComfyBoxWorkflow | null = null): WritableLayoutStat
|
|||||||
const queue = [state.allItems[rootID]]
|
const queue = [state.allItems[rootID]]
|
||||||
while (queue.length > 0) {
|
while (queue.length > 0) {
|
||||||
const entry = queue.shift();
|
const entry = queue.shift();
|
||||||
|
|
||||||
|
if (entry.dragItem.type === "template") {
|
||||||
|
// If this happens then there's a bug somewhere
|
||||||
|
console.error("[layoutState] Found template drag item in current layout, skipping!")
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
allItems[entry.dragItem.id] = {
|
allItems[entry.dragItem.id] = {
|
||||||
dragItem: {
|
dragItem: {
|
||||||
type: entry.dragItem.type,
|
type: entry.dragItem.type,
|
||||||
@@ -1193,6 +1252,13 @@ function createRaw(workflow: ComfyBoxWorkflow | null = null): WritableLayoutStat
|
|||||||
const allItems: Record<DragItemID, SerializedDragEntry> = {}
|
const allItems: Record<DragItemID, SerializedDragEntry> = {}
|
||||||
for (const pair of Object.entries(state.allItems)) {
|
for (const pair of Object.entries(state.allItems)) {
|
||||||
const [id, entry] = pair;
|
const [id, entry] = pair;
|
||||||
|
|
||||||
|
if (entry.dragItem.type === "template") {
|
||||||
|
// If this happens then there's a bug somewhere
|
||||||
|
console.error("[layoutState] Found template drag item in current layout, skipping!")
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
allItems[id] = {
|
allItems[id] = {
|
||||||
dragItem: {
|
dragItem: {
|
||||||
type: entry.dragItem.type,
|
type: entry.dragItem.type,
|
||||||
@@ -1211,12 +1277,19 @@ function createRaw(workflow: ComfyBoxWorkflow | null = null): WritableLayoutStat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function deserialize(data: SerializedLayoutState, graph: LGraph) {
|
function deserializeRaw(data: SerializedLayoutState, graph: LGraph): LayoutState {
|
||||||
const allItems: Record<DragItemID, DragItemEntry> = {}
|
const allItems: Record<DragItemID, DragItemEntry> = {}
|
||||||
const allItemsByNode: Record<number, DragItemEntry> = {}
|
const allItemsByNode: Record<number, DragItemEntry> = {}
|
||||||
|
|
||||||
for (const pair of Object.entries(data.allItems)) {
|
for (const pair of Object.entries(data.allItems)) {
|
||||||
const [id, entry] = pair;
|
const [id, entry] = pair;
|
||||||
|
|
||||||
|
if (entry.dragItem.type === "template") {
|
||||||
|
// If this happens then there's a bug somewhere
|
||||||
|
console.error("[layoutState] Found template drag item in serialized layout, skipping!")
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const dragItem: IDragItem = {
|
const dragItem: IDragItem = {
|
||||||
type: entry.dragItem.type,
|
type: entry.dragItem.type,
|
||||||
id: entry.dragItem.id,
|
id: entry.dragItem.id,
|
||||||
@@ -1265,6 +1338,12 @@ function createRaw(workflow: ComfyBoxWorkflow | null = null): WritableLayoutStat
|
|||||||
isConfiguring: false,
|
isConfiguring: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
function deserialize(data: SerializedLayoutState, graph: LGraph) {
|
||||||
|
const state = deserializeRaw(data, graph);
|
||||||
|
|
||||||
console.debug("[layoutState] deserialize", data, state, defaultWorkflowAttributes)
|
console.debug("[layoutState] deserialize", data, state, defaultWorkflowAttributes)
|
||||||
|
|
||||||
store.set(state)
|
store.set(state)
|
||||||
@@ -1298,6 +1377,7 @@ function createRaw(workflow: ComfyBoxWorkflow | null = null): WritableLayoutStat
|
|||||||
updateChildren,
|
updateChildren,
|
||||||
nodeAdded,
|
nodeAdded,
|
||||||
nodeRemoved,
|
nodeRemoved,
|
||||||
|
insertTemplate,
|
||||||
moveItem,
|
moveItem,
|
||||||
groupItems,
|
groupItems,
|
||||||
findLayoutEntryForNode,
|
findLayoutEntryForNode,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { SerializedComfyBoxTemplate } from '$lib/ComfyBoxTemplate';
|
|||||||
import type { UUID } from '@litegraph-ts/core';
|
import type { UUID } from '@litegraph-ts/core';
|
||||||
import { get, writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
import type { Readable, Writable } from 'svelte/store';
|
import type { Readable, Writable } from 'svelte/store';
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
export type TemplateState = {
|
export type TemplateState = {
|
||||||
templates: SerializedComfyBoxTemplate[]
|
templates: SerializedComfyBoxTemplate[]
|
||||||
@@ -12,6 +13,7 @@ type TemplateStateOps = {
|
|||||||
save: () => void,
|
save: () => void,
|
||||||
load: () => void,
|
load: () => void,
|
||||||
add: (template: SerializedComfyBoxTemplate) => boolean,
|
add: (template: SerializedComfyBoxTemplate) => boolean,
|
||||||
|
update: (template: SerializedComfyBoxTemplate) => boolean,
|
||||||
remove: (templateID: UUID) => boolean,
|
remove: (templateID: UUID) => boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +59,30 @@ function remove(templateID: UUID): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function update(template: SerializedComfyBoxTemplate): boolean {
|
||||||
|
const state = get(store);
|
||||||
|
if (!state.templatesByID[template.id]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
store.update(s => {
|
||||||
|
const oldId = template.id
|
||||||
|
const index = s.templates.findIndex(t => t.id === oldId)
|
||||||
|
s.templates.splice(index, 1);
|
||||||
|
delete s.templatesByID[oldId];
|
||||||
|
|
||||||
|
template.id = uuidv4();
|
||||||
|
|
||||||
|
s.templates.push(template);
|
||||||
|
s.templatesByID[template.id] = template;
|
||||||
|
return s;
|
||||||
|
})
|
||||||
|
|
||||||
|
save();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
const json = JSON.stringify(get(store).templates)
|
const json = JSON.stringify(get(store).templates)
|
||||||
localStorage.setItem("templates", json)
|
localStorage.setItem("templates", json)
|
||||||
@@ -95,6 +121,7 @@ const templateStateStore: WritableTemplateStateStore =
|
|||||||
...store,
|
...store,
|
||||||
add,
|
add,
|
||||||
remove,
|
remove,
|
||||||
|
update,
|
||||||
save,
|
save,
|
||||||
load,
|
load,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ body {
|
|||||||
border-color: var(--comfy-disabled-textbox-border-color);
|
border-color: var(--comfy-disabled-textbox-border-color);
|
||||||
box-shadow: 0 0 0 var(--shadow-spread) transparent, rgba(0, 0, 0, 0.08) 0px 2px 4px 0px inset;
|
box-shadow: 0 0 0 var(--shadow-spread) transparent, rgba(0, 0, 0, 0.08) 0px 2px 4px 0px inset;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
color: var(--comfy-disabled-label-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin disable-inputs {
|
@mixin disable-inputs {
|
||||||
|
|||||||
@@ -6,13 +6,17 @@ import FullReload from 'vite-plugin-full-reload';
|
|||||||
import { viteStaticCopy } from 'vite-plugin-static-copy'
|
import { viteStaticCopy } from 'vite-plugin-static-copy'
|
||||||
import removeConsole from 'vite-plugin-svelte-console-remover';
|
import removeConsole from 'vite-plugin-svelte-console-remover';
|
||||||
import glsl from 'vite-plugin-glsl';
|
import glsl from 'vite-plugin-glsl';
|
||||||
|
import { execSync } from "child_process"
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV === "production";
|
const isProduction = process.env.NODE_ENV === "production";
|
||||||
console.log("Production build: " + isProduction)
|
console.log("Production build: " + isProduction)
|
||||||
|
|
||||||
|
const commitHash = execSync('git rev-parse HEAD').toString();
|
||||||
|
console.log("Commit: " + commitHash)
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
define: {
|
define: {
|
||||||
"__GIT_COMMIT_HASH__": '"test"'
|
"__GIT_COMMIT_HASH__": JSON.stringify(commitHash)
|
||||||
},
|
},
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
base: "./",
|
base: "./",
|
||||||
|
|||||||
Reference in New Issue
Block a user