1
+ import { useAppDispatch , useAppSelector } from 'app/store/storeHooks' ;
2
+ import { useAssertSingleton } from 'common/hooks/useAssertSingleton' ;
3
+ import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy' ;
4
+ import { bboxChangedFromCanvas } from 'features/controlLayers/store/canvasSlice' ;
5
+ import { selectMaskBlur } from 'features/controlLayers/store/paramsSlice' ;
6
+ import { selectCanvasSlice } from 'features/controlLayers/store/selectors' ;
7
+ import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData' ;
8
+ import type { Rect } from 'features/controlLayers/store/types' ;
9
+ import { useCallback , useMemo } from 'react' ;
10
+
11
+ export const useCanvasAdjustBboxHotkey = ( ) => {
12
+ useAssertSingleton ( 'useCanvasAdjustBboxHotkey' ) ;
13
+ const dispatch = useAppDispatch ( ) ;
14
+ const canvasSlice = useAppSelector ( selectCanvasSlice ) ;
15
+ const maskBlur = useAppSelector ( selectMaskBlur ) ;
16
+ const isBusy = useCanvasIsBusy ( ) ;
17
+ const inpaintMasks = canvasSlice . inpaintMasks . entities ;
18
+
19
+ // Calculate the bounding box that contains all inpaint masks
20
+ const calculateMaskBbox = useCallback ( ( ) : Rect | null => {
21
+ if ( inpaintMasks . length === 0 ) {
22
+ return null ;
23
+ }
24
+ let minX = Infinity ;
25
+ let minY = Infinity ;
26
+ let maxX = - Infinity ;
27
+ let maxY = - Infinity ;
28
+ for ( const mask of inpaintMasks ) {
29
+ if ( ! mask . isEnabled || ! mask . objects || mask . objects . length === 0 ) {
30
+ continue ;
31
+ }
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
+ }
77
+ }
78
+ if ( minX === Infinity || minY === Infinity || maxX === - Infinity || maxY === - Infinity ) {
79
+ return null ;
80
+ }
81
+ return { x : minX , y : minY , width : maxX - minX , height : maxY - minY } ;
82
+ } , [ inpaintMasks ] ) ;
83
+
84
+ const handleAdjustBbox = useCallback ( ( ) => {
85
+ const maskBbox = calculateMaskBbox ( ) ;
86
+ if ( ! maskBbox ) {
87
+ return ;
88
+ }
89
+
90
+ const padding = maskBlur + 8 ;
91
+ const adjustedBbox : Rect = {
92
+ x : maskBbox . x - padding ,
93
+ y : maskBbox . y - padding ,
94
+ width : maskBbox . width + padding * 2 ,
95
+ height : maskBbox . height + padding * 2 ,
96
+ } ;
97
+
98
+ dispatch ( bboxChangedFromCanvas ( adjustedBbox ) ) ;
99
+ } , [ dispatch , calculateMaskBbox , maskBlur ] ) ;
100
+
101
+ const isAdjustBboxAllowed = useMemo ( ( ) => {
102
+ const hasValidMasks = inpaintMasks . some ( ( mask ) => mask . isEnabled && mask . objects && mask . objects . length > 0 ) ;
103
+ return hasValidMasks ;
104
+ } , [ inpaintMasks ] ) ;
105
+
106
+ useRegisteredHotkeys ( {
107
+ id : 'adjustBbox' ,
108
+ category : 'canvas' ,
109
+ callback : handleAdjustBbox ,
110
+ options : { enabled : isAdjustBboxAllowed && ! isBusy , preventDefault : true } ,
111
+ dependencies : [ isAdjustBboxAllowed , isBusy , handleAdjustBbox ] ,
112
+ } ) ;
113
+ } ;
0 commit comments