Skip to content

Commit 9022d28

Browse files
feat(ui): handle FLUX bbox constraints
- Update canvas slice's to track the current base model architecture instead of just the optimal dimension. This lets us derive both optimal dimension _and_ grid size for the currently selected model. - Update all bbox size utilities to use derived grid size instead of hardcoded values of 8 or 64 - Review every damned instance of the number 8 in the whole frontend and update the ones that need to use the grid size - Update the invoke button blocking logic to check against scaled bbox size, unless scaling is disabled. - Update the invoke button blocking to say if it's width or height that is invalid and if its bbox or scaled, for both FLUX and the T2I adapter constraints
1 parent a969129 commit 9022d28

File tree

19 files changed

+297
-116
lines changed

19 files changed

+297
-116
lines changed

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,7 +1006,11 @@
10061006
"noFLUXVAEModelSelected": "No VAE model selected for FLUX generation",
10071007
"noCLIPEmbedModelSelected": "No CLIP Embed model selected for FLUX generation",
10081008
"canvasManagerNotLoaded": "Canvas Manager not loaded",
1009-
"fluxModelIncompatibleDimensions": "FLUX requires image dimension to be multiples of 16",
1009+
"fluxRequiresDimensionsToBeMultipleOf16": "FLUX requires width/height to be multiple of 16",
1010+
"fluxModelIncompatibleBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), bbox width is {{width}}",
1011+
"fluxModelIncompatibleBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), bbox height is {{height}}",
1012+
"fluxModelIncompatibleScaledWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), scaled bbox width is {{width}}",
1013+
"fluxModelIncompatibleScaledHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), scaled bbox height is {{height}}",
10101014
"canvasIsFiltering": "Canvas is filtering",
10111015
"canvasIsTransforming": "Canvas is transforming",
10121016
"canvasIsRasterizing": "Canvas is rasterizing",
@@ -1020,7 +1024,11 @@
10201024
"controlAdapterIncompatibleBaseModel": "incompatible Control Adapter base model",
10211025
"controlAdapterNoImageSelected": "no Control Adapter image selected",
10221026
"controlAdapterImageNotProcessed": "Control Adapter image not processed",
1023-
"t2iAdapterIncompatibleDimensions": "T2I Adapter requires image dimension to be multiples of {{multiple}}",
1027+
"t2iAdapterRequiresDimensionsToBeMultipleOf": "T2I Adapter requires width/height to be multiple of",
1028+
"t2iAdapterIncompatibleBboxWidth": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, bbox width is {{width}}",
1029+
"t2iAdapterIncompatibleBboxHeight": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, bbox height is {{height}}",
1030+
"t2iAdapterIncompatibleScaledBboxWidth": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, scaled bbox width is {{width}}",
1031+
"t2iAdapterIncompatibleScaledBboxHeight": "$t(parameters.invoke.layer.t2iAdapterRequiresDimensionsToBeMultipleOf) {{multiple}}, scaled bbox height is {{height}}",
10241032
"ipAdapterNoModelSelected": "no IP adapter selected",
10251033
"ipAdapterIncompatibleBaseModel": "incompatible IP Adapter base model",
10261034
"ipAdapterNoImageSelected": "no IP Adapter image selected",

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { logger } from 'app/logging/logger';
22
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
3-
import { bboxOptimalDimensionChanged, bboxSyncedToOptimalDimension } from 'features/controlLayers/store/canvasSlice';
3+
import { bboxSyncedToOptimalDimension } from 'features/controlLayers/store/canvasSlice';
44
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
55
import { loraDeleted } from 'features/controlLayers/store/lorasSlice';
66
import { modelChanged, vaeSelected } from 'features/controlLayers/store/paramsSlice';
77
import { modelSelected } from 'features/parameters/store/actions';
88
import { zParameterModel } from 'features/parameters/types/parameterSchemas';
9-
import { getOptimalDimension } from 'features/parameters/util/optimalDimension';
109
import { toast } from 'features/toast/toast';
1110
import { t } from 'i18next';
1211

@@ -71,8 +70,6 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
7170
}
7271

