Skip to content

Commit f27d26c

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

File tree

7 files changed

+38
-53
lines changed

7 files changed

+38
-53
lines changed

invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FloatField/useFloatField.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ export const useFloatField = (
6868
(v: number) =>
6969
constrainNumber(
7070
v,
71-
{ min, max, multipleOf: step },
72-
{ min: overrideMin, max: overrideMax, multipleOf: overrideStep }
71+
{ min, max, step: step },
72+
{ min: overrideMin, max: overrideMax, step: overrideStep }
7373
),
7474
[max, min, overrideMax, overrideMin, overrideStep, step]
7575
);

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,15 +180,15 @@ export const InputFieldRenderer = memo(({ nodeId, fieldName, settings }: Props)
180180
}
181181

182182
if (settings.component === 'number-input') {
183-
return <IntegerFieldInput nodeId={nodeId} field={field} fieldTemplate={template} />;
183+
return <IntegerFieldInput nodeId={nodeId} field={field} fieldTemplate={template} settings={settings} />;
184184
}
185185

186186
if (settings.component === 'slider') {
187-
return <IntegerFieldSlider nodeId={nodeId} field={field} fieldTemplate={template} />;
187+
return <IntegerFieldSlider nodeId={nodeId} field={field} fieldTemplate={template} settings={settings} />;
188188
}
189189

190190
if (settings.component === 'number-input-and-slider') {
191-
return <IntegerFieldInputAndSlider nodeId={nodeId} field={field} fieldTemplate={template} />;
191+
return <IntegerFieldInputAndSlider nodeId={nodeId} field={field} fieldTemplate={template} settings={settings} />;
192192
}
193193

194194
assert<Equals<never, typeof settings.component>>(false, 'Unexpected settings.component');

invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/IntegerField/useIntegerField.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ export const useIntegerField = (
6868
(v: number) =>
6969
constrainNumber(
7070
v,
71-
{ min, max, multipleOf: step },
72-
{ min: overrideMin, max: overrideMax, multipleOf: overrideStep }
71+
{ min, max, step: step },
72+
{ min: overrideMin, max: overrideMax, step: overrideStep }
7373
),
7474
[max, min, overrideMax, overrideMin, overrideStep, step]
7575
);

invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementFloatSettings.tsx

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { fieldFloatValueChanged } from 'features/nodes/store/nodesSlice';
66
import { formElementNodeFieldDataChanged } from 'features/nodes/store/workflowSlice';
77
import type { FloatFieldInputInstance, FloatFieldInputTemplate } from 'features/nodes/types/field';
88
import { type NodeFieldFloatSettings, zNumberComponent } from 'features/nodes/types/workflow';
9+
import { constrainNumber } from 'features/nodes/util/constrainNumber';
910
import type { ChangeEvent } from 'react';
1011
import { memo, useCallback } from 'react';
1112
import { useTranslation } from 'react-i18next';
@@ -79,7 +80,9 @@ const SettingMin = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props
7980
min: v,
8081
};
8182
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
82-
const constrained = constrain(v, floatField, newConfig);
83+
84+
// We may need to update the value if it is outside the new min/max range
85+
const constrained = constrainNumber(field.value, floatField, newConfig);
8386
if (field.value !== constrained) {
8487
dispatch(fieldFloatValueChanged({ nodeId, fieldName, value: v }));
8588
}
@@ -128,9 +131,11 @@ const SettingMax = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props
128131
max: v,
129132
};
130133
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
131-
const constrained = constrain(v, floatField, newConfig);
134+
135+
// We may need to update the value if it is outside the new min/max range
136+
const constrained = constrainNumber(field.value, floatField, newConfig);
132137
if (field.value !== constrained) {
133-
dispatch(fieldFloatValueChanged({ nodeId, fieldName, value: v }));
138+
dispatch(fieldFloatValueChanged({ nodeId, fieldName, value: constrained }));
134139
}
135140
},
136141
[config, dispatch, field.value, fieldName, floatField, id, nodeId]
@@ -154,14 +159,3 @@ const SettingMax = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props
154159
);
155160
});
156161
SettingMax.displayName = 'SettingMax';
157-
158-
const constrain = (v: number, fieldSettings: ReturnType<typeof useFloatField>, overrides: NodeFieldFloatSettings) => {
159-
const min = overrides.min ?? fieldSettings.min;
160-
const max = overrides.max ?? fieldSettings.max;
161-
const step = overrides.step ?? fieldSettings.step;
162-
163-
const _v = Math.min(max, Math.max(min, v));
164-
const _diff = _v - min;
165-
const _steps = Math.round(_diff / step);
166-
return min + _steps * step;
167-
};

invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementIntegerSettings.tsx

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import { formElementNodeFieldDataChanged } from 'features/nodes/store/workflowSl
77
import type { IntegerFieldInputInstance, IntegerFieldInputTemplate } from 'features/nodes/types/field';
88
import type { NodeFieldIntegerSettings } from 'features/nodes/types/workflow';
99
import { zNumberComponent } from 'features/nodes/types/workflow';
10+
import { constrainNumber } from 'features/nodes/util/constrainNumber';
1011
import type { ChangeEvent } from 'react';
1112
import { memo, useCallback } from 'react';
1213
import { useTranslation } from 'react-i18next';
13-
import type { PartialDeep } from 'type-fest';
1414

1515
type Props = {
1616
id: string;
@@ -80,10 +80,13 @@ const SettingMin = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props
8080
...config,
8181
min: v,
8282
};
83+
8384
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
84-
const constrained = constrain(v, floatField, newConfig);
85+
86+
// We may need to update the value if it is outside the new min/max range
87+
const constrained = constrainNumber(field.value, { ...floatField }, newConfig);
8588
if (field.value !== constrained) {
86-
dispatch(fieldIntegerValueChanged({ nodeId, fieldName, value: v }));
89+
dispatch(fieldIntegerValueChanged({ nodeId, fieldName, value: constrained }));
8790
}
8891
},
8992
[config, dispatch, field.value, fieldName, floatField, id, nodeId]
@@ -101,7 +104,7 @@ const SettingMin = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props
101104
value={config.min === undefined ? (`${floatField.min} (inherited)` as unknown as number) : config.min}
102105
onChange={onChange}
103106
min={floatField.min}
104-
max={(config.max ?? floatField.max) - 0.1}
107+
max={(config.max ?? floatField.max) - 1}
105108
/>
106109
</FormControl>
107110
);
@@ -129,10 +132,13 @@ const SettingMax = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props
129132
...config,
130133
max: v,
131134
};
135+
132136
dispatch(formElementNodeFieldDataChanged({ id, changes: { settings: newConfig } }));
133-
const constrained = constrain(v, floatField, newConfig);
137+
138+
// We may need to update the value if it is outside the new min/max range
139+
const constrained = constrainNumber(field.value, floatField, newConfig);
134140
if (field.value !== constrained) {
135-
dispatch(fieldIntegerValueChanged({ nodeId, fieldName, value: v }));
141+
dispatch(fieldIntegerValueChanged({ nodeId, fieldName, value: constrained }));
136142
}
137143
},
138144
[config, dispatch, field.value, fieldName, floatField, id, nodeId]
@@ -149,25 +155,10 @@ const SettingMax = memo(({ id, config, nodeId, fieldName, fieldTemplate }: Props
149155
isDisabled={config.max === undefined}
150156
value={config.max === undefined ? (`${floatField.max} (inherited)` as unknown as number) : config.max}
151157
onChange={onChange}
152-
min={(config.min ?? floatField.min) + 0.1}
158+
min={(config.min ?? floatField.min) + 1}
153159
max={floatField.max}
154160
/>
155161
</FormControl>
156162
);
157163
});
158164
SettingMax.displayName = 'SettingMax';
159-
160-
type NumberConstraints = { min: number; max: number; step: number };
161-
162-
const constrain = (v: number, constraints: NumberConstraints, overrides: PartialDeep<NumberConstraints>) => {
163-
const min = overrides.min ?? constraints.min;
164-
const max = overrides.max ?? constraints.max;
165-
const step = overrides.step ?? constraints.step;
166-
167-
console.log({ min, max, step });
168-
169-
const _v = Math.min(max, Math.max(min, v));
170-
const _diff = _v - min;
171-
const _steps = Math.round(_diff / step);
172-
return min + _steps * step;
173-
};

invokeai/frontend/web/src/features/nodes/util/constrainNumber.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { constrainNumber } from './constrainNumber';
44

