diff --git a/litegraph b/litegraph index a1bf4cb..fd0c428 160000 --- a/litegraph +++ b/litegraph @@ -1 +1 @@ -Subproject commit a1bf4cb511c46831b371d79d1495b76052beecce +Subproject commit fd0c428237523606d0a339a81c6e244b3c306790 diff --git a/package.json b/package.json index 4df6182..aa66494 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "style-mod": "^4.0.3", "svelte-bootstrap-icons": "^2.3.1", "svelte-feather-icons": "^4.0.0", + "svelte-floating-ui": "^1.5.2", "svelte-preprocess": "^5.0.3", "svelte-select": "^5.5.3", "svelte-splitpanes": "^0.7.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7cfc57..8314f4f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -151,6 +151,9 @@ importers: svelte-feather-icons: specifier: ^4.0.0 version: 4.0.0 + svelte-floating-ui: + specifier: ^1.5.2 + version: 1.5.2 svelte-preprocess: specifier: ^5.0.3 version: 5.0.3(sass@1.61.0)(svelte@3.58.0)(typescript@5.0.3) @@ -8135,6 +8138,13 @@ packages: '@floating-ui/dom': 1.2.8 dev: false + /svelte-floating-ui@1.5.2: + resolution: {integrity: sha512-nV50eno74CEsFfFJ6iyN/oNYDEOck1TZjGV3lmJksVRbiiUAVF6bHspyAhR7GZ7c/4qbRWp9UyX24J+UXdEpag==} + dependencies: + '@floating-ui/core': 1.2.6 + '@floating-ui/dom': 1.2.8 + dev: false + /svelte-hmr@0.15.1(svelte@3.58.0): resolution: {integrity: sha512-BiKB4RZ8YSwRKCNVdNxK/GfY+r4Kjgp9jCLEy0DuqAKfmQtpL38cQK3afdpjw4sqSs4PLi3jIPJIFp259NkZtA==} engines: {node: ^12.20 || ^14.13.1 || >= 16} diff --git a/public/image/graph-bg.png b/public/image/graph-bg.png new file mode 100644 index 0000000..2feb956 Binary files /dev/null and b/public/image/graph-bg.png differ diff --git a/src/lib/ComfyBoxTemplate.ts b/src/lib/ComfyBoxTemplate.ts index fdb23f4..d6355ca 100644 --- a/src/lib/ComfyBoxTemplate.ts +++ b/src/lib/ComfyBoxTemplate.ts @@ -40,6 +40,7 @@ export type SerializedComfyBoxTemplate = { isComfyBoxTemplate: true, version: 1, id: UUID, + commitHash: string, /* * Serialized metadata @@ -182,6 +183,7 @@ function renderSvg(canvas: ComfyGraphCanvas, graph: LGraph, padding: number, ext const offset = canvas.ds.offset; const show_info = canvas.show_info; const background_image = canvas.background_image; + const clear_background = canvas.clear_background; const render_canvas_border = canvas.render_canvas_border; const render_subgraph_panels = canvas.render_subgraph_panels const render_subgraph_stack_header = canvas.render_subgraph_stack_header @@ -189,6 +191,7 @@ function renderSvg(canvas: ComfyGraphCanvas, graph: LGraph, padding: number, ext canvas.openSubgraph(graph) canvas.show_info = false; canvas.background_image = null; + canvas.clear_background = false; canvas.render_canvas_border = false; canvas.render_subgraph_panels = false; canvas.render_subgraph_stack_header = false; @@ -233,12 +236,10 @@ function renderSvg(canvas: ComfyGraphCanvas, graph: LGraph, padding: number, ext canvas.ds.offset = [-bounds[0], -bounds[1]]; canvas.ctx = svgCtx; - let saving = false; - // Trigger saving - saving = true; + canvas.isExportingSVG = true; canvas.draw(true, true); - saving = false; + canvas.isExportingSVG = false; // Restore original settings canvas.closeSubgraph(); @@ -248,6 +249,7 @@ function renderSvg(canvas: ComfyGraphCanvas, graph: LGraph, padding: number, ext canvas.ds.offset = offset; canvas.ctx = ctx; canvas.show_info = show_info; + canvas.clear_background = clear_background; canvas.background_image = background_image; canvas.render_canvas_border = render_canvas_border; canvas.render_subgraph_panels = render_subgraph_panels; @@ -323,6 +325,7 @@ export function serializeTemplate(canvas: ComfyGraphCanvas, template: ComfyBoxTe const serTemplate: SerializedComfyBoxTemplate = { isComfyBoxTemplate: true, version: 1, + commitHash: __GIT_COMMIT_HASH__, id: template.id, metadata, nodes, diff --git a/src/lib/ComfyGraph.ts b/src/lib/ComfyGraph.ts index d159df6..cf6cb34 100644 --- a/src/lib/ComfyGraph.ts +++ b/src/lib/ComfyGraph.ts @@ -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 EventEmitter from "events"; import type TypedEmitter from "typed-emitter"; @@ -14,6 +14,31 @@ import layoutStates from "./stores/layoutStates"; import type { ComfyBoxWorkflow, WorkflowInstID } from "./stores/workflowState"; import workflowState from "./stores/workflowState"; 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 = { configured: (graph: LGraph) => void @@ -38,6 +63,10 @@ export default class ComfyGraph extends LGraph { return workflowState.getWorkflow(workflowID) } + get layout(): WritableLayoutStateStore | null { + return this.workflow?.layout; + } + constructor(workflowID?: WorkflowInstID) { super(); this.workflowID = workflowID; @@ -218,4 +247,62 @@ export default class ComfyGraph extends LGraph { // console.debug("ConnectionChange", node); this.eventBus.emit("nodeConnectionChanged", kind, node, slot, targetNode, targetSlot); } + + /* + * Inserts a template. + * Layout deserialization must be handled afterwards! + */ + insertTemplate(template: SerializedComfyBoxTemplate, pos: Vector2): Record { + const minPos = calculateMinPosOfNodes(template.nodes); + + const templateNodeIDToNewNode: Record = {} + + 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; + } } diff --git a/src/lib/ComfyGraphCanvas.ts b/src/lib/ComfyGraphCanvas.ts index af302c9..c5738dd 100644 --- a/src/lib/ComfyGraphCanvas.ts +++ b/src/lib/ComfyGraphCanvas.ts @@ -3,47 +3,22 @@ import { get, type Unsubscriber } from "svelte/store"; import type ComfyGraph from "./ComfyGraph"; import type ComfyApp from "./components/ComfyApp"; import { ComfyReroute } from "./nodes"; -import layoutStates from "./stores/layoutStates"; +import layoutStates, { type ContainerLayout } from "./stores/layoutStates"; import queueState from "./stores/queueState"; import selectionState from "./stores/selectionState"; import templateState from "./stores/templateState"; import { createTemplate, type ComfyBoxTemplate, serializeTemplate, type SerializedComfyBoxTemplate } from "./ComfyBoxTemplate"; import notify from "./notify"; -import { v4 as uuidv4 } from "uuid" -import { download } from "./utils"; export type SerializedGraphCanvasState = { offset: Vector2, 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 { app: ComfyApp | null; private _unsubscribe: Unsubscriber; + isExportingSVG: boolean = false; get comfyGraph(): ComfyGraph | null { 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 * clipboard in vanilla litegraph. */ - insertTemplate(template: SerializedComfyBoxTemplate, pos: Vector2) { - const minPos = getMinPos(template.nodes); + insertTemplate(template: SerializedComfyBoxTemplate, pos: Vector2, container: ContainerLayout, containerIndex: number): [LGraphNode[], IDragItem] { + const comfyGraph = this.graph as ComfyGraph; - const templateNodeIDToNewNode: Record = {} - - var nodes = []; - for (var i = 0; i < template.nodes.length; ++i) { - 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); - } + const layout = comfyGraph.layout; + if (layout == null) { + console.error("[ComfyGraphCanvas] graph has no layout!", comfyGraph) + return; } - //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); - } + const nodeMapping = comfyGraph.insertTemplate(template, pos); + const templateLayoutRoot = layout.insertTemplate(template, comfyGraph, nodeMapping, container, containerIndex); - 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) { @@ -593,4 +544,17 @@ export default class ComfyGraphCanvas extends LGraphCanvas { return options } + + override onRenderBackground(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D): boolean { + if (this.isExportingSVG) { + ctx.clearRect( + this.visible_area[0], + this.visible_area[1], + this.visible_area[2], + this.visible_area[3] + ); + return true; + } + return false; + } } diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index b0b1b8e..9d9ce64 100644 --- a/src/lib/components/ComfyApp.ts +++ b/src/lib/components/ComfyApp.ts @@ -1013,7 +1013,8 @@ export default class ComfyApp { closeOnClick: false, showCloseButton: false, svelteProps: { - templateAndSvg + templateAndSvg, + editable: false }, buttons: [ { diff --git a/src/lib/components/ComfyTemplates.svelte b/src/lib/components/ComfyTemplates.svelte index 2b1bdeb..f9daa0d 100644 --- a/src/lib/components/ComfyTemplates.svelte +++ b/src/lib/components/ComfyTemplates.svelte @@ -1,8 +1,8 @@
@@ -92,9 +150,10 @@ on:consider={handleDndConsider} on:finalize={handleDndFinalize}> {#each _sorted.filter(i => i.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)} -
-
{truncateString(item.template.metadata.title, 16)}
-
{truncateString(item.template.metadata.description, 24)}
+ +
handleClick(item)}> +
{item.template.metadata.title}
+
{item.template.metadata.description}
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
@@ -140,6 +199,9 @@ background: var(--panel-background-fill); max-height: 14rem; position: relative; + user-select: none; + text-overflow: ellipsis; + overflow: hidden; font-size: 13pt; .template-desc { @@ -147,11 +209,19 @@ font-size: 11pt; } + &.draggable { + border: 5px dashed var(--secondary-500); + margin: 0.2em; + } + &:hover:not(:has(img:hover)):not(:has(button:hover)) { + cursor: pointer; + background: var(--block-background-fill); + &.draggable { cursor: grab; + background: var(--secondary-700); } - background: var(--block-background-fill); &.running { background: var(--comfy-accent-soft); diff --git a/src/lib/components/GlobalModal.svelte b/src/lib/components/GlobalModal.svelte index 01bbe5e..25e76bd 100644 --- a/src/lib/components/GlobalModal.svelte +++ b/src/lib/components/GlobalModal.svelte @@ -29,9 +29,11 @@ {/if}
- {#if modal != null && modal.svelteComponent != null} - - {/if} +
{#if modal != null && modal.buttons?.length > 0} @@ -52,6 +54,12 @@ diff --git a/src/lib/components/Modal.svelte b/src/lib/components/Modal.svelte index fe6a6b9..63f82a5 100644 --- a/src/lib/components/Modal.svelte +++ b/src/lib/components/Modal.svelte @@ -94,9 +94,6 @@ display: flex; flex-direction: row; padding-top: 0.5em; - } - - .button-row, .buttons { - gap: var(--spacing-sm); + gap: var(--spacing-md); } diff --git a/src/lib/components/menu/Menu.svelte b/src/lib/components/menu/Menu.svelte index 6322a94..34deacf 100644 --- a/src/lib/components/menu/Menu.svelte +++ b/src/lib/components/menu/Menu.svelte @@ -1,35 +1,73 @@ - - @@ -75,7 +90,7 @@