mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): bbox calc for raster layers
This commit is contained in:
parent
90313091db
commit
51de25122a
@ -1,9 +1,14 @@
|
|||||||
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
||||||
import { imageDataToDataURL } from 'features/canvas/util/blobToDataURL';
|
import { imageDataToDataURL } from 'features/canvas/util/blobToDataURL';
|
||||||
import { BBOX_SELECTED_STROKE } from 'features/controlLayers/konva/constants';
|
import { BBOX_SELECTED_STROKE } from 'features/controlLayers/konva/constants';
|
||||||
import { getLayerBboxId, LAYER_BBOX_NAME, RG_LAYER_OBJECT_GROUP_NAME } from 'features/controlLayers/konva/naming';
|
import {
|
||||||
|
getLayerBboxId,
|
||||||
|
LAYER_BBOX_NAME,
|
||||||
|
RASTER_LAYER_OBJECT_GROUP_NAME,
|
||||||
|
RG_LAYER_OBJECT_GROUP_NAME,
|
||||||
|
} from 'features/controlLayers/konva/naming';
|
||||||
import type { Layer, Tool } from 'features/controlLayers/store/types';
|
import type { Layer, Tool } from 'features/controlLayers/store/types';
|
||||||
import { isRegionalGuidanceLayer } from 'features/controlLayers/store/types';
|
import { isRegionalGuidanceLayer, isRGOrRasterlayer } from 'features/controlLayers/store/types';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import type { IRect } from 'konva/lib/types';
|
import type { IRect } from 'konva/lib/types';
|
||||||
import { assert } from 'tsafe';
|
import { assert } from 'tsafe';
|
||||||
@ -64,9 +69,13 @@ const getImageDataBbox = (imageData: ImageData): Extents | null => {
|
|||||||
* Clones a regional guidance konva layer onto an offscreen stage/canvas. This allows the pixel data for a given layer
|
* Clones a regional guidance konva layer onto an offscreen stage/canvas. This allows the pixel data for a given layer
|
||||||
* to be captured, manipulated or analyzed without interference from other layers.
|
* to be captured, manipulated or analyzed without interference from other layers.
|
||||||
* @param layer The konva layer to clone.
|
* @param layer The konva layer to clone.
|
||||||
|
* @param filterChildren A callback to filter out unwanted children
|
||||||
* @returns The cloned stage and layer.
|
* @returns The cloned stage and layer.
|
||||||
*/
|
*/
|
||||||
const getIsolatedRGLayerClone = (layer: Konva.Layer): { stageClone: Konva.Stage; layerClone: Konva.Layer } => {
|
const getIsolatedLayerClone = (
|
||||||
|
layer: Konva.Layer,
|
||||||
|
filterChildren: (node: Konva.Node) => boolean
|
||||||
|
): { stageClone: Konva.Stage; layerClone: Konva.Layer } => {
|
||||||
const stage = layer.getStage();
|
const stage = layer.getStage();
|
||||||
|
|
||||||
// Construct an offscreen canvas with the same dimensions as the layer's stage.
|
// Construct an offscreen canvas with the same dimensions as the layer's stage.
|
||||||
@ -84,7 +93,7 @@ const getIsolatedRGLayerClone = (layer: Konva.Layer): { stageClone: Konva.Stage;
|
|||||||
stageClone.add(layerClone);
|
stageClone.add(layerClone);
|
||||||
|
|
||||||
for (const child of layerClone.getChildren()) {
|
for (const child of layerClone.getChildren()) {
|
||||||
if (child.name() === RG_LAYER_OBJECT_GROUP_NAME && child.hasChildren()) {
|
if (filterChildren(child) && child.hasChildren()) {
|
||||||
// We need to cache the group to ensure it composites out eraser strokes correctly
|
// We need to cache the group to ensure it composites out eraser strokes correctly
|
||||||
child.opacity(1);
|
child.opacity(1);
|
||||||
child.cache();
|
child.cache();
|
||||||
@ -102,7 +111,11 @@ const getIsolatedRGLayerClone = (layer: Konva.Layer): { stageClone: Konva.Stage;
|
|||||||
* @param layer The konva layer to get the bounding box of.
|
* @param layer The konva layer to get the bounding box of.
|
||||||
* @param preview Whether to open a new tab displaying the rendered layer, which is used to calculate the bbox.
|
* @param preview Whether to open a new tab displaying the rendered layer, which is used to calculate the bbox.
|
||||||
*/
|
*/
|
||||||
const getLayerBboxPixels = (layer: Konva.Layer, preview: boolean = false): IRect | null => {
|
const getLayerBboxPixels = (
|
||||||
|
layer: Konva.Layer,
|
||||||
|
filterChildren: (node: Konva.Node) => boolean,
|
||||||
|
preview: boolean = false
|
||||||
|
): IRect | null => {
|
||||||
// To calculate the layer's bounding box, we must first export it to a pixel array, then do some math.
|
// To calculate the layer's bounding box, we must first export it to a pixel array, then do some math.
|
||||||
//
|
//
|
||||||
// Though it is relatively fast, we can't use Konva's `getClientRect`. It programmatically determines the rect
|
// Though it is relatively fast, we can't use Konva's `getClientRect`. It programmatically determines the rect
|
||||||
@ -110,7 +123,7 @@ const getLayerBboxPixels = (layer: Konva.Layer, preview: boolean = false): IRect
|
|||||||
//
|
//
|
||||||
// This doesn't work when some shapes are drawn with composite operations that "erase" pixels, like eraser lines.
|
// This doesn't work when some shapes are drawn with composite operations that "erase" pixels, like eraser lines.
|
||||||
// These shapes' extents are still calculated as if they were solid, leading to a bounding box that is too large.
|
// These shapes' extents are still calculated as if they were solid, leading to a bounding box that is too large.
|
||||||
const { stageClone, layerClone } = getIsolatedRGLayerClone(layer);
|
const { stageClone, layerClone } = getIsolatedLayerClone(layer, filterChildren);
|
||||||
|
|
||||||
// Get a worst-case rect using the relatively fast `getClientRect`.
|
// Get a worst-case rect using the relatively fast `getClientRect`.
|
||||||
const layerRect = layerClone.getClientRect();
|
const layerRect = layerClone.getClientRect();
|
||||||
@ -178,6 +191,9 @@ const createBboxRect = (layerState: Layer, konvaLayer: Konva.Layer): Konva.Rect
|
|||||||
return rect;
|
return rect;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const filterRGChildren = (node: Konva.Node): boolean => node.name() === RG_LAYER_OBJECT_GROUP_NAME;
|
||||||
|
const filterRasterChildren = (node: Konva.Node): boolean => node.name() === RASTER_LAYER_OBJECT_GROUP_NAME;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the bbox of each regional guidance layer. Only calculates if the mask has changed.
|
* Calculates the bbox of each regional guidance layer. Only calculates if the mask has changed.
|
||||||
* @param stage The konva stage
|
* @param stage The konva stage
|
||||||
@ -189,23 +205,24 @@ export const updateBboxes = (
|
|||||||
layerStates: Layer[],
|
layerStates: Layer[],
|
||||||
onBboxChanged: (layerId: string, bbox: IRect | null) => void
|
onBboxChanged: (layerId: string, bbox: IRect | null) => void
|
||||||
): void => {
|
): void => {
|
||||||
for (const rgLayer of layerStates.filter(isRegionalGuidanceLayer)) {
|
for (const layerState of layerStates.filter(isRGOrRasterlayer)) {
|
||||||
const konvaLayer = stage.findOne<Konva.Layer>(`#${rgLayer.id}`);
|
const konvaLayer = stage.findOne<Konva.Layer>(`#${layerState.id}`);
|
||||||
assert(konvaLayer, `Layer ${rgLayer.id} not found in stage`);
|
assert(konvaLayer, `Layer ${layerState.id} not found in stage`);
|
||||||
// We only need to recalculate the bbox if the layer has changed
|
// We only need to recalculate the bbox if the layer has changed
|
||||||
if (rgLayer.bboxNeedsUpdate) {
|
if (layerState.bboxNeedsUpdate) {
|
||||||
const bboxRect = konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(rgLayer, konvaLayer);
|
const bboxRect = konvaLayer.findOne<Konva.Rect>(`.${LAYER_BBOX_NAME}`) ?? createBboxRect(layerState, konvaLayer);
|
||||||
|
|
||||||
// Hide the bbox while we calculate the new bbox, else the bbox will be included in the calculation
|
// Hide the bbox while we calculate the new bbox, else the bbox will be included in the calculation
|
||||||
const visible = bboxRect.visible();
|
const visible = bboxRect.visible();
|
||||||
bboxRect.visible(false);
|
bboxRect.visible(false);
|
||||||
|
|
||||||
if (rgLayer.objects.length === 0) {
|
if (layerState.objects.length === 0) {
|
||||||
// No objects - no bbox to calculate
|
// No objects - no bbox to calculate
|
||||||
onBboxChanged(rgLayer.id, null);
|
onBboxChanged(layerState.id, null);
|
||||||
} else {
|
} else {
|
||||||
// Calculate the bbox by rendering the layer and checking its pixels
|
// Calculate the bbox by rendering the layer and checking its pixels
|
||||||
onBboxChanged(rgLayer.id, getLayerBboxPixels(konvaLayer));
|
const filterChildren = isRegionalGuidanceLayer(layerState) ? filterRGChildren : filterRasterChildren;
|
||||||
|
onBboxChanged(layerState.id, getLayerBboxPixels(konvaLayer, filterChildren));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore the visibility of the bbox
|
// Restore the visibility of the bbox
|
||||||
@ -232,7 +249,7 @@ export const renderBboxes = (stage: Konva.Stage, layerStates: Layer[], tool: Too
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const layer of layerStates.filter(isRegionalGuidanceLayer)) {
|
for (const layer of layerStates.filter(isRGOrRasterlayer)) {
|
||||||
if (!layer.bbox) {
|
if (!layer.bbox) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -162,10 +162,15 @@ export const controlLayersSlice = createSlice({
|
|||||||
if (isRenderableLayer(layer)) {
|
if (isRenderableLayer(layer)) {
|
||||||
layer.bbox = bbox;
|
layer.bbox = bbox;
|
||||||
layer.bboxNeedsUpdate = false;
|
layer.bboxNeedsUpdate = false;
|
||||||
if (bbox === null && layer.type === 'regional_guidance_layer') {
|
if (bbox === null) {
|
||||||
// The layer was fully erased, empty its objects to prevent accumulation of invisible objects
|
// The layer was fully erased, empty its objects to prevent accumulation of invisible objects
|
||||||
layer.objects = [];
|
if (isRegionalGuidanceLayer(layer)) {
|
||||||
layer.uploadedMaskImage = null;
|
layer.objects = [];
|
||||||
|
layer.uploadedMaskImage = null;
|
||||||
|
}
|
||||||
|
if (isRasterLayer(layer)) {
|
||||||
|
layer.objects = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -298,18 +298,13 @@ export const isRasterLayer = (layer?: Layer): layer is RasterLayer => {
|
|||||||
};
|
};
|
||||||
export const isRenderableLayer = (
|
export const isRenderableLayer = (
|
||||||
layer?: Layer
|
layer?: Layer
|
||||||
): layer is RegionalGuidanceLayer | ControlAdapterLayer | InitialImageLayer => {
|
): layer is RegionalGuidanceLayer | ControlAdapterLayer | InitialImageLayer | RasterLayer => {
|
||||||
return (
|
return (
|
||||||
layer?.type === 'regional_guidance_layer' ||
|
isRegionalGuidanceLayer(layer) || isControlAdapterLayer(layer) || isInitialImageLayer(layer) || isRasterLayer(layer)
|
||||||
layer?.type === 'control_adapter_layer' ||
|
|
||||||
layer?.type === 'initial_image_layer' ||
|
|
||||||
layer?.type === 'raster_layer'
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export const isLayerWithOpacity = (layer?: Layer): layer is ControlAdapterLayer | InitialImageLayer | RasterLayer => {
|
export const isLayerWithOpacity = (layer?: Layer): layer is ControlAdapterLayer | InitialImageLayer | RasterLayer => {
|
||||||
return (
|
return isControlAdapterLayer(layer) || isInitialImageLayer(layer) || isRasterLayer(layer);
|
||||||
layer?.type === 'control_adapter_layer' || layer?.type === 'initial_image_layer' || layer?.type === 'raster_layer'
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
export const isCAOrIPALayer = (layer?: Layer): layer is ControlAdapterLayer | IPAdapterLayer => {
|
export const isCAOrIPALayer = (layer?: Layer): layer is ControlAdapterLayer | IPAdapterLayer => {
|
||||||
return isControlAdapterLayer(layer) || isIPAdapterLayer(layer);
|
return isControlAdapterLayer(layer) || isIPAdapterLayer(layer);
|
||||||
|
Loading…
Reference in New Issue
Block a user