Adds user/result galleries, refactors workarea CSS

This commit is contained in:
psychedelicious 2022-10-29 03:54:46 +11:00
parent 3a7b495167
commit 0855ab4173
30 changed files with 362 additions and 482 deletions

View File

@ -443,10 +443,10 @@ class InvokeAIWebServer:
print("\n")
# TODO: I think this needs a safety mechanism.
@socketio.on("uploadInitialImage")
def handle_upload_initial_image(bytes, name):
@socketio.on("uploadImage")
def handle_upload_image(bytes, name, destination):
try:
print(f'>> Init image upload requested "{name}"')
print(f'>> Image upload requested "{name}"')
file_path = self.save_file_unique_uuid_name(
bytes=bytes, name=name, path=self.init_image_path
)
@ -454,13 +454,14 @@ class InvokeAIWebServer:
(width, height) = Image.open(file_path).size
print(file_path)
socketio.emit(
"initialImageUploaded",
"imageUploaded",
{
"url": self.get_url_from_image_path(file_path),
"mtime": mtime,
"width": width,
"height": height,
"category": "user",
"destination": destination,
},
)
except Exception as e:

View File

@ -7,12 +7,11 @@
# was trained on.
stable-diffusion-1.4:
config: configs/stable-diffusion/v1-inference.yaml
weights: models/ldm/stable-diffusion-v1/model.ckpt
weights: models/ldm/stable-diffusion-v1/sd-v1-4.ckpt
# vae: models/ldm/stable-diffusion-v1/vae-ft-mse-840000-ema-pruned.ckpt
description: Stable Diffusion inference model version 1.4
width: 512
height: 512
default: true
inpainting-1.5:
description: runwayML tuned inpainting model v1.5
weights: models/ldm/stable-diffusion-v1/sd-v1-5-inpainting.ckpt
@ -27,3 +26,4 @@ stable-diffusion-1.5:
description: Stable Diffusion inference model version 1.5
width: 512
height: 512
default: true

View File

