mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-02 14:27:40 +00:00
seed widget2
This commit is contained in:
@@ -5,18 +5,12 @@ import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
|
||||
import { NumberControlMode } from '../composables/useNumberControl'
|
||||
|
||||
type ControlSettings = {
|
||||
linkToGlobal: boolean
|
||||
randomize: boolean
|
||||
increment: boolean
|
||||
decrement: boolean
|
||||
}
|
||||
import { NumberControlMode } from '../composables/useStepperControl'
|
||||
|
||||
type ControlOption = {
|
||||
key: keyof ControlSettings
|
||||
mode: NumberControlMode
|
||||
icon?: string
|
||||
title: string
|
||||
description: string
|
||||
@@ -25,33 +19,40 @@ type ControlOption = {
|
||||
|
||||
const popover = ref()
|
||||
const settingStore = useSettingStore()
|
||||
const dialogService = useDialogService()
|
||||
|
||||
const toggle = (event: Event) => {
|
||||
popover.value.toggle(event)
|
||||
}
|
||||
defineExpose({ toggle })
|
||||
|
||||
const ENABLE_LINK_TO_GLOBAL = false
|
||||
|
||||
const controlOptions: ControlOption[] = [
|
||||
...(ENABLE_LINK_TO_GLOBAL
|
||||
? ([
|
||||
{
|
||||
mode: NumberControlMode.LINK_TO_GLOBAL,
|
||||
icon: 'pi pi-link',
|
||||
title: 'linkToGlobal',
|
||||
description: 'linkToGlobalDesc'
|
||||
} satisfies ControlOption
|
||||
] as ControlOption[])
|
||||
: []),
|
||||
{
|
||||
key: 'linkToGlobal',
|
||||
icon: 'pi pi-link',
|
||||
title: 'linkToGlobal',
|
||||
description: 'linkToGlobalDesc'
|
||||
},
|
||||
{
|
||||
key: 'randomize',
|
||||
mode: NumberControlMode.RANDOMIZE,
|
||||
icon: 'icon-[lucide--shuffle]',
|
||||
title: 'randomize',
|
||||
description: 'randomizeDesc'
|
||||
},
|
||||
{
|
||||
key: 'increment',
|
||||
mode: NumberControlMode.INCREMENT,
|
||||
text: '+1',
|
||||
title: 'increment',
|
||||
description: 'incrementDesc'
|
||||
},
|
||||
{
|
||||
key: 'decrement',
|
||||
mode: NumberControlMode.DECREMENT,
|
||||
text: '-1',
|
||||
title: 'decrement',
|
||||
description: 'decrementDesc'
|
||||
@@ -70,16 +71,18 @@ const emit = defineEmits<{
|
||||
'update:controlMode': [mode: NumberControlMode]
|
||||
}>()
|
||||
|
||||
const handleToggle = (key: keyof ControlSettings) => {
|
||||
const newMode =
|
||||
props.controlMode === key
|
||||
? NumberControlMode.FIXED
|
||||
: (key as NumberControlMode)
|
||||
emit('update:controlMode', newMode)
|
||||
const handleToggle = (mode: NumberControlMode) => {
|
||||
if (props.controlMode === mode) return
|
||||
emit('update:controlMode', mode)
|
||||
}
|
||||
|
||||
const isActive = (key: keyof ControlSettings) => {
|
||||
return props.controlMode === key
|
||||
const isActive = (mode: NumberControlMode) => {
|
||||
return props.controlMode === mode
|
||||
}
|
||||
|
||||
const handleEditSettings = () => {
|
||||
popover.value.hide()
|
||||
dialogService.showSettingsDialog()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -101,7 +104,7 @@ const isActive = (key: keyof ControlSettings) => {
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="option in controlOptions"
|
||||
:key="option.key"
|
||||
:key="option.mode"
|
||||
class="flex items-center justify-between p-2 rounded"
|
||||
>
|
||||
<div class="flex gap-3 flex-1">
|
||||
@@ -115,7 +118,7 @@ const isActive = (key: keyof ControlSettings) => {
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-sm font-normal">
|
||||
<span v-if="option.key === 'linkToGlobal'">
|
||||
<span v-if="option.mode === NumberControlMode.LINK_TO_GLOBAL">
|
||||
{{ $t('widgets.numberControl.linkToGlobal') }}
|
||||
<em>{{ $t('widgets.numberControl.linkToGlobalSeed') }}</em>
|
||||
</span>
|
||||
@@ -129,16 +132,21 @@ const isActive = (key: keyof ControlSettings) => {
|
||||
</div>
|
||||
</div>
|
||||
<ToggleSwitch
|
||||
:model-value="isActive(option.key)"
|
||||
:model-value="isActive(option.mode)"
|
||||
class="flex-shrink-0"
|
||||
@update:model-value="handleToggle(option.key)"
|
||||
@update:model-value="handleToggle(option.mode)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="border-charcoal-400 border-1" />
|
||||
|
||||
<Button severity="secondary" size="small" class="w-full">
|
||||
<Button
|
||||
severity="secondary"
|
||||
size="small"
|
||||
class="w-full"
|
||||
@click="handleEditSettings"
|
||||
>
|
||||
<i class="pi pi-cog mr-2 text-xs" />
|
||||
{{ $t('widgets.numberControl.editSettings') }}
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import { computed } from 'vue'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
import { useNumberWidgetValue } from '@/composables/graph/useWidgetValue'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
import { useNumberStepCalculation } from '../composables/useNumberStepCalculation'
|
||||
import { useNumberWidgetButtonPt } from '../composables/useNumberWidgetButtonPt'
|
||||
import { WidgetInputBaseClass } from './layout'
|
||||
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
||||
@@ -29,6 +30,15 @@ const { localValue, onChange } = useNumberWidgetValue(
|
||||
emit
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
if (newValue !== localValue.value) {
|
||||
localValue.value = newValue
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, INPUT_EXCLUDED_PROPS)
|
||||
)
|
||||
@@ -41,28 +51,10 @@ const precision = computed(() => {
|
||||
})
|
||||
|
||||
// Calculate the step value based on precision or widget options
|
||||
const stepValue = computed(() => {
|
||||
// Use step2 (correct input spec value) instead of step (legacy 10x value)
|
||||
if (props.widget.options?.step2 !== undefined) {
|
||||
return Number(props.widget.options.step2)
|
||||
}
|
||||
// Otherwise, derive from precision
|
||||
if (precision.value !== undefined) {
|
||||
if (precision.value === 0) {
|
||||
return 1
|
||||
}
|
||||
// For precision > 0, step = 1 / (10^precision)
|
||||
// precision 1 → 0.1, precision 2 → 0.01, etc.
|
||||
return Number((1 / Math.pow(10, precision.value)).toFixed(precision.value))
|
||||
}
|
||||
// Default to 'any' for unrestricted stepping
|
||||
return 0
|
||||
})
|
||||
const stepValue = useNumberStepCalculation(props.widget.options, precision)
|
||||
|
||||
// Disable grouping separators by default unless explicitly enabled by the node author
|
||||
const useGrouping = computed(() => {
|
||||
return props.widget.options?.useGrouping === true
|
||||
})
|
||||
const useGrouping = computed(() => props.widget.options?.useGrouping === true)
|
||||
|
||||
// Check if increment/decrement buttons should be disabled due to precision limits
|
||||
const buttonsDisabled = computed(() => {
|
||||
@@ -73,6 +65,7 @@ const buttonsDisabled = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
// Tooltip message for disabled buttons
|
||||
const buttonTooltip = computed(() => {
|
||||
if (buttonsDisabled.value) {
|
||||
return 'Increment/decrement disabled: value exceeds JavaScript precision limit (±2^53)'
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
import { useNumberStepCalculation } from '../composables/useNumberStepCalculation'
|
||||
import { useNumberWidgetButtonPt } from '../composables/useNumberWidgetButtonPt'
|
||||
import { WidgetInputBaseClass } from './layout'
|
||||
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
||||
@@ -68,7 +69,7 @@ const updateLocalValue = (newValue: number[] | undefined): void => {
|
||||
}
|
||||
|
||||
const handleNumberInputUpdate = (newValue: number | undefined) => {
|
||||
if (newValue) {
|
||||
if (newValue !== undefined) {
|
||||
updateLocalValue([newValue])
|
||||
return
|
||||
}
|
||||
@@ -87,25 +88,7 @@ const precision = computed(() => {
|
||||
})
|
||||
|
||||
// Calculate the step value based on precision or widget options
|
||||
const stepValue = computed(() => {
|
||||
// Use step2 (correct input spec value) instead of step (legacy 10x value)
|
||||
if (widget.options?.step2 !== undefined) {
|
||||
return widget.options.step2
|
||||
}
|
||||
|
||||
// Otherwise, derive from precision
|
||||
if (precision.value === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (precision.value === 0) {
|
||||
return 1
|
||||
}
|
||||
|
||||
// For precision > 0, step = 1 / (10^precision)
|
||||
// precision 1 → 0.1, precision 2 → 0.01, etc.
|
||||
return 1 / Math.pow(10, precision.value)
|
||||
})
|
||||
const stepValue = useNumberStepCalculation(widget.options, precision, true)
|
||||
|
||||
const buttonsDisabled = computed(() => {
|
||||
const currentValue = localValue.value ?? 0
|
||||
|
||||
@@ -1,22 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { ref } from 'vue'
|
||||
import { defineAsyncComponent, ref } from 'vue'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
import { useNumberControl } from '../composables/useNumberControl'
|
||||
import NumberControlPopover from './NumberControlPopover.vue'
|
||||
import { useControlButtonIcon } from '../composables/useControlButtonIcon'
|
||||
import {
|
||||
NumberControlMode,
|
||||
useStepperControl
|
||||
} from '../composables/useStepperControl'
|
||||
import WidgetInputNumberInput from './WidgetInputNumberInput.vue'
|
||||
|
||||
const NumberControlPopover = defineAsyncComponent(
|
||||
() => import('./NumberControlPopover.vue')
|
||||
)
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<number>
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: number]
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<number>({ default: 0 })
|
||||
const popover = ref()
|
||||
|
||||
const { controlMode } = useNumberControl(modelValue, props.widget.options || {})
|
||||
const handleControlChange = (newValue: number) => {
|
||||
modelValue.value = newValue
|
||||
emit('update:modelValue', newValue)
|
||||
}
|
||||
|
||||
const { controlMode } = useStepperControl(modelValue, {
|
||||
...props.widget.options,
|
||||
onChange: handleControlChange
|
||||
})
|
||||
|
||||
if (controlMode.value === NumberControlMode.FIXED) {
|
||||
controlMode.value = NumberControlMode.RANDOMIZE
|
||||
}
|
||||
|
||||
const controlButtonIcon = useControlButtonIcon(controlMode)
|
||||
|
||||
const setControlMode = (mode: NumberControlMode) => {
|
||||
controlMode.value = mode
|
||||
}
|
||||
|
||||
const togglePopover = (event: Event) => {
|
||||
popover.value.toggle(event)
|
||||
@@ -37,13 +66,13 @@ const togglePopover = (event: Event) => {
|
||||
class="absolute right-12 top-1/2 -translate-y-1/2 h-4 w-7 p-0 bg-blue-100/30 rounded-xl"
|
||||
@click="togglePopover"
|
||||
>
|
||||
<i class="icon-[lucide--shuffle] text-blue-100" />
|
||||
<i :class="`${controlButtonIcon} text-blue-100 text-xs`" />
|
||||
</Button>
|
||||
|
||||
<NumberControlPopover
|
||||
ref="popover"
|
||||
:control-mode="controlMode"
|
||||
@update:control-mode="controlMode = $event"
|
||||
@update:control-mode="setControlMode"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,126 +1,110 @@
|
||||
<template>
|
||||
<WidgetSelectDropdown
|
||||
v-if="isDropdownUIWidget"
|
||||
v-bind="props"
|
||||
:asset-kind="assetKind"
|
||||
:allow-upload="allowUpload"
|
||||
:upload-folder="uploadFolder"
|
||||
:is-asset-mode="isAssetMode"
|
||||
:default-layout-mode="defaultLayoutMode"
|
||||
@update:model-value="handleUpdateModelValue"
|
||||
/>
|
||||
<WidgetSelectDefault
|
||||
v-else
|
||||
:widget="widget"
|
||||
:model-value="modelValue"
|
||||
@update:model-value="handleUpdateModelValue"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import Button from 'primevue/button'
|
||||
import { type Ref, computed, defineAsyncComponent, ref } from 'vue'
|
||||
|
||||
import { assetService } from '@/platform/assets/services/assetService'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import WidgetSelectDefault from '@/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue'
|
||||
import WidgetSelectDropdown from '@/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue'
|
||||
import type { LayoutMode } from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/types'
|
||||
import type { ResultItemType } from '@/schemas/apiSchema'
|
||||
import { isComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { ComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import type { AssetKind } from '@/types/widgetTypes'
|
||||
|
||||
import { useComboControl } from '../composables/useComboControl'
|
||||
import { useControlButtonIcon } from '../composables/useControlButtonIcon'
|
||||
import { NumberControlMode } from '../composables/useStepperControl'
|
||||
import WidgetSelectBase from './WidgetSelectBase.vue'
|
||||
|
||||
const NumberControlPopover = defineAsyncComponent(
|
||||
() => import('./NumberControlPopover.vue')
|
||||
)
|
||||
|
||||
type ComboValue = string | number | undefined
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<string | number | undefined>
|
||||
modelValue: string | number | undefined
|
||||
nodeType?: string
|
||||
widget: SimplifiedWidget<ComboValue>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string | number | undefined]
|
||||
'update:modelValue': [value: ComboValue]
|
||||
}>()
|
||||
|
||||
function handleUpdateModelValue(value: string | number | undefined) {
|
||||
const modelValue = defineModel<ComboValue>({ default: undefined })
|
||||
const modelRef = modelValue as Ref<ComboValue>
|
||||
|
||||
const hasControlAfterGenerate = computed(
|
||||
() => props.widget.spec?.control_after_generate === true
|
||||
)
|
||||
|
||||
const popover = ref()
|
||||
|
||||
const availableValues = computed<ComboValue[]>(() => {
|
||||
const values = props.widget.options?.values
|
||||
if (Array.isArray(values)) return values
|
||||
if (typeof values === 'function') {
|
||||
try {
|
||||
const result = values()
|
||||
return Array.isArray(result) ? result : []
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
if (values && typeof values === 'object') {
|
||||
return Object.values(values) as ComboValue[]
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
const updateModelValue = (value: ComboValue) => {
|
||||
modelRef.value = value
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
const comboSpec = computed<ComboInputSpec | undefined>(() => {
|
||||
if (props.widget.spec && isComboInputSpec(props.widget.spec)) {
|
||||
return props.widget.spec
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
const controlModeRef: Ref<NumberControlMode> = hasControlAfterGenerate.value
|
||||
? (() => {
|
||||
const comboControl = useComboControl(modelRef, {
|
||||
values: availableValues,
|
||||
onChange: updateModelValue
|
||||
})
|
||||
if (comboControl.controlMode.value === NumberControlMode.FIXED) {
|
||||
comboControl.controlMode.value = NumberControlMode.RANDOMIZE
|
||||
}
|
||||
return comboControl.controlMode
|
||||
})()
|
||||
: ref(NumberControlMode.FIXED)
|
||||
|
||||
const specDescriptor = computed<{
|
||||
kind: AssetKind
|
||||
allowUpload: boolean
|
||||
folder: ResultItemType | undefined
|
||||
}>(() => {
|
||||
const spec = comboSpec.value
|
||||
if (!spec) {
|
||||
return {
|
||||
kind: 'unknown',
|
||||
allowUpload: false,
|
||||
folder: undefined
|
||||
}
|
||||
}
|
||||
const controlButtonIcon = useControlButtonIcon(controlModeRef)
|
||||
|
||||
const {
|
||||
image_upload,
|
||||
animated_image_upload,
|
||||
video_upload,
|
||||
image_folder,
|
||||
audio_upload
|
||||
} = spec
|
||||
const togglePopover = (event: Event) => {
|
||||
popover.value?.toggle(event)
|
||||
}
|
||||
|
||||
let kind: AssetKind = 'unknown'
|
||||
if (video_upload) {
|
||||
kind = 'video'
|
||||
} else if (image_upload || animated_image_upload) {
|
||||
kind = 'image'
|
||||
} else if (audio_upload) {
|
||||
kind = 'audio'
|
||||
}
|
||||
// TODO: add support for models (checkpoints, VAE, LoRAs, etc.) -- get widgetType from spec
|
||||
|
||||
const allowUpload =
|
||||
image_upload === true ||
|
||||
animated_image_upload === true ||
|
||||
video_upload === true ||
|
||||
audio_upload === true
|
||||
return {
|
||||
kind,
|
||||
allowUpload,
|
||||
folder: image_folder
|
||||
}
|
||||
})
|
||||
|
||||
const isAssetMode = computed(() => {
|
||||
if (isCloud) {
|
||||
const settingStore = useSettingStore()
|
||||
const isUsingAssetAPI = settingStore.get('Comfy.Assets.UseAssetAPI')
|
||||
const isEligible = assetService.isAssetBrowserEligible(
|
||||
props.nodeType,
|
||||
props.widget.name
|
||||
)
|
||||
|
||||
return isUsingAssetAPI && isEligible
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
const assetKind = computed(() => specDescriptor.value.kind)
|
||||
const isDropdownUIWidget = computed(
|
||||
() => isAssetMode.value || assetKind.value !== 'unknown'
|
||||
)
|
||||
const allowUpload = computed(() => specDescriptor.value.allowUpload)
|
||||
const uploadFolder = computed<ResultItemType>(() => {
|
||||
return specDescriptor.value.folder ?? 'input'
|
||||
})
|
||||
const defaultLayoutMode = computed<LayoutMode>(() => {
|
||||
return isAssetMode.value ? 'list' : 'grid'
|
||||
})
|
||||
const setControlMode = (mode: NumberControlMode) => {
|
||||
controlModeRef.value = mode
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="hasControlAfterGenerate" class="relative">
|
||||
<WidgetSelectBase
|
||||
:widget="widget"
|
||||
:model-value="modelValue"
|
||||
@update:model-value="updateModelValue"
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="link"
|
||||
size="small"
|
||||
class="absolute right-12 top-1/2 -translate-y-1/2 h-4 w-7 p-0 bg-blue-100/30 rounded-xl"
|
||||
@click="togglePopover"
|
||||
>
|
||||
<i :class="`${controlButtonIcon} text-blue-100 text-xs`" />
|
||||
</Button>
|
||||
|
||||
<NumberControlPopover
|
||||
ref="popover"
|
||||
:control-mode="controlModeRef"
|
||||
@update:control-mode="setControlMode"
|
||||
/>
|
||||
</div>
|
||||
<WidgetSelectBase
|
||||
v-else
|
||||
:widget="widget"
|
||||
:model-value="modelValue"
|
||||
@update:model-value="updateModelValue"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<WidgetSelectDropdown
|
||||
v-if="isDropdownUIWidget"
|
||||
v-bind="props"
|
||||
:asset-kind="assetKind"
|
||||
:allow-upload="allowUpload"
|
||||
:upload-folder="uploadFolder"
|
||||
@update:model-value="handleUpdateModelValue"
|
||||
/>
|
||||
<WidgetSelectDefault
|
||||
v-else
|
||||
v-bind="props"
|
||||
@update:model-value="handleUpdateModelValue"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { ResultItemType } from '@/schemas/apiSchema'
|
||||
import {
|
||||
type ComboInputSpec,
|
||||
isComboInputSpec
|
||||
} from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import type { AssetKind } from '@/types/widgetTypes'
|
||||
|
||||
import WidgetSelectDefault from './WidgetSelectDefault.vue'
|
||||
import WidgetSelectDropdown from './WidgetSelectDropdown.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<string | number | undefined>
|
||||
modelValue: string | number | undefined
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string | number | undefined]
|
||||
}>()
|
||||
|
||||
function handleUpdateModelValue(value: string | number | undefined) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
const comboSpec = computed<ComboInputSpec | undefined>(() => {
|
||||
if (props.widget.spec && isComboInputSpec(props.widget.spec)) {
|
||||
return props.widget.spec
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const specDescriptor = computed<{
|
||||
kind: AssetKind
|
||||
allowUpload: boolean
|
||||
folder: ResultItemType | undefined
|
||||
}>(() => {
|
||||
const spec = comboSpec.value
|
||||
if (!spec) {
|
||||
return {
|
||||
kind: 'unknown',
|
||||
allowUpload: false,
|
||||
folder: undefined
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
image_upload,
|
||||
animated_image_upload,
|
||||
video_upload,
|
||||
image_folder,
|
||||
audio_upload
|
||||
} = spec
|
||||
|
||||
let kind: AssetKind = 'unknown'
|
||||
if (video_upload) {
|
||||
kind = 'video'
|
||||
} else if (image_upload || animated_image_upload) {
|
||||
kind = 'image'
|
||||
} else if (audio_upload) {
|
||||
kind = 'audio'
|
||||
}
|
||||
// TODO: add support for models (checkpoints, VAE, LoRAs, etc.) -- get widgetType from spec
|
||||
|
||||
const allowUpload =
|
||||
image_upload === true ||
|
||||
animated_image_upload === true ||
|
||||
video_upload === true ||
|
||||
audio_upload === true
|
||||
return {
|
||||
kind,
|
||||
allowUpload,
|
||||
folder: image_folder
|
||||
}
|
||||
})
|
||||
|
||||
const assetKind = computed(() => specDescriptor.value.kind)
|
||||
const isDropdownUIWidget = computed(() => assetKind.value !== 'unknown')
|
||||
const allowUpload = computed(() => specDescriptor.value.allowUpload)
|
||||
const uploadFolder = computed<ResultItemType>(() => {
|
||||
return specDescriptor.value.folder ?? 'input'
|
||||
})
|
||||
</script>
|
||||
@@ -150,12 +150,6 @@ const outputItems = computed<DropdownItem[]>(() => {
|
||||
}))
|
||||
})
|
||||
|
||||
const allItems = computed<DropdownItem[]>(() => {
|
||||
if (props.isAssetMode && assetData) {
|
||||
return assetData.dropdownItems.value
|
||||
}
|
||||
return [...inputItems.value, ...outputItems.value]
|
||||
})
|
||||
const dropdownItems = computed<DropdownItem[]>(() => {
|
||||
if (props.isAssetMode) {
|
||||
return allItems.value
|
||||
@@ -168,7 +162,7 @@ const dropdownItems = computed<DropdownItem[]>(() => {
|
||||
return outputItems.value
|
||||
case 'all':
|
||||
default:
|
||||
return allItems.value
|
||||
return [...inputItems.value, ...outputItems.value]
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { type ComputedRef, type Ref, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import { numberControlRegistry } from '../services/NumberControlRegistry'
|
||||
import { NumberControlMode } from './useStepperControl'
|
||||
|
||||
type ComboValue = string | number | undefined
|
||||
|
||||
interface ComboControlOptions {
|
||||
values: ComputedRef<ComboValue[]>
|
||||
onChange?: (value: ComboValue) => void
|
||||
}
|
||||
|
||||
export function useComboControl(
|
||||
modelValue: Ref<ComboValue>,
|
||||
options: ComboControlOptions
|
||||
) {
|
||||
const controlMode = ref<NumberControlMode>(NumberControlMode.FIXED)
|
||||
const controlId = Symbol('comboControl')
|
||||
|
||||
const applyControl = () => {
|
||||
const choices = options.values.value.filter(
|
||||
(value): value is string | number => value !== undefined
|
||||
)
|
||||
if (!choices.length) return
|
||||
|
||||
const currentIndex = Math.max(
|
||||
0,
|
||||
choices.findIndex((value) => value === modelValue.value)
|
||||
)
|
||||
|
||||
let nextValue: ComboValue = modelValue.value
|
||||
|
||||
switch (controlMode.value) {
|
||||
case NumberControlMode.FIXED:
|
||||
return
|
||||
case NumberControlMode.INCREMENT:
|
||||
nextValue = choices[Math.min(currentIndex + 1, choices.length - 1)]
|
||||
break
|
||||
case NumberControlMode.DECREMENT:
|
||||
nextValue = choices[Math.max(currentIndex - 1, 0)]
|
||||
break
|
||||
case NumberControlMode.RANDOMIZE:
|
||||
nextValue = choices[Math.floor(Math.random() * choices.length)]
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
if (options.onChange) {
|
||||
options.onChange(nextValue)
|
||||
} else {
|
||||
modelValue.value = nextValue
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
numberControlRegistry.register(controlId, applyControl)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
numberControlRegistry.unregister(controlId)
|
||||
})
|
||||
|
||||
return {
|
||||
controlMode,
|
||||
applyControl
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { type Ref, computed } from 'vue'
|
||||
|
||||
import { NumberControlMode } from './useStepperControl'
|
||||
|
||||
export 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.RANDOMIZE:
|
||||
return 'icon-[lucide--shuffle]'
|
||||
case NumberControlMode.LINK_TO_GLOBAL:
|
||||
return 'pi pi-link'
|
||||
default:
|
||||
return 'icon-[lucide--shuffle]'
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { type Ref, computed } 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,
|
||||
precision: Ref<number | undefined>,
|
||||
returnUndefinedForDefault = false
|
||||
) {
|
||||
return computed(() => {
|
||||
// Use step2 (correct input spec value) instead of step (legacy 10x value)
|
||||
if (options?.step2 !== undefined) {
|
||||
return Number(options.step2)
|
||||
}
|
||||
|
||||
if (precision.value === undefined) {
|
||||
return returnUndefinedForDefault ? undefined : 0
|
||||
}
|
||||
|
||||
if (precision.value === 0) return 1
|
||||
|
||||
// For precision > 0, step = 1 / (10^precision)
|
||||
const step = 1 / Math.pow(10, precision.value)
|
||||
return returnUndefinedForDefault
|
||||
? step
|
||||
: Number(step.toFixed(precision.value))
|
||||
})
|
||||
}
|
||||
@@ -12,45 +12,53 @@ export enum NumberControlMode {
|
||||
LINK_TO_GLOBAL = 'linkToGlobal'
|
||||
}
|
||||
|
||||
interface NumberControlOptions {
|
||||
interface StepperControlOptions {
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
step2?: number
|
||||
onChange?: (value: number) => void
|
||||
}
|
||||
|
||||
export { executeNumberControls } from '../services/NumberControlRegistry'
|
||||
|
||||
export function useNumberControl(
|
||||
export function useStepperControl(
|
||||
modelValue: Ref<number>,
|
||||
options: NumberControlOptions
|
||||
options: StepperControlOptions
|
||||
) {
|
||||
const controlMode = ref<NumberControlMode>(NumberControlMode.FIXED)
|
||||
const controlId = Symbol('numberControl')
|
||||
const globalSeedStore = useGlobalSeedStore()
|
||||
|
||||
const applyControl = () => {
|
||||
const { min = 0, max = 1000000, step = 1 } = options
|
||||
const { min = 0, max = 1000000, step2, step = 1, onChange } = options
|
||||
// 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
|
||||
break
|
||||
return
|
||||
case NumberControlMode.INCREMENT:
|
||||
modelValue.value = Math.min(max, modelValue.value + step)
|
||||
newValue = Math.min(max, modelValue.value + actualStep)
|
||||
break
|
||||
case NumberControlMode.DECREMENT:
|
||||
modelValue.value = Math.max(min, modelValue.value - step)
|
||||
newValue = Math.max(min, modelValue.value - actualStep)
|
||||
break
|
||||
case NumberControlMode.RANDOMIZE:
|
||||
modelValue.value = Math.floor(Math.random() * (max - min + 1)) + min
|
||||
newValue = Math.floor(Math.random() * (max - min + 1)) + min
|
||||
break
|
||||
case NumberControlMode.LINK_TO_GLOBAL:
|
||||
// Use global seed value, constrained by min/max
|
||||
modelValue.value = Math.max(
|
||||
min,
|
||||
Math.min(max, globalSeedStore.globalSeed)
|
||||
)
|
||||
newValue = Math.max(min, Math.min(max, globalSeedStore.globalSeed))
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
if (onChange) {
|
||||
onChange(newValue)
|
||||
} else {
|
||||
modelValue.value = newValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import { ref } from 'vue'
|
||||
|
||||
import {
|
||||
NumberControlMode,
|
||||
useNumberControl
|
||||
} from '@/renderer/extensions/vueNodes/widgets/composables/useNumberControl'
|
||||
useStepperControl
|
||||
} from '@/renderer/extensions/vueNodes/widgets/composables/useStepperControl'
|
||||
|
||||
// Mock the global seed store
|
||||
vi.mock('@/stores/globalSeedStore', () => ({
|
||||
@@ -29,7 +29,7 @@ vi.mock(
|
||||
})
|
||||
)
|
||||
|
||||
describe('useNumberControl', () => {
|
||||
describe('useStepperControl', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
@@ -40,7 +40,7 @@ describe('useNumberControl', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = { min: 0, max: 1000, step: 1 }
|
||||
|
||||
const { controlMode } = useNumberControl(modelValue, options)
|
||||
const { controlMode } = useStepperControl(modelValue, options)
|
||||
|
||||
expect(controlMode.value).toBe(NumberControlMode.FIXED)
|
||||
})
|
||||
@@ -49,7 +49,7 @@ describe('useNumberControl', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = { min: 0, max: 1000, step: 1 }
|
||||
|
||||
const { controlMode, applyControl } = useNumberControl(
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
@@ -64,7 +64,7 @@ describe('useNumberControl', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = { min: 0, max: 1000, step: 1 }
|
||||
|
||||
const { applyControl } = useNumberControl(modelValue, options)
|
||||
const { applyControl } = useStepperControl(modelValue, options)
|
||||
|
||||
applyControl()
|
||||
expect(modelValue.value).toBe(100)
|
||||
@@ -74,7 +74,7 @@ describe('useNumberControl', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = { min: 0, max: 1000, step: 5 }
|
||||
|
||||
const { controlMode, applyControl } = useNumberControl(
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
@@ -88,7 +88,7 @@ describe('useNumberControl', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = { min: 0, max: 1000, step: 5 }
|
||||
|
||||
const { controlMode, applyControl } = useNumberControl(
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
@@ -102,7 +102,7 @@ describe('useNumberControl', () => {
|
||||
const modelValue = ref(995)
|
||||
const options = { min: 0, max: 1000, step: 10 }
|
||||
|
||||
const { controlMode, applyControl } = useNumberControl(
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
@@ -116,7 +116,7 @@ describe('useNumberControl', () => {
|
||||
const modelValue = ref(5)
|
||||
const options = { min: 0, max: 1000, step: 10 }
|
||||
|
||||
const { controlMode, applyControl } = useNumberControl(
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
@@ -130,7 +130,7 @@ describe('useNumberControl', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = { min: 0, max: 10, step: 1 }
|
||||
|
||||
const { controlMode, applyControl } = useNumberControl(
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
@@ -159,7 +159,7 @@ describe('useNumberControl', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = { min: 0, max: 100000, step: 1 }
|
||||
|
||||
const { controlMode, applyControl } = useNumberControl(
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
@@ -173,7 +173,7 @@ describe('useNumberControl', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = { min: 20000, max: 50000, step: 1 }
|
||||
|
||||
const { controlMode, applyControl } = useNumberControl(
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
@@ -189,7 +189,7 @@ describe('useNumberControl', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = {} // Empty options
|
||||
|
||||
const { controlMode, applyControl } = useNumberControl(
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
@@ -203,7 +203,7 @@ describe('useNumberControl', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = {} // Empty options - should use defaults
|
||||
|
||||
const { controlMode, applyControl } = useNumberControl(
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
@@ -216,4 +216,50 @@ describe('useNumberControl', () => {
|
||||
expect(modelValue.value).toBeLessThanOrEqual(1000000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('onChange callback', () => {
|
||||
it('should call onChange callback when provided', () => {
|
||||
const modelValue = ref(100)
|
||||
const onChange = vi.fn()
|
||||
const options = { min: 0, max: 1000, step: 1, onChange }
|
||||
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
controlMode.value = NumberControlMode.INCREMENT
|
||||
|
||||
applyControl()
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(101)
|
||||
})
|
||||
|
||||
it('should fallback to direct assignment when onChange not provided', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = { min: 0, max: 1000, step: 1 } // No onChange
|
||||
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
controlMode.value = NumberControlMode.INCREMENT
|
||||
|
||||
applyControl()
|
||||
|
||||
expect(modelValue.value).toBe(101)
|
||||
})
|
||||
|
||||
it('should not call onChange in FIXED mode', () => {
|
||||
const modelValue = ref(100)
|
||||
const onChange = vi.fn()
|
||||
const options = { min: 0, max: 1000, step: 1, onChange }
|
||||
|
||||
const { applyControl } = useStepperControl(modelValue, options)
|
||||
// controlMode remains FIXED by default
|
||||
|
||||
applyControl()
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user