feat(ui): staging area works more better

This commit is contained in:
psychedelicious 2024-06-26 19:53:15 +10:00
parent 7824cb7a1a
commit 1806aa187b
20 changed files with 197 additions and 143 deletions

View File

@ -69,7 +69,7 @@ export const addStagingListeners = (startAppListening: AppStartListening) => {
if (!layer) { if (!layer) {
// We need to create a new layer to add the accepted image // We need to create a new layer to add the accepted image
api.dispatch(layerAdded()); api.dispatch(layerAdded());
layer = layers.entities[0]; layer = api.getState().canvasV2.layers.entities[0];
} }
assert(layer, 'No layer found to stage image'); assert(layer, 'No layer found to stage image');

View File

@ -25,6 +25,9 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
assert(model, 'No model found in state'); assert(model, 'No model found in state');
const base = model.base; const base = model.base;
manager.getInpaintMaskImage({ bbox: state.canvasV2.bbox, preview: true });
manager.getImageSourceImage({ bbox: state.canvasV2.bbox, preview: true });
if (base === 'sdxl') { if (base === 'sdxl') {
graph = await buildSDXLGraph(state, manager); graph = await buildSDXLGraph(state, manager);
} else if (base === 'sd-1' || base === 'sd-2') { } else if (base === 'sd-1' || base === 'sd-2') {

View File

@ -61,7 +61,6 @@ export const StageComponent = memo(({ asPreview = false }: Props) => {
bottom={0} bottom={0}
left={0} left={0}
ref={containerRef} ref={containerRef}
tabIndex={-1}
borderRadius="base" borderRadius="base"
border={1} border={1}
borderStyle="solid" borderStyle="solid"

View File

@ -88,12 +88,6 @@ type Util = {
image_category: ImageCategory, image_category: ImageCategory,
is_intermediate: boolean is_intermediate: boolean
) => Promise<ImageDTO>; ) => Promise<ImageDTO>;
getRegionMaskImage: (arg: { id: string; bbox?: Rect; preview?: boolean }) => Promise<ImageDTO>;
getInpaintMaskImage: (arg: { bbox?: Rect; preview?: boolean }) => Promise<ImageDTO>;
getImageSourceImage: (arg: { bbox?: Rect; preview?: boolean }) => Promise<ImageDTO>;
getMaskLayerClone: (arg: { id: string }) => Konva.Layer;
getCompositeLayerStageClone: () => Konva.Stage;
getGenerationMode: () => GenerationMode;
}; };
const $nodeManager = atom<KonvaNodeManager | null>(null); const $nodeManager = atom<KonvaNodeManager | null>(null);
@ -131,12 +125,6 @@ export class KonvaNodeManager {
this.util = { this.util = {
getImageDTO, getImageDTO,
uploadImage, 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( this.preview = new CanvasPreview(
@ -310,9 +298,7 @@ export class KonvaNodeManager {
this.renderDocumentSizeOverlay(); this.renderDocumentSizeOverlay();
} }
_getMaskLayerClone(): Konva.Layer { getInpaintMaskLayerClone(): Konva.Layer {
assert(this.inpaintMask, 'Inpaint mask layer has not been set');
const layerClone = this.inpaintMask.konvaLayer.clone(); const layerClone = this.inpaintMask.konvaLayer.clone();
const objectGroupClone = this.inpaintMask.konvaObjectGroup.clone(); const objectGroupClone = this.inpaintMask.konvaObjectGroup.clone();
@ -325,7 +311,25 @@ export class KonvaNodeManager {
return layerClone; 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 layersState = this.stateApi.getLayersState();
const stageClone = this.stage.clone(); const stageClone = this.stage.clone();
@ -357,12 +361,12 @@ export class KonvaNodeManager {
return stageClone; return stageClone;
} }
_getGenerationMode(): GenerationMode { getGenerationMode(): GenerationMode {
const { x, y, width, height } = this.stateApi.getBbox(); 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 inpaintMaskImageData = konvaNodeToImageData(inpaintMaskLayer, { x, y, width, height });
const inpaintMaskTransparency = getImageDataTransparency(inpaintMaskImageData); const inpaintMaskTransparency = getImageDataTransparency(inpaintMaskImageData);
const compositeLayer = this.util.getCompositeLayerStageClone(); const compositeLayer = this.getCompositeLayerStageClone();
const compositeLayerImageData = konvaNodeToImageData(compositeLayer, { x, y, width, height }); const compositeLayerImageData = konvaNodeToImageData(compositeLayer, { x, y, width, height });
const compositeLayerTransparency = getImageDataTransparency(compositeLayerImageData); const compositeLayerTransparency = getImageDataTransparency(compositeLayerImageData);
if (compositeLayerTransparency.isPartiallyTransparent) { if (compositeLayerTransparency.isPartiallyTransparent) {
@ -378,7 +382,7 @@ export class KonvaNodeManager {
} }
} }
async _getRegionMaskImage(arg: { id: string; bbox?: Rect; preview?: boolean }): Promise<ImageDTO> { async getRegionMaskImage(arg: { id: string; bbox?: Rect; preview?: boolean }): Promise<ImageDTO> {
const { id, bbox, preview = false } = arg; const { id, bbox, preview = false } = arg;
const region = this.stateApi.getRegionsState().entities.find((entity) => entity.id === id); const region = this.stateApi.getRegionsState().entities.find((entity) => entity.id === id);
assert(region, `Region entity state with id ${id} not found`); 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); const blob = await konvaNodeToBlob(layerClone, bbox);
if (preview) { if (preview) {
@ -404,9 +408,9 @@ export class KonvaNodeManager {
return imageDTO; return imageDTO;
} }
async _getInpaintMaskImage(arg: { bbox?: Rect; preview?: boolean }): Promise<ImageDTO> { async getInpaintMaskImage(arg: { bbox?: Rect; preview?: boolean }): Promise<ImageDTO> {
const { bbox, preview = false } = arg; const { bbox, preview = false } = arg;
const inpaintMask = this.stateApi.getInpaintMaskState(); // const inpaintMask = this.stateApi.getInpaintMaskState();
// if (inpaintMask.imageCache) { // if (inpaintMask.imageCache) {
// const imageDTO = await this.util.getImageDTO(inpaintMask.imageCache.name); // 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); const blob = await konvaNodeToBlob(layerClone, bbox);
if (preview) { if (preview) {
@ -429,7 +433,7 @@ export class KonvaNodeManager {
return imageDTO; return imageDTO;
} }
async _getImageSourceImage(arg: { bbox?: Rect; preview?: boolean }): Promise<ImageDTO> { async getImageSourceImage(arg: { bbox?: Rect; preview?: boolean }): Promise<ImageDTO> {
const { bbox, preview = false } = arg; const { bbox, preview = false } = arg;
// const { imageCache } = this.stateApi.getLayersState(); // 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); const blob = await konvaNodeToBlob(stageClone, bbox);

View File

@ -67,6 +67,7 @@ export class CanvasInpaintMask {
// Destroy any objects that are no longer in state // Destroy any objects that are no longer in state
for (const object of this.objects.values()) { for (const object of this.objects.values()) {
if (!objectIds.includes(object.id)) { if (!objectIds.includes(object.id)) {
this.objects.delete(object.id);
object.destroy(); object.destroy();
groupNeedsCache = true; groupNeedsCache = true;
} }
@ -80,7 +81,7 @@ export class CanvasInpaintMask {
if (!brushLine) { if (!brushLine) {
brushLine = new KonvaBrushLine({ brushLine: obj }); brushLine = new KonvaBrushLine({ brushLine: obj });
this.objects.set(brushLine.id, brushLine); this.objects.set(brushLine.id, brushLine);
this.konvaLayer.add(brushLine.konvaLineGroup); this.konvaObjectGroup.add(brushLine.konvaLineGroup);
groupNeedsCache = true; groupNeedsCache = true;
} }
@ -95,7 +96,7 @@ export class CanvasInpaintMask {
if (!eraserLine) { if (!eraserLine) {
eraserLine = new KonvaEraserLine({ eraserLine: obj }); eraserLine = new KonvaEraserLine({ eraserLine: obj });
this.objects.set(eraserLine.id, eraserLine); this.objects.set(eraserLine.id, eraserLine);
this.konvaLayer.add(eraserLine.konvaLineGroup); this.konvaObjectGroup.add(eraserLine.konvaLineGroup);
groupNeedsCache = true; groupNeedsCache = true;
} }
@ -110,7 +111,7 @@ export class CanvasInpaintMask {
if (!rect) { if (!rect) {
rect = new KonvaRect({ rectShape: obj }); rect = new KonvaRect({ rectShape: obj });
this.objects.set(rect.id, rect); this.objects.set(rect.id, rect);
this.konvaLayer.add(rect.konvaRect); this.konvaObjectGroup.add(rect.konvaRect);
groupNeedsCache = true; groupNeedsCache = true;
} }
} }
@ -128,50 +129,72 @@ export class CanvasInpaintMask {
return; return;
} }
const isSelected = selectedEntityIdentifier?.id === inpaintMaskState.id;
/** // We must clear the cache first so Konva will re-draw the group with the new compositing rect
* When the group is selected, we use a rect of the selected preview color, composited over the shapes. This allows if (this.konvaObjectGroup.isCached()) {
* shapes to render as a "raster" layer with all pixels drawn at the same color and opacity. this.konvaObjectGroup.clearCache();
*
* 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);
} }
// 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 = // const bboxRect =
// regionMap.konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rg, regionMap.konvaLayer); // regionMap.konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rg, regionMap.konvaLayer);

View File

@ -52,6 +52,7 @@ export class CanvasLayer {
// Destroy any objects that are no longer in state // Destroy any objects that are no longer in state
for (const object of this.objects.values()) { for (const object of this.objects.values()) {
if (!objectIds.includes(object.id)) { if (!objectIds.includes(object.id)) {
this.objects.delete(object.id);
object.destroy(); object.destroy();
} }
} }
@ -64,7 +65,7 @@ export class CanvasLayer {
if (!brushLine) { if (!brushLine) {
brushLine = new KonvaBrushLine({ brushLine: obj }); brushLine = new KonvaBrushLine({ brushLine: obj });
this.objects.set(brushLine.id, brushLine); this.objects.set(brushLine.id, brushLine);
this.konvaLayer.add(brushLine.konvaLineGroup); this.konvaObjectGroup.add(brushLine.konvaLineGroup);
} }
if (obj.points.length !== brushLine.konvaLine.points().length) { if (obj.points.length !== brushLine.konvaLine.points().length) {
brushLine.konvaLine.points(obj.points); brushLine.konvaLine.points(obj.points);
@ -76,7 +77,7 @@ export class CanvasLayer {
if (!eraserLine) { if (!eraserLine) {
eraserLine = new KonvaEraserLine({ eraserLine: obj }); eraserLine = new KonvaEraserLine({ eraserLine: obj });
this.objects.set(eraserLine.id, eraserLine); this.objects.set(eraserLine.id, eraserLine);
this.konvaLayer.add(eraserLine.konvaLineGroup); this.konvaObjectGroup.add(eraserLine.konvaLineGroup);
} }
if (obj.points.length !== eraserLine.konvaLine.points().length) { if (obj.points.length !== eraserLine.konvaLine.points().length) {
eraserLine.konvaLine.points(obj.points); eraserLine.konvaLine.points(obj.points);
@ -88,7 +89,7 @@ export class CanvasLayer {
if (!rect) { if (!rect) {
rect = new KonvaRect({ rectShape: obj }); rect = new KonvaRect({ rectShape: obj });
this.objects.set(rect.id, rect); this.objects.set(rect.id, rect);
this.konvaLayer.add(rect.konvaRect); this.konvaObjectGroup.add(rect.konvaRect);
} }
} else if (obj.type === 'image') { } else if (obj.type === 'image') {
let image = this.objects.get(obj.id); let image = this.objects.get(obj.id);
@ -97,7 +98,7 @@ export class CanvasLayer {
if (!image) { if (!image) {
image = await new KonvaImage({ imageObject: obj }); image = await new KonvaImage({ imageObject: obj });
this.objects.set(image.id, image); this.objects.set(image.id, image);
this.konvaLayer.add(image.konvaImageGroup); this.konvaObjectGroup.add(image.konvaImageGroup);
} }
if (image.imageName !== obj.image.name) { if (image.imageName !== obj.image.name) {
image.updateImageSource(obj.image.name); image.updateImageSource(obj.image.name);

View File

@ -1,7 +1,7 @@
import { rgbaColorToString } from 'common/util/colorCodeTransformers'; import { rgbaColorToString } from 'common/util/colorCodeTransformers';
import { getLayerBboxId, LAYER_BBOX_NAME } from 'features/controlLayers/konva/naming'; import { getLayerBboxId, LAYER_BBOX_NAME } from 'features/controlLayers/konva/naming';
import type { BrushLine, CanvasEntity, EraserLine, ImageObject, RectShape } from 'features/controlLayers/store/types'; 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 { t } from 'i18next';
import Konva from 'konva'; import Konva from 'konva';
import { getImageDTO as defaultGetImageDTO } from 'services/api/endpoints/images'; import { getImageDTO as defaultGetImageDTO } from 'services/api/endpoints/images';
@ -77,7 +77,7 @@ export class KonvaEraserLine {
lineCap: 'round', lineCap: 'round',
lineJoin: 'round', lineJoin: 'round',
globalCompositeOperation: 'destination-out', globalCompositeOperation: 'destination-out',
stroke: rgbaColorToString(DEFAULT_RGBA_COLOR), stroke: rgbaColorToString(RGBA_RED),
}); });
this.konvaLineGroup.add(this.konvaLine); this.konvaLineGroup.add(this.konvaLine);
} }

View File

@ -215,6 +215,7 @@ export class CanvasTool {
strokeEnabled: false, strokeEnabled: false,
}), }),
}; };
this.rect.group.add(this.rect.fillRect);
this.group.add(this.rect.group); this.group.add(this.rect.group);
} }

View File

@ -67,6 +67,7 @@ export class CanvasRegion {
// Destroy any objects that are no longer in state // Destroy any objects that are no longer in state
for (const object of this.objects.values()) { for (const object of this.objects.values()) {
if (!objectIds.includes(object.id)) { if (!objectIds.includes(object.id)) {
this.objects.delete(object.id);
object.destroy(); object.destroy();
groupNeedsCache = true; groupNeedsCache = true;
} }
@ -80,7 +81,7 @@ export class CanvasRegion {
if (!brushLine) { if (!brushLine) {
brushLine = new KonvaBrushLine({ brushLine: obj }); brushLine = new KonvaBrushLine({ brushLine: obj });
this.objects.set(brushLine.id, brushLine); this.objects.set(brushLine.id, brushLine);
this.konvaLayer.add(brushLine.konvaLineGroup); this.konvaObjectGroup.add(brushLine.konvaLineGroup);
groupNeedsCache = true; groupNeedsCache = true;
} }
@ -95,7 +96,7 @@ export class CanvasRegion {
if (!eraserLine) { if (!eraserLine) {
eraserLine = new KonvaEraserLine({ eraserLine: obj }); eraserLine = new KonvaEraserLine({ eraserLine: obj });
this.objects.set(eraserLine.id, eraserLine); this.objects.set(eraserLine.id, eraserLine);
this.konvaLayer.add(eraserLine.konvaLineGroup); this.konvaObjectGroup.add(eraserLine.konvaLineGroup);
groupNeedsCache = true; groupNeedsCache = true;
} }
@ -110,7 +111,7 @@ export class CanvasRegion {
if (!rect) { if (!rect) {
rect = new KonvaRect({ rectShape: obj }); rect = new KonvaRect({ rectShape: obj });
this.objects.set(rect.id, rect); this.objects.set(rect.id, rect);
this.konvaLayer.add(rect.konvaRect); this.konvaObjectGroup.add(rect.konvaRect);
groupNeedsCache = true; groupNeedsCache = true;
} }
} }
@ -128,48 +129,67 @@ export class CanvasRegion {
return; return;
} }
const isSelected = selectedEntityIdentifier?.id === regionState.id; // 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();
* 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);
} }
// 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 = // const bboxRect =
// regionMap.konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rg, regionMap.konvaLayer); // regionMap.konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rg, regionMap.konvaLayer);

View File

@ -18,6 +18,7 @@ import {
imEraserLineAdded, imEraserLineAdded,
imImageCacheChanged, imImageCacheChanged,
imLinePointAdded, imLinePointAdded,
imRectAdded,
imTranslated, imTranslated,
layerBboxChanged, layerBboxChanged,
layerBrushLineAdded, layerBrushLineAdded,
@ -146,6 +147,8 @@ export const initializeRenderer = (
dispatch(layerRectAdded(arg)); dispatch(layerRectAdded(arg));
} else if (entityType === 'regional_guidance') { } else if (entityType === 'regional_guidance') {
dispatch(rgRectAdded(arg)); dispatch(rgRectAdded(arg));
} else if (entityType === 'inpaint_mask') {
dispatch(imRectAdded(arg));
} }
}; };
const onBboxTransformed = (bbox: IRect) => { const onBboxTransformed = (bbox: IRect) => {

View File

@ -20,7 +20,7 @@ import type { AspectRatioState } from 'features/parameters/components/ImageSize/
import { atom } from 'nanostores'; import { atom } from 'nanostores';
import type { CanvasEntityIdentifier, CanvasV2State, StageAttrs } from './types'; import type { CanvasEntityIdentifier, CanvasV2State, StageAttrs } from './types';
import { DEFAULT_RGBA_COLOR } from './types'; import { RGBA_RED } from './types';
const initialState: CanvasV2State = { const initialState: CanvasV2State = {
_version: 3, _version: 3,
@ -35,7 +35,7 @@ const initialState: CanvasV2State = {
type: 'inpaint_mask', type: 'inpaint_mask',
bbox: null, bbox: null,
bboxNeedsUpdate: false, bboxNeedsUpdate: false,
fill: DEFAULT_RGBA_COLOR, fill: RGBA_RED,
imageCache: null, imageCache: null,
isEnabled: true, isEnabled: true,
objects: [], objects: [],
@ -46,7 +46,7 @@ const initialState: CanvasV2State = {
selected: 'bbox', selected: 'bbox',
selectedBuffer: null, selectedBuffer: null,
invertScroll: false, invertScroll: false,
fill: DEFAULT_RGBA_COLOR, fill: RGBA_RED,
brush: { brush: {
width: 50, width: 50,
}, },

View File

@ -1,7 +1,7 @@
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming';
import type { CanvasV2State, InpaintMaskEntity } from 'features/controlLayers/store/types'; 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 { IRect } from 'konva/lib/types';
import type { ImageDTO } from 'services/api/types'; import type { ImageDTO } from 'services/api/types';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@ -44,13 +44,13 @@ export const inpaintMaskReducers = {
}, },
imBrushLineAdded: { imBrushLineAdded: {
reducer: (state, action: PayloadAction<Omit<BrushLineAddedArg, 'id'> & { lineId: string }>) => { reducer: (state, action: PayloadAction<Omit<BrushLineAddedArg, 'id'> & { lineId: string }>) => {
const { points, lineId, color, width, clip } = action.payload; const { points, lineId, width, clip } = action.payload;
state.inpaintMask.objects.push({ state.inpaintMask.objects.push({
id: getBrushLineId(state.inpaintMask.id, lineId), id: getBrushLineId(state.inpaintMask.id, lineId),
type: 'brush_line', type: 'brush_line',
points, points,
strokeWidth: width, strokeWidth: width,
color, color: RGBA_RED,
clip, clip,
}); });
state.inpaintMask.bboxNeedsUpdate = true; state.inpaintMask.bboxNeedsUpdate = true;
@ -89,7 +89,7 @@ export const inpaintMaskReducers = {
}, },
imRectAdded: { imRectAdded: {
reducer: (state, action: PayloadAction<Omit<RectShapeAddedArg, 'id'> & { rectId: string }>) => { reducer: (state, action: PayloadAction<Omit<RectShapeAddedArg, 'id'> & { rectId: string }>) => {
const { rect, rectId, color } = action.payload; const { rect, rectId } = action.payload;
if (rect.height === 0 || rect.width === 0) { if (rect.height === 0 || rect.width === 0) {
// Ignore zero-area rectangles // Ignore zero-area rectangles
return; return;
@ -98,7 +98,7 @@ export const inpaintMaskReducers = {
type: 'rect_shape', type: 'rect_shape',
id: getRectShapeId(state.inpaintMask.id, rectId), id: getRectShapeId(state.inpaintMask.id, rectId),
...rect, ...rect,
color, color: RGBA_RED,
}); });
state.inpaintMask.bboxNeedsUpdate = true; state.inpaintMask.bboxNeedsUpdate = true;
state.inpaintMask.imageCache = null; state.inpaintMask.imageCache = null;

View File

@ -2,7 +2,7 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils'; import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming'; import { getBrushLineId, getEraserLineId, getRectShapeId } from 'features/controlLayers/konva/naming';
import type { CanvasV2State, CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types'; 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 { zModelIdentifierField } from 'features/nodes/types/common';
import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas'; import type { ParameterAutoNegative } from 'features/parameters/types/parameterSchemas';
import type { IRect } from 'konva/lib/types'; import type { IRect } from 'konva/lib/types';
@ -319,7 +319,7 @@ export const regionsReducers = {
type: 'brush_line', type: 'brush_line',
points, points,
strokeWidth: width, strokeWidth: width,
color: DEFAULT_RGBA_COLOR, color: RGBA_RED,
clip, clip,
}); });
rg.bboxNeedsUpdate = true; rg.bboxNeedsUpdate = true;
@ -379,7 +379,7 @@ export const regionsReducers = {
type: 'rect_shape', type: 'rect_shape',
id: getRectShapeId(id, rectId), id: getRectShapeId(id, rectId),
...rect, ...rect,
color: DEFAULT_RGBA_COLOR, color: RGBA_RED,
}); });
rg.bboxNeedsUpdate = true; rg.bboxNeedsUpdate = true;
rg.imageCache = null; rg.imageCache = null;

View File

@ -495,7 +495,7 @@ const zRgbaColor = zRgbColor.extend({
a: z.number().min(0).max(1), a: z.number().min(0).max(1),
}); });
export type RgbaColor = z.infer<typeof zRgbaColor>; export type RgbaColor = z.infer<typeof zRgbaColor>;
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); const zOpacity = z.number().gte(0).lte(1);

View File

@ -18,7 +18,7 @@ export const addImageToImage = async (
denoise.denoising_start = denoising_start; denoise.denoising_start = denoising_start;
const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']);
const initialImage = await manager.util.getImageSourceImage({ const initialImage = await manager.getImageSourceImage({
bbox: cropBbox, bbox: cropBbox,
preview: true, preview: true,
}); });

View File

@ -22,11 +22,11 @@ export const addInpaint = async (
denoise.denoising_start = denoising_start; denoise.denoising_start = denoising_start;
const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']);
const initialImage = await manager.util.getImageSourceImage({ const initialImage = await manager.getImageSourceImage({
bbox: cropBbox, bbox: cropBbox,
preview: true, preview: true,
}); });
const maskImage = await manager.util.getInpaintMaskImage({ const maskImage = await manager.getInpaintMaskImage({
bbox: cropBbox, bbox: cropBbox,
preview: true, preview: true,
}); });

View File

@ -21,11 +21,11 @@ export const addOutpaint = async (
vaePrecision: ParameterPrecision vaePrecision: ParameterPrecision
): Promise<Invocation<'canvas_paste_back'>> => { ): Promise<Invocation<'canvas_paste_back'>> => {
const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']); const cropBbox = pick(bbox, ['x', 'y', 'width', 'height']);
const initialImage = await manager.util.getImageSourceImage({ const initialImage = await manager.getImageSourceImage({
bbox: cropBbox, bbox: cropBbox,
preview: true, preview: true,
}); });
const maskImage = await manager.util.getInpaintMaskImage({ const maskImage = await manager.getInpaintMaskImage({
bbox: cropBbox, bbox: cropBbox,
preview: true, preview: true,
}); });

View File

@ -44,7 +44,7 @@ export const addRegions = async (
for (const region of validRegions) { for (const region of validRegions) {
// Upload the mask image, or get the cached image if it exists // 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 // The main mask-to-tensor node
const maskToTensor = g.addNode({ const maskToTensor = g.addNode({

View File

@ -36,7 +36,7 @@ import { assert } from 'tsafe';
import { addRegions } from './addRegions'; import { addRegions } from './addRegions';
export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager): Promise<GraphType> => { export const buildSD1Graph = async (state: RootState, manager: KonvaNodeManager): Promise<GraphType> => {
const generationMode = manager.util.getGenerationMode(); const generationMode = manager.getGenerationMode();
const { bbox, params } = state.canvasV2; const { bbox, params } = state.canvasV2;

View File

@ -34,7 +34,7 @@ import { assert } from 'tsafe';
import { addRegions } from './addRegions'; import { addRegions } from './addRegions';
export const buildSDXLGraph = async (state: RootState, manager: KonvaNodeManager): Promise<NonNullableGraph> => { export const buildSDXLGraph = async (state: RootState, manager: KonvaNodeManager): Promise<NonNullableGraph> => {
const generationMode = manager.util.getGenerationMode(); const generationMode = manager.getGenerationMode();
const { bbox, params } = state.canvasV2; const { bbox, params } = state.canvasV2;