@ -172,7 +172,9 @@ export declare type SystemConfigResponse = SystemConfig;
export declare type ImageResultResponse = Omit<Image, 'uuid'>;
export declare type ImageUploadResponse = Omit<Image, 'uuid' | 'metadata'>;
export declare type ImageUploadResponse = Omit<Image, 'uuid' | 'metadata'> & {
destination: 'img2img' | 'inpainting';
};
export declare type ErrorResponse = {
message: string;
@ -194,3 +196,8 @@ export declare type ImageDeletedResponse = {
export declare type ImageUrlResponse = {
url: string;
};
export declare type UploadImagePayload = {
file: File;
destination: 'img2img' | 'inpainting';
};

View File

@ -3,6 +3,7 @@ import { GalleryCategory } from '../../features/gallery/gallerySlice';
import { InvokeTabName } from '../../features/tabs/InvokeTabs';
import * as InvokeAI from '../invokeai';
/**
* We can't use redux-toolkit's createSlice() to make these actions,
* because they have no associated reducer. They only exist to dispatch
@ -16,20 +17,22 @@ export const generateImage = createAction<InvokeTabName>(
export const runESRGAN = createAction<InvokeAI.Image>('socketio/runESRGAN');
export const runFacetool = createAction<InvokeAI.Image>('socketio/runFacetool');
export const deleteImage = createAction<InvokeAI.Image>('socketio/deleteImage');
export const requestImages = createAction<GalleryCategory>('socketio/requestImages');
export const requestImages = createAction<GalleryCategory>(
'socketio/requestImages'
);
export const requestNewImages = createAction<GalleryCategory>(
'socketio/requestNewImages'
);
export const cancelProcessing = createAction<undefined>(
'socketio/cancelProcessing'
);
export const uploadInitialImage = createAction<File>(
'socketio/uploadInitialImage'
);
export const uploadImage = createAction<InvokeAI.UploadImagePayload>('socketio/uploadImage');
export const uploadMaskImage = createAction<File>('socketio/uploadMaskImage');
export const requestSystemConfig = createAction<undefined>(
'socketio/requestSystemConfig'
);
export const requestModelChange = createAction<string>('socketio/requestModelChange');
export const requestModelChange = createAction<string>(
'socketio/requestModelChange'
);

View File

@ -178,8 +178,9 @@ const makeSocketIOEmitters = (
emitCancelProcessing: () => {
socketio.emit('cancel');
},
emitUploadInitialImage: (file: File) => {
socketio.emit('uploadInitialImage', file, file.name);
emitUploadImage: (payload: InvokeAI.UploadImagePayload) => {
const { file, destination } = payload;
socketio.emit('uploadImage', file, file.name, destination);
},
emitUploadMaskImage: (file: File) => {
socketio.emit('uploadMaskImage', file, file.name);

View File

@ -32,6 +32,7 @@ import {
setMaskPath,
} from '../../features/options/optionsSlice';
import { requestImages, requestNewImages } from './actions';
import { setImageToInpaint } from '../../features/tabs/Inpainting/inpaintingSlice';
/**
* Returns an object containing listener callbacks for socketio events.
@ -285,15 +286,26 @@ const makeSocketIOListeners = (
})
);
},
onInitialImageUploaded: (data: InvokeAI.ImageUploadResponse) => {
onImageUploaded: (data: InvokeAI.ImageUploadResponse) => {
const { destination, ...rest } = data;
const image = {
uuid: uuidv4(),
...data,
...rest,
};
try {
dispatch(addImage({ image, category: 'user' }));
dispatch(setInitialImage(image));
switch (destination) {
case 'img2img': {
dispatch(setInitialImage(image));
break;
}
case 'inpainting': {
dispatch(setImageToInpaint(image));
break;
}
}
dispatch(
addLogEntry({

View File

@ -43,7 +43,7 @@ export const socketioMiddleware = () => {
onGalleryImages,
onProcessingCanceled,
onImageDeleted,
onInitialImageUploaded,
onImageUploaded,
onMaskImageUploaded,
onSystemConfig,
onModelChanged,
@ -58,7 +58,7 @@ export const socketioMiddleware = () => {
emitRequestImages,
emitRequestNewImages,
emitCancelProcessing,
emitUploadInitialImage,
emitUploadImage,
emitUploadMaskImage,
emitRequestSystemConfig,
emitRequestModelChange,
@ -104,14 +104,10 @@ export const socketioMiddleware = () => {
onImageDeleted(data);
});
// socketio.on('initialImageUploaded', (data: InvokeAI.ImageUrlResponse) => {
// onInitialImageUploaded(data);
// });
socketio.on(
'initialImageUploaded',
'imageUploaded',
(data: InvokeAI.ImageUploadResponse) => {
onInitialImageUploaded(data);
onImageUploaded(data);
}
);
@ -173,8 +169,8 @@ export const socketioMiddleware = () => {
break;
}
case 'socketio/uploadInitialImage': {
emitUploadInitialImage(action.payload);
case 'socketio/uploadImage': {
emitUploadImage(action.payload);
break;
}

View File

@ -0,0 +1,30 @@
.image-upload-zone {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 0.5rem;
color: var(--subtext-color-bright);
&:hover {
background-color: var(--inpaint-bg-color);
}
}
.image-upload-child-wrapper {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.image-upload-child {
display: flex;
flex-direction: column;
row-gap: 3rem;
align-items: center;
justify-content: center;
}

View File

@ -1,29 +1,23 @@
import { Button, useToast } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import { Button, Heading, useToast } from '@chakra-ui/react';
import { useCallback } from 'react';
import { FileRejection } from 'react-dropzone';
import { useAppDispatch } from '../../app/store';
import { FaUpload } from 'react-icons/fa';
import ImageUploader from '../../features/options/ImageUploader';
interface InvokeImageUploaderProps {
label?: string;
icon?: any;
onMouseOver?: any;
OnMouseout?: any;
dispatcher: any;
handleFile: (file: File) => void;
styleClass?: string;
}
export default function InvokeImageUploader(props: InvokeImageUploaderProps) {
const { label, icon, dispatcher, styleClass, onMouseOver, OnMouseout } =
props;
const { handleFile, styleClass } = props;
const toast = useToast();
const dispatch = useAppDispatch();
// Callbacks to for handling file upload attempts
const fileAcceptedCallback = useCallback(
(file: File) => dispatch(dispatcher(file)),
[dispatch, dispatcher]
(file: File) => handleFile(file),
[handleFile]
);
const fileRejectionCallback = useCallback(
@ -44,22 +38,17 @@ export default function InvokeImageUploader(props: InvokeImageUploaderProps) {
);
return (
<ImageUploader
fileAcceptedCallback={fileAcceptedCallback}
fileRejectionCallback={fileRejectionCallback}
styleClass={styleClass}
>
<Button
size={'sm'}
fontSize={'md'}
fontWeight={'normal'}
onMouseOver={onMouseOver}
onMouseOut={OnMouseout}
leftIcon={icon}
width={'100%'}
<div className="image-upload-zone">
<ImageUploader
fileAcceptedCallback={fileAcceptedCallback}
fileRejectionCallback={fileRejectionCallback}
styleClass={`${styleClass} image-upload-child-wrapper`}
>
{label ? label : null}
</Button>
</ImageUploader>
<div className="image-upload-child">
<FaUpload size={'7rem'} />
<Heading size={'lg'}>Upload or Drop Image Here</Heading>
</div>
</ImageUploader>
</div>
);
}

View File

@ -1,28 +1,23 @@
@use '../../styles/Mixins/' as *;
.current-image-display {
.current-image-area {
display: flex;
flex-direction: column;
height: 100%;
row-gap: 1rem;
background-color: var(--background-color-secondary);
border-radius: 0.5rem;
&[data-tab-name='txt2img'] {
height: $app-text-to-image-height;
}
}
.current-image-options {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
column-gap: 0.5rem;
padding: 1rem;
button {
@include Button(
$btn-width: 3rem,
$icon-size: 22px,
$btn-color: var(--btn-grey),
$btn-color-hover: var(--btn-grey-hover)
);
}
}
.current-image-viewer {
@ -32,25 +27,18 @@
}
.current-image-preview {
position: absolute;
top:0;
grid-area: current-image-preview;
position: relative;
justify-content: center;
align-items: center;
display: grid;
display: flex;
width: 100%;
height: 100%;
grid-template-areas: 'current-image-content';
// height: 100%;
height: $app-text-to-image-height;
img {
grid-area: current-image-content;
background-color: var(--img2img-img-bg-color);
border-radius: 0.5rem;
object-fit: contain;
// width: auto;
// height: $app-gallery-height;
max-height: $app-gallery-height;
}
}

View File

@ -3,39 +3,65 @@ import CurrentImageButtons from './CurrentImageButtons';
import { MdPhoto } from 'react-icons/md';
import CurrentImagePreview from './CurrentImagePreview';
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
import { tabMap } from '../tabs/InvokeTabs';
import { GalleryState } from './gallerySlice';
import { OptionsState } from '../options/optionsSlice';
import _ from 'lodash';
import { createSelector } from '@reduxjs/toolkit';
export const currentImageDisplaySelector = createSelector(
[(state: RootState) => state.gallery, (state: RootState) => state.options],
(gallery: GalleryState, options: OptionsState) => {
const { currentImage, intermediateImage } = gallery;
const { activeTab, shouldShowImageDetails } = options;
return {
currentImage,
intermediateImage,
activeTabName: tabMap[activeTab],
shouldShowImageDetails,
};
},
{
memoizeOptions: {
resultEqualityCheck: _.isEqual,
},
}
);
/**
* Displays the current image if there is one, plus associated actions.
*/
const CurrentImageDisplay = () => {
const { currentImage, intermediateImage } = useAppSelector(
(state: RootState) => state.gallery
);
const shouldShowImageDetails = useAppSelector(
(state: RootState) => state.options.shouldShowImageDetails
);
const {
currentImage,
intermediateImage,
activeTabName,
shouldShowImageDetails,
} = useAppSelector(currentImageDisplaySelector);
const imageToDisplay = intermediateImage || currentImage;
return imageToDisplay ? (
<div className="current-image-display">
<div className="current-image-tools">
<CurrentImageButtons image={imageToDisplay} />
</div>
<div className="current-image-viewer">
<CurrentImagePreview imageToDisplay={imageToDisplay} />
{shouldShowImageDetails && (
<ImageMetadataViewer
image={imageToDisplay}
styleClass="current-image-metadata"
/>
)}
</div>
</div>
) : (
<div className="current-image-display-placeholder">
<MdPhoto />
return (
<div className="current-image-area" data-tab-name={activeTabName}>
{imageToDisplay ? (
<>
<CurrentImageButtons image={imageToDisplay} />
<div className="current-image-viewer">
<CurrentImagePreview imageToDisplay={imageToDisplay} />
{shouldShowImageDetails && (
<ImageMetadataViewer
image={imageToDisplay}
styleClass="current-image-metadata"
/>
)}
</div>
</>
) : (
<div className="current-image-display-placeholder">
<MdPhoto />
</div>
)}
</div>
);
};

View File

@ -6,11 +6,13 @@ import { GalleryState, selectNextImage, selectPrevImage } from './gallerySlice';
import * as InvokeAI from '../../app/invokeai';
import { createSelector } from '@reduxjs/toolkit';
import _ from 'lodash';
import { OptionsState } from '../options/optionsSlice';
const imagesSelector = createSelector(
(state: RootState) => state.gallery,
(gallery: GalleryState) => {
export const imagesSelector = createSelector(
[(state: RootState) => state.gallery, (state: RootState) => state.options],
(gallery: GalleryState, options: OptionsState) => {
const { currentCategory } = gallery;
const { shouldShowImageDetails } = options;
const tempImages = gallery.categories[currentCategory].images;
const currentImageIndex = tempImages.findIndex(
@ -22,6 +24,7 @@ const imagesSelector = createSelector(
isOnFirstImage: currentImageIndex === 0,
isOnLastImage:
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
shouldShowImageDetails,
};
},
{
@ -39,11 +42,12 @@ export default function CurrentImagePreview(props: CurrentImagePreviewProps) {
const { imageToDisplay } = props;
const dispatch = useAppDispatch();
const { isOnFirstImage, isOnLastImage, currentCategory } = useAppSelector(imagesSelector);
const shouldShowImageDetails = useAppSelector(
(state: RootState) => state.options.shouldShowImageDetails
);
const {
isOnFirstImage,
isOnLastImage,
currentCategory,
shouldShowImageDetails,
} = useAppSelector(imagesSelector);
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] =
useState<boolean>(false);
@ -65,7 +69,7 @@ export default function CurrentImagePreview(props: CurrentImagePreviewProps) {
};
return (
<div className="current-image-preview">
<div className={'current-image-preview'}>
<Image
src={imageToDisplay.url}
fit="contain"

View File

@ -1,5 +1,11 @@
import { Box } from '@chakra-ui/react';
import { cloneElement, ReactElement, SyntheticEvent, useCallback } from 'react';
import {
cloneElement,
ReactElement,
ReactNode,
SyntheticEvent,
useCallback,
} from 'react';
import { FileRejection, useDropzone } from 'react-dropzone';
type ImageUploaderProps = {
@ -55,7 +61,7 @@ const ImageUploader = ({
};
return (
<Box {...getRootProps()} flexGrow={3} className={`${styleClass}`}>
<Box {...getRootProps()} className={styleClass}>
<input {...getInputProps({ multiple: false })} />
{cloneElement(children, {
onClick: handleClickUploadIcon,

View File

@ -1,9 +1,11 @@
@use '../../../styles/Mixins/' as *;
.image-to-image-workarea {
display: grid;
grid-template-columns: max-content auto;
column-gap: 1rem;
.image-to-image-area {
display: flex;
flex-direction: column;
row-gap: 1rem;
width: 100%;
height: 100%;
}
.image-to-image-panel {
@ -15,16 +17,6 @@
@include HideScrollbar;
}
.image-to-image-display-area {
display: grid;
column-gap: 0.5rem;
.image-gallery-area {
z-index: 2;
height: 100%;
}
}
.image-to-image-strength-main-option {
display: grid;
grid-template-columns: none !important;
@ -34,113 +26,29 @@
}
}
.image-to-image-display {
border-radius: 0.5rem;
background-color: var(--background-color-secondary);
display: grid;
height: $app-content-height;
.init-image-preview-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.current-image-options {
grid-auto-columns: max-content;
justify-self: center;
align-self: start;
h2 {
font-weight: bold;
font-size: 0.9rem;
}
}
.image-to-image-single-preview {
display: grid;
column-gap: 0.5rem;
padding: 0 1rem;
place-content: center;
}
.image-to-image-dual-preview-container {
display: grid;
grid-template-areas: 'img2img-preview';
}
.image-to-image-dual-preview {
grid-area: img2img-preview;
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 0.5rem;
padding: 0 1rem;
place-content: center;
.current-image-preview {
img {
height: calc($app-gallery-height - 2rem);
max-height: calc($app-gallery-height - 2rem);
}
}
}
.img2img-metadata {
grid-area: img2img-preview;
z-index: 3;
}
.init-image-preview {
display: grid;
grid-template-areas: 'init-image-content';
justify-content: center;
align-items: center;
border-radius: 0.5rem;
.init-image-preview-header {
grid-area: init-image-content;
z-index: 2;
display: grid;
grid-template-columns: auto max-content;
height: max-content;
align-items: center;
align-self: start;
padding: 1rem;
border-radius: 0.5rem;
h1 {
padding: 0.2rem 0.6rem;
border-radius: 0.4rem;
background-color: var(--tab-hover-color);
width: max-content;
font-weight: bold;
font-size: 0.85rem;
}
}
.init-image-image {
grid-area: init-image-content;
img {
border-radius: 0.5rem;
object-fit: contain;
background-color: var(--img2img-img-bg-color);
width: auto;
height: calc($app-gallery-height - 2rem);
max-height: calc($app-gallery-height - 2rem);
}
}
}
.image-to-image-upload-btn {
display: grid;
height: 100%;
width: 100%;
height: $app-content-height;
display: flex;
align-items: center;
justify-content: center;
height: $app-text-to-image-height;
button {
overflow: hidden;
width: 100%;
height: 100%;
font-size: 1.5rem;
color: var(--text-color-secondary);
background-color: var(--background-color-secondary);
&:hover {
background-color: var(--img2img-img-bg-color);
}
img {
border-radius: 0.5rem;
object-fit: contain;
width: auto;
}
}
.image-to-image-current-image-display {
position: relative;
}

View File

@ -1,74 +1,40 @@
import React from 'react';
import { FaUpload } from 'react-icons/fa';
import { uploadInitialImage } from '../../../app/socketio/actions';
import { RootState, useAppSelector } from '../../../app/store';
import { uploadImage } from '../../../app/socketio/actions';
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
import InvokeImageUploader from '../../../common/components/InvokeImageUploader';
import CurrentImageButtons from '../../gallery/CurrentImageButtons';
import CurrentImagePreview from '../../gallery/CurrentImagePreview';
import ImageMetadataViewer from '../../gallery/ImageMetaDataViewer/ImageMetadataViewer';
import CurrentImageDisplay from '../../gallery/CurrentImageDisplay';
import InitImagePreview from './InitImagePreview';
export default function ImageToImageDisplay() {
const ImageToImageDisplay = () => {
const dispatch = useAppDispatch();
const initialImage = useAppSelector(
(state: RootState) => state.options.initialImage
);
const { currentImage, intermediateImage } = useAppSelector(
(state: RootState) => state.gallery
);
const { currentImage } = useAppSelector((state: RootState) => state.gallery);
const shouldShowImageDetails = useAppSelector(
(state: RootState) => state.options.shouldShowImageDetails
const imageToImageComponent = initialImage ? (
<div className="image-to-image-area">
<InitImagePreview />
</div>
) : (
<InvokeImageUploader
handleFile={(file: File) =>
dispatch(uploadImage({ file, destination: 'img2img' }))
}
/>
);
const imageToDisplay = intermediateImage || currentImage;
return (
<div
className="image-to-image-display"
style={
imageToDisplay
? { gridAutoRows: 'max-content auto' }
: { gridAutoRows: 'auto' }
}
>
{initialImage ? (
<>
{imageToDisplay ? (
<>
<CurrentImageButtons image={imageToDisplay} />
<div className="image-to-image-dual-preview-container">
<div className="image-to-image-dual-preview">
<InitImagePreview />
<div className="image-to-image-current-image-display">
<CurrentImagePreview imageToDisplay={imageToDisplay} />
{shouldShowImageDetails && (
<ImageMetadataViewer
image={imageToDisplay}
styleClass="img2img-metadata"
/>
)}
</div>
</div>
</div>
</>
) : (
<div className="image-to-image-single-preview">
<InitImagePreview />
</div>
)}
</>
) : (
<div className="upload-image">
<InvokeImageUploader
label="Upload or Drop Image Here"
icon={<FaUpload />}
styleClass="image-to-image-upload-btn"
dispatcher={uploadInitialImage}
/>
<div className="workarea-split-view">
<div className="workarea-split-view-left">{imageToImageComponent} </div>
{currentImage && (
<div className="workarea-split-view-right">
<CurrentImageDisplay />
</div>
)}
</div>
);
}
};
export default ImageToImageDisplay;

View File

@ -27,29 +27,29 @@ export default function InitImagePreview() {
};
return (
<div className="init-image-preview">
<>
<div className="init-image-preview-header">
<h1>Initial Image</h1>
<h2>Initial Image</h2>
<IconButton
isDisabled={!initialImage}
size={'sm'}
aria-label={'Reset Initial Image'}
onClick={handleClickResetInitialImage}
icon={<MdClear />}
/>
</div>
{initialImage && (
<div className="init-image-image">
<div className="init-image-preview">
<Image
fit={'contain'}
maxWidth={'100%'}
maxHeight={'100%'}
src={
typeof initialImage === 'string' ? initialImage : initialImage.url
}
rounded={'md'}
onError={alertMissingInitImage}
/>
</div>
)}
</div>
</>
);
}

View File

@ -1,62 +1,47 @@
@use '../../../styles/Mixins/' as *;
.brush-preview-wrapper {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 0;
left: 0;
}
.brush-preview {
border-radius: 50%;
border: 1px black solid;
}
.inpainting-workarea {
display: grid;
grid-template-columns: max-content auto;
column-gap: 1rem;
}
.inpainting-display-area {
display: grid;
column-gap: 0.5rem;
}
.inpainting-display {
border-radius: 0.5rem;
background-color: var(--background-color-secondary);
display: grid;
grid-template-columns: 1fr 1fr;
// column-gap: 1rem;
height: $app-content-height;
}
.inpainting-toolkit {
// display: grid;
// grid-template-rows: auto auto;
.inpainting-main-area {
display: flex;
flex-direction: column;
background-color: var(--inpaint-bg-color);
border-radius: 0.5rem;
}
.inpainting-canvas-container {
align-items: center;
row-gap: 1rem;
width: 100%;
height: 100%;
padding-left: 1rem;
padding-right: 1rem;
padding-bottom: 1rem;
display: flex;
align-items: center;
justify-content: center;
.inpainting-settings {
display: flex;
align-items: center;
column-gap: 1rem;
.inpainting-buttons-group {
display: flex;
align-items: center;
column-gap: 0.5rem;
}
.inpainting-slider-numberinput {
display: flex;
column-gap: 1rem;
align-items: center;
}
}
.inpainting-canvas-area {
display: flex;
flex-direction: column;
align-items: center;
row-gap: 1rem;
width: 100%;
height: 100%;
}
.inpainting-canvas-wrapper {
display: flex;
align-items: center;
justify-content: center;
position: relative;
width: min-content;
height: min-content;
height: 100%;
width: 100%;
border-radius: 0.5rem;
.inpainting-alerts {
@ -80,6 +65,7 @@
}
.inpainting-canvas-stage {
border-radius: 0.5rem;
canvas {
border-radius: 0.5rem;
}
@ -87,61 +73,8 @@
}
}
// .canvas-scale-calculator {
// width: calc(100% - 1rem);
// height: calc(100% - 1rem);
// display: flex;
// align-items: center;
// justify-content: center;
// }
.inpainting-canvas-scale-wrapper {
width: calc(100% - 1rem);
height: calc(100% - 1rem);
// width: max-content;
// height: max-content;
display: flex;
justify-content: center;
align-items: center;
}
.inpainting-settings {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
row-gap: 1rem;
padding: 1rem;
.inpainting-buttons {
display: flex;
align-items: center;
column-gap: 0.8rem;
button {
height: 2.4rem;
svg {
width: 15px;
height: 15px;
}
}
.inpainting-buttons-group {
display: flex;
align-items: center;
column-gap: 0.5rem;
}
}
.inpainting-slider-numberinput {
display: flex;
column-gap: 1rem;
align-items: center;
}
}
// Overrides
.inpainting-workarea-container {
.inpainting-workarea-overrides {
.image-gallery-area {
.chakra-popover__popper {
inset: 0 auto auto -75px !important;
@ -149,21 +82,8 @@
}
.current-image-options {
button {
@include Button(
$btn-width: 2.5rem,
$icon-size: 18px,
$btn-color: var(--btn-grey),
$btn-color-hover: var(--btn-grey-hover)
);
}
.chakra-popover__popper {
z-index: 11;
}
}
.current-image-preview {
padding: 0 1rem 1rem 1rem;
}
}

View File

@ -218,7 +218,7 @@ const InpaintingCanvas = () => {
);
return (
<div className="inpainting-canvas-wrapper checkerboard" tabIndex={1}>
<div className="inpainting-canvas-wrapper" tabIndex={1}>
<div className="inpainting-alerts">
{!shouldShowMask && (
<div style={{ pointerEvents: 'none' }}>Mask Hidden (H)</div>
@ -245,7 +245,7 @@ const InpaintingCanvas = () => {
onMouseOut={handleMouseOutCanvas}
onMouseLeave={handleMouseOutCanvas}
style={{ cursor: shouldShowMask ? 'none' : 'default' }}
className="inpainting-canvas-stage"
className="inpainting-canvas-stage checkerboard"
ref={stageRef}
>
{!shouldInvertMask && !shouldShowCheckboardTransparency && (

View File

@ -25,7 +25,7 @@ const InpaintingCanvasPlaceholder = () => {
}, [dispatch, imageToInpaint, needsRepaint]);
return (
<div ref={ref} className="inpainting-canvas-container">
<div ref={ref} className="inpainting-canvas-area">
<Spinner thickness="2px" speed="1s" size="xl" />
</div>
);

View File

@ -280,7 +280,6 @@ const InpaintingControls = () => {
return (
<div className="inpainting-settings">
<div className="inpainting-buttons">
<div className="inpainting-buttons-group">
<IAIPopover
trigger="hover"
@ -397,7 +396,6 @@ const InpaintingControls = () => {
onClick={handleDualDisplay}
/>
</div>
</div>
</div>
);
};

View File

@ -1,7 +1,9 @@
import { createSelector } from '@reduxjs/toolkit';
import _ from 'lodash';
import { useLayoutEffect } from 'react';
import { uploadImage } from '../../../app/socketio/actions';
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
import InvokeImageUploader from '../../../common/components/InvokeImageUploader';
import CurrentImageDisplay from '../../gallery/CurrentImageDisplay';
import { OptionsState } from '../../options/optionsSlice';
import InpaintingCanvas from './InpaintingCanvas';
@ -12,11 +14,12 @@ import { InpaintingState, setNeedsRepaint } from './inpaintingSlice';
const inpaintingDisplaySelector = createSelector(
[(state: RootState) => state.inpainting, (state: RootState) => state.options],
(inpainting: InpaintingState, options: OptionsState) => {
const { needsRepaint } = inpainting;
const { needsRepaint, imageToInpaint } = inpainting;
const { showDualDisplay } = options;
return {
needsRepaint,
showDualDisplay,
imageToInpaint,
};
},
{
@ -28,7 +31,7 @@ const inpaintingDisplaySelector = createSelector(
const InpaintingDisplay = () => {
const dispatch = useAppDispatch();
const { showDualDisplay, needsRepaint } = useAppSelector(
const { showDualDisplay, needsRepaint, imageToInpaint } = useAppSelector(
inpaintingDisplaySelector
);
@ -41,27 +44,33 @@ const InpaintingDisplay = () => {
return () => window.removeEventListener('resize', resizeCallback);
}, [dispatch]);
const inpaintingComponent = imageToInpaint ? (
<div className="inpainting-main-area">
<InpaintingControls />
<div className="inpainting-canvas-area">
{needsRepaint ? <InpaintingCanvasPlaceholder /> : <InpaintingCanvas />}
</div>
</div>
) : (
<InvokeImageUploader
handleFile={(file: File) =>
dispatch(uploadImage({ file, destination: 'inpainting' }))
}
/>
);
return (
<div
className="inpainting-display"
style={
showDualDisplay
? { gridTemplateColumns: '1fr 1fr' }
: { gridTemplateColumns: 'auto' }
className={
showDualDisplay ? 'workarea-split-view' : 'workarea-single-view'
}
>
<div className="inpainting-toolkit">
<InpaintingControls />
<div className="inpainting-canvas-container">
{needsRepaint ? (
<InpaintingCanvasPlaceholder />
) : (
<InpaintingCanvas />
)}
<div className="workarea-split-view-left">{inpaintingComponent} </div>
{showDualDisplay && (
<div className="workarea-split-view-right">
<CurrentImageDisplay />
</div>
</div>
{showDualDisplay && <CurrentImageDisplay />}
)}
</div>
);
};

View File

@ -6,7 +6,7 @@ export default function InpaintingWorkarea() {
return (
<InvokeWorkarea
optionsPanel={<InpaintingPanel />}
className="inpainting-workarea-container"
className="inpainting-workarea-overrides"
>
<InpaintingDisplay />
</InvokeWorkarea>

View File

@ -156,10 +156,12 @@ export const inpaintingSlice = createSlice({
setMaskColor: (state, action: PayloadAction<RgbaColor>) => {
state.maskColor = action.payload;
},
// },
setCursorPosition: (state, action: PayloadAction<Vector2d | null>) => {
state.cursorPosition = action.payload;
},
clearImageToInpaint: (state) => {
state.imageToInpaint = undefined;
},
setImageToInpaint: (state, action: PayloadAction<InvokeAI.Image>) => {
const { width: imageWidth, height: imageHeight } = action.payload;
const { width: boundingBoxWidth, height: boundingBoxHeight } =

View File

@ -1,11 +1,11 @@
@use '../../styles/Mixins/' as *;
.workarea-container {
.workarea-wrapper {
position: relative;
width: 100%;
height: 100%;
.workarea {
.workarea-main {
display: flex;
column-gap: 1rem;
@ -15,8 +15,40 @@
flex-shrink: 0;
}
.workarea-content {
.workarea-split-view {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
height: $app-content-height;
background-color: var(--background-color-secondary);
border-radius: 0.5rem;
}
.workarea-single-view {
width: 100%;
height: $app-content-height;
background-color: var(--background-color-secondary);
border-radius: 0.5rem;
}
.workarea-split-view-left,
.workarea-split-view-right {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
row-gap: 1rem;
background-color: var(--background-color-secondary);
border-radius: 0.5rem;
padding: 1rem;
}
.workarea-split-view-left {
padding-right: 0.5rem;
}
.workarea-split-view-right {
padding-left: 0.5rem;
}
}
}

View File

@ -5,25 +5,20 @@ import ShowHideGalleryButton from '../gallery/ShowHideGalleryButton';
type InvokeWorkareaProps = {
optionsPanel: ReactNode;
className?: string;
children: ReactNode;
};
const InvokeWorkarea = (props: InvokeWorkareaProps) => {
const { optionsPanel, className, children } = props;
const { optionsPanel, children } = props;
const { shouldShowGallery, shouldHoldGalleryOpen, shouldPinGallery } =
useAppSelector((state: RootState) => state.gallery);
return (
<div
className={
className ? `workarea-container ${className}` : `workarea-container`
}
>
<div className="workarea">
<div className="workarea-wrapper">
<div className="workarea-main">
<div className="workarea-options-panel">{optionsPanel}</div>
<div className="workarea-content">{children}</div>
{children}
<ImageGallery />
</div>
{!(shouldShowGallery || (shouldHoldGalleryOpen && !shouldPinGallery)) && (

View File

@ -1,9 +1,7 @@
@use '../../../styles/Mixins/' as *;
.text-to-image-workarea {
display: grid;
grid-template-columns: max-content auto;
column-gap: 1rem;
.text-to-image-area {
padding: 1rem;
}
.text-to-image-panel {
@ -14,27 +12,3 @@
overflow-y: scroll;
@include HideScrollbar;
}
.text-to-image-display {
display: grid;
grid-template-areas: 'text-to-image-display';
column-gap: 0.5rem;
.current-image-display,
.current-image-display-placeholder {
grid-area: text-to-image-display;
}
.image-gallery-area {
height: 100%;
z-index: 2;
place-self: end;
}
}
// Overrides
.txt-to-image-workarea-container {
.current-image-preview {
padding: 0 1rem 1rem 1rem;
}
}

View File

@ -0,0 +1,13 @@
import CurrentImageDisplay from '../../gallery/CurrentImageDisplay';
const TextToImageDisplay = () => {
return (
<div className="workarea-single-view">
<div className="text-to-image-area">
<CurrentImageDisplay />
</div>
</div>
);
};
export default TextToImageDisplay;

View File

@ -1,14 +1,11 @@
import TextToImagePanel from './TextToImagePanel';
import CurrentImageDisplay from '../../gallery/CurrentImageDisplay';
import InvokeWorkarea from '../InvokeWorkarea';
import TextToImageDisplay from './TextToImageDisplay';
export default function TextToImageWorkarea() {
return (
<InvokeWorkarea
optionsPanel={<TextToImagePanel />}
className="txt-to-image-workarea-container"
>
<CurrentImageDisplay />
<InvokeWorkarea optionsPanel={<TextToImagePanel />}>
<TextToImageDisplay />
</InvokeWorkarea>
);
}

View File

@ -7,9 +7,11 @@ $app-content-height-cutoff: 7rem; // default: 7rem
$app-width: calc(100vw - $app-cutoff);
$app-height: calc(100vh - $app-cutoff);
$app-content-height: calc(100vh - $app-content-height-cutoff);
$app-gallery-height: calc(100vh - ($app-content-height-cutoff + 6rem));
$app-gallery-height: calc(100vh - ($app-content-height-cutoff + 5.5rem));
$app-gallery-popover-height: calc(100vh - ($app-content-height-cutoff + 6rem));
$app-metadata-height: calc(100vh - ($app-content-height-cutoff + 4.4rem));
// $app-text-to-image-height: calc(100vh - 9.4375rem);
$app-text-to-image-height: calc(100vh - 9.4375rem - 1.925rem - 1.35rem);
// option bar
$options-bar-max-width: 22.5rem;

View File

@ -51,6 +51,7 @@
@use '../common/components/IAICheckbox.scss';
@use '../common/components/IAIPopover.scss';
@use '../common/components/IAIColorPicker.scss';
@use '../common/components/InvokeImageUploader.scss';
@use '../common/components/WorkInProgress/WorkInProgress.scss';
@use '../common/components/GuidePopover.scss';