mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 07:30:11 +00:00
[Refactor] Split NodeSlot out to separate files (#960)
Split files only; no code changes. New files moved to `/node`.
This commit is contained in:
40
src/node/NodeInputSlot.ts
Normal file
40
src/node/NodeInputSlot.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { INodeInputSlot, INodeOutputSlot, OptionalProps } from "@/interfaces"
|
||||
import type { LinkId } from "@/LLink"
|
||||
|
||||
import { LabelPosition } from "@/draw"
|
||||
import { LiteGraph } from "@/litegraph"
|
||||
import { type IDrawOptions, NodeSlot } from "@/node/NodeSlot"
|
||||
|
||||
export class NodeInputSlot extends NodeSlot implements INodeInputSlot {
|
||||
link: LinkId | null
|
||||
|
||||
get isWidgetInputSlot(): boolean {
|
||||
return !!this.widget
|
||||
}
|
||||
|
||||
constructor(slot: OptionalProps<INodeInputSlot, "boundingRect">) {
|
||||
super(slot)
|
||||
this.link = slot.link
|
||||
}
|
||||
|
||||
override isConnected(): boolean {
|
||||
return this.link != null
|
||||
}
|
||||
|
||||
override isValidTarget(fromSlot: INodeInputSlot | INodeOutputSlot): boolean {
|
||||
return "links" in fromSlot && LiteGraph.isValidConnection(this.type, fromSlot.type)
|
||||
}
|
||||
|
||||
override draw(ctx: CanvasRenderingContext2D, options: Omit<IDrawOptions, "doStroke" | "labelPosition">) {
|
||||
const originalTextAlign = ctx.textAlign
|
||||
ctx.textAlign = "left"
|
||||
|
||||
super.draw(ctx, {
|
||||
...options,
|
||||
labelPosition: LabelPosition.Right,
|
||||
doStroke: false,
|
||||
})
|
||||
|
||||
ctx.textAlign = originalTextAlign
|
||||
}
|
||||
}
|
||||
47
src/node/NodeOutputSlot.ts
Normal file
47
src/node/NodeOutputSlot.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { INodeInputSlot, INodeOutputSlot, OptionalProps } from "@/interfaces"
|
||||
import type { LinkId } from "@/LLink"
|
||||
|
||||
import { LabelPosition } from "@/draw"
|
||||
import { LiteGraph } from "@/litegraph"
|
||||
import { type IDrawOptions, NodeSlot } from "@/node/NodeSlot"
|
||||
|
||||
export class NodeOutputSlot extends NodeSlot implements INodeOutputSlot {
|
||||
links: LinkId[] | null
|
||||
_data?: unknown
|
||||
slot_index?: number
|
||||
|
||||
get isWidgetInputSlot(): false {
|
||||
return false
|
||||
}
|
||||
|
||||
constructor(slot: OptionalProps<INodeOutputSlot, "boundingRect">) {
|
||||
super(slot)
|
||||
this.links = slot.links
|
||||
this._data = slot._data
|
||||
this.slot_index = slot.slot_index
|
||||
}
|
||||
|
||||
override isValidTarget(fromSlot: INodeInputSlot | INodeOutputSlot): boolean {
|
||||
return "link" in fromSlot && LiteGraph.isValidConnection(this.type, fromSlot.type)
|
||||
}
|
||||
|
||||
override isConnected(): boolean {
|
||||
return this.links != null && this.links.length > 0
|
||||
}
|
||||
|
||||
override draw(ctx: CanvasRenderingContext2D, options: Omit<IDrawOptions, "doStroke" | "labelPosition">) {
|
||||
const originalTextAlign = ctx.textAlign
|
||||
const originalStrokeStyle = ctx.strokeStyle
|
||||
ctx.textAlign = "right"
|
||||
ctx.strokeStyle = "black"
|
||||
|
||||
super.draw(ctx, {
|
||||
...options,
|
||||
labelPosition: LabelPosition.Left,
|
||||
doStroke: true,
|
||||
})
|
||||
|
||||
ctx.textAlign = originalTextAlign
|
||||
ctx.strokeStyle = originalStrokeStyle
|
||||
}
|
||||
}
|
||||
240
src/node/NodeSlot.ts
Normal file
240
src/node/NodeSlot.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import type { CanvasColour, Dictionary, INodeInputSlot, INodeOutputSlot, INodeSlot, ISlotType, IWidgetLocator, OptionalProps, Point, Rect } from "@/interfaces"
|
||||
|
||||
import { LabelPosition, SlotShape, SlotType } from "@/draw"
|
||||
import { LiteGraph } from "@/litegraph"
|
||||
import { getCentre } from "@/measure"
|
||||
import { LinkDirection, RenderShape } from "@/types/globalEnums"
|
||||
|
||||
import { NodeInputSlot } from "./NodeInputSlot"
|
||||
|
||||
export interface ConnectionColorContext {
|
||||
default_connection_color: {
|
||||
input_off: string
|
||||
input_on: string
|
||||
output_off: string
|
||||
output_on: string
|
||||
}
|
||||
default_connection_color_byType: Dictionary<CanvasColour>
|
||||
default_connection_color_byTypeOff: Dictionary<CanvasColour>
|
||||
}
|
||||
|
||||
export interface IDrawOptions {
|
||||
colorContext: ConnectionColorContext
|
||||
labelPosition?: LabelPosition
|
||||
lowQuality?: boolean
|
||||
doStroke?: boolean
|
||||
highlight?: boolean
|
||||
}
|
||||
|
||||
export abstract class NodeSlot implements INodeSlot {
|
||||
name: string
|
||||
localized_name?: string
|
||||
label?: string
|
||||
type: ISlotType
|
||||
dir?: LinkDirection
|
||||
removable?: boolean
|
||||
shape?: RenderShape
|
||||
color_off?: CanvasColour
|
||||
color_on?: CanvasColour
|
||||
locked?: boolean
|
||||
nameLocked?: boolean
|
||||
pos?: Point
|
||||
widget?: IWidgetLocator
|
||||
hasErrors?: boolean
|
||||
boundingRect: Rect
|
||||
|
||||
get highlightColor(): CanvasColour {
|
||||
return LiteGraph.NODE_TEXT_HIGHLIGHT_COLOR ?? LiteGraph.NODE_SELECTED_TITLE_COLOR ?? LiteGraph.NODE_TEXT_COLOR
|
||||
}
|
||||
|
||||
abstract get isWidgetInputSlot(): boolean
|
||||
|
||||
constructor(slot: OptionalProps<INodeSlot, "boundingRect">) {
|
||||
Object.assign(this, slot)
|
||||
this.name = slot.name
|
||||
this.type = slot.type
|
||||
this.boundingRect = slot.boundingRect ?? [0, 0, 0, 0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this slot is a valid target for a dragging link.
|
||||
* @param fromSlot The slot that the link is being connected from.
|
||||
*/
|
||||
abstract isValidTarget(fromSlot: INodeInputSlot | INodeOutputSlot): boolean
|
||||
|
||||
/**
|
||||
* The label to display in the UI.
|
||||
*/
|
||||
get renderingLabel(): string {
|
||||
return this.label || this.localized_name || this.name || ""
|
||||
}
|
||||
|
||||
abstract isConnected(): boolean
|
||||
|
||||
connectedColor(context: ConnectionColorContext): CanvasColour {
|
||||
return this.color_on ||
|
||||
context.default_connection_color_byType[this.type] ||
|
||||
context.default_connection_color.output_on
|
||||
}
|
||||
|
||||
disconnectedColor(context: ConnectionColorContext): CanvasColour {
|
||||
return this.color_off ||
|
||||
context.default_connection_color_byTypeOff[this.type] ||
|
||||
context.default_connection_color_byType[this.type] ||
|
||||
context.default_connection_color.output_off
|
||||
}
|
||||
|
||||
renderingColor(context: ConnectionColorContext): CanvasColour {
|
||||
return this.isConnected()
|
||||
? this.connectedColor(context)
|
||||
: this.disconnectedColor(context)
|
||||
}
|
||||
|
||||
draw(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
{
|
||||
colorContext,
|
||||
labelPosition = LabelPosition.Right,
|
||||
lowQuality = false,
|
||||
highlight = false,
|
||||
doStroke = false,
|
||||
}: IDrawOptions,
|
||||
) {
|
||||
// Save the current fillStyle and strokeStyle
|
||||
const originalFillStyle = ctx.fillStyle
|
||||
const originalStrokeStyle = ctx.strokeStyle
|
||||
const originalLineWidth = ctx.lineWidth
|
||||
|
||||
const labelColor = highlight
|
||||
? this.highlightColor
|
||||
: LiteGraph.NODE_TEXT_COLOR
|
||||
|
||||
const pos = getCentre(this.boundingRect)
|
||||
const slot_type = this.type
|
||||
const slot_shape = (
|
||||
slot_type === SlotType.Array ? SlotShape.Grid : this.shape
|
||||
) as SlotShape
|
||||
|
||||
ctx.beginPath()
|
||||
let doFill = true
|
||||
|
||||
ctx.fillStyle = this.renderingColor(colorContext)
|
||||
ctx.lineWidth = 1
|
||||
if (slot_type === SlotType.Event || slot_shape === SlotShape.Box) {
|
||||
ctx.rect(pos[0] - 6 + 0.5, pos[1] - 5 + 0.5, 14, 10)
|
||||
} else if (slot_shape === SlotShape.Arrow) {
|
||||
ctx.moveTo(pos[0] + 8, pos[1] + 0.5)
|
||||
ctx.lineTo(pos[0] - 4, pos[1] + 6 + 0.5)
|
||||
ctx.lineTo(pos[0] - 4, pos[1] - 6 + 0.5)
|
||||
ctx.closePath()
|
||||
} else if (slot_shape === SlotShape.Grid) {
|
||||
const gridSize = 3
|
||||
const cellSize = 2
|
||||
const spacing = 3
|
||||
|
||||
for (let x = 0; x < gridSize; x++) {
|
||||
for (let y = 0; y < gridSize; y++) {
|
||||
ctx.rect(
|
||||
pos[0] - 4 + x * spacing,
|
||||
pos[1] - 4 + y * spacing,
|
||||
cellSize,
|
||||
cellSize,
|
||||
)
|
||||
}
|
||||
}
|
||||
doStroke = false
|
||||
} else {
|
||||
// Default rendering for circle, hollow circle.
|
||||
if (lowQuality) {
|
||||
ctx.rect(pos[0] - 4, pos[1] - 4, 8, 8)
|
||||
} else {
|
||||
let radius: number
|
||||
if (slot_shape === SlotShape.HollowCircle) {
|
||||
doFill = false
|
||||
doStroke = true
|
||||
ctx.lineWidth = 3
|
||||
ctx.strokeStyle = ctx.fillStyle
|
||||
radius = highlight ? 4 : 3
|
||||
} else {
|
||||
// Normal circle
|
||||
radius = highlight ? 5 : 4
|
||||
}
|
||||
ctx.arc(pos[0], pos[1], radius, 0, Math.PI * 2)
|
||||
}
|
||||
}
|
||||
|
||||
if (doFill) ctx.fill()
|
||||
if (!lowQuality && doStroke) ctx.stroke()
|
||||
|
||||
// render slot label
|
||||
const hideLabel = lowQuality || this.isWidgetInputSlot
|
||||
if (!hideLabel) {
|
||||
const text = this.renderingLabel
|
||||
if (text) {
|
||||
// TODO: Finish impl. Highlight text on mouseover unless we're connecting links.
|
||||
ctx.fillStyle = labelColor
|
||||
|
||||
if (labelPosition === LabelPosition.Right) {
|
||||
if (this.dir == LinkDirection.UP) {
|
||||
ctx.fillText(text, pos[0], pos[1] - 10)
|
||||
} else {
|
||||
ctx.fillText(text, pos[0] + 10, pos[1] + 5)
|
||||
}
|
||||
} else {
|
||||
if (this.dir == LinkDirection.DOWN) {
|
||||
ctx.fillText(text, pos[0], pos[1] - 8)
|
||||
} else {
|
||||
ctx.fillText(text, pos[0] - 10, pos[1] + 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw a red circle if the slot has errors.
|
||||
if (this.hasErrors) {
|
||||
ctx.lineWidth = 2
|
||||
ctx.strokeStyle = "red"
|
||||
ctx.beginPath()
|
||||
ctx.arc(pos[0], pos[1], 12, 0, Math.PI * 2)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// Restore the original fillStyle and strokeStyle
|
||||
ctx.fillStyle = originalFillStyle
|
||||
ctx.strokeStyle = originalStrokeStyle
|
||||
ctx.lineWidth = originalLineWidth
|
||||
}
|
||||
|
||||
drawCollapsed(ctx: CanvasRenderingContext2D, options: { pos: Point }) {
|
||||
const [x, y] = options.pos
|
||||
|
||||
// Save original styles
|
||||
const originalFillStyle = ctx.fillStyle
|
||||
|
||||
ctx.fillStyle = "#686"
|
||||
ctx.beginPath()
|
||||
|
||||
if (this.type === SlotType.Event || this.shape === RenderShape.BOX) {
|
||||
ctx.rect(x - 7 + 0.5, y - 4, 14, 8)
|
||||
} else if (this.shape === RenderShape.ARROW) {
|
||||
// Adjust arrow direction based on whether this is an input or output slot
|
||||
const isInput = this instanceof NodeInputSlot
|
||||
if (isInput) {
|
||||
ctx.moveTo(x + 8, y)
|
||||
ctx.lineTo(x - 4, y - 4)
|
||||
ctx.lineTo(x - 4, y + 4)
|
||||
} else {
|
||||
ctx.moveTo(x + 6, y)
|
||||
ctx.lineTo(x - 6, y - 4)
|
||||
ctx.lineTo(x - 6, y + 4)
|
||||
}
|
||||
ctx.closePath()
|
||||
} else {
|
||||
ctx.arc(x, y, 4, 0, Math.PI * 2)
|
||||
}
|
||||
ctx.fill()
|
||||
|
||||
// Restore original styles
|
||||
ctx.fillStyle = originalFillStyle
|
||||
}
|
||||
}
|
||||
67
src/node/slotUtils.ts
Normal file
67
src/node/slotUtils.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { IWidgetInputSlot, SharedIntersection } from "@/interfaces"
|
||||
import type { INodeInputSlot, INodeOutputSlot, INodeSlot, IWidget } from "@/litegraph"
|
||||
import type { ISerialisableNodeInput, ISerialisableNodeOutput } from "@/types/serialisation"
|
||||
|
||||
import { NodeInputSlot } from "./NodeInputSlot"
|
||||
import { NodeOutputSlot } from "./NodeOutputSlot"
|
||||
|
||||
type CommonIoSlotProps = SharedIntersection<ISerialisableNodeInput, ISerialisableNodeOutput>
|
||||
|
||||
export function shallowCloneCommonProps(slot: CommonIoSlotProps): CommonIoSlotProps {
|
||||
const { color_off, color_on, dir, label, localized_name, locked, name, nameLocked, removable, shape, type } = slot
|
||||
return { color_off, color_on, dir, label, localized_name, locked, name, nameLocked, removable, shape, type }
|
||||
}
|
||||
|
||||
export function inputAsSerialisable(slot: INodeInputSlot): ISerialisableNodeInput {
|
||||
const { link } = slot
|
||||
const widgetOrPos = slot.widget
|
||||
? { widget: { name: slot.widget.name } }
|
||||
: { pos: slot.pos }
|
||||
|
||||
return {
|
||||
...shallowCloneCommonProps(slot),
|
||||
...widgetOrPos,
|
||||
link,
|
||||
}
|
||||
}
|
||||
|
||||
export function outputAsSerialisable(slot: INodeOutputSlot & { widget?: IWidget }): ISerialisableNodeOutput {
|
||||
const { pos, slot_index, links, widget } = slot
|
||||
// Output widgets do not exist in Litegraph; this is a temporary downstream workaround.
|
||||
const outputWidget = widget
|
||||
? { widget: { name: widget.name } }
|
||||
: null
|
||||
|
||||
return {
|
||||
...shallowCloneCommonProps(slot),
|
||||
...outputWidget,
|
||||
pos,
|
||||
slot_index,
|
||||
links,
|
||||
}
|
||||
}
|
||||
|
||||
export function isINodeInputSlot(slot: INodeSlot): slot is INodeInputSlot {
|
||||
return "link" in slot
|
||||
}
|
||||
|
||||
export function isINodeOutputSlot(slot: INodeSlot): slot is INodeOutputSlot {
|
||||
return "links" in slot
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard: Whether this input slot is attached to a widget.
|
||||
* @param slot The slot to check.
|
||||
*/
|
||||
|
||||
export function isWidgetInputSlot(slot: INodeInputSlot): slot is IWidgetInputSlot {
|
||||
return !!slot.widget
|
||||
}
|
||||
|
||||
export function toNodeSlotClass(slot: INodeInputSlot | INodeOutputSlot): NodeInputSlot | NodeOutputSlot {
|
||||
if (slot instanceof NodeInputSlot || slot instanceof NodeOutputSlot) return slot
|
||||
|
||||
return "link" in slot
|
||||
? new NodeInputSlot(slot)
|
||||
: new NodeOutputSlot(slot)
|
||||
}
|
||||
Reference in New Issue
Block a user