react-native-expo-cropper 1.2.44 → 1.2.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.babelrc +6 -6
- package/App.js +27 -27
- package/app.json +2 -2
- package/babel.config.js +5 -5
- package/dist/CustomCamera.js +8 -4
- package/dist/ImageCropper.js +37 -13
- package/dist/ImageCropperStyles.js +11 -0
- package/dist/ImageMaskProcessor.js +25 -25
- package/dist/ImageProcessor.js +7 -7
- package/index.js +6 -6
- package/package.json +59 -59
- package/src/CustomCamera.js +355 -352
- package/src/ImageCropper.js +1453 -1447
- package/src/ImageCropperStyles.js +255 -244
- package/src/ImageMaskProcessor.js +143 -143
- package/src/ImageProcessor.js +20 -20
package/src/ImageCropper.js
CHANGED
|
@@ -1,1448 +1,1454 @@
|
|
|
1
|
-
import styles from './ImageCropperStyles';
|
|
2
|
-
import React, { useState, useRef, useEffect } from 'react';
|
|
3
|
-
import { Modal, View, Image,
|
|
4
|
-
import Svg, { Path, Circle } from 'react-native-svg';
|
|
5
|
-
import CustomCamera from './CustomCamera';
|
|
6
|
-
import * as ImageManipulator from 'expo-image-manipulator';
|
|
7
|
-
import { Ionicons } from '@expo/vector-icons';
|
|
8
|
-
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
9
|
-
import { applyMaskToImage, MaskView } from './ImageMaskProcessor';
|
|
10
|
-
|
|
11
|
-
const PRIMARY_GREEN = '#198754';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
// ✅
|
|
30
|
-
//
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// ✅
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
// ✅
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
// ✅
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
// ✅
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const [
|
|
134
|
-
const [
|
|
135
|
-
const [
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
width: imgWidth,
|
|
272
|
-
height: imgHeight,
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
originalImageDimensions.current
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
//
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
if (
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
if (
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
// ✅
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
const
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
const
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
const
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
const
|
|
841
|
-
const
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
if
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
// ✅ SAFETY:
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
//
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
//
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
// ✅ CRITICAL:
|
|
1149
|
-
// This
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
//
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
//
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
//
|
|
1196
|
-
//
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
{
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
</
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
scale = actualImageWidth /
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
const
|
|
1356
|
-
const
|
|
1357
|
-
const
|
|
1358
|
-
const
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
};
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1
|
+
import styles from './ImageCropperStyles';
|
|
2
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
3
|
+
import { Modal, View, Image, TouchableOpacity, Animated, Text, Platform, SafeAreaView, PixelRatio, StyleSheet, ActivityIndicator, useWindowDimensions } from 'react-native';
|
|
4
|
+
import Svg, { Path, Circle } from 'react-native-svg';
|
|
5
|
+
import CustomCamera from './CustomCamera';
|
|
6
|
+
import * as ImageManipulator from 'expo-image-manipulator';
|
|
7
|
+
import { Ionicons } from '@expo/vector-icons';
|
|
8
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
9
|
+
import { applyMaskToImage, MaskView } from './ImageMaskProcessor';
|
|
10
|
+
|
|
11
|
+
const PRIMARY_GREEN = '#198754';
|
|
12
|
+
|
|
13
|
+
// Max width for crop preview on large screens (tablets) - must match CustomCamera.js
|
|
14
|
+
const CAMERA_PREVIEW_MAX_WIDTH = 500;
|
|
15
|
+
|
|
16
|
+
const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rotationLabel }) => {
|
|
17
|
+
const { width: windowWidth } = useWindowDimensions();
|
|
18
|
+
const cameraPreviewWidth = Math.min(windowWidth, CAMERA_PREVIEW_MAX_WIDTH);
|
|
19
|
+
|
|
20
|
+
const [image, setImage] = useState(null);
|
|
21
|
+
const [points, setPoints] = useState([]);
|
|
22
|
+
const [showResult, setShowResult] = useState(false);
|
|
23
|
+
const [showCustomCamera, setShowCustomCamera] = useState(false);
|
|
24
|
+
const viewRef = useRef(null);
|
|
25
|
+
const maskViewRef = useRef(null); // Ref pour la vue de masque (invisible)
|
|
26
|
+
const sourceImageUri = useRef(null); // keep original image URI (full-res) for upload
|
|
27
|
+
const cameraFrameData = useRef(null); // ✅ Store green frame coordinates from camera
|
|
28
|
+
|
|
29
|
+
// ✅ RÉFÉRENTIEL UNIQUE : Wrapper commun 9/16 (identique à CustomCamera)
|
|
30
|
+
// Ce wrapper est utilisé dans CustomCamera ET ImageCropper pour garantir pixel-perfect matching
|
|
31
|
+
const commonWrapperRef = useRef(null);
|
|
32
|
+
const commonWrapperLayout = useRef({ x: 0, y: 0, width: 0, height: 0 });
|
|
33
|
+
|
|
34
|
+
// ✅ REFACTORISATION : Séparation claire entre dimensions originales et affichage
|
|
35
|
+
// Dimensions réelles de l'image originale (pixels)
|
|
36
|
+
const originalImageDimensions = useRef({ width: 0, height: 0 });
|
|
37
|
+
// Dimensions et position d'affichage à l'écran (pour le calcul des points de crop)
|
|
38
|
+
const displayedImageLayout = useRef({ x: 0, y: 0, width: 0, height: 0 });
|
|
39
|
+
// Conserver imageMeasure pour compatibilité avec le code existant (utilisé pour SVG overlay)
|
|
40
|
+
const imageMeasure = useRef({ x: 0, y: 0, width: 0, height: 0 });
|
|
41
|
+
// ✅ imageDisplayRect : Rectangle réel de l'image affichée (quand resizeMode='contain') à l'intérieur du wrapper commun
|
|
42
|
+
// C'est la zone où l'image est réellement visible (avec letterboxing si nécessaire)
|
|
43
|
+
// Les points de crop DOIVENT rester dans cette zone pour éviter de cropper hors de l'image
|
|
44
|
+
const imageDisplayRect = useRef({ x: 0, y: 0, width: 0, height: 0 });
|
|
45
|
+
|
|
46
|
+
// ✅ COMPATIBILITÉ : displayedContentRect reste pour le code existant, mais pointe vers imageDisplayRect
|
|
47
|
+
const displayedContentRect = imageDisplayRect;
|
|
48
|
+
|
|
49
|
+
// ✅ RÉFÉRENTIEL UNIQUE : Calculer imageDisplayRect à l'intérieur du wrapper commun
|
|
50
|
+
// - CAMERA: use "cover" mode → image fills wrapper, imageDisplayRect = full wrapper (same as preview)
|
|
51
|
+
// - GALLERY: use "contain" mode → imageDisplayRect = letterboxed area
|
|
52
|
+
const updateImageDisplayRect = (wrapperWidth, wrapperHeight) => {
|
|
53
|
+
const iw = originalImageDimensions.current.width;
|
|
54
|
+
const ih = originalImageDimensions.current.height;
|
|
55
|
+
|
|
56
|
+
// ✅ CAMERA IMAGE: Use full wrapper so green frame and white frame show same content
|
|
57
|
+
if (cameraFrameData.current && cameraFrameData.current.greenFrame && wrapperWidth > 0 && wrapperHeight > 0) {
|
|
58
|
+
imageDisplayRect.current = { x: 0, y: 0, width: wrapperWidth, height: wrapperHeight };
|
|
59
|
+
console.log("✅ Image display rect (COVER mode for camera - full wrapper):", imageDisplayRect.current);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log("🔄 updateImageDisplayRect called:", {
|
|
64
|
+
originalDimensions: { width: iw, height: ih },
|
|
65
|
+
wrapperDimensions: { width: wrapperWidth, height: wrapperHeight }
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (iw > 0 && ih > 0 && wrapperWidth > 0 && wrapperHeight > 0) {
|
|
69
|
+
// Calculer comment l'image s'affiche en "contain" dans le wrapper (gallery)
|
|
70
|
+
const scale = Math.min(wrapperWidth / iw, wrapperHeight / ih);
|
|
71
|
+
const imageDisplayWidth = iw * scale;
|
|
72
|
+
const imageDisplayHeight = ih * scale;
|
|
73
|
+
const offsetX = (wrapperWidth - imageDisplayWidth) / 2;
|
|
74
|
+
const offsetY = (wrapperHeight - imageDisplayHeight) / 2;
|
|
75
|
+
|
|
76
|
+
imageDisplayRect.current = {
|
|
77
|
+
x: offsetX,
|
|
78
|
+
y: offsetY,
|
|
79
|
+
width: imageDisplayWidth,
|
|
80
|
+
height: imageDisplayHeight,
|
|
81
|
+
};
|
|
82
|
+
console.log("✅ Image display rect (contain in wrapper) calculated:", {
|
|
83
|
+
wrapper: { width: wrapperWidth, height: wrapperHeight },
|
|
84
|
+
imageDisplayRect: imageDisplayRect.current,
|
|
85
|
+
scale: scale.toFixed(4)
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (wrapperWidth > 0 && wrapperHeight > 0) {
|
|
91
|
+
imageDisplayRect.current = { x: 0, y: 0, width: wrapperWidth, height: wrapperHeight };
|
|
92
|
+
console.log("⚠️ Using wrapper dimensions as fallback (original dimensions not available yet):", imageDisplayRect.current);
|
|
93
|
+
} else {
|
|
94
|
+
imageDisplayRect.current = { x: 0, y: 0, width: 0, height: 0 };
|
|
95
|
+
console.warn("❌ Cannot calculate imageDisplayRect: missing dimensions");
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// ✅ COMPATIBILITÉ : Alias pour le code existant
|
|
100
|
+
const updateDisplayedContentRect = updateImageDisplayRect;
|
|
101
|
+
|
|
102
|
+
const selectedPointIndex = useRef(null);
|
|
103
|
+
const lastTap = useRef(null);
|
|
104
|
+
|
|
105
|
+
// ✅ CRITICAL: Guard to prevent double crop box initialization
|
|
106
|
+
// This ensures crop box is initialized only once, especially for camera images
|
|
107
|
+
const hasInitializedCropBox = useRef(false);
|
|
108
|
+
const imageSource = useRef(null); // 'camera' | 'gallery' | null
|
|
109
|
+
|
|
110
|
+
// ✅ FREE DRAG: Store initial touch position and point position for delta-based movement
|
|
111
|
+
const initialTouchPosition = useRef(null); // { x, y } - initial touch position when drag starts
|
|
112
|
+
const initialPointPosition = useRef(null); // { x, y } - initial point position when drag starts
|
|
113
|
+
const lastTouchPosition = useRef(null); // { x, y } - last touch position for incremental delta calculation
|
|
114
|
+
|
|
115
|
+
// ✅ CRITICAL: dragBase stores the VISUAL position (can be overshoot) during drag
|
|
116
|
+
// This ensures smooth continuous drag without "dead zones" at boundaries
|
|
117
|
+
// dragBase is updated with applied position (overshoot) after each movement
|
|
118
|
+
// and is used as base for next delta calculation
|
|
119
|
+
const dragBase = useRef(null); // { x, y } - visual position during drag (can be overshoot)
|
|
120
|
+
|
|
121
|
+
// ✅ NEW APPROACH: touchOffset stores the initial offset between point and touch
|
|
122
|
+
// This eliminates delta accumulation issues and "dead zones"
|
|
123
|
+
// Once set at drag start, it remains constant throughout the drag
|
|
124
|
+
const touchOffset = useRef(null); // { x, y } - offset = pointPosition - touchPosition
|
|
125
|
+
|
|
126
|
+
// ✅ Track if point was clamped in previous frame (to detect transition)
|
|
127
|
+
const wasClampedLastFrame = useRef({ x: false, y: false });
|
|
128
|
+
|
|
129
|
+
// Angle de rotation accumulé (pour éviter les rotations multiples)
|
|
130
|
+
const rotationAngle = useRef(0);
|
|
131
|
+
|
|
132
|
+
// États pour la vue de masque temporaire
|
|
133
|
+
const [maskImageUri, setMaskImageUri] = useState(null);
|
|
134
|
+
const [maskPoints, setMaskPoints] = useState([]);
|
|
135
|
+
const [maskDimensions, setMaskDimensions] = useState({ width: 0, height: 0 });
|
|
136
|
+
const [showMaskView, setShowMaskView] = useState(false);
|
|
137
|
+
|
|
138
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
139
|
+
const [showFullScreenCapture, setShowFullScreenCapture] = useState(false);
|
|
140
|
+
const [isRotating, setIsRotating] = useState(false);
|
|
141
|
+
const rotationInProgressRef = useRef(false); // block duplicate taps immediately
|
|
142
|
+
const lastValidPosition = useRef(null);
|
|
143
|
+
const insets = useSafeAreaInsets();
|
|
144
|
+
|
|
145
|
+
// ✅ NEW ARCH: mobile does NOT export the final crop.
|
|
146
|
+
|
|
147
|
+
// No view-shot / captureRef / bitmap masking on device.
|
|
148
|
+
const enableMask = false;
|
|
149
|
+
const enableRotation = true; // rotation resets crop box so it is re-initialized on rotated image.
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
if (openCameraFirst) {
|
|
157
|
+
setShowCustomCamera(true);
|
|
158
|
+
} else if (initialImage) {
|
|
159
|
+
setImage(initialImage);
|
|
160
|
+
sourceImageUri.current = initialImage;
|
|
161
|
+
// ✅ CRITICAL: Reset points when loading a new image from gallery
|
|
162
|
+
// This ensures the crop box will be automatically initialized
|
|
163
|
+
setPoints([]);
|
|
164
|
+
rotationAngle.current = 0;
|
|
165
|
+
// Clear camera frame data for gallery images
|
|
166
|
+
cameraFrameData.current = null;
|
|
167
|
+
// ✅ CRITICAL: Reset initialization guard for new image
|
|
168
|
+
hasInitializedCropBox.current = false;
|
|
169
|
+
imageSource.current = null;
|
|
170
|
+
}
|
|
171
|
+
}, [openCameraFirst, initialImage]);
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
// ✅ REFACTORISATION : Stocker uniquement les dimensions originales (pas de calcul théorique)
|
|
175
|
+
// ✅ CRITICAL FIX: Single source of truth for image dimensions
|
|
176
|
+
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (!image) {
|
|
179
|
+
originalImageDimensions.current = { width: 0, height: 0 };
|
|
180
|
+
hasInitializedCropBox.current = false;
|
|
181
|
+
imageSource.current = null;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (!sourceImageUri.current) {
|
|
185
|
+
sourceImageUri.current = image;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ✅ CRITICAL FIX #1: If we have capturedImageSize from camera, use it as SINGLE SOURCE OF TRUTH
|
|
189
|
+
// takePictureAsync returns physical dimensions, while Image.getSize() may return EXIF-oriented dimensions
|
|
190
|
+
// DO NOT call Image.getSize() for camera images - it can return swapped dimensions on Android
|
|
191
|
+
if (cameraFrameData.current && cameraFrameData.current.capturedImageSize) {
|
|
192
|
+
const { width: capturedWidth, height: capturedHeight } = cameraFrameData.current.capturedImageSize;
|
|
193
|
+
originalImageDimensions.current = {
|
|
194
|
+
width: capturedWidth,
|
|
195
|
+
height: capturedHeight,
|
|
196
|
+
};
|
|
197
|
+
imageSource.current = 'camera';
|
|
198
|
+
hasInitializedCropBox.current = false; // Reset guard for new camera image
|
|
199
|
+
|
|
200
|
+
console.log("✅ Using captured image dimensions from takePictureAsync (SINGLE SOURCE OF TRUTH):", {
|
|
201
|
+
width: capturedWidth,
|
|
202
|
+
height: capturedHeight,
|
|
203
|
+
source: 'takePictureAsync',
|
|
204
|
+
note: 'Image.getSize() will NOT be called for camera images'
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ✅ CRITICAL: Recalculate imageDisplayRect immediately with camera dimensions
|
|
208
|
+
const wrapper = commonWrapperLayout.current;
|
|
209
|
+
if (wrapper.width > 0 && wrapper.height > 0) {
|
|
210
|
+
updateImageDisplayRect(wrapper.width, wrapper.height);
|
|
211
|
+
|
|
212
|
+
// ✅ CRITICAL FIX #2: Initialize crop box immediately when cameraFrameData is available
|
|
213
|
+
// This ensures camera images are initialized from greenFrame BEFORE any other initialization
|
|
214
|
+
if (cameraFrameData.current && cameraFrameData.current.greenFrame && !hasInitializedCropBox.current) {
|
|
215
|
+
console.log("✅ Initializing crop box from cameraFrameData (immediate in useEffect):", {
|
|
216
|
+
hasGreenFrame: !!cameraFrameData.current.greenFrame,
|
|
217
|
+
wrapper: wrapper,
|
|
218
|
+
originalDimensions: originalImageDimensions.current
|
|
219
|
+
});
|
|
220
|
+
initializeCropBox();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return; // ✅ CRITICAL: Exit early - DO NOT call Image.getSize()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ✅ FALLBACK: Use Image.getSize() ONLY for gallery images (no cameraFrameData)
|
|
228
|
+
// BUT: Check again right before calling to avoid race condition
|
|
229
|
+
if (cameraFrameData.current && cameraFrameData.current.capturedImageSize) {
|
|
230
|
+
console.log("⚠️ cameraFrameData exists, skipping Image.getSize() call");
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ✅ CRITICAL: Also check imageSource - if it's 'camera', don't call Image.getSize()
|
|
235
|
+
if (imageSource.current === 'camera') {
|
|
236
|
+
console.log("⚠️ imageSource is 'camera', skipping Image.getSize() call");
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ✅ CRITICAL: Set imageSource to 'gallery' ONLY if we're sure it's not a camera image
|
|
241
|
+
// Don't set it yet - we'll set it in the callback after verifying
|
|
242
|
+
hasInitializedCropBox.current = false; // Reset guard for new image
|
|
243
|
+
|
|
244
|
+
Image.getSize(image, (imgWidth, imgHeight) => {
|
|
245
|
+
// ✅ CRITICAL SAFETY #1: Check if cameraFrameData appeared while Image.getSize() was resolving
|
|
246
|
+
// This is the PRIMARY check - cameraFrameData takes precedence
|
|
247
|
+
if (cameraFrameData.current && cameraFrameData.current.capturedImageSize) {
|
|
248
|
+
console.warn("⚠️ Image.getSize() resolved but cameraFrameData exists - IGNORING Image.getSize() result to prevent dimension swap");
|
|
249
|
+
console.warn("⚠️ Camera dimensions (correct):", cameraFrameData.current.capturedImageSize);
|
|
250
|
+
console.warn("⚠️ Image.getSize() dimensions (potentially swapped):", { width: imgWidth, height: imgHeight });
|
|
251
|
+
return; // ✅ CRITICAL: Exit early - do NOT update dimensions or initialize crop box
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ✅ CRITICAL SAFETY #2: Check imageSource (should be 'camera' if cameraFrameData was set)
|
|
255
|
+
if (imageSource.current === 'camera') {
|
|
256
|
+
console.warn("⚠️ Image.getSize() resolved but imageSource is 'camera' - IGNORING Image.getSize() result");
|
|
257
|
+
console.warn("⚠️ Image.getSize() dimensions (potentially swapped):", { width: imgWidth, height: imgHeight });
|
|
258
|
+
return; // ✅ CRITICAL: Exit early - do NOT update dimensions or initialize crop box
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ✅ CRITICAL SAFETY #3: Check if crop box was already initialized (from camera)
|
|
262
|
+
if (hasInitializedCropBox.current) {
|
|
263
|
+
console.warn("⚠️ Image.getSize() resolved but crop box already initialized - IGNORING result to prevent double initialization");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ✅ SAFE: This is a gallery image, proceed with Image.getSize() result
|
|
268
|
+
imageSource.current = 'gallery';
|
|
269
|
+
|
|
270
|
+
originalImageDimensions.current = {
|
|
271
|
+
width: imgWidth,
|
|
272
|
+
height: imgHeight,
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
console.log("✅ Image dimensions from Image.getSize() (gallery image):", {
|
|
276
|
+
width: imgWidth,
|
|
277
|
+
height: imgHeight,
|
|
278
|
+
platform: Platform.OS,
|
|
279
|
+
pixelRatio: PixelRatio.get(),
|
|
280
|
+
uri: image,
|
|
281
|
+
source: 'Image.getSize()'
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// ✅ RÉFÉRENTIEL UNIQUE : Recalculer imageDisplayRect dans le wrapper commun
|
|
285
|
+
// dès qu'on connaît la taille originale de l'image
|
|
286
|
+
const wrapper = commonWrapperLayout.current;
|
|
287
|
+
if (wrapper.width > 0 && wrapper.height > 0) {
|
|
288
|
+
updateImageDisplayRect(wrapper.width, wrapper.height);
|
|
289
|
+
// ✅ IMPORTANT: pour les images de la galerie (pas de cameraFrameData),
|
|
290
|
+
// initialiser automatiquement le cadre blanc (70% du wrapper) une fois que
|
|
291
|
+
// nous connaissons à la fois le wrapper et les dimensions originales.
|
|
292
|
+
// ✅ CRITICAL: Guard against double initialization
|
|
293
|
+
if (!hasInitializedCropBox.current && points.length === 0 && imageSource.current === 'gallery') {
|
|
294
|
+
initializeCropBox();
|
|
295
|
+
hasInitializedCropBox.current = true;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}, (error) => {
|
|
299
|
+
console.error("Error getting image size:", error);
|
|
300
|
+
});
|
|
301
|
+
}, [image]);
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
// Le cadre blanc doit être calculé sur le MÊME wrapper que le cadre vert (9/16)
|
|
307
|
+
// Ensuite, on restreint les points pour qu'ils restent dans imageDisplayRect (image visible)
|
|
308
|
+
const initializeCropBox = () => {
|
|
309
|
+
// ✅ CRITICAL FIX #2: Guard against double initialization
|
|
310
|
+
if (hasInitializedCropBox.current) {
|
|
311
|
+
console.log("⚠️ Crop box already initialized, skipping duplicate initialization");
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ✅ CRITICAL: Ensure common wrapper layout is available
|
|
316
|
+
const wrapper = commonWrapperLayout.current;
|
|
317
|
+
if (wrapper.width === 0 || wrapper.height === 0) {
|
|
318
|
+
console.warn("Cannot initialize crop box: common wrapper layout not ready");
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ✅ CRITICAL: Ensure imageDisplayRect is available (zone réelle de l'image dans le wrapper)
|
|
323
|
+
let imageRect = imageDisplayRect.current;
|
|
324
|
+
if (imageRect.width === 0 || imageRect.height === 0) {
|
|
325
|
+
// Recalculer si nécessaire
|
|
326
|
+
if (originalImageDimensions.current.width > 0 && originalImageDimensions.current.height > 0) {
|
|
327
|
+
updateImageDisplayRect(wrapper.width, wrapper.height);
|
|
328
|
+
imageRect = imageDisplayRect.current;
|
|
329
|
+
} else {
|
|
330
|
+
console.warn("Cannot initialize crop box: imageDisplayRect not available (original dimensions missing)");
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ✅ CRITICAL FIX: Calculate crop box as percentage of VISIBLE IMAGE AREA (imageDisplayRect)
|
|
336
|
+
// NOT the wrapper. This ensures the crop box is truly 80% of the image, not 80% of wrapper then clamped.
|
|
337
|
+
// Calculate absolute position of imageDisplayRect within wrapper
|
|
338
|
+
const imageRectX = wrapper.x + imageRect.x;
|
|
339
|
+
const imageRectY = wrapper.y + imageRect.y;
|
|
340
|
+
|
|
341
|
+
// ✅ PRIORITY RULE #3: IF image comes from camera → Use EXACT green frame coordinates
|
|
342
|
+
// Image is displayed in "cover" mode (full wrapper), so green frame coords = white frame coords.
|
|
343
|
+
// Store points in WRAPPER-RELATIVE coordinates (greenFrame.x, greenFrame.y) so they match
|
|
344
|
+
// touch events (locationX/locationY are relative to the wrapper). This keeps display→image
|
|
345
|
+
// conversion correct in Confirm.
|
|
346
|
+
if (cameraFrameData.current && cameraFrameData.current.greenFrame && originalImageDimensions.current.width > 0) {
|
|
347
|
+
const greenFrame = cameraFrameData.current.greenFrame;
|
|
348
|
+
|
|
349
|
+
const boxX = greenFrame.x;
|
|
350
|
+
const boxY = greenFrame.y;
|
|
351
|
+
const boxWidth = greenFrame.width;
|
|
352
|
+
const boxHeight = greenFrame.height;
|
|
353
|
+
|
|
354
|
+
// ✅ SAFETY: Validate calculated coordinates before creating points
|
|
355
|
+
const isValidCoordinate = (val) => typeof val === 'number' && isFinite(val) && !isNaN(val);
|
|
356
|
+
|
|
357
|
+
if (!isValidCoordinate(boxX) || !isValidCoordinate(boxY) ||
|
|
358
|
+
!isValidCoordinate(boxWidth) || !isValidCoordinate(boxHeight)) {
|
|
359
|
+
console.warn("⚠️ Invalid coordinates calculated for crop box, skipping initialization");
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ✅ Points in wrapper-relative coords (same as touch events)
|
|
364
|
+
const newPoints = [
|
|
365
|
+
{ x: boxX, y: boxY }, // Top-left
|
|
366
|
+
{ x: boxX + boxWidth, y: boxY }, // Top-right
|
|
367
|
+
{ x: boxX + boxWidth, y: boxY + boxHeight }, // Bottom-right
|
|
368
|
+
{ x: boxX, y: boxY + boxHeight }, // Bottom-left
|
|
369
|
+
];
|
|
370
|
+
|
|
371
|
+
// ✅ SAFETY: Validate all points before setting
|
|
372
|
+
const validPoints = newPoints.filter(p => isValidCoordinate(p.x) && isValidCoordinate(p.y));
|
|
373
|
+
if (validPoints.length !== newPoints.length) {
|
|
374
|
+
console.warn("⚠️ Some points have invalid coordinates, skipping initialization");
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
console.log("✅ Initializing crop box for camera image (COVER MODE - exact green frame):", {
|
|
379
|
+
greenFrame: { x: greenFrame.x, y: greenFrame.y, width: greenFrame.width, height: greenFrame.height },
|
|
380
|
+
whiteFrame: { x: boxX.toFixed(2), y: boxY.toFixed(2), width: boxWidth.toFixed(2), height: boxHeight.toFixed(2) },
|
|
381
|
+
note: "Points in wrapper-relative coords - same as touch events and crop_image_size",
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
setPoints(newPoints);
|
|
385
|
+
hasInitializedCropBox.current = true; // ✅ CRITICAL: Mark as initialized
|
|
386
|
+
// ✅ CRITICAL: DO NOT nullify cameraFrameData here - keep it for Image.getSize() callback check
|
|
387
|
+
// It will be cleared when loading a new image
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ✅ PRIORITY RULE #3: DEFAULT logic ONLY for gallery images (NOT camera)
|
|
392
|
+
// If we reach here and imageSource is 'camera', something went wrong
|
|
393
|
+
if (imageSource.current === 'camera') {
|
|
394
|
+
console.warn("⚠️ Camera image but no greenFrame found - this should not happen");
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ✅ DEFAULT: Crop box (70% of VISIBLE IMAGE AREA - centered) - ONLY for gallery images
|
|
399
|
+
const boxWidth = imageRect.width * 0.70; // 70% of visible image width
|
|
400
|
+
const boxHeight = imageRect.height * 0.70; // 70% of visible image height
|
|
401
|
+
const boxX = imageRectX + (imageRect.width - boxWidth) / 2; // Centered in image area
|
|
402
|
+
const boxY = imageRectY + (imageRect.height - boxHeight) / 2; // Centered in image area
|
|
403
|
+
|
|
404
|
+
// ✅ SAFETY: Validate calculated coordinates before creating points
|
|
405
|
+
const isValidCoordinate = (val) => typeof val === 'number' && isFinite(val) && !isNaN(val);
|
|
406
|
+
|
|
407
|
+
if (!isValidCoordinate(boxX) || !isValidCoordinate(boxY) ||
|
|
408
|
+
!isValidCoordinate(boxWidth) || !isValidCoordinate(boxHeight)) {
|
|
409
|
+
console.warn("⚠️ Invalid coordinates calculated for default crop box, skipping initialization");
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const newPoints = [
|
|
414
|
+
{ x: boxX, y: boxY },
|
|
415
|
+
{ x: boxX + boxWidth, y: boxY },
|
|
416
|
+
{ x: boxX + boxWidth, y: boxY + boxHeight },
|
|
417
|
+
{ x: boxX, y: boxY + boxHeight },
|
|
418
|
+
];
|
|
419
|
+
|
|
420
|
+
// ✅ SAFETY: Validate all points before setting
|
|
421
|
+
const validPoints = newPoints.filter(p => isValidCoordinate(p.x) && isValidCoordinate(p.y));
|
|
422
|
+
if (validPoints.length !== newPoints.length) {
|
|
423
|
+
console.warn("⚠️ Some points have invalid coordinates in default crop box, skipping initialization");
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
console.log("✅ Initializing crop box (default - 70% of visible image area, gallery only):", {
|
|
428
|
+
wrapper: { width: wrapper.width, height: wrapper.height },
|
|
429
|
+
imageDisplayRect: imageRect,
|
|
430
|
+
boxInImage: { x: boxX, y: boxY, width: boxWidth, height: boxHeight },
|
|
431
|
+
points: newPoints
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
setPoints(newPoints);
|
|
435
|
+
hasInitializedCropBox.current = true; // ✅ CRITICAL: Mark as initialized
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
// ✅ RÉFÉRENTIEL UNIQUE : Callback pour mettre à jour le layout du wrapper commun
|
|
439
|
+
// Ce wrapper a exactement les mêmes dimensions que le wrapper de CustomCamera (9/16, width = screenWidth)
|
|
440
|
+
const onCommonWrapperLayout = (e) => {
|
|
441
|
+
const layout = e.nativeEvent.layout;
|
|
442
|
+
commonWrapperLayout.current = {
|
|
443
|
+
x: layout.x,
|
|
444
|
+
y: layout.y,
|
|
445
|
+
width: layout.width,
|
|
446
|
+
height: layout.height
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
console.log("✅ Common wrapper layout updated:", commonWrapperLayout.current);
|
|
450
|
+
|
|
451
|
+
// ✅ Recalculer imageDisplayRect dès que le wrapper est prêt
|
|
452
|
+
if (originalImageDimensions.current.width > 0 && originalImageDimensions.current.height > 0) {
|
|
453
|
+
updateImageDisplayRect(layout.width, layout.height);
|
|
454
|
+
|
|
455
|
+
// ✅ CRITICAL FIX #2: Initialize crop box ONLY if not already initialized
|
|
456
|
+
// For camera images: initialize ONLY from greenFrame (already done when cameraFrameData was set)
|
|
457
|
+
if (!hasInitializedCropBox.current && points.length === 0) {
|
|
458
|
+
// ✅ CRITICAL: Only initialize for gallery images here
|
|
459
|
+
// Camera images should be initialized when cameraFrameData is set, not here
|
|
460
|
+
if (imageSource.current !== 'camera') {
|
|
461
|
+
initializeCropBox();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
// ✅ REFACTORISATION : Mettre à jour les dimensions d'affichage et les dimensions pour SVG
|
|
468
|
+
const onImageLayout = (e) => {
|
|
469
|
+
const layout = e.nativeEvent.layout;
|
|
470
|
+
|
|
471
|
+
// Stocker les dimensions d'affichage réelles (pour conversion de coordonnées)
|
|
472
|
+
displayedImageLayout.current = {
|
|
473
|
+
x: layout.x,
|
|
474
|
+
y: layout.y,
|
|
475
|
+
width: layout.width,
|
|
476
|
+
height: layout.height
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
// Conserver aussi dans imageMeasure pour compatibilité avec SVG overlay
|
|
480
|
+
imageMeasure.current = {
|
|
481
|
+
x: layout.x,
|
|
482
|
+
y: layout.y,
|
|
483
|
+
width: layout.width,
|
|
484
|
+
height: layout.height
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
// ✅ Si l'image vient de la caméra et que les dimensions originales ne sont pas encore définies,
|
|
488
|
+
// les initialiser à partir de cameraFrameData AVANT de calculer le contentRect.
|
|
489
|
+
if (
|
|
490
|
+
originalImageDimensions.current.width === 0 &&
|
|
491
|
+
cameraFrameData.current &&
|
|
492
|
+
cameraFrameData.current.capturedImageSize
|
|
493
|
+
) {
|
|
494
|
+
const { width, height } = cameraFrameData.current.capturedImageSize;
|
|
495
|
+
originalImageDimensions.current = { width, height };
|
|
496
|
+
console.log("✅ originalImageDimensions initialisées depuis cameraFrameData dans onImageLayout:", {
|
|
497
|
+
width,
|
|
498
|
+
height,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ✅ RÉFÉRENTIEL UNIQUE : Recalculer imageDisplayRect dans le wrapper commun
|
|
503
|
+
// Si le wrapper commun n'est pas encore prêt, on attendra onCommonWrapperLayout
|
|
504
|
+
const wrapper = commonWrapperLayout.current;
|
|
505
|
+
if (wrapper.width > 0 && wrapper.height > 0) {
|
|
506
|
+
updateImageDisplayRect(wrapper.width, wrapper.height);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
console.log("Displayed image layout updated:", {
|
|
510
|
+
width: layout.width,
|
|
511
|
+
height: layout.height,
|
|
512
|
+
x: layout.x,
|
|
513
|
+
y: layout.y
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// ✅ CRITICAL FIX #2: Do NOT initialize crop box in onImageLayout for camera images
|
|
517
|
+
// Camera images should be initialized ONLY when cameraFrameData is set (in useEffect)
|
|
518
|
+
// Gallery images can be initialized here if not already done
|
|
519
|
+
if (
|
|
520
|
+
wrapper.width > 0 &&
|
|
521
|
+
wrapper.height > 0 &&
|
|
522
|
+
layout.width > 0 &&
|
|
523
|
+
layout.height > 0 &&
|
|
524
|
+
!hasInitializedCropBox.current &&
|
|
525
|
+
points.length === 0 &&
|
|
526
|
+
originalImageDimensions.current.width > 0 &&
|
|
527
|
+
originalImageDimensions.current.height > 0
|
|
528
|
+
) {
|
|
529
|
+
// ✅ CRITICAL: Only initialize for gallery images here
|
|
530
|
+
// Camera images should be initialized when cameraFrameData is set, not here
|
|
531
|
+
if (imageSource.current !== 'camera') {
|
|
532
|
+
initializeCropBox();
|
|
533
|
+
} else if (cameraFrameData.current && cameraFrameData.current.greenFrame) {
|
|
534
|
+
// ✅ For camera images, initialize ONLY if greenFrame is available
|
|
535
|
+
initializeCropBox();
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
const createPath = () => {
|
|
541
|
+
if (points.length < 1) return '';
|
|
542
|
+
let path = `M ${points[0].x} ${points[0].y} `;
|
|
543
|
+
points.forEach(point => path += `L ${point.x} ${point.y} `);
|
|
544
|
+
return path + 'Z';
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
// ✅ Helper function: Find closest point on a line segment to a tap point
|
|
548
|
+
const findClosestPointOnLine = (tapX, tapY, lineStartX, lineStartY, lineEndX, lineEndY) => {
|
|
549
|
+
const dx = lineEndX - lineStartX;
|
|
550
|
+
const dy = lineEndY - lineStartY;
|
|
551
|
+
const lengthSquared = dx * dx + dy * dy;
|
|
552
|
+
|
|
553
|
+
if (lengthSquared === 0) {
|
|
554
|
+
// Line segment is a point
|
|
555
|
+
return { x: lineStartX, y: lineStartY, distance: Math.sqrt((tapX - lineStartX) ** 2 + (tapY - lineStartY) ** 2) };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Calculate projection parameter t (0 to 1)
|
|
559
|
+
const t = Math.max(0, Math.min(1, ((tapX - lineStartX) * dx + (tapY - lineStartY) * dy) / lengthSquared));
|
|
560
|
+
|
|
561
|
+
// Calculate closest point on line segment
|
|
562
|
+
const closestX = lineStartX + t * dx;
|
|
563
|
+
const closestY = lineStartY + t * dy;
|
|
564
|
+
|
|
565
|
+
// Calculate distance from tap to closest point
|
|
566
|
+
const distance = Math.sqrt((tapX - closestX) ** 2 + (tapY - closestY) ** 2);
|
|
567
|
+
|
|
568
|
+
return { x: closestX, y: closestY, distance, t };
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
// ✅ Helper function: Check if tap is near any line segment and find closest point
|
|
572
|
+
const findClosestPointOnFrame = (tapX, tapY, lineTolerance = 30) => {
|
|
573
|
+
if (points.length < 2) return null;
|
|
574
|
+
|
|
575
|
+
let closestPoint = null;
|
|
576
|
+
let minDistance = Infinity;
|
|
577
|
+
let insertIndex = -1;
|
|
578
|
+
|
|
579
|
+
// Check each line segment (closed polygon: last point connects to first)
|
|
580
|
+
for (let i = 0; i < points.length; i++) {
|
|
581
|
+
const start = points[i];
|
|
582
|
+
const end = points[(i + 1) % points.length];
|
|
583
|
+
|
|
584
|
+
const result = findClosestPointOnLine(tapX, tapY, start.x, start.y, end.x, end.y);
|
|
585
|
+
|
|
586
|
+
if (result.distance < minDistance && result.distance < lineTolerance) {
|
|
587
|
+
minDistance = result.distance;
|
|
588
|
+
closestPoint = { x: result.x, y: result.y };
|
|
589
|
+
// Insert after the start point of this segment
|
|
590
|
+
insertIndex = i + 1;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return closestPoint ? { point: closestPoint, insertIndex } : null;
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
const handleTap = (e) => {
|
|
598
|
+
if (!image || showResult) return;
|
|
599
|
+
const now = Date.now();
|
|
600
|
+
const { locationX: tapX, locationY: tapY } = e.nativeEvent;
|
|
601
|
+
|
|
602
|
+
// ✅ RÉFÉRENTIEL UNIQUE : Utiliser imageDisplayRect (zone réelle de l'image dans le wrapper)
|
|
603
|
+
// Les coordonnées du tap sont relatives au wrapper commun
|
|
604
|
+
let imageRect = imageDisplayRect.current;
|
|
605
|
+
const wrapper = commonWrapperLayout.current;
|
|
606
|
+
|
|
607
|
+
// Recalculate if not available
|
|
608
|
+
if (imageRect.width === 0 || imageRect.height === 0) {
|
|
609
|
+
if (wrapper.width > 0 && wrapper.height > 0 && originalImageDimensions.current.width > 0) {
|
|
610
|
+
updateImageDisplayRect(wrapper.width, wrapper.height);
|
|
611
|
+
imageRect = imageDisplayRect.current;
|
|
612
|
+
}
|
|
613
|
+
// If still not available, use wrapper as fallback
|
|
614
|
+
if (imageRect.width === 0 || imageRect.height === 0) {
|
|
615
|
+
if (wrapper.width > 0 && wrapper.height > 0) {
|
|
616
|
+
imageRect = {
|
|
617
|
+
x: wrapper.x,
|
|
618
|
+
y: wrapper.y,
|
|
619
|
+
width: wrapper.width,
|
|
620
|
+
height: wrapper.height
|
|
621
|
+
};
|
|
622
|
+
} else {
|
|
623
|
+
console.warn("⚠️ Cannot handle tap: wrapper or imageDisplayRect not available");
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// ✅ Clamp to real displayed image content (imageDisplayRect dans le wrapper)
|
|
630
|
+
// Les coordonnées tapX/tapY sont relatives au wrapper commun
|
|
631
|
+
const imageRectX = wrapper.x + imageRect.x;
|
|
632
|
+
const imageRectY = wrapper.y + imageRect.y;
|
|
633
|
+
const { x: cx, y: cy, width: cw, height: ch } = {
|
|
634
|
+
x: imageRectX,
|
|
635
|
+
y: imageRectY,
|
|
636
|
+
width: imageRect.width,
|
|
637
|
+
height: imageRect.height
|
|
638
|
+
};
|
|
639
|
+
// ✅ Larger select radius for easier point selection (especially on touch screens)
|
|
640
|
+
const selectRadius = 50; // Increased from 28 to 50 for better UX
|
|
641
|
+
|
|
642
|
+
// ✅ CRITICAL: Check for existing point selection FIRST (using raw tap coordinates)
|
|
643
|
+
// Don't clamp tapX/Y for point selection - points can be anywhere in wrapper now
|
|
644
|
+
const index = points.findIndex(p => Math.abs(p.x - tapX) < selectRadius && Math.abs(p.y - tapY) < selectRadius);
|
|
645
|
+
|
|
646
|
+
if (index !== -1) {
|
|
647
|
+
// ✅ Point found - select it for dragging
|
|
648
|
+
selectedPointIndex.current = index;
|
|
649
|
+
|
|
650
|
+
// Store initial positions
|
|
651
|
+
initialTouchPosition.current = { x: tapX, y: tapY };
|
|
652
|
+
lastTouchPosition.current = { x: tapX, y: tapY };
|
|
653
|
+
initialPointPosition.current = { ...points[index] };
|
|
654
|
+
lastValidPosition.current = { ...points[index] };
|
|
655
|
+
|
|
656
|
+
// Calculate offset between point and touch at drag start
|
|
657
|
+
touchOffset.current = {
|
|
658
|
+
x: points[index].x - tapX,
|
|
659
|
+
y: points[index].y - tapY
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
console.log("🎯 DRAG START - Offset calculated:", {
|
|
663
|
+
pointX: points[index].x.toFixed(2),
|
|
664
|
+
pointY: points[index].y.toFixed(2),
|
|
665
|
+
touchX: tapX.toFixed(2),
|
|
666
|
+
touchY: tapY.toFixed(2),
|
|
667
|
+
offsetX: touchOffset.current.x.toFixed(2),
|
|
668
|
+
offsetY: touchOffset.current.y.toFixed(2)
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
// Disable parent ScrollView scrolling when dragging
|
|
672
|
+
try {
|
|
673
|
+
const findScrollView = (node) => {
|
|
674
|
+
if (!node) return null;
|
|
675
|
+
if (node._component && node._component.setNativeProps) {
|
|
676
|
+
node._component.setNativeProps({ scrollEnabled: false });
|
|
677
|
+
}
|
|
678
|
+
return findScrollView(node._owner || node._parent);
|
|
679
|
+
};
|
|
680
|
+
} catch (e) {
|
|
681
|
+
// Ignore errors
|
|
682
|
+
}
|
|
683
|
+
} else {
|
|
684
|
+
// ✅ No point found - check if double-tap on a line to create new point
|
|
685
|
+
const isDoubleTap = lastTap.current && now - lastTap.current < 300;
|
|
686
|
+
|
|
687
|
+
if (isDoubleTap && points.length >= 2) {
|
|
688
|
+
// Find closest point on frame lines
|
|
689
|
+
const lineResult = findClosestPointOnFrame(tapX, tapY, 30); // 30px tolerance
|
|
690
|
+
|
|
691
|
+
if (lineResult) {
|
|
692
|
+
const { point, insertIndex } = lineResult;
|
|
693
|
+
|
|
694
|
+
// Check if a point already exists very close to this position
|
|
695
|
+
const exists = points.some(p => Math.abs(p.x - point.x) < selectRadius && Math.abs(p.y - point.y) < selectRadius);
|
|
696
|
+
|
|
697
|
+
if (!exists) {
|
|
698
|
+
// Insert new point at the correct position in the polygon
|
|
699
|
+
const newPoints = [...points];
|
|
700
|
+
newPoints.splice(insertIndex, 0, point);
|
|
701
|
+
setPoints(newPoints);
|
|
702
|
+
|
|
703
|
+
console.log("✅ New point created on frame line:", {
|
|
704
|
+
tap: { x: tapX.toFixed(2), y: tapY.toFixed(2) },
|
|
705
|
+
newPoint: { x: point.x.toFixed(2), y: point.y.toFixed(2) },
|
|
706
|
+
insertIndex,
|
|
707
|
+
totalPoints: newPoints.length
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
lastTap.current = null; // Reset to prevent triple-tap
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
lastTap.current = now;
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
const handleMove = (e) => {
|
|
721
|
+
if (showResult || selectedPointIndex.current === null) return;
|
|
722
|
+
|
|
723
|
+
// ✅ FREE DRAG: Use delta-based movement for smooth, unconstrained dragging
|
|
724
|
+
|
|
725
|
+
const nativeEvent = e.nativeEvent;
|
|
726
|
+
const currentX = nativeEvent.locationX;
|
|
727
|
+
const currentY = nativeEvent.locationY;
|
|
728
|
+
|
|
729
|
+
// ✅ Validate coordinates
|
|
730
|
+
if (currentX === undefined || currentY === undefined || isNaN(currentX) || isNaN(currentY)) {
|
|
731
|
+
console.warn("⚠️ Cannot get touch coordinates", {
|
|
732
|
+
locationX: nativeEvent.locationX,
|
|
733
|
+
locationY: nativeEvent.locationY
|
|
734
|
+
});
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// This is more reliable when ScrollView affects coordinate updates
|
|
739
|
+
let deltaX, deltaY;
|
|
740
|
+
if (lastTouchPosition.current) {
|
|
741
|
+
// Calculate incremental delta from last touch position
|
|
742
|
+
deltaX = currentX - lastTouchPosition.current.x;
|
|
743
|
+
deltaY = currentY - lastTouchPosition.current.y;
|
|
744
|
+
} else if (initialTouchPosition.current) {
|
|
745
|
+
// Fallback to absolute delta if lastTouchPosition not set
|
|
746
|
+
deltaX = currentX - initialTouchPosition.current.x;
|
|
747
|
+
deltaY = currentY - initialTouchPosition.current.y;
|
|
748
|
+
} else {
|
|
749
|
+
console.warn("⚠️ No touch position reference available");
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Les coordonnées de mouvement sont relatives au wrapper commun
|
|
754
|
+
let imageRect = imageDisplayRect.current;
|
|
755
|
+
const wrapper = commonWrapperLayout.current;
|
|
756
|
+
|
|
757
|
+
// Recalculate if not available
|
|
758
|
+
if (imageRect.width === 0 || imageRect.height === 0) {
|
|
759
|
+
if (wrapper.width > 0 && wrapper.height > 0 && originalImageDimensions.current.width > 0) {
|
|
760
|
+
updateImageDisplayRect(wrapper.width, wrapper.height);
|
|
761
|
+
imageRect = imageDisplayRect.current;
|
|
762
|
+
}
|
|
763
|
+
// If still not available, use wrapper as fallback
|
|
764
|
+
if (imageRect.width === 0 || imageRect.height === 0) {
|
|
765
|
+
if (wrapper.width > 0 && wrapper.height > 0) {
|
|
766
|
+
imageRect = {
|
|
767
|
+
x: wrapper.x,
|
|
768
|
+
y: wrapper.y,
|
|
769
|
+
width: wrapper.width,
|
|
770
|
+
height: wrapper.height
|
|
771
|
+
};
|
|
772
|
+
} else {
|
|
773
|
+
console.warn("⚠️ Cannot move point: wrapper or imageDisplayRect not available");
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// ✅ CRITICAL: Calculate absolute bounds of imageDisplayRect within the wrapper
|
|
780
|
+
// imageRect is relative to wrapper, so we need to add wrapper offset
|
|
781
|
+
const imageRectX = wrapper.x + imageRect.x;
|
|
782
|
+
const imageRectY = wrapper.y + imageRect.y;
|
|
783
|
+
const contentRect = {
|
|
784
|
+
x: imageRectX,
|
|
785
|
+
y: imageRectY,
|
|
786
|
+
width: imageRect.width,
|
|
787
|
+
height: imageRect.height
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
// ✅ FREE DRAG: Ensure initial positions are set
|
|
791
|
+
if (!initialPointPosition.current) {
|
|
792
|
+
const currentPoint = points[selectedPointIndex.current];
|
|
793
|
+
if (currentPoint && typeof currentPoint.x === 'number' && typeof currentPoint.y === 'number') {
|
|
794
|
+
initialPointPosition.current = { ...currentPoint };
|
|
795
|
+
} else {
|
|
796
|
+
console.warn("⚠️ No point found for selected index or invalid point data");
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// ✅ NEW APPROACH: Use touchOffset to map touch position directly to point position
|
|
802
|
+
// This eliminates delta accumulation and "dead zone" issues completely
|
|
803
|
+
if (!touchOffset.current) {
|
|
804
|
+
console.warn("⚠️ touchOffset not initialized, cannot move point");
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// ✅ DIRECT MAPPING: newPosition = touchPosition + offset
|
|
809
|
+
// No delta accumulation, no zone morte
|
|
810
|
+
const newX = currentX + touchOffset.current.x;
|
|
811
|
+
const newY = currentY + touchOffset.current.y;
|
|
812
|
+
|
|
813
|
+
// ✅ SEPARATE DRAG BOUNDS vs CROP BOUNDS
|
|
814
|
+
const { x: cx, y: cy, width: cw, height: ch } = contentRect;
|
|
815
|
+
|
|
816
|
+
// ✅ STRICT BOUNDS: For final crop safety (imageDisplayRect)
|
|
817
|
+
const strictMinX = cx;
|
|
818
|
+
const strictMaxX = cx + cw;
|
|
819
|
+
const strictMinY = cy;
|
|
820
|
+
const strictMaxY = cy + ch;
|
|
821
|
+
|
|
822
|
+
// ✅ DRAG BOUNDS: Allow movement ANYWHERE in wrapper during drag
|
|
823
|
+
// Points can move freely across the entire screen for maximum flexibility
|
|
824
|
+
// They will be clamped to imageDisplayRect only on release for safe cropping
|
|
825
|
+
const wrapperRect = wrapper;
|
|
826
|
+
const overshootMinX = wrapperRect.x;
|
|
827
|
+
const overshootMaxX = wrapperRect.x + wrapperRect.width;
|
|
828
|
+
const overshootMinY = wrapperRect.y;
|
|
829
|
+
const overshootMaxY = wrapperRect.y + wrapperRect.height;
|
|
830
|
+
|
|
831
|
+
// ✅ DRAG BOUNDS: Clamp ONLY to overshootBounds during drag (NOT strictBounds)
|
|
832
|
+
const dragX = Math.max(overshootMinX, Math.min(newX, overshootMaxX));
|
|
833
|
+
const dragY = Math.max(overshootMinY, Math.min(newY, overshootMaxY));
|
|
834
|
+
|
|
835
|
+
// ✅ UPDATE POINT: Use drag bounds (overshoot) - allows visual freedom
|
|
836
|
+
const updatedPoint = { x: dragX, y: dragY };
|
|
837
|
+
|
|
838
|
+
// ✅ CRITICAL: Detect if point is AT overshoot boundary (not just clamped)
|
|
839
|
+
// Check if point is exactly at overshootMin/Max (within 1px tolerance)
|
|
840
|
+
const isAtOvershootMinX = Math.abs(dragX - overshootMinX) < 1;
|
|
841
|
+
const isAtOvershootMaxX = Math.abs(dragX - overshootMaxX) < 1;
|
|
842
|
+
const isAtOvershootMinY = Math.abs(dragY - overshootMinY) < 1;
|
|
843
|
+
const isAtOvershootMaxY = Math.abs(dragY - overshootMaxY) < 1;
|
|
844
|
+
|
|
845
|
+
const isAtBoundaryX = isAtOvershootMinX || isAtOvershootMaxX;
|
|
846
|
+
const isAtBoundaryY = isAtOvershootMinY || isAtOvershootMaxY;
|
|
847
|
+
|
|
848
|
+
// Only recalculate offset when FIRST hitting boundary (transition free → boundary)
|
|
849
|
+
const justHitBoundaryX = isAtBoundaryX && !wasClampedLastFrame.current.x;
|
|
850
|
+
const justHitBoundaryY = isAtBoundaryY && !wasClampedLastFrame.current.y;
|
|
851
|
+
|
|
852
|
+
if (justHitBoundaryX || justHitBoundaryY) {
|
|
853
|
+
// Point JUST hit overshoot boundary - recalculate offset once
|
|
854
|
+
const newOffsetX = justHitBoundaryX ? (dragX - currentX) : touchOffset.current.x;
|
|
855
|
+
const newOffsetY = justHitBoundaryY ? (dragY - currentY) : touchOffset.current.y;
|
|
856
|
+
|
|
857
|
+
touchOffset.current = {
|
|
858
|
+
x: newOffsetX,
|
|
859
|
+
y: newOffsetY
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
console.log("✅ OFFSET RECALCULATED (hit boundary):", {
|
|
863
|
+
axis: justHitBoundaryX ? 'X' : 'Y',
|
|
864
|
+
touchY: currentY.toFixed(2),
|
|
865
|
+
dragY: dragY.toFixed(2),
|
|
866
|
+
newOffsetY: touchOffset.current.y.toFixed(2),
|
|
867
|
+
note: "First contact with boundary - offset locked"
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Update boundary state for next frame
|
|
872
|
+
wasClampedLastFrame.current = { x: isAtBoundaryX, y: isAtBoundaryY };
|
|
873
|
+
|
|
874
|
+
// ✅ DEBUG: Log when in overshoot zone (only when not at boundary)
|
|
875
|
+
const isInOvershootY = dragY < strictMinY || dragY > strictMaxY;
|
|
876
|
+
if (isInOvershootY && !isAtBoundaryY) {
|
|
877
|
+
console.log("🎯 IN OVERSHOOT ZONE:", {
|
|
878
|
+
touchY: currentY.toFixed(2),
|
|
879
|
+
appliedY: dragY.toFixed(2),
|
|
880
|
+
overshootRange: `${overshootMinY.toFixed(2)} - ${overshootMaxY.toFixed(2)}`,
|
|
881
|
+
strictRange: `${strictMinY.toFixed(2)} - ${strictMaxY.toFixed(2)}`
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// ✅ Update lastValidPosition ONLY if point is within strictBounds
|
|
886
|
+
const isStrictlyValid =
|
|
887
|
+
dragX >= strictMinX && dragX <= strictMaxX &&
|
|
888
|
+
dragY >= strictMinY && dragY <= strictMaxY;
|
|
889
|
+
|
|
890
|
+
if (isStrictlyValid) {
|
|
891
|
+
lastValidPosition.current = updatedPoint;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// ✅ Update lastTouchPosition for next frame (simple tracking)
|
|
895
|
+
lastTouchPosition.current = { x: currentX, y: currentY };
|
|
896
|
+
|
|
897
|
+
// ✅ DEBUG: Log the point update before setPoints
|
|
898
|
+
console.log("📍 UPDATING POINT:", {
|
|
899
|
+
index: selectedPointIndex.current,
|
|
900
|
+
newX: updatedPoint.x.toFixed(2),
|
|
901
|
+
newY: updatedPoint.y.toFixed(2),
|
|
902
|
+
touchX: currentX.toFixed(2),
|
|
903
|
+
touchY: currentY.toFixed(2),
|
|
904
|
+
offsetX: touchOffset.current.x.toFixed(2),
|
|
905
|
+
offsetY: touchOffset.current.y.toFixed(2)
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
setPoints(prev => {
|
|
909
|
+
// ✅ SAFETY: Ensure prev is a valid array
|
|
910
|
+
if (!Array.isArray(prev) || prev.length === 0) {
|
|
911
|
+
return prev;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const pointIndex = selectedPointIndex.current;
|
|
915
|
+
// ✅ SAFETY: Validate pointIndex
|
|
916
|
+
if (pointIndex === null || pointIndex === undefined || pointIndex < 0 || pointIndex >= prev.length) {
|
|
917
|
+
return prev;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// ✅ SAFETY: Filter out any invalid points and update the selected one
|
|
921
|
+
const newPoints = prev.map((p, i) => {
|
|
922
|
+
if (i === pointIndex) {
|
|
923
|
+
return updatedPoint;
|
|
924
|
+
}
|
|
925
|
+
// ✅ SAFETY: Ensure existing points are valid
|
|
926
|
+
if (p && typeof p.x === 'number' && typeof p.y === 'number') {
|
|
927
|
+
return p;
|
|
928
|
+
}
|
|
929
|
+
// If point is invalid, return a default point (shouldn't happen, but safety first)
|
|
930
|
+
return { x: 0, y: 0 };
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
// ✅ DEBUG: Log the state update
|
|
934
|
+
console.log("✅ STATE UPDATED:", {
|
|
935
|
+
index: pointIndex,
|
|
936
|
+
oldY: prev[pointIndex]?.y.toFixed(2),
|
|
937
|
+
newY: newPoints[pointIndex]?.y.toFixed(2),
|
|
938
|
+
changed: Math.abs(prev[pointIndex]?.y - newPoints[pointIndex]?.y) > 0.01
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
return newPoints;
|
|
942
|
+
});
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
const handleRelease = () => {
|
|
946
|
+
const wasDragging = selectedPointIndex.current !== null;
|
|
947
|
+
|
|
948
|
+
// ✅ CRITICAL: Reset drag state when drag ends
|
|
949
|
+
touchOffset.current = null;
|
|
950
|
+
wasClampedLastFrame.current = { x: false, y: false };
|
|
951
|
+
|
|
952
|
+
// ✅ VISUAL OVERSHOOT: Clamp points back to imageDisplayRect when drag ends
|
|
953
|
+
// This ensures final crop is always within valid image bounds
|
|
954
|
+
if (wasDragging && selectedPointIndex.current !== null) {
|
|
955
|
+
const wrapper = commonWrapperLayout.current;
|
|
956
|
+
let imageRect = imageDisplayRect.current;
|
|
957
|
+
|
|
958
|
+
// Recalculate imageDisplayRect if needed
|
|
959
|
+
if (imageRect.width === 0 || imageRect.height === 0) {
|
|
960
|
+
if (wrapper.width > 0 && wrapper.height > 0 && originalImageDimensions.current.width > 0) {
|
|
961
|
+
updateImageDisplayRect(wrapper.width, wrapper.height);
|
|
962
|
+
imageRect = imageDisplayRect.current;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (imageRect.width > 0 && imageRect.height > 0) {
|
|
967
|
+
const imageRectX = wrapper.x + imageRect.x;
|
|
968
|
+
const imageRectY = wrapper.y + imageRect.y;
|
|
969
|
+
const imageRectMaxX = imageRectX + imageRect.width;
|
|
970
|
+
const imageRectMaxY = imageRectY + imageRect.height;
|
|
971
|
+
|
|
972
|
+
// Clamp the dragged point back to strict image bounds
|
|
973
|
+
setPoints(prev => {
|
|
974
|
+
// ✅ SAFETY: Ensure prev is a valid array
|
|
975
|
+
if (!Array.isArray(prev) || prev.length === 0) {
|
|
976
|
+
return prev;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const pointIndex = selectedPointIndex.current;
|
|
980
|
+
// ✅ SAFETY: Validate pointIndex and ensure point exists
|
|
981
|
+
if (pointIndex === null || pointIndex === undefined || pointIndex < 0 || pointIndex >= prev.length) {
|
|
982
|
+
return prev;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const point = prev[pointIndex];
|
|
986
|
+
// ✅ SAFETY: Ensure point exists and has valid x/y properties
|
|
987
|
+
if (!point || typeof point.x !== 'number' || typeof point.y !== 'number') {
|
|
988
|
+
return prev;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const clampedPoint = {
|
|
992
|
+
x: Math.max(imageRectX, Math.min(point.x, imageRectMaxX)),
|
|
993
|
+
y: Math.max(imageRectY, Math.min(point.y, imageRectMaxY))
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
// Only update if point was outside bounds
|
|
997
|
+
if (point.x !== clampedPoint.x || point.y !== clampedPoint.y) {
|
|
998
|
+
console.log("🔒 Clamping point back to image bounds on release:", {
|
|
999
|
+
before: { x: point.x.toFixed(2), y: point.y.toFixed(2) },
|
|
1000
|
+
after: { x: clampedPoint.x.toFixed(2), y: clampedPoint.y.toFixed(2) },
|
|
1001
|
+
bounds: { minX: imageRectX.toFixed(2), maxX: imageRectMaxX.toFixed(2), minY: imageRectY.toFixed(2), maxY: imageRectMaxY.toFixed(2) }
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
return prev.map((p, i) => i === pointIndex ? clampedPoint : p);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
return prev;
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// ✅ FREE DRAG: Clear initial positions when drag ends
|
|
1013
|
+
initialTouchPosition.current = null;
|
|
1014
|
+
initialPointPosition.current = null;
|
|
1015
|
+
lastValidPosition.current = null;
|
|
1016
|
+
selectedPointIndex.current = null;
|
|
1017
|
+
|
|
1018
|
+
// ✅ CRITICAL: Re-enable parent ScrollView scrolling when drag ends
|
|
1019
|
+
if (wasDragging) {
|
|
1020
|
+
try {
|
|
1021
|
+
// Re-enable scrolling after a short delay to avoid conflicts
|
|
1022
|
+
setTimeout(() => {
|
|
1023
|
+
// ScrollView will be re-enabled automatically when responder is released
|
|
1024
|
+
}, 100);
|
|
1025
|
+
} catch (e) {
|
|
1026
|
+
// Ignore errors
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
const handleReset = () => {
|
|
1032
|
+
// setPoints([]);
|
|
1033
|
+
hasInitializedCropBox.current = false; // ✅ CRITICAL: Reset guard to allow reinitialization
|
|
1034
|
+
initializeCropBox();
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
// ✅ REFACTORISATION : Stocker l'angle de rotation au lieu de modifier l'image immédiatement
|
|
1038
|
+
// La rotation sera appliquée uniquement lors du crop final pour éviter les interpolations multiples
|
|
1039
|
+
const rotatePreviewImage = async (degrees) => {
|
|
1040
|
+
if (!image) return;
|
|
1041
|
+
if (rotationInProgressRef.current) return; // block duplicate taps immediately (no re-render delay)
|
|
1042
|
+
rotationInProgressRef.current = true;
|
|
1043
|
+
setIsRotating(true);
|
|
1044
|
+
|
|
1045
|
+
try {
|
|
1046
|
+
rotationAngle.current = (rotationAngle.current + degrees) % 360;
|
|
1047
|
+
|
|
1048
|
+
// Use JPEG for preview rotation (faster than PNG for large images; quality 0.92 is fine for preview)
|
|
1049
|
+
const rotated = await ImageManipulator.manipulateAsync(
|
|
1050
|
+
image,
|
|
1051
|
+
[{ rotate: degrees }],
|
|
1052
|
+
{
|
|
1053
|
+
compress: 0.92,
|
|
1054
|
+
format: ImageManipulator.SaveFormat.JPEG,
|
|
1055
|
+
}
|
|
1056
|
+
);
|
|
1057
|
+
|
|
1058
|
+
// ✅ Send rotated image to backend: use rotated URI and dimensions so crop bbox matches
|
|
1059
|
+
sourceImageUri.current = rotated.uri;
|
|
1060
|
+
originalImageDimensions.current = {
|
|
1061
|
+
width: rotated.width,
|
|
1062
|
+
height: rotated.height,
|
|
1063
|
+
};
|
|
1064
|
+
cameraFrameData.current = null; // rotated image is no longer "camera preview" frame
|
|
1065
|
+
imageSource.current = 'gallery'; // so layout callbacks run initializeCropBox() and show the white box
|
|
1066
|
+
|
|
1067
|
+
setPoints([]);
|
|
1068
|
+
hasInitializedCropBox.current = false;
|
|
1069
|
+
setImage(rotated.uri);
|
|
1070
|
+
console.log("Rotation applied:", degrees, "degrees; accumulated:", rotationAngle.current);
|
|
1071
|
+
} catch (error) {
|
|
1072
|
+
console.error("Error rotating image:", error);
|
|
1073
|
+
alert("Error rotating image");
|
|
1074
|
+
} finally {
|
|
1075
|
+
rotationInProgressRef.current = false;
|
|
1076
|
+
setIsRotating(false);
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
|
|
1080
|
+
// Helper function to wait for multiple render cycles (works on iOS)
|
|
1081
|
+
const waitForRender = (cycles = 5) => {
|
|
1082
|
+
return new Promise((resolve) => {
|
|
1083
|
+
let count = 0;
|
|
1084
|
+
const tick = () => {
|
|
1085
|
+
count++;
|
|
1086
|
+
if (count >= cycles) {
|
|
1087
|
+
resolve();
|
|
1088
|
+
} else {
|
|
1089
|
+
setImmediate(tick);
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
setImmediate(tick);
|
|
1093
|
+
});
|
|
1094
|
+
};
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
return (
|
|
1098
|
+
<View style={styles.container}>
|
|
1099
|
+
|
|
1100
|
+
{showCustomCamera ? (
|
|
1101
|
+
<CustomCamera
|
|
1102
|
+
onPhotoCaptured={(uri, frameData) => {
|
|
1103
|
+
// ✅ Reset refs for new image so second (and later) photos don't use first image's layout (fixes white screen on some devices)
|
|
1104
|
+
originalImageDimensions.current = { width: 0, height: 0 };
|
|
1105
|
+
imageDisplayRect.current = { x: 0, y: 0, width: 0, height: 0 };
|
|
1106
|
+
displayedImageLayout.current = { x: 0, y: 0, width: 0, height: 0 };
|
|
1107
|
+
imageMeasure.current = { x: 0, y: 0, width: 0, height: 0 };
|
|
1108
|
+
sourceImageUri.current = uri;
|
|
1109
|
+
|
|
1110
|
+
// ✅ CRITICAL FIX: Store green frame coordinates for coordinate conversion
|
|
1111
|
+
if (frameData && frameData.greenFrame) {
|
|
1112
|
+
cameraFrameData.current = {
|
|
1113
|
+
greenFrame: frameData.greenFrame,
|
|
1114
|
+
capturedImageSize: frameData.capturedImageSize
|
|
1115
|
+
};
|
|
1116
|
+
// ✅ CRITICAL: Set imageSource to 'camera' IMMEDIATELY to prevent Image.getSize() from being called
|
|
1117
|
+
imageSource.current = 'camera';
|
|
1118
|
+
hasInitializedCropBox.current = false; // Reset guard for new camera image
|
|
1119
|
+
console.log("✅ Camera frame data received:", cameraFrameData.current);
|
|
1120
|
+
}
|
|
1121
|
+
setImage(uri);
|
|
1122
|
+
setShowCustomCamera(false);
|
|
1123
|
+
// ✅ CORRECTION : Réinitialiser les points et l'angle de rotation quand une nouvelle photo est capturée
|
|
1124
|
+
setPoints([]);
|
|
1125
|
+
rotationAngle.current = 0;
|
|
1126
|
+
// ✅ CRITICAL: initializeCropBox will be called automatically when image layout is ready
|
|
1127
|
+
// The green frame coordinates are stored in cameraFrameData.current and will be used
|
|
1128
|
+
}}
|
|
1129
|
+
onCancel={() => setShowCustomCamera(false)}
|
|
1130
|
+
/>
|
|
1131
|
+
) : (
|
|
1132
|
+
<>
|
|
1133
|
+
{image && (
|
|
1134
|
+
<View
|
|
1135
|
+
style={[
|
|
1136
|
+
styles.commonWrapper,
|
|
1137
|
+
{ width: cameraPreviewWidth },
|
|
1138
|
+
]}
|
|
1139
|
+
ref={commonWrapperRef}
|
|
1140
|
+
onLayout={onCommonWrapperLayout}
|
|
1141
|
+
>
|
|
1142
|
+
<View
|
|
1143
|
+
ref={viewRef}
|
|
1144
|
+
collapsable={false}
|
|
1145
|
+
style={StyleSheet.absoluteFill}
|
|
1146
|
+
onStartShouldSetResponder={() => true}
|
|
1147
|
+
onMoveShouldSetResponder={(evt, gestureState) => {
|
|
1148
|
+
// ✅ CRITICAL: Always capture movement when a point is selected
|
|
1149
|
+
// This ensures vertical movement is captured correctly
|
|
1150
|
+
if (selectedPointIndex.current !== null) {
|
|
1151
|
+
return true;
|
|
1152
|
+
}
|
|
1153
|
+
// ✅ CRITICAL: Capture ANY movement immediately (even 0px) to prevent ScrollView interception
|
|
1154
|
+
// This is especially important for vertical movement which ScrollView tries to intercept
|
|
1155
|
+
// We return true for ANY movement to ensure we capture it before ScrollView
|
|
1156
|
+
const hasMovement = Math.abs(gestureState.dx) > 0 || Math.abs(gestureState.dy) > 0;
|
|
1157
|
+
if (hasMovement && Math.abs(gestureState.dy) > 5) {
|
|
1158
|
+
console.log("🔄 Vertical movement detected in responder:", {
|
|
1159
|
+
dx: gestureState.dx.toFixed(2),
|
|
1160
|
+
dy: gestureState.dy.toFixed(2),
|
|
1161
|
+
selectedPoint: selectedPointIndex.current
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
return true;
|
|
1165
|
+
}}
|
|
1166
|
+
onResponderGrant={(e) => {
|
|
1167
|
+
// ✅ CRITICAL: Grant responder immediately to prevent ScrollView from intercepting
|
|
1168
|
+
// This ensures we capture all movement, especially vertical
|
|
1169
|
+
// Handle tap to select point if needed
|
|
1170
|
+
if (selectedPointIndex.current === null) {
|
|
1171
|
+
handleTap(e);
|
|
1172
|
+
}
|
|
1173
|
+
}}
|
|
1174
|
+
onResponderStart={handleTap}
|
|
1175
|
+
onResponderMove={(e) => {
|
|
1176
|
+
// ✅ CRITICAL: Always handle move events to ensure smooth movement in all directions
|
|
1177
|
+
// This is called for every move event, ensuring vertical movement is captured
|
|
1178
|
+
// handleMove now uses incremental delta calculation which is more reliable
|
|
1179
|
+
handleMove(e);
|
|
1180
|
+
}}
|
|
1181
|
+
onResponderRelease={handleRelease}
|
|
1182
|
+
onResponderTerminationRequest={() => {
|
|
1183
|
+
// ✅ CRITICAL: Never allow termination when dragging a point
|
|
1184
|
+
// This prevents ScrollView from stealing the responder during vertical movement
|
|
1185
|
+
return selectedPointIndex.current === null;
|
|
1186
|
+
}}
|
|
1187
|
+
// ✅ CRITICAL: Prevent parent ScrollView from intercepting touches
|
|
1188
|
+
// Capture responder BEFORE parent ScrollView can intercept
|
|
1189
|
+
onStartShouldSetResponderCapture={() => {
|
|
1190
|
+
// Always capture start events
|
|
1191
|
+
return true;
|
|
1192
|
+
}}
|
|
1193
|
+
onMoveShouldSetResponderCapture={(evt, gestureState) => {
|
|
1194
|
+
// ✅ CRITICAL: Always capture movement events before parent ScrollView
|
|
1195
|
+
// This is essential for vertical movement which ScrollView tries to intercept
|
|
1196
|
+
// Especially important when a point is selected or when there's any movement
|
|
1197
|
+
if (selectedPointIndex.current !== null) {
|
|
1198
|
+
return true;
|
|
1199
|
+
}
|
|
1200
|
+
// ✅ CRITICAL: Capture movement BEFORE ScrollView can intercept
|
|
1201
|
+
// This ensures we get vertical movement even if ScrollView tries to steal it
|
|
1202
|
+
const hasMovement = Math.abs(gestureState.dx) > 0 || Math.abs(gestureState.dy) > 0;
|
|
1203
|
+
return hasMovement;
|
|
1204
|
+
}}
|
|
1205
|
+
>
|
|
1206
|
+
<Image
|
|
1207
|
+
key={image}
|
|
1208
|
+
source={{ uri: image }}
|
|
1209
|
+
style={styles.image}
|
|
1210
|
+
resizeMode={cameraFrameData.current?.greenFrame ? 'cover' : 'contain'}
|
|
1211
|
+
onLayout={onImageLayout}
|
|
1212
|
+
/>
|
|
1213
|
+
{/* ✅ RÉFÉRENTIEL UNIQUE : SVG overlay utilise les dimensions du wrapper commun */}
|
|
1214
|
+
{/* IMPORTANT: prevent SVG overlay from stealing touch events so dragging works reliably */}
|
|
1215
|
+
<Svg style={styles.overlay} pointerEvents="none">
|
|
1216
|
+
{(() => {
|
|
1217
|
+
// ✅ Use wrapper dimensions for SVG path (wrapper coordinates)
|
|
1218
|
+
const wrapperWidth = commonWrapperLayout.current.width || cameraPreviewWidth;
|
|
1219
|
+
const wrapperHeight = commonWrapperLayout.current.height || (cameraPreviewWidth * 16 / 9);
|
|
1220
|
+
return (
|
|
1221
|
+
<>
|
|
1222
|
+
<Path
|
|
1223
|
+
d={`M 0 0 H ${wrapperWidth} V ${wrapperHeight} H 0 Z ${createPath()}`}
|
|
1224
|
+
fill={showResult ? 'white' : 'rgba(0, 0, 0, 0.8)'}
|
|
1225
|
+
fillRule="evenodd"
|
|
1226
|
+
/>
|
|
1227
|
+
{!showResult && points.length > 0 && (
|
|
1228
|
+
<Path d={createPath()} fill="transparent" stroke="white" strokeWidth={2} />
|
|
1229
|
+
)}
|
|
1230
|
+
{!showResult && points.map((point, index) => (
|
|
1231
|
+
<Circle key={index} cx={point.x} cy={point.y} r={10} fill="white" />
|
|
1232
|
+
))}
|
|
1233
|
+
</>
|
|
1234
|
+
);
|
|
1235
|
+
})()}
|
|
1236
|
+
</Svg>
|
|
1237
|
+
</View>
|
|
1238
|
+
{isRotating && (
|
|
1239
|
+
<View style={{ position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'center', alignItems: 'center' }}>
|
|
1240
|
+
<ActivityIndicator size="large" color={PRIMARY_GREEN} />
|
|
1241
|
+
<Text style={{ color: PRIMARY_GREEN, marginTop: 8, fontSize: 14 }}>{rotationLabel ?? 'Rotation...'}</Text>
|
|
1242
|
+
</View>
|
|
1243
|
+
)}
|
|
1244
|
+
</View>
|
|
1245
|
+
)}
|
|
1246
|
+
|
|
1247
|
+
{/* ✅ Buttons positioned BELOW the image, not overlapping */}
|
|
1248
|
+
{!showResult && image && (
|
|
1249
|
+
<View style={[styles.buttonContainerBelow, { paddingBottom: Math.max(insets.bottom, 16) }]}>
|
|
1250
|
+
{Platform.OS === 'android' && (
|
|
1251
|
+
<TouchableOpacity
|
|
1252
|
+
style={[styles.rotationButton, isRotating && { opacity: 0.7 }]}
|
|
1253
|
+
onPress={() => enableRotation && rotatePreviewImage(90)}
|
|
1254
|
+
disabled={isRotating}
|
|
1255
|
+
>
|
|
1256
|
+
{isRotating ? (
|
|
1257
|
+
<ActivityIndicator size="small" color="white" />
|
|
1258
|
+
) : (
|
|
1259
|
+
<Ionicons name="sync" size={24} color="white" />
|
|
1260
|
+
)}
|
|
1261
|
+
</TouchableOpacity>
|
|
1262
|
+
)}
|
|
1263
|
+
|
|
1264
|
+
<TouchableOpacity style={styles.button} onPress={handleReset}>
|
|
1265
|
+
<Text style={styles.buttonText}>Reset</Text>
|
|
1266
|
+
</TouchableOpacity>
|
|
1267
|
+
|
|
1268
|
+
<TouchableOpacity
|
|
1269
|
+
style={styles.button}
|
|
1270
|
+
onPress={async () => {
|
|
1271
|
+
setIsLoading(true);
|
|
1272
|
+
try {
|
|
1273
|
+
console.log("=== Starting pixel-perfect metadata export (no bitmap crop on mobile) ===");
|
|
1274
|
+
|
|
1275
|
+
let actualImageWidth = originalImageDimensions.current.width;
|
|
1276
|
+
let actualImageHeight = originalImageDimensions.current.height;
|
|
1277
|
+
|
|
1278
|
+
// ✅ CRITICAL: Camera JPEGs often have EXIF 6 (90° CW). The Image component displays
|
|
1279
|
+
// the EXIF-corrected view (3120×4160 portrait) but takePictureAsync returns raw (4160×3120).
|
|
1280
|
+
// Use swapped dimensions for coordinate conversion so bbox matches what the user sees.
|
|
1281
|
+
const isCoverMode = !!(cameraFrameData.current && cameraFrameData.current.greenFrame);
|
|
1282
|
+
const captured = cameraFrameData.current?.capturedImageSize;
|
|
1283
|
+
if (isCoverMode && captured && captured.width > captured.height) {
|
|
1284
|
+
actualImageWidth = captured.height;
|
|
1285
|
+
actualImageHeight = captured.width;
|
|
1286
|
+
console.log("✅ Using EXIF-swapped dimensions for bbox (raw was landscape, display is portrait):", {
|
|
1287
|
+
raw: { w: captured.width, h: captured.height },
|
|
1288
|
+
display: { w: actualImageWidth, h: actualImageHeight },
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
if (actualImageWidth === 0 || actualImageHeight === 0) {
|
|
1293
|
+
throw new Error("Original image dimensions not available. Please wait for image to load.");
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
const layout = displayedImageLayout.current;
|
|
1297
|
+
if (layout.width > 0 && layout.height > 0) {
|
|
1298
|
+
updateDisplayedContentRect(layout.width, layout.height);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
let contentRect = displayedContentRect.current;
|
|
1302
|
+
let displayedWidth = contentRect.width;
|
|
1303
|
+
let displayedHeight = contentRect.height;
|
|
1304
|
+
|
|
1305
|
+
if (displayedWidth === 0 || displayedHeight === 0) {
|
|
1306
|
+
if (layout.width > 0 && layout.height > 0) {
|
|
1307
|
+
contentRect = {
|
|
1308
|
+
x: layout.x,
|
|
1309
|
+
y: layout.y,
|
|
1310
|
+
width: layout.width,
|
|
1311
|
+
height: layout.height
|
|
1312
|
+
};
|
|
1313
|
+
displayedWidth = contentRect.width;
|
|
1314
|
+
displayedHeight = contentRect.height;
|
|
1315
|
+
displayedContentRect.current = contentRect;
|
|
1316
|
+
} else {
|
|
1317
|
+
throw new Error("Displayed image dimensions not available.");
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
let scale, coverOffsetX = 0, coverOffsetY = 0;
|
|
1322
|
+
if (isCoverMode) {
|
|
1323
|
+
scale = Math.max(displayedWidth / actualImageWidth, displayedHeight / actualImageHeight);
|
|
1324
|
+
const scaledWidth = actualImageWidth * scale;
|
|
1325
|
+
const scaledHeight = actualImageHeight * scale;
|
|
1326
|
+
coverOffsetX = (scaledWidth - displayedWidth) / 2;
|
|
1327
|
+
coverOffsetY = (scaledHeight - displayedHeight) / 2;
|
|
1328
|
+
} else {
|
|
1329
|
+
scale = actualImageWidth / displayedWidth;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
const originalUri = sourceImageUri.current || image;
|
|
1333
|
+
let cropMeta = null;
|
|
1334
|
+
|
|
1335
|
+
if (points.length > 0) {
|
|
1336
|
+
try {
|
|
1337
|
+
const imagePoints = points.map(point => {
|
|
1338
|
+
let clampedX, clampedY, origX, origY;
|
|
1339
|
+
if (isCoverMode) {
|
|
1340
|
+
clampedX = Math.max(0, Math.min(point.x, contentRect.width));
|
|
1341
|
+
clampedY = Math.max(0, Math.min(point.y, contentRect.height));
|
|
1342
|
+
origX = (clampedX + coverOffsetX) / scale;
|
|
1343
|
+
origY = (clampedY + coverOffsetY) / scale;
|
|
1344
|
+
} else {
|
|
1345
|
+
clampedX = Math.max(contentRect.x, Math.min(point.x, contentRect.x + contentRect.width));
|
|
1346
|
+
clampedY = Math.max(contentRect.y, Math.min(point.y, contentRect.y + contentRect.height));
|
|
1347
|
+
origX = (clampedX - contentRect.x) * scale;
|
|
1348
|
+
origY = (clampedY - contentRect.y) * scale;
|
|
1349
|
+
}
|
|
1350
|
+
const finalX = Math.max(0, Math.min(origX, actualImageWidth));
|
|
1351
|
+
const finalY = Math.max(0, Math.min(origY, actualImageHeight));
|
|
1352
|
+
return { x: finalX, y: finalY };
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
const minX = Math.min(...imagePoints.map(p => p.x));
|
|
1356
|
+
const minY = Math.min(...imagePoints.map(p => p.y));
|
|
1357
|
+
const maxX = Math.max(...imagePoints.map(p => p.x));
|
|
1358
|
+
const maxY = Math.max(...imagePoints.map(p => p.y));
|
|
1359
|
+
|
|
1360
|
+
const cropX = Math.max(0, Math.floor(minX));
|
|
1361
|
+
const cropY = Math.max(0, Math.floor(minY));
|
|
1362
|
+
const cropEndX = Math.min(actualImageWidth, Math.ceil(maxX));
|
|
1363
|
+
const cropEndY = Math.min(actualImageHeight, Math.ceil(maxY));
|
|
1364
|
+
const cropWidth = Math.max(0, cropEndX - cropX);
|
|
1365
|
+
const cropHeight = Math.max(0, cropEndY - cropY);
|
|
1366
|
+
|
|
1367
|
+
if (cropWidth > 0 && cropHeight > 0) {
|
|
1368
|
+
const bbox = { x: cropX, y: cropY, width: cropWidth, height: cropHeight };
|
|
1369
|
+
const polygon = imagePoints.map(point => ({
|
|
1370
|
+
x: point.x - cropX,
|
|
1371
|
+
y: point.y - cropY
|
|
1372
|
+
}));
|
|
1373
|
+
cropMeta = {
|
|
1374
|
+
bbox,
|
|
1375
|
+
polygon,
|
|
1376
|
+
rotation: 0,
|
|
1377
|
+
imageSize: { width: actualImageWidth, height: actualImageHeight },
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
} catch (cropError) {
|
|
1381
|
+
console.error("Error computing crop meta:", cropError);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
const name = `IMAGE XTK${Date.now()}`;
|
|
1386
|
+
if (onConfirm) {
|
|
1387
|
+
onConfirm(originalUri, name, cropMeta);
|
|
1388
|
+
}
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
console.error("Erreur lors du crop :", error);
|
|
1391
|
+
alert("Erreur lors du crop ! " + error.message);
|
|
1392
|
+
} finally {
|
|
1393
|
+
setShowResult(false);
|
|
1394
|
+
setIsLoading(false);
|
|
1395
|
+
setShowFullScreenCapture(false);
|
|
1396
|
+
}
|
|
1397
|
+
}}
|
|
1398
|
+
>
|
|
1399
|
+
<Text style={styles.buttonText}>Confirm</Text>
|
|
1400
|
+
</TouchableOpacity>
|
|
1401
|
+
</View>
|
|
1402
|
+
)}
|
|
1403
|
+
|
|
1404
|
+
{/* ✅ Show welcome screen when no image */}
|
|
1405
|
+
{!showResult && !image && (
|
|
1406
|
+
<View style={styles.centerButtonsContainer}>
|
|
1407
|
+
<Text style={styles.welcomeText}>Sélectionnez une image</Text>
|
|
1408
|
+
</View>
|
|
1409
|
+
)}
|
|
1410
|
+
</>
|
|
1411
|
+
)}
|
|
1412
|
+
|
|
1413
|
+
{/* ✅ CORRECTION : Vue de masque temporaire pour la capture
|
|
1414
|
+
Cette vue est rendue hors écran mais nécessaire pour captureRef
|
|
1415
|
+
Utiliser une position négative mais pas trop éloignée pour éviter les problèmes de captureRef */}
|
|
1416
|
+
{showMaskView && maskImageUri && maskPoints.length > 0 && (
|
|
1417
|
+
<View
|
|
1418
|
+
style={{
|
|
1419
|
+
position: 'absolute',
|
|
1420
|
+
left: -maskDimensions.width - 100,
|
|
1421
|
+
top: -maskDimensions.height - 100,
|
|
1422
|
+
width: maskDimensions.width,
|
|
1423
|
+
height: maskDimensions.height,
|
|
1424
|
+
opacity: 1,
|
|
1425
|
+
pointerEvents: 'none',
|
|
1426
|
+
zIndex: 9999,
|
|
1427
|
+
overflow: 'hidden',
|
|
1428
|
+
}}
|
|
1429
|
+
collapsable={false}
|
|
1430
|
+
>
|
|
1431
|
+
<MaskView
|
|
1432
|
+
ref={maskViewRef}
|
|
1433
|
+
imageUri={maskImageUri}
|
|
1434
|
+
points={maskPoints}
|
|
1435
|
+
width={maskDimensions.width}
|
|
1436
|
+
height={maskDimensions.height}
|
|
1437
|
+
/>
|
|
1438
|
+
</View>
|
|
1439
|
+
)}
|
|
1440
|
+
|
|
1441
|
+
<Modal visible={isLoading} transparent animationType="fade">
|
|
1442
|
+
<View style={styles.loadingOverlay}>
|
|
1443
|
+
<Image
|
|
1444
|
+
source={require('../src/assets/loadingCamera.gif')}
|
|
1445
|
+
style={styles.loadingGif}
|
|
1446
|
+
resizeMode="contain"
|
|
1447
|
+
/>
|
|
1448
|
+
</View>
|
|
1449
|
+
</Modal>
|
|
1450
|
+
</View>
|
|
1451
|
+
);
|
|
1452
|
+
};
|
|
1453
|
+
|
|
1448
1454
|
export default ImageCropper;
|