mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
perf(ui): more bbox optimizations
- Keep track of whether the bbox needs to be recalculated (e.g. had lines/points added) - Keep track of whether the bbox has eraser strokes - if yes, we need to do the full pixel-perfect bbox calculation, otherwise we can use the faster getClientRect - Use comparison rather than Math.min/max in bbox calculation (slightly faster) - Return `null` if no pixel data at all in bbox
This commit is contained in:
parent
a71ed10b71
commit
8a69fbd336
@ -49,7 +49,7 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem
|
||||
);
|
||||
|
||||
const onBboxChanged = useCallback(
|
||||
(layerId: string, bbox: IRect) => {
|
||||
(layerId: string, bbox: IRect | null) => {
|
||||
dispatch(rpLayerBboxChanged({ layerId, bbox }));
|
||||
},
|
||||
[dispatch]
|
||||
@ -138,8 +138,8 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem
|
||||
if (!stage) {
|
||||
return;
|
||||
}
|
||||
renderBbox(stage, tool, state.selectedLayerId, onBboxChanged);
|
||||
}, [dispatch, stage, tool, state.selectedLayerId, onBboxChanged]);
|
||||
renderBbox(stage, state.layers, state.selectedLayerId, tool, onBboxChanged);
|
||||
}, [dispatch, stage, state.layers, state.selectedLayerId, tool, onBboxChanged]);
|
||||
};
|
||||
|
||||
const $container = atom<HTMLDivElement | null>(null);
|
||||
|
@ -53,6 +53,8 @@ export type RegionalPromptLayer = LayerBase & {
|
||||
x: number;
|
||||
y: number;
|
||||
bbox: IRect | null;
|
||||
bboxNeedsUpdate: boolean;
|
||||
hasEraserStrokes: boolean;
|
||||
kind: 'regionalPromptLayer';
|
||||
objects: LayerObject[];
|
||||
positivePrompt: string;
|
||||
@ -104,6 +106,8 @@ export const regionalPromptsSlice = createSlice({
|
||||
x: 0,
|
||||
y: 0,
|
||||
autoNegative: 'off',
|
||||
bboxNeedsUpdate: false,
|
||||
hasEraserStrokes: false,
|
||||
};
|
||||
state.layers.push(layer);
|
||||
state.selectedLayerId = layer.id;
|
||||
@ -154,6 +158,8 @@ export const regionalPromptsSlice = createSlice({
|
||||
layer.objects = [];
|
||||
layer.bbox = null;
|
||||
layer.isVisible = true;
|
||||
layer.hasEraserStrokes = false;
|
||||
layer.bboxNeedsUpdate = false;
|
||||
}
|
||||
},
|
||||
rpLayerTranslated: (state, action: PayloadAction<{ layerId: string; x: number; y: number }>) => {
|
||||
@ -169,6 +175,7 @@ export const regionalPromptsSlice = createSlice({
|
||||
const layer = state.layers.find((l) => l.id === layerId);
|
||||
if (isRPLayer(layer)) {
|
||||
layer.bbox = bbox;
|
||||
layer.bboxNeedsUpdate = false;
|
||||
}
|
||||
},
|
||||
allLayersDeleted: (state) => {
|
||||
@ -218,6 +225,10 @@ export const regionalPromptsSlice = createSlice({
|
||||
points: [points[0] - layer.x, points[1] - layer.y, points[2] - layer.x, points[3] - layer.y],
|
||||
strokeWidth: state.brushSize,
|
||||
});
|
||||
layer.bboxNeedsUpdate = true;
|
||||
if (!layer.hasEraserStrokes) {
|
||||
layer.hasEraserStrokes = tool === 'eraser';
|
||||
}
|
||||
}
|
||||
},
|
||||
prepare: (payload: { layerId: string; points: [number, number, number, number]; tool: DrawingTool }) => ({
|
||||
@ -236,6 +247,7 @@ export const regionalPromptsSlice = createSlice({
|
||||
// Points must be offset by the layer's x and y coordinates
|
||||
// TODO: Handle this in the event listener
|
||||
lastLine.points.push(point[0] - layer.x, point[1] - layer.y);
|
||||
layer.bboxNeedsUpdate = true;
|
||||
}
|
||||
},
|
||||
rpLayerAutoNegativeChanged: (
|
||||
@ -364,7 +376,7 @@ const undoableGroupByMatcher = isAnyOf(
|
||||
rpLayerPositivePromptChanged,
|
||||
rpLayerNegativePromptChanged,
|
||||
rpLayerTranslated,
|
||||
rpLayerColorChanged,
|
||||
rpLayerColorChanged
|
||||
);
|
||||
|
||||
const LINE_1 = 'LINE_1';
|
||||
|
@ -6,31 +6,49 @@ import type { Node as KonvaNodeType, NodeConfig as KonvaNodeConfigType } from 'k
|
||||
import type { IRect } from 'konva/lib/types';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
type Extents = {
|
||||
minX: number;
|
||||
minY: number;
|
||||
maxX: number;
|
||||
maxY: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the bounding box of an image.
|
||||
* @param imageData The ImageData object to get the bounding box of.
|
||||
* @returns The minimum and maximum x and y values of the image's bounding box.
|
||||
*/
|
||||
const getImageDataBbox = (imageData: ImageData) => {
|
||||
const getImageDataBbox = (imageData: ImageData): Extents | null => {
|
||||
const { data, width, height } = imageData;
|
||||
let minX = width;
|
||||
let minY = height;
|
||||
let maxX = 0;
|
||||
let maxY = 0;
|
||||
let maxX = -1;
|
||||
let maxY = -1;
|
||||
let alpha = 0;
|
||||
let isEmpty = true;
|
||||
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const alpha = data[(y * width + x) * 4 + 3] ?? 0;
|
||||
alpha = data[(y * width + x) * 4 + 3] ?? 0;
|
||||
if (alpha > 0) {
|
||||
minX = Math.min(minX, x);
|
||||
maxX = Math.max(maxX, x);
|
||||
minY = Math.min(minY, y);
|
||||
maxY = Math.max(maxY, y);
|
||||
isEmpty = false;
|
||||
if (x < minX) {
|
||||
minX = x;
|
||||
}
|
||||
if (x > maxX) {
|
||||
maxX = x;
|
||||
}
|
||||
if (y < minY) {
|
||||
minY = y;
|
||||
}
|
||||
if (y > maxY) {
|
||||
maxY = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { minX, minY, maxX, maxY };
|
||||
return isEmpty ? null : { minX, minY, maxX, maxY };
|
||||
};
|
||||
|
||||
/**
|
||||
@ -43,7 +61,7 @@ export const getKonvaLayerBbox = (
|
||||
layer: KonvaLayerType,
|
||||
filterChildren?: (item: KonvaNodeType<KonvaNodeConfigType>) => boolean,
|
||||
preview: boolean = false
|
||||
): IRect => {
|
||||
): IRect | null => {
|
||||
// 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
|
||||
@ -89,6 +107,10 @@ export const getKonvaLayerBbox = (
|
||||
// Calculate the layer's bounding box.
|
||||
const layerBbox = getImageDataBbox(layerImageData);
|
||||
|
||||
if (!layerBbox) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Correct the bounding box to be relative to the layer's position.
|
||||
const correctedLayerBbox = {
|
||||
x: layerBbox.minX - stage.x() + layerRect.x - layer.x(),
|
||||
|
@ -302,6 +302,8 @@ export const renderLayers = (
|
||||
const selectPromptLayerObjectGroup = (item: Node<NodeConfig>) =>
|
||||
item.name() !== REGIONAL_PROMPT_LAYER_OBJECT_GROUP_NAME;
|
||||
|
||||
const GET_CLIENT_RECT_CONFIG = { skipTransform: true };
|
||||
|
||||
/**
|
||||
*
|
||||
* @param stage The konva stage to render on.
|
||||
@ -312,9 +314,10 @@ const selectPromptLayerObjectGroup = (item: Node<NodeConfig>) =>
|
||||
*/
|
||||
export const renderBbox = (
|
||||
stage: Konva.Stage,
|
||||
tool: RPTool,
|
||||
reduxLayers: Layer[],
|
||||
selectedLayerIdId: string | null,
|
||||
onBboxChanged: (layerId: string, bbox: IRect) => void
|
||||
tool: RPTool,
|
||||
onBboxChanged: (layerId: string, bbox: IRect | null) => void
|
||||
) => {
|
||||
// Hide all bounding boxes
|
||||
for (const bboxRect of stage.find<Konva.Rect>(`.${REGIONAL_PROMPT_LAYER_BBOX_NAME}`)) {
|
||||
@ -327,11 +330,27 @@ export const renderBbox = (
|
||||
return;
|
||||
}
|
||||
|
||||
const reduxLayer = reduxLayers.find((layer) => layer.id === selectedLayerIdId);
|
||||
assert(reduxLayer, `Selected layer ${selectedLayerIdId} not found in redux layers`);
|
||||
|
||||
const konvaLayer = stage.findOne<Konva.Layer>(`#${selectedLayerIdId}`);
|
||||
assert(konvaLayer, `Selected layer ${selectedLayerIdId} not found in stage`);
|
||||
|
||||
const bbox = getKonvaLayerBbox(konvaLayer, selectPromptLayerObjectGroup);
|
||||
let bbox = reduxLayer.bbox;
|
||||
|
||||
// We only need to recalculate the bbox if the layer has changed and it has objects
|
||||
if (reduxLayer.bboxNeedsUpdate && reduxLayer.objects.length) {
|
||||
// We only need to use the pixel-perfect bounding box if the layer has eraser strokes
|
||||
bbox = reduxLayer.hasEraserStrokes
|
||||
? getKonvaLayerBbox(konvaLayer, selectPromptLayerObjectGroup)
|
||||
: konvaLayer.getClientRect(GET_CLIENT_RECT_CONFIG);
|
||||
|
||||
onBboxChanged(selectedLayerIdId, bbox);
|
||||
}
|
||||
|
||||
if (!bbox) {
|
||||
return;
|
||||
}
|
||||
|
||||
let rect = konvaLayer.findOne<Konva.Rect>(`.${REGIONAL_PROMPT_LAYER_BBOX_NAME}`);
|
||||
if (!rect) {
|
||||
|
Loading…
Reference in New Issue
Block a user