Skip to content

Commit 42161a5

Browse files
Actual fixes.
1 parent d4bd43b commit 42161a5

File tree

5 files changed

+268
-147
lines changed

5 files changed

+268
-147
lines changed

invokeai/frontend/web/src/features/controlLayers/components/InpaintMask/InpaintMaskBboxAdjuster.tsx

Lines changed: 91 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,16 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
33
import { bboxChangedFromCanvas } from 'features/controlLayers/store/canvasSlice';
44
import { selectMaskBlur } from 'features/controlLayers/store/paramsSlice';
55
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
6-
import type { Rect } from 'features/controlLayers/store/types';
6+
import type {
7+
CanvasBrushLineState,
8+
CanvasBrushLineWithPressureState,
9+
CanvasEraserLineState,
10+
CanvasEraserLineWithPressureState,
11+
CanvasImageState,
12+
CanvasRectState,
13+
Rect,
14+
} from 'features/controlLayers/store/types';
15+
import { maskObjectsToBitmap } from 'features/controlLayers/util/bitmapToMaskObjects';
716
import { memo, useCallback, useMemo } from 'react';
817
import { useTranslation } from 'react-i18next';
918
import { PiCropBold } from 'react-icons/pi';
@@ -14,93 +23,118 @@ export const InpaintMaskBboxAdjuster = memo(() => {
1423
const canvasSlice = useAppSelector(selectCanvasSlice);
1524
const maskBlur = useAppSelector(selectMaskBlur);
1625

17-
// Get all inpaint mask entities
26+
// Get all inpaint mask entities and bbox
1827
const inpaintMasks = canvasSlice.inpaintMasks.entities;
28+
const bboxRect = canvasSlice.bbox.rect;
1929

2030
// Calculate the bounding box that contains all inpaint masks
2131
const calculateMaskBbox = useCallback((): Rect | null => {
2232
if (inpaintMasks.length === 0) {
2333
return null;
2434
}
2535

26-
let minX = Infinity;
27-
let minY = Infinity;
28-
let maxX = -Infinity;
29-
let maxY = -Infinity;
30-
31-
// Iterate through all inpaint masks to find the overall bounds
36+
// Use the current bbox as the reference container
37+
const canvasWidth = bboxRect.width;
38+
const canvasHeight = bboxRect.height;
39+
40+
// Collect all mask objects and adjust their positions relative to the bbox
41+
const allObjects: (
42+
| CanvasBrushLineState
43+
| CanvasBrushLineWithPressureState
44+
| CanvasEraserLineState
45+
| CanvasEraserLineWithPressureState
46+
| CanvasRectState
47+
| CanvasImageState
48+
)[] = [];
3249
for (const mask of inpaintMasks) {
33-
if (!mask.isEnabled || mask.objects.length === 0) {
50+
if (!mask.isEnabled || !mask.objects || mask.objects.length === 0) {
3451
continue;
3552
}
3653

37-
// Calculate bounds for this mask's objects
54+
// Adjust object positions relative to the bbox (not the entity position)
3855
for (const obj of mask.objects) {
39-
let objMinX = 0;
40-
let objMinY = 0;
41-
let objMaxX = 0;
42-
let objMaxY = 0;
43-
4456
if (obj.type === 'rect') {
45-
objMinX = mask.position.x + obj.rect.x;
46-
objMinY = mask.position.y + obj.rect.y;
47-
objMaxX = objMinX + obj.rect.width;
48-
objMaxY = objMinY + obj.rect.height;
57+
const adjustedObj = {
58+
...obj,
59+
rect: {
60+
...obj.rect,
61+
x: obj.rect.x + mask.position.x - bboxRect.x,
62+
y: obj.rect.y + mask.position.y - bboxRect.y,
63+
},
64+
};
65+
allObjects.push(adjustedObj);
4966
} else if (
5067
obj.type === 'brush_line' ||
5168
obj.type === 'brush_line_with_pressure' ||
5269
obj.type === 'eraser_line' ||
5370
obj.type === 'eraser_line_with_pressure'
5471
) {
55-
// For lines, find the min/max points
72+
const adjustedPoints: number[] = [];
5673
for (let i = 0; i < obj.points.length; i += 2) {
57-
const x = mask.position.x + (obj.points[i] ?? 0);
58-
const y = mask.position.y + (obj.points[i + 1] ?? 0);
59-
60-
if (i === 0) {
61-
objMinX = objMaxX = x;
62-
objMinY = objMaxY = y;
63-
} else {
64-
objMinX = Math.min(objMinX, x);
65-
objMinY = Math.min(objMinY, y);
66-
objMaxX = Math.max(objMaxX, x);
67-
objMaxY = Math.max(objMaxY, y);
68-
}
74+
adjustedPoints.push((obj.points[i] ?? 0) + mask.position.x - bboxRect.x);
75+
adjustedPoints.push((obj.points[i + 1] ?? 0) + mask.position.y - bboxRect.y);
6976
}
70-
// Add stroke width to account for line thickness
71-
const strokeRadius = (obj.strokeWidth ?? 50) / 2;
72-
objMinX -= strokeRadius;
73-
objMinY -= strokeRadius;
74-
objMaxX += strokeRadius;
75-
objMaxY += strokeRadius;
77+
const adjustedObj = {
78+
...obj,
79+
points: adjustedPoints,
80+
};
81+
allObjects.push(adjustedObj);
7682
} else if (obj.type === 'image') {
77-
// Image objects are positioned at the entity's position
78-
objMinX = mask.position.x;
79-
objMinY = mask.position.y;
80-
objMaxX = objMinX + obj.image.width;
81-
objMaxY = objMinY + obj.image.height;
83+
// For image objects, we need to handle them differently since they don't have rect or points
84+
// We'll skip them for now as they're not commonly used in masks
85+
continue;
8286
}
87+
}
88+
}
8389

84-
// Update overall bounds
85-
minX = Math.min(minX, objMinX);
86-
minY = Math.min(minY, objMinY);
87-
maxX = Math.max(maxX, objMaxX);
88-
maxY = Math.max(maxY, objMaxY);
90+
if (allObjects.length === 0) {
91+
return null;
92+
}
93+
94+
// Render the consolidated mask to a bitmap
95+
const bitmap = maskObjectsToBitmap(allObjects, canvasWidth, canvasHeight);
96+
const { width, height, data } = bitmap;
97+
98+
// Find the actual bounds of the rendered mask
99+
let maskMinX = width;
100+
let maskMinY = height;
101+
let maskMaxX = 0;
102+
let maskMaxY = 0;
103+
104+
for (let y = 0; y < height; y++) {
105+
for (let x = 0; x < width; x++) {
106+
const pixelIndex = (y * width + x) * 4;
107+
const alpha = data[pixelIndex + 3] ?? 0;
108+
109+
// If this pixel has any opacity, it's part of the mask
110+
if (alpha > 0) {
111+
maskMinX = Math.min(maskMinX, x);
112+
maskMinY = Math.min(maskMinY, y);
113+
maskMaxX = Math.max(maskMaxX, x);
114+
maskMaxY = Math.max(maskMaxY, y);
115+
}
89116
}
90117
}
91118

92-
// If no valid bounds found, return null
93-
if (minX === Infinity || minY === Infinity || maxX === -Infinity || maxY === -Infinity) {
119+
// If no mask pixels found, return null
120+
if (maskMinX >= maskMaxX || maskMinY >= maskMaxY) {
94121
return null;
95122
}
96123

124+
// Clamp the mask bounds to the bbox boundaries
125+
maskMinX = Math.max(0, maskMinX);
126+
maskMinY = Math.max(0, maskMinY);
127+
maskMaxX = Math.min(width - 1, maskMaxX);
128+
maskMaxY = Math.min(height - 1, maskMaxY);
129+
130+
// Convert back to world coordinates relative to the bbox
97131
return {
98-
x: minX,
99-
y: minY,
100-
width: maxX - minX,
101-
height: maxY - minY,
132+
x: bboxRect.x + maskMinX,
133+
y: bboxRect.y + maskMinY,
134+
width: maskMaxX - maskMinX + 1,
135+
height: maskMaxY - maskMinY + 1,
102136
};
103-
}, [inpaintMasks]);
137+
}, [inpaintMasks, bboxRect]);
104138

105139
const maskBbox = useMemo(() => calculateMaskBbox(), [calculateMaskBbox]);
106140

@@ -128,13 +162,13 @@ export const InpaintMaskBboxAdjuster = memo(() => {
128162
}
129163

130164
return (
131-
<Flex w="full" ps={2} pe={2} pb={1}>
165+
<Flex w="full" ps={2} pe={2} pb={1} gap={2}>
132166
<Button
133167
size="sm"
134168
variant="ghost"
135169
leftIcon={<Icon as={PiCropBold} boxSize={3} />}
136170
onClick={handleAdjustBbox}
137-
w="full"
171+
flex={1}
138172
justifyContent="flex-start"
139173
h={6}
140174
fontSize="xs"

invokeai/frontend/web/src/features/controlLayers/components/Toolbar/CanvasToolbar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ import { CanvasToolbarResetViewButton } from 'features/controlLayers/components/
99
import { CanvasToolbarSaveToGalleryButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarSaveToGalleryButton';
1010
import { CanvasToolbarScale } from 'features/controlLayers/components/Toolbar/CanvasToolbarScale';
1111
import { CanvasToolbarUndoButton } from 'features/controlLayers/components/Toolbar/CanvasToolbarUndoButton';
12+
import { useCanvasAdjustBboxHotkey } from 'features/controlLayers/hooks/useCanvasAdjustBboxHotkey';
1213
import { useCanvasDeleteLayerHotkey } from 'features/controlLayers/hooks/useCanvasDeleteLayerHotkey';
1314
import { useCanvasEntityQuickSwitchHotkey } from 'features/controlLayers/hooks/useCanvasEntityQuickSwitchHotkey';
1415
import { useCanvasFilterHotkey } from 'features/controlLayers/hooks/useCanvasFilterHotkey';
16+
import { useCanvasInvertMaskHotkey } from 'features/controlLayers/hooks/useCanvasInvertMaskHotkey';
1517
import { useCanvasResetLayerHotkey } from 'features/controlLayers/hooks/useCanvasResetLayerHotkey';
1618
import { useCanvasToggleNonRasterLayersHotkey } from 'features/controlLayers/hooks/useCanvasToggleNonRasterLayersHotkey';
1719
import { useCanvasTransformHotkey } from 'features/controlLayers/hooks/useCanvasTransformHotkey';
1820
import { useCanvasUndoRedoHotkeys } from 'features/controlLayers/hooks/useCanvasUndoRedoHotkeys';
19-
import { useCanvasInvertMaskHotkey } from 'features/controlLayers/hooks/useCanvasInvertMaskHotkey';
20-
import { useCanvasAdjustBboxHotkey } from 'features/controlLayers/hooks/useCanvasAdjustBboxHotkey';
2121
import { useNextPrevEntityHotkeys } from 'features/controlLayers/hooks/useNextPrevEntity';
2222
import { memo } from 'react';
2323

0 commit comments

Comments
 (0)