Skip to content

Commit f9a49e2

Browse files
Fix bugs. Extract into transform utilities
1 parent 9155a53 commit f9a49e2

File tree

6 files changed

+687
-133
lines changed

6 files changed

+687
-133
lines changed

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

Lines changed: 42 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,17 @@ 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+
Rect,
8+
CanvasBrushLineState,
9+
CanvasBrushLineWithPressureState,
10+
CanvasEraserLineState,
11+
CanvasEraserLineWithPressureState,
12+
CanvasRectState,
13+
CanvasImageState,
14+
} from 'features/controlLayers/store/types';
15+
import { transformMaskObjectsRelativeToBbox, calculateMaskBoundsFromBitmap } from 'features/controlLayers/util/maskObjectTransform';
16+
import { convertTransformedToOriginal } from 'features/controlLayers/util/coordinateTransform';
717
import { memo, useCallback, useMemo } from 'react';
818
import { useTranslation } from 'react-i18next';
919
import { PiCropBold } from 'react-icons/pi';
@@ -14,93 +24,57 @@ export const InpaintMaskBboxAdjuster = memo(() => {
1424
const canvasSlice = useAppSelector(selectCanvasSlice);
1525
const maskBlur = useAppSelector(selectMaskBlur);
1626

17-
// Get all inpaint mask entities
27+
// Get all inpaint mask entities and bbox
1828
const inpaintMasks = canvasSlice.inpaintMasks.entities;
29+
const bboxRect = canvasSlice.bbox.rect;
1930

2031
// Calculate the bounding box that contains all inpaint masks
2132
const calculateMaskBbox = useCallback((): Rect | null => {
2233
if (inpaintMasks.length === 0) {
2334
return null;
2435
}
2536

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
37+
// Collect all mask objects from enabled masks
38+
const allObjects: (
39+
| CanvasBrushLineState
40+
| CanvasBrushLineWithPressureState
41+
| CanvasEraserLineState
42+
| CanvasEraserLineWithPressureState
43+
| CanvasRectState
44+
| CanvasImageState
45+
)[] = [];
46+
3247
for (const mask of inpaintMasks) {
33-
if (!mask.isEnabled || mask.objects.length === 0) {
48+
if (!mask.isEnabled || !mask.objects || mask.objects.length === 0) {
3449
continue;
3550
}
3651

37-
// Calculate bounds for this mask's objects
38-
for (const obj of mask.objects) {
39-
let objMinX = 0;
40-
let objMinY = 0;
41-
let objMaxX = 0;
42-
let objMaxY = 0;
43-
44-
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;
49-
} else if (
50-
obj.type === 'brush_line' ||
51-
obj.type === 'brush_line_with_pressure' ||
52-
obj.type === 'eraser_line' ||
53-
obj.type === 'eraser_line_with_pressure'
54-
) {
55-
// For lines, find the min/max points
56-
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-
}
69-
}
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;
76-
} 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;
82-
}
52+
// Transform objects to be relative to the bbox
53+
const transformedObjects = transformMaskObjectsRelativeToBbox(mask.objects, bboxRect);
54+
// Convert back to original types for compatibility
55+
const originalObjects = transformedObjects.map(convertTransformedToOriginal);
56+
allObjects.push(...originalObjects);
57+
}
8358

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);
89-
}
59+
if (allObjects.length === 0) {
60+
return null;
9061
}
9162

92-
// If no valid bounds found, return null
93-
if (minX === Infinity || minY === Infinity || maxX === -Infinity || maxY === -Infinity) {
63+
// Calculate bounds from the rendered bitmap for accurate results
64+
const maskBounds = calculateMaskBoundsFromBitmap(allObjects, bboxRect.width, bboxRect.height);
65+
66+
if (!maskBounds) {
9467
return null;
9568
}
9669

70+
// Convert back to world coordinates relative to the bbox
9771
return {
98-
x: minX,
99-
y: minY,
100-
width: maxX - minX,
101-
height: maxY - minY,
72+
x: bboxRect.x + maskBounds.x,
73+
y: bboxRect.y + maskBounds.y,
74+
width: maskBounds.width,
75+
height: maskBounds.height,
10276
};
103-
}, [inpaintMasks]);
77+
}, [inpaintMasks, bboxRect]);
10478

10579
const maskBbox = useMemo(() => calculateMaskBbox(), [calculateMaskBbox]);
10680

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

invokeai/frontend/web/src/features/controlLayers/hooks/useCanvasAdjustBboxHotkey.ts

Lines changed: 47 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,18 @@ import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
44
import { bboxChangedFromCanvas } from 'features/controlLayers/store/canvasSlice';
55
import { selectMaskBlur } from 'features/controlLayers/store/paramsSlice';
66
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
7+
import type {
8+
Rect,
9+
CanvasBrushLineState,
10+
CanvasBrushLineWithPressureState,
11+
CanvasEraserLineState,
12+
CanvasEraserLineWithPressureState,
13+
CanvasRectState,
14+
CanvasImageState,
15+
} from 'features/controlLayers/store/types';
16+
import { transformMaskObjectsRelativeToBbox, calculateMaskBoundsFromBitmap } from 'features/controlLayers/util/maskObjectTransform';
17+
import { convertTransformedToOriginal } from 'features/controlLayers/util/coordinateTransform';
718
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
8-
import type { Rect } from 'features/controlLayers/store/types';
919
import { useCallback, useMemo } from 'react';
1020

1121
export const useCanvasAdjustBboxHotkey = () => {
@@ -15,71 +25,55 @@ export const useCanvasAdjustBboxHotkey = () => {
1525
const maskBlur = useAppSelector(selectMaskBlur);
1626
const isBusy = useCanvasIsBusy();
1727
const inpaintMasks = canvasSlice.inpaintMasks.entities;
28+
const bboxRect = canvasSlice.bbox.rect;
1829

1930
// Calculate the bounding box that contains all inpaint masks
2031
const calculateMaskBbox = useCallback((): Rect | null => {
2132
if (inpaintMasks.length === 0) {
2233
return null;
2334
}
24-
let minX = Infinity;
25-
let minY = Infinity;
26-
let maxX = -Infinity;
27-
let maxY = -Infinity;
35+
36+
// Collect all mask objects from enabled masks
37+
const allObjects: (
38+
| CanvasBrushLineState
39+
| CanvasBrushLineWithPressureState
40+
| CanvasEraserLineState
41+
| CanvasEraserLineWithPressureState
42+
| CanvasRectState
43+
| CanvasImageState
44+
)[] = [];
45+
2846
for (const mask of inpaintMasks) {
2947
if (!mask.isEnabled || !mask.objects || mask.objects.length === 0) {
3048
continue;
3149
}
32-
for (const obj of mask.objects) {
33-
let objMinX = 0;
34-
let objMinY = 0;
35-
let objMaxX = 0;
36-
let objMaxY = 0;
37-
if (obj.type === 'rect') {
38-
objMinX = mask.position.x + obj.rect.x;
39-
objMinY = mask.position.y + obj.rect.y;
40-
objMaxX = objMinX + obj.rect.width;
41-
objMaxY = objMinY + obj.rect.height;
42-
} else if (
43-
obj.type === 'brush_line' ||
44-
obj.type === 'brush_line_with_pressure' ||
45-
obj.type === 'eraser_line' ||
46-
obj.type === 'eraser_line_with_pressure'
47-
) {
48-
for (let i = 0; i < obj.points.length; i += 2) {
49-
const x = mask.position.x + (obj.points[i] ?? 0);
50-
const y = mask.position.y + (obj.points[i + 1] ?? 0);
51-
if (i === 0) {
52-
objMinX = objMaxX = x;
53-
objMinY = objMaxY = y;
54-
} else {
55-
objMinX = Math.min(objMinX, x);
56-
objMinY = Math.min(objMinY, y);
57-
objMaxX = Math.max(objMaxX, x);
58-
objMaxY = Math.max(objMaxY, y);
59-
}
60-
}
61-
const strokeRadius = (obj.strokeWidth ?? 50) / 2;
62-
objMinX -= strokeRadius;
63-
objMinY -= strokeRadius;
64-
objMaxX += strokeRadius;
65-
objMaxY += strokeRadius;
66-
} else if (obj.type === 'image') {
67-
objMinX = mask.position.x;
68-
objMinY = mask.position.y;
69-
objMaxX = objMinX + obj.image.width;
70-
objMaxY = objMinY + obj.image.height;
71-
}
72-
minX = Math.min(minX, objMinX);
73-
minY = Math.min(minY, objMinY);
74-
maxX = Math.max(maxX, objMaxX);
75-
maxY = Math.max(maxY, objMaxY);
76-
}
50+
51+
// Transform objects to be relative to the bbox
52+
const transformedObjects = transformMaskObjectsRelativeToBbox(mask.objects, bboxRect);
53+
// Convert back to original types for compatibility
54+
const originalObjects = transformedObjects.map(convertTransformedToOriginal);
55+
allObjects.push(...originalObjects);
7756
}
78-
if (minX === Infinity || minY === Infinity || maxX === -Infinity || maxY === -Infinity) {
57+
58+
if (allObjects.length === 0) {
7959
return null;
8060
}
81-
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
82-
}, [inpaintMasks]);
61+
62+
// Calculate bounds from the rendered bitmap for accurate results
63+
const maskBounds = calculateMaskBoundsFromBitmap(allObjects, bboxRect.width, bboxRect.height);
64+
65+
if (!maskBounds) {
66+
return null;
67+
}
68+
69+
// Convert back to world coordinates relative to the bbox
70+
return {
71+
x: bboxRect.x + maskBounds.x,
72+
y: bboxRect.y + maskBounds.y,
73+
width: maskBounds.width,
74+
height: maskBounds.height,
75+
};
76+
}, [inpaintMasks, bboxRect]);
8377

8478
const handleAdjustBbox = useCallback(() => {
8579
const maskBbox = calculateMaskBbox();

invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,10 +1010,7 @@ export const canvasSlice = createSlice({
10101010
return;
10111011
}
10121012

1013-
// For now, we'll use a simple approach: create a full rectangle and add eraser lines
1014-
// This is a temporary solution until we can properly handle the bitmap conversion
1015-
1016-
// Get the bbox dimensions for the mask
1013+
// Get the current bbox dimensions for the mask
10171014
const bboxRect = state.bbox.rect;
10181015

10191016
// Create a full rectangle covering the bbox
@@ -1038,15 +1035,23 @@ export const canvasSlice = createSlice({
10381035
| CanvasBrushLineWithPressureState
10391036
)[] = [fillRect];
10401037

1038+
// Create a clip region that constrains all objects to the bbox
1039+
const bboxClip = {
1040+
x: bboxRect.x - entity.position.x,
1041+
y: bboxRect.y - entity.position.y,
1042+
width: bboxRect.width,
1043+
height: bboxRect.height,
1044+
};
1045+
10411046
for (const obj of entity.objects) {
10421047
if (obj.type === 'brush_line') {
1043-
// Convert brush line to eraser line
1048+
// Convert brush line to eraser line, ensuring it's clipped to the bbox
10441049
const eraserLine: CanvasEraserLineState = {
10451050
id: getPrefixedId('eraser_line'),
10461051
type: 'eraser_line',
10471052
strokeWidth: obj.strokeWidth,
10481053
points: obj.points,
1049-
clip: obj.clip,
1054+
clip: bboxClip, // Always clip to the current bbox
10501055
};
10511056
invertedObjects.push(eraserLine);
10521057
} else if (obj.type === 'brush_line_with_pressure') {
@@ -1056,7 +1061,7 @@ export const canvasSlice = createSlice({
10561061
type: 'eraser_line_with_pressure',
10571062
strokeWidth: obj.strokeWidth,
10581063
points: obj.points,
1059-
clip: obj.clip,
1064+
clip: bboxClip, // Always clip to the current bbox
10601065
};
10611066
invertedObjects.push(eraserLine);
10621067
} else if (obj.type === 'rect') {
@@ -1080,7 +1085,7 @@ export const canvasSlice = createSlice({
10801085
type: 'eraser_line',
10811086
points,
10821087
strokeWidth: Math.max(width, height) / 2, // Use a stroke width that covers the rectangle
1083-
clip: null,
1088+
clip: bboxClip, // Always clip to the current bbox
10841089
};
10851090
invertedObjects.push(eraserLine);
10861091
} else if (obj.type === 'eraser_line') {
@@ -1090,7 +1095,7 @@ export const canvasSlice = createSlice({
10901095
type: 'brush_line',
10911096
strokeWidth: obj.strokeWidth,
10921097
points: obj.points,
1093-
clip: obj.clip,
1098+
clip: bboxClip, // Always clip to the current bbox
10941099
color: { r: 255, g: 255, b: 255, a: 1 },
10951100
};
10961101
invertedObjects.push(brushLine);
@@ -1101,7 +1106,7 @@ export const canvasSlice = createSlice({
11011106
type: 'brush_line_with_pressure',
11021107
strokeWidth: obj.strokeWidth,
11031108
points: obj.points,
1104-
clip: obj.clip,
1109+
clip: bboxClip, // Always clip to the current bbox
11051110
color: { r: 255, g: 255, b: 255, a: 1 },
11061111
};
11071112
invertedObjects.push(brushLine);

0 commit comments

Comments
 (0)