7372
dispatch(modelChanged({ model: newModel, previousModel: state.params.model }));
74-
// When staging, we don't want to change the bbox, but we must keep the optimal dimension in sync.
75-
dispatch(bboxOptimalDimensionChanged({ optimalDimension: getOptimalDimension(newModel) }));
7673
if (!selectIsStaging(state)) {
7774
dispatch(bboxSyncedToOptimalDimension());
7875
}

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
33
import type { AppDispatch, RootState } from 'app/store/store';
44
import type { SerializableObject } from 'common/types';
55
import {
6-
bboxOptimalDimensionChanged,
76
bboxSyncedToOptimalDimension,
87
controlLayerModelChanged,
98
referenceImageIPAdapterModelChanged,
@@ -29,7 +28,6 @@ import {
2928
zParameterT5EncoderModel,
3029
zParameterVAEModel,
3130
} from 'features/parameters/types/parameterSchemas';
32-
import { getOptimalDimension } from 'features/parameters/util/optimalDimension';
3331
import type { Logger } from 'roarr';
3432
import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models';
3533
import type { AnyModelConfig } from 'services/api/types';
@@ -123,8 +121,6 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => {
123121
'No selected main model or selected main model is not available, selecting default model'
124122
);
125123
dispatch(modelChanged({ model: zParameterModel.parse(defaultModel), previousModel: selectedMainModel }));
126-
// When staging, we don't want to change the bbox, but we must keep the optimal dimension in sync.
127-
dispatch(bboxOptimalDimensionChanged({ optimalDimension: getOptimalDimension(defaultModel) }));
128124
if (!selectIsStaging(state)) {
129125
dispatch(bboxSyncedToOptimalDimension());
130126
}
@@ -137,8 +133,6 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => {
137133
'No selected main model or selected main model is not available, selecting first available model'
138134
);
139135
dispatch(modelChanged({ model: zParameterModel.parse(firstModel), previousModel: selectedMainModel }));
140-
// When staging, we don't want to change the bbox, but we must keep the optimal dimension in sync.
141-
dispatch(bboxOptimalDimensionChanged({ optimalDimension: getOptimalDimension(firstModel) }));
142136
if (!selectIsStaging(state)) {
143137
dispatch(bboxSyncedToOptimalDimension());
144138
}

invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,32 @@ const createSelector = (
157157
if (!params.fluxVAE) {
158158
reasons.push({ content: i18n.t('parameters.invoke.noFLUXVAEModelSelected') });
159159
}
160-
if (bbox.rect.width % 16 !== 0 || bbox.rect.height % 16 !== 0) {
161-
reasons.push({ content: i18n.t('parameters.invoke.fluxModelIncompatibleDimensions') });
160+
if (bbox.scaleMethod === 'none') {
161+
if (bbox.rect.width % 16 !== 0) {
162+
reasons.push({
163+
content: i18n.t('parameters.invoke.fluxModelIncompatibleBboxWidth', { width: bbox.rect.width }),
164+
});
165+
}
166+
if (bbox.rect.height % 16 !== 0) {
167+
reasons.push({
168+
content: i18n.t('parameters.invoke.fluxModelIncompatibleBboxHeight', { height: bbox.rect.height }),
169+
});
170+
}
171+
} else {
172+
if (bbox.scaledSize.width % 16 !== 0) {
173+
reasons.push({
174+
content: i18n.t('parameters.invoke.fluxModelIncompatibleScaledBboxWidth', {
175+
width: bbox.scaledSize.width,
176+
}),
177+
});
178+
}
179+
if (bbox.scaledSize.height % 16 !== 0) {
180+
reasons.push({
181+
content: i18n.t('parameters.invoke.fluxModelIncompatibleScaledBboxHeight', {
182+
height: bbox.scaledSize.height,
183+
}),
184+
});
185+
}
162186
}
163187
}
164188

@@ -181,8 +205,40 @@ const createSelector = (
181205
// T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL)
182206
if (controlLayer.controlAdapter.type === 't2i_adapter') {
183207
const multiple = model?.base === 'sdxl' ? 32 : 64;
184-
if (bbox.rect.width % multiple !== 0 || bbox.rect.height % multiple !== 0) {
185-
problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple }));
208+
if (bbox.scaleMethod === 'none') {
209+
if (bbox.rect.width % 16 !== 0) {
210+
reasons.push({
211+
content: i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleBboxWidth', {
212+
multiple,
213+
width: bbox.rect.width,
214+
}),
215+
});
216+
}
217+
if (bbox.rect.height % 16 !== 0) {
218+
reasons.push({
219+
content: i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleBboxHeight', {
220+
multiple,
221+
height: bbox.rect.height,
222+
}),
223+
});
224+
}
225+
} else {
226+
if (bbox.scaledSize.width % 16 !== 0) {
227+
reasons.push({
228+
content: i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleScaledBboxWidth', {
229+
multiple,
230+
width: bbox.scaledSize.width,
231+
}),
232+
});
233+
}
234+
if (bbox.scaledSize.height % 16 !== 0) {
235+
reasons.push({
236+
content: i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleScaledBboxHeight', {
237+
multiple,
238+
height: bbox.scaledSize.height,
239+
}),
240+
});
241+
}
186242
}
187243
}
188244

