diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 3f6eb807966..c32eb78770c 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1948,6 +1948,8 @@ "addToForm": "Add to Form", "label": "Label", "showDescription": "Show Description", + "showShuffle": "Show Shuffle", + "shuffle": "Shuffle", "component": "Component", "numberInput": "Number Input", "singleLine": "Single Line", diff --git a/invokeai/frontend/web/src/common/util/randomFloat.ts b/invokeai/frontend/web/src/common/util/randomFloat.ts new file mode 100644 index 00000000000..084eede6bc8 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/randomFloat.ts @@ -0,0 +1,5 @@ +const randomFloat = (min: number, max: number): number => { + return Math.random() * (max - min + Number.EPSILON) + min; +}; + +export default randomFloat; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/FloatFieldInput.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/FloatFieldInput.tsx index e7df094ee3f..e0c1cd70903 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/FloatFieldInput.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/FloatFieldInput.tsx @@ -1,35 +1,43 @@ -import { CompositeNumberInput } from '@invoke-ai/ui-library'; +import { Button, CompositeNumberInput } from '@invoke-ai/ui-library'; import { useFloatField } from 'features/nodes/components/flow/nodes/Invocation/fields/FloatField/useFloatField'; import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types'; import { NO_DRAG_CLASS } from 'features/nodes/types/constants'; import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field'; import type { NodeFieldFloatSettings } from 'features/nodes/types/workflow'; import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiShuffleBold } from 'react-icons/pi'; export const FloatFieldInput = memo( ( props: FieldComponentProps ) => { const { nodeId, field, fieldTemplate, settings } = props; - const { defaultValue, onChange, min, max, step, fineStep } = useFloatField( - nodeId, - field.name, - fieldTemplate, - settings - ); + const { defaultValue, onChange, min, max, step, fineStep, constrainValue, showShuffle, randomizeValue } = + useFloatField(nodeId, field.name, fieldTemplate, settings); + + const { t } = useTranslation(); return ( - + <> + + {showShuffle && ( + + )} + ); } ); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/FloatFieldInputAndSlider.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/FloatFieldInputAndSlider.tsx index df9ff4eeab3..7d1d1806d70 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/FloatFieldInputAndSlider.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/FloatFieldInputAndSlider.tsx @@ -1,22 +1,22 @@ -import { CompositeNumberInput, CompositeSlider } from '@invoke-ai/ui-library'; +import { Button, CompositeNumberInput, CompositeSlider } from '@invoke-ai/ui-library'; import { useFloatField } from 'features/nodes/components/flow/nodes/Invocation/fields/FloatField/useFloatField'; import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types'; import { NO_DRAG_CLASS } from 'features/nodes/types/constants'; import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field'; import type { NodeFieldFloatSettings } from 'features/nodes/types/workflow'; import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiShuffleBold } from 'react-icons/pi'; export const FloatFieldInputAndSlider = memo( ( props: FieldComponentProps ) => { const { nodeId, field, fieldTemplate, settings } = props; - const { defaultValue, onChange, min, max, step, fineStep } = useFloatField( - nodeId, - field.name, - fieldTemplate, - settings - ); + const { defaultValue, onChange, min, max, step, fineStep, constrainValue, showShuffle, randomizeValue } = + useFloatField(nodeId, field.name, fieldTemplate, settings); + + const { t } = useTranslation(); return ( <> @@ -43,7 +43,13 @@ export const FloatFieldInputAndSlider = memo( fineStep={fineStep} className={NO_DRAG_CLASS} flex="1 1 0" + constrainValue={constrainValue} /> + {showShuffle && ( + + )} ); } diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/FloatFieldSlider.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/FloatFieldSlider.tsx index e368efc5503..ba0ffb84648 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/FloatFieldSlider.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/FloatFieldSlider.tsx @@ -1,37 +1,48 @@ -import { CompositeSlider } from '@invoke-ai/ui-library'; +import { Button, CompositeSlider } from '@invoke-ai/ui-library'; import { useFloatField } from 'features/nodes/components/flow/nodes/Invocation/fields/FloatField/useFloatField'; import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types'; import { NO_DRAG_CLASS } from 'features/nodes/types/constants'; import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field'; import type { NodeFieldFloatSettings } from 'features/nodes/types/workflow'; import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiShuffleBold } from 'react-icons/pi'; export const FloatFieldSlider = memo( ( props: FieldComponentProps ) => { const { nodeId, field, fieldTemplate, settings } = props; - const { defaultValue, onChange, min, max, step, fineStep } = useFloatField( + const { defaultValue, onChange, min, max, step, fineStep, showShuffle, randomizeValue } = useFloatField( nodeId, field.name, fieldTemplate, settings ); + const { t } = useTranslation(); + return ( - + <> + + {showShuffle && ( + + )} + ); } ); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/useFloatField.ts b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/useFloatField.ts index 8e85ec3eb02..b4f242fc8df 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/useFloatField.ts +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/useFloatField.ts @@ -1,5 +1,6 @@ import { NUMPY_RAND_MAX } from 'app/constants'; import { useAppDispatch } from 'app/store/storeHooks'; +import randomFloat from 'common/util/randomFloat'; import { roundDownToMultiple, roundUpToMultiple } from 'common/util/roundDownToMultiple'; import { isNil } from 'es-toolkit/compat'; import { fieldFloatValueChanged } from 'features/nodes/store/nodesSlice'; @@ -11,9 +12,9 @@ export const useFloatField = ( nodeId: string, fieldName: string, fieldTemplate: FloatFieldInputTemplate, - overrides: { min?: number; max?: number; step?: number } = {} + overrides: { showShuffle?: boolean; min?: number; max?: number; step?: number } = {} ) => { - const { min: overrideMin, max: overrideMax, step: overrideStep } = overrides; + const { showShuffle, min: overrideMin, max: overrideMax, step: overrideStep } = overrides; const dispatch = useAppDispatch(); const step = useMemo(() => { @@ -77,6 +78,11 @@ export const useFloatField = ( [dispatch, fieldName, nodeId] ); + const randomizeValue = useCallback(() => { + const value = Number((Math.round(randomFloat(min, max) / step) * step).toFixed(10)); + dispatch(fieldFloatValueChanged({ nodeId, fieldName, value })); + }, [dispatch, fieldName, nodeId, min, max, step]); + return { defaultValue: fieldTemplate.default, onChange, @@ -85,5 +91,7 @@ export const useFloatField = ( step, fineStep, constrainValue, + showShuffle, + randomizeValue, }; }; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldInput.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldInput.tsx index 9ca2819bf34..20ff9bd09e1 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldInput.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldInput.tsx @@ -1,10 +1,12 @@ -import { CompositeNumberInput } from '@invoke-ai/ui-library'; +import { Button, CompositeNumberInput } from '@invoke-ai/ui-library'; import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types'; import { useIntegerField } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField'; import { NO_DRAG_CLASS } from 'features/nodes/types/constants'; import type { IntegerFieldInputInstance, IntegerFieldInputTemplate } from 'features/nodes/types/field'; import type { NodeFieldIntegerSettings } from 'features/nodes/types/workflow'; import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiShuffleBold } from 'react-icons/pi'; export const IntegerFieldInput = memo( ( @@ -15,26 +17,31 @@ export const IntegerFieldInput = memo( > ) => { const { nodeId, field, fieldTemplate, settings } = props; - const { defaultValue, onChange, min, max, step, fineStep, constrainValue } = useIntegerField( - nodeId, - field.name, - fieldTemplate, - settings - ); + const { defaultValue, onChange, min, max, step, fineStep, constrainValue, showShuffle, randomizeValue } = + useIntegerField(nodeId, field.name, fieldTemplate, settings); + + const { t } = useTranslation(); return ( - + <> + + {showShuffle && ( + + )} + ); } ); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldInputAndSlider.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldInputAndSlider.tsx index 715f40d92d9..1e78943be15 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldInputAndSlider.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldInputAndSlider.tsx @@ -1,10 +1,12 @@ -import { CompositeNumberInput, CompositeSlider } from '@invoke-ai/ui-library'; +import { Button, CompositeNumberInput, CompositeSlider } from '@invoke-ai/ui-library'; import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types'; import { useIntegerField } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField'; import { NO_DRAG_CLASS } from 'features/nodes/types/constants'; import type { IntegerFieldInputInstance, IntegerFieldInputTemplate } from 'features/nodes/types/field'; import type { NodeFieldIntegerSettings } from 'features/nodes/types/workflow'; import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiShuffleBold } from 'react-icons/pi'; export const IntegerFieldInputAndSlider = memo( ( @@ -15,12 +17,10 @@ export const IntegerFieldInputAndSlider = memo( > ) => { const { nodeId, field, fieldTemplate, settings } = props; - const { defaultValue, onChange, min, max, step, fineStep } = useIntegerField( - nodeId, - field.name, - fieldTemplate, - settings - ); + const { defaultValue, onChange, min, max, step, fineStep, constrainValue, showShuffle, randomizeValue } = + useIntegerField(nodeId, field.name, fieldTemplate, settings); + + const { t } = useTranslation(); return ( <> @@ -47,7 +47,13 @@ export const IntegerFieldInputAndSlider = memo( fineStep={fineStep} className={NO_DRAG_CLASS} flex="1 1 0" + constrainValue={constrainValue} /> + {showShuffle && ( + + )} ); } diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldSlider.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldSlider.tsx index fb9379d7b1d..97faa3e34fd 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldSlider.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/IntegerFieldSlider.tsx @@ -1,10 +1,12 @@ -import { CompositeSlider } from '@invoke-ai/ui-library'; +import { Button, CompositeSlider } from '@invoke-ai/ui-library'; import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types'; import { useIntegerField } from 'features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField'; import { NO_DRAG_CLASS } from 'features/nodes/types/constants'; import type { IntegerFieldInputInstance, IntegerFieldInputTemplate } from 'features/nodes/types/field'; import type { NodeFieldIntegerSettings } from 'features/nodes/types/workflow'; import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiShuffleBold } from 'react-icons/pi'; export const IntegerFieldSlider = memo( ( @@ -15,27 +17,36 @@ export const IntegerFieldSlider = memo( > ) => { const { nodeId, field, fieldTemplate, settings } = props; - const { defaultValue, onChange, min, max, step, fineStep } = useIntegerField( + const { defaultValue, onChange, min, max, step, fineStep, showShuffle, randomizeValue } = useIntegerField( nodeId, field.name, fieldTemplate, settings ); + const { t } = useTranslation(); + return ( - + <> + + {showShuffle && ( + + )} + ); } ); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField.ts b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField.ts index 51b1f3baff3..ec0882e5db6 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField.ts +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField.ts @@ -1,5 +1,6 @@ import { NUMPY_RAND_MAX } from 'app/constants'; import { useAppDispatch } from 'app/store/storeHooks'; +import randomInt from 'common/util/randomInt'; import { roundDownToMultiple, roundUpToMultiple } from 'common/util/roundDownToMultiple'; import { isNil } from 'es-toolkit/compat'; import { fieldIntegerValueChanged } from 'features/nodes/store/nodesSlice'; @@ -11,9 +12,9 @@ export const useIntegerField = ( nodeId: string, fieldName: string, fieldTemplate: IntegerFieldInputTemplate, - overrides: { min?: number; max?: number; step?: number } = {} + overrides: { showShuffle?: boolean; min?: number; max?: number; step?: number } = {} ) => { - const { min: overrideMin, max: overrideMax, step: overrideStep } = overrides; + const { showShuffle, min: overrideMin, max: overrideMax, step: overrideStep } = overrides; const dispatch = useAppDispatch(); const step = useMemo(() => { @@ -65,8 +66,7 @@ export const useIntegerField = ( }, [fieldTemplate.exclusiveMaximum, fieldTemplate.maximum, overrideMax, step]); const constrainValue = useCallback( - (v: number) => - constrainNumber(v, { min, max, step: step }, { min: overrideMin, max: overrideMax, step: overrideStep }), + (v: number) => constrainNumber(v, { min, max, step }, { min: overrideMin, max: overrideMax, step: overrideStep }), [max, min, overrideMax, overrideMin, overrideStep, step] ); @@ -77,6 +77,11 @@ export const useIntegerField = ( [dispatch, fieldName, nodeId] ); + const randomizeValue = useCallback(() => { + const value = Math.round(randomInt(min, max) / step) * step; + dispatch(fieldIntegerValueChanged({ nodeId, fieldName, value })); + }, [dispatch, fieldName, nodeId, min, max, step]); + return { defaultValue: fieldTemplate.default, onChange, @@ -85,5 +90,7 @@ export const useIntegerField = ( step, fineStep, constrainValue, + showShuffle, + randomizeValue, }; }; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementFloatSettings.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementFloatSettings.tsx index 9485987ae16..8b5a0f7ae81 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementFloatSettings.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementFloatSettings.tsx @@ -22,6 +22,7 @@ type Props = { export const NodeFieldElementFloatSettings = memo(({ id, settings, nodeId, fieldName, fieldTemplate }: Props) => { return ( <> + { + const { showShuffle } = settings; + + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const toggleShowShuffle = useCallback(() => { + const newSettings: NodeFieldFloatSettings = { + ...settings, + showShuffle: !showShuffle, + }; + dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } })); + }, [dispatch, id, settings, showShuffle]); + + return ( + + {t('workflows.builder.showShuffle')} + + + ); +}); +SettingShuffle.displayName = 'SettingShuffle'; + const SettingComponent = memo(({ id, settings }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementIntegerSettings.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementIntegerSettings.tsx index 8fe6414ef93..c7032eeb2b3 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementIntegerSettings.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementIntegerSettings.tsx @@ -23,6 +23,7 @@ type Props = { export const NodeFieldElementIntegerSettings = memo(({ id, settings, nodeId, fieldName, fieldTemplate }: Props) => { return ( <> + { + const { showShuffle } = settings; + + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const toggleShowShuffle = useCallback(() => { + const newSettings: NodeFieldIntegerSettings = { + ...settings, + showShuffle: !showShuffle, + }; + dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newSettings } })); + }, [dispatch, id, settings, showShuffle]); + + return ( + + {t('workflows.builder.showShuffle')} + + + ); +}); +SettingShuffle.displayName = 'SettingShuffle'; + const SettingComponent = memo(({ id, settings }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/nodes/types/workflow.ts b/invokeai/frontend/web/src/features/nodes/types/workflow.ts index 5f73f8fcb02..a241a5e8fe8 100644 --- a/invokeai/frontend/web/src/features/nodes/types/workflow.ts +++ b/invokeai/frontend/web/src/features/nodes/types/workflow.ts @@ -74,6 +74,7 @@ export const NODE_FIELD_CLASS_NAME = `form-builder-${NODE_FIELD_TYPE}`; const FLOAT_FIELD_SETTINGS_TYPE = 'float-field-config'; const zNodeFieldFloatSettings = z.object({ type: z.literal(FLOAT_FIELD_SETTINGS_TYPE).default(FLOAT_FIELD_SETTINGS_TYPE), + showShuffle: z.boolean().default(false), component: zNumberComponent.default('number-input'), min: z.number().optional(), max: z.number().optional(), @@ -84,6 +85,7 @@ export type NodeFieldFloatSettings = z.infer; const INTEGER_FIELD_CONFIG_TYPE = 'integer-field-config'; const zNodeFieldIntegerSettings = z.object({ type: z.literal(INTEGER_FIELD_CONFIG_TYPE).default(INTEGER_FIELD_CONFIG_TYPE), + showShuffle: z.boolean().default(false), component: zNumberComponent.default('number-input'), min: z.number().optional(), max: z.number().optional(),