mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-07 08:30:06 +00:00
Redesign invalid node indicator (#358)
* nit * Remove unused code Gradient can (and should) be impl. directly by caching a CanvasGradient. * nit * nit - Refactor * Remove redundant code * Add line width & colour options to shape stroke * Rename drawSelectionBounding to strokeShape * nit - Doc * Fix rounded corners not scaling with padding * Optimise node badge draw * Redesign invalid node visual indication Customisable boundary indicator now used, replacing red background. * Update snapshot --------- Co-authored-by: huchenlei <huchenlei@proton.me>
This commit is contained in:
@@ -47,10 +47,10 @@ export class LGraphBadge {
|
||||
getWidth(ctx: CanvasRenderingContext2D) {
|
||||
if (!this.visible) return 0
|
||||
|
||||
ctx.save()
|
||||
const { font } = ctx
|
||||
ctx.font = `${this.fontSize}px sans-serif`
|
||||
const textWidth = ctx.measureText(this.text).width
|
||||
ctx.restore()
|
||||
ctx.font = font
|
||||
return textWidth + this.padding * 2
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export class LGraphBadge {
|
||||
): void {
|
||||
if (!this.visible) return
|
||||
|
||||
ctx.save()
|
||||
const { fillStyle } = ctx
|
||||
ctx.font = `${this.fontSize}px sans-serif`
|
||||
const badgeWidth = this.getWidth(ctx)
|
||||
const badgeX = 0
|
||||
@@ -85,6 +85,6 @@ export class LGraphBadge {
|
||||
y + this.height - this.padding,
|
||||
)
|
||||
|
||||
ctx.restore()
|
||||
ctx.fillStyle = fillStyle
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,9 +137,10 @@ interface IDrawSelectionBoundingOptions {
|
||||
shape?: RenderShape
|
||||
title_height?: number
|
||||
title_mode?: TitleMode
|
||||
fgcolor?: CanvasColour
|
||||
colour?: CanvasColour
|
||||
padding?: number
|
||||
collapsed?: boolean
|
||||
thickness?: number
|
||||
}
|
||||
|
||||
/** @inheritdoc {@link LGraphCanvas.state} */
|
||||
@@ -4737,7 +4738,7 @@ export class LGraphCanvas {
|
||||
this.current_node = node
|
||||
|
||||
const color = node.color || node.constructor.color || LiteGraph.NODE_DEFAULT_COLOR
|
||||
let bgcolor = node.bgcolor || node.constructor.bgcolor || LiteGraph.NODE_DEFAULT_BGCOLOR
|
||||
const bgcolor = node.bgcolor || node.constructor.bgcolor || LiteGraph.NODE_DEFAULT_BGCOLOR
|
||||
|
||||
const low_quality = this.ds.scale < 0.6 // zoomed out
|
||||
const editor_alpha = this.editor_alpha
|
||||
@@ -4790,9 +4791,6 @@ export class LGraphCanvas {
|
||||
}
|
||||
|
||||
// draw shape
|
||||
if (node.has_errors) {
|
||||
bgcolor = "red"
|
||||
}
|
||||
this.drawNodeShape(
|
||||
node,
|
||||
ctx,
|
||||
@@ -4808,7 +4806,10 @@ export class LGraphCanvas {
|
||||
|
||||
ctx.shadowColor = "transparent"
|
||||
|
||||
// draw foreground
|
||||
// TODO: Legacy behaviour: onDrawForeground received ctx in this state
|
||||
ctx.strokeStyle = LiteGraph.NODE_BOX_OUTLINE_COLOR
|
||||
|
||||
// Draw Foreground
|
||||
node.onDrawForeground?.(ctx, this, this.canvas)
|
||||
|
||||
// connection slots
|
||||
@@ -5117,13 +5118,14 @@ export class LGraphCanvas {
|
||||
): void {
|
||||
// Rendering options
|
||||
ctx.strokeStyle = fgcolor
|
||||
ctx.fillStyle = bgcolor
|
||||
ctx.fillStyle = LiteGraph.use_legacy_node_error_indicator ? "#F00" : bgcolor
|
||||
|
||||
const title_height = LiteGraph.NODE_TITLE_HEIGHT
|
||||
const low_quality = this.ds.scale < 0.5
|
||||
|
||||
const { collapsed } = node.flags
|
||||
const shape = node._shape || node.constructor.shape || RenderShape.ROUND
|
||||
const title_mode = node.constructor.title_mode
|
||||
const { title_mode } = node.constructor
|
||||
|
||||
const render_title = title_mode == TitleMode.TRANSPARENT_TITLE || title_mode == TitleMode.NO_TITLE
|
||||
? false
|
||||
@@ -5138,39 +5140,48 @@ export class LGraphCanvas {
|
||||
const old_alpha = ctx.globalAlpha
|
||||
|
||||
// Draw node background (shape)
|
||||
{
|
||||
ctx.beginPath()
|
||||
if (shape == RenderShape.BOX || low_quality) {
|
||||
ctx.fillRect(area[0], area[1], area[2], area[3])
|
||||
} else if (shape == RenderShape.ROUND || shape == RenderShape.CARD) {
|
||||
ctx.roundRect(
|
||||
area[0],
|
||||
area[1],
|
||||
area[2],
|
||||
area[3],
|
||||
shape == RenderShape.CARD
|
||||
? [this.round_radius, this.round_radius, 0, 0]
|
||||
: [this.round_radius],
|
||||
)
|
||||
} else if (shape == RenderShape.CIRCLE) {
|
||||
ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5, 0, Math.PI * 2)
|
||||
}
|
||||
ctx.fill()
|
||||
ctx.beginPath()
|
||||
if (shape == RenderShape.BOX || low_quality) {
|
||||
ctx.fillRect(area[0], area[1], area[2], area[3])
|
||||
} else if (shape == RenderShape.ROUND || shape == RenderShape.CARD) {
|
||||
ctx.roundRect(
|
||||
area[0],
|
||||
area[1],
|
||||
area[2],
|
||||
area[3],
|
||||
shape == RenderShape.CARD
|
||||
? [this.round_radius, this.round_radius, 0, 0]
|
||||
: [this.round_radius],
|
||||
)
|
||||
} else if (shape == RenderShape.CIRCLE) {
|
||||
ctx.arc(size[0] * 0.5, size[1] * 0.5, size[0] * 0.5, 0, Math.PI * 2)
|
||||
}
|
||||
ctx.fill()
|
||||
|
||||
// separator
|
||||
if (!node.flags.collapsed && render_title) {
|
||||
ctx.shadowColor = "transparent"
|
||||
ctx.fillStyle = "rgba(0,0,0,0.2)"
|
||||
ctx.fillRect(0, -1, area[2], 2)
|
||||
}
|
||||
if (node.has_errors && !LiteGraph.use_legacy_node_error_indicator) {
|
||||
this.strokeShape(ctx, area, {
|
||||
shape,
|
||||
title_mode,
|
||||
title_height,
|
||||
padding: 12,
|
||||
colour: LiteGraph.NODE_ERROR_COLOUR,
|
||||
collapsed,
|
||||
thickness: 10,
|
||||
})
|
||||
}
|
||||
|
||||
// Separator - title bar <-> body
|
||||
if (!collapsed && render_title) {
|
||||
ctx.shadowColor = "transparent"
|
||||
ctx.fillStyle = "rgba(0,0,0,0.2)"
|
||||
ctx.fillRect(0, -1, area[2], 2)
|
||||
}
|
||||
ctx.shadowColor = "transparent"
|
||||
|
||||
node.onDrawBackground?.(ctx, this, this.canvas, this.graph_mouse)
|
||||
|
||||
// title bg (remember, it is rendered ABOVE the node)
|
||||
// Title bar background (remember, it is rendered ABOVE the node)
|
||||
if (render_title || title_mode == TitleMode.TRANSPARENT_TITLE) {
|
||||
// title bar
|
||||
if (node.onDrawTitleBar) {
|
||||
node.onDrawTitleBar(ctx, title_height, size, this.ds.scale, fgcolor)
|
||||
} else if (
|
||||
@@ -5179,30 +5190,13 @@ export class LGraphCanvas {
|
||||
) {
|
||||
const title_color = node.constructor.title_color || fgcolor
|
||||
|
||||
if (node.flags.collapsed) {
|
||||
if (collapsed) {
|
||||
ctx.shadowColor = LiteGraph.DEFAULT_SHADOW_COLOR
|
||||
}
|
||||
|
||||
//* gradient test
|
||||
if (this.use_gradients) {
|
||||
// TODO: This feature may not have been completed. Could finish or remove.
|
||||
// Original impl. may cause CanvasColour to be used as index key. Also, colour requires validation before blindly passing on.
|
||||
// @ts-expect-error Fix or remove gradient feature
|
||||
let grad = LGraphCanvas.gradients[title_color]
|
||||
if (!grad) {
|
||||
// @ts-expect-error Fix or remove gradient feature
|
||||
grad = LGraphCanvas.gradients[title_color] =
|
||||
ctx.createLinearGradient(0, 0, 400, 0)
|
||||
grad.addColorStop(0, title_color)
|
||||
grad.addColorStop(1, "#000")
|
||||
}
|
||||
ctx.fillStyle = grad
|
||||
} else {
|
||||
ctx.fillStyle = title_color
|
||||
}
|
||||
|
||||
// ctx.globalAlpha = 0.5 * old_alpha;
|
||||
ctx.fillStyle = title_color
|
||||
ctx.beginPath()
|
||||
|
||||
if (shape == RenderShape.BOX || low_quality) {
|
||||
ctx.rect(0, -title_height, size[0], title_height)
|
||||
} else if (shape == RenderShape.ROUND || shape == RenderShape.CARD) {
|
||||
@@ -5211,7 +5205,7 @@ export class LGraphCanvas {
|
||||
-title_height,
|
||||
size[0],
|
||||
title_height,
|
||||
node.flags.collapsed
|
||||
collapsed
|
||||
? [this.round_radius]
|
||||
: [this.round_radius, this.round_radius, 0, 0],
|
||||
)
|
||||
@@ -5220,12 +5214,10 @@ export class LGraphCanvas {
|
||||
ctx.shadowColor = "transparent"
|
||||
}
|
||||
|
||||
let colState: string | boolean = false
|
||||
if (LiteGraph.node_box_coloured_by_mode) {
|
||||
if (LiteGraph.NODE_MODES_COLORS[node.mode]) {
|
||||
colState = LiteGraph.NODE_MODES_COLORS[node.mode]
|
||||
}
|
||||
}
|
||||
let colState = LiteGraph.node_box_coloured_by_mode && LiteGraph.NODE_MODES_COLORS[node.mode]
|
||||
? LiteGraph.NODE_MODES_COLORS[node.mode]
|
||||
: false
|
||||
|
||||
if (LiteGraph.node_box_coloured_when_on) {
|
||||
colState = node.action_triggered
|
||||
? "#FFF"
|
||||
@@ -5256,8 +5248,7 @@ export class LGraphCanvas {
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
ctx.fillStyle =
|
||||
node.boxcolor || colState || LiteGraph.NODE_DEFAULT_BOXCOLOR
|
||||
ctx.fillStyle = node.boxcolor || colState || LiteGraph.NODE_DEFAULT_BOXCOLOR
|
||||
if (low_quality)
|
||||
ctx.fillRect(
|
||||
title_height * 0.5 - box_size * 0.5,
|
||||
@@ -5309,14 +5300,15 @@ export class LGraphCanvas {
|
||||
}
|
||||
if (!low_quality) {
|
||||
ctx.font = this.title_text_font
|
||||
const title = String(node.getTitle()) + (node.pinned ? "📌" : "")
|
||||
const rawTitle = node.getTitle() ?? `❌ ${node.type}`
|
||||
const title = String(rawTitle) + (node.pinned ? "📌" : "")
|
||||
if (title) {
|
||||
if (selected) {
|
||||
ctx.fillStyle = LiteGraph.NODE_SELECTED_TITLE_COLOR
|
||||
} else {
|
||||
ctx.fillStyle = node.constructor.title_text_color || this.node_title_color
|
||||
}
|
||||
if (node.flags.collapsed) {
|
||||
if (collapsed) {
|
||||
ctx.textAlign = "left"
|
||||
// const measure = ctx.measureText(title)
|
||||
ctx.fillText(
|
||||
@@ -5338,7 +5330,7 @@ export class LGraphCanvas {
|
||||
|
||||
// subgraph box
|
||||
if (
|
||||
!node.flags.collapsed &&
|
||||
!collapsed &&
|
||||
node.subgraph &&
|
||||
!node.skip_subgraph_button
|
||||
) {
|
||||
@@ -5376,11 +5368,13 @@ export class LGraphCanvas {
|
||||
if (selected) {
|
||||
node.onBounding?.(area)
|
||||
|
||||
this.drawSelectionBounding(ctx, area, {
|
||||
const padding = node.has_errors && !LiteGraph.use_legacy_node_error_indicator ? 20 : undefined
|
||||
|
||||
this.strokeShape(ctx, area, {
|
||||
shape,
|
||||
title_height,
|
||||
title_mode,
|
||||
fgcolor,
|
||||
padding,
|
||||
collapsed: node.flags?.collapsed,
|
||||
})
|
||||
}
|
||||
@@ -5391,20 +5385,31 @@ export class LGraphCanvas {
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the selection bounding of an area.
|
||||
* Draws only the path of a shape on the canvas, without filling.
|
||||
* Used to draw indicators for node status, e.g. "selected".
|
||||
* @param ctx The 2D context to draw on
|
||||
* @param area The position and size of the shape to render
|
||||
*/
|
||||
drawSelectionBounding(
|
||||
strokeShape(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
area: Rect,
|
||||
{
|
||||
/** The shape to render */
|
||||
shape = RenderShape.BOX,
|
||||
/** Shape will extend above the Y-axis 0 by this amount */
|
||||
title_height = LiteGraph.NODE_TITLE_HEIGHT,
|
||||
/** @deprecated This is node-specific: it should be removed entirely, and behaviour defined by the caller more explicitly */
|
||||
title_mode = TitleMode.NORMAL_TITLE,
|
||||
fgcolor = LiteGraph.NODE_BOX_OUTLINE_COLOR,
|
||||
/** The colour that should be drawn */
|
||||
colour = LiteGraph.NODE_BOX_OUTLINE_COLOR,
|
||||
/** The distance between the edge of the {@link area} and the middle of the line */
|
||||
padding = 6,
|
||||
/** @deprecated This is node-specific: it should be removed entirely, and behaviour defined by the caller more explicitly */
|
||||
collapsed = false,
|
||||
/** Thickness of the line drawn (`lineWidth`) */
|
||||
thickness = 1,
|
||||
}: IDrawSelectionBoundingOptions = {},
|
||||
) {
|
||||
): void {
|
||||
// Adjust area if title is transparent
|
||||
if (title_mode === TitleMode.TRANSPARENT_TITLE) {
|
||||
area[1] -= title_height
|
||||
@@ -5412,8 +5417,10 @@ export class LGraphCanvas {
|
||||
}
|
||||
|
||||
// Set up context
|
||||
ctx.lineWidth = 1
|
||||
const { lineWidth, strokeStyle } = ctx
|
||||
ctx.lineWidth = thickness
|
||||
ctx.globalAlpha = 0.8
|
||||
ctx.strokeStyle = colour
|
||||
ctx.beginPath()
|
||||
|
||||
// Draw shape based on type
|
||||
@@ -5430,7 +5437,7 @@ export class LGraphCanvas {
|
||||
}
|
||||
case RenderShape.ROUND:
|
||||
case RenderShape.CARD: {
|
||||
const radius = this.round_radius * 2
|
||||
const radius = this.round_radius + padding
|
||||
const isCollapsed = shape === RenderShape.CARD && collapsed
|
||||
const cornerRadii =
|
||||
isCollapsed || shape === RenderShape.ROUND
|
||||
@@ -5455,11 +5462,13 @@ export class LGraphCanvas {
|
||||
}
|
||||
|
||||
// Stroke the shape
|
||||
ctx.strokeStyle = LiteGraph.NODE_BOX_OUTLINE_COLOR
|
||||
ctx.stroke()
|
||||
|
||||
// Reset context
|
||||
ctx.strokeStyle = fgcolor
|
||||
ctx.lineWidth = lineWidth
|
||||
ctx.strokeStyle = strokeStyle
|
||||
|
||||
// TODO: Store and reset value properly. Callers currently expect this behaviour (e.g. muted nodes).
|
||||
ctx.globalAlpha = 1
|
||||
}
|
||||
|
||||
|
||||
@@ -183,11 +183,8 @@ export class LGraphGroup implements Positionable, IPinnable {
|
||||
ctx.fillText(this.title + (this.pinned ? "📌" : ""), x + padding, y + font_size)
|
||||
|
||||
if (LiteGraph.highlight_selected_group && this.selected) {
|
||||
graphCanvas.drawSelectionBounding(ctx, this._bounding, {
|
||||
shape: RenderShape.BOX,
|
||||
graphCanvas.strokeShape(ctx, this._bounding, {
|
||||
title_height: this.titleHeight,
|
||||
title_mode: TitleMode.NORMAL_TITLE,
|
||||
fgcolor: this.color,
|
||||
padding,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ export class LiteGraphGlobal {
|
||||
NODE_DEFAULT_BOXCOLOR = "#666"
|
||||
NODE_DEFAULT_SHAPE = "box"
|
||||
NODE_BOX_OUTLINE_COLOR = "#FFF"
|
||||
NODE_ERROR_COLOUR = "#E00"
|
||||
DEFAULT_SHADOW_COLOR = "rgba(0,0,0,0.5)"
|
||||
DEFAULT_GROUP_FONT = 24
|
||||
DEFAULT_GROUP_FONT_SIZE?: any
|
||||
@@ -252,6 +253,9 @@ export class LiteGraphGlobal {
|
||||
// Whether to highlight the bounding box of selected groups
|
||||
highlight_selected_group = true
|
||||
|
||||
/** If `true`, the old "eye-melting-red" error indicator will be used for nodes */
|
||||
use_legacy_node_error_indicator = false
|
||||
|
||||
// TODO: Remove legacy accessors
|
||||
LGraph = LGraph
|
||||
LLink = LLink
|
||||
|
||||
Reference in New Issue
Block a user