diff --git a/src/lib/ComfyGraphCanvas.ts b/src/lib/ComfyGraphCanvas.ts index b63f9a5..72a7107 100644 --- a/src/lib/ComfyGraphCanvas.ts +++ b/src/lib/ComfyGraphCanvas.ts @@ -1,4 +1,4 @@ -import { BuiltInSlotShape, ContextMenu, LGraphCanvas, LGraphNode, LLink, LiteGraph, NodeMode, Subgraph, TitleMode, type ContextMenuItem, type IContextMenuItem, type MouseEventExt, type NodeID, type Vector2, type Vector4, LGraph } from "@litegraph-ts/core"; +import { BuiltInSlotShape, ContextMenu, LGraphCanvas, LGraphNode, LLink, LiteGraph, NodeMode, Subgraph, TitleMode, type ContextMenuItem, type IContextMenuItem, type MouseEventExt, type NodeID, type Vector2, type Vector4, LGraph, type SlotIndex, type SlotNameOrIndex } from "@litegraph-ts/core"; import { get, type Unsubscriber } from "svelte/store"; import { createTemplate, serializeTemplate, type ComfyBoxTemplate, type SerializedComfyBoxTemplate } from "./ComfyBoxTemplate"; import type ComfyGraph from "./ComfyGraph"; @@ -24,11 +24,19 @@ export default class ComfyGraphCanvas extends LGraphCanvas { activeErrors?: ComfyGraphErrors = null; blinkError: ComfyGraphErrorLocation | null = null; blinkErrorTime: number = 0; + highlightNodeAndInput: [LGraphNode, number] | null = null; get comfyGraph(): ComfyGraph | null { return this.graph as ComfyGraph; } + clearErrors() { + this.activeErrors = null; + this.blinkError = null; + this.blinkErrorTime = 0; + this.highlightNodeAndInput = null; + } + constructor( app: ComfyApp, canvas: HTMLCanvasElement | string, @@ -97,6 +105,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas { const isRunningNode = node.id == state.runningNodeID const nodeErrors = this.activeErrors?.errorsByID[node.id]; + const isHighlightedNode = this.highlightNodeAndInput && this.highlightNodeAndInput[0].id === node.id; if (this.blinkErrorTime > 0) { this.blinkErrorTime -= this.graph.elapsed_time; @@ -126,6 +135,10 @@ export default class ComfyGraphCanvas extends LGraphCanvas { } thickness = 2 } + else if (isHighlightedNode) { + color = "cyan"; + thickness = 2 + } if (blink) { if (nodeErrors && nodeErrors.includes(this.blinkError) && this.blinkErrorTime > 0) { @@ -146,13 +159,28 @@ export default class ComfyGraphCanvas extends LGraphCanvas { } if (nodeErrors) { - this.drawFailedValidationInputs(node, nodeErrors, ctx); + this.drawFailedValidationInputs(node, nodeErrors, color, ctx); + } + + if (isHighlightedNode) { + let draw = true; + if (this.blinkErrorTime > 0) { + if ((Math.floor(this.blinkErrorTime / 2)) % 2 === 0) { + draw = false; + } + } + if (draw) { + const [node, inputSlot] = this.highlightNodeAndInput; + ctx.lineWidth = 2; + ctx.strokeStyle = color; + this.highlightNodeInput(node, inputSlot, ctx); + } } } - private drawFailedValidationInputs(node: LGraphNode, errors: ComfyGraphErrorLocation[], ctx: CanvasRenderingContext2D) { + private drawFailedValidationInputs(node: LGraphNode, errors: ComfyGraphErrorLocation[], color: string, ctx: CanvasRenderingContext2D) { ctx.lineWidth = 2; - ctx.strokeStyle = "red"; + ctx.strokeStyle = color || "red"; for (const errorLocation of errors) { if (errorLocation.input != null) { if (errorLocation === this.blinkError && this.blinkErrorTime > 0) { @@ -160,17 +188,25 @@ export default class ComfyGraphCanvas extends LGraphCanvas { continue; } } - const inputIndex = node.findInputSlotIndexByName(errorLocation.input.name) - if (inputIndex !== -1) { - let pos = node.getConnectionPos(true, inputIndex); - ctx.beginPath(); - ctx.arc(pos[0] - node.pos[0], pos[1] - node.pos[1], 12, 0, 2 * Math.PI, false) - ctx.stroke(); - } + this.highlightNodeInput(node, errorLocation.input.name, ctx); } } } + private highlightNodeInput(node: LGraphNode, inputSlot: SlotNameOrIndex, ctx: CanvasRenderingContext2D) { + let inputIndex: number; + if (typeof inputSlot === "number") + inputIndex = inputSlot + else + inputIndex = node.findInputSlotIndexByName(inputSlot) + if (inputIndex !== -1) { + let pos = node.getConnectionPos(true, inputIndex); + ctx.beginPath(); + ctx.arc(pos[0] - node.pos[0], pos[1] - node.pos[1], 12, 0, 2 * Math.PI, false) + ctx.stroke(); + } + } + private drawNodeOutline(node: LGraphNode, ctx: CanvasRenderingContext2D, size: Vector2, mouseOver: boolean, fgColor: string, bgColor: string, outlineColor: string, outlineThickness: number) { const shape = node.shape || BuiltInSlotShape.ROUND_SHAPE; @@ -656,22 +692,28 @@ export default class ComfyGraphCanvas extends LGraphCanvas { return } - this.closeAllSubgraphs(); - - const subgraphs: LGraph[] = [] - let node_ = node; - while (node_.graph._is_subgraph) { - subgraphs.push(node.graph); - node_ = node.graph._subgraph_node - } - - for (const subgraph of subgraphs) { - this.openSubgraph(subgraph) - } - - this.centerOnNode(node); + this.jumpToNode(node) + this.highlightNodeAndInput = null; this.blinkError = error; this.blinkErrorTime = 20; } + + jumpToNode(node: LGraphNode) { + this.closeAllSubgraphs(); + + const subgraphs = Array.from(node.iterateParentSubgraphNodes()).reverse(); + + for (const subgraph of subgraphs) { + this.openSubgraph(subgraph.subgraph) + } + + this.centerOnNode(node); + } + + jumpToNodeAndInput(node: LGraphNode, slotIndex: number) { + this.jumpToNode(node); + this.highlightNodeAndInput = [node, slotIndex]; + this.blinkErrorTime = 20; + } } diff --git a/src/lib/apiErrors.ts b/src/lib/apiErrors.ts index 5a26ee0..f027f94 100644 --- a/src/lib/apiErrors.ts +++ b/src/lib/apiErrors.ts @@ -5,6 +5,7 @@ import type { SerializedPromptInputLink } from "./components/ComfyApp" import type { WorkflowError, WorkflowInstID } from "./stores/workflowState" import { exclude_internal_props } from "svelte/internal" import type ComfyGraphCanvas from "./ComfyGraphCanvas" +import type { QueueEntry } from "./stores/queueState" enum ComfyPromptErrorType { NoOutputs = "prompt_no_outputs", @@ -165,6 +166,7 @@ export type ComfyGraphErrorLocation = { errorType: ComfyNodeErrorType | "execution", message: string, dependentOutputs: NodeID[], + queueEntry: QueueEntry, input?: ComfyGraphErrorInput, @@ -182,7 +184,7 @@ export type ComfyGraphErrors = { errorsByID: Record } -export function validationErrorToGraphErrors(workflowID: WorkflowInstID, validationError: ComfyAPIPromptErrorResponse): ComfyGraphErrors { +export function validationErrorToGraphErrors(workflowID: WorkflowInstID, validationError: ComfyAPIPromptErrorResponse, queueEntry: QueueEntry): ComfyGraphErrors { const errorsByID: Record = {} for (const [nodeID, nodeErrors] of Object.entries(validationError.node_errors)) { @@ -194,6 +196,7 @@ export function validationErrorToGraphErrors(workflowID: WorkflowInstID, validat errorType: e.type, message: e.message, dependentOutputs: nodeErrors.dependent_outputs, + queueEntry } if (isInputWithValue(e.extra_info)) { @@ -231,7 +234,7 @@ export function validationErrorToGraphErrors(workflowID: WorkflowInstID, validat } } -export function executionErrorToGraphErrors(workflowID: WorkflowInstID, executionError: ComfyExecutionError): ComfyGraphErrors { +export function executionErrorToGraphErrors(workflowID: WorkflowInstID, executionError: ComfyExecutionError, queueEntry: QueueEntry): ComfyGraphErrors { const errorsByID: Record = {} errorsByID[executionError.node_id] = [{ @@ -241,6 +244,7 @@ export function executionErrorToGraphErrors(workflowID: WorkflowInstID, executio errorType: "execution", message: executionError.message, dependentOutputs: [], // TODO + queueEntry, exceptionMessage: executionError.message, exceptionType: executionError.exception_type, @@ -256,11 +260,11 @@ export function executionErrorToGraphErrors(workflowID: WorkflowInstID, executio } } -export function workflowErrorToGraphErrors(workflowID: WorkflowInstID, workflowError: WorkflowError): ComfyGraphErrors { +export function workflowErrorToGraphErrors(workflowID: WorkflowInstID, workflowError: WorkflowError, queueEntry: QueueEntry): ComfyGraphErrors { if (workflowError.type === "validation") { - return validationErrorToGraphErrors(workflowID, workflowError.error) + return validationErrorToGraphErrors(workflowID, workflowError.error, queueEntry) } else { - return executionErrorToGraphErrors(workflowID, workflowError.error) + return executionErrorToGraphErrors(workflowID, workflowError.error, queueEntry) } } diff --git a/src/lib/components/ComfyBoxWorkflowsView.svelte b/src/lib/components/ComfyBoxWorkflowsView.svelte index 49b798e..865ecb2 100644 --- a/src/lib/components/ComfyBoxWorkflowsView.svelte +++ b/src/lib/components/ComfyBoxWorkflowsView.svelte @@ -246,7 +246,7 @@ app.resizeCanvas(); app.lCanvas.draw(true, true); - app.lCanvas.activeErrors = workflowErrorToGraphErrors(workflow.id, completed.error); + app.lCanvas.activeErrors = workflowErrorToGraphErrors(workflow.id, completed.error, completed.entry); app.lCanvas.jumpToFirstError(); } @@ -271,8 +271,7 @@ function hideError() { if (app?.lCanvas) { - app.lCanvas.activeErrors = null; - app.lCanvas.blinkError = null; + app.lCanvas.clearErrors(); } } diff --git a/src/lib/components/ComfyGraphErrorList.svelte b/src/lib/components/ComfyGraphErrorList.svelte index e435c18..a676874 100644 --- a/src/lib/components/ComfyGraphErrorList.svelte +++ b/src/lib/components/ComfyGraphErrorList.svelte @@ -1,16 +1,17 @@ {#if value === null} {:else} -
+
+
@@ -63,9 +68,13 @@ cursor: crosshair; } - .download { + .buttons { + display: flex; position: absolute; - top: 6px; - right: 6px; + top: var(--size-2); + right: var(--size-2); + justify-content: flex-end; + gap: var(--spacing-sm); + z-index: var(--layer-5); }