55
describe('constrainNumber', () => {
66
// Default constraints to be used in tests
7-
const defaultConstraints = { min: 0, max: 10, multipleOf: 1 };
7+
const defaultConstraints = { min: 0, max: 10, step: 1 };
88

99
it('should keep values within range', () => {
1010
expect(constrainNumber(5, defaultConstraints)).toBe(5);
@@ -13,7 +13,7 @@ describe('constrainNumber', () => {
1313
});
1414

1515
it('should round to nearest multiple', () => {
16-
const constraints = { min: 0, max: 10, multipleOf: 2 };
16+
const constraints = { min: 0, max: 10, step: 2 };
1717
expect(constrainNumber(1, constraints)).toBe(2);
1818
expect(constrainNumber(2, constraints)).toBe(2);
1919
expect(constrainNumber(3, constraints)).toBe(4);
@@ -22,7 +22,7 @@ describe('constrainNumber', () => {
2222
});
2323

2424
it('should handle negative multiples', () => {
25-
const constraints = { min: -10, max: 10, multipleOf: 3 };
25+
const constraints = { min: -10, max: 10, step: 3 };
2626
expect(constrainNumber(-9, constraints)).toBe(-9);
2727
expect(constrainNumber(-8, constraints)).toBe(-9);
2828
expect(constrainNumber(-7, constraints)).toBe(-6);
@@ -41,15 +41,15 @@ describe('constrainNumber', () => {
4141
});
4242

4343
it('should respect boundaries when rounding', () => {
44-
const constraints = { min: 0, max: 10, multipleOf: 4 };
44+
const constraints = { min: 0, max: 10, step: 4 };
4545
// Value at 9 would normally round to 8
4646
expect(constrainNumber(9, constraints)).toBe(8);
4747
// Value at 11 would normally round to 12, but max is 10
4848
expect(constrainNumber(11, constraints)).toBe(10);
4949
});
5050

5151
it('should handle decimal multiples', () => {
52-
const constraints = { min: 0, max: 1, multipleOf: 0.25 };
52+
const constraints = { min: 0, max: 1, step: 0.25 };
5353
expect(constrainNumber(0.3, constraints)).toBe(0.25);
5454
expect(constrainNumber(0.87, constraints)).toBe(0.75);
5555
expect(constrainNumber(0.88, constraints)).toBe(1.0);
@@ -64,10 +64,10 @@ describe('constrainNumber', () => {
6464
expect(constrainNumber(8, defaultConstraints, { max: 7 })).toBe(7);
6565

6666
// Override multipleOf
67-
expect(constrainNumber(4.7, defaultConstraints, { multipleOf: 2 })).toBe(4);
67+
expect(constrainNumber(4.7, defaultConstraints, { step: 2 })).toBe(4);
6868

6969
// Override all
70-
expect(constrainNumber(15, defaultConstraints, { min: 5, max: 20, multipleOf: 5 })).toBe(15);
70+
expect(constrainNumber(15, defaultConstraints, { min: 5, max: 20, step: 5 })).toBe(15);
7171
});
7272

7373
it('should handle edge cases', () => {
@@ -78,7 +78,7 @@ describe('constrainNumber', () => {
7878
expect(constrainNumber(10, defaultConstraints)).toBe(10);
7979

8080
// multipleOf larger than range
81-
const narrowConstraints = { min: 5, max: 7, multipleOf: 5 };
81+
const narrowConstraints = { min: 5, max: 7, step: 5 };
8282
expect(constrainNumber(6, narrowConstraints)).toBe(5);
8383
});
8484
});

invokeai/frontend/web/src/features/nodes/util/constrainNumber.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { PartialDeep } from 'type-fest';
22

3-
type NumberConstraints = { min: number; max: number; multipleOf?: number };
3+
type NumberConstraints = { min: number; max: number; step?: number };
44

55
/**
66
* Constrain a number to a range and round to the nearest multiple of a given value.
@@ -16,7 +16,7 @@ export const constrainNumber = (
1616
) => {
1717
const min = overrides?.min ?? constraints.min;
1818
const max = overrides?.max ?? constraints.max;
19-
const multipleOf = overrides?.multipleOf ?? constraints.multipleOf;
19+
const multipleOf = overrides?.step ?? constraints.step;
2020

2121
if (multipleOf === undefined) {
2222
return Math.min(Math.max(v, min), max);

0 commit comments

Comments
 (0)