Support "control after generate" in vue (#6985)

Continuation of #6034 with
- Updated synchronization for seed
- Properly truncates the displayed widget value for the button
- Synchronizes control after generate state with litegraph and allows
for serialization

Several issues from original PR have not (yet) been addressed, but are
likely better moved to future PR
- fix step value being 10 (legacy system)
- ensure it works with COMBO (Fixed in #7095)
- ensure it works with FLOAT (Fixed in #7095)
- either implement or remove the config button functionality - think it
should open settings?

<img width="280" height="694" alt="image"
src="https://github.com/user-attachments/assets/f36f1cb0-237d-4bfc-bff1-e4976775cf98"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6985-Support-control-after-generate-in-vue-2b86d73d365081d8b01ce489d887ff00)
by [Unito](https://www.unito.io)

---------

Co-authored-by: bymyself <cbyrne@comfy.org>
Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
AustinMroz
2025-12-13 04:23:56 -08:00
committed by GitHub
parent aab9a30511
commit b22ba97a13
47 changed files with 990 additions and 61 deletions

View File

@@ -13,16 +13,16 @@ import {
import { assetService } from '@/platform/assets/services/assetService'
import { isCloud } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration'
import { isComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type {
ComboInputSpec,
InputSpec
} from '@/schemas/nodeDef/nodeDefSchemaV2'
import { isComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration'
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
import type { BaseDOMWidget } from '@/scripts/domWidget'
import { addValueControlWidgets } from '@/scripts/widgets'
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
import { addValueControlWidgets } from '@/scripts/widgets'
import { useAssetsStore } from '@/stores/assetsStore'
import { getMediaTypeFromFilename } from '@/utils/formatUtil'
@@ -69,6 +69,16 @@ const addMultiSelectWidget = (
addWidget(node, widget as BaseDOMWidget<object | string>)
// TODO: Add remote support to multi-select widget
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/3003
if (inputSpec.control_after_generate) {
widget.linkedWidgets = addValueControlWidgets(
node,
widget,
'fixed',
undefined,
transformInputSpecV2ToV1(inputSpec)
)
}
return widget
}

View File

@@ -6,6 +6,8 @@ import { useSettingStore } from '@/platform/settings/settingStore'
import { isFloatInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
import { addValueControlWidget } from '@/scripts/widgets'
import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration'
function onFloatValueChange(this: INumericWidget, v: number) {
const round = this.options.round
@@ -55,7 +57,7 @@ export const useFloatWidget = () => {
/** Assertion {@link inputSpec.default} */
const defaultValue = (inputSpec.default as number | undefined) ?? 0
return node.addWidget(
const widget = node.addWidget(
widgetType,
inputSpec.name,
defaultValue,
@@ -73,6 +75,20 @@ export const useFloatWidget = () => {
precision
}
)
if (inputSpec.control_after_generate) {
const controlWidget = addValueControlWidget(
node,
widget,
'fixed',
undefined,
undefined,
transformInputSpecV2ToV1(inputSpec)
)
widget.linkedWidgets = [controlWidget]
}
return widget
}
return widgetConstructor

View File

@@ -1,11 +1,11 @@
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { INumericWidget } from '@/lib/litegraph/src/types/widgets'
import { useSettingStore } from '@/platform/settings/settingStore'
import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration'
import { isIntInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { addValueControlWidget } from '@/scripts/widgets'
import { isIntInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
import { addValueControlWidget } from '@/scripts/widgets'
import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration'
function onValueChange(this: INumericWidget, v: number) {
// For integers, always round to the nearest step
@@ -69,14 +69,10 @@ export const useIntWidget = () => {
const controlAfterGenerate =
inputSpec.control_after_generate ??
/**
* Compatibility with legacy node convention. Int input with name
* 'seed' or 'noise_seed' get automatically added a control widget.
*/
['seed', 'noise_seed'].includes(inputSpec.name)
if (controlAfterGenerate) {
const seedControl = addValueControlWidget(
const controlWidget = addValueControlWidget(
node,
widget,
'randomize',
@@ -84,7 +80,7 @@ export const useIntWidget = () => {
undefined,
transformInputSpecV2ToV1(inputSpec)
)
widget.linkedWidgets = [seedControl]
widget.linkedWidgets = [controlWidget]
}
return widget

View File

@@ -0,0 +1,35 @@
import { computed, toValue } from 'vue'
import type { MaybeRefOrGetter } from 'vue'
interface NumberWidgetOptions {
step2?: number
precision?: number
}
/**
* Shared composable for calculating step values in number input widgets
* Handles both explicit step2 values and precision-derived steps
*/
export function useNumberStepCalculation(
options: NumberWidgetOptions | undefined,
precisionArg: MaybeRefOrGetter<number | undefined>,
returnUndefinedForDefault = false
) {
return computed(() => {
const precision = toValue(precisionArg)
// Use step2 (correct input spec value) instead of step (legacy 10x value)
if (options?.step2 !== undefined) {
return Number(options.step2)
}
if (precision === undefined) {
return returnUndefinedForDefault ? undefined : 0
}
if (precision === 0) return 1
// For precision > 0, step = 1 / (10^precision)
const step = 1 / Math.pow(10, precision)
return returnUndefinedForDefault ? step : Number(step.toFixed(precision))
})
}

View File

@@ -0,0 +1,111 @@
import { computed, onMounted, onUnmounted, ref } from 'vue'
import type { Ref } from 'vue'
import type { ControlOptions } from '@/types/simplifiedWidget'
import { numberControlRegistry } from '../services/NumberControlRegistry'
export enum NumberControlMode {
FIXED = 'fixed',
INCREMENT = 'increment',
DECREMENT = 'decrement',
RANDOMIZE = 'randomize',
LINK_TO_GLOBAL = 'linkToGlobal'
}
interface StepperControlOptions {
min?: number
max?: number
step?: number
step2?: number
onChange?: (value: number) => void
}
function convertToEnum(str?: ControlOptions): NumberControlMode {
switch (str) {
case 'fixed':
return NumberControlMode.FIXED
case 'increment':
return NumberControlMode.INCREMENT
case 'decrement':
return NumberControlMode.DECREMENT
case 'randomize':
return NumberControlMode.RANDOMIZE
}
return NumberControlMode.RANDOMIZE
}
function useControlButtonIcon(controlMode: Ref<NumberControlMode>) {
return computed(() => {
switch (controlMode.value) {
case NumberControlMode.INCREMENT:
return 'pi pi-plus'
case NumberControlMode.DECREMENT:
return 'pi pi-minus'
case NumberControlMode.FIXED:
return 'icon-[lucide--pencil-off]'
case NumberControlMode.LINK_TO_GLOBAL:
return 'pi pi-link'
default:
return 'icon-[lucide--shuffle]'
}
})
}
export function useStepperControl(
modelValue: Ref<number>,
options: StepperControlOptions,
defaultValue?: ControlOptions
) {
const controlMode = ref<NumberControlMode>(convertToEnum(defaultValue))
const controlId = Symbol('numberControl')
const applyControl = () => {
const { min = 0, max = 1000000, step2, step = 1, onChange } = options
const safeMax = Math.min(2 ** 50, max)
const safeMin = Math.max(-(2 ** 50), min)
// Use step2 if available (widget context), otherwise use step as-is (direct API usage)
const actualStep = step2 !== undefined ? step2 : step
let newValue: number
switch (controlMode.value) {
case NumberControlMode.FIXED:
// Do nothing - keep current value
return
case NumberControlMode.INCREMENT:
newValue = Math.min(safeMax, modelValue.value + actualStep)
break
case NumberControlMode.DECREMENT:
newValue = Math.max(safeMin, modelValue.value - actualStep)
break
case NumberControlMode.RANDOMIZE:
newValue = Math.floor(Math.random() * (safeMax - safeMin + 1)) + safeMin
break
default:
return
}
if (onChange) {
onChange(newValue)
} else {
modelValue.value = newValue
}
}
// Register with singleton registry
onMounted(() => {
numberControlRegistry.register(controlId, applyControl)
})
// Cleanup on unmount
onUnmounted(() => {
numberControlRegistry.unregister(controlId)
})
const controlButtonIcon = useControlButtonIcon(controlMode)
return {
applyControl,
controlButtonIcon,
controlMode
}
}