mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-08 00:50:05 +00:00
Widget promotion (#1100)
This commit is contained in:
@@ -689,6 +689,9 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable
|
||||
}
|
||||
}
|
||||
|
||||
/** Internal callback for subgraph nodes. Do not implement externally. */
|
||||
_internalConfigureAfterSlots?(): void
|
||||
|
||||
/**
|
||||
* configure a node from an object containing the serialized info
|
||||
*/
|
||||
@@ -754,6 +757,9 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable
|
||||
this.onOutputAdded?.(output)
|
||||
}
|
||||
|
||||
// SubgraphNode callback.
|
||||
this._internalConfigureAfterSlots?.()
|
||||
|
||||
if (this.widgets) {
|
||||
for (const w of this.widgets) {
|
||||
if (!w) continue
|
||||
@@ -1788,6 +1794,28 @@ export class LGraphNode implements NodeLike, Positionable, IPinnable, IColorable
|
||||
return widget
|
||||
}
|
||||
|
||||
removeWidgetByName(name: string): void {
|
||||
const widget = this.widgets?.find(x => x.name === name)
|
||||
if (widget) this.removeWidget(widget)
|
||||
}
|
||||
|
||||
removeWidget(widget: IBaseWidget): void {
|
||||
if (!this.widgets) throw new Error("removeWidget called on node without widgets")
|
||||
|
||||
const widgetIndex = this.widgets.indexOf(widget)
|
||||
if (widgetIndex === -1) throw new Error("Widget not found on this node")
|
||||
|
||||
this.widgets.splice(widgetIndex, 1)
|
||||
}
|
||||
|
||||
ensureWidgetRemoved(widget: IBaseWidget): void {
|
||||
try {
|
||||
this.removeWidget(widget)
|
||||
} catch (error) {
|
||||
console.debug("Failed to remove widget", error)
|
||||
}
|
||||
}
|
||||
|
||||
move(deltaX: number, deltaY: number): void {
|
||||
if (this.pinned) return
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ import type { Serialisable, SerialisableLLink, SubgraphIO } from "./types/serial
|
||||
|
||||
import { SUBGRAPH_INPUT_ID, SUBGRAPH_OUTPUT_ID } from "@/constants"
|
||||
|
||||
import { Subgraph } from "./litegraph"
|
||||
|
||||
export type LinkId = number
|
||||
|
||||
export type SerialisedLLinkArray = [
|
||||
@@ -405,6 +407,13 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
}
|
||||
}
|
||||
network.links.delete(this.id)
|
||||
|
||||
if (this.originIsIoNode && network instanceof Subgraph) {
|
||||
const subgraphInput = network.inputs.at(this.origin_slot)
|
||||
if (!subgraphInput) throw new Error("Invalid link - subgraph input not found")
|
||||
|
||||
subgraphInput.events.dispatch("input-disconnected", { input: subgraphInput })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
15
src/infrastructure/SubgraphInputEventMap.ts
Normal file
15
src/infrastructure/SubgraphInputEventMap.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { LGraphEventMap } from "./LGraphEventMap"
|
||||
import type { INodeInputSlot } from "@/litegraph"
|
||||
import type { SubgraphInput } from "@/subgraph/SubgraphInput"
|
||||
import type { IBaseWidget } from "@/types/widgets"
|
||||
|
||||
export interface SubgraphInputEventMap extends LGraphEventMap {
|
||||
"input-connected": {
|
||||
input: INodeInputSlot
|
||||
widget: IBaseWidget
|
||||
}
|
||||
|
||||
"input-disconnected": {
|
||||
input: SubgraphInput
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,10 @@ import type { ContextMenu } from "./ContextMenu"
|
||||
import type { LGraphNode, NodeId } from "./LGraphNode"
|
||||
import type { LinkId, LLink } from "./LLink"
|
||||
import type { Reroute, RerouteId } from "./Reroute"
|
||||
import type { SubgraphInput } from "./subgraph/SubgraphInput"
|
||||
import type { SubgraphInputNode } from "./subgraph/SubgraphInputNode"
|
||||
import type { SubgraphOutputNode } from "./subgraph/SubgraphOutputNode"
|
||||
import type { LinkDirection, RenderShape } from "./types/globalEnums"
|
||||
import type { IBaseWidget } from "./types/widgets"
|
||||
import type { Rectangle } from "@/infrastructure/Rectangle"
|
||||
import type { CanvasPointerEvent } from "@/types/events"
|
||||
|
||||
@@ -342,12 +342,16 @@ export interface INodeFlags {
|
||||
*/
|
||||
export interface IWidgetLocator {
|
||||
name: string
|
||||
[key: string | symbol]: unknown
|
||||
}
|
||||
|
||||
export interface INodeInputSlot extends INodeSlot {
|
||||
link: LinkId | null
|
||||
widget?: IWidgetLocator
|
||||
|
||||
/**
|
||||
* Internal use only; API is not finalised and may change at any time.
|
||||
*/
|
||||
_widget?: IBaseWidget
|
||||
}
|
||||
|
||||
export interface IWidgetInputSlot extends INodeInputSlot {
|
||||
@@ -437,7 +441,7 @@ export interface DefaultConnectionColors {
|
||||
}
|
||||
|
||||
export interface ISubgraphInput extends INodeInputSlot {
|
||||
_subgraphSlot: SubgraphInput
|
||||
_listenerController?: AbortController
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { INodeInputSlot, INodeOutputSlot, OptionalProps, ReadOnlyPoint } from "@/interfaces"
|
||||
import type { LGraphNode } from "@/LGraphNode"
|
||||
import type { LinkId } from "@/LLink"
|
||||
import type { IBaseWidget } from "@/types/widgets"
|
||||
|
||||
import { LabelPosition } from "@/draw"
|
||||
import { LiteGraph } from "@/litegraph"
|
||||
@@ -13,6 +14,17 @@ export class NodeInputSlot extends NodeSlot implements INodeInputSlot {
|
||||
return !!this.widget
|
||||
}
|
||||
|
||||
#widget: WeakRef<IBaseWidget> | undefined
|
||||
|
||||
/** Internal use only; API is not finalised and may change at any time. */
|
||||
get _widget(): IBaseWidget | undefined {
|
||||
return this.#widget?.deref()
|
||||
}
|
||||
|
||||
set _widget(widget: IBaseWidget | undefined) {
|
||||
this.#widget = widget ? new WeakRef(widget) : undefined
|
||||
}
|
||||
|
||||
get collapsedPos(): ReadOnlyPoint {
|
||||
return [0, LiteGraph.NODE_TITLE_HEIGHT * -0.5]
|
||||
}
|
||||
|
||||
@@ -68,7 +68,9 @@ export class Subgraph extends LGraph implements BaseLGraph, Serialisable<Exporte
|
||||
if (inputs) {
|
||||
this.inputs.length = 0
|
||||
for (const input of inputs) {
|
||||
this.inputs.push(new SubgraphInput(input, this.inputNode))
|
||||
const subgraphInput = new SubgraphInput(input, this.inputNode)
|
||||
this.inputs.push(subgraphInput)
|
||||
this.events.dispatch("input-added", { input: subgraphInput })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import type { SubgraphInputNode } from "./SubgraphInputNode"
|
||||
import type { SubgraphInputEventMap } from "@/infrastructure/SubgraphInputEventMap"
|
||||
import type { INodeInputSlot, Point, ReadOnlyRect } from "@/interfaces"
|
||||
import type { LGraphNode } from "@/LGraphNode"
|
||||
import type { RerouteId } from "@/Reroute"
|
||||
import type { IBaseWidget } from "@/types/widgets"
|
||||
|
||||
import { CustomEventTarget } from "@/infrastructure/CustomEventTarget"
|
||||
import { LLink } from "@/LLink"
|
||||
import { NodeSlotType } from "@/types/globalEnums"
|
||||
|
||||
@@ -22,6 +25,19 @@ import { SubgraphSlot } from "./SubgraphSlotBase"
|
||||
export class SubgraphInput extends SubgraphSlot {
|
||||
declare parent: SubgraphInputNode
|
||||
|
||||
events = new CustomEventTarget<SubgraphInputEventMap>()
|
||||
|
||||
/** The linked widget that this slot is connected to. */
|
||||
#widgetRef?: WeakRef<IBaseWidget>
|
||||
|
||||
get _widget() {
|
||||
return this.#widgetRef?.deref()
|
||||
}
|
||||
|
||||
set _widget(widget) {
|
||||
this.#widgetRef = widget ? new WeakRef(widget) : undefined
|
||||
}
|
||||
|
||||
override connect(slot: INodeInputSlot, node: LGraphNode, afterRerouteId?: RerouteId): LLink | undefined {
|
||||
const { subgraph } = this.parent
|
||||
|
||||
@@ -49,6 +65,17 @@ export class SubgraphInput extends SubgraphSlot {
|
||||
this.parent._disconnectNodeInput(node, slot, link)
|
||||
}
|
||||
|
||||
const inputWidget = node.getWidgetFromSlot(slot)
|
||||
if (inputWidget) {
|
||||
if (!this.matchesWidget(inputWidget)) {
|
||||
console.warn("Target input has invalid widget.", slot, node)
|
||||
return
|
||||
}
|
||||
|
||||
this._widget ??= inputWidget
|
||||
this.events.dispatch("input-connected", { input: slot, widget: inputWidget })
|
||||
}
|
||||
|
||||
const link = new LLink(
|
||||
++subgraph.state.lastLinkId,
|
||||
slot.type,
|
||||
@@ -104,6 +131,73 @@ export class SubgraphInput extends SubgraphSlot {
|
||||
return [x, y + height * 0.5]
|
||||
}
|
||||
|
||||
getConnectedWidgets(): IBaseWidget[] {
|
||||
const { subgraph } = this.parent
|
||||
const widgets: IBaseWidget[] = []
|
||||
|
||||
for (const linkId of this.linkIds) {
|
||||
const link = subgraph.getLink(linkId)
|
||||
if (!link) {
|
||||
console.error("Link not found", linkId)
|
||||
continue
|
||||
}
|
||||
|
||||
const resolved = link.resolve(subgraph)
|
||||
if (resolved.input && resolved.inputNode?.widgets) {
|
||||
// Has no widget
|
||||
const widgetNamePojo = resolved.input.widget
|
||||
if (!widgetNamePojo) continue
|
||||
|
||||
// Invalid widget name
|
||||
if (!widgetNamePojo.name) {
|
||||
console.warn("Invalid widget name", widgetNamePojo)
|
||||
continue
|
||||
}
|
||||
|
||||
const widget = resolved.inputNode.widgets.find(w => w.name === widgetNamePojo.name)
|
||||
if (!widget) {
|
||||
console.warn("Widget not found", widgetNamePojo)
|
||||
continue
|
||||
}
|
||||
|
||||
widgets.push(widget)
|
||||
} else {
|
||||
console.debug("No input found on link id", linkId, link)
|
||||
}
|
||||
}
|
||||
return widgets
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the connection between the new slot and the existing widget is valid.
|
||||
* Used to prevent connections between widgets that are not of the same type.
|
||||
* @param otherWidget The widget to compare to.
|
||||
* @returns `true` if the connection is valid, otherwise `false`.
|
||||
*/
|
||||
matchesWidget(otherWidget: IBaseWidget): boolean {
|
||||
const widget = this.#widgetRef?.deref()
|
||||
if (!widget) return true
|
||||
|
||||
if (
|
||||
otherWidget.type !== widget.type ||
|
||||
otherWidget.options.min !== widget.options.min ||
|
||||
otherWidget.options.max !== widget.options.max ||
|
||||
otherWidget.options.step !== widget.options.step ||
|
||||
otherWidget.options.step2 !== widget.options.step2 ||
|
||||
otherWidget.options.precision !== widget.options.precision
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override disconnect(): void {
|
||||
super.disconnect()
|
||||
|
||||
this.events.dispatch("input-disconnected", { input: this })
|
||||
}
|
||||
|
||||
/** For inputs, x is the right edge of the input node. */
|
||||
override arrange(rect: ReadOnlyRect): void {
|
||||
const [right, top, width, height] = rect
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import type { SubgraphInput } from "./SubgraphInput"
|
||||
import type { ISubgraphInput } from "@/interfaces"
|
||||
import type { BaseLGraph, LGraph } from "@/LGraph"
|
||||
import type { INodeInputSlot, ISlotType, NodeId } from "@/litegraph"
|
||||
import type { GraphOrSubgraph, Subgraph } from "@/subgraph/Subgraph"
|
||||
import type { ExportedSubgraphInstance } from "@/types/serialisation"
|
||||
import type { IBaseWidget } from "@/types/widgets"
|
||||
import type { UUID } from "@/utils/uuid"
|
||||
|
||||
import { RecursionError } from "@/infrastructure/RecursionError"
|
||||
import { LGraphNode } from "@/LGraphNode"
|
||||
import { type INodeInputSlot, type ISlotType, type NodeId } from "@/litegraph"
|
||||
import { LLink, type ResolvedConnection } from "@/LLink"
|
||||
import { NodeInputSlot } from "@/node/NodeInputSlot"
|
||||
import { NodeOutputSlot } from "@/node/NodeOutputSlot"
|
||||
import { toConcreteWidget } from "@/widgets/widgetMap"
|
||||
|
||||
import { type ExecutableLGraphNode, ExecutableNodeDTO } from "./ExecutableNodeDTO"
|
||||
|
||||
@@ -17,6 +20,8 @@ import { type ExecutableLGraphNode, ExecutableNodeDTO } from "./ExecutableNodeDT
|
||||
* An instance of a {@link Subgraph}, displayed as a node on the containing (parent) graph.
|
||||
*/
|
||||
export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
declare inputs: (INodeInputSlot & Partial<ISubgraphInput>)[]
|
||||
|
||||
override readonly type: UUID
|
||||
override readonly isVirtualNode = true as const
|
||||
|
||||
@@ -32,6 +37,8 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
return true
|
||||
}
|
||||
|
||||
override widgets: IBaseWidget[] = []
|
||||
|
||||
constructor(
|
||||
/** The (sub)graph that contains this subgraph instance. */
|
||||
override readonly graph: GraphOrSubgraph,
|
||||
@@ -44,10 +51,17 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
// Update this node when the subgraph input / output slots are changed
|
||||
const subgraphEvents = this.subgraph.events
|
||||
subgraphEvents.addEventListener("input-added", (e) => {
|
||||
const { name, type } = e.detail.input
|
||||
this.addInput(name, type)
|
||||
const subgraphInput = e.detail.input
|
||||
const { name, type } = subgraphInput
|
||||
const input = this.addInput(name, type)
|
||||
|
||||
this.#addSubgraphInputListeners(subgraphInput, input)
|
||||
})
|
||||
|
||||
subgraphEvents.addEventListener("removing-input", (e) => {
|
||||
const widget = e.detail.input._widget
|
||||
if (widget) this.ensureWidgetRemoved(widget)
|
||||
|
||||
this.removeInput(e.detail.index)
|
||||
})
|
||||
|
||||
@@ -55,6 +69,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
const { name, type } = e.detail.output
|
||||
this.addOutput(name, type)
|
||||
})
|
||||
|
||||
subgraphEvents.addEventListener("removing-output", (e) => {
|
||||
this.removeOutput(e.detail.index)
|
||||
})
|
||||
@@ -79,7 +94,46 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
this.configure(instanceData)
|
||||
}
|
||||
|
||||
#addSubgraphInputListeners(subgraphInput: SubgraphInput, input: INodeInputSlot & Partial<ISubgraphInput>) {
|
||||
input._listenerController?.abort()
|
||||
input._listenerController = new AbortController()
|
||||
const { signal } = input._listenerController
|
||||
|
||||
subgraphInput.events.addEventListener(
|
||||
"input-connected",
|
||||
() => {
|
||||
if (input._widget) return
|
||||
|
||||
const widget = subgraphInput._widget
|
||||
if (!widget) return
|
||||
|
||||
this.#setWidget(subgraphInput, input, widget)
|
||||
},
|
||||
{ signal },
|
||||
)
|
||||
|
||||
subgraphInput.events.addEventListener(
|
||||
"input-disconnected",
|
||||
() => {
|
||||
// If the input is connected to more than one widget, don't remove the widget
|
||||
const connectedWidgets = subgraphInput.getConnectedWidgets()
|
||||
if (connectedWidgets.length > 0) return
|
||||
|
||||
this.removeWidgetByName(input.name)
|
||||
|
||||
delete input.pos
|
||||
delete input.widget
|
||||
input._widget = undefined
|
||||
},
|
||||
{ signal },
|
||||
)
|
||||
}
|
||||
|
||||
override configure(info: ExportedSubgraphInstance): void {
|
||||
for (const input of this.inputs) {
|
||||
input._listenerController?.abort()
|
||||
}
|
||||
|
||||
this.inputs.length = 0
|
||||
this.inputs.push(
|
||||
...this.subgraph.inputNode.slots.map(
|
||||
@@ -97,6 +151,71 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
super.configure(info)
|
||||
}
|
||||
|
||||
override _internalConfigureAfterSlots() {
|
||||
// Reset widgets
|
||||
this.widgets.length = 0
|
||||
|
||||
// Check all inputs for connected widgets
|
||||
for (const input of this.inputs) {
|
||||
const subgraphInput = this.subgraph.inputNode.slots.find(slot => slot.name === input.name)
|
||||
if (!subgraphInput) throw new Error(`[SubgraphNode.configure] No subgraph input found for input ${input.name}`)
|
||||
|
||||
this.#addSubgraphInputListeners(subgraphInput, input)
|
||||
|
||||
// Find the first widget that this slot is connected to
|
||||
for (const linkId of subgraphInput.linkIds) {
|
||||
const link = this.subgraph.getLink(linkId)
|
||||
if (!link) {
|
||||
console.warn(`[SubgraphNode.configure] No link found for link ID ${linkId}`, this)
|
||||
continue
|
||||
}
|
||||
|
||||
const resolved = link.resolve(this.subgraph)
|
||||
if (!resolved.input || !resolved.inputNode) {
|
||||
console.warn("Invalid resolved link", resolved, this)
|
||||
continue
|
||||
}
|
||||
|
||||
// No widget - ignore this link
|
||||
const widget = resolved.inputNode.getWidgetFromSlot(resolved.input)
|
||||
if (!widget) continue
|
||||
|
||||
this.#setWidget(subgraphInput, input, widget)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#setWidget(subgraphInput: Readonly<SubgraphInput>, input: INodeInputSlot, widget: Readonly<IBaseWidget>) {
|
||||
// Use the first matching widget
|
||||
const promotedWidget = toConcreteWidget(widget, this).createCopyForNode(this)
|
||||
Object.assign(promotedWidget, {
|
||||
get name() {
|
||||
return subgraphInput.name
|
||||
},
|
||||
set name(value) {
|
||||
console.warn("Promoted widget: setting name is not allowed", this, value)
|
||||
},
|
||||
get localized_name() {
|
||||
return subgraphInput.localized_name
|
||||
},
|
||||
set localized_name(value) {
|
||||
console.warn("Promoted widget: setting localized_name is not allowed", this, value)
|
||||
},
|
||||
get label() {
|
||||
return subgraphInput.label
|
||||
},
|
||||
set label(value) {
|
||||
console.warn("Promoted widget: setting label is not allowed", this, value)
|
||||
},
|
||||
})
|
||||
|
||||
this.widgets.push(promotedWidget)
|
||||
|
||||
input.widget = { name: subgraphInput.name }
|
||||
input._widget = promotedWidget
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the subgraph slot is in the params before adding the input as normal.
|
||||
* @param name The name of the input slot.
|
||||
@@ -179,4 +298,10 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
override onRemoved(): void {
|
||||
for (const input of this.inputs) {
|
||||
input._listenerController?.abort()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,4 +282,19 @@ export abstract class BaseWidget<TWidget extends IBaseWidget = IBaseWidget> impl
|
||||
node.onWidgetChanged?.(this.name ?? "", v, oldValue, this)
|
||||
if (node.graph) node.graph._version++
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones the widget.
|
||||
* @param node The node that will own the cloned widget.
|
||||
* @returns A new widget with the same properties as the original
|
||||
* @remarks Subclasses with custom constructors must override this method.
|
||||
*
|
||||
* Correctly and safely typing this is currently not possible (practical?) in TypeScript 5.8.
|
||||
*/
|
||||
createCopyForNode(node: LGraphNode): this {
|
||||
// @ts-expect-error
|
||||
const cloned: this = new (this.constructor as typeof this)(this, node)
|
||||
cloned.value = this.value
|
||||
return cloned
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user