invokeai/frontend/web/src/features/controlLayers/konva/CanvasBboxModule.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,8 @@ export class CanvasBboxModule extends CanvasModuleBase {
241241
* - Pushes the new bbox rect into app state
242242
*/
243243
onDragMove = () => {
244+
// The grid size here is the _position_ grid size, not the _dimension_ grid size - it is not constratined by the
245+
// currently-selected model.
244246
const gridSize = this.manager.stateApi.$ctrlKey.get() || this.manager.stateApi.$metaKey.get() ? 8 : 64;
245247
const bbox = this.manager.stateApi.getBbox();
246248
const bboxRect: Rect = {
@@ -277,7 +279,7 @@ export class CanvasBboxModule extends CanvasModuleBase {
277279
const shift = this.manager.stateApi.$shiftKey.get();
278280

279281
// Grid size depends on the modifier keys
280-
let gridSize = ctrl || meta ? 8 : 64;
282+
let gridSize = ctrl || meta ? this.manager.stateApi.getBboxGridSize() : 64;
281283

282284
// Alt key indicates we are using centered scaling. We need to double the gride size used when calculating the
283285
// new dimensions so that each size scales in the correct increments and doesn't mis-place the bbox. For example, if
@@ -384,7 +386,7 @@ export class CanvasBboxModule extends CanvasModuleBase {
384386

385387
// Determine the bbox size that fits within the visible rect. The bbox must be at least 64px in width and height,
386388
// and its width and height must be multiples of 8px.
387-
const gridSize = 8;
389+
const gridSize = this.manager.stateApi.getBboxGridSize();
388390

389391
// To be conservative, we will round up the x and y to the nearest grid size, and round down the width and height.
390392
// This ensures the bbox is never _larger_ than the visible rect. If the bbox is larger than the visible, we
@@ -407,8 +409,12 @@ export class CanvasBboxModule extends CanvasModuleBase {
407409
const stage = this.konva.transformer.getStage();
408410
assert(stage, 'Stage must exist');
409411

410-
// We need to snap the anchors to the grid. If the user is holding ctrl/meta, we use the finer 8px grid.
411-
const gridSize = this.manager.stateApi.$ctrlKey.get() || this.manager.stateApi.$metaKey.get() ? 8 : 64;
412+
// We need to snap the anchors to the grid. If the user is holding ctrl/meta, we use the finest grid size allowed
413+
// currently-selected model.
414+
const gridSize =
415+
this.manager.stateApi.$ctrlKey.get() || this.manager.stateApi.$metaKey.get()
416+
? this.manager.stateApi.getBboxGridSize()
417+
: 64;
412418
// Because we are working in absolute coordinates, we need to scale the grid size by the stage scale.
413419
const scaledGridSize = gridSize * stage.scaleX();
414420
// To snap the anchor to the grid, we need to calculate an offset from the stage's absolute position.

invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ import {
2525
entityReset,
2626
} from 'features/controlLayers/store/canvasSlice';
2727
import { selectCanvasStagingAreaSlice } from 'features/controlLayers/store/canvasStagingAreaSlice';
28-
import { selectAllRenderableEntities, selectBbox, selectCanvasSlice } from 'features/controlLayers/store/selectors';
28+
import {
29+
selectAllRenderableEntities,
30+
selectBbox,
31+
selectCanvasSlice,
32+
selectGridSize,
33+
} from 'features/controlLayers/store/selectors';
2934
import type {
3035
CanvasEntityType,
3136
CanvasState,
@@ -401,6 +406,10 @@ export class CanvasStateApiModule extends CanvasModuleBase {
401406
return this.runSelector(selectCanvasSettingsSlice);
402407
};
403408

409+
/**
410+
* Gets the _positional_ grid size for the current canvas. Note that this is not the same as bbox grid size, which is
411+
* based on the currently-selected model.
412+
*/
404413
getGridSize = (): number => {
405414
const snapToGrid = this.getSettings().snapToGrid;
406415
if (!snapToGrid) {
@@ -448,6 +457,13 @@ export class CanvasStateApiModule extends CanvasModuleBase {
448457
return this.runSelector(selectCanvasStagingAreaSlice);
449458
};
450459

460+
/**
461+
* Gets the grid size for the current canvas, based on the currently-selected model
462+
*/
463+
getBboxGridSize = (): number => {
464+
return this.runSelector(selectGridSize);
465+
};
466+
451467
/**
452468
* Checks if an entity is selected.
453469
*/

0 commit comments

Comments
 (0)