qtitpc commited on
Commit
73e552c
·
verified ·
1 Parent(s): edbfcdb

Upload 14 files

Browse files
public/file.svg ADDED
public/globe.svg ADDED
public/next.svg ADDED
public/vercel.svg ADDED
public/window.svg ADDED
src/app/DrawingBoard.js ADDED
@@ -0,0 +1,600 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import {
3
+ Undo2,
4
+ Redo2,
5
+ Pencil,
6
+ Hand,
7
+ Square,
8
+ Eraser,
9
+ Sun,
10
+ Moon,
11
+ ZoomIn,
12
+ ZoomOut,
13
+ Grid,
14
+ GripHorizontal,
15
+ RotateCcw,
16
+ Scan,
17
+ Download,
18
+ Menu,
19
+ X,
20
+ Ruler,
21
+ Palette
22
+ } from 'lucide-react';
23
+
24
+ const PRESET_COLORS = [
25
+ '#000000', '#FFFFFF', '#FF4500', '#FFA500',
26
+ '#FF0000', '#FF1493', '#800080', '#4B0082',
27
+ '#008000', '#C0C0C0', '#FFA500', '#4169E1',
28
+ '#00FF00', '#40E0D0', '#0000FF', '#FFD700'
29
+ ];
30
+
31
+
32
+
33
+
34
+ const DrawingBoard = ({
35
+ tool,
36
+ setTool,
37
+ elements,
38
+ pencilSize,
39
+ setPencilSize,
40
+ scale,
41
+ setScale,
42
+ onZoom,
43
+ undo,
44
+ redo,
45
+ action,
46
+ selectedElement,
47
+ panOffset,
48
+ scaleOffset,
49
+ handleMouseDown,
50
+ handleMouseMove,
51
+ handleMouseUp,
52
+ handleBlur,
53
+ captureDrawnArea,
54
+ handleDetectRegions,
55
+ handleDownloadRegions
56
+ }) => {
57
+ const textAreaRef = React.useRef(null);
58
+ const backgroundCanvasRef = React.useRef(null);
59
+ const drawingCanvasRef = React.useRef(null);
60
+ const [backgroundType, setBackgroundType] = React.useState('none');
61
+ const [gridSize, setGridSize] = React.useState(45);
62
+ const [isMenuOpen, setIsMenuOpen] = React.useState(false);
63
+ const [showDownloadButton, setShowDownloadButton] = React.useState(false);
64
+ const [showColorPicker, setShowColorPicker] = React.useState(false);
65
+ const [currentColor, setCurrentColor] = React.useState('#000000');
66
+ const [customColor, setCustomColor] = React.useState('#000000');
67
+ const [darkMode, setDarkMode] = React.useState(() => {
68
+ const savedMode = localStorage.getItem('drawingBoardDarkMode');
69
+ if (savedMode !== null) {
70
+ return savedMode === 'true';
71
+ }
72
+ return window.matchMedia('(prefers-color-scheme: dark)').matches;
73
+ });
74
+
75
+ React.useEffect(() => {
76
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
77
+ const handleChange = (e) => {
78
+ const savedMode = localStorage.getItem('drawingBoardDarkMode');
79
+ if (savedMode === null) {
80
+ setDarkMode(e.matches);
81
+ }
82
+ };
83
+
84
+ mediaQuery.addEventListener('change', handleChange);
85
+ return () => mediaQuery.removeEventListener('change', handleChange);
86
+ }, []);
87
+
88
+ React.useEffect(() => {
89
+ localStorage.setItem('drawingBoardDarkMode', darkMode);
90
+ drawElements();
91
+ }, [darkMode]);
92
+
93
+ const drawBackground = React.useCallback(() => {
94
+ const canvas = backgroundCanvasRef.current;
95
+ if (!canvas) return;
96
+
97
+ const ctx = canvas.getContext('2d');
98
+ const width = canvas.width;
99
+ const height = canvas.height;
100
+
101
+ ctx.fillStyle = darkMode ? '#ffffff' : '#000000';
102
+ ctx.fillRect(0, 0, width, height);
103
+
104
+ const offsetX = (panOffset.x * scale - scaleOffset.x) % (gridSize * scale);
105
+ const offsetY = (panOffset.y * scale - scaleOffset.y) % (gridSize * scale);
106
+ const gridColor = darkMode ? 'rgba(0, 0, 0, 0.6)' : 'rgba(255, 255, 255, 0.6)';
107
+
108
+ if (backgroundType === 'grid') {
109
+ const majorGridSize = gridSize *5;
110
+ const majorGridColor = darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
111
+ const minorGridColor = darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
112
+ ctx.beginPath();
113
+ ctx.strokeStyle = gridColor;
114
+ ctx.lineWidth = 0.5;
115
+
116
+ for (let x = offsetX; x < width; x += gridSize * scale) {
117
+ ctx.moveTo(x, 0);
118
+ ctx.lineTo(x, height);
119
+ }
120
+
121
+ for (let y = offsetY; y < height; y += gridSize * scale) {
122
+ ctx.moveTo(0, y);
123
+ ctx.lineTo(width, y);
124
+ }
125
+
126
+ ctx.stroke();
127
+ ctx.beginPath();
128
+ ctx.strokeStyle = majorGridColor;
129
+ ctx.lineWidth = 1.5;
130
+
131
+ // Tính offset cho grid lớn
132
+ const majorOffsetX = offsetX - (offsetX % (majorGridSize * scale));
133
+ const majorOffsetY = offsetY - (offsetY % (majorGridSize * scale));
134
+ for (let x = majorOffsetX; x < width; x += majorGridSize * scale) {
135
+ ctx.moveTo(x, 0);
136
+ ctx.lineTo(x, height);
137
+ }
138
+
139
+ // Vẽ các đường ngang lớn
140
+ for (let y = majorOffsetY; y < height; y += majorGridSize * scale) {
141
+ ctx.moveTo(0, y);
142
+ ctx.lineTo(width, y);
143
+ }
144
+ ctx.stroke();
145
+ } else if (backgroundType === 'dots') {
146
+ ctx.fillStyle = gridColor;
147
+ const dotSize = 2;
148
+
149
+ for (let x = offsetX; x < width; x += gridSize * scale) {
150
+ for (let y = offsetY; y < height; y += gridSize * scale) {
151
+ ctx.beginPath();
152
+ ctx.arc(x, y, 1, 0, Math.PI * 2);
153
+ ctx.fill();
154
+ }
155
+ }
156
+ }
157
+ }, [backgroundType, gridSize, scale, panOffset, scaleOffset, darkMode]);
158
+ const toggleBackground = () => {
159
+ setBackgroundType(prev => {
160
+ if (prev === 'none') return 'grid';
161
+ if (prev === 'grid') return 'dots';
162
+ return 'none';
163
+ });
164
+ };
165
+ const drawElements = React.useCallback(() => {
166
+ const canvas = drawingCanvasRef.current;
167
+ if (!canvas) return;
168
+
169
+ const ctx = canvas.getContext('2d');
170
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
171
+
172
+ ctx.save();
173
+ ctx.translate(panOffset.x * scale - scaleOffset.x, panOffset.y * scale - scaleOffset.y);
174
+ ctx.scale(scale, scale);
175
+
176
+ elements.forEach(element => {
177
+ if (element.type === 'pencil') {
178
+ ctx.beginPath();
179
+ ctx.strokeStyle = element.color || currentColor;
180
+ ctx.lineWidth = element.size;
181
+ ctx.lineCap = 'round';
182
+ ctx.lineJoin = 'round';
183
+
184
+ element.points.forEach((point, index) => {
185
+ if (index === 0) {
186
+ ctx.moveTo(point.x, point.y);
187
+ } else {
188
+ ctx.lineTo(point.x, point.y);
189
+ }
190
+ });
191
+ ctx.stroke();
192
+ } else if (element.type === 'line') {
193
+ ctx.beginPath();
194
+ ctx.strokeStyle = element.color || currentColor;
195
+ ctx.lineWidth = 2;
196
+ ctx.moveTo(element.x1, element.y1);
197
+ ctx.lineTo(element.x2, element.y2);
198
+ ctx.stroke();
199
+ } else if (element.type === 'rectangle') {
200
+ ctx.strokeStyle = element.color || currentColor;
201
+ ctx.lineWidth = 2;
202
+ ctx.strokeRect(
203
+ element.x1,
204
+ element.y1,
205
+ element.x2 - element.x1,
206
+ element.y2 - element.y1
207
+ );
208
+ } else if (element.type === 'text') {
209
+ ctx.font = '24px sans-serif';
210
+ ctx.fillStyle = element.color || currentColor;
211
+ ctx.fillText(element.text, element.x1, element.y1);
212
+ }
213
+ });
214
+
215
+ ctx.restore();
216
+ }, [elements, scale, panOffset, scaleOffset, currentColor]);
217
+ React.useEffect(() => {
218
+ drawBackground();
219
+ drawElements();
220
+ }, [drawBackground, drawElements]);
221
+ const ColorPickerButton = () => (
222
+ <div className="relative">
223
+ <button
224
+ onClick={() => setShowColorPicker(!showColorPicker)}
225
+ className="p-2 rounded hover:bg-opacity-80 flex items-center gap-1"
226
+ title="Color Picker"
227
+ >
228
+ <div
229
+ className="w-5 h-5 rounded-full border border-gray-300"
230
+ style={{ backgroundColor: currentColor }}
231
+ />
232
+ <Palette size={16} color={iconColor} />
233
+ </button>
234
+ {showColorPicker && <ColorPicker />}
235
+ </div>
236
+ );
237
+
238
+ React.useEffect(() => {
239
+ const handleResize = () => {
240
+ if (backgroundCanvasRef.current && drawingCanvasRef.current) {
241
+ backgroundCanvasRef.current.width = window.innerWidth;
242
+ backgroundCanvasRef.current.height = window.innerHeight;
243
+ drawingCanvasRef.current.width = window.innerWidth;
244
+ drawingCanvasRef.current.height = window.innerHeight;
245
+ drawBackground();
246
+ drawElements();
247
+ }
248
+ };
249
+
250
+ window.addEventListener('resize', handleResize);
251
+ handleResize();
252
+ return () => window.removeEventListener('resize', handleResize);
253
+ }, [drawBackground, drawElements]);
254
+
255
+ const toggleDarkMode = () => {
256
+ setDarkMode(prev => !prev);
257
+ };
258
+
259
+ const handleDetectAndShowDownload = () => {
260
+ handleDetectRegions();
261
+ setShowDownloadButton(true);
262
+ setIsMenuOpen(false);
263
+ };
264
+
265
+ const iconColor = darkMode ? "#000000" : "#ffffff"; // Đảo ngược màu icon
266
+ const barBgColor = darkMode ? "bg-white" : "bg-gray-800"; // Giữ nguyên màu nền
267
+ const textColor = darkMode ? 'text-black' : 'text-white'; // Đảo ngược màu text
268
+ const handleColorChange =(color)=>{
269
+ setCurrentColor(color);
270
+ setShowColorPicker(false);
271
+ };
272
+ const handleCustomColorChange = (e) =>{
273
+ const newColor = e.target.value;
274
+ setCustomColor(newColor);
275
+ setCurrentColor(newColor);
276
+ };
277
+ const ColorPicker = () => (
278
+ <div className={`absolute top-full mt-2 p-2 ${barBgColor} rounded-lg shadow-lg`}>
279
+ <div className="grid grid-cols-4 gap-2 mb-2">
280
+ {PRESET_COLORS.map((color, index) => (
281
+ <button
282
+ key={index}
283
+ onClick={() => handleColorChange(color)}
284
+ className="w-6 h-6 rounded-full border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500"
285
+ style={{
286
+ backgroundColor: color,
287
+ border: currentColor === color ? '2px solid #3B82F6' : '1px solid #D1D5DB'
288
+ }}
289
+ />
290
+ ))}
291
+ </div>
292
+ <div className="flex items-center gap-2 mt-2 pt-2 border-t border-gray-600">
293
+ <input
294
+ type="color"
295
+ value={customColor}
296
+ onChange={handleCustomColorChange}
297
+ className="w-8 h-8 p-0 border-0 rounded cursor-pointer"
298
+ />
299
+ <input
300
+ type="text"
301
+ value={customColor.toUpperCase()}
302
+ onChange={(e) => {
303
+ const val = e.target.value;
304
+ if (/^#[0-9A-F]{0,6}$/i.test(val)) {
305
+ setCustomColor(val);
306
+ if (val.length === 7) setCurrentColor(val);
307
+ }
308
+ }}
309
+ className={`w-20 px-1 py-0.5 text-sm rounded ${
310
+ darkMode ? 'bg-gray-700 text-white' : 'bg-white text-black'
311
+ }`}
312
+ placeholder="#000000"
313
+ />
314
+ </div>
315
+ </div>
316
+ );
317
+ return (
318
+ <div className={`relative w-full h-screen ${darkMode ? 'bg-gray-900' : 'bg-white'}`}>
319
+ {/* Corner Menu Button */}
320
+ <button
321
+ onClick={() => setIsMenuOpen(!isMenuOpen)}
322
+ className={`fixed top-4 left-4 z-20 p-2 ${barBgColor} rounded-lg shadow-lg hover:bg-opacity-80`}
323
+ >
324
+ {isMenuOpen ?
325
+ <X size={24} color={iconColor} /> :
326
+ <Menu size={24} color={iconColor} />
327
+ }
328
+ </button>
329
+
330
+ {/* Corner Menu Panel */}
331
+ {isMenuOpen && (
332
+ <div className={`fixed top-16 left-4 z-20 ${barBgColor} rounded-lg shadow-lg p-4 w-64`}>
333
+ <div className="flex flex-col gap-4">
334
+ {/* Theme Toggle */}
335
+ <button
336
+ onClick={toggleDarkMode}
337
+ className={`w-full flex items-center justify-between p-2 rounded hover:bg-opacity-80 ${darkMode ? 'hover:bg-gray-700' : 'hover:bg-gray-100'}`}
338
+ >
339
+ <span className={`text-sm ${darkMode ? 'text-gray-600' : 'text-white'}`}>
340
+ {darkMode ? 'Light Mode' : 'Dark Mode'}
341
+ </span>
342
+ {darkMode ?
343
+ <Sun size={20} color={iconColor} /> :
344
+ <Moon size={20} color={iconColor} />
345
+ }
346
+ </button>
347
+
348
+ {/* Grid Toggle */}
349
+ <button
350
+ onClick={() => setBackgroundType(prev => prev === 'grid' ? 'none' : 'grid')}
351
+ className={`w-full flex items-center justify-between p-2 rounded hover:bg-opacity-80 ${darkMode ? 'hover:bg-gray-700' : 'hover:bg-gray-100'}`}
352
+ >
353
+ <span className={`text-sm ${darkMode ? 'text-gray-600' : 'text-white'}`}>
354
+ Show Grid
355
+ </span>
356
+ <Grid
357
+ size={20}
358
+ color={iconColor}
359
+ className={backgroundType === 'grid' ? 'opacity-100' : 'opacity-50'}
360
+ />
361
+ </button>
362
+ <button
363
+ onClick={toggleBackground}
364
+ className={`w-full flex items-center justify-between p-2 rounded hover:bg-opacity-80 ${
365
+ darkMode ? 'hover:bg-gray-700' : 'hover:bg-gray-100'
366
+ }`}
367
+ >
368
+ <span className={`text-sm ${darkMode ? 'text-black' : 'text-white'}`}>
369
+ {backgroundType === 'none' ? 'No Grid' :
370
+ backgroundType === 'grid' ? 'Grid Lines' : 'Grid Dots'}
371
+ </span>
372
+ <Grid
373
+ size={20}
374
+ color={iconColor}
375
+ className={backgroundType !== 'none' ? 'opacity-100' : 'opacity-50'}
376
+ />
377
+ </button>
378
+
379
+ {backgroundType !== 'none' && (
380
+ <div className="flex flex-col gap-2 p-2">
381
+ <div className="flex items-center justify-between">
382
+ <span className={`text-sm ${darkMode ? 'text-black' : 'text-white'}`}>
383
+ Grid Size
384
+ </span>
385
+ <span className={`text-sm ${darkMode ? 'text-black' : 'text-white'}`}>
386
+ {gridSize}px
387
+ </span>
388
+ </div>
389
+ <input
390
+ type="range"
391
+ min="10"
392
+ max="100"
393
+ value={gridSize}
394
+ onChange={(e) => setGridSize(parseInt(e.target.value))}
395
+ className="w-full h-1 bg-gray-300 rounded-lg appearance-none cursor-pointer"
396
+ />
397
+ </div>
398
+ )}
399
+ <style jsx global>{`
400
+ input[type="range"] {
401
+ -webkit-appearance: none;
402
+ height: 4px;
403
+ background: ${darkMode ? '#ffffff40' : '#00000040'};
404
+ border-radius: 2px;
405
+ outline: none;
406
+ }
407
+
408
+ input[type="range"]::-webkit-slider-thumb {
409
+ -webkit-appearance: none;
410
+ appearance: none;
411
+ width: 12px;
412
+ height: 12px;
413
+ background: ${darkMode ? '#ffffff' : '#000000'};
414
+ border-radius: 50%;
415
+ cursor: pointer;
416
+ }
417
+
418
+ input[type="range"]::-moz-range-thumb {
419
+ width: 12px;
420
+ height: 12px;
421
+ background: ${darkMode ? '#ffffff' : '#000000'};
422
+ border-radius: 50%;
423
+ cursor: pointer;
424
+ border: none;
425
+ }
426
+
427
+ input[type="range"]:hover::-webkit-slider-thumb {
428
+ background: #3b82f6;
429
+ }
430
+
431
+ input[type="range"]:hover::-moz-range-thumb {
432
+ background: #3b82f6;
433
+ }
434
+ `}</style>
435
+ {/* Region Detection */}
436
+ <button
437
+ onClick={handleDetectAndShowDownload}
438
+ className={`w-full flex items-center justify-between p-2 rounded hover:bg-opacity-80 ${darkMode ? 'hover:bg-gray-700' : 'hover:bg-gray-100'}`}
439
+ >
440
+ <span className={`text-sm ${darkMode ? 'text-gray-600' : 'text-white'}`}>
441
+ Detect Regions
442
+ </span>
443
+ <Scan size={20} color={iconColor} />
444
+ </button>
445
+ </div>
446
+ </div>
447
+ )}
448
+
449
+ {/* Main Toolbar */}
450
+ <div className={`fixed top-4 left-1/2 transform -translate-x-1/2 flex items-center gap-1 ${barBgColor} rounded-lg shadow-lg p-2 z-10`}>
451
+ <div className="flex items-center gap-1 pr-2 border-r border-gray-200">
452
+ <button onClick={undo} className="p-2 rounded hover:bg-opacity-80" title="Undo">
453
+ <Undo2 size={20} color={iconColor} />
454
+ </button>
455
+ <button onClick={redo} className="p-2 rounded hover:bg-opacity-80" title="Redo">
456
+ <Redo2 size={20} color={iconColor} />
457
+ </button>
458
+ </div>
459
+
460
+ <div className="flex items-center gap-1 px-2">
461
+ <ColorPickerButton />
462
+ <button
463
+ onClick={() => setTool('selection')}
464
+ className={`p-2 rounded ${tool === 'selection' ? 'bg-blue-500' : 'hover:bg-opacity-80'}`}
465
+ title="Selection"
466
+ >
467
+ <Hand size={20} color={tool === 'selection' ? '#ffffff' : iconColor} />
468
+ </button>
469
+ <button
470
+ onClick={() => setTool('line')}
471
+ className={`p-2 rounded ${tool === 'line' ? 'bg-blue-500' : 'hover:bg-opacity-80'}`}
472
+ title="Line"
473
+ >
474
+ <GripHorizontal size={20} color={tool === 'line' ? '#ffffff' : iconColor} />
475
+ </button>
476
+ <button
477
+ onClick={() => setTool('rectangle')}
478
+ className={`p-2 rounded ${tool === 'rectangle' ? 'bg-blue-500' : 'hover:bg-opacity-80'}`}
479
+ title="Rectangle"
480
+ >
481
+ <Square size={20} color={tool === 'rectangle' ? '#ffffff' : iconColor} />
482
+ </button>
483
+ <button
484
+ onClick={() => setTool('pencil')}
485
+ className={`p-2 rounded ${tool === 'pencil' ? 'bg-blue-500' : 'hover:bg-opacity-80'}`}
486
+ title="Pencil"
487
+ >
488
+ <Pencil size={20} color={tool === 'pencil' ? '#ffffff' : iconColor} />
489
+ </button>
490
+ <button
491
+ onClick={() => setTool('eraser')}
492
+ className={`p-2 rounded ${tool === 'eraser' ? 'bg-blue-500' : 'hover:bg-opacity-80'}`}
493
+ title="Eraser"
494
+ >
495
+ <Eraser size={20} color={tool === 'eraser' ? '#ffffff' : iconColor} />
496
+ </button>
497
+ {(tool === 'pencil') && (
498
+ <div className=' flex items-center gap-2 ml-2'>
499
+ <Ruler size={16} color={iconColor} />
500
+ <input
501
+ type="range"
502
+ min="1"
503
+ max="20"
504
+ value={pencilSize}
505
+ onChange={(e) => setPencilSize(parseInt(e.target.value))}
506
+ className="w-24 h-1 bg-gray-300 rounded-lg appearance-none cursor-pointer"
507
+ title="Pencil Size"
508
+ />
509
+ <span className={'text-sm ${textColor}'}>{pencilSize}px</span>
510
+ </div>
511
+ )}
512
+ </div>
513
+
514
+ <div className="flex items-center gap-1 pl-2 border-l border-gray-200">
515
+ <button
516
+ onClick={() => setScale(1)}
517
+ className="p-2 rounded hover:bg-opacity-80"
518
+ title="Reset View"
519
+ >
520
+ <RotateCcw size={20} color={iconColor} />
521
+ </button>
522
+ <button
523
+ onClick={() => onZoom(-0.1)}
524
+ className="p-2 rounded hover:bg-opacity-80"
525
+ title="Zoom Out"
526
+ >
527
+ <ZoomOut size={20} color={iconColor} />
528
+ </button>
529
+ <span className={`text-sm min-w-[4rem] text-center ${textColor}`}>
530
+ {Math.round(scale * 100)}%
531
+ </span>
532
+ <button
533
+ onClick={() => onZoom(0.1)}
534
+ className="p-2 rounded hover:bg-opacity-80"
535
+ title="Zoom In"
536
+ >
537
+ <ZoomIn size={20} color={iconColor} />
538
+ </button>
539
+ </div>
540
+ </div>
541
+
542
+ {/* Download Button */}
543
+ {showDownloadButton && (
544
+ <button
545
+ onClick={() => {
546
+ handleDownloadRegions();
547
+ setShowDownloadButton(false);
548
+ }}
549
+ className="fixed bottom-8 left-1/2 transform -translate-x-1/2 z-20 px-4 py-2 bg-blue-500 text-white rounded-lg shadow-lg hover:bg-blue-600 flex items-center gap-2"
550
+ >
551
+ <Download size={20} color="#ffffff" />
552
+ Download Detected Regions
553
+ </button>
554
+ )}
555
+
556
+
557
+ {/* Canvas Container */}
558
+ <div className="absolute inset-0">
559
+ <canvas
560
+ ref={backgroundCanvasRef}
561
+ className="absolute inset-0 z-0"
562
+ />
563
+ <canvas
564
+ id="canvas"
565
+ ref={drawingCanvasRef}
566
+ onMouseDown={handleMouseDown}
567
+ onMouseMove={handleMouseMove}
568
+ onMouseUp={handleMouseUp}
569
+ className="absolute inset-0 z-1"
570
+ />
571
+ </div>
572
+
573
+ {/* Text Area for Writing */}
574
+ {action === "writing" && (
575
+ <textarea
576
+ ref={textAreaRef}
577
+ onBlur={handleBlur}
578
+ style={{
579
+ position: "fixed",
580
+ top: (selectedElement.y1 - 2) * scale + panOffset.y * scale - scaleOffset.y,
581
+ left: selectedElement.x1 * scale + panOffset.x * scale - scaleOffset.x,
582
+ font: `${24 * scale}px sans-serif`,
583
+ margin: 0,
584
+ padding: 0,
585
+ border: 0,
586
+ outline: 0,
587
+ resize: "auto",
588
+ overflow: "hidden",
589
+ whiteSpace: "pre",
590
+ background: "transparent",
591
+ color: darkMode ? 'white' : 'black',
592
+ zIndex: 2,
593
+ }}
594
+ />
595
+ )}
596
+ </div>
597
+ );
598
+ };
599
+
600
+ export default DrawingBoard;
src/app/element-utils.js ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import rough from "roughjs/bundled/rough.esm";
2
+ import getStroke from "perfect-freehand";
3
+
4
+ const generator = rough.generator();
5
+
6
+ export const createElement = (id, x1, y1, x2, y2, type, pencilSize = 3) => {
7
+ switch (type) {
8
+ case "line":
9
+ case "rectangle":
10
+ const roughElement =
11
+ type === "line"
12
+ ? generator.line(x1, y1, x2, y2)
13
+ : generator.rectangle(x1, y1, x2 - x1, y2 - y1);
14
+ return { id, x1, y1, x2, y2, type, roughElement };
15
+ case "pencil":
16
+ return { id, type, points: [{ x: x1, y: y1, isErased: false }], size: pencilSize };
17
+ case "text":
18
+ return { id, type, x1, y1, x2, y2, text: "" };
19
+ case "capture":
20
+ return { id, type, x1, y1, x2, y2 };
21
+ default:
22
+ throw new Error(`Type not recognised: ${type}`);
23
+ }
24
+ };
25
+
26
+ export const isPointNearEraser = (x, y, eraserX, eraserY, eraserSize) => {
27
+ const distance = Math.sqrt(Math.pow(x - eraserX, 2) + Math.pow(y - eraserY, 2));
28
+ return distance <= eraserSize;
29
+ };
30
+
31
+ const nearPoint = (x, y, x1, y1, name) => {
32
+ return Math.abs(x - x1) < 5 && Math.abs(y - y1) < 5 ? name : null;
33
+ };
34
+
35
+ const onLine = (x1, y1, x2, y2, x, y, maxDistance = 1) => {
36
+ const a = { x: x1, y: y1 };
37
+ const b = { x: x2, y: y2 };
38
+ const c = { x, y };
39
+ const offset = distance(a, b) - (distance(a, c) + distance(b, c));
40
+ return Math.abs(offset) < maxDistance ? "inside" : null;
41
+ };
42
+
43
+ const positionWithinElement = (x, y, element) => {
44
+ const { type, x1, x2, y1, y2 } = element;
45
+ switch (type) {
46
+ case "line":
47
+ const on = onLine(x1, y1, x2, y2, x, y);
48
+ const start = nearPoint(x, y, x1, y1, "start");
49
+ const end = nearPoint(x, y, x2, y2, "end");
50
+ return start || end || on;
51
+ case "rectangle":
52
+ const topLeft = nearPoint(x, y, x1, y1, "tl");
53
+ const topRight = nearPoint(x, y, x2, y1, "tr");
54
+ const bottomLeft = nearPoint(x, y, x1, y2, "bl");
55
+ const bottomRight = nearPoint(x, y, x2, y2, "br");
56
+ const inside = x >= x1 && x <= x2 && y >= y1 && y <= y2 ? "inside" : null;
57
+ return topLeft || topRight || bottomLeft || bottomRight || inside;
58
+ case "pencil":
59
+ const betweenAnyPoint = element.points.some((point, index) => {
60
+ const nextPoint = element.points[index + 1];
61
+ if (!nextPoint) return false;
62
+ return onLine(point.x, point.y, nextPoint.x, nextPoint.y, x, y, 5) != null;
63
+ });
64
+ return betweenAnyPoint ? "inside" : null;
65
+ case "text":
66
+ return x >= x1 && x <= x2 && y >= y1 && y <= y2 ? "inside" : null;
67
+ default:
68
+ throw new Error(`Type not recognised: ${type}`);
69
+ }
70
+ };
71
+
72
+ export const distance = (a, b) => Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
73
+
74
+ export const getElementAtPosition = (x, y, elements) => {
75
+ return elements
76
+ .map(element => ({ ...element, position: positionWithinElement(x, y, element) }))
77
+ .find(element => element.position !== null);
78
+ };
79
+
80
+ export const adjustElementCoordinates = element => {
81
+ const { type, x1, y1, x2, y2 } = element;
82
+ if (type === "rectangle") {
83
+ const minX = Math.min(x1, x2);
84
+ const maxX = Math.max(x1, x2);
85
+ const minY = Math.min(y1, y2);
86
+ const maxY = Math.max(y1, y2);
87
+ return { x1: minX, y1: minY, x2: maxX, y2: maxY };
88
+ } else {
89
+ if (x1 < x2 || (x1 === x2 && y1 < y2)) {
90
+ return { x1, y1, x2, y2 };
91
+ } else {
92
+ return { x1: x2, y1: y2, x2: x1, y2: y1 };
93
+ }
94
+ }
95
+ };
96
+
97
+ export const cursorForPosition = position => {
98
+ switch (position) {
99
+ case "tl":
100
+ case "br":
101
+ case "start":
102
+ case "end":
103
+ return "nwse-resize";
104
+ case "tr":
105
+ case "bl":
106
+ return "nesw-resize";
107
+ default:
108
+ return "move";
109
+ }
110
+ };
111
+
112
+ export const resizedCoordinates = (clientX, clientY, position, coordinates) => {
113
+ const { x1, y1, x2, y2 } = coordinates;
114
+ switch (position) {
115
+ case "tl":
116
+ case "start":
117
+ return { x1: clientX, y1: clientY, x2, y2 };
118
+ case "tr":
119
+ return { x1, y1: clientY, x2: clientX, y2 };
120
+ case "bl":
121
+ return { x1: clientX, y1, x2, y2: clientY };
122
+ case "br":
123
+ case "end":
124
+ return { x1, y1, x2: clientX, y2: clientY };
125
+ default:
126
+ return null;
127
+ }
128
+ };
129
+
130
+ export const getSvgPathFromStroke = stroke => {
131
+ if (!stroke.length) return "";
132
+
133
+ const d = stroke.reduce(
134
+ (acc, [x0, y0], i, arr) => {
135
+ const [x1, y1] = arr[(i + 1) % arr.length];
136
+ acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2);
137
+ return acc;
138
+ },
139
+ ["M", ...stroke[0], "Q"]
140
+ );
141
+
142
+ d.push("Z");
143
+ return d.join(" ");
144
+ };
145
+
146
+ export const drawElement = (roughCanvas, context, element) => {
147
+ switch (element.type) {
148
+ case "line":
149
+ case "rectangle":
150
+ roughCanvas.draw(element.roughElement);
151
+ break;
152
+ case "pencil":
153
+ const nonErasedSegments = [];
154
+ let currentSegment = [];
155
+
156
+ element.points.forEach((point, index) => {
157
+ if (!point.isErased) {
158
+ currentSegment.push([point.x, point.y]);
159
+ } else if (currentSegment.length > 0) {
160
+ nonErasedSegments.push([...currentSegment]);
161
+ currentSegment = [];
162
+ }
163
+ });
164
+
165
+ if (currentSegment.length > 0) {
166
+ nonErasedSegments.push(currentSegment);
167
+ }
168
+
169
+ nonErasedSegments.forEach(segment => {
170
+ if (segment.length > 1) {
171
+ const stroke = getSvgPathFromStroke(getStroke(segment, {
172
+ size: element.size || 3,
173
+ thinning: 0.5,
174
+ smoothing: 0.5,
175
+ streamline: 0.5,
176
+ }));
177
+ context.fillStyle = '#000';
178
+ context.fill(new Path2D(stroke));
179
+ }
180
+ });
181
+ break;
182
+ case "text":
183
+ context.textBaseline = "top";
184
+ context.font = "24px sans-serif";
185
+ context.fillText(element.text, element.x1, element.y1);
186
+ break;
187
+ case "capture":
188
+ context.save();
189
+ context.strokeStyle = "#000000";
190
+ context.setLineDash([5, 5]);
191
+ context.lineWidth = 1;
192
+ context.strokeRect(
193
+ element.x1,
194
+ element.y1,
195
+ element.x2 - element.x1,
196
+ element.y2 - element.y1
197
+ );
198
+ context.restore();
199
+ break;
200
+ default:
201
+ throw new Error(`Type not recognised: ${element.type}`);
202
+ }
203
+ };
204
+
205
+ export const adjustmentRequired = type => ["line", "rectangle"].includes(type);
206
+
207
+ export const calculateBoundsForElements = (elementsList) => {
208
+ if (elementsList.length === 0) return null;
209
+
210
+ let minX = Infinity;
211
+ let minY = Infinity;
212
+ let maxX = -Infinity;
213
+ let maxY = -Infinity;
214
+
215
+ elementsList.forEach(element => {
216
+ if (element.type === "text") {
217
+ minX = Math.min(minX, element.x1);
218
+ minY = Math.min(minY, element.y1);
219
+ maxX = Math.max(maxX, element.x2);
220
+ maxY = Math.max(maxY, element.y2);
221
+ } else if (element.type === "pencil") {
222
+ element.points.forEach(point => {
223
+ minX = Math.min(minX, point.x);
224
+ minY = Math.min(minY, point.y);
225
+ maxX = Math.max(maxX, point.x);
226
+ maxY = Math.max(maxY, point.y);
227
+ });
228
+ } else if (element.type === "line" || element.type === "rectangle") {
229
+ minX = Math.min(minX, element.x1, element.x2);
230
+ minY = Math.min(minY, element.y1, element.y2);
231
+ maxX = Math.max(maxX, element.x1, element.x2);
232
+ maxY = Math.max(maxY, element.y2, element.y2);
233
+ }
234
+ });
235
+
236
+ const padding = 20;
237
+ return {
238
+ x1: minX - padding,
239
+ y1: minY - padding,
240
+ x2: maxX + padding,
241
+ y2: maxY + padding
242
+ };
243
+ };
src/app/favicon.ico ADDED
src/app/fonts/GeistMonoVF.woff ADDED
Binary file (67.9 kB). View file
 
