Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix: ensure node input order upon creation using input_order
  • Loading branch information
simula-r committed Aug 20, 2025
commit 4552cdecc7929366c4e24ec455524ac303e5b157
41 changes: 18 additions & 23 deletions src/schemas/nodeDef/migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
isComboInputSpec,
isComboInputSpecV1
} from '@/schemas/nodeDefSchema'
import { getOrderedInputNames } from '@/utils/nodeDefOrderingUtil'

/**
* Transforms a V1 node definition to V2 format
Expand All @@ -23,32 +22,28 @@ export function transformNodeDefV1ToV2(
// Transform inputs
const inputs: Record<string, InputSpecV2> = {}

// Process required inputs in the correct order
// Process required inputs
if (nodeDefV1.input?.required) {
const orderedNames = getOrderedInputNames(nodeDefV1, 'required')
orderedNames.forEach((name) => {
const inputSpecV1 = nodeDefV1.input!.required![name]
if (inputSpecV1) {
inputs[name] = transformInputSpecV1ToV2(inputSpecV1, {
name,
isOptional: false
})
}
})
for (const [name, inputSpecV1] of Object.entries(
nodeDefV1.input.required
)) {
inputs[name] = transformInputSpecV1ToV2(inputSpecV1, {
name,
isOptional: false
})
}
}

// Process optional inputs in the correct order
// Process optional inputs
if (nodeDefV1.input?.optional) {
const orderedNames = getOrderedInputNames(nodeDefV1, 'optional')
orderedNames.forEach((name) => {
const inputSpecV1 = nodeDefV1.input!.optional![name]
if (inputSpecV1) {
inputs[name] = transformInputSpecV1ToV2(inputSpecV1, {
name,
isOptional: true
})
}
})
for (const [name, inputSpecV1] of Object.entries(
nodeDefV1.input.optional
)) {
inputs[name] = transformInputSpecV1ToV2(inputSpecV1, {
name,
isOptional: true
})
}
}

// Transform outputs
Expand Down
19 changes: 15 additions & 4 deletions src/services/litegraphService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
isVideoNode,
migrateWidgetsValues
} from '@/utils/litegraphUtil'
import { getOrderedInputSpecs } from '@/utils/nodeDefOrderingUtil'

import { useExtensionService } from './extensionService'

Expand Down Expand Up @@ -248,9 +249,14 @@ export const useLitegraphService = () => {
* @internal Add inputs to the node.
*/
#addInputs(inputs: Record<string, InputSpec>) {
for (const inputSpec of Object.values(inputs))
// Use input_order if available to ensure consistent widget ordering
const nodeDefImpl = ComfyNode.nodeData as ComfyNodeDefImpl
const orderedInputSpecs = getOrderedInputSpecs(nodeDefImpl, inputs)

// Create sockets and widgets in the determined order
for (const inputSpec of orderedInputSpecs)
this.#addInputSocket(inputSpec)
for (const inputSpec of Object.values(inputs))
for (const inputSpec of orderedInputSpecs)
this.#addInputWidget(inputSpec)
}

Expand Down Expand Up @@ -508,9 +514,14 @@ export const useLitegraphService = () => {
* @internal Add inputs to the node.
*/
#addInputs(inputs: Record<string, InputSpec>) {
for (const inputSpec of Object.values(inputs))
// Use input_order if available to ensure consistent widget ordering
const nodeDefImpl = ComfyNode.nodeData as ComfyNodeDefImpl
const orderedInputSpecs = getOrderedInputSpecs(nodeDefImpl, inputs)

// Create sockets and widgets in the determined order
for (const inputSpec of orderedInputSpecs)
this.#addInputSocket(inputSpec)
for (const inputSpec of Object.values(inputs))
for (const inputSpec of orderedInputSpecs)
this.#addInputWidget(inputSpec)
}

