Skip to content

Commit 965bcba

Browse files
feat(ui): configurable form field constraints (WIP)
1 parent c9f2460 commit 965bcba

20 files changed

+849
-246
lines changed

invokeai/frontend/web/public/locales/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1768,7 +1768,9 @@
17681768
"containerPlaceholder": "Empty Container",
17691769
"headingPlaceholder": "Empty Heading",
17701770
"textPlaceholder": "Empty Text",
1771-
"workflowBuilderAlphaWarning": "The workflow builder is currently in alpha. There may be breaking changes before the stable release."
1771+
"workflowBuilderAlphaWarning": "The workflow builder is currently in alpha. There may be breaking changes before the stable release.",
1772+
"minimum": "Minimum",
1773+
"maximum": "Maximum"
17721774
}
17731775
},
17741776
"controlLayers": {

invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/FloatFieldInput.tsx

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,35 @@ import { useFloatField } from 'features/nodes/components/flow/nodes/Invocation/f
33
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
44
import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
55
import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field';
6+
import type { NodeFieldFloatSettings } from 'features/nodes/types/workflow';
67
import { memo } from 'react';
78

8-
export const FloatFieldInput = memo((props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate>) => {
9-
const { defaultValue, onChange, value, min, max, step, fineStep } = useFloatField(props);
9+
export const FloatFieldInput = memo(
10+
(
11+
props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate, { settings?: NodeFieldFloatSettings }>
12+
) => {
13+
const { nodeId, field, fieldTemplate, settings } = props;
14+
const { defaultValue, onChange, min, max, step, fineStep } = useFloatField(
15+
nodeId,
16+
field.name,
17+
fieldTemplate,
18+
settings
19+
);
1020

11-
return (
12-
<CompositeNumberInput
13-
defaultValue={defaultValue}
14-
onChange={onChange}
15-
value={value}
16-
min={min}
17-
max={max}
18-
step={step}
19-
fineStep={fineStep}
20-
className={NO_DRAG_CLASS}
21-
flex="1 1 0"
22-
/>
23-
);
24-
});
21+
return (
22+
<CompositeNumberInput
23+
defaultValue={defaultValue}
24+
onChange={onChange}
25+
value={field.value}
26+
min={min}
27+
max={max}
28+
step={step}
29+
fineStep={fineStep}
30+
className={NO_DRAG_CLASS}
31+
flex="1 1 0"
32+
/>
33+
);
34+
}
35+
);
2536

2637
FloatFieldInput.displayName = 'FloatFieldInput ';

invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/FloatFieldInputAndSlider.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,27 @@ import { useFloatField } from 'features/nodes/components/flow/nodes/Invocation/f
33
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
44
import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
55
import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field';
6+
import type { NodeFieldFloatSettings } from 'features/nodes/types/workflow';
67
import { memo } from 'react';
78

89
export const FloatFieldInputAndSlider = memo(
9-
(props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate>) => {
10-
const { defaultValue, onChange, value, min, max, step, fineStep } = useFloatField(props);
10+
(
11+
props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate, { settings?: NodeFieldFloatSettings }>
12+
) => {
13+
const { nodeId, field, fieldTemplate, settings } = props;
14+
const { defaultValue, onChange, min, max, step, fineStep } = useFloatField(
15+
nodeId,
16+
field.name,
17+
fieldTemplate,
18+
settings
19+
);
1120

1221
return (
1322
<>
1423
<CompositeSlider
1524
defaultValue={defaultValue}
1625
onChange={onChange}
17-
value={value}
26+
value={field.value}
1827
min={min}
1928
max={max}
2029
step={step}
@@ -27,7 +36,7 @@ export const FloatFieldInputAndSlider = memo(
2736
<CompositeNumberInput
2837
defaultValue={defaultValue}
2938
onChange={onChange}
30-
value={value}
39+
value={field.value}
3140
min={min}
3241
max={max}
3342
step={step}

invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/FloatFieldSlider.tsx

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,37 @@ import { useFloatField } from 'features/nodes/components/flow/nodes/Invocation/f
33
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
44
import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
55
import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field';
6+
import type { NodeFieldFloatSettings } from 'features/nodes/types/workflow';
67
import { memo } from 'react';
78

8-
export const FloatFieldSlider = memo((props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate>) => {
9-
const { defaultValue, onChange, value, min, max, step, fineStep } = useFloatField(props);
9+
export const FloatFieldSlider = memo(
10+
(
11+
props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate, { settings?: NodeFieldFloatSettings }>
12+
) => {
13+
const { nodeId, field, fieldTemplate, settings } = props;
14+
const { defaultValue, onChange, min, max, step, fineStep } = useFloatField(
15+
nodeId,
16+
field.name,
17+
fieldTemplate,
18+
settings
19+
);
1020

11-
return (
12-
<CompositeSlider
13-
defaultValue={defaultValue}
14-
onChange={onChange}
15-
value={value}
16-
min={min}
17-
max={max}
18-
step={step}
19-
fineStep={fineStep}
20-
className={NO_DRAG_CLASS}
21-
marks
22-
withThumbTooltip
23-
flex="1 1 0"
24-
/>
25-
);
26-
});
21+
return (
22+
<CompositeSlider
23+
defaultValue={defaultValue}
24+
onChange={onChange}
25+
value={field.value}
26+
min={min}
27+
max={max}
28+
step={step}
29+
fineStep={fineStep}
30+
className={NO_DRAG_CLASS}
31+
marks
32+
withThumbTooltip
33+
flex="1 1 0"
34+
/>
35+
);
36+
}
37+
);
2738

2839
FloatFieldSlider.displayName = 'FloatFieldSlider ';
Lines changed: 61 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,93 @@
11
import { NUMPY_RAND_MAX } from 'app/constants';
22
import { useAppDispatch } from 'app/store/storeHooks';
3-
import type { FieldComponentProps } from 'features/nodes/components/flow/nodes/Invocation/fields/inputs/types';
3+
import { roundDownToMultiple, roundUpToMultiple } from 'common/util/roundDownToMultiple';
44
import { fieldFloatValueChanged } from 'features/nodes/store/nodesSlice';
5-
import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field';
5+
import type { FloatFieldInputTemplate } from 'features/nodes/types/field';
6+
import { constrainNumber } from 'features/nodes/util/constrainNumber';
67
import { isNil } from 'lodash-es';
78
import { useCallback, useMemo } from 'react';
89

9-
export const useFloatField = (props: FieldComponentProps<FloatFieldInputInstance, FloatFieldInputTemplate>) => {
10-
const { nodeId, field, fieldTemplate } = props;
10+
export const useFloatField = (
11+
nodeId: string,
12+
fieldName: string,
13+
fieldTemplate: FloatFieldInputTemplate,
14+
overrides: { min?: number; max?: number; step?: number } = {}
15+
) => {
16+
const { min: overrideMin, max: overrideMax, step: overrideStep } = overrides;
1117
const dispatch = useAppDispatch();
1218

13-
const onChange = useCallback(
14-
(value: number) => {
15-
dispatch(fieldFloatValueChanged({ nodeId, fieldName: field.name, value }));
16-
},
17-
[dispatch, field.name, nodeId]
18-
);
19+
const step = useMemo(() => {
20+
if (overrideStep !== undefined) {
21+
return overrideStep;
22+
}
23+
if (isNil(fieldTemplate.multipleOf)) {
24+
return 0.1;
25+
}
26+
return fieldTemplate.multipleOf;
27+
}, [fieldTemplate.multipleOf, overrideStep]);
28+
29+
const fineStep = useMemo(() => {
30+
if (overrideStep !== undefined) {
31+
return overrideStep;
32+
}
33+
if (isNil(fieldTemplate.multipleOf)) {
34+
return 0.01;
35+
}
36+
return fieldTemplate.multipleOf;
37+
}, [fieldTemplate.multipleOf, overrideStep]);
1938

2039
const min = useMemo(() => {
2140
let min = -NUMPY_RAND_MAX;
22-
if (!isNil(fieldTemplate.minimum)) {
41+
42+
if (overrideMin !== undefined) {
43+
min = overrideMin;
44+
} else if (!isNil(fieldTemplate.minimum)) {
2345
min = fieldTemplate.minimum;
24-
}
25-
if (!isNil(fieldTemplate.exclusiveMinimum)) {
46+
} else if (!isNil(fieldTemplate.exclusiveMinimum)) {
2647
min = fieldTemplate.exclusiveMinimum + 0.01;
2748
}
28-
return min;
29-
}, [fieldTemplate.exclusiveMinimum, fieldTemplate.minimum]);
49+
50+
return roundUpToMultiple(min, step);
51+
}, [fieldTemplate.exclusiveMinimum, fieldTemplate.minimum, overrideMin, step]);
3052

3153
const max = useMemo(() => {
3254
let max = NUMPY_RAND_MAX;
33-
if (!isNil(fieldTemplate.maximum)) {
55+
56+
if (overrideMax !== undefined) {
57+
max = overrideMax;
58+
} else if (!isNil(fieldTemplate.maximum)) {
3459
max = fieldTemplate.maximum;
35-
}
36-
if (!isNil(fieldTemplate.exclusiveMaximum)) {
60+
} else if (!isNil(fieldTemplate.exclusiveMaximum)) {
3761
max = fieldTemplate.exclusiveMaximum - 0.01;
3862
}
39-
return max;
40-
}, [fieldTemplate.exclusiveMaximum, fieldTemplate.maximum]);
4163

42-
const step = useMemo(() => {
43-
if (isNil(fieldTemplate.multipleOf)) {
44-
return 0.1;
45-
}
46-
return fieldTemplate.multipleOf;
47-
}, [fieldTemplate.multipleOf]);
64+
return roundDownToMultiple(max, step);
65+
}, [fieldTemplate.exclusiveMaximum, fieldTemplate.maximum, overrideMax, step]);
4866

49-
const fineStep = useMemo(() => {
50-
if (isNil(fieldTemplate.multipleOf)) {
51-
return 0.01;
52-
}
53-
return fieldTemplate.multipleOf;
54-
}, [fieldTemplate.multipleOf]);
67+
const constrainValue = useCallback(
68+
(v: number) =>
69+
constrainNumber(
70+
v,
71+
{ min, max, multipleOf: step },
72+
{ min: overrideMin, max: overrideMax, multipleOf: overrideStep }
73+
),
74+
[max, min, overrideMax, overrideMin, overrideStep, step]
75+
);
76+
77+
const onChange = useCallback(
78+
(value: number) => {
79+
dispatch(fieldFloatValueChanged({ nodeId, fieldName, value }));
80+
},
81+
[dispatch, fieldName, nodeId]
82+
);
5583

5684
return {
5785
defaultValue: fieldTemplate.default,
5886
onChange,
59-
value: field.value,
6087
min,
6188
max,
6289
step,
6390
fineStep,
91+
constrainValue,
6492
};
6593
};

invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ import {
9595
} from 'features/nodes/types/field';
9696
import type { NodeFieldElement } from 'features/nodes/types/workflow';
9797
import { memo } from 'react';
98+
import type { Equals } from 'tsafe';
99+
import { assert } from 'tsafe';
98100

99101
import BoardFieldInputComponent from './inputs/BoardFieldInputComponent';
100102
import BooleanFieldInputComponent from './inputs/BooleanFieldInputComponent';
@@ -157,6 +159,8 @@ export const InputFieldRenderer = memo(({ nodeId, fieldName, settings }: Props)
157159
return <StringFieldInput nodeId={nodeId} field={field} fieldTemplate={template} />;
158160
} else if (settings.component === 'textarea') {
159161
return <StringFieldTextarea nodeId={nodeId} field={field} fieldTemplate={template} />;
162+
} else {
163+
assert<Equals<never, typeof settings.component>>(false, 'Unexpected settings.component');
160164
}
161165
}
162166

@@ -171,32 +175,47 @@ export const InputFieldRenderer = memo(({ nodeId, fieldName, settings }: Props)
171175
if (!isIntegerFieldInputInstance(field)) {
172176
return null;
173177
}
174-
if (settings?.type !== 'integer-field-config') {
178+
if (!settings || settings.type !== 'integer-field-config') {
175179
return <IntegerFieldInput nodeId={nodeId} field={field} fieldTemplate={template} />;
176180
}
181+
177182
if (settings.component === 'number-input') {
178183
return <IntegerFieldInput nodeId={nodeId} field={field} fieldTemplate={template} />;
179-
} else if (settings.component === 'slider') {
184+
}
185+
186+
if (settings.component === 'slider') {
180187
return <IntegerFieldSlider nodeId={nodeId} field={field} fieldTemplate={template} />;
181-
} else if (settings.component === 'number-input-and-slider') {
188+
}
189+
190+
if (settings.component === 'number-input-and-slider') {
182191
return <IntegerFieldInputAndSlider nodeId={nodeId} field={field} fieldTemplate={template} />;
183192
}
193+
194+
assert<Equals<never, typeof settings.component>>(false, 'Unexpected settings.component');
184195
}
185196

186197
if (isFloatFieldInputTemplate(template)) {
187198
if (!isFloatFieldInputInstance(field)) {
188199
return null;
189200
}
190-
if (settings?.type !== 'float-field-config') {
201+
202+
if (!settings || settings.type !== 'float-field-config') {
191203
return <FloatFieldInput nodeId={nodeId} field={field} fieldTemplate={template} />;
192204
}
205+
193206
if (settings.component === 'number-input') {
194-
return <FloatFieldInput nodeId={nodeId} field={field} fieldTemplate={template} />;
195-
} else if (settings.component === 'slider') {
196-
return <FloatFieldSlider nodeId={nodeId} field={field} fieldTemplate={template} />;
197-
} else if (settings.component === 'number-input-and-slider') {
198-
return <FloatFieldInputAndSlider nodeId={nodeId} field={field} fieldTemplate={template} />;
207+
return <FloatFieldInput nodeId={nodeId} field={field} fieldTemplate={template} settings={settings} />;
208+
}
209+
210+
if (settings.component === 'slider') {
211+
return <FloatFieldSlider nodeId={nodeId} field={field} fieldTemplate={template} settings={settings} />;
199212
}
213+
214+
if (settings.component === 'number-input-and-slider') {
215+
return <FloatFieldInputAndSlider nodeId={nodeId} field={field} fieldTemplate={template} settings={settings} />;
216+
}
217+
218+
assert<Equals<never, typeof settings.component>>(false, 'Unexpected settings.component');
200219
}
201220

202221
if (isIntegerFieldCollectionInputTemplate(template)) {

0 commit comments

Comments
 (0)