diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/DeleteAllLayersButton.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/DeleteAllLayersButton.tsx new file mode 100644 index 0000000000..59d30f8a07 --- /dev/null +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/DeleteAllLayersButton.tsx @@ -0,0 +1,15 @@ +import { Button } from '@invoke-ai/ui-library'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { allLayersDeleted } from 'features/regionalPrompts/store/regionalPromptsSlice'; +import { memo, useCallback } from 'react'; + +export const DeleteAllLayersButton = memo(() => { + const dispatch = useAppDispatch(); + const onClick = useCallback(() => { + dispatch(allLayersDeleted()); + }, [dispatch]); + + return ; +}); + +DeleteAllLayersButton.displayName = 'DeleteAllLayersButton'; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsEditor.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsEditor.tsx index 66fcd6aba5..59eabb0622 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsEditor.tsx +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/RegionalPromptsEditor.tsx @@ -4,10 +4,10 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { AddLayerButton } from 'features/regionalPrompts/components/AddLayerButton'; import { BrushSize } from 'features/regionalPrompts/components/BrushSize'; +import { DeleteAllLayersButton } from 'features/regionalPrompts/components/DeleteAllLayersButton'; import { StageComponent } from 'features/regionalPrompts/components/imperative/konvaApiDraft'; import { LayerListItem } from 'features/regionalPrompts/components/LayerListItem'; import { PromptLayerOpacity } from 'features/regionalPrompts/components/PromptLayerOpacity'; -import { RegionalPromptsStage } from 'features/regionalPrompts/components/RegionalPromptsStage'; import { ToolChooser } from 'features/regionalPrompts/components/ToolChooser'; import { selectRegionalPromptsSlice } from 'features/regionalPrompts/store/regionalPromptsSlice'; import { getRegionalPromptLayerBlobs } from 'features/regionalPrompts/util/getLayerBlobs'; @@ -27,8 +27,11 @@ export const RegionalPromptsEditor = memo(() => { return ( - - + + + + + diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/imperative/konvaApiDraft.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/imperative/konvaApiDraft.tsx index 761bef5805..fcfba724df 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/components/imperative/konvaApiDraft.tsx +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/imperative/konvaApiDraft.tsx @@ -113,7 +113,7 @@ const renderBrushPreview = ( }); }; -const renderLayers = ( +export const renderLayers = ( stage: Konva.Stage, reduxLayers: Layer[], selectedLayerId: string | null, @@ -369,7 +369,11 @@ export const StageComponent = () => { const container = useStore($container); return ( <> - + ); diff --git a/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts b/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts index 4cd5658095..106f982378 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts +++ b/invokeai/frontend/web/src/features/regionalPrompts/store/regionalPromptsSlice.ts @@ -159,6 +159,10 @@ export const regionalPromptsSlice = createSlice({ } layer.bbox = bbox; }, + allLayersDeleted: (state) => { + state.layers = []; + state.selectedLayer = null; + }, promptChanged: (state, action: PayloadAction<{ layerId: string; prompt: string }>) => { const { layerId, prompt } = action.payload; const layer = state.layers.find((l) => l.id === layerId); @@ -258,6 +262,7 @@ export const { layerTranslated, layerBboxChanged, promptLayerOpacityChanged, + allLayersDeleted, } = regionalPromptsSlice.actions; export const selectRegionalPromptsSlice = (state: RootState) => state.regionalPrompts; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/util/getLayerBlobs.ts b/invokeai/frontend/web/src/features/regionalPrompts/util/getLayerBlobs.ts index 5129c3c9d0..b67dd92302 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/util/getLayerBlobs.ts +++ b/invokeai/frontend/web/src/features/regionalPrompts/util/getLayerBlobs.ts @@ -1,7 +1,8 @@ +import { getStore } from 'app/store/nanostores/store'; import openBase64ImageInTab from 'common/util/openBase64ImageInTab'; import { blobToDataURL } from 'features/canvas/util/blobToDataURL'; -import { selectPromptLayerObjectGroup } from 'features/regionalPrompts/components/LayerComponent'; -import { getStage, REGIONAL_PROMPT_LAYER_NAME } from 'features/regionalPrompts/store/regionalPromptsSlice'; +import { renderLayers } from 'features/regionalPrompts/components/imperative/konvaApiDraft'; +import { REGIONAL_PROMPT_LAYER_NAME } from 'features/regionalPrompts/store/regionalPromptsSlice'; import Konva from 'konva'; import { assert } from 'tsafe'; @@ -15,40 +16,27 @@ export const getRegionalPromptLayerBlobs = async ( layerIds?: string[], preview: boolean = false ): Promise> => { - const stage = getStage(); - - // This automatically omits layers that are not rendered. Rendering is controlled by the layer's `isVisible` flag in redux. - const regionalPromptLayers = stage.getLayers().filter((l) => { - console.log(l.name(), l.id()); - const isRegionalPromptLayer = l.name() === REGIONAL_PROMPT_LAYER_NAME; - const isRequestedLayerId = layerIds ? layerIds.includes(l.id()) : true; - return isRegionalPromptLayer && isRequestedLayerId; - }); - - // We need to reconstruct each layer to only output the desired data. This logic mirrors the logic in - // `getKonvaLayerBbox()` in `invokeai/frontend/web/src/features/regionalPrompts/util/bbox.ts` - const offscreenStageContainer = document.createElement('div'); - const offscreenStage = new Konva.Stage({ - container: offscreenStageContainer, - width: stage.width(), - height: stage.height(), - }); + const state = getStore().getState(); + const container = document.createElement('div'); + const stage = new Konva.Stage({ container, width: state.generation.width, height: state.generation.height }); + renderLayers(stage, state.regionalPrompts.layers, state.regionalPrompts.selectedLayer); + const layers = stage.find(`.${REGIONAL_PROMPT_LAYER_NAME}`); const blobs: Record = {}; - for (const layer of regionalPromptLayers) { - const layerClone = layer.clone(); - for (const child of layerClone.getChildren()) { - if (selectPromptLayerObjectGroup(child)) { - child.destroy(); - } else { - // We need to re-cache to handle children with transparency and multiple objects - like prompt region layers. - child.cache(); - } + // First remove all layers + for (const layer of layers) { + layer.remove(); + } + + // Next render each layer to a blob + for (const layer of layers) { + if (layerIds && !layerIds.includes(layer.id())) { + continue; } - offscreenStage.add(layerClone); + stage.add(layer); const blob = await new Promise((resolve) => { - offscreenStage.toBlob({ + stage.toBlob({ callback: (blob) => { assert(blob, 'Blob is null'); resolve(blob); @@ -60,7 +48,7 @@ export const getRegionalPromptLayerBlobs = async ( const base64 = await blobToDataURL(blob); openBase64ImageInTab([{ base64, caption: layer.id() }]); } - layerClone.destroy(); + layer.remove(); blobs[layer.id()] = blob; }