src/app/fonts/GeistVF.woff ADDED
Binary file (66.3 kB). View file
 
src/app/globals.css ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ :root {
6
+ --background: #ffffff;
7
+ --foreground: #171717;
8
+ }
9
+
10
+ @media (prefers-color-scheme: dark) {
11
+ :root {
12
+ --background: #0a0a0a;
13
+ --foreground: #ededed;
14
+ }
15
+ }
16
+
17
+ body {
18
+ color: var(--foreground);
19
+ background: var(--background);
20
+ font-family: Arial, Helvetica, sans-serif;
21
+ }
src/app/handleMouse.js ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createElement, getElementAtPosition, adjustElementCoordinates, cursorForPosition, resizedCoordinates, adjustmentRequired } from './element-utils';
2
+
3
+ export const createMouseHandlers = ({
4
+ action,
5
+ setAction,
6
+ tool,
7
+ setTool,
8
+ elements,
9
+ setElements,
10
+ selectedElement,
11
+ setSelectedElement,
12
+ panOffset,
13
+ setPanOffset,
14
+ scale,
15
+ scaleOffset,
16
+ startPanMousePosition,
17
+ setStartPanMousePosition,
18
+ pressedKeys,
19
+ pencilSize,
20
+ setCaptureArea,
21
+ isDrawing,
22
+ setIsDrawing
23
+ }) => {
24
+ const getMouseCoordinates = event => {
25
+ const clientX = (event.clientX - panOffset.x * scale + scaleOffset.x) / scale;
26
+ const clientY = (event.clientY - panOffset.y * scale + scaleOffset.y) / scale;
27
+ return { clientX, clientY };
28
+ };
29
+
30
+ const updateElement = (id, x1, y1, x2, y2, type, options) => {
31
+ const elementsCopy = [...elements];
32
+
33
+ switch (type) {
34
+ case "line":
35
+ case "rectangle":
36
+ elementsCopy[id] = createElement(id, x1, y1, x2, y2, type);
37
+ elementsCopy[id].size = pencilSize;
38
+ break;
39
+ case "pencil":
40
+ elementsCopy[id].points = [...elementsCopy[id].points, { x: x2, y: y2 }];
41
+ break;
42
+ case "text":
43
+ const textWidth = document
44
+ .getElementById("canvas")
45
+ .getContext("2d")
46
+ .measureText(options.text).width;
47
+ const textHeight = 24;
48
+ elementsCopy[id] = {
49
+ ...createElement(id, x1, y1, x1 + textWidth, y1 + textHeight, type),
50
+ text: options.text,
51
+ };
52
+ break;
53
+ default:
54
+ throw new Error(`Type not recognised: ${type}`);
55
+ }
56
+
57
+ setElements(elementsCopy, true);
58
+ };
59
+
60
+ const handleMouseDown = event => {
61
+ if (action === "writing") return;
62
+
63
+ const { clientX, clientY } = getMouseCoordinates(event);
64
+
65
+ if (event.button === 1 || pressedKeys.has(" ")) {
66
+ setAction("panning");
67
+ setStartPanMousePosition({ x: clientX, y: clientY });
68
+ return;
69
+ }
70
+
71
+ setIsDrawing(true);
72
+
73
+ if (tool === "capture") {
74
+ const id = elements.length;
75
+ const element = createElement(id, clientX, clientY, clientX, clientY, "capture");
76
+ setElements(prevState => [...prevState, element]);
77
+ setSelectedElement(element);
78
+ setAction("drawing");
79
+ return;
80
+ }
81
+
82
+ if (tool === "eraser") {
83
+ setAction("erasing");
84
+ const elementToErase = getElementAtPosition(clientX, clientY, elements);
85
+ if (elementToErase) {
86
+ setElements(prevState =>
87
+ prevState.filter(el => el.id !== elementToErase.id)
88
+ );
89
+ }
90
+ return;
91
+ }
92
+
93
+ if (tool === "selection") {
94
+ const element = getElementAtPosition(clientX, clientY, elements);
95
+ if (element) {
96
+ if (element.type === "pencil") {
97
+ const xOffsets = element.points.map(point => clientX - point.x);
98
+ const yOffsets = element.points.map(point => clientY - point.y);
99
+ setSelectedElement({ ...element, xOffsets, yOffsets });
100
+ } else {
101
+ const offsetX = clientX - element.x1;
102
+ const offsetY = clientY - element.y1;
103
+ setSelectedElement({ ...element, offsetX, offsetY });
104
+ }
105
+ setElements(prevState => prevState);
106
+
107
+ if (element.position === "inside") {
108
+ setAction("moving");
109
+ } else {
110
+ setAction("resizing");
111
+ }
112
+ }
113
+ } else {
114
+ const id = elements.length;
115
+ const element = createElement(id, clientX, clientY, clientX, clientY, tool, pencilSize);
116
+ setElements(prevState => [...prevState, element]);
117
+ setSelectedElement(element);
118
+ setAction(tool === "text" ? "writing" : "drawing");
119
+ }
120
+ };
121
+
122
+ const handleMouseMove = event => {
123
+ const { clientX, clientY } = getMouseCoordinates(event);
124
+
125
+ if (action === "panning") {
126
+ const deltaX = clientX - startPanMousePosition.x;
127
+ const deltaY = clientY - startPanMousePosition.y;
128
+ setPanOffset({
129
+ x: panOffset.x + deltaX,
130
+ y: panOffset.y + deltaY,
131
+ });
132
+ return;
133
+ }
134
+
135
+ if (action === "erasing") {
136
+ const elementToErase = getElementAtPosition(clientX, clientY, elements);
137
+ if (elementToErase) {
138
+ setElements(prevState =>
139
+ prevState.filter(el => el.id !== elementToErase.id)
140
+ );
141
+ }
142
+ return;
143
+ }
144
+
145
+ if (tool === "selection") {
146
+ const element = getElementAtPosition(clientX, clientY, elements);
147
+ event.target.style.cursor = element ? cursorForPosition(element.position) : "default";
148
+ }
149
+
150
+ if (action === "drawing") {
151
+ const index = elements.length - 1;
152
+ const { x1, y1 } = elements[index];
153
+
154
+ if (tool === "capture") {
155
+ const element = elements[index];
156
+ const updatedElement = {
157
+ ...element,
158
+ x2: clientX,
159
+ y2: clientY
160
+ };
161
+ const elementsCopy = [...elements];
162
+ elementsCopy[index] = updatedElement;
163
+ setElements(elementsCopy, true);
164
+ setCaptureArea(updatedElement);
165
+ } else {
166
+ updateElement(index, x1, y1, clientX, clientY, tool);
167
+ }
168
+ } else if (action === "moving") {
169
+ if (selectedElement.type === "pencil") {
170
+ const newPoints = selectedElement.points.map((_, index) => ({
171
+ x: clientX - selectedElement.xOffsets[index],
172
+ y: clientY - selectedElement.yOffsets[index],
173
+ }));
174
+ const elementsCopy = [...elements];
175
+ elementsCopy[selectedElement.id] = {
176
+ ...elementsCopy[selectedElement.id],
177
+ points: newPoints,
178
+ };
179
+ setElements(elementsCopy, true);
180
+ } else {
181
+ const { id, x1, x2, y1, y2, type, offsetX, offsetY } = selectedElement;
182
+ const width = x2 - x1;
183
+ const height = y2 - y1;
184
+ const newX1 = clientX - offsetX;
185
+ const newY1 = clientY - offsetY;
186
+ const options = type === "text" ? { text: selectedElement.text } : {};
187
+ updateElement(id, newX1, newY1, newX1 + width, newY1 + height, type, options);
188
+ }
189
+ } else if (action === "resizing") {
190
+ const { id, type, position, ...coordinates } = selectedElement;
191
+ const { x1, y1, x2, y2 } = resizedCoordinates(clientX, clientY, position, coordinates);
192
+ updateElement(id, x1, y1, x2, y2, type);
193
+ }
194
+ };
195
+
196
+ const handleMouseUp = event => {
197
+ setIsDrawing(false);
198
+ const { clientX, clientY } = getMouseCoordinates(event);
199
+
200
+ if (selectedElement) {
201
+ if (
202
+ selectedElement.type === "text" &&
203
+ clientX - selectedElement.offsetX === selectedElement.x1 &&
204
+ clientY - selectedElement.offsetY === selectedElement.y1
205
+ ) {
206
+ setAction("writing");
207
+ return;
208
+ }
209
+
210
+ const index = selectedElement.id;
211
+ const { id, type } = elements[index];
212
+ if ((action === "drawing" || action === "resizing") && adjustmentRequired(type)) {
213
+ const { x1, y1, x2, y2 } = adjustElementCoordinates(elements[index]);
214
+ updateElement(id, x1, y1, x2, y2, type);
215
+ }
216
+ }
217
+
218
+ if (action === "drawing") {
219
+ console.log("Hoàn thành một nét vẽ");
220
+ }
221
+
222
+ if (action === "writing") return;
223
+
224
+ setAction("none");
225
+ setSelectedElement(null);
226
+ };
227
+
228
+ const handleBlur = event => {
229
+ const { id, x1, y1, type } = selectedElement;
230
+ setAction("none");
231
+ setSelectedElement(null);
232
+ updateElement(id, x1, y1, null, null, type, { text: event.target.value });
233
+ };
234
+
235
+ return {
236
+ handleMouseDown,
237
+ handleMouseMove,
238
+ handleMouseUp,
239
+ handleBlur
240
+ };
241
+ };
src/app/layout.js ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import localFont from "next/font/local";
2
+ import "./globals.css";
3
+
4
+ const geistSans = localFont({
5
+ src: "./fonts/GeistVF.woff",
6
+ variable: "--font-geist-sans",
7
+ weight: "100 900",
8
+ });
9
+ const geistMono = localFont({
10
+ src: "./fonts/GeistMonoVF.woff",
11
+ variable: "--font-geist-mono",
12
+ weight: "100 900",
13
+ });
14
+
15
+ export const metadata = {
16
+ title: "Create Next App",
17
+ description: "Generated by create next app",
18
+ };
19
+
20
+ export default function RootLayout({ children }) {
21
+ return (
22
+ <html lang="en">
23
+ <body
24
+ className={`${geistSans.variable} ${geistMono.variable} antialiased`}
25
+ >
26
+ {children}
27
+ </body>
28
+ </html>
29
+ );
30
+ }
src/app/page.js ADDED
@@ -0,0 +1,548 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+ import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
3
+ import rough from "roughjs/bundled/rough.esm";
4
+ import DrawingBoard from './DrawingBoard';
5
+ import {
6
+ createElement,
7
+ drawElement,
8
+ } from './element-utils';
9
+ import { createMouseHandlers } from './handleMouse';
10
+ const captureRegionToPNG = (canvas, region, elements, scale, panOffset, scaleOffset) => {
11
+ const tempCanvas = document.createElement('canvas');
12
+ const ctx = tempCanvas.getContext('2d');
13
+
14
+ const width = (region.bounds.x2 - region.bounds.x1);
15
+ const height = (region.bounds.y2 - region.bounds.y1);
16
+
17
+ tempCanvas.width = width * scale;
18
+ tempCanvas.height = height * scale;
19
+
20
+ // Fill white background
21
+ ctx.fillStyle = 'white';
22
+ ctx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
23
+
24
+ ctx.save();
25
+ ctx.scale(scale, scale);
26
+ ctx.translate(-region.bounds.x1, -region.bounds.y1);
27
+
28
+ const mainCanvas = canvas.getContext('2d');
29
+ const roughCanvas = rough.canvas(tempCanvas);
30
+ ctx.translate(panOffset.x, panOffset.y);
31
+
32
+ elements.forEach(element => {
33
+ if (isElementInRegion(element, region.bounds)) {
34
+ drawElement(roughCanvas, ctx, element);
35
+ }
36
+ });
37
+
38
+ ctx.restore();
39
+
40
+ return tempCanvas.toDataURL('image/png');
41
+ };
42
+
43
+ // Update the downloadRegionImage function:
44
+ const downloadRegionImage = (dataUrl, regionId) => {
45
+ const link = document.createElement('a');
46
+ link.href = dataUrl;
47
+ link.download = `region-${regionId}.png`; // Changed extension to .png
48
+ document.body.appendChild(link);
49
+ link.click();
50
+ document.body.removeChild(link);
51
+ };
52
+ const isElementInRegion = (element, bounds) => {
53
+ if (element.type === 'pencil') {
54
+ return element.points.some(point =>
55
+ point.x >= bounds.x1 && point.x <= bounds.x2 &&
56
+ point.y >= bounds.y1 && point.y <= bounds.y2
57
+ );
58
+ } else {
59
+ const elementBounds = {
60
+ minX: Math.min(element.x1, element.x2),
61
+ maxX: Math.max(element.x1, element.x2),
62
+ minY: Math.min(element.y1, element.y2),
63
+ maxY: Math.max(element.y1, element.y2)
64
+ };
65
+
66
+ return !(elementBounds.maxX < bounds.x1 ||
67
+ elementBounds.minX > bounds.x2 ||
68
+ elementBounds.maxY < bounds.y1 ||
69
+ elementBounds.minY > bounds.y2);
70
+ }
71
+ };
72
+
73
+
74
+ const downloadAllRegions = (canvas, regions, elements, scale, panOffset, scaleOffset) => {
75
+ regions.forEach(region => {
76
+ const dataUrl = captureRegionToPNG(canvas, region, elements, scale, panOffset, scaleOffset);
77
+ downloadRegionImage(dataUrl, region.id);
78
+ });
79
+ };
80
+ const isPixelNonTransparent = (pixel) => {
81
+ return pixel.a > 0;
82
+ };
83
+
84
+ const isWithinBounds = (x, y, width, height) => {
85
+ return x >= 0 && x < width && y >= 0 && y < height;
86
+ };
87
+
88
+ const getPixel = (imageData, x, y) => {
89
+ const index = (y * imageData.width + x) * 4;
90
+ return imageData.data[index + 3] > 0; // Only check alpha channel for performance
91
+ };
92
+
93
+ // Use Set for faster lookups and Int32Array for coordinates
94
+ const floodFill = (imageData, startX, startY, visited) => {
95
+ const width = imageData.width;
96
+ const height = imageData.height;
97
+ const queue = new Int32Array(width * height * 2); // Pre-allocate queue
98
+ let queueStart = 0;
99
+ let queueEnd = 2;
100
+ queue[0] = startX;
101
+ queue[1] = startY;
102
+
103
+ const region = {
104
+ points: [],
105
+ minX: startX,
106
+ maxX: startX,
107
+ minY: startY,
108
+ maxY: startY
109
+ };
110
+
111
+ // Optimize bounds checking
112
+ const isWithinBounds = (x, y) => x >= 0 && x < width && y >= 0 && y < height;
113
+
114
+ // Pre-calculate neighbor offsets
115
+ const neighborOffsets = [
116
+ [1, 0], [-1, 0],
117
+ [0, 1], [0, -1],
118
+ [1, 1], [-1, -1],
119
+ [1, -1], [-1, 1]
120
+ ];
121
+
122
+ while (queueStart < queueEnd) {
123
+ const x = queue[queueStart];
124
+ const y = queue[queueStart + 1];
125
+ queueStart += 2;
126
+
127
+ const key = `${x},${y}`;
128
+ if (visited.has(key)) continue;
129
+
130
+ if (!getPixel(imageData, x, y)) continue;
131
+
132
+ visited.add(key);
133
+ region.points.push({ x, y });
134
+
135
+ // Use Math.min/max for bounds tracking
136
+ region.minX = Math.min(region.minX, x);
137
+ region.maxX = Math.max(region.maxX, x);
138
+ region.minY = Math.min(region.minY, y);
139
+ region.maxY = Math.max(region.maxY, y);
140
+
141
+ // Check neighbors using pre-calculated offsets
142
+ for (const [dx, dy] of neighborOffsets) {
143
+ const nx = x + dx;
144
+ const ny = y + dy;
145
+ if (isWithinBounds(nx, ny) && !visited.has(`${nx},${ny}`)) {
146
+ queue[queueEnd] = nx;
147
+ queue[queueEnd + 1] = ny;
148
+ queueEnd += 2;
149
+ }
150
+ }
151
+ }
152
+
153
+ return region;
154
+ };
155
+
156
+ // Optimize distance calculation
157
+ const calculateRegionDistance = (region1, region2) => {
158
+ // Quick overlap check
159
+ const xOverlap = !(region1.maxX < region2.minX || region1.minX > region2.maxX);
160
+ const yOverlap = !(region1.maxY < region2.minY || region1.minY > region2.maxY);
161
+
162
+ if (xOverlap && yOverlap) return 0;
163
+
164
+ // Calculate distance only when necessary
165
+ const dx = !xOverlap ? Math.min(
166
+ Math.abs(region1.maxX - region2.minX),
167
+ Math.abs(region1.minX - region2.maxX)
168
+ ) : 0;
169
+
170
+ const dy = !yOverlap ? Math.min(
171
+ Math.abs(region1.maxY - region2.minY),
172
+ Math.abs(region1.minY - region2.maxY)
173
+ ) : 0;
174
+
175
+ return Math.sqrt(dx * dx + dy * dy);
176
+ };
177
+
178
+ // Optimize region merging using Set
179
+ const mergeRegions = (region1, region2) => ({
180
+ points: [...region1.points, ...region2.points],
181
+ minX: Math.min(region1.minX, region2.minX),
182
+ maxX: Math.max(region1.maxX, region2.maxX),
183
+ minY: Math.min(region1.minY, region2.minY),
184
+ maxY: Math.max(region1.maxY, region2.maxY)
185
+ });
186
+
187
+ const detectRegions = (canvas, elements, panOffset, scale, scaleOffset, minRegionSize = 100, groupingDistance = 80) => {
188
+ // Calculate bounds only once
189
+ const bounds = elements.reduce((acc, element) => {
190
+ if (element.type === 'pencil') {
191
+ element.points.forEach(point => {
192
+ acc.minX = Math.min(acc.minX, point.x);
193
+ acc.minY = Math.min(acc.minY, point.y);
194
+ acc.maxX = Math.max(acc.maxX, point.x);
195
+ acc.maxY = Math.max(acc.maxY, point.y);
196
+ });
197
+ } else {
198
+ const { x1, y1, x2, y2 } = element;
199
+ acc.minX = Math.min(acc.minX, x1, x2);
200
+ acc.minY = Math.min(acc.minY, y1, y2);
201
+ acc.maxX = Math.max(acc.maxX, x1, x2);
202
+ acc.maxY = Math.max(acc.maxY, y1, y2);
203
+ }
204
+ return acc;
205
+ }, { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity });
206
+
207
+ // Add padding
208
+ const padding = 100;
209
+ bounds.minX -= padding;
210
+ bounds.minY -= padding;
211
+ bounds.maxX += padding;
212
+ bounds.maxY += padding;
213
+
214
+ // Create optimized temporary canvas
215
+ const tempCanvas = document.createElement("canvas");
216
+ const width = Math.max(bounds.maxX - bounds.minX, canvas.width);
217
+ const height = Math.max(bounds.maxY - bounds.minY, canvas.height);
218
+ tempCanvas.width = width * scale;
219
+ tempCanvas.height = height * scale;
220
+
221
+ // Use OffscreenCanvas when available for better performance
222
+ const ctx = tempCanvas.getContext("2d", { alpha: true });
223
+ const roughCanvas = rough.canvas(tempCanvas);
224
+
225
+ // Draw elements with transformation
226
+ ctx.save();
227
+ ctx.translate(-bounds.minX * scale, -bounds.minY * scale);
228
+ ctx.scale(scale, scale);
229
+ elements.forEach(element => drawElement(roughCanvas, ctx, element));
230
+ ctx.restore();
231
+
232
+ // Process image data
233
+ const imageData = ctx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
234
+ const visited = new Set();
235
+ const initialRegions = [];
236
+
237
+ // Optimize pixel scanning with stride
238
+ const stride = 4; // Check every 4th pixel initially
239
+ for (let y = 0; y < tempCanvas.height; y += stride) {
240
+ for (let x = 0; x < tempCanvas.width; x += stride) {
241
+ const key = `${x},${y}`;
242
+ if (visited.has(key)) continue;
243
+
244
+ if (!getPixel(imageData, x, y)) continue;
245
+
246
+ const region = floodFill(imageData, x, y, visited);
247
+ if (region.points.length >= minRegionSize) {
248
+ // Transform coordinates back
249
+ const transformedRegion = {
250
+ ...region,
251
+ minX: region.minX / scale + bounds.minX,
252
+ maxX: region.maxX / scale + bounds.minX,
253
+ minY: region.minY / scale + bounds.minY,
254
+ maxY: region.maxY / scale + bounds.minY,
255
+ points: region.points.map(point => ({
256
+ x: point.x / scale + bounds.minX,
257
+ y: point.y / scale + bounds.minY
258
+ }))
259
+ };
260
+ initialRegions.push(transformedRegion);
261
+ }
262
+ }
263
+ }
264
+
265
+ // Optimize region merging
266
+ const mergedRegions = [];
267
+ const used = new Set();
268
+
269
+ for (let i = 0; i < initialRegions.length; i++) {
270
+ if (used.has(i)) continue;
271
+
272
+ let currentRegion = initialRegions[i];
273
+ used.add(i);
274
+
275
+ let merged;
276
+ do {
277
+ merged = false;
278
+ for (let j = 0; j < initialRegions.length; j++) {
279
+ if (used.has(j)) continue;
280
+
281
+ const distance = calculateRegionDistance(currentRegion, initialRegions[j]);
282
+ if (distance <= groupingDistance) {
283
+ currentRegion = mergeRegions(currentRegion, initialRegions[j]);
284
+ used.add(j);
285
+ merged = true;
286
+ }
287
+ }
288
+ } while (merged);
289
+
290
+ mergedRegions.push(currentRegion);
291
+ }
292
+
293
+ // Return final regions
294
+ return mergedRegions.map((region, index) => ({
295
+ id: index + 1,
296
+ bounds: {
297
+ x1: region.minX,
298
+ y1: region.minY,
299
+ x2: region.maxX,
300
+ y2: region.maxY
301
+ },
302
+ elements: []
303
+ }));
304
+ };
305
+
306
+ const useHistory = initialState => {
307
+ const [index, setIndex] = useState(0);
308
+ const [history, setHistory] = useState([initialState]);
309
+
310
+ const setState = (action, overwrite = false) => {
311
+ const newState = typeof action === "function" ? action(history[index]) : action;
312
+ if (overwrite) {
313
+ const historyCopy = [...history];
314
+ historyCopy[index] = newState;
315
+ setHistory(historyCopy);
316
+ } else {
317
+ const updatedState = [...history].slice(0, index + 1);
318
+ setHistory([...updatedState, newState]);
319
+ setIndex(prevState => prevState + 1);
320
+ }
321
+ };
322
+
323
+ const undo = () => index > 0 && setIndex(prevState => prevState - 1);
324
+ const redo = () => index < history.length - 1 && setIndex(prevState => prevState + 1);
325
+
326
+ return [history[index], setState, undo, redo];
327
+ };
328
+
329
+ const usePressedKeys = () => {
330
+ const [pressedKeys, setPressedKeys] = useState(new Set());
331
+
332
+ useEffect(() => {
333
+ const handleKeyDown = event => {
334
+ setPressedKeys(prevKeys => new Set(prevKeys).add(event.key));
335
+ };
336
+
337
+ const handleKeyUp = event => {
338
+ setPressedKeys(prevKeys => {
339
+ const updatedKeys = new Set(prevKeys);
340
+ updatedKeys.delete(event.key);
341
+ return updatedKeys;
342
+ });
343
+ };
344
+
345
+ window.addEventListener("keydown", handleKeyDown);
346
+ window.addEventListener("keyup", handleKeyUp);
347
+ return () => {
348
+ window.removeEventListener("keydown", handleKeyDown);
349
+ window.removeEventListener("keyup", handleKeyUp);
350
+ };
351
+ }, []);
352
+
353
+ return pressedKeys;
354
+ };
355
+ const drawBoundingBoxes = (context, regions, scale) => {
356
+ context.save();
357
+ regions.forEach(region => {
358
+ const { bounds, id } = region;
359
+
360
+ // Set styling for bounding box
361
+ context.strokeStyle = "#FF5733"; // Custom color for better visibility
362
+ context.lineWidth = 1 / scale;
363
+ context.setLineDash([5 / scale, 5 / scale]); // Dotted lines for bounding box
364
+
365
+ // Draw bounding box
366
+ context.strokeRect(
367
+ bounds.x1,
368
+ bounds.y1,
369
+ bounds.x2 - bounds.x1,
370
+ bounds.y2 - bounds.y1
371
+ );
372
+
373
+ // Display region ID inside bounding box
374
+ context.setLineDash([]); // Reset line dash for text
375
+ context.font = `${16 / scale}px sans-serif`;
376
+ context.fillStyle = "#FF5733";
377
+ context.fillText(
378
+ `#${id}`,
379
+ bounds.x1 + 5 / scale,
380
+ bounds.y1 + 20 / scale
381
+ );
382
+ });
383
+ context.restore();
384
+ };
385
+
386
+ const App = () => {
387
+ const [elements, setElements, undo, redo] = useHistory([]);
388
+ const [action, setAction] = useState("none");
389
+ const [tool, setTool] = useState("rectangle");
390
+ const [selectedElement, setSelectedElement] = useState(null);
391
+ const [panOffset, setPanOffset] = useState({ x: 0, y: 0 });
392
+ const [startPanMousePosition, setStartPanMousePosition] = useState({ x: 0, y: 0 });
393
+ const [scale, setScale] = useState(1);
394
+ const [scaleOffset, setScaleOffset] = useState({ x: 0, y: 0 });
395
+ const [captureArea, setCaptureArea] = useState(null);
396
+ const [drawingRegions, setDrawingRegions] = useState([]);
397
+ const textAreaRef = useRef();
398
+ const pressedKeys = usePressedKeys();
399
+ const [pencilSize, setPencilSize] = useState(3);
400
+ const [isDrawing, setIsDrawing] = useState(false);
401
+ const handleDetectRegions = () => {
402
+ const canvas = document.getElementById("canvas");
403
+ if (!canvas) return;
404
+
405
+ const newRegions = detectRegions(canvas, elements, panOffset, scale, scaleOffset);
406
+ setDrawingRegions(newRegions);
407
+
408
+ // Notify user
409
+ if (newRegions.length > 0) {
410
+ console.log(`Phát hiện ${newRegions.length} vùng vẽ!`);
411
+ alert(`Đã phát hiện ${newRegions.length} vùng vẽ!`);
412
+ } else {
413
+ alert("Không phát hiện được vùng vẽ nào!");
414
+ }
415
+ };
416
+ useLayoutEffect(() => {
417
+ const canvas = document.getElementById("canvas");
418
+ const context = canvas.getContext("2d");
419
+ const roughCanvas = rough.canvas(canvas);
420
+
421
+ // Clear canvas for redrawing
422
+ context.clearRect(0, 0, canvas.width, canvas.height);
423
+
424
+ // Calculate scaled dimensions
425
+ const scaledWidth = canvas.width * scale;
426
+ const scaledHeight = canvas.height * scale;
427
+ const scaleOffsetX = (scaledWidth - canvas.width) / 2;
428
+ const scaleOffsetY = (scaledHeight - canvas.height) / 2;
429
+ setScaleOffset({ x: scaleOffsetX, y: scaleOffsetY });
430
+
431
+ context.save();
432
+ context.translate(panOffset.x * scale - scaleOffsetX, panOffset.y * scale - scaleOffsetY);
433
+ context.scale(scale, scale);
434
+
435
+ // Draw elements
436
+ elements.forEach(element => {
437
+ if (action === "writing" && selectedElement?.id === element.id) return;
438
+ drawElement(roughCanvas, context, element);
439
+ });
440
+
441
+ // Call drawBoundingBoxes to render detected regions
442
+ drawBoundingBoxes(context, drawingRegions, scale);
443
+
444
+ context.restore();
445
+ }, [elements, action, selectedElement, panOffset, scale, drawingRegions]);
446
+
447
+
448
+ useEffect(() => {
449
+ const undoRedoFunction = event => {
450
+ if ((event.metaKey || event.ctrlKey) && event.key === "z") {
451
+ if (event.shiftKey) {
452
+ redo();
453
+ } else {
454
+ undo();
455
+ }
456
+ }
457
+ };
458
+
459
+ document.addEventListener("keydown", undoRedoFunction);
460
+ return () => {
461
+ document.removeEventListener("keydown", undoRedoFunction);
462
+ };
463
+ }, [undo, redo]);
464
+ useEffect(() => {
465
+ const panOrZoomFunction = event => {
466
+ if (pressedKeys.has("Meta") || pressedKeys.has("Control")) {
467
+ onZoom(event.deltaY * -0.01);
468
+ } else {
469
+ setPanOffset(prevState => ({
470
+ x: prevState.x - event.deltaX,
471
+ y: prevState.y - event.deltaY,
472
+ }));
473
+ }
474
+ };
475
+
476
+ document.addEventListener("wheel", panOrZoomFunction);
477
+ return () => {
478
+ document.removeEventListener("wheel", panOrZoomFunction);
479
+ };
480
+ }, [pressedKeys]);
481
+ const onZoom = delta => {
482
+ setScale(prevState => Math.min(Math.max(prevState + delta, 0.1), 2));
483
+ };
484
+ const handleDownloadRegions = () => {
485
+ const canvas = document.getElementById("canvas");
486
+ if (!canvas || drawingRegions.length === 0) {
487
+ alert("Không có vùng vẽ nào để tải xuống!");
488
+ return;
489
+ }
490
+
491
+ downloadAllRegions(canvas, drawingRegions, elements, scale, panOffset, scaleOffset);
492
+ alert(`Đã tải xuống ${drawingRegions.length} vùng vẽ!`);
493
+ };
494
+ const {
495
+ handleMouseDown,
496
+ handleMouseMove,
497
+ handleMouseUp,
498
+ handleBlur
499
+ } = createMouseHandlers({
500
+ action,
501
+ setAction,
502
+ tool,
503
+ setTool,
504
+ elements,
505
+ setElements,
506
+ selectedElement,
507
+ setSelectedElement,
508
+ panOffset,
509
+ setPanOffset,
510
+ scale,
511
+ scaleOffset,
512
+ startPanMousePosition,
513
+ setStartPanMousePosition,
514
+ pressedKeys,
515
+ pencilSize,
516
+ setCaptureArea,
517
+ isDrawing,
518
+ setIsDrawing
519
+ });
520
+
521
+ return (
522
+
523
+ <DrawingBoard
524
+ tool={tool}
525
+ setTool={setTool}
526
+ elements={elements}
527
+ pencilSize={pencilSize}
528
+ setPencilSize={setPencilSize}
529
+ scale={scale}
530
+ setScale={setScale}
531
+ onZoom={onZoom}
532
+ undo={undo}
533
+ redo={redo}
534
+ action={action}
535
+ selectedElement={selectedElement}
536
+ panOffset={panOffset}
537
+ scaleOffset={scaleOffset}
538
+ handleMouseDown={handleMouseDown}
539
+ handleMouseMove={handleMouseMove}
540
+ handleMouseUp={handleMouseUp}
541
+ handleBlur={handleBlur}
542
+ handleDetectRegions={handleDetectRegions}
543
+ handleDownloadRegions={handleDownloadRegions}
544
+ />
545
+ );
546
+ };
547
+
548
+ export default App;