Expand Down
108 changes: 38 additions & 70 deletions src/utils/nodeDefOrderingUtil.ts
Original file line number Diff line number Diff line change
@@ -1,86 +1,54 @@
import { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'

/**
* Sorts the inputs of a node definition according to the input_order property.
* If input_order is not provided, returns inputs unchanged (maintains backward compatibility).
* Gets an ordered array of InputSpec objects based on input_order.
* This is designed to work with V2 format used by litegraphService.
*
* @param nodeDef - The node definition containing inputs and potentially input_order
* @returns A new object with sorted inputs, or the original if no sorting needed
* @param nodeDefImpl - The ComfyNodeDefImpl containing both V1 and V2 formats
* @param inputs - The V2 format inputs (flat Record<string, InputSpec>)
* @returns Array of InputSpec objects in the correct order
*/
export function sortNodeInputsByOrder(nodeDef: ComfyNodeDef): ComfyNodeDef {
// If no input_order or no inputs, return as-is
if (!nodeDef.input_order || !nodeDef.input) {
return nodeDef
export function getOrderedInputSpecs(
nodeDefImpl: ComfyNodeDefImpl,
inputs: Record<string, InputSpec>
): InputSpec[] {
const orderedInputSpecs: InputSpec[] = []

// If no input_order, return default Object.values order
if (!nodeDefImpl.input_order) {
return Object.values(inputs)
}

const sortedNodeDef = { ...nodeDef }

// Sort each input category (required, optional, hidden)
if (sortedNodeDef.input) {
sortedNodeDef.input = { ...sortedNodeDef.input }

// Process each category that has an order defined
for (const category of ['required', 'optional', 'hidden'] as const) {
const order = nodeDef.input_order[category]
const inputs = sortedNodeDef.input[category]

if (order && inputs) {
// Create a new sorted object based on the order
const sortedInputs: Record<string, any> = {}

// First, add inputs in the specified order
for (const inputName of order) {
if (inputName in inputs) {
sortedInputs[inputName] = inputs[inputName]
}
}

// Then add any remaining inputs not in the order (for safety)
for (const [inputName, inputSpec] of Object.entries(inputs)) {
if (!(inputName in sortedInputs)) {
sortedInputs[inputName] = inputSpec
}
}

sortedNodeDef.input[category] = sortedInputs
// Process required inputs in specified order
if (nodeDefImpl.input_order.required) {
for (const name of nodeDefImpl.input_order.required) {
const inputSpec = inputs[name]
if (inputSpec && !inputSpec.isOptional) {
orderedInputSpecs.push(inputSpec)
}
}
}

return sortedNodeDef
}

/**
* Gets an ordered array of input names for a given category.
* Uses input_order if available, otherwise returns Object.keys() order.
*
* @param nodeDef - The node definition
* @param category - The input category ('required', 'optional', or 'hidden')
* @returns Array of input names in the correct order
*/
export function getOrderedInputNames(
nodeDef: ComfyNodeDef,
category: 'required' | 'optional' | 'hidden'
): string[] {
const inputs = nodeDef.input?.[category]
if (!inputs) return []

// Use input_order if available
const order = nodeDef.input_order?.[category]
if (order) {
// Filter to only include inputs that actually exist
const existingInputs = order.filter((name) => name in inputs)

// Add any inputs not in the order (shouldn't happen, but for safety)
const remainingInputs = Object.keys(inputs).filter(
(name) => !order.includes(name)
)
// Process optional inputs in specified order
if (nodeDefImpl.input_order.optional) {
for (const name of nodeDefImpl.input_order.optional) {
const inputSpec = inputs[name]
if (inputSpec && inputSpec.isOptional) {
orderedInputSpecs.push(inputSpec)
}
}
}

return [...existingInputs, ...remainingInputs]
// Add any remaining inputs not specified in input_order
const processedNames = new Set(orderedInputSpecs.map((spec) => spec.name))
for (const inputSpec of Object.values(inputs)) {
if (!processedNames.has(inputSpec.name)) {
orderedInputSpecs.push(inputSpec)
}
}

// Fallback to Object.keys order
return Object.keys(inputs)
return orderedInputSpecs
}

/**
Expand Down
Loading