Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix,feat: canvas followups 7 #6849

Merged
merged 23 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
988c718
feat(ui): close viewer on send image to canvas
psychedelicious Sep 13, 2024
42e1263
feat(ui): add setting to disable progress images on canvas
psychedelicious Sep 13, 2024
d4ddb97
feat(ui): brighter border around fill color buttons
psychedelicious Sep 13, 2024
f868d19
feat(ui): select layer when dropping image on it to replace its data
psychedelicious Sep 13, 2024
51c46a5
feat(ui): add upload button to control layer
psychedelicious Sep 13, 2024
50dbf74
fix(ui): wrong translation used for transform fit to bbox
psychedelicious Sep 13, 2024
4cad712
feat(ui): revised canvas "busyness" state tracking
psychedelicious Sep 13, 2024
c0c51e3
fix(ui): off-by-one layer zIndex
psychedelicious Sep 13, 2024
42e1479
fix(ui): set opacity to 1 when rasterizing for filter
psychedelicious Sep 13, 2024
cd99f4c
fix(ui): do not show/hide objects when filtering
psychedelicious Sep 13, 2024
e9c9bee
feat(ui): more interaction restrictions
psychedelicious Sep 13, 2024
fae9c5d
feat(ui): global canvas hotkey interaction restrictions
psychedelicious Sep 13, 2024
31d9bc7
feat(ui): "Composite Masked Regions" -> "Output Only Masked Regions"
psychedelicious Sep 13, 2024
02fce39
feat(ui): show alert when selected layer is empty
psychedelicious Sep 13, 2024
61bd13a
feat(ui): revised `Result` implementation
psychedelicious Sep 13, 2024
c4efb48
docs(ui): update canvasToBlob docstring
psychedelicious Sep 13, 2024
8c266e9
feat(ui): handle errors when saving canvas to gallery
psychedelicious Sep 13, 2024
f68db92
feat(ui): add filter/transform/delete to canvas context menu
psychedelicious Sep 13, 2024
31d5293
feat(ui): on-theme checkboard pattern colors
psychedelicious Sep 13, 2024
79bf0db
feat(ui): filter process button does immediate processing
psychedelicious Sep 13, 2024
5b5c906
chore(ui): lint
psychedelicious Sep 13, 2024
6cd6dcc
feat(ui): bbox has black and white outline
psychedelicious Sep 13, 2024
c78dd12
feat(ui): region starts w/ no prompts or ip adapters
psychedelicious Sep 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1021,7 +1021,10 @@
"incompatibleBaseModelForControlAdapter": "Control Adapter #{{number}} model is incompatible with main model.",
"noModelSelected": "No model selected",
"canvasManagerNotLoaded": "Canvas Manager not loaded",
"canvasBusy": "Canvas is busy",
"canvasIsFiltering": "Canvas is filtering",
"canvasIsTransforming": "Canvas is transforming",
"canvasIsRasterizing": "Canvas is rasterizing",
"canvasIsCompositing": "Canvas is compositing",
"noPrompts": "No prompts generated",
"noNodesInGraph": "No nodes in graph",
"systemDisconnected": "System disconnected",
Expand Down Expand Up @@ -1661,6 +1664,7 @@
"storeNotInitialized": "Store is not initialized"
},
"controlLayers": {
"canvas": "Canvas",
"bookmark": "Bookmark for Quick Switch",
"fitBboxToLayers": "Fit Bbox To Layers",
"removeBookmark": "Remove Bookmark",
Expand All @@ -1672,6 +1676,7 @@
"sendBboxToRasterLayer": "Send Bbox to Raster Layer",
"savedToGalleryOk": "Saved to Gallery",
"savedToGalleryError": "Error saving to gallery",
"regionIsEmpty": "Selected region is empty",
"mergeVisible": "Merge Visible",
"mergeVisibleOk": "Merged visible layers",
"mergeVisibleError": "Error merging visible layers",
Expand All @@ -1686,7 +1691,7 @@
"clearCaches": "Clear Caches",
"recalculateRects": "Recalculate Rects",
"clipToBbox": "Clip Strokes to Bbox",
"compositeMaskedRegions": "Composite Masked Regions",
"outputOnlyMaskedRegions": "Output Only Masked Regions",
"addLayer": "Add Layer",
"duplicate": "Duplicate",
"moveToFront": "Move to Front",
Expand Down Expand Up @@ -1788,6 +1793,7 @@
"replaceLayer": "Replace Layer",
"pullBboxIntoLayer": "Pull Bbox into Layer",
"pullBboxIntoIPAdapter": "Pull Bbox into IP Adapter",
"showProgressOnCanvas": "Show Progress on Canvas",
"fill": {
"fillColor": "Fill Color",
"fillStyle": "Fill Style",
Expand Down Expand Up @@ -1919,6 +1925,7 @@
"isLocked": "is locked",
"isHidden": "is hidden",
"isDisabled": "is disabled",
"isEmpty": "is empty",
"enabled": "Enabled"
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { enqueueRequested } from 'app/store/actions';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import type { SerializableObject } from 'common/types';
import type { Result } from 'common/util/result';
import { isErr, withResult, withResultAsync } from 'common/util/result';
import { withResult, withResultAsync } from 'common/util/result';
import { $canvasManager } from 'features/controlLayers/store/canvasSlice';
import {
selectIsStaging,
Expand Down Expand Up @@ -66,7 +66,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
assert(false, `No graph builders for base ${base}`);
}

if (isErr(buildGraphResult)) {
if (buildGraphResult.isErr()) {
log.error({ error: serializeError(buildGraphResult.error) }, 'Failed to build graph');
abortStaging();
return;
Expand All @@ -80,7 +80,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
prepareLinearUIBatch(state, g, prepend, noise, posCond, 'generation', destination)
);

if (isErr(prepareBatchResult)) {
if (prepareBatchResult.isErr()) {
log.error({ error: serializeError(prepareBatchResult.error) }, 'Failed to prepare batch');
abortStaging();
return;
Expand All @@ -95,7 +95,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)

const enqueueResult = await withResultAsync(() => req.unwrap());

if (isErr(enqueueResult)) {
if (enqueueResult.isErr()) {
log.error({ error: serializeError(enqueueResult.error) }, 'Failed to enqueue batch');
abortStaging();
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { selectDefaultControlAdapter } from 'features/controlLayers/hooks/addLay
import {
controlLayerAdded,
entityRasterized,
entitySelected,
ipaImageChanged,
rasterLayerAdded,
rgIPAdapterImageChanged,
Expand Down Expand Up @@ -127,6 +128,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
const imageObject = imageDTOToImageObject(activeData.payload.imageDTO);
const { x, y } = selectCanvasSlice(state).bbox.rect;
dispatch(entityRasterized({ entityIdentifier, imageObject, position: { x, y }, replaceObjects: true }));
dispatch(entitySelected({ entityIdentifier }));
return;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { ipaImageChanged, rgIPAdapterImageChanged } from 'features/controlLayers/store/canvasSlice';
import {
entityRasterized,
entitySelected,
ipaImageChanged,
rgIPAdapterImageChanged,
} from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors';
import { boardIdSelected, galleryViewChanged } from 'features/gallery/store/gallerySlice';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
Expand Down Expand Up @@ -114,6 +121,17 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
toast({ ...DEFAULT_UPLOADED_TOAST, description: `${t('toast.setNodeField')} ${fieldName}` });
return;
}

if (postUploadAction?.type === 'REPLACE_LAYER_WITH_IMAGE') {
const { entityIdentifier } = postUploadAction;

const state = getState();
const imageObject = imageDTOToImageObject(imageDTO);
const { x, y } = selectCanvasSlice(state).bbox.rect;
dispatch(entityRasterized({ entityIdentifier, imageObject, position: { x, y }, replaceObjects: true }));
dispatch(entitySelected({ entityIdentifier }));
return;
}
},
});

Expand Down
42 changes: 35 additions & 7 deletions invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { selectSystemSlice } from 'features/system/store/systemSlice';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import i18n from 'i18next';
import { forEach, upperFirst } from 'lodash-es';
import type { ReadableAtom } from 'nanostores';
import { atom } from 'nanostores';
import { useMemo } from 'react';
import { getConnectedEdges } from 'reactflow';
Expand All @@ -30,7 +31,14 @@ const LAYER_TYPE_TO_TKEY = {
control_layer: 'controlLayers.globalControlAdapter',
} as const;

const createSelector = (templates: Templates, isConnected: boolean, canvasIsBusy: boolean) =>
const createSelector = (
templates: Templates,
isConnected: boolean,
canvasIsFiltering: boolean,
canvasIsTransforming: boolean,
canvasIsRasterizing: boolean,
canvasIsCompositing: boolean
) =>
createMemoizedSelector(
[
selectSystemSlice,
Expand Down Expand Up @@ -119,8 +127,17 @@ const createSelector = (templates: Templates, isConnected: boolean, canvasIsBusy
reasons.push({ content: i18n.t('upscaling.missingTileControlNetModel') });
}
} else {
if (canvasIsBusy) {
reasons.push({ content: i18n.t('parameters.invoke.canvasBusy') });
if (canvasIsFiltering) {
reasons.push({ content: i18n.t('parameters.invoke.canvasIsFiltering') });
}
if (canvasIsTransforming) {
reasons.push({ content: i18n.t('parameters.invoke.canvasIsTransforming') });
}
if (canvasIsRasterizing) {
reasons.push({ content: i18n.t('parameters.invoke.canvasIsRasterizing') });
}
if (canvasIsCompositing) {
reasons.push({ content: i18n.t('parameters.invoke.canvasIsCompositing') });
}

if (dynamicPrompts.prompts.length === 0 && getShouldProcessPrompt(positivePrompt)) {
Expand Down Expand Up @@ -246,16 +263,27 @@ const createSelector = (templates: Templates, isConnected: boolean, canvasIsBusy
}
);

const dummyAtom = atom(true);
const $fallbackTrue: ReadableAtom<true> = atom(true);

export const useIsReadyToEnqueue = () => {
const templates = useStore($templates);
const isConnected = useStore($isConnected);
const canvasManager = useCanvasManagerSafe();
const canvasIsBusy = useStore(canvasManager?.$isBusy ?? dummyAtom);
const canvasIsFiltering = useStore(canvasManager?.stateApi.$isFiltering ?? $fallbackTrue);
const canvasIsTransforming = useStore(canvasManager?.stateApi.$isTransforming ?? $fallbackTrue);
const canvasIsRasterizing = useStore(canvasManager?.stateApi.$isRasterizing ?? $fallbackTrue);
const canvasIsCompositing = useStore(canvasManager?.compositor.$isBusy ?? $fallbackTrue);
const selector = useMemo(
() => createSelector(templates, isConnected, canvasIsBusy),
[templates, isConnected, canvasIsBusy]
() =>
createSelector(
templates,
isConnected,
canvasIsFiltering,
canvasIsTransforming,
canvasIsRasterizing,
canvasIsCompositing
),
[templates, isConnected, canvasIsFiltering, canvasIsTransforming, canvasIsRasterizing, canvasIsCompositing]
);
const value = useAppSelector(selector);
return value;
Expand Down
45 changes: 23 additions & 22 deletions invokeai/frontend/web/src/common/util/result.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
import { describe, expect, it } from 'vitest';

import type { ErrResult, OkResult } from './result';
import { Err, isErr, isOk, Ok, withResult, withResultAsync } from './result'; // Adjust import as needed
import { Err, ErrResult, Ok, OkResult, withResult, withResultAsync } from './result';

const promiseify = <T>(fn: () => T): (() => Promise<T>) => {
return () =>
Expand All @@ -13,28 +12,30 @@ const promiseify = <T>(fn: () => T): (() => Promise<T>) => {
};

describe('Result Utility Functions', () => {
it('Ok() should create an OkResult', () => {
const result = Ok(42);
expect(result).toEqual({ type: 'Ok', value: 42 });
expect(isOk(result)).toBe(true);
expect(isErr(result)).toBe(false);
assert<Equals<OkResult<number>, typeof result>>(result);
it('OkResult() should create an Ok result', () => {
const result = OkResult(42);
expect(result).toBeInstanceOf(Ok);
expect(result.isOk()).toBe(true);
expect(result.isErr()).toBe(false);
expect(result.value).toBe(42);
assert<Equals<Ok<number>, typeof result>>(result);
});

it('Err() should create an ErrResult', () => {
it('ErrResult() should create an Err result', () => {
const error = new Error('Something went wrong');
const result = Err(error);
expect(result).toEqual({ type: 'Err', error });
expect(isOk(result)).toBe(false);
expect(isErr(result)).toBe(true);
assert<Equals<ErrResult<Error>, typeof result>>(result);
const result = ErrResult(error);
expect(result).toBeInstanceOf(Err);
expect(result.isOk()).toBe(false);
expect(result.isErr()).toBe(true);
expect(result.error).toBe(error);
assert<Equals<Err<Error>, typeof result>>(result);
});

it('withResult() should return Ok on success', () => {
const fn = () => 42;
const result = withResult(fn);
expect(isOk(result)).toBe(true);
if (isOk(result)) {
expect(result.isOk()).toBe(true);
if (result.isOk()) {
expect(result.value).toBe(42);
}
});
Expand All @@ -44,17 +45,17 @@ describe('Result Utility Functions', () => {
throw new Error('Failure');
};
const result = withResult(fn);
expect(isErr(result)).toBe(true);
if (isErr(result)) {
expect(result.isErr()).toBe(true);
if (result.isErr()) {
expect(result.error.message).toBe('Failure');
}
});

it('withResultAsync() should return Ok on success', async () => {
const fn = promiseify(() => 42);
const result = await withResultAsync(fn);
expect(isOk(result)).toBe(true);
if (isOk(result)) {
expect(result.isOk()).toBe(true);
if (result.isOk()) {
expect(result.value).toBe(42);
}
});
Expand All @@ -64,8 +65,8 @@ describe('Result Utility Functions', () => {
throw new Error('Async failure');
});
const result = await withResultAsync(fn);
expect(isErr(result)).toBe(true);
if (isErr(result)) {
expect(result.isErr()).toBe(true);
if (result.isErr()) {
expect(result.error.message).toBe('Async failure');
}
});
Expand Down
Loading
Loading