diff --git a/src/renderer/extensions/vueNodes/widgets/components/NumberControlPopover.vue b/src/renderer/extensions/vueNodes/widgets/components/NumberControlPopover.vue index 0a2ed7fdf..70b716248 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/NumberControlPopover.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/NumberControlPopover.vue @@ -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() } @@ -101,7 +104,7 @@ const isActive = (key: keyof ControlSettings) => {
@@ -115,7 +118,7 @@ const isActive = (key: keyof ControlSettings) => {
- + {{ $t('widgets.numberControl.linkToGlobal') }} {{ $t('widgets.numberControl.linkToGlobalSeed') }} @@ -129,16 +132,21 @@ const isActive = (key: keyof ControlSettings) => {

- diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue index 9f44d285a..43a705eff 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue @@ -1,6 +1,6 @@ + + diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectBase.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectBase.vue new file mode 100644 index 000000000..dcdc4c9ce --- /dev/null +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectBase.vue @@ -0,0 +1,101 @@ + + + diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue index d46bcd0f0..b880e9676 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue @@ -150,12 +150,6 @@ const outputItems = computed(() => { })) }) -const allItems = computed(() => { - if (props.isAssetMode && assetData) { - return assetData.dropdownItems.value - } - return [...inputItems.value, ...outputItems.value] -}) const dropdownItems = computed(() => { if (props.isAssetMode) { return allItems.value @@ -168,7 +162,7 @@ const dropdownItems = computed(() => { return outputItems.value case 'all': default: - return allItems.value + return [...inputItems.value, ...outputItems.value] } }) diff --git a/src/renderer/extensions/vueNodes/widgets/composables/useComboControl.ts b/src/renderer/extensions/vueNodes/widgets/composables/useComboControl.ts new file mode 100644 index 000000000..cb17ea841 --- /dev/null +++ b/src/renderer/extensions/vueNodes/widgets/composables/useComboControl.ts @@ -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 + onChange?: (value: ComboValue) => void +} + +export function useComboControl( + modelValue: Ref, + options: ComboControlOptions +) { + const controlMode = ref(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 + } +} diff --git a/src/renderer/extensions/vueNodes/widgets/composables/useControlButtonIcon.ts b/src/renderer/extensions/vueNodes/widgets/composables/useControlButtonIcon.ts new file mode 100644 index 000000000..b5655e345 --- /dev/null +++ b/src/renderer/extensions/vueNodes/widgets/composables/useControlButtonIcon.ts @@ -0,0 +1,20 @@ +import { type Ref, computed } from 'vue' + +import { NumberControlMode } from './useStepperControl' + +export function useControlButtonIcon(controlMode: Ref) { + 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]' + } + }) +} diff --git a/src/renderer/extensions/vueNodes/widgets/composables/useNumberStepCalculation.ts b/src/renderer/extensions/vueNodes/widgets/composables/useNumberStepCalculation.ts new file mode 100644 index 000000000..8ed286bbb --- /dev/null +++ b/src/renderer/extensions/vueNodes/widgets/composables/useNumberStepCalculation.ts @@ -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, + 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)) + }) +} diff --git a/src/renderer/extensions/vueNodes/widgets/composables/useNumberControl.ts b/src/renderer/extensions/vueNodes/widgets/composables/useStepperControl.ts similarity index 61% rename from src/renderer/extensions/vueNodes/widgets/composables/useNumberControl.ts rename to src/renderer/extensions/vueNodes/widgets/composables/useStepperControl.ts index c99092bba..8db20aae2 100644 --- a/src/renderer/extensions/vueNodes/widgets/composables/useNumberControl.ts +++ b/src/renderer/extensions/vueNodes/widgets/composables/useStepperControl.ts @@ -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, - options: NumberControlOptions + options: StepperControlOptions ) { const controlMode = ref(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 } } diff --git a/tests-ui/tests/renderer/extensions/vueNodes/widgets/composables/useNumberControl.test.ts b/tests-ui/tests/renderer/extensions/vueNodes/widgets/composables/useStepperControl.test.ts similarity index 71% rename from tests-ui/tests/renderer/extensions/vueNodes/widgets/composables/useNumberControl.test.ts rename to tests-ui/tests/renderer/extensions/vueNodes/widgets/composables/useStepperControl.test.ts index 025c8329f..f62dc1359 100644 --- a/tests-ui/tests/renderer/extensions/vueNodes/widgets/composables/useNumberControl.test.ts +++ b/tests-ui/tests/renderer/extensions/vueNodes/widgets/composables/useStepperControl.test.ts @@ -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() + }) + }) })