diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts index 48e52a46aa..9e988b8bd4 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts @@ -69,7 +69,7 @@ export const addStagingListeners = (startAppListening: AppStartListening) => { if (!layer) { // We need to create a new layer to add the accepted image api.dispatch(layerAdded()); - layer = layers.entities[0]; + layer = api.getState().canvasV2.layers.entities[0]; } assert(layer, 'No layer found to stage image'); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index 277e6b2206..8ded74a06d 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -25,6 +25,9 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) assert(model, 'No model found in state'); const base = model.base; + manager.getInpaintMaskImage({ bbox: state.canvasV2.bbox, preview: true }); + manager.getImageSourceImage({ bbox: state.canvasV2.bbox, preview: true }); + if (base === 'sdxl') { graph = await buildSDXLGraph(state, manager); } else if (base === 'sd-1' || base === 'sd-2') { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index 0be12933a1..cb2e1070e1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -61,7 +61,6 @@ export const StageComponent = memo(({ asPreview = false }: Props) => { bottom={0} left={0} ref={containerRef} - tabIndex={-1} borderRadius="base" border={1} borderStyle="solid" diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts index 73d2202406..c61857fe98 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/nodeManager.ts @@ -88,12 +88,6 @@ type Util = { image_category: ImageCategory, is_intermediate: boolean ) => Promise; - getRegionMaskImage: (arg: { id: string; bbox?: Rect; preview?: boolean }) => Promise; - getInpaintMaskImage: (arg: { bbox?: Rect; preview?: boolean }) => Promise; - getImageSourceImage: (arg: { bbox?: Rect; preview?: boolean }) => Promise; - getMaskLayerClone: (arg: { id: string }) => Konva.Layer; - getCompositeLayerStageClone: () => Konva.Stage; - getGenerationMode: () => GenerationMode; }; const $nodeManager = atom(null); @@ -131,12 +125,6 @@ export class KonvaNodeManager { this.util = { getImageDTO, uploadImage, - getRegionMaskImage: this._getRegionMaskImage.bind(this), - getInpaintMaskImage: this._getInpaintMaskImage.bind(this), - getImageSourceImage: this._getImageSourceImage.bind(this), - getMaskLayerClone: this._getMaskLayerClone.bind(this), - getCompositeLayerStageClone: this._getCompositeLayerStageClone.bind(this), - getGenerationMode: this._getGenerationMode.bind(this), }; this.preview = new CanvasPreview( @@ -310,9 +298,7 @@ export class KonvaNodeManager { this.renderDocumentSizeOverlay(); } - _getMaskLayerClone(): Konva.Layer { - assert(this.inpaintMask, 'Inpaint mask layer has not been set'); - + getInpaintMaskLayerClone(): Konva.Layer { const layerClone = this.inpaintMask.konvaLayer.clone(); const objectGroupClone = this.inpaintMask.konvaObjectGroup.clone(); @@ -325,7 +311,25 @@ export class KonvaNodeManager { return layerClone; } - _getCompositeLayerStageClone(): Konva.Stage { + getRegionMaskLayerClone(arg: { id: string }): Konva.Layer { + const { id } = arg; + + const canvasRegion = this.regions.get(id); + assert(canvasRegion, `Canvas region with id ${id} not found`); + + const layerClone = canvasRegion.konvaLayer.clone(); + const objectGroupClone = canvasRegion.konvaObjectGroup.clone(); + + layerClone.destroyChildren(); + layerClone.add(objectGroupClone); + + objectGroupClone.opacity(1); + objectGroupClone.cache(); + + return layerClone; + } + + getCompositeLayerStageClone(): Konva.Stage { const layersState = this.stateApi.getLayersState(); const stageClone = this.stage.clone(); @@ -357,12 +361,12 @@ export class KonvaNodeManager { return stageClone; } - _getGenerationMode(): GenerationMode { + getGenerationMode(): GenerationMode { const { x, y, width, height } = this.stateApi.getBbox(); - const inpaintMaskLayer = this.util.getMaskLayerClone({ id: 'inpaint_mask' }); + const inpaintMaskLayer = this.getInpaintMaskLayerClone(); const inpaintMaskImageData = konvaNodeToImageData(inpaintMaskLayer, { x, y, width, height }); const inpaintMaskTransparency = getImageDataTransparency(inpaintMaskImageData); - const compositeLayer = this.util.getCompositeLayerStageClone(); + const compositeLayer = this.getCompositeLayerStageClone(); const compositeLayerImageData = konvaNodeToImageData(compositeLayer, { x, y, width, height }); const compositeLayerTransparency = getImageDataTransparency(compositeLayerImageData); if (compositeLayerTransparency.isPartiallyTransparent) { @@ -378,7 +382,7 @@ export class KonvaNodeManager { } } - async _getRegionMaskImage(arg: { id: string; bbox?: Rect; preview?: boolean }): Promise { + async getRegionMaskImage(arg: { id: string; bbox?: Rect; preview?: boolean }): Promise { const { id, bbox, preview = false } = arg; const region = this.stateApi.getRegionsState().entities.find((entity) => entity.id === id); assert(region, `Region entity state with id ${id} not found`); @@ -390,7 +394,7 @@ export class KonvaNodeManager { // } // } - const layerClone = this.util.getMaskLayerClone({ id }); + const layerClone = this.getRegionMaskLayerClone({ id }); const blob = await konvaNodeToBlob(layerClone, bbox); if (preview) { @@ -404,9 +408,9 @@ export class KonvaNodeManager { return imageDTO; } - async _getInpaintMaskImage(arg: { bbox?: Rect; preview?: boolean }): Promise { + async getInpaintMaskImage(arg: { bbox?: Rect; preview?: boolean }): Promise { const { bbox, preview = false } = arg; - const inpaintMask = this.stateApi.getInpaintMaskState(); + // const inpaintMask = this.stateApi.getInpaintMaskState(); // if (inpaintMask.imageCache) { // const imageDTO = await this.util.getImageDTO(inpaintMask.imageCache.name); @@ -415,7 +419,7 @@ export class KonvaNodeManager { // } // } - const layerClone = this.util.getMaskLayerClone({ id: inpaintMask.id }); + const layerClone = this.getInpaintMaskLayerClone(); const blob = await konvaNodeToBlob(layerClone, bbox); if (preview) { @@ -429,7 +433,7 @@ export class KonvaNodeManager { return imageDTO; } - async _getImageSourceImage(arg: { bbox?: Rect; preview?: boolean }): Promise { + async getImageSourceImage(arg: { bbox?: Rect; preview?: boolean }): Promise { const { bbox, preview = false } = arg; // const { imageCache } = this.stateApi.getLayersState(); @@ -440,7 +444,7 @@ export class KonvaNodeManager { // } // } - const stageClone = this.util.getCompositeLayerStageClone(); + const stageClone = this.getCompositeLayerStageClone(); const blob = await konvaNodeToBlob(stageClone, bbox); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts index 8663989804..93d56be444 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/inpaintMask.ts @@ -67,6 +67,7 @@ export class CanvasInpaintMask { // Destroy any objects that are no longer in state for (const object of this.objects.values()) { if (!objectIds.includes(object.id)) { + this.objects.delete(object.id); object.destroy(); groupNeedsCache = true; } @@ -80,7 +81,7 @@ export class CanvasInpaintMask { if (!brushLine) { brushLine = new KonvaBrushLine({ brushLine: obj }); this.objects.set(brushLine.id, brushLine); - this.konvaLayer.add(brushLine.konvaLineGroup); + this.konvaObjectGroup.add(brushLine.konvaLineGroup); groupNeedsCache = true; } @@ -95,7 +96,7 @@ export class CanvasInpaintMask { if (!eraserLine) { eraserLine = new KonvaEraserLine({ eraserLine: obj }); this.objects.set(eraserLine.id, eraserLine); - this.konvaLayer.add(eraserLine.konvaLineGroup); + this.konvaObjectGroup.add(eraserLine.konvaLineGroup); groupNeedsCache = true; } @@ -110,7 +111,7 @@ export class CanvasInpaintMask { if (!rect) { rect = new KonvaRect({ rectShape: obj }); this.objects.set(rect.id, rect); - this.konvaLayer.add(rect.konvaRect); + this.konvaObjectGroup.add(rect.konvaRect); groupNeedsCache = true; } } @@ -128,50 +129,72 @@ export class CanvasInpaintMask { return; } - const isSelected = selectedEntityIdentifier?.id === inpaintMaskState.id; - /** - * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows - * shapes to render as a "raster" layer with all pixels drawn at the same color and opacity. - * - * Without this special handling, each shape is drawn individually with the given opacity, atop the other shapes. The - * effect is like if you have a Photoshop Group consisting of many shapes, each of which has the given opacity. - * Overlapping shapes will have their colors blended together, and the final color is the result of all the shapes. - * - * Instead, with the special handling, the effect is as if you drew all the shapes at 100% opacity, flattened them to - * a single raster image, and _then_ applied the 50% opacity. - */ - if (isSelected && selectedTool !== 'move') { - // We must clear the cache first so Konva will re-draw the group with the new compositing rect - if (this.konvaObjectGroup.isCached()) { - this.konvaObjectGroup.clearCache(); - } - // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work - this.konvaObjectGroup.opacity(1); - - this.compositingRect.setAttrs({ - // The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already - ...(!inpaintMaskState.bboxNeedsUpdate && inpaintMaskState.bbox - ? inpaintMaskState.bbox - : getLayerBboxFast(this.konvaLayer)), - fill: rgbColor, - opacity: maskOpacity, - // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) - globalCompositeOperation: 'source-in', - visible: true, - // This rect must always be on top of all other shapes - zIndex: this.objects.size + 1, - }); - } else { - // The compositing rect should only be shown when the layer is selected. - this.compositingRect.visible(false); - // Cache only if needed - or if we are on this code path and _don't_ have a cache - if (groupNeedsCache || !this.konvaObjectGroup.isCached()) { - this.konvaObjectGroup.cache(); - } - // Updating group opacity does not require re-caching - this.konvaObjectGroup.opacity(maskOpacity); + // We must clear the cache first so Konva will re-draw the group with the new compositing rect + if (this.konvaObjectGroup.isCached()) { + this.konvaObjectGroup.clearCache(); } + // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work + this.konvaObjectGroup.opacity(1); + + this.compositingRect.setAttrs({ + // The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already + ...(!inpaintMaskState.bboxNeedsUpdate && inpaintMaskState.bbox + ? inpaintMaskState.bbox + : getLayerBboxFast(this.konvaLayer)), + fill: rgbColor, + opacity: maskOpacity, + // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) + globalCompositeOperation: 'source-in', + visible: true, + // This rect must always be on top of all other shapes + zIndex: this.objects.size + 1, + }); + + // const isSelected = selectedEntityIdentifier?.id === inpaintMaskState.id; + + // /** + // * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows + // * shapes to render as a "raster" layer with all pixels drawn at the same color and opacity. + // * + // * Without this special handling, each shape is drawn individually with the given opacity, atop the other shapes. The + // * effect is like if you have a Photoshop Group consisting of many shapes, each of which has the given opacity. + // * Overlapping shapes will have their colors blended together, and the final color is the result of all the shapes. + // * + // * Instead, with the special handling, the effect is as if you drew all the shapes at 100% opacity, flattened them to + // * a single raster image, and _then_ applied the 50% opacity. + // */ + // if (isSelected && selectedTool !== 'move') { + // // We must clear the cache first so Konva will re-draw the group with the new compositing rect + // if (this.konvaObjectGroup.isCached()) { + // this.konvaObjectGroup.clearCache(); + // } + // // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work + // this.konvaObjectGroup.opacity(1); + + // this.compositingRect.setAttrs({ + // // The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already + // ...(!inpaintMaskState.bboxNeedsUpdate && inpaintMaskState.bbox + // ? inpaintMaskState.bbox + // : getLayerBboxFast(this.konvaLayer)), + // fill: rgbColor, + // opacity: maskOpacity, + // // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) + // globalCompositeOperation: 'source-in', + // visible: true, + // // This rect must always be on top of all other shapes + // zIndex: this.objects.size + 1, + // }); + // } else { + // // The compositing rect should only be shown when the layer is selected. + // this.compositingRect.visible(false); + // // Cache only if needed - or if we are on this code path and _don't_ have a cache + // if (groupNeedsCache || !this.konvaObjectGroup.isCached()) { + // this.konvaObjectGroup.cache(); + // } + // // Updating group opacity does not require re-caching + // this.konvaObjectGroup.opacity(maskOpacity); + // } // const bboxRect = // regionMap.konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rg, regionMap.konvaLayer); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts index ad138a66ab..c216f978b2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/layers.ts @@ -52,6 +52,7 @@ export class CanvasLayer { // Destroy any objects that are no longer in state for (const object of this.objects.values()) { if (!objectIds.includes(object.id)) { + this.objects.delete(object.id); object.destroy(); } } @@ -64,7 +65,7 @@ export class CanvasLayer { if (!brushLine) { brushLine = new KonvaBrushLine({ brushLine: obj }); this.objects.set(brushLine.id, brushLine); - this.konvaLayer.add(brushLine.konvaLineGroup); + this.konvaObjectGroup.add(brushLine.konvaLineGroup); } if (obj.points.length !== brushLine.konvaLine.points().length) { brushLine.konvaLine.points(obj.points); @@ -76,7 +77,7 @@ export class CanvasLayer { if (!eraserLine) { eraserLine = new KonvaEraserLine({ eraserLine: obj }); this.objects.set(eraserLine.id, eraserLine); - this.konvaLayer.add(eraserLine.konvaLineGroup); + this.konvaObjectGroup.add(eraserLine.konvaLineGroup); } if (obj.points.length !== eraserLine.konvaLine.points().length) { eraserLine.konvaLine.points(obj.points); @@ -88,7 +89,7 @@ export class CanvasLayer { if (!rect) { rect = new KonvaRect({ rectShape: obj }); this.objects.set(rect.id, rect); - this.konvaLayer.add(rect.konvaRect); + this.konvaObjectGroup.add(rect.konvaRect); } } else if (obj.type === 'image') { let image = this.objects.get(obj.id); @@ -97,7 +98,7 @@ export class CanvasLayer { if (!image) { image = await new KonvaImage({ imageObject: obj }); this.objects.set(image.id, image); - this.konvaLayer.add(image.konvaImageGroup); + this.konvaObjectGroup.add(image.konvaImageGroup); } if (image.imageName !== obj.image.name) { image.updateImageSource(obj.image.name); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts index 8a6d81a35f..5a0c90b805 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/objects.ts @@ -1,7 +1,7 @@ import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { getLayerBboxId, LAYER_BBOX_NAME } from 'features/controlLayers/konva/naming'; import type { BrushLine, CanvasEntity, EraserLine, ImageObject, RectShape } from 'features/controlLayers/store/types'; -import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types'; +import { RGBA_RED } from 'features/controlLayers/store/types'; import { t } from 'i18next'; import Konva from 'konva'; import { getImageDTO as defaultGetImageDTO } from 'services/api/endpoints/images'; @@ -77,7 +77,7 @@ export class KonvaEraserLine { lineCap: 'round', lineJoin: 'round', globalCompositeOperation: 'destination-out', - stroke: rgbaColorToString(DEFAULT_RGBA_COLOR), + stroke: rgbaColorToString(RGBA_RED), }); this.konvaLineGroup.add(this.konvaLine); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts index c2ada34fc4..5df04d4d6c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/preview.ts @@ -215,6 +215,7 @@ export class CanvasTool { strokeEnabled: false, }), }; + this.rect.group.add(this.rect.fillRect); this.group.add(this.rect.group); } diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts index c715ae3781..cc0862ecd2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/regions.ts @@ -67,6 +67,7 @@ export class CanvasRegion { // Destroy any objects that are no longer in state for (const object of this.objects.values()) { if (!objectIds.includes(object.id)) { + this.objects.delete(object.id); object.destroy(); groupNeedsCache = true; } @@ -80,7 +81,7 @@ export class CanvasRegion { if (!brushLine) { brushLine = new KonvaBrushLine({ brushLine: obj }); this.objects.set(brushLine.id, brushLine); - this.konvaLayer.add(brushLine.konvaLineGroup); + this.konvaObjectGroup.add(brushLine.konvaLineGroup); groupNeedsCache = true; } @@ -95,7 +96,7 @@ export class CanvasRegion { if (!eraserLine) { eraserLine = new KonvaEraserLine({ eraserLine: obj }); this.objects.set(eraserLine.id, eraserLine); - this.konvaLayer.add(eraserLine.konvaLineGroup); + this.konvaObjectGroup.add(eraserLine.konvaLineGroup); groupNeedsCache = true; } @@ -110,7 +111,7 @@ export class CanvasRegion { if (!rect) { rect = new KonvaRect({ rectShape: obj }); this.objects.set(rect.id, rect); - this.konvaLayer.add(rect.konvaRect); + this.konvaObjectGroup.add(rect.konvaRect); groupNeedsCache = true; } } @@ -128,48 +129,67 @@ export class CanvasRegion { return; } - const isSelected = selectedEntityIdentifier?.id === regionState.id; - - /** - * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows - * shapes to render as a "raster" layer with all pixels drawn at the same color and opacity. - * - * Without this special handling, each shape is drawn individually with the given opacity, atop the other shapes. The - * effect is like if you have a Photoshop Group consisting of many shapes, each of which has the given opacity. - * Overlapping shapes will have their colors blended together, and the final color is the result of all the shapes. - * - * Instead, with the special handling, the effect is as if you drew all the shapes at 100% opacity, flattened them to - * a single raster image, and _then_ applied the 50% opacity. - */ - if (isSelected && selectedTool !== 'move') { - // We must clear the cache first so Konva will re-draw the group with the new compositing rect - if (this.konvaObjectGroup.isCached()) { - this.konvaObjectGroup.clearCache(); - } - // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work - this.konvaObjectGroup.opacity(1); - - this.compositingRect.setAttrs({ - // The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already - ...(!regionState.bboxNeedsUpdate && regionState.bbox ? regionState.bbox : getLayerBboxFast(this.konvaLayer)), - fill: rgbColor, - opacity: maskOpacity, - // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) - globalCompositeOperation: 'source-in', - visible: true, - // This rect must always be on top of all other shapes - zIndex: this.objects.size + 1, - }); - } else { - // The compositing rect should only be shown when the layer is selected. - this.compositingRect.visible(false); - // Cache only if needed - or if we are on this code path and _don't_ have a cache - if (groupNeedsCache || !this.konvaObjectGroup.isCached()) { - this.konvaObjectGroup.cache(); - } - // Updating group opacity does not require re-caching - this.konvaObjectGroup.opacity(maskOpacity); + // We must clear the cache first so Konva will re-draw the group with the new compositing rect + if (this.konvaObjectGroup.isCached()) { + this.konvaObjectGroup.clearCache(); } + // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work + this.konvaObjectGroup.opacity(1); + + this.compositingRect.setAttrs({ + // The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already + ...(!regionState.bboxNeedsUpdate && regionState.bbox ? regionState.bbox : getLayerBboxFast(this.konvaLayer)), + fill: rgbColor, + opacity: maskOpacity, + // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) + globalCompositeOperation: 'source-in', + visible: true, + // This rect must always be on top of all other shapes + zIndex: this.objects.size + 1, + }); + + // const isSelected = selectedEntityIdentifier?.id === regionState.id; + + // /** + // * When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows + // * shapes to render as a "raster" layer with all pixels drawn at the same color and opacity. + // * + // * Without this special handling, each shape is drawn individually with the given opacity, atop the other shapes. The + // * effect is like if you have a Photoshop Group consisting of many shapes, each of which has the given opacity. + // * Overlapping shapes will have their colors blended together, and the final color is the result of all the shapes. + // * + // * Instead, with the special handling, the effect is as if you drew all the shapes at 100% opacity, flattened them to + // * a single raster image, and _then_ applied the 50% opacity. + // */ + // if (isSelected && selectedTool !== 'move') { + // // We must clear the cache first so Konva will re-draw the group with the new compositing rect + // if (this.konvaObjectGroup.isCached()) { + // this.konvaObjectGroup.clearCache(); + // } + // // The user is allowed to reduce mask opacity to 0, but we need the opacity for the compositing rect to work + // this.konvaObjectGroup.opacity(1); + + // this.compositingRect.setAttrs({ + // // The rect should be the size of the layer - use the fast method if we don't have a pixel-perfect bbox already + // ...(!regionState.bboxNeedsUpdate && regionState.bbox ? regionState.bbox : getLayerBboxFast(this.konvaLayer)), + // fill: rgbColor, + // opacity: maskOpacity, + // // Draw this rect only where there are non-transparent pixels under it (e.g. the mask shapes) + // globalCompositeOperation: 'source-in', + // visible: true, + // // This rect must always be on top of all other shapes + // zIndex: this.objects.size + 1, + // }); + // } else { + // // The compositing rect should only be shown when the layer is selected. + // this.compositingRect.visible(false); + // // Cache only if needed - or if we are on this code path and _don't_ have a cache + // if (groupNeedsCache || !this.konvaObjectGroup.isCached()) { + // this.konvaObjectGroup.cache(); + // } + // // Updating group opacity does not require re-caching + // this.konvaObjectGroup.opacity(maskOpacity); + // } // const bboxRect = // regionMap.konvaLayer.findOne(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rg, regionMap.konvaLayer); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts index 1551752ef2..eb0f83fc73 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/renderers/renderer.ts @@ -18,6 +18,7 @@ import { imEraserLineAdded, imImageCacheChanged, imLinePointAdded, + imRectAdded, imTranslated, layerBboxChanged, layerBrushLineAdded, @@ -146,6 +147,8 @@ export const initializeRenderer = ( dispatch(layerRectAdded(arg)); } else if (entityType === 'regional_guidance') { dispatch(rgRectAdded(arg)); + } else if (entityType === 'inpaint_mask') { + dispatch(imRectAdded(arg)); } }; const onBboxTransformed = (bbox: IRect) => { diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 8c579c1cb4..f0ac34622f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -20,7 +20,7 @@ import type { AspectRatioState } from 'features/parameters/components/ImageSize/ import { atom } from 'nanostores'; import type { CanvasEntityIdentifier, CanvasV2State, StageAttrs } from './types'; -import { DEFAULT_RGBA_COLOR } from './types'; +import { RGBA_RED } from './types'; const initialState: CanvasV2State = { _version: 3, @@ -35,7 +35,7 @@ const initialState: CanvasV2State = { type: 'inpaint_mask', bbox: null, bboxNeedsUpdate: false, - fill: DEFAULT_RGBA_COLOR, + fill: RGBA_RED, imageCache: null, isEnabled: true, objects: [], @@ -46,7 +46,7 @@ const initialState: CanvasV2State = { selected: 'bbox', selectedBuffer: null, invertScroll: false, - fill: DEFAULT_RGBA_COLOR, + fill: RGBA_RED, brush: { width: 50, }, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts index 881028b0d6..1b7a9346ee 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/inpaintMaskReducers.ts @@ -1,7 +1,7 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; import type { CanvasV2State, InpaintMaskEntity } from 'features/controlLayers/store/types'; -import { imageDTOToImageWithDims } from 'features/controlLayers/store/types'; +import { imageDTOToImageWithDims,RGBA_RED } from 'features/controlLayers/store/types'; import type { IRect } from 'konva/lib/types'; import type { ImageDTO } from 'services/api/types'; import { v4 as uuidv4 } from 'uuid'; @@ -44,13 +44,13 @@ export const inpaintMaskReducers = { }, imBrushLineAdded: { reducer: (state, action: PayloadAction & { lineId: string }>) => { - const { points, lineId, color, width, clip } = action.payload; + const { points, lineId, width, clip } = action.payload; state.inpaintMask.objects.push({ id: getBrushLineId(state.inpaintMask.id, lineId), type: 'brush_line', points, strokeWidth: width, - color, + color: RGBA_RED, clip, }); state.inpaintMask.bboxNeedsUpdate = true; @@ -89,7 +89,7 @@ export const inpaintMaskReducers = { }, imRectAdded: { reducer: (state, action: PayloadAction & { rectId: string }>) => { - const { rect, rectId, color } = action.payload; + const { rect, rectId } = action.payload; if (rect.height === 0 || rect.width === 0) { // Ignore zero-area rectangles return; @@ -98,7 +98,7 @@ export const inpaintMaskReducers = { type: 'rect_shape', id: getRectShapeId(state.inpaintMask.id, rectId), ...rect, - color, + color: RGBA_RED, }); state.inpaintMask.bboxNeedsUpdate = true; state.inpaintMask.imageCache = null; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts index 54e73bc582..59ad0d5314 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/regionsReducers.ts @@ -2,7 +2,7 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; import type { CanvasV2State, CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; -import { DEFAULT_RGBA_COLOR, imageDTOToImageObject, imageDTOToImageWithDims } from 'features/controlLayers/store/types'; +import { imageDTOToImageObject, imageDTOToImageWithDims,RGBA_RED } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; import type { IRect } from 'konva/lib/types'; @@ -319,7 +319,7 @@ export const regionsReducers = { type: 'brush_line', points, strokeWidth: width, - color: DEFAULT_RGBA_COLOR, + color: RGBA_RED, clip, }); rg.bboxNeedsUpdate = true; @@ -379,7 +379,7 @@ export const regionsReducers = { type: 'rect_shape', id: getRectShapeId(id, rectId), ...rect, - color: DEFAULT_RGBA_COLOR, + color: RGBA_RED, }); rg.bboxNeedsUpdate = true; rg.imageCache = null; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 4f27d72186..ea55c6b581 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -495,7 +495,7 @@ const zRgbaColor = zRgbColor.extend({ a: z.number().min(0).max(1), }); export type RgbaColor = z.infer; -export const DEFAULT_RGBA_COLOR: RgbaColor = { r: 255, g: 255, b: 255, a: 1 }; +export const RGBA_RED: RgbaColor = { r: 255, g: 0, b: 0, a: 1 }; const zOpacity = z.number().gte(0).lte(1); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts index 32e36a5c82..59f2cd9a6a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addImageToImage.ts @@ -18,7 +18,7 @@ export const addImageToImage = async ( denoise.denoising_start = denoising_start; const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); - const initialImage = await manager.util.getImageSourceImage({ + const initialImage = await manager.getImageSourceImage({ bbox: cropBbox, preview: true, }); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts index 41e18071b5..9936166ec6 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addInpaint.ts @@ -22,11 +22,11 @@ export const addInpaint = async ( denoise.denoising_start = denoising_start; const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); - const initialImage = await manager.util.getImageSourceImage({ + const initialImage = await manager.getImageSourceImage({ bbox: cropBbox, preview: true, }); - const maskImage = await manager.util.getInpaintMaskImage({ + const maskImage = await manager.getInpaintMaskImage({ bbox: cropBbox, preview: true, }); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts index 29102cc2e3..aede59db13 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addOutpaint.ts @@ -21,11 +21,11 @@ export const addOutpaint = async ( vaePrecision: ParameterPrecision ): Promise> => { const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); - const initialImage = await manager.util.getImageSourceImage({ + const initialImage = await manager.getImageSourceImage({ bbox: cropBbox, preview: true, }); - const maskImage = await manager.util.getInpaintMaskImage({ + const maskImage = await manager.getInpaintMaskImage({ bbox: cropBbox, preview: true, }); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts index c7a8a3a002..3b85f15bfd 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/addRegions.ts @@ -44,7 +44,7 @@ export const addRegions = async ( for (const region of validRegions) { // Upload the mask image, or get the cached image if it exists - const { image_name } = await manager.util.getRegionMaskImage({ id: region.id, bbox, preview: true }); + const { image_name } = await manager.getRegionMaskImage({ id: region.id, bbox, preview: true }); // The main mask-to-tensor node const maskToTensor = g.addNode({ diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts index a954938c15..f55c8ad6b4 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts @@ -36,7 +36,7 @@ import { assert } from 'tsafe'; import { addRegions } from './addRegions'; export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager): Promise => { - const generationMode = manager.util.getGenerationMode(); + const generationMode = manager.getGenerationMode(); const { bbox, params } = state.canvasV2; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts index 3d780a2f3d..d75044c736 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts @@ -34,7 +34,7 @@ import { assert } from 'tsafe'; import { addRegions } from './addRegions'; export const buildSDXLGraph = async (state: RootState, manager: KonvaNodeManager): Promise => { - const generationMode = manager.util.getGenerationMode(); + const generationMode = manager.getGenerationMode(); const { bbox, params } = state.canvasV2;