@@ -3,7 +3,16 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
3
3
import { bboxChangedFromCanvas } from 'features/controlLayers/store/canvasSlice' ;
4
4
import { selectMaskBlur } from 'features/controlLayers/store/paramsSlice' ;
5
5
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' ;
7
16
import { memo , useCallback , useMemo } from 'react' ;
8
17
import { useTranslation } from 'react-i18next' ;
9
18
import { PiCropBold } from 'react-icons/pi' ;
@@ -14,93 +23,118 @@ export const InpaintMaskBboxAdjuster = memo(() => {
14
23
const canvasSlice = useAppSelector ( selectCanvasSlice ) ;
15
24
const maskBlur = useAppSelector ( selectMaskBlur ) ;
16
25
17
- // Get all inpaint mask entities
26
+ // Get all inpaint mask entities and bbox
18
27
const inpaintMasks = canvasSlice . inpaintMasks . entities ;
28
+ const bboxRect = canvasSlice . bbox . rect ;
19
29
20
30
// Calculate the bounding box that contains all inpaint masks
21
31
const calculateMaskBbox = useCallback ( ( ) : Rect | null => {
22
32
if ( inpaintMasks . length === 0 ) {
23
33
return null ;
24
34
}
25
35
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
+ ) [ ] = [ ] ;
32
49
for ( const mask of inpaintMasks ) {
33
- if ( ! mask . isEnabled || mask . objects . length === 0 ) {
50
+ if ( ! mask . isEnabled || ! mask . objects || mask . objects . length === 0 ) {
34
51
continue ;
35
52
}
36
53
37
- // Calculate bounds for this mask's objects
54
+ // Adjust object positions relative to the bbox (not the entity position)
38
55
for ( const obj of mask . objects ) {
39
- let objMinX = 0 ;
40
- let objMinY = 0 ;
41
- let objMaxX = 0 ;
42
- let objMaxY = 0 ;
43
-
44
56
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 ) ;
49
66
} else if (
50
67
obj . type === 'brush_line' ||
51
68
obj . type === 'brush_line_with_pressure' ||
52
69
obj . type === 'eraser_line' ||
53
70
obj . type === 'eraser_line_with_pressure'
54
71
) {
55
- // For lines, find the min/max points
72
+ const adjustedPoints : number [ ] = [ ] ;
56
73
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 ) ;
69
76
}
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 ) ;
76
82
} 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 ;
82
86
}
87
+ }
88
+ }
83
89
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
+ }
89
116
}
90
117
}
91
118
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 ) {
94
121
return null ;
95
122
}
96
123
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
97
131
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 ,
102
136
} ;
103
- } , [ inpaintMasks ] ) ;
137
+ } , [ inpaintMasks , bboxRect ] ) ;
104
138
105
139
const maskBbox = useMemo ( ( ) => calculateMaskBbox ( ) , [ calculateMaskBbox ] ) ;
106
140
@@ -128,13 +162,13 @@ export const InpaintMaskBboxAdjuster = memo(() => {
128
162
}
129
163
130
164
return (
131
- < Flex w = "full" ps = { 2 } pe = { 2 } pb = { 1 } >
165
+ < Flex w = "full" ps = { 2 } pe = { 2 } pb = { 1 } gap = { 2 } >
132
166
< Button
133
167
size = "sm"
134
168
variant = "ghost"
135
169
leftIcon = { < Icon as = { PiCropBold } boxSize = { 3 } /> }
136
170
onClick = { handleAdjustBbox }
137
- w = "full"
171
+ flex = { 1 }
138
172
justifyContent = "flex-start"
139
173
h = { 6 }
140
174
fontSize = "xs"
0 commit comments