feat(ui): rip out document size

barely knew ye
This commit is contained in:
psychedelicious 2024-07-16 17:03:55 +10:00
parent 22ab63fe8d
commit c3c95754f7
28 changed files with 202 additions and 332 deletions

View File

@ -3,9 +3,9 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
import type { AppDispatch, RootState } from 'app/store/store';
import type { JSONObject } from 'common/types';
import {
bboxHeightChanged,
bboxWidthChanged,
caModelChanged,
documentHeightChanged,
documentWidthChanged,
ipaModelChanged,
loraDeleted,
modelChanged,
@ -83,16 +83,16 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => {
dispatch(modelChanged({ model: defaultModelInList, previousModel: currentModel }));
const optimalDimension = getOptimalDimension(defaultModelInList);
if (getIsSizeOptimal(state.canvasV2.document.rect.width, state.canvasV2.document.rect.height, optimalDimension)) {
if (getIsSizeOptimal(state.canvasV2.bbox.rect.width, state.canvasV2.bbox.rect.height, optimalDimension)) {
return;
}
const { width, height } = calculateNewSize(
state.canvasV2.document.aspectRatio.value,
state.canvasV2.bbox.aspectRatio.value,
optimalDimension * optimalDimension
);
dispatch(documentWidthChanged({ width }));
dispatch(documentHeightChanged({ height }));
dispatch(bboxWidthChanged({ width }));
dispatch(bboxHeightChanged({ height }));
return;
}
}

View File

@ -1,7 +1,7 @@
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import {
documentHeightChanged,
documentWidthChanged,
bboxHeightChanged,
bboxWidthChanged,
setCfgRescaleMultiplier,
setCfgScale,
setScheduler,
@ -99,13 +99,13 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni
const setSizeOptions = { updateAspectRatio: true, clamp: true };
if (width) {
if (isParameterWidth(width)) {
dispatch(documentWidthChanged({ width, ...setSizeOptions }));
dispatch(bboxWidthChanged({ width, ...setSizeOptions }));
}
}
if (height) {
if (isParameterHeight(height)) {
dispatch(documentHeightChanged({ height, ...setSizeOptions }));
dispatch(bboxHeightChanged({ height, ...setSizeOptions }));
}
}

View File

@ -25,7 +25,7 @@ type ResizeDirection =
| 'down-right';
export const CanvasResizer = memo(() => {
const document = useAppSelector((s) => s.canvasV2.document);
const bbox = useAppSelector((s) => s.canvasV2.bbox);
const [resizeDirection, setResizeDirection] = useState<ResizeDirection>('center-out');
const setDirUpLeft = useCallback(() => {

View File

@ -3,7 +3,7 @@ import { skipToken } from '@reduxjs/toolkit/query';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDndImage from 'common/components/IAIDndImage';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import { documentHeightChanged, documentWidthChanged } from 'features/controlLayers/store/canvasV2Slice';
import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectOptimalDimension } from 'features/controlLayers/store/selectors';
import type { ControlAdapterEntity } from 'features/controlLayers/store/types';
import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types';
@ -89,15 +89,15 @@ export const CAImagePreview = memo(
if (shift) {
const { width, height } = controlImage;
dispatch(documentWidthChanged({ width, ...options }));
dispatch(documentHeightChanged({ height, ...options }));
dispatch(bboxWidthChanged({ width, ...options }));
dispatch(bboxHeightChanged({ height, ...options }));
} else {
const { width, height } = calculateNewSize(
controlImage.width / controlImage.height,
optimalDimension * optimalDimension
);
dispatch(documentWidthChanged({ width, ...options }));
dispatch(documentHeightChanged({ height, ...options }));
dispatch(bboxWidthChanged({ width, ...options }));
dispatch(bboxHeightChanged({ height, ...options }));
}
}, [controlImage, dispatch, optimalDimension, shift]);

View File

@ -20,12 +20,10 @@ export const HeadsUpDisplay = memo(() => {
const lastMouseDownPos = useStore($lastMouseDownPos);
const lastAddedPoint = useStore($lastAddedPoint);
const bbox = useAppSelector((s) => s.canvasV2.bbox);
const document = useAppSelector((s) => s.canvasV2.document);
return (
<Flex flexDir="column" bg="blackAlpha.400" borderBottomEndRadius="base" p={2} minW={64} gap={2}>
<HUDItem label="Zoom" value={`${round(stageAttrs.scale * 100, 2)}%`} />
<HUDItem label="Document Size" value={`${document.rect.width}×${document.rect.height} px`} />
<HUDItem label="Stage Pos" value={`${round(stageAttrs.x, 3)}, ${round(stageAttrs.y, 3)}`} />
<HUDItem
label="Stage Size"

View File

@ -3,7 +3,7 @@ import { skipToken } from '@reduxjs/toolkit/query';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDndImage from 'common/components/IAIDndImage';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import { documentHeightChanged, documentWidthChanged } from 'features/controlLayers/store/canvasV2Slice';
import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectOptimalDimension } from 'features/controlLayers/store/selectors';
import type { ImageWithDims } from 'features/controlLayers/store/types';
import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types';
@ -42,15 +42,15 @@ export const IPAImagePreview = memo(({ image, onChangeImage, ipAdapterId, droppa
const options = { updateAspectRatio: true, clamp: true };
if (shift) {
const { width, height } = controlImage;
dispatch(documentWidthChanged({ width, ...options }));
dispatch(documentHeightChanged({ height, ...options }));
dispatch(bboxWidthChanged({ width, ...options }));
dispatch(bboxHeightChanged({ height, ...options }));
} else {
const { width, height } = calculateNewSize(
controlImage.width / controlImage.height,
optimalDimension * optimalDimension
);
dispatch(documentWidthChanged({ width, ...options }));
dispatch(documentHeightChanged({ height, ...options }));
dispatch(bboxWidthChanged({ width, ...options }));
dispatch(bboxHeightChanged({ height, ...options }));
}
}, [controlImage, dispatch, optimalDimension, shift]);

View File

@ -3,7 +3,7 @@ import { skipToken } from '@reduxjs/toolkit/query';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDndImage from 'common/components/IAIDndImage';
import IAIDndImageIcon from 'common/components/IAIDndImageIcon';
import { documentHeightChanged, documentWidthChanged, iiReset } from 'features/controlLayers/store/canvasV2Slice';
import { bboxHeightChanged, bboxWidthChanged, iiReset } from 'features/controlLayers/store/canvasV2Slice';
import { selectOptimalDimension } from 'features/controlLayers/store/selectors';
import type { ImageDraggableData, InitialImageDropData } from 'features/dnd/types';
import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize';
@ -36,12 +36,12 @@ export const InitialImagePreview = memo(() => {
const options = { updateAspectRatio: true, clamp: true };
if (shift) {
const { width, height } = imageDTO;
dispatch(documentWidthChanged({ width, ...options }));
dispatch(documentHeightChanged({ height, ...options }));
dispatch(bboxWidthChanged({ width, ...options }));
dispatch(bboxHeightChanged({ height, ...options }));
} else {
const { width, height } = calculateNewSize(imageDTO.width / imageDTO.height, optimalDimension * optimalDimension);
dispatch(documentWidthChanged({ width, ...options }));
dispatch(documentHeightChanged({ height, ...options }));
dispatch(bboxWidthChanged({ width, ...options }));
dispatch(bboxHeightChanged({ height, ...options }));
}
}, [imageDTO, dispatch, optimalDimension, shift]);

View File

@ -1,67 +0,0 @@
import { getArbitraryBaseColor } from '@invoke-ai/ui-library';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { DOCUMENT_FIT_PADDING_PX } from 'features/controlLayers/konva/constants';
import Konva from 'konva';
export class CanvasDocumentSizeOverlay {
group: Konva.Group;
outerRect: Konva.Rect;
innerRect: Konva.Rect;
padding: number;
manager: CanvasManager;
constructor(manager: CanvasManager, padding?: number) {
this.manager = manager;
this.padding = padding ?? DOCUMENT_FIT_PADDING_PX;
this.group = new Konva.Group({ id: 'document_overlay_group', listening: false });
this.outerRect = new Konva.Rect({
id: 'document_overlay_outer_rect',
listening: false,
fill: getArbitraryBaseColor(10),
opacity: 0.7,
});
this.innerRect = new Konva.Rect({
id: 'document_overlay_inner_rect',
listening: false,
fill: 'white',
globalCompositeOperation: 'destination-out',
});
this.group.add(this.outerRect);
this.group.add(this.innerRect);
}
render() {
const document = this.manager.stateApi.getDocument();
this.group.zIndex(0);
const x = this.manager.stage.x();
const y = this.manager.stage.y();
const width = this.manager.stage.width();
const height = this.manager.stage.height();
const scale = this.manager.stage.scaleX();
this.outerRect.setAttrs({
offsetX: x / scale,
offsetY: y / scale,
width: width / scale,
height: height / scale,
});
this.innerRect.setAttrs(document.rect);
}
fitToStage() {
const document = this.manager.stateApi.getDocument();
// Fit & center the document on the stage
const width = this.manager.stage.width();
const height = this.manager.stage.height();
const docWidthWithBuffer = document.rect.width + this.padding * 2;
const docHeightWithBuffer = document.rect.height + this.padding * 2;
const scale = Math.min(Math.min(width / docWidthWithBuffer, height / docHeightWithBuffer), 1);
const x = (width - docWidthWithBuffer * scale) / 2 + this.padding * scale;
const y = (height - docHeightWithBuffer * scale) / 2 + this.padding * scale;
this.manager.stage.setAttrs({ x, y, width, height, scaleX: scale, scaleY: scale });
this.manager.stateApi.setStageAttrs({ x, y, width, height, scale });
}
}

View File

@ -22,7 +22,6 @@ import { assert } from 'tsafe';
import { CanvasBackground } from './CanvasBackground';
import { CanvasBbox } from './CanvasBbox';
import { CanvasControlAdapter } from './CanvasControlAdapter';
import { CanvasDocumentSizeOverlay } from './CanvasDocumentSizeOverlay';
import { CanvasInpaintMask } from './CanvasInpaintMask';
import { CanvasLayer } from './CanvasLayer';
import { CanvasPreview } from './CanvasPreview';
@ -92,7 +91,6 @@ export class CanvasManager {
this.preview = new CanvasPreview(
new CanvasBbox(this),
new CanvasTool(this),
new CanvasDocumentSizeOverlay(this),
new CanvasStagingArea(this),
new CanvasProgressPreview(this)
);
@ -221,7 +219,6 @@ export class CanvasManager {
scale: this.stage.scaleX(),
});
this.background.render();
this.preview.documentSizeOverlay.render();
}
render = async () => {
@ -245,7 +242,7 @@ export class CanvasManager {
if (
this.isFirstRender ||
state.initialImage !== this.prevState.initialImage ||
state.document !== this.prevState.document ||
state.bbox.rect !== this.prevState.bbox.rect ||
state.tool.selected !== this.prevState.tool.selected ||
state.selectedEntityIdentifier?.id !== this.prevState.selectedEntityIdentifier?.id
) {
@ -285,11 +282,6 @@ export class CanvasManager {
await this.renderControlAdapters();
}
if (this.isFirstRender || state.document !== this.prevState.document) {
log.debug('Rendering document bounds overlay');
await this.preview.documentSizeOverlay.render();
}
if (
this.isFirstRender ||
state.bbox !== this.prevState.bbox ||
@ -367,9 +359,6 @@ export class CanvasManager {
});
log.debug('First render of konva stage');
// On first render, the document should be fit to the stage.
this.preview.documentSizeOverlay.render();
this.preview.documentSizeOverlay.fitToStage();
this.preview.tool.render();
this.render();

View File

@ -2,7 +2,6 @@ import type { CanvasProgressPreview } from 'features/controlLayers/konva/CanvasP
import Konva from 'konva';
import type { CanvasBbox } from './CanvasBbox';
import type { CanvasDocumentSizeOverlay } from './CanvasDocumentSizeOverlay';
import type { CanvasStagingArea } from './CanvasStagingArea';
import type { CanvasTool } from './CanvasTool';
@ -10,22 +9,17 @@ export class CanvasPreview {
layer: Konva.Layer;
tool: CanvasTool;
bbox: CanvasBbox;
documentSizeOverlay: CanvasDocumentSizeOverlay;
stagingArea: CanvasStagingArea;
progressPreview: CanvasProgressPreview;
constructor(
bbox: CanvasBbox,
tool: CanvasTool,
documentSizeOverlay: CanvasDocumentSizeOverlay,
stagingArea: CanvasStagingArea,
progressPreview: CanvasProgressPreview
) {
this.layer = new Konva.Layer({ listening: true, imageSmoothingEnabled: false });
this.documentSizeOverlay = documentSizeOverlay;
this.layer.add(this.documentSizeOverlay.group);
this.stagingArea = stagingArea;
this.layer.add(this.stagingArea.group);

View File

@ -207,9 +207,6 @@ export class CanvasStateApi {
getBbox = () => {
return this.getState().bbox;
};
getDocument = () => {
return this.getState().document;
};
getToolState = () => {
return this.getState().tool;
};

View File

@ -137,14 +137,14 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
function getClip(entity: RegionEntity | LayerEntity | InpaintMaskEntity) {
const settings = getSettings();
const bbox = getBbox();
const bboxRect = getBbox().rect;
if (settings.clipToBbox) {
return {
x: bbox.x - entity.x,
y: bbox.y - entity.y,
width: bbox.width,
height: bbox.height,
x: bboxRect.x - entity.x,
y: bboxRect.y - entity.y,
width: bboxRect.width,
height: bboxRect.height,
};
} else {
return {
@ -486,7 +486,6 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
stage.position(newPos);
setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale });
manager.background.render();
manager.preview.documentSizeOverlay.render();
}
}
manager.preview.tool.render();
@ -502,7 +501,6 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
scale: stage.scaleX(),
});
manager.background.render();
manager.preview.documentSizeOverlay.render();
manager.preview.tool.render();
});
@ -540,9 +538,8 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
} else if (e.key === 'r') {
setLastCursorPos(null);
setLastMouseDownPos(null);
manager.preview.documentSizeOverlay.fitToStage();
manager.background.render();
manager.preview.documentSizeOverlay.render();
// TODO(psyche): restore some kind of fit
}
manager.preview.tool.render();
};

View File

@ -1,12 +1,17 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { deepClone } from 'common/util/deepClone';
import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple';
import type { BoundingBoxScaleMethod, CanvasV2State, Size } from 'features/controlLayers/store/types';
import { getScaledBoundingBoxDimensions } from 'features/controlLayers/util/getScaledBoundingBoxDimensions';
import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize';
import { ASPECT_RATIO_MAP, initialAspectRatioState } from 'features/parameters/components/DocumentSize/constants';
import type { AspectRatioID } from 'features/parameters/components/DocumentSize/types';
import { getOptimalDimension } from 'features/parameters/util/optimalDimension';
import type { IRect } from 'konva/lib/types';
import { pick } from 'lodash-es';
export const bboxReducers = {
scaledBboxChanged: (state, action: PayloadAction<Partial<Size>>) => {
bboxScaledSizeChanged: (state, action: PayloadAction<Partial<Size>>) => {
state.layers.imageCache = null;
state.bbox.scaledSize = { ...state.bbox.scaledSize, ...action.payload };
},
@ -30,4 +35,116 @@ export const bboxReducers = {
state.bbox.scaledSize = getScaledBoundingBoxDimensions(size, optimalDimension);
}
},
bboxWidthChanged: (state, action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>) => {
const { width, updateAspectRatio, clamp } = action.payload;
state.bbox.rect.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width;
if (state.bbox.aspectRatio.isLocked) {
state.bbox.rect.height = roundToMultiple(state.bbox.rect.width / state.bbox.aspectRatio.value, 8);
}
if (updateAspectRatio || !state.bbox.aspectRatio.isLocked) {
state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height;
state.bbox.aspectRatio.id = 'Free';
state.bbox.aspectRatio.isLocked = false;
}
if (!state.session.isActive) {
if (state.initialImage.imageObject) {
state.initialImage.imageObject.width = state.bbox.rect.width;
state.initialImage.imageObject.height = state.bbox.rect.height;
}
}
},
bboxHeightChanged: (
state,
action: PayloadAction<{ height: number; updateAspectRatio?: boolean; clamp?: boolean }>
) => {
const { height, updateAspectRatio, clamp } = action.payload;
state.bbox.rect.height = clamp ? Math.max(roundDownToMultiple(height, 8), 64) : height;
if (state.bbox.aspectRatio.isLocked) {
state.bbox.rect.width = roundToMultiple(state.bbox.rect.height * state.bbox.aspectRatio.value, 8);
}
if (updateAspectRatio || !state.bbox.aspectRatio.isLocked) {
state.bbox.aspectRatio.value = state.bbox.rect.width / state.bbox.rect.height;
state.bbox.aspectRatio.id = 'Free';
state.bbox.aspectRatio.isLocked = false;
}
if (!state.session.isActive) {
if (state.initialImage.imageObject) {
state.initialImage.imageObject.width = state.bbox.rect.width;
state.initialImage.imageObject.height = state.bbox.rect.height;
}
}
},
bboxAspectRatioLockToggled: (state) => {
state.bbox.aspectRatio.isLocked = !state.bbox.aspectRatio.isLocked;
},
bboxAspectRatioIdChanged: (state, action: PayloadAction<{ id: AspectRatioID }>) => {
const { id } = action.payload;
state.bbox.aspectRatio.id = id;
if (id === 'Free') {
state.bbox.aspectRatio.isLocked = false;
} else {
state.bbox.aspectRatio.isLocked = true;
state.bbox.aspectRatio.value = ASPECT_RATIO_MAP[id].ratio;
const { width, height } = calculateNewSize(
state.bbox.aspectRatio.value,
state.bbox.rect.width * state.bbox.rect.height
);
state.bbox.rect.width = width;
state.bbox.rect.height = height;
}
if (!state.session.isActive) {
if (state.initialImage.imageObject) {
state.initialImage.imageObject.width = state.bbox.rect.width;
state.initialImage.imageObject.height = state.bbox.rect.height;
}
}
},
bboxDimensionsSwapped: (state) => {
state.bbox.aspectRatio.value = 1 / state.bbox.aspectRatio.value;
if (state.bbox.aspectRatio.id === 'Free') {
const newWidth = state.bbox.rect.height;
const newHeight = state.bbox.rect.width;
state.bbox.rect.width = newWidth;
state.bbox.rect.height = newHeight;
} else {
const { width, height } = calculateNewSize(
state.bbox.aspectRatio.value,
state.bbox.rect.width * state.bbox.rect.height
);
state.bbox.rect.width = width;
state.bbox.rect.height = height;
state.bbox.aspectRatio.id = ASPECT_RATIO_MAP[state.bbox.aspectRatio.id].inverseID;
}
if (!state.session.isActive) {
if (state.initialImage.imageObject) {
state.initialImage.imageObject.width = state.bbox.rect.width;
state.initialImage.imageObject.height = state.bbox.rect.height;
}
}
},
bboxSizeOptimized: (state) => {
const optimalDimension = getOptimalDimension(state.params.model);
if (state.bbox.aspectRatio.isLocked) {
const { width, height } = calculateNewSize(state.bbox.aspectRatio.value, optimalDimension ** 2);
state.bbox.rect.width = width;
state.bbox.rect.height = height;
} else {
state.bbox.aspectRatio = deepClone(initialAspectRatioState);
state.bbox.rect.width = optimalDimension;
state.bbox.rect.height = optimalDimension;
}
if (!state.session.isActive) {
if (state.initialImage.imageObject) {
state.initialImage.imageObject.width = state.bbox.rect.width;
state.initialImage.imageObject.height = state.bbox.rect.height;
}
}
},
} satisfies SliceCaseReducers<CanvasV2State>;

View File

@ -5,7 +5,6 @@ import { deepClone } from 'common/util/deepClone';
import { bboxReducers } from 'features/controlLayers/store/bboxReducers';
import { compositingReducers } from 'features/controlLayers/store/compositingReducers';
import { controlAdaptersReducers } from 'features/controlLayers/store/controlAdaptersReducers';
import { documentReducers } from 'features/controlLayers/store/documentReducers';
import { initialImageReducers } from 'features/controlLayers/store/initialImageReducers';
import { inpaintMaskReducers } from 'features/controlLayers/store/inpaintMaskReducers';
import { ipAdaptersReducers } from 'features/controlLayers/store/ipAdaptersReducers';
@ -63,12 +62,9 @@ const initialState: CanvasV2State = {
width: 50,
},
},
document: {
rect: { x: 0, y: 0, width: 512, height: 512 },
aspectRatio: deepClone(initialAspectRatioState),
},
bbox: {
rect: { x: 0, y: 0, width: 512, height: 512 },
aspectRatio: deepClone(initialAspectRatioState),
scaleMethod: 'auto',
scaledSize: {
width: 512,
@ -149,7 +145,6 @@ export const canvasV2Slice = createSlice({
...bboxReducers,
...inpaintMaskReducers,
...sessionReducers,
...documentReducers,
...initialImageReducers,
entitySelected: (state, action: PayloadAction<CanvasEntityIdentifier>) => {
state.selectedEntityIdentifier = action.payload;
@ -164,7 +159,6 @@ export const canvasV2Slice = createSlice({
canvasReset: (state) => {
state.bbox = deepClone(initialState.bbox);
state.controlAdapters = deepClone(initialState.controlAdapters);
state.document = deepClone(initialState.document);
state.ipAdapters = deepClone(initialState.ipAdapters);
state.layers = deepClone(initialState.layers);
state.regions = deepClone(initialState.regions);
@ -178,7 +172,6 @@ export const canvasV2Slice = createSlice({
});
export const {
bboxChanged,
brushWidthChanged,
eraserWidthChanged,
fillChanged,
@ -188,17 +181,18 @@ export const {
maskOpacityChanged,
entitySelected,
allEntitiesDeleted,
scaledBboxChanged,
bboxScaleMethodChanged,
clipToBboxChanged,
canvasReset,
// document
documentWidthChanged,
documentHeightChanged,
documentAspectRatioLockToggled,
documentAspectRatioIdChanged,
documentDimensionsSwapped,
documentSizeOptimized,
// bbox
bboxChanged,
bboxScaledSizeChanged,
bboxScaleMethodChanged,
bboxWidthChanged,
bboxHeightChanged,
bboxAspectRatioLockToggled,
bboxAspectRatioIdChanged,
bboxDimensionsSwapped,
bboxSizeOptimized,
// layers
layerAdded,
layerRecalled,

View File

@ -1,141 +0,0 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { deepClone } from 'common/util/deepClone';
import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple';
import type { CanvasV2State } from 'features/controlLayers/store/types';
import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize';
import { ASPECT_RATIO_MAP, initialAspectRatioState } from 'features/parameters/components/DocumentSize/constants';
import type { AspectRatioID } from 'features/parameters/components/DocumentSize/types';
import { getOptimalDimension } from 'features/parameters/util/optimalDimension';
export const documentReducers = {
documentWidthChanged: (
state,
action: PayloadAction<{ width: number; updateAspectRatio?: boolean; clamp?: boolean }>
) => {
const { width, updateAspectRatio, clamp } = action.payload;
state.document.rect.width = clamp ? Math.max(roundDownToMultiple(width, 8), 64) : width;
if (state.document.aspectRatio.isLocked) {
state.document.rect.height = roundToMultiple(state.document.rect.width / state.document.aspectRatio.value, 8);
}
if (updateAspectRatio || !state.document.aspectRatio.isLocked) {
state.document.aspectRatio.value = state.document.rect.width / state.document.rect.height;
state.document.aspectRatio.id = 'Free';
state.document.aspectRatio.isLocked = false;
}
if (!state.session.isActive) {
state.bbox.rect.width = state.document.rect.width;
state.bbox.rect.height = state.document.rect.height;
if (state.initialImage.imageObject) {
state.initialImage.imageObject.width = state.document.rect.width;
state.initialImage.imageObject.height = state.document.rect.height;
}
}
},
documentHeightChanged: (
state,
action: PayloadAction<{ height: number; updateAspectRatio?: boolean; clamp?: boolean }>
) => {
const { height, updateAspectRatio, clamp } = action.payload;
state.document.rect.height = clamp ? Math.max(roundDownToMultiple(height, 8), 64) : height;
if (state.document.aspectRatio.isLocked) {
state.document.rect.width = roundToMultiple(state.document.rect.height * state.document.aspectRatio.value, 8);
}
if (updateAspectRatio || !state.document.aspectRatio.isLocked) {
state.document.aspectRatio.value = state.document.rect.width / state.document.rect.height;
state.document.aspectRatio.id = 'Free';
state.document.aspectRatio.isLocked = false;
}
if (!state.session.isActive) {
state.bbox.rect.width = state.document.rect.width;
state.bbox.rect.height = state.document.rect.height;
if (state.initialImage.imageObject) {
state.initialImage.imageObject.width = state.document.rect.width;
state.initialImage.imageObject.height = state.document.rect.height;
}
}
},
documentAspectRatioLockToggled: (state) => {
state.document.aspectRatio.isLocked = !state.document.aspectRatio.isLocked;
},
documentAspectRatioIdChanged: (state, action: PayloadAction<{ id: AspectRatioID }>) => {
const { id } = action.payload;
state.document.aspectRatio.id = id;
if (id === 'Free') {
state.document.aspectRatio.isLocked = false;
} else {
state.document.aspectRatio.isLocked = true;
state.document.aspectRatio.value = ASPECT_RATIO_MAP[id].ratio;
const { width, height } = calculateNewSize(
state.document.aspectRatio.value,
state.document.rect.width * state.document.rect.height
);
state.document.rect.width = width;
state.document.rect.height = height;
}
if (!state.session.isActive) {
state.bbox.rect.width = state.document.rect.width;
state.bbox.rect.height = state.document.rect.height;
if (state.initialImage.imageObject) {
state.initialImage.imageObject.width = state.document.rect.width;
state.initialImage.imageObject.height = state.document.rect.height;
}
}
},
documentDimensionsSwapped: (state) => {
state.document.aspectRatio.value = 1 / state.document.aspectRatio.value;
if (state.document.aspectRatio.id === 'Free') {
const newWidth = state.document.rect.height;
const newHeight = state.document.rect.width;
state.document.rect.width = newWidth;
state.document.rect.height = newHeight;
} else {
const { width, height } = calculateNewSize(
state.document.aspectRatio.value,
state.document.rect.width * state.document.rect.height
);
state.document.rect.width = width;
state.document.rect.height = height;
state.document.aspectRatio.id = ASPECT_RATIO_MAP[state.document.aspectRatio.id].inverseID;
}
if (!state.session.isActive) {
state.bbox.rect.width = state.document.rect.width;
state.bbox.rect.height = state.document.rect.height;
if (state.initialImage.imageObject) {
state.initialImage.imageObject.width = state.document.rect.width;
state.initialImage.imageObject.height = state.document.rect.height;
}
}
},
documentSizeOptimized: (state) => {
const optimalDimension = getOptimalDimension(state.params.model);
if (state.document.aspectRatio.isLocked) {
const { width, height } = calculateNewSize(state.document.aspectRatio.value, optimalDimension ** 2);
state.document.rect.width = width;
state.document.rect.height = height;
} else {
state.document.aspectRatio = deepClone(initialAspectRatioState);
state.document.rect.width = optimalDimension;
state.document.rect.height = optimalDimension;
}
if (!state.session.isActive) {
state.bbox.rect.width = state.document.rect.width;
state.bbox.rect.height = state.document.rect.height;
if (state.initialImage.imageObject) {
state.initialImage.imageObject.width = state.document.rect.width;
state.initialImage.imageObject.height = state.document.rect.height;
}
}
},
} satisfies SliceCaseReducers<CanvasV2State>;

View File

@ -62,8 +62,8 @@ export const paramsReducers = {
// Update the bbox size to match the new model's optimal size
// TODO(psyche): Should we change the document size too?
const optimalDimension = getOptimalDimension(model);
if (!getIsSizeOptimal(state.document.rect.width, state.document.rect.height, optimalDimension)) {
const bboxDims = calculateNewSize(state.document.aspectRatio.value, optimalDimension * optimalDimension);
if (!getIsSizeOptimal(state.bbox.rect.width, state.bbox.rect.height, optimalDimension)) {
const bboxDims = calculateNewSize(state.bbox.aspectRatio.value, optimalDimension * optimalDimension);
state.bbox.rect.width = bboxDims.width;
state.bbox.rect.height = bboxDims.height;

View File

@ -847,15 +847,6 @@ export type CanvasV2State = {
eraser: { width: number };
fill: RgbaColor;
};
document: {
rect: {
x: number;
y: number;
width: ParameterWidth;
height: ParameterHeight;
};
aspectRatio: AspectRatioState;
};
settings: {
imageSmoothing: boolean;
maskOpacity: number;
@ -872,6 +863,7 @@ export type CanvasV2State = {
width: ParameterWidth;
height: ParameterHeight;
};
aspectRatio: AspectRatioState;
scaledSize: {
width: ParameterWidth;
height: ParameterHeight;

View File

@ -11,9 +11,9 @@ import {
getRGId,
} from 'features/controlLayers/konva/naming';
import {
bboxHeightChanged,
bboxWidthChanged,
caRecalled,
documentHeightChanged,
documentWidthChanged,
ipaRecalled,
layerAllDeleted,
layerRecalled,
@ -115,11 +115,11 @@ const recallScheduler: MetadataRecallFunc<ParameterScheduler> = (scheduler) => {
const setSizeOptions = { updateAspectRatio: true, clamp: true };
const recallWidth: MetadataRecallFunc<ParameterWidth> = (width) => {
getStore().dispatch(documentWidthChanged({ width, ...setSizeOptions }));
getStore().dispatch(bboxWidthChanged({ width, ...setSizeOptions }));
};
const recallHeight: MetadataRecallFunc<ParameterHeight> = (height) => {
getStore().dispatch(documentHeightChanged({ height, ...setSizeOptions }));
getStore().dispatch(bboxHeightChanged({ height, ...setSizeOptions }));
};
const recallSteps: MetadataRecallFunc<ParameterSteps> = (steps) => {

View File

@ -1,6 +1,6 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { scaledBboxChanged } from 'features/controlLayers/store/canvasV2Slice';
import { bboxScaledSizeChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectOptimalDimension } from 'features/controlLayers/store/selectors';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -20,7 +20,7 @@ const ParamScaledHeight = () => {
const onChange = useCallback(
(height: number) => {
dispatch(scaledBboxChanged({ height }));
dispatch(bboxScaledSizeChanged({ height }));
},
[dispatch]
);

View File

@ -1,6 +1,6 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { scaledBboxChanged } from 'features/controlLayers/store/canvasV2Slice';
import { bboxScaledSizeChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectOptimalDimension } from 'features/controlLayers/store/selectors';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -19,7 +19,7 @@ const ParamScaledWidth = () => {
const fineStep = useAppSelector((s) => s.config.sd.scaledBoundingBoxWidth.fineStep);
const onChange = useCallback(
(width: number) => {
dispatch(scaledBboxChanged({ width }));
dispatch(bboxScaledSizeChanged({ width }));
},
[dispatch]
);

View File

@ -1,7 +1,7 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { documentHeightChanged } from 'features/controlLayers/store/canvasV2Slice';
import { bboxHeightChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectOptimalDimension } from 'features/controlLayers/store/selectors';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@ -10,7 +10,7 @@ export const ParamHeight = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const optimalDimension = useAppSelector(selectOptimalDimension);
const height = useAppSelector((s) => s.canvasV2.document.rect.height);
const height = useAppSelector((s) => s.canvasV2.bbox.rect.height);
const sliderMin = useAppSelector((s) => s.config.sd.height.sliderMin);
const sliderMax = useAppSelector((s) => s.config.sd.height.sliderMax);
const numberInputMin = useAppSelector((s) => s.config.sd.height.numberInputMin);
@ -20,7 +20,7 @@ export const ParamHeight = memo(() => {
const onChange = useCallback(
(v: number) => {
dispatch(documentHeightChanged({ height: v }));
dispatch(bboxHeightChanged({ height: v }));
},
[dispatch]
);

View File

@ -1,7 +1,7 @@
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { documentWidthChanged } from 'features/controlLayers/store/canvasV2Slice';
import { bboxWidthChanged } from 'features/controlLayers/store/canvasV2Slice';
import { selectOptimalDimension } from 'features/controlLayers/store/selectors';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
export const ParamWidth = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const width = useAppSelector((s) => s.canvasV2.document.rect.width);
const width = useAppSelector((s) => s.canvasV2.bbox.rect.width);
const optimalDimension = useAppSelector(selectOptimalDimension);
const sliderMin = useAppSelector((s) => s.config.sd.width.sliderMin);
const sliderMax = useAppSelector((s) => s.config.sd.width.sliderMax);
@ -20,7 +20,7 @@ export const ParamWidth = memo(() => {
const onChange = useCallback(
(v: number) => {
dispatch(documentWidthChanged({ width: v }));
dispatch(bboxWidthChanged({ width: v }));
},
[dispatch]
);

View File

@ -16,13 +16,13 @@ import {
} from './constants';
export const AspectRatioIconPreview = memo(() => {
const document = useAppSelector((s) => s.canvasV2.document);
const bbox = useAppSelector((s) => s.canvasV2.bbox);
const containerRef = useRef<HTMLDivElement>(null);
const containerSize = useSize(containerRef);
const shouldShowIcon = useMemo(
() => document.aspectRatio.value < ICON_HIGH_CUTOFF && document.aspectRatio.value > ICON_LOW_CUTOFF,
[document.aspectRatio.value]
() => bbox.aspectRatio.value < ICON_HIGH_CUTOFF && bbox.aspectRatio.value > ICON_LOW_CUTOFF,
[bbox.aspectRatio.value]
);
const { width, height } = useMemo(() => {
@ -30,19 +30,19 @@ export const AspectRatioIconPreview = memo(() => {
return { width: 0, height: 0 };
}
let width = document.rect.width;
let height = document.rect.height;
let width = bbox.rect.width;
let height = bbox.rect.height;
if (document.rect.width > document.rect.height) {
if (bbox.rect.width > bbox.rect.height) {
width = containerSize.width;
height = width / document.aspectRatio.value;
height = width / bbox.aspectRatio.value;
} else {
height = containerSize.height;
width = height * document.aspectRatio.value;
width = height * bbox.aspectRatio.value;
}
return { width, height };
}, [containerSize, document.rect.width, document.rect.height, document.aspectRatio.value]);
}, [containerSize, bbox.rect.width, bbox.rect.height, bbox.aspectRatio.value]);
return (
<Flex w="full" h="full" alignItems="center" justifyContent="center" ref={containerRef}>

View File

@ -3,7 +3,7 @@ import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import type { SingleValue } from 'chakra-react-select';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { documentAspectRatioIdChanged } from 'features/controlLayers/store/canvasV2Slice';
import { bboxAspectRatioIdChanged } from 'features/controlLayers/store/canvasV2Slice';
import { ASPECT_RATIO_OPTIONS } from 'features/parameters/components/DocumentSize/constants';
import { isAspectRatioID } from 'features/parameters/components/DocumentSize/types';
import { memo, useCallback, useMemo } from 'react';
@ -12,14 +12,14 @@ import { useTranslation } from 'react-i18next';
export const AspectRatioSelect = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const id = useAppSelector((s) => s.canvasV2.document.aspectRatio.id);
const id = useAppSelector((s) => s.canvasV2.bbox.aspectRatio.id);
const onChange = useCallback(
(v: SingleValue<ComboboxOption>) => {
if (!v || !isAspectRatioID(v.value)) {
return;
}
dispatch(documentAspectRatioIdChanged({ id: v.value }));
dispatch(bboxAspectRatioIdChanged({ id: v.value }));
},
[dispatch]
);

View File

@ -1,6 +1,6 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { documentAspectRatioLockToggled } from 'features/controlLayers/store/canvasV2Slice';
import { bboxAspectRatioLockToggled } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiLockSimpleFill, PiLockSimpleOpenBold } from 'react-icons/pi';
@ -8,9 +8,9 @@ import { PiLockSimpleFill, PiLockSimpleOpenBold } from 'react-icons/pi';
export const LockAspectRatioButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isLocked = useAppSelector((s) => s.canvasV2.document.aspectRatio.isLocked);
const isLocked = useAppSelector((s) => s.canvasV2.bbox.aspectRatio.isLocked);
const onClick = useCallback(() => {
dispatch(documentAspectRatioLockToggled());
dispatch(bboxAspectRatioLockToggled());
}, [dispatch]);
return (

View File

@ -1,6 +1,6 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { documentSizeOptimized } from 'features/controlLayers/store/canvasV2Slice';
import { bboxSizeOptimized } from 'features/controlLayers/store/canvasV2Slice';
import { selectOptimalDimension } from 'features/controlLayers/store/selectors';
import { getIsSizeTooLarge, getIsSizeTooSmall } from 'features/parameters/util/optimalDimension';
import { memo, useCallback, useMemo } from 'react';
@ -10,8 +10,8 @@ import { RiSparklingFill } from 'react-icons/ri';
export const SetOptimalSizeButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const width = useAppSelector((s) => s.canvasV2.document.rect.width);
const height = useAppSelector((s) => s.canvasV2.document.rect.height);
const width = useAppSelector((s) => s.canvasV2.bbox.rect.width);
const height = useAppSelector((s) => s.canvasV2.bbox.rect.height);
const optimalDimension = useAppSelector(selectOptimalDimension);
const isSizeTooSmall = useMemo(
() => getIsSizeTooSmall(width, height, optimalDimension),
@ -22,7 +22,7 @@ export const SetOptimalSizeButton = memo(() => {
[height, width, optimalDimension]
);
const onClick = useCallback(() => {
dispatch(documentSizeOptimized());
dispatch(bboxSizeOptimized());
}, [dispatch]);
const tooltip = useMemo(() => {
if (isSizeTooSmall) {

View File

@ -1,6 +1,6 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { documentDimensionsSwapped } from 'features/controlLayers/store/canvasV2Slice';
import { bboxDimensionsSwapped } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowsDownUpBold } from 'react-icons/pi';
@ -9,7 +9,7 @@ export const SwapDimensionsButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const onClick = useCallback(() => {
dispatch(documentDimensionsSwapped());
dispatch(bboxDimensionsSwapped());
}, [dispatch]);
return (
<IconButton

View File

@ -24,8 +24,8 @@ const selector = createMemoizedSelector([selectHrfSlice, selectCanvasV2Slice], (
const badges: string[] = [];
const isSDXL = model?.base === 'sdxl';
const { aspectRatio } = canvasV2.document;
const { width, height } = canvasV2.document.rect;
const { aspectRatio } = canvasV2.bbox;
const { width, height } = canvasV2.bbox.rect;
badges.push(`${width}×${height}`);
badges.push(aspectRatio.id);