mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-23 06:10:32 +00:00
feat(extension-api): core extension v2 conversions
Restratified i-ext. Adds v2 conversions for 6 core extensions: - dynamicPrompts.v2.ts - imageCrop.v2.ts - previewAny.v2.ts - noteNode.v2.ts - rerouteNode.v2.ts - slotDefaults.v2.ts And registers the first 3 in src/extensions/core/index.ts. Note: noteNode.v2, rerouteNode.v2, slotDefaults.v2 are NOT yet registered in index.ts (pre-existing issue from original i-ext branch). Filed as a follow-up TODO. Original (pre-restratify) branch tip backed up at refs/backup/restratify-20260511/ext-api-i-ext.
This commit is contained in:
28
src/extensions/core/dynamicPrompts.v2.ts
Normal file
28
src/extensions/core/dynamicPrompts.v2.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* DynamicPrompts — rewritten with the v2 extension API.
|
||||
*
|
||||
* v1: reads node.widgets, assigns widget.serializeValue
|
||||
* v2: same logic, uses WidgetHandle instead of raw widget
|
||||
*/
|
||||
|
||||
import { defineNodeExtension } from '@/extension-api'
|
||||
import { processDynamicPrompt } from '@/utils/formatUtil'
|
||||
|
||||
defineNodeExtension({
|
||||
name: 'Comfy.DynamicPrompts.V2',
|
||||
|
||||
nodeCreated(node) {
|
||||
for (const widget of node.widgets()) {
|
||||
if (widget.getOption('dynamicPrompts')) {
|
||||
widget.on('beforeSerialize', (e) => {
|
||||
if (e.context === 'prompt') {
|
||||
const value = widget.getValue() as string
|
||||
e.setSerializedValue(
|
||||
typeof value === 'string' ? processDynamicPrompt(value) : value
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
18
src/extensions/core/imageCrop.v2.ts
Normal file
18
src/extensions/core/imageCrop.v2.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* ImageCrop — rewritten with the v2 extension API.
|
||||
*
|
||||
* v1: 13 lines, accesses node.size and node.constructor.comfyClass directly
|
||||
* v2: 12 lines, uses NodeHandle — type filtering via nodeTypes option
|
||||
*/
|
||||
|
||||
import { defineNodeExtension } from '@/extension-api'
|
||||
|
||||
defineNodeExtension({
|
||||
name: 'Comfy.ImageCrop.V2',
|
||||
nodeTypes: ['ImageCropV2'],
|
||||
|
||||
nodeCreated(node) {
|
||||
const [w, h] = node.getSize()
|
||||
node.setSize([Math.max(w, 300), Math.max(h, 450)])
|
||||
}
|
||||
})
|
||||
@@ -4,6 +4,7 @@ import './clipspace'
|
||||
import './contextMenuFilter'
|
||||
import './customWidgets'
|
||||
import './dynamicPrompts'
|
||||
import './dynamicPrompts.v2'
|
||||
import './editAttention'
|
||||
import './electronAdapter'
|
||||
import './groupNode'
|
||||
@@ -11,6 +12,7 @@ import './groupNodeManage'
|
||||
import './groupOptions'
|
||||
import './imageCompare'
|
||||
import './imageCrop'
|
||||
import './imageCrop.v2'
|
||||
// load3d and saveMesh are loaded on-demand to defer THREE.js (~1.8MB)
|
||||
// The lazy loader triggers loading when a 3D node is used
|
||||
import './load3dLazy'
|
||||
@@ -21,6 +23,7 @@ if (!isCloud) {
|
||||
import './noteNode'
|
||||
import './painter'
|
||||
import './previewAny'
|
||||
import './previewAny.v2'
|
||||
import './rerouteNode'
|
||||
import './saveImageExtraOutput'
|
||||
// saveMesh is loaded on-demand with load3d (see load3dLazy.ts)
|
||||
|
||||
120
src/extensions/core/noteNode.v2.ts
Normal file
120
src/extensions/core/noteNode.v2.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* NoteNode + MarkdownNoteNode — rewritten with the v2 extension API.
|
||||
*
|
||||
* v1 used `registerCustomNodes` to call `LiteGraph.registerNodeType()` directly.
|
||||
* v2 does NOT yet have a `registerNodeType` hook on `defineExtension`. The
|
||||
* custom-node-type registration surface is a planned addition (gap tracked in
|
||||
* the inline comment below).
|
||||
*
|
||||
* What this file demonstrates to Simon/Austin:
|
||||
* 1. Pure app-level extensions use `defineExtension({ setup() })`.
|
||||
* 2. Shell settings are accessed via the `ExtensionManager` passed to `setup`.
|
||||
* 3. Custom LiteGraph node type registration has NO v2 equivalent yet.
|
||||
* The v2 API surface covers node *instance* hooks (nodeCreated, executed,
|
||||
* etc.) but not node *type* registration, which today still requires
|
||||
* LiteGraph.registerNodeType(). That gap will be addressed in PKG4 /
|
||||
* the ComfyNodeRegistry design.
|
||||
*
|
||||
* Compare with noteNode.ts (v1):
|
||||
* v1: registerCustomNodes() callback, direct LiteGraph + ComfyWidgets calls
|
||||
* v2: setup() callback, custom-node-type registration still needs v1 bridge
|
||||
*
|
||||
* API GAPS (feedback items for Simon/Austin):
|
||||
* GAP-1: No `registerNodeTypes` hook on `ExtensionOptions` — can't replace
|
||||
* `registerCustomNodes` in pure v2. Need a `NodeTypeRegistry` surface
|
||||
* or a first-class "custom node type" abstraction in the v2 API.
|
||||
* GAP-2: No `addWidget` for node *type* construction time (before any
|
||||
* instance exists) — `ComfyWidgets.STRING(this, ...)` has no analog.
|
||||
* GAP-3: Node colour + visual styling (`this.color`, `this.bgcolor`,
|
||||
* `this.groupcolor`) has no API surface; would need NodeHandle setter.
|
||||
*
|
||||
* Interim bridge: call LiteGraph directly inside `setup()` to register the
|
||||
* types, then rely on `defineNodeExtension({ nodeTypes: ['Note'] })` for any
|
||||
* per-instance extension logic. This hybrid is the least-bad option until
|
||||
* GAP-1 is closed.
|
||||
*/
|
||||
|
||||
import { LGraphCanvas, LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { ComfyWidgets } from '../../scripts/widgets'
|
||||
import { defineExtension } from '@/extension-api'
|
||||
|
||||
// ── GAP-1: Interim bridge for custom node type registration ──────────────────
|
||||
// We still call LiteGraph.registerNodeType() directly because there is no v2
|
||||
// `registerNodeTypes` hook. This is intentionally non-ideal — the explicit goal
|
||||
// is to surface this gap for the Simon/Austin design discussion.
|
||||
|
||||
function registerNoteTypes() {
|
||||
class NoteNode extends LGraphNode {
|
||||
static override category: string
|
||||
static collapsable: boolean
|
||||
static title_mode: number
|
||||
|
||||
groupcolor = LGraphCanvas.node_colors.yellow.groupcolor
|
||||
override isVirtualNode: boolean
|
||||
|
||||
constructor(title: string) {
|
||||
super(title)
|
||||
// GAP-3: node colour should be settable via NodeHandle in nodeCreated.
|
||||
this.color = LGraphCanvas.node_colors.yellow.color
|
||||
this.bgcolor = LGraphCanvas.node_colors.yellow.bgcolor
|
||||
if (!this.properties) this.properties = { text: '' }
|
||||
// GAP-2: no v2 analog for widget addition at type-construction time.
|
||||
ComfyWidgets.STRING(
|
||||
this,
|
||||
'text',
|
||||
['STRING', { default: this.properties.text, multiline: true }],
|
||||
// @ts-expect-error app not available at this layer
|
||||
undefined
|
||||
)
|
||||
this.serialize_widgets = true
|
||||
this.isVirtualNode = true
|
||||
}
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType(
|
||||
'Note',
|
||||
Object.assign(NoteNode, {
|
||||
title_mode: LiteGraph.NORMAL_TITLE,
|
||||
title: 'Note',
|
||||
collapsable: true
|
||||
})
|
||||
)
|
||||
NoteNode.category = 'utils'
|
||||
|
||||
class MarkdownNoteNode extends LGraphNode {
|
||||
static override title = 'Markdown Note'
|
||||
groupcolor = LGraphCanvas.node_colors.yellow.groupcolor
|
||||
|
||||
constructor(title: string) {
|
||||
super(title)
|
||||
this.color = LGraphCanvas.node_colors.yellow.color
|
||||
this.bgcolor = LGraphCanvas.node_colors.yellow.bgcolor
|
||||
if (!this.properties) this.properties = { text: '' }
|
||||
ComfyWidgets.MARKDOWN(
|
||||
this,
|
||||
'text',
|
||||
['STRING', { default: this.properties.text }],
|
||||
// @ts-expect-error app not available at this layer
|
||||
undefined
|
||||
)
|
||||
this.serialize_widgets = true
|
||||
this.isVirtualNode = true
|
||||
}
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType('MarkdownNote', MarkdownNoteNode)
|
||||
MarkdownNoteNode.category = 'utils'
|
||||
}
|
||||
|
||||
// ── v2 registration ──────────────────────────────────────────────────────────
|
||||
|
||||
defineExtension({
|
||||
name: 'Comfy.NoteNode.V2',
|
||||
|
||||
setup() {
|
||||
// GAP-1: Custom node types must be registered here via LiteGraph directly.
|
||||
// In the intended v2 design this would be a `registerNodeTypes(registry)`
|
||||
// hook on ExtensionOptions where `registry.add('Note', NoteNodeDef)`.
|
||||
registerNoteTypes()
|
||||
}
|
||||
})
|
||||
49
src/extensions/core/previewAny.v2.ts
Normal file
49
src/extensions/core/previewAny.v2.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* PreviewAny — rewritten with the v2 extension API.
|
||||
*
|
||||
* Compare with previewAny.ts (v1) which uses beforeRegisterNodeDef +
|
||||
* prototype patching + manual callback chaining.
|
||||
*
|
||||
* v1: 90 lines, prototype.onNodeCreated override, prototype.onExecuted override
|
||||
* v2: 35 lines, no prototype access, no manual chaining
|
||||
*/
|
||||
|
||||
import { defineNodeExtension } from '@/extension-api'
|
||||
|
||||
defineNodeExtension({
|
||||
name: 'Comfy.PreviewAny.V2',
|
||||
nodeTypes: ['PreviewAny'],
|
||||
|
||||
nodeCreated(node) {
|
||||
const markdown = node.addWidget('MARKDOWN', 'preview_markdown', '', {
|
||||
hidden: true,
|
||||
readonly: true,
|
||||
serialize: false,
|
||||
label: 'Preview'
|
||||
})
|
||||
|
||||
const plaintext = node.addWidget('STRING', 'preview_text', '', {
|
||||
multiline: true,
|
||||
readonly: true,
|
||||
serialize: false,
|
||||
label: 'Preview'
|
||||
})
|
||||
|
||||
const toggle = node.addWidget('BOOLEAN', 'previewMode', false, {
|
||||
labelOn: 'Markdown',
|
||||
labelOff: 'Plaintext'
|
||||
})
|
||||
|
||||
toggle.on('valueChange', (e) => {
|
||||
markdown.setHidden(!e.newValue)
|
||||
plaintext.setHidden(e.newValue as boolean)
|
||||
})
|
||||
|
||||
node.on('executed', (e) => {
|
||||
const text = (e.output['text'] as string | string[]) ?? ''
|
||||
const content = Array.isArray(text) ? text.join('\n\n') : text
|
||||
markdown.setValue(content)
|
||||
plaintext.setValue(content)
|
||||
})
|
||||
}
|
||||
})
|
||||
342
src/extensions/core/rerouteNode.v2.ts
Normal file
342
src/extensions/core/rerouteNode.v2.ts
Normal file
@@ -0,0 +1,342 @@
|
||||
/**
|
||||
* RerouteNode — annotated port to the v2 extension API.
|
||||
*
|
||||
* v1 used `registerCustomNodes` to call `LiteGraph.registerNodeType()` with
|
||||
* a class that heavily overrides LiteGraph node behaviour (`onConnectionsChange`,
|
||||
* `clone`, `computeSize`, `getExtraMenuOptions`).
|
||||
*
|
||||
* RerouteNode is the *most v1-coupled* core extension: its entire value lives
|
||||
* in LiteGraph prototype methods. It is the intentional hard case for this
|
||||
* conversion exercise.
|
||||
*
|
||||
* What this file demonstrates to Simon/Austin:
|
||||
* 1. The `defineNodeExtension` pattern works only for *per-instance hooks*
|
||||
* — events that fire after a node exists. LiteGraph prototype overrides
|
||||
* (`onConnectionsChange`, `computeSize`, `clone`) fire synchronously
|
||||
* inside LiteGraph's own rendering loop and have no v2 equivalent.
|
||||
* 2. Custom context-menu contributions (`getExtraMenuOptions`) have no v2
|
||||
* surface. This is intentionally out of scope for the initial API.
|
||||
* 3. `localStorage` / settings persistence (`defaultVisibility`) works the
|
||||
* same in v2 — no v2 API involvement needed.
|
||||
*
|
||||
* API GAPS (feedback items for Simon/Austin):
|
||||
* GAP-1: (same as noteNode.v2) No `registerNodeTypes` hook — custom LiteGraph
|
||||
* node types cannot be registered via the v2 API.
|
||||
* GAP-7: No v2 hook for `onConnectionsChange`. This is a hot-path LiteGraph
|
||||
* callback that fires during canvas interaction. Mapping it to the v2
|
||||
* model would require an `NodeConnectedEvent` / `NodeDisconnectedEvent`
|
||||
* that fires SYNCHRONOUSLY and allows the handler to mutate outputs
|
||||
* and downstream nodes. Current v2 `node.on('connected')` is async-safe
|
||||
* and does not support synchronous output-type mutation.
|
||||
* GAP-8: No v2 surface for `getExtraMenuOptions` (context menu extension).
|
||||
* Would need an `onContextMenu(items)` hook on NodeExtensionOptions
|
||||
* that allows item injection.
|
||||
* GAP-9: `clone()` override. No v2 equivalent. If we want the cloned reroute
|
||||
* node to have its output reset, we'd need a post-copy lifecycle hook
|
||||
* (e.g. `nodeCopied(clone, source)`) which D12 explicitly deferred.
|
||||
* GAP-10: `computeSize()` override. Pure LiteGraph geometry; unlikely to
|
||||
* ever have a v2 equivalent. Extensions that need custom size should
|
||||
* either accept a fixed size or use a separate API.
|
||||
*
|
||||
* Conclusion: RerouteNode cannot be converted to pure v2 in the current API.
|
||||
* It is a LiteGraph-native "virtual node" with synchronous connection-type
|
||||
* propagation logic. The correct long-term path is to make RerouteNode a
|
||||
* first-class feature of the ComfyUI graph engine (not an extension at all)
|
||||
* and expose its behaviour through a higher-level abstraction.
|
||||
*
|
||||
* What *can* be expressed in v2 is shown in the `defineNodeExtension` block
|
||||
* below — the per-instance "user changed show/hide type" preference is a clean
|
||||
* v2 pattern. The rest remains in the v1 bridge.
|
||||
*/
|
||||
|
||||
import {
|
||||
LGraphCanvas,
|
||||
LGraphNode,
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { IContextMenuValue } from '@/lib/litegraph/src/litegraph'
|
||||
import type { ISlotType } from '@/lib/litegraph/src/interfaces'
|
||||
import { getWidgetConfig, mergeIfValid, setWidgetConfig } from './widgetInputs'
|
||||
import { defineExtension } from '@/extension-api'
|
||||
|
||||
// ── GAP-1: Interim bridge — LiteGraph node type registration ─────────────────
|
||||
|
||||
function registerRerouteType() {
|
||||
// Declaration-merging interface so the class gains `__outputType`.
|
||||
interface RerouteNode extends LGraphNode {
|
||||
__outputType?: string | number
|
||||
}
|
||||
|
||||
class RerouteNode extends LGraphNode {
|
||||
static override category: string | undefined
|
||||
static defaultVisibility = false
|
||||
|
||||
constructor(title?: string) {
|
||||
super(title ?? '')
|
||||
if (!this.properties) this.properties = {}
|
||||
this.properties.showOutputText = RerouteNode.defaultVisibility
|
||||
this.properties.horizontal = false
|
||||
this.addInput('', '*')
|
||||
this.addOutput(this.properties.showOutputText ? '*' : '', '*')
|
||||
this.setSize(this.computeSize())
|
||||
this.isVirtualNode = true
|
||||
}
|
||||
|
||||
override onAfterGraphConfigured() {
|
||||
requestAnimationFrame(() => {
|
||||
this.onConnectionsChange(LiteGraph.INPUT, undefined, true)
|
||||
})
|
||||
}
|
||||
|
||||
// GAP-9: This clone() override would need a v2 `nodeCopied` lifecycle hook.
|
||||
override clone(): LGraphNode | null {
|
||||
const cloned = super.clone()
|
||||
if (!cloned) return cloned
|
||||
cloned.removeOutput(0)
|
||||
cloned.addOutput(this.properties.showOutputText ? '*' : '', '*')
|
||||
cloned.setSize(cloned.computeSize())
|
||||
return cloned
|
||||
}
|
||||
|
||||
// GAP-7: onConnectionsChange cannot be expressed in v2 — synchronous
|
||||
// output-type mutation during connection is not supported by v2 event model.
|
||||
override onConnectionsChange(
|
||||
type: ISlotType,
|
||||
_index: number | undefined,
|
||||
connected: boolean
|
||||
) {
|
||||
const { graph } = this
|
||||
if (!graph) return
|
||||
// @ts-expect-error ComfyApp
|
||||
if (globalThis.app?.configuringGraph) return
|
||||
|
||||
if (connected && type === LiteGraph.OUTPUT) {
|
||||
const types = new Set(
|
||||
this.outputs[0].links
|
||||
?.map((l) => graph.links[l]?.type)
|
||||
?.filter((t) => t && t !== '*') ?? []
|
||||
)
|
||||
if (types.size > 1) {
|
||||
const linksToDisconnect = []
|
||||
for (const linkId of this.outputs[0].links ?? []) {
|
||||
linksToDisconnect.push(graph.links[linkId])
|
||||
}
|
||||
linksToDisconnect.pop()
|
||||
for (const link of linksToDisconnect) {
|
||||
if (!link) continue
|
||||
const node = graph.getNodeById(link.target_id)
|
||||
node?.disconnectInput(link.target_slot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let currentNode: RerouteNode | null = this
|
||||
let updateNodes: RerouteNode[] = []
|
||||
let inputType = null
|
||||
let inputNode = null
|
||||
while (currentNode) {
|
||||
updateNodes.unshift(currentNode)
|
||||
const linkId = currentNode.inputs[0].link
|
||||
if (linkId !== null) {
|
||||
const link = graph.links[linkId]
|
||||
if (!link) return
|
||||
const node = graph.getNodeById(link.origin_id)
|
||||
if (!node) return
|
||||
if (node instanceof RerouteNode) {
|
||||
if (node === this) {
|
||||
currentNode.disconnectInput(link.target_slot)
|
||||
currentNode = null
|
||||
} else {
|
||||
currentNode = node
|
||||
}
|
||||
} else {
|
||||
inputNode = currentNode
|
||||
inputType = node.outputs[link.origin_slot]?.type ?? null
|
||||
break
|
||||
}
|
||||
} else {
|
||||
currentNode = null
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const nodes: RerouteNode[] = [this]
|
||||
let outputType = null
|
||||
while (nodes.length) {
|
||||
currentNode = nodes.pop()!
|
||||
const outputs = currentNode.outputs?.[0]?.links ?? []
|
||||
for (const linkId of outputs) {
|
||||
const link = graph.links[linkId]
|
||||
if (!link) continue
|
||||
const node = graph.getNodeById(link.target_id)
|
||||
if (!node) continue
|
||||
if (node instanceof RerouteNode) {
|
||||
nodes.push(node)
|
||||
updateNodes.push(node)
|
||||
} else {
|
||||
const nodeInput = node.inputs[link.target_slot]
|
||||
const nodeOutType = nodeInput.type
|
||||
const keep =
|
||||
!inputType ||
|
||||
!nodeOutType ||
|
||||
LiteGraph.isValidConnection(inputType, nodeOutType)
|
||||
if (!keep) {
|
||||
node.disconnectInput(link.target_slot)
|
||||
continue
|
||||
}
|
||||
node.onConnectionsChange?.(
|
||||
LiteGraph.INPUT,
|
||||
link.target_slot,
|
||||
keep,
|
||||
link,
|
||||
nodeInput
|
||||
)
|
||||
outputType = node.inputs[link.target_slot].type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const displayType = inputType || outputType || '*'
|
||||
const color = LGraphCanvas.link_type_colors[displayType]
|
||||
|
||||
let widgetConfig
|
||||
let widgetType
|
||||
for (const node of updateNodes) {
|
||||
node.outputs[0].type = inputType || '*'
|
||||
node.__outputType = displayType
|
||||
node.outputs[0].name = node.properties.showOutputText ? `${displayType}` : ''
|
||||
node.setSize(node.computeSize())
|
||||
for (const l of node.outputs[0].links || []) {
|
||||
const link = graph.links[l]
|
||||
if (!link) continue
|
||||
link.color = color
|
||||
// @ts-expect-error ComfyApp
|
||||
if (globalThis.app?.configuringGraph) continue
|
||||
const targetNode = graph.getNodeById(link.target_id)
|
||||
if (!targetNode) continue
|
||||
const targetInput = targetNode.inputs?.[link.target_slot]
|
||||
if (targetInput?.widget) {
|
||||
const config = getWidgetConfig(targetInput)
|
||||
if (!widgetConfig) {
|
||||
widgetConfig = config[1] ?? {}
|
||||
widgetType = config[0]
|
||||
}
|
||||
const merged = mergeIfValid(targetInput, [config[0], widgetConfig])
|
||||
if (merged.customConfig) widgetConfig = merged.customConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of updateNodes) {
|
||||
if (widgetConfig && outputType) {
|
||||
node.inputs[0].widget = { name: 'value' }
|
||||
setWidgetConfig(node.inputs[0], [widgetType ?? `${displayType}`, widgetConfig])
|
||||
} else {
|
||||
setWidgetConfig(node.inputs[0], undefined)
|
||||
}
|
||||
}
|
||||
|
||||
if (inputNode?.inputs?.[0]?.link) {
|
||||
const link = graph.links[inputNode.inputs[0].link]
|
||||
if (link) link.color = color
|
||||
}
|
||||
}
|
||||
|
||||
// GAP-8: getExtraMenuOptions has no v2 equivalent.
|
||||
override getExtraMenuOptions(
|
||||
_: unknown,
|
||||
options: (IContextMenuValue | null)[]
|
||||
): IContextMenuValue[] {
|
||||
options.unshift(
|
||||
{
|
||||
content: (this.properties.showOutputText ? 'Hide' : 'Show') + ' Type',
|
||||
callback: () => {
|
||||
this.properties.showOutputText = !this.properties.showOutputText
|
||||
if (this.properties.showOutputText) {
|
||||
this.outputs[0].name = `${this.__outputType || this.outputs[0].type}`
|
||||
} else {
|
||||
this.outputs[0].name = ''
|
||||
}
|
||||
this.setSize(this.computeSize())
|
||||
// @ts-expect-error ComfyApp
|
||||
globalThis.app?.canvas?.setDirty(true, true)
|
||||
}
|
||||
},
|
||||
{
|
||||
content:
|
||||
(RerouteNode.defaultVisibility ? 'Hide' : 'Show') +
|
||||
' Type By Default',
|
||||
callback: () => {
|
||||
RerouteNode.setDefaultTextVisibility(!RerouteNode.defaultVisibility)
|
||||
}
|
||||
}
|
||||
)
|
||||
return []
|
||||
}
|
||||
|
||||
// GAP-10: computeSize override — no v2 surface.
|
||||
override computeSize(): [number, number] {
|
||||
return [
|
||||
this.properties.showOutputText && this.outputs?.length
|
||||
? Math.max(
|
||||
75,
|
||||
LiteGraph.NODE_TEXT_SIZE * this.outputs[0].name.length * 0.6 + 40
|
||||
)
|
||||
: 75,
|
||||
26
|
||||
]
|
||||
}
|
||||
|
||||
static setDefaultTextVisibility(visible: boolean) {
|
||||
RerouteNode.defaultVisibility = visible
|
||||
if (visible) {
|
||||
localStorage['Comfy.RerouteNode.DefaultVisibility'] = 'true'
|
||||
} else {
|
||||
delete localStorage['Comfy.RerouteNode.DefaultVisibility']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RerouteNode.setDefaultTextVisibility(
|
||||
!!localStorage['Comfy.RerouteNode.DefaultVisibility']
|
||||
)
|
||||
|
||||
LiteGraph.registerNodeType(
|
||||
'Reroute',
|
||||
Object.assign(RerouteNode, {
|
||||
title_mode: LiteGraph.NO_TITLE,
|
||||
title: 'Reroute',
|
||||
collapsable: false
|
||||
})
|
||||
)
|
||||
RerouteNode.category = 'utils'
|
||||
}
|
||||
|
||||
// ── v2: app-level registration (GAP-1 bridge) ─────────────────────────────────
|
||||
|
||||
defineExtension({
|
||||
name: 'Comfy.RerouteNode.V2',
|
||||
setup() {
|
||||
registerRerouteType()
|
||||
}
|
||||
})
|
||||
|
||||
// ── v2: what *can* be expressed cleanly ──────────────────────────────────────
|
||||
// The context-menu "Show/Hide Type" toggle persists a preference to localStorage.
|
||||
// In a fully realized v2 API this would live here. Today it's inside the
|
||||
// LiteGraph class because there's no v2 hook for per-node menu items (GAP-8).
|
||||
//
|
||||
// If GAP-7 (synchronous connection-type propagation) were solved, the
|
||||
// onConnectionsChange logic above could become:
|
||||
//
|
||||
// defineNodeExtension({
|
||||
// name: 'Comfy.RerouteNode.V2',
|
||||
// nodeTypes: ['Reroute'],
|
||||
// nodeCreated(node) {
|
||||
// node.on('connected', (e) => propagateType(node, e))
|
||||
// node.on('disconnected', (e) => propagateType(node, e))
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// That path requires the connected/disconnected events to be synchronous
|
||||
// and to carry a mutable output descriptor — a non-trivial API contract.
|
||||
59
src/extensions/core/slotDefaults.v2.ts
Normal file
59
src/extensions/core/slotDefaults.v2.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* SlotDefaults — rewritten with the v2 extension API.
|
||||
*
|
||||
* v1 used `init` + `beforeRegisterNodeDef` + direct `app.ui.settings.addSetting`.
|
||||
* v2 uses `defineExtension({ setup(ext) })`. The `ExtensionManager` passed to
|
||||
* `setup` exposes `setting.get/set` but NOT `addSetting` — that gap is noted below.
|
||||
*
|
||||
* What this file demonstrates to Simon/Austin:
|
||||
* 1. App-level extensions (init/setup) map cleanly to `defineExtension`.
|
||||
* 2. `beforeRegisterNodeDef` has no v2 equivalent — node type metadata is not
|
||||
* surfaced through the v2 API at registration time.
|
||||
* 3. `app.ui.settings.addSetting` (declares a new setting with slider + label)
|
||||
* has no v2 `ExtensionManager` surface.
|
||||
*
|
||||
* API GAPS (feedback items for Simon/Austin):
|
||||
* GAP-4: No `beforeRegisterNodeDef` hook on `ExtensionOptions`. This hook
|
||||
* fires *once per node type*, before any instance exists, giving access
|
||||
* to `nodeData` (input/output schema). Needed for type-level analysis
|
||||
* (e.g. slot type registry). Candidate: `onNodeTypeRegistered(typeDef)`.
|
||||
* GAP-5: `ExtensionManager.setting` exposes only `get/set`. It does NOT
|
||||
* expose `addSetting` (declare a new setting with UI metadata, type,
|
||||
* default, onChange callback). Needed for extensions that contribute
|
||||
* settings to the settings dialog. Candidate: extend the `setting`
|
||||
* interface with `add(spec: SettingSpec)`.
|
||||
* GAP-6: `LiteGraph.registered_slot_in_types` / `slot_types_out` are
|
||||
* global LiteGraph state mutated here. No v2 abstraction exists for
|
||||
* the "node suggestions" subsystem. Low priority — this is fine to
|
||||
* keep calling LiteGraph directly as an implementation detail.
|
||||
*
|
||||
* Interim strategy: `setup()` falls back to direct LiteGraph manipulation for
|
||||
* slot type data. The settings contribution stays as a TODO annotation until
|
||||
* GAP-5 is resolved.
|
||||
*/
|
||||
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { defineExtension } from '@/extension-api'
|
||||
|
||||
// ── v2 registration ──────────────────────────────────────────────────────────
|
||||
|
||||
defineExtension({
|
||||
name: 'Comfy.SlotDefaults.V2',
|
||||
|
||||
init() {
|
||||
LiteGraph.search_filter_enabled = true
|
||||
},
|
||||
|
||||
setup() {
|
||||
// GAP-5: In v1, `app.ui.settings.addSetting(spec)` declared a user-facing
|
||||
// slider in the settings dialog with an onChange callback. In v2,
|
||||
// `defineExtension({ setup })` takes no arguments — the ExtensionManager
|
||||
// is not yet plumbed into the setup callback. Until GAP-5 is resolved,
|
||||
// we cannot register the user-facing setting from a v2 extension.
|
||||
//
|
||||
// GAP-4: In v1, `beforeRegisterNodeDef(nodeType, nodeData)` processed each
|
||||
// node type's input/output schema. In v2 there is no equivalent hook.
|
||||
// The slot-type accumulator logic from v1 cannot be ported until
|
||||
// `onNodeTypeRegistered(def)` or equivalent is added to the API.
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user