MAO commited on
Commit
ccd2cb2
·
1 Parent(s): 548ea5f

Translate UI to English and refine interaction logic

Browse files
Files changed (3) hide show
  1. .DS_Store +0 -0
  2. drawingPaperTemplate.html +303 -0
  3. src/App.jsx +34 -36
.DS_Store ADDED
Binary file (6.15 kB). View file
 
drawingPaperTemplate.html ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ArUco Drawing Template Generator</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@900&display=swap" rel="stylesheet">
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
10
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
11
+ <style>
12
+ body {
13
+ background-color: #f3f4f6;
14
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
15
+ }
16
+ .a4-page {
17
+ width: 210mm;
18
+ height: 297mm;
19
+ background: white;
20
+ box-shadow: 0 0 20px rgba(0,0,0,0.1);
21
+ position: relative;
22
+ overflow: hidden;
23
+ margin: 0 auto;
24
+ transform-origin: top center;
25
+ }
26
+ .marker {
27
+ width: 15mm;
28
+ height: 15mm;
29
+ background: black;
30
+ position: absolute;
31
+ z-index: 50; /* Markers on top */
32
+ }
33
+ .main-draw-area {
34
+ position: absolute;
35
+ left: 22.5mm;
36
+ top: 56mm; /* Shifted up by 10mm */
37
+ width: 165mm;
38
+ height: 165mm;
39
+ display: flex;
40
+ align-items: center;
41
+ justify-content: center;
42
+ z-index: 30; /* Image above border box but below markers */
43
+ }
44
+ /* 165mm Guide Border Box */
45
+ .center-border-box {
46
+ position: absolute;
47
+ left: 22.5mm;
48
+ top: 56mm; /* Shifted up by 10mm */
49
+ width: 165mm;
50
+ height: 165mm;
51
+ border: 0.2mm solid #e5e7eb; /* Light gray */
52
+ box-sizing: border-box;
53
+ z-index: 20; /* Below image */
54
+ }
55
+ .title-area {
56
+ position: absolute;
57
+ top: 15mm; /* Shifted up by 10mm */
58
+ left: 0;
59
+ width: 100%;
60
+ text-align: center;
61
+ font-family: 'Noto Sans JP', sans-serif;
62
+ font-weight: 900;
63
+ font-size: 42pt;
64
+ color: #000;
65
+ z-index: 40;
66
+ pointer-events: none;
67
+ }
68
+ @media print {
69
+ .no-print { display: none; }
70
+ body { background: white; margin: 0; }
71
+ .a4-page { box-shadow: none; transform: none !important; }
72
+ }
73
+ #controls {
74
+ width: 350px;
75
+ }
76
+ </style>
77
+ </head>
78
+ <body class="flex flex-col md:flex-row min-h-screen">
79
+
80
+ <!-- Control Panel -->
81
+ <div id="controls" class="no-print bg-white p-6 shadow-xl z-50 overflow-y-auto max-h-screen">
82
+ <h1 class="text-2xl font-bold mb-6 text-gray-800">ArUco Template</h1>
83
+
84
+ <div class="space-y-6">
85
+ <!-- Title Input -->
86
+ <div>
87
+ <label class="block text-sm font-medium text-gray-700 mb-1">Template Title</label>
88
+ <input type="text" id="titleInput" value=""
89
+ class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-gray-400 outline-none"
90
+ placeholder="Kabutomushi">
91
+ </div>
92
+
93
+ <!-- ID Selector -->
94
+ <div>
95
+ <label class="block text-sm font-medium text-gray-700 mb-1">Bottom Marker ID (1-100)</label>
96
+ <input type="number" id="idInput" min="1" max="100" value="1"
97
+ class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-gray-400 outline-none">
98
+ <p class="text-xs text-gray-400 mt-1">* Top markers use ID Data Index 0</p>
99
+ </div>
100
+
101
+ <!-- Custom Image Upload -->
102
+ <div>
103
+ <label class="block text-sm font-medium text-gray-700 mb-1">Upload Line Art</label>
104
+ <label for="imageUpload" class="cursor-pointer bg-gray-100 text-gray-700 px-4 py-2 rounded-md text-sm font-semibold hover:bg-gray-200 inline-block w-full text-center border border-gray-300 transition">
105
+ Choose File
106
+ </label>
107
+ <input type="file" id="imageUpload" accept="image/*" class="hidden">
108
+ <p id="fileNameDisplay" class="text-xs text-gray-400 mt-1 italic text-center">No file chosen</p>
109
+ </div>
110
+
111
+ <hr>
112
+
113
+ <!-- Buttons -->
114
+ <div class="grid grid-cols-1 gap-3">
115
+ <button onclick="downloadPDF()" class="w-full bg-gray-600 text-white py-2 rounded-md hover:bg-gray-700 transition font-bold shadow-lg text-sm">
116
+ Download A4 PDF
117
+ </button>
118
+ <button onclick="downloadCrop()" class="w-full bg-gray-400 text-white py-2 rounded-md hover:bg-gray-500 transition font-bold shadow-md text-sm">
119
+ Export Crop (165mm Area)
120
+ </button>
121
+ </div>
122
+
123
+ <div class="mt-8 p-4 bg-gray-50 rounded-lg text-xs text-gray-600 leading-relaxed border border-gray-100">
124
+ <strong>Instructions:</strong><br>
125
+ 1. Image will be centered within the 165mm area.<br>
126
+ 2. Markers are at 165x165mm square (center-to-center).
127
+ </div>
128
+ </div>
129
+ </div>
130
+
131
+ <!-- Preview Area -->
132
+ <div class="flex-1 flex justify-center items-start p-8 bg-gray-200 overflow-auto">
133
+ <div id="capture-area" class="a4-page">
134
+ <div id="displayTitle" class="title-area"></div>
135
+
136
+ <!-- Guide Box (Bottom Layer) -->
137
+ <div class="center-border-box"></div>
138
+
139
+ <!-- ArUco Markers (Top Layer) -->
140
+ <!-- Top markers: 48.5mm from top -->
141
+ <div class="marker" style="left: 15mm; top: 48.5mm;" data-is-top="true">
142
+ <canvas width="120" height="120" class="w-full h-full block"></canvas>
143
+ </div>
144
+ <div class="marker" style="left: 180mm; top: 48.5mm;" data-is-top="true">
145
+ <canvas width="120" height="120" class="w-full h-full block"></canvas>
146
+ </div>
147
+ <!-- Bottom markers: 213.5mm from top -->
148
+ <div class="marker" style="left: 15mm; top: 213.5mm;" data-is-top="false">
149
+ <canvas width="120" height="120" class="w-full h-full block"></canvas>
150
+ </div>
151
+ <div class="marker" style="left: 180mm; top: 213.5mm;" data-is-top="false">
152
+ <canvas width="120" height="120" class="w-full h-full block"></canvas>
153
+ </div>
154
+
155
+ <!-- Drawing Area (Middle Layer) -->
156
+ <div id="crop-target" class="main-draw-area">
157
+ <img id="previewImg" class="max-w-full max-h-full object-contain opacity-80" src="" style="display:none;">
158
+ </div>
159
+ </div>
160
+ </div>
161
+
162
+ <script>
163
+ // ArUco DICT_6x6_1000 Data Matrix
164
+ const arucoData = [
165
+ [30,61,216,42,6],[14,251,163,137,1],[21,144,126,172,13],[201,27,48,105,14],[214,7,214,225,5],[216,232,224,230,8],[66,104,180,31,5],[136,165,15,41,10],[48,125,82,79,13],[60,47,52,179,12],[69,223,199,78,3],[72,216,91,37,7],[113,5,88,252,6],[134,220,250,208,7],[141,114,169,63,6],[162,184,157,205,14],[9,253,30,156,4],[21,77,189,24,15],[48,10,49,14,2],[72,7,239,175,13],[86,223,17,219,6],[102,136,50,116,12],[118,232,203,120,1],[154,83,217,207,3],[169,203,132,2,4],[198,117,73,73,0],[193,210,136,148,1],[231,72,8,82,11],[234,47,202,132,8],[233,99,183,123,1],[250,54,101,42,15],[6,91,255,123,13],[5,65,215,45,6],[12,247,36,106,2],[19,56,163,158,11],[21,168,147,231,4],[58,65,126,233,14],[79,17,226,108,0],[83,13,182,210,0],[88,155,250,227,4],[100,9,232,160,11],[96,83,122,137,1],[97,89,6,155,10],[107,255,120,215,11],[112,173,150,164,15],[117,132,111,113,10],[122,149,25,47,12],[134,9,118,10,10],[138,45,68,195,15],[147,235,120,177,4],[152,141,168,77,4],[158,222,43,60,8],[165,41,224,123,8],[181,147,184,85,15],[183,248,228,38,15],[188,32,82,37,14],[192,68,135,118,5],[196,195,36,37,9],[197,169,27,216,13],[206,115,230,178,12],[205,12,166,39,2],[201,67,93,68,13],[207,190,128,243,4],[229,125,21,135,7],[239,198,133,142,9],[247,126,243,119,2],[44,228,63,37,4],[43,220,255,75,3],[55,199,221,189,10],[161,162,84,224,15],[169,130,193,187,5],[216,27,73,176,8],[3,88,41,248,6],[7,196,9,95,12],[15,226,102,23,11],[20,72,54,68,1],[16,173,95,251,7],[18,130,149,83,15],[22,225,49,132,12],[24,122,73,107,0],[26,232,134,17,2],[25,19,174,10,1],[27,103,181,161,7],[37,220,149,240,11],[40,137,97,247,6],[51,84,20,106,10],[49,193,108,31,7],[51,203,24,198,6],[62,207,228,144,15],[70,69,24,163,15],[68,186,112,182,7],[65,156,98,62,8],[72,209,145,74,1],[84,244,153,246,13],[87,90,156,129,3],[85,131,85,178,12],[87,183,118,16,15],[92,52,54,254,4],[92,72,252,119,14],[94,110,239,64,2],[95,35,59,111,15],[91,116,42,99,2],[101,15,163,58,14],[101,211,23,92,12],[106,156,36,90,14],[105,197,243,4,2],[105,210,72,78,10],[116,121,226,222,6],[114,207,35,234,11],[119,177,220,65,4],[126,12,7,33,7],[122,105,112,100,7],[120,178,216,112,7],[121,197,133,121,4],[134,111,89,252,6],[130,246,114,127,5],[133,78,47,65,4],[154,17,133,147,4],[156,113,96,201,7],[157,209,148,253,8],[162,30,18,227,8],[174,112,28,130,12],[173,1,33,156,1],[176,53,31,158,14],[182,74,216,13,4],[181,55,49,75,4],[190,170,199,227,11],[187,104,61,188,15],[198,114,247,44,1],[193,231,77,186,11],[203,85,238,89,13],[203,160,83,114,4],[208,9,15,207,1],[208,108,58,213,4],[211,241,32,87,4],[230,227,59,26,7],[227,83,62,164,10],[232,6,142,177,4],[236,7,192,89,7],[234,243,128,61,10],[246,59,39,216,8],[243,7,152,55,9],[254,75,186,155,9],[171,165,125,134,11],[192,209,98,90,11],[19,206,123,174,7],[78,129,253,97,7],[86,224,118,50,0],[106,112,138,84,0],[114,168,152,161,8],[129,93,66,248,0],[207,76,195,213,15],[214,187,101,134,4],[236,211,19,163,1],[245,33,245,32,7],[249,31,165,223,7],[0,36,244,122,7],[0,8,77,136,2],[4,60,194,242,9],[4,123,80,33,1],[6,122,228,193,13],[0,170,150,138,3],[4,209,56,233,4],[5,16,168,13,10],[1,64,176,0,7],[1,157,156,238,1],[8,16,87,227,11],[8,107,151,182,6],[14,232,184,96,10],[11,108,118,185,11],[15,220,185,140,11],[15,202,207,58,0],[20,36,159,217,8],[20,7,32,31,13],[21,9,16,213,7],[19,92,215,48,7],[17,71,154,187,6],[28,185,169,35,8],[28,221,7,118,6],[31,46,124,36,11],[25,102,66,71,7],[25,87,212,200,4],[31,168,244,240,4],[27,130,70,237,8],[27,174,225,15,14],[34,164,182,60,10],[34,191,144,18,15],[35,44,21,180,0],[37,90,169,102,12],[39,165,175,169,7],[37,244,14,66,5],[40,102,85,205,14],[44,66,126,14,0],[42,185,124,189,0],[41,70,225,210,3],[45,166,40,65,0],[43,251,32,154,6],[54,140,214,107,12],[52,135,119,124,7],[52,221,235,132,0],[55,145,247,111,1],[58,34,142,23,5],[62,19,189,64,8],[60,152,67,202,2],[57,88,157,23,9],[57,116,218,238,11],[63,109,188,115,1],[61,107,192,80,12],[57,171,39,73,7],[70,2,78,37,14],[70,130,186,11,12],[66,233,205,90,14],[68,201,183,179,15],[64,199,212,30,9],[70,210,180,204,14],[67,25,83,86,11],[65,34,230,221,9],[71,83,165,154,11],[78,30,241,224,8],[78,74,192,150,0],[78,95,170,6,15],[74,141,50,148,3],[73,21,148,179,9],[77,77,219,98,1],[75,167,97,232,1],[73,212,131,216,14],[86,41,14,246,12],[83,126,213,255,12],[85,245,167,175,10],[85,213,234,100,15],[88,27,171,29,10],[94,190,146,109,13],[95,16,249,155,5],[93,30,223,165,12],[95,113,141,240,2],[93,225,30,70,8],[96,51,187,36,7],[100,88,26,254,1],[99,200,221,167,6],[97,218,61,143,13],[110,58,34,175,10],[110,97,5,183,1],[106,137,169,232,12],[106,151,34,79,5],[107,18,195,128,1],[107,104,75,34,10],[111,148,193,87,9],[109,166,254,160,13],[111,234,202,69,7],[112,61,56,166,0],[118,108,53,231,8],[112,74,13,255,6],[117,120,169,200,0],[113,74,112,19,8],[117,127,140,187,9],[124,35,104,51,1],[124,181,167,211,1],[124,248,44,237,14],[127,36,226,52,15],[127,71,41,141,8],[134,216,3,209,9],[131,139,27,161,3],[135,162,121,197,9],[138,67,100,140,14],[136,147,59,76,8],[143,33,223,78,3],[141,132,53,114,9],[141,136,215,31,13],[137,159,120,252,13],[146,107,22,121,12],[148,142,34,241,2],[144,229,230,49,7],[150,216,133,42,1],[149,57,59,164,6],[149,60,251,77,13],[145,62,170,18,6],[151,111,90,175,9],[145,178,41,253,10],[145,211,250,118,1],[154,112,134,200,8],[152,142,205,3,1],[152,199,16,151,10],[157,203,235,70,6],[164,40,245,182,14],[163,55,241,121,3],[163,68,64,245,10],[161,127,173,133,8],[167,210,150,35,13],[168,69,112,43,11],[174,72,127,160,9],[172,79,182,214,8],[168,168,211,133,3],[169,139,10,203,8],[173,254,140,222,2],[180,239,46,46,14],[183,153,137,199,0],[190,12,162,14,12],[188,112,34,122,9],[190,188,47,145,10],[184,233,10,152,3],[189,10,48,236,8],[194,2,224,243,1],[194,107,50,227,7],[198,202,66,106,8],[199,30,238,104,14],[199,125,46,145,3],[206,60,32,116,2],[204,74,185,197,7],[206,247,99,220,13],[205,67,34,202,2],[207,183,204,29,0],[201,206,200,53,10],[207,243,75,113,3],[214,46,123,112,13],[212,23,75,59,4],[215,141,250,151,14],[209,216,245,85,1],[213,207,225,211,9],[218,22,168,204,9],[216,76,68,133,9],[220,217,114,142,13],[223,103,17,126,8],[219,153,125,230,7],[221,171,142,49,14],[224,25,8,76,13],[230,54,218,82,1],[226,172,199,155,0],[228,141,33,98,0],[226,254,208,197,1],[225,58,125,2,10],[231,208,91,142,5],[236,48,156,107,5],[236,170,73,210,0],[238,179,122,196,6],[232,224,103,46,2],[234,229,213,36,12],[237,107,28,44,2],[235,200,175,29,6],[242,5,98,212,5],[246,25,188,251,2],[246,163,92,109,11],[244,241,189,15,0],[241,106,155,67,5],[241,178,145,41,14],[250,84,91,243,5],[254,110,134,124,6],[249,13,185,67,2],[249,105,102,43,13],[251,65,203,72,4],[253,87,191,152,5],[251,152,144,126,6],[255,234,33,198,3],[163,165,111,69,0],[161,152,104,48,2],[15,55,131,43,0],[38,236,72,39,2],[65,152,184,168,15],[78,181,67,138,4],[99,197,227,123,10],[110,89,221,230,12],[128,212,89,240,8],[152,8,136,159,13],[163,6,103,166,15],[2,25,166,20,7],[2,21,202,78,2],[0,104,204,57,9],[0,161,19,254,8],[2,185,86,117,5],[2,198,187,83,2],[2,243,31,29,9],[7,42,193,126,3],[7,55,141,151,2],[1,111,31,231,5],[1,119,48,21,5],[7,149,114,65,10],[7,200,163,134,14],[5,254,251,247,9],[12,21,243,16,1],[10,68,98,226,14],[8,94,55,238,9],[8,82,222,18,13],[12,102,136,3,9],[8,140,186,71,15],[12,172,39,30,5],[8,179,56,11,15],[12,162,165,217,4],[8,227,82,192,1],[14,255,68,245,14],[14,255,93,234,11],[11,52,109,201,14],[13,9,254,187,3],[15,16,170,146,6],[13,31,101,167,14],[15,177,95,160,0],[13,162,5,35,3],[13,175,35,219,11],[11,240,165,238,4],[13,200,153,251,14],[15,253,48,39,7],[11,199,93,86,2],[18,47,163,1,13],[18,117,72,114,5],[16,79,174,98,7],[16,140,138,232,11],[16,243,244,46,14],[19,55,238,112,2],[23,11,35,235,0],[21,2,111,27,11],[17,161,171,203,2],[19,166,74,200,13],[23,142,53,205,3],[21,139,229,157,12],[23,166,249,125,4],[21,220,164,180,4],[17,223,5,67,12],[21,210,1,147,5],[24,31,173,250,10],[28,64,14,171,13],[30,76,93,61,2],[24,111,246,127,9],[24,87,52,184,15],[30,86,137,227,4],[26,149,209,132,5],[24,167,255,2,10],[24,158,177,201,14],[28,178,10,96,10],[28,146,53,136,1],[28,147,183,214,13],[26,202,188,88,1],[28,226,172,181,8],[27,121,237,6,4],[31,106,57,19,2],[29,152,56,117,0],[32,13,193,3,15],[38,53,212,232,0],[38,117,44,47,8],[32,83,133,143,6],[34,136,31,122,3],[39,44,250,229,5],[33,107,76,67,11],[37,188,103,11,10],[33,163,233,176,14],[33,150,242,145,15],[39,162,43,140,10],[39,159,72,50,8],[35,226,188,201,7],[33,194,87,244,1],[39,207,186,248,11],[42,47,189,228,11],[42,84,58,140,12],[42,209,187,151,15],[40,255,58,99,1],[43,84,197,185,8],[45,97,175,26,10],[43,188,219,62,6],[45,159,154,13,2],[47,187,114,106,3],[43,217,204,255,7],[41,198,223,142,12],[52,12,63,195,5],[48,3,27,40,14],[48,113,29,236,3],[52,76,222,162,10],[50,122,138,139,12],[54,87,99,215,2],[52,141,50,170,6],[54,129,250,177,1],[54,153,111,21,13],[50,204,108,49,14],[54,224,7,77,4],[51,64,141,156,5],[51,125,174,182,10],[49,94,255,61,1],[53,164,249,40,11],[51,134,161,198,4],[53,232,26,158,12],[55,216,88,186,3],[56,21,51,89,12],[56,27,98,106,12],[58,54,80,219,10],[56,136,209,242,9],[58,204,141,200,2],[60,252,249,7,11],[60,246,137,57,12],[59,15,174,199,9],[61,11,95,245,6],[57,73,131,170,6],[61,92,175,229,6],[59,111,209,254,2],[61,114,229,206,7],[59,165,24,48,4],[59,215,215,116,14],[66,21,7,134,1],[68,42,51,204,5],[64,105,62,32,12],[68,76,84,241,12],[64,78,207,5,5],[64,223,74,196,6],[70,218,103,26,14],[67,32,239,178,13],[65,99,121,242,7],[71,127,169,44,10],[67,168,36,115,10],[69,174,2,166,1],[72,29,125,107,6],[72,68,41,14,4],[76,93,176,242,6],[72,79,166,76,0],[72,129,242,45,11],[72,140,105,154,11],[76,230,39,197,2],[79,61,163,205,7],[73,73,253,184,8],[77,89,111,25,7],[79,79,213,39,11],[75,148,100,228,11],[77,160,106,169,14],[75,146,209,251,2],[79,237,128,190,11],[73,250,110,175,3],[84,32,172,167,7],[84,25,164,142,8],[86,116,218,31,8],[82,70,69,212,11],[80,153,112,192,3],[82,162,196,106,5],[80,232,170,66,4],[82,242,173,89,8],[82,218,235,246,10],[85,46,248,226,2],[81,65,96,182,2],[85,104,6,21,8],[83,74,126,75,13],[83,107,211,224,11],[87,114,130,210,7],[87,129,213,88,2],[87,164,195,74,8],[81,151,175,148,8],[87,143,23,115,11],[85,194,224,207,0],[87,246,164,229,1],[92,5,94,2,5],[94,56,204,77,0],[94,101,102,31,5],[90,165,235,123,5],[94,173,81,224,13],[88,179,133,252,1],[90,197,248,110,0],[88,241,107,96,0],[91,48,177,32,8],[95,52,239,231,11],[89,64,115,102,9],[93,105,19,173,1],[89,82,185,227,11],[93,70,244,172,2],[95,128,157,46,8],[95,224,251,80,14],[95,235,211,215,0],[100,105,134,115,5],[96,188,232,205,8],[100,158,227,5,6],[98,212,165,37,13],[98,246,143,142,3],[101,96,72,36,7],[99,180,12,145,8],[99,159,233,153,13],[101,237,229,156,7],[103,235,231,112,4],[106,1,200,157,4],[104,61,242,11,0],[104,24,102,125,13],[108,84,97,80,15],[104,75,24,245,13],[104,79,21,129,0],[110,110,244,89,0],[110,152,128,38,5],[110,144,99,51,3],[110,241,228,10,7],[108,195,36,252,0],[105,0,81,91,15],[107,64,14,250,12],[107,124,181,68,7],[105,172,245,202,2],[105,167,102,247,12],[109,235,50,140,11],[116,23,252,110,12],[112,97,170,185,4],[116,67,17,82,1],[112,185,187,138,9],[118,148,62,229,4],[118,188,96,18,9],[112,155,7,165,4],[116,139,191,1,3],[114,248,93,79,11],[118,211,218,167,15],[117,36,208,249,9],[117,60,188,48,6],[119,3,201,213,15],[113,69,59,42,5],[115,108,25,223,1],[113,72,238,44,10],[115,111,34,124,0],[115,145,99,174,14],[117,131,158,142,9],[119,191,100,123,6],[115,229,189,18,10],[115,207,104,128,3],[113,247,74,10,0],[120,45,207,39,6],[124,2,58,157,7],[122,92,206,58,4],[122,114,16,7,7],[126,178,36,208,11],[120,253,197,213,2],[126,232,117,158,9],[123,28,43,68,14],[127,4,160,250,2],[123,39,78,235,3],[125,38,127,122,1],[127,50,169,94,10],[121,180,170,170,7],[125,176,215,37,3],[127,147,79,12,2],[125,252,16,66,2],[125,213,216,211,15],[128,56,32,103,11],[132,17,185,214,3],[128,3,252,100,10],[130,59,15,187,10],[134,42,167,117,10],[132,27,89,75,7],[130,68,49,200,10],[130,89,216,117,3],[132,72,31,220,14],[130,181,229,107,4],[128,183,10,183,0],[134,178,80,25,10],[128,204,47,123,8],[134,253,182,100,8],[129,121,60,62,9],[133,96,132,133,11],[131,98,22,146,5],[131,128,229,221,15],[135,160,76,203,2],[135,184,138,180,13],[142,2,6,56,4],[140,54,125,172,7],[140,65,233,44,2],[140,152,59,175,12],[136,244,107,79,11],[143,44,197,204,4],[141,55,126,115,3],[139,69,79,63,7],[141,86,37,150,0],[139,191,24,57,2],[141,166,67,9,4],[141,208,202,166,8],[139,246,152,79,4],[144,18,153,165,4],[150,46,4,135,5],[146,92,163,212,0],[146,133,143,119,10],[148,169,61,69,10],[145,32,243,51,2],[145,39,109,79,3],[151,51,253,233,10],[151,72,243,129,3],[151,159,160,6,9],[147,208,63,215,12],[149,252,208,110,0],[147,195,178,11,1],[145,195,66,1,14],[158,10,212,208,9],[158,62,24,70,8],[154,97,122,220,9],[152,200,135,101,13],[152,193,223,48,9],[158,249,40,250,12],[158,240,171,22,1],[152,251,117,9,13],[159,32,1,53,4],[153,18,120,7,12],[155,131,245,126,11],[153,249,200,173,5],[157,228,46,236,2],[155,219,144,210,3],[159,254,132,153,15],[162,66,140,215,9],[164,205,25,53,7],[166,213,162,25,0],[160,223,192,176,6],[163,49,180,144,4],[167,32,134,58,15],[167,56,45,40,13],[163,63,204,220,14],[165,62,179,132,11],[161,112,26,189,7],[167,103,101,55,9],[165,123,102,174,4],[161,213,29,147,2],[163,248,233,155,14],[165,211,131,218,3],[165,214,235,188,6],[170,38,227,151,9],[174,30,201,63,12],[172,119,184,237,2],[170,169,238,77,15],[170,128,121,170,6],[174,194,96,202,1],[169,58,152,98,11],[171,18,81,200,12],[173,102,219,216,13],[173,82,221,74,1],[169,182,113,16,8],[173,186,226,53,0],[173,151,65,223,12],[171,243,182,44,7],[182,34,108,113,12],[180,30,37,122,7],[182,55,195,138,3],[176,76,212,55,0],[178,127,139,5,3],[176,173,216,34,5],[180,155,130,43,11],[176,211,78,194,3],[177,61,126,195,14],[177,6,164,99,14],[179,43,116,63,14],[183,31,214,70,15],[177,110,241,244,5],[177,126,83,138,14],[183,98,223,55,7],[177,169,248,148,9],[179,183,217,210,4],[179,238,187,76,6],[184,44,165,82,4],[184,20,13,235,15],[188,25,220,199,7],[188,84,38,185,6],[190,103,55,196,5],[184,250,232,211,5],[190,194,26,55,8],[187,24,207,164,11],[189,1,126,246,13],[189,34,47,210,7],[189,2,85,133,5],[185,114,78,96,5],[189,119,92,22,15],[185,136,172,46,1],[191,160,38,103,5],[185,130,150,168,6],[189,135,23,24,10],[191,190,239,45,7],[189,250,159,0,7],[194,112,53,187,8],[196,108,5,172,13],[196,121,84,220,3],[194,106,81,13,8],[198,164,159,104,4],[192,158,44,235,4],[198,146,33,73,13],[196,186,131,207,8],[198,231,133,67,11],[198,215,175,70,4],[197,54,85,77,11],[195,147,255,13,7],[193,245,112,165,10],[204,57,113,197,0],[200,2,137,73,2],[200,124,100,74,5],[202,103,225,13,7],[200,153,231,66,2],[200,170,178,16,5],[204,166,213,159,3],[202,229,134,251,14],[202,217,124,49,15],[206,228,202,14,6],[203,45,164,50,12],[203,87,214,130,2],[201,144,33,177,0],[201,130,95,176,10],[203,158,212,36,7],[205,134,171,157,13],[201,201,176,119,5],[201,250,31,99,2],[203,247,64,6,9],[205,218,13,28,15],[208,11,130,83,15],[212,59,207,214,15],[214,98,171,209,11],[212,102,200,58,15],[209,32,28,120,10],[211,57,65,195,9],[209,7,160,253,4],[209,149,58,14,2],[215,189,66,29,13],[215,192,166,80,13],[215,229,141,245,12],[215,197,83,246,7],[213,254,112,184,8],[216,12,185,190,6],[222,52,186,37,6],[220,14,198,139,14],[222,3,247,164,11],[218,93,11,138,2],[216,113,249,127,15],[220,189,130,231,2],[220,176,90,229,5],[218,147,222,80,0],[218,183,241,191,8],[222,159,2,50,14],[218,196,13,85,0],[220,197,171,128,14],[218,194,178,58,10],[218,218,88,253,0],[217,37,142,168,1],[221,5,118,59,14],[221,46,153,8,12],[217,85,104,199,5],[219,80,109,76,3],[223,104,170,56,8],[219,66,135,167,5],[219,129,40,179,13],[217,163,194,250,13],[221,179,56,211,8],[217,202,123,155,1],[219,246,158,176,7],[226,8,108,175,8],[224,27,171,150,4],[224,72,115,243,0],[228,117,169,91,14],[230,128,236,116,9],[226,175,118,72,7],[224,162,200,17,13],[226,203,113,80,12],[224,210,73,197,15],[225,12,194,130,9],[225,127,68,52,3],[231,173,69,177,14],[225,182,90,159,9],[231,252,32,65,12],[238,51,65,56,1],[238,124,54,51,4],[236,112,123,248,10],[232,123,59,230,14],[232,149,232,57,1],[238,172,9,125,5],[236,161,201,55,4],[232,151,189,197,5],[232,204,18,29,0],[238,193,29,105,8],[237,7,255,219,10],[237,105,243,54,11],[237,118,20,181,12],[239,132,33,209,7],[235,245,218,120,2],[233,249,229,214,13],[235,211,15,145,9],[233,219,241,32,9],[244,41,139,109,8],[246,10,52,34,5],[244,88,85,155,1],[242,118,55,97,3],[244,75,10,224,6],[246,110,129,75,12],[246,67,29,76,6],[244,87,34,238,1],[244,153,83,229,11],[240,252,47,27,5],[247,9,30,9,12],[247,38,87,194,0],[241,133,27,198,13],[243,136,98,232,1],[247,169,183,234,6],[247,138,138,92,2],[243,237,194,21,2],[245,245,148,242,4],[248,15,62,15,4],[252,190,99,125,14],[248,237,33,73,11],[250,216,194,128,14],[254,229,5,156,6],[254,196,253,27,8],[250,210,225,31,4],[252,195,230,115,2],[249,63,162,110,11],[251,86,194,94,3],[255,78,140,162,15],[255,79,112,226,4],[249,177,42,52,9],[255,135,146,120,1],[249,195,188,9,10],[255,202,25,209,12],[255,239,23,165,10],[253,254,218,140,1],[6,66,233,9,7],[3,36,36,70,13],[7,94,92,135,12],[7,136,66,250,6],[10,7,178,27,11],[16,148,220,241,14],[22,162,90,176,15],[16,253,203,109,14],[17,52,172,162,12],[17,166,62,16,1],[30,88,44,226,2],[24,99,58,139,0],[26,99,1,47,13],[26,241,71,206,15],[25,108,177,139,1],[29,149,207,92,12],[25,158,56,146,2],[31,252,86,35,10],[36,27,41,179,3],[38,231,136,133,0],[33,7,54,188,1],[40,77,70,86,10],[46,236,227,69,8],[45,79,225,77,4],[45,138,40,60,7],[52,248,62,55,1],[56,99,93,125,8],[63,162,28,196,12],[66,221,151,174,14],[68,223,18,214,11],[65,58,205,236,8],[65,192,219,73,4],[76,180,0,200,13],[73,35,72,149,2],[82,55,185,92,6],[82,81,56,221,10],[94,113,146,65,3],[92,178,113,235,13],[91,238,55,54,6],[96,5,132,21,2],[100,8,128,51,8],[110,40,135,140,3],[108,85,84,181,7],[104,186,254,70,1],[104,179,85,202,7],[117,156,155,102,9],[124,11,200,10,0],[126,89,104,196,9],[120,111,61,52,5],[120,193,191,255,10],[122,218,185,69,10],[128,60,123,156,0],[135,20,88,249,7],[131,27,5,165,13],[136,59,180,103,4],[140,169,136,156,0],[142,177,3,26,11],[143,39,191,54,4],[143,22,179,202,0],[148,24,222,74,0],[147,69,36,197,0],[156,253,156,216,5],[155,34,55,120,13],[153,250,163,209,6],[162,159,28,84,3],[172,79,91,26,11],[172,191,109,54,6],[169,185,76,64,8],[182,40,233,2,12],[180,109,218,222,0],[178,110,246,10,1],[179,37,153,105,14],[179,80,40,17,5],[190,28,27,59,6],[188,127,85,99,14],[188,175,230,141,5],[189,37,146,140,4],[196,27,214,183,1],[198,99,190,252,14],[198,86,130,19,13],[198,139,73,36,10],[193,61,206,190,2],[197,25,109,109,1],[199,77,61,239,11],[193,90,43,220,9],[202,35,114,33,12],[207,199,244,213,9],[208,49,39,226,6],[212,119,84,14,0],[209,134,49,90,2],[219,76,100,122,11],[219,132,135,144,8],[223,222,6,112,0],[226,41,186,96,0],[225,64,224,141,6],[225,154,144,165,2],[231,242,192,250,9],[238,173,190,131,8],[240,28,242,124,1],[247,101,168,38,4],[247,236,195,164,13],[248,45,84,113,4],[254,133,143,205,11],[248,227,91,11,6],[254,214,62,31,15]
166
+ ];
167
+
168
+ /**
169
+ * 繪製 ArUco 標記 (正確解析位元組邏輯)
170
+ * 使用您提供的演算法確保 36 位元完整提取
171
+ */
172
+ function drawArUco(canvas, dataIndex) {
173
+ const ctx = canvas.getContext('2d', { alpha: false });
174
+ const bytes = arucoData[dataIndex] || arucoData[0];
175
+ const width = 6;
176
+ const height = 6;
177
+ const bitsCount = width * height; // 36
178
+
179
+ let bits = [];
180
+
181
+ // 根據您提供的邏輯解析位元組
182
+ for (let byte of bytes) {
183
+ let start = bitsCount - bits.length;
184
+ // 取剩餘需要的位元,最高不超過 8 (7+1)
185
+ for (let i = Math.min(7, start - 1); i >= 0; i--) {
186
+ bits.push((byte >> i) & 1);
187
+ }
188
+ }
189
+
190
+ const size = 8; // 6x6 content + 2 for black border
191
+ const cellSize = canvas.width / size;
192
+
193
+ // 清除背景 (黑色邊框)
194
+ ctx.fillStyle = "black";
195
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
196
+
197
+ // 繪製內部 6x6 資料位元
198
+ ctx.fillStyle = "white";
199
+ for (let i = 0; i < bits.length; i++) {
200
+ if (bits[i] === 1) {
201
+ const x = (i % 6) + 1;
202
+ const y = Math.floor(i / 6) + 1;
203
+ // 使用稍微偏移來消除渲染時的空隙
204
+ ctx.fillRect(
205
+ x * cellSize - 0.5,
206
+ y * cellSize - 0.5,
207
+ cellSize + 1,
208
+ cellSize + 1
209
+ );
210
+ }
211
+ }
212
+ }
213
+
214
+ function refreshMarkers() {
215
+ const bottomId = parseInt(document.getElementById('idInput').value) || 1;
216
+ document.querySelectorAll('.marker').forEach(m => {
217
+ const canvas = m.querySelector('canvas');
218
+ const isTop = m.getAttribute('data-is-top') === 'true';
219
+ const dataIndex = isTop ? 0 : bottomId;
220
+ drawArUco(canvas, dataIndex);
221
+ });
222
+ }
223
+
224
+ document.getElementById('titleInput').addEventListener('input', (e) => {
225
+ document.getElementById('displayTitle').innerText = e.target.value;
226
+ });
227
+
228
+ document.getElementById('idInput').addEventListener('input', refreshMarkers);
229
+
230
+ // Handle File Upload and update display text
231
+ document.getElementById('imageUpload').addEventListener('change', function(e) {
232
+ const file = e.target.files[0];
233
+ const display = document.getElementById('fileNameDisplay');
234
+ if (file) {
235
+ display.innerText = file.name;
236
+ const reader = new FileReader();
237
+ reader.onload = function(event) {
238
+ const img = document.getElementById('previewImg');
239
+ img.src = event.target.result;
240
+ img.style.display = 'block';
241
+ }
242
+ reader.readAsDataURL(file);
243
+ } else {
244
+ display.innerText = "No file chosen";
245
+ }
246
+ });
247
+
248
+ async function downloadPDF() {
249
+ const { jsPDF } = window.jspdf;
250
+ const pdf = new jsPDF('p', 'mm', 'a4');
251
+ const page = document.getElementById('capture-area');
252
+
253
+ const originalTransform = page.style.transform;
254
+ page.style.transform = 'none';
255
+
256
+ const canvas = await html2canvas(page, {
257
+ scale: 3,
258
+ useCORS: true,
259
+ logging: false,
260
+ width: 794,
261
+ height: 1123
262
+ });
263
+
264
+ page.style.transform = originalTransform;
265
+
266
+ const imgData = canvas.toDataURL('image/png');
267
+ pdf.addImage(imgData, 'PNG', 0, 0, 210, 297);
268
+ pdf.save(`aruco_template_id_${document.getElementById('idInput').value}.pdf`);
269
+ }
270
+
271
+ async function downloadCrop() {
272
+ const cropArea = document.getElementById('crop-target');
273
+ const canvas = await html2canvas(cropArea, {
274
+ scale: 2,
275
+ useCORS: true,
276
+ backgroundColor: "#ffffff"
277
+ });
278
+
279
+ const link = document.createElement('a');
280
+ link.download = 'recognition_zone_165mm.png';
281
+ link.href = canvas.toDataURL();
282
+ link.click();
283
+ }
284
+
285
+ window.onload = () => {
286
+ refreshMarkers();
287
+ adjustZoom();
288
+ };
289
+
290
+ function adjustZoom() {
291
+ const page = document.getElementById('capture-area');
292
+ const container = page.parentElement;
293
+ const padding = 40;
294
+ const scale = Math.min(
295
+ (container.offsetWidth - padding) / (210 * 3.78),
296
+ (container.offsetHeight - padding) / (297 * 3.78)
297
+ );
298
+ page.style.transform = `scale(${scale})`;
299
+ }
300
+ window.onresize = adjustZoom;
301
+ </script>
302
+ </body>
303
+ </html>
src/App.jsx CHANGED
@@ -1,7 +1,7 @@
1
  import React, { useState, useRef, useEffect } from 'react';
2
  import { Upload, Plus, Eye, EyeOff, Trash2, Save, FolderOpen, Download, Search } from 'lucide-react';
3
 
4
- // --- 數學與幾何輔助函數 ---
5
 
6
  const dist = (p1, p2) => Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
7
 
@@ -22,7 +22,7 @@ const getRandomColor = () => {
22
  return color;
23
  };
24
 
25
- // --- 主應用程序 ---
26
 
27
  export default function App() {
28
  const [imgA, setImgA] = useState(null);
@@ -36,7 +36,7 @@ export default function App() {
36
  const [hoveredPoint, setHoveredPoint] = useState(null);
37
  const [draggingPoint, setDraggingPoint] = useState(null);
38
 
39
- // Zoom 功能相關狀態
40
  const [isZooming, setIsZooming] = useState(false);
41
  const [mousePos, setMousePos] = useState({ x: 0, y: 0, relX: 0, relY: 0, rect: null });
42
 
@@ -44,7 +44,7 @@ export default function App() {
44
  const imgRefB = useRef(null);
45
  const canvasPreviewRef = useRef(null);
46
 
47
- const zoomFactor = 4; // 縮放倍率
48
 
49
  const handleImageUpload = (e, setImg) => {
50
  const file = e.target.files[0];
@@ -66,7 +66,7 @@ export default function App() {
66
  setSelectedRegionId(data.length > 0 ? data[0].id : null);
67
  }
68
  } catch (err) {
69
- console.error("JSON 格式錯誤", err);
70
  }
71
  };
72
  reader.readAsText(file);
@@ -340,14 +340,12 @@ export default function App() {
340
  URL.revokeObjectURL(url);
341
  }, 'image/png');
342
  } catch (err) {
343
- console.error("下載錯誤:", err);
344
  }
345
  };
346
 
347
  const renderOverlay = (side, isZoomView = false) => {
348
  const pointsKey = side === 'A' ? 'pointsA' : 'pointsB';
349
-
350
- // Side B 的過濾邏輯:勾選 hideUnselected 時只顯示選中區域
351
  const visibleRegions = (side === 'B' && hideUnselected && selectedRegionId)
352
  ? regions.filter(r => r.id === selectedRegionId)
353
  : regions;
@@ -356,7 +354,6 @@ export default function App() {
356
  const selectedStrokeWidth = 3;
357
  const baseCircleRadius = 0.5;
358
 
359
- // 縮放視圖中保持線條比例
360
  const currentStrokeW = isZoomView ? (baseStrokeWidth / zoomFactor) : baseStrokeWidth;
361
  const currentSelectedStrokeW = isZoomView ? (selectedStrokeWidth / zoomFactor) : selectedStrokeWidth;
362
  const currentCircleRadius = isZoomView ? (baseCircleRadius / zoomFactor) : baseCircleRadius;
@@ -435,12 +432,7 @@ export default function App() {
435
  const renderMagnifier = (side, imageSrc, rect) => {
436
  if (!isZooming || !mousePos.relX || !rect) return null;
437
 
438
- const viewSize = 240; // 放大鏡圓圈的大小
439
-
440
- // 計算邏輯修正:
441
- // 放大鏡中心點應該對應滑鼠目前的相對位置 (relX, relY)
442
- // 放大後的內容左上角,相對於放大鏡中心的偏移量:
443
- // offsetX = (放大鏡半徑) - (滑鼠相對位置 * 圖片目前的寬度 * 縮放倍率)
444
  const centerX = viewSize / 2;
445
  const centerY = viewSize / 2;
446
 
@@ -455,11 +447,11 @@ export default function App() {
455
  top: `${mousePos.relY * 100}%`,
456
  width: `${viewSize}px`,
457
  height: `${viewSize}px`,
458
- transform: 'translate(-50%, -115%)' // 讓放大鏡浮在滑鼠上方
459
  }}
460
  >
461
  <div
462
- className="absolute origin-top-left" // 固定從左上角開始繪製內容,不使用 flex 置中
463
  style={{
464
  width: rect.width,
465
  height: rect.height,
@@ -472,10 +464,9 @@ export default function App() {
472
  </div>
473
  </div>
474
 
475
- {/* 精準導航準星 */}
476
  <div className="absolute top-1/2 left-0 w-full h-px bg-blue-400/50"></div>
477
  <div className="absolute left-1/2 top-0 w-px h-full bg-blue-400/50"></div>
478
- {/* 內陰影效果 */}
479
  <div className="absolute inset-0 rounded-full border-[4px] border-black/20 pointer-events-none shadow-inner"></div>
480
  </div>
481
  );
@@ -485,35 +476,41 @@ export default function App() {
485
  <div className="flex flex-col h-screen bg-gray-900 text-gray-100 font-sans select-none">
486
  <div className="flex items-center justify-between p-4 bg-gray-800 border-b border-gray-700 shadow-md z-50">
487
  <h1 className="text-xl font-bold bg-gradient-to-r from-blue-400 to-emerald-400 bg-clip-text text-transparent tracking-tight">UV Texture Mapper</h1>
 
488
  <div className="flex items-center space-x-2 text-[10px] text-gray-500 mr-auto ml-6 font-medium">
489
- <span className="bg-gray-700/50 border border-gray-600 px-1.5 py-0.5 rounded text-gray-300">A</span> 新增點
490
- <span className="bg-gray-700/50 border border-gray-600 px-1.5 py-0.5 rounded text-gray-300 ml-1">D</span> 刪除點
491
- <span className="bg-gray-700/50 border border-gray-600 px-1.5 py-0.5 rounded text-gray-300 ml-1">Z</span> 長按放大
492
  </div>
 
493
  <div className="flex items-center space-x-3">
494
  <div className="flex space-x-2">
495
  <label className="flex items-center gap-2 px-3 py-1.5 bg-gray-700 hover:bg-gray-600 rounded cursor-pointer text-xs transition-colors border border-gray-600">
496
- <Upload size={14} /> <span>圖片 A</span>
497
  <input type="file" accept="image/*" className="hidden" onChange={(e) => handleImageUpload(e, setImgA)} />
498
  </label>
499
  <label className="flex items-center gap-2 px-3 py-1.5 bg-gray-700 hover:bg-gray-600 rounded cursor-pointer text-xs transition-colors border border-gray-600">
500
- <Upload size={14} /> <span>圖片 B</span>
501
  <input type="file" accept="image/*" className="hidden" onChange={(e) => handleImageUpload(e, setImgB)} />
502
  </label>
503
  </div>
504
  <div className="w-px h-6 bg-gray-700 mx-1"></div>
505
- <button onClick={addRegion} className="flex items-center gap-2 px-3 py-1.5 bg-blue-600 hover:bg-blue-500 rounded text-xs font-medium transition-all"><Plus size={14} /> 新增區域</button>
 
506
  <div className="flex space-x-2">
507
  <label className="flex items-center gap-2 px-3 py-1.5 bg-gray-700 hover:bg-gray-600 rounded cursor-pointer text-xs transition-colors">
508
- <FolderOpen size={14} /> <span>匯入 JSON</span>
509
  <input type="file" accept=".json" className="hidden" onChange={handleJsonUpload} />
510
  </label>
511
- <button onClick={exportJson} className="flex items-center gap-2 px-3 py-1.5 bg-gray-700 hover:bg-gray-600 rounded text-xs transition-colors"><Save size={14} /> 匯出 JSON</button>
512
  </div>
513
- {selectedRegionId && <button onClick={() => deleteRegion(selectedRegionId)} className="flex items-center gap-2 px-3 py-1.5 bg-red-900/30 text-red-300 hover:bg-red-900/50 rounded border border-red-800/40 text-xs transition-colors"><Trash2 size={14} /> 刪除</button>}
 
 
514
  <div className="w-px h-6 bg-gray-700 mx-1"></div>
 
515
  <button onClick={() => setPreviewMode(!previewMode)} className={`flex items-center gap-2 px-4 py-1.5 rounded text-xs font-bold transition-all ${previewMode ? 'bg-emerald-600 text-white shadow-lg' : 'bg-gray-700 hover:bg-gray-600'}`}>
516
- {previewMode ? <EyeOff size={14} /> : <Eye size={14} />} {previewMode ? '返回編輯' : '預覽結果'}
517
  </button>
518
  </div>
519
  </div>
@@ -522,7 +519,7 @@ export default function App() {
522
  {!previewMode ? (
523
  <div className="flex w-full h-full">
524
  <div className="flex-1 flex flex-col border-r border-gray-800 bg-gray-950 p-6 relative">
525
- <div className="mb-3 text-[10px] font-black uppercase tracking-[0.2em] text-gray-500 text-center">目標畫布 (Side A)</div>
526
  <div className="relative flex-1 bg-black/40 rounded-xl border border-gray-800 overflow-hidden select-none flex items-center justify-center shadow-inner" onMouseMove={(e) => handleMouseMove(e, 'A')} onMouseDown={() => setSelectedRegionId(null)}>
527
  {imgA ? (
528
  <div className="relative" style={{ width: 'fit-content', height: 'fit-content', maxWidth: '100%', maxHeight: '100%' }}>
@@ -530,15 +527,16 @@ export default function App() {
530
  {renderOverlay('A')}
531
  {renderMagnifier('A', imgA, mousePos.rect)}
532
  </div>
533
- ) : <div className="text-gray-700 text-sm italic">請上傳圖��� A</div>}
534
  </div>
535
  </div>
 
536
  <div className="flex-1 flex flex-col bg-gray-950 p-6 relative">
537
  <div className="mb-3 text-[10px] font-black uppercase tracking-[0.2em] text-gray-500 text-center flex items-center justify-center gap-4">
538
- 紋理來源 (Side B)
539
  <label className="flex items-center gap-2 cursor-pointer normal-case tracking-normal text-gray-400 hover:text-gray-200 transition-colors ml-4 border border-gray-800 px-2 py-0.5 rounded bg-gray-900/50">
540
  <input type="checkbox" className="w-3 h-3 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-0" checked={hideUnselected} onChange={(e) => setHideUnselected(e.target.checked)} />
541
- <span className="text-[10px] font-medium">僅顯示選中區域</span>
542
  </label>
543
  </div>
544
  <div className="relative flex-1 bg-black/40 rounded-xl border border-gray-800 overflow-hidden select-none flex items-center justify-center shadow-inner" onMouseMove={(e) => handleMouseMove(e, 'B')} onMouseDown={() => setSelectedRegionId(null)}>
@@ -548,7 +546,7 @@ export default function App() {
548
  {renderOverlay('B')}
549
  {renderMagnifier('B', imgB, mousePos.rect)}
550
  </div>
551
- ) : <div className="text-gray-700 text-sm italic">請上傳圖片 B</div>}
552
  </div>
553
  </div>
554
  </div>
@@ -556,10 +554,10 @@ export default function App() {
556
  <div className="absolute inset-0 z-20 bg-gray-950 flex flex-col items-center justify-center p-8 overflow-auto">
557
  <div className="mb-6 flex gap-4">
558
  <button type="button" onClick={downloadPreview} className="flex items-center gap-2 px-6 py-2 bg-emerald-600 hover:bg-emerald-500 text-white rounded-full font-bold shadow-xl transition-all active:scale-95">
559
- <Download size={18} /> 下載結果 (.png)
560
  </button>
561
  <button onClick={() => setPreviewMode(false)} className="flex items-center gap-2 px-6 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-full font-bold transition-all">
562
- 返回編輯
563
  </button>
564
  </div>
565
  <div className="relative shadow-2xl rounded-2xl overflow-hidden border border-gray-800 bg-black max-w-full max-h-[80vh]">
 
1
  import React, { useState, useRef, useEffect } from 'react';
2
  import { Upload, Plus, Eye, EyeOff, Trash2, Save, FolderOpen, Download, Search } from 'lucide-react';
3
 
4
+ // --- Math & Geometry Helpers ---
5
 
6
  const dist = (p1, p2) => Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
7
 
 
22
  return color;
23
  };
24
 
25
+ // --- Main Application ---
26
 
27
  export default function App() {
28
  const [imgA, setImgA] = useState(null);
 
36
  const [hoveredPoint, setHoveredPoint] = useState(null);
37
  const [draggingPoint, setDraggingPoint] = useState(null);
38
 
39
+ // Zoom feature states
40
  const [isZooming, setIsZooming] = useState(false);
41
  const [mousePos, setMousePos] = useState({ x: 0, y: 0, relX: 0, relY: 0, rect: null });
42
 
 
44
  const imgRefB = useRef(null);
45
  const canvasPreviewRef = useRef(null);
46
 
47
+ const zoomFactor = 4; // Zoom multiplier
48
 
49
  const handleImageUpload = (e, setImg) => {
50
  const file = e.target.files[0];
 
66
  setSelectedRegionId(data.length > 0 ? data[0].id : null);
67
  }
68
  } catch (err) {
69
+ console.error("JSON parse error", err);
70
  }
71
  };
72
  reader.readAsText(file);
 
340
  URL.revokeObjectURL(url);
341
  }, 'image/png');
342
  } catch (err) {
343
+ console.error("Download error:", err);
344
  }
345
  };
346
 
347
  const renderOverlay = (side, isZoomView = false) => {
348
  const pointsKey = side === 'A' ? 'pointsA' : 'pointsB';
 
 
349
  const visibleRegions = (side === 'B' && hideUnselected && selectedRegionId)
350
  ? regions.filter(r => r.id === selectedRegionId)
351
  : regions;
 
354
  const selectedStrokeWidth = 3;
355
  const baseCircleRadius = 0.5;
356
 
 
357
  const currentStrokeW = isZoomView ? (baseStrokeWidth / zoomFactor) : baseStrokeWidth;
358
  const currentSelectedStrokeW = isZoomView ? (selectedStrokeWidth / zoomFactor) : selectedStrokeWidth;
359
  const currentCircleRadius = isZoomView ? (baseCircleRadius / zoomFactor) : baseCircleRadius;
 
432
  const renderMagnifier = (side, imageSrc, rect) => {
433
  if (!isZooming || !mousePos.relX || !rect) return null;
434
 
435
+ const viewSize = 240;
 
 
 
 
 
436
  const centerX = viewSize / 2;
437
  const centerY = viewSize / 2;
438
 
 
447
  top: `${mousePos.relY * 100}%`,
448
  width: `${viewSize}px`,
449
  height: `${viewSize}px`,
450
+ transform: 'translate(-50%, -115%)'
451
  }}
452
  >
453
  <div
454
+ className="absolute origin-top-left"
455
  style={{
456
  width: rect.width,
457
  height: rect.height,
 
464
  </div>
465
  </div>
466
 
467
+ {/* Navigation Crosshair */}
468
  <div className="absolute top-1/2 left-0 w-full h-px bg-blue-400/50"></div>
469
  <div className="absolute left-1/2 top-0 w-px h-full bg-blue-400/50"></div>
 
470
  <div className="absolute inset-0 rounded-full border-[4px] border-black/20 pointer-events-none shadow-inner"></div>
471
  </div>
472
  );
 
476
  <div className="flex flex-col h-screen bg-gray-900 text-gray-100 font-sans select-none">
477
  <div className="flex items-center justify-between p-4 bg-gray-800 border-b border-gray-700 shadow-md z-50">
478
  <h1 className="text-xl font-bold bg-gradient-to-r from-blue-400 to-emerald-400 bg-clip-text text-transparent tracking-tight">UV Texture Mapper</h1>
479
+
480
  <div className="flex items-center space-x-2 text-[10px] text-gray-500 mr-auto ml-6 font-medium">
481
+ <span className="bg-gray-700/50 border border-gray-600 px-1.5 py-0.5 rounded text-gray-300">A</span> Add Node
482
+ <span className="bg-gray-700/50 border border-gray-600 px-1.5 py-0.5 rounded text-gray-300 ml-1">D</span> Delete Node
483
+ <span className="bg-gray-700/50 border border-gray-600 px-1.5 py-0.5 rounded text-gray-300 ml-1">Hold Z</span> Zoom
484
  </div>
485
+
486
  <div className="flex items-center space-x-3">
487
  <div className="flex space-x-2">
488
  <label className="flex items-center gap-2 px-3 py-1.5 bg-gray-700 hover:bg-gray-600 rounded cursor-pointer text-xs transition-colors border border-gray-600">
489
+ <Upload size={14} /> <span>Image A</span>
490
  <input type="file" accept="image/*" className="hidden" onChange={(e) => handleImageUpload(e, setImgA)} />
491
  </label>
492
  <label className="flex items-center gap-2 px-3 py-1.5 bg-gray-700 hover:bg-gray-600 rounded cursor-pointer text-xs transition-colors border border-gray-600">
493
+ <Upload size={14} /> <span>Image B</span>
494
  <input type="file" accept="image/*" className="hidden" onChange={(e) => handleImageUpload(e, setImgB)} />
495
  </label>
496
  </div>
497
  <div className="w-px h-6 bg-gray-700 mx-1"></div>
498
+ <button onClick={addRegion} className="flex items-center gap-2 px-3 py-1.5 bg-blue-600 hover:bg-blue-500 rounded text-xs font-medium transition-all"><Plus size={14} /> New Region</button>
499
+
500
  <div className="flex space-x-2">
501
  <label className="flex items-center gap-2 px-3 py-1.5 bg-gray-700 hover:bg-gray-600 rounded cursor-pointer text-xs transition-colors">
502
+ <FolderOpen size={14} /> <span>Import JSON</span>
503
  <input type="file" accept=".json" className="hidden" onChange={handleJsonUpload} />
504
  </label>
505
+ <button onClick={exportJson} className="flex items-center gap-2 px-3 py-1.5 bg-gray-700 hover:bg-gray-600 rounded text-xs transition-colors"><Save size={14} /> Export JSON</button>
506
  </div>
507
+
508
+ {selectedRegionId && <button onClick={() => deleteRegion(selectedRegionId)} className="flex items-center gap-2 px-3 py-1.5 bg-red-900/30 text-red-300 hover:bg-red-900/50 rounded border border-red-800/40 text-xs transition-colors"><Trash2 size={14} /> Delete</button>}
509
+
510
  <div className="w-px h-6 bg-gray-700 mx-1"></div>
511
+
512
  <button onClick={() => setPreviewMode(!previewMode)} className={`flex items-center gap-2 px-4 py-1.5 rounded text-xs font-bold transition-all ${previewMode ? 'bg-emerald-600 text-white shadow-lg' : 'bg-gray-700 hover:bg-gray-600'}`}>
513
+ {previewMode ? <EyeOff size={14} /> : <Eye size={14} />} {previewMode ? 'Back to Edit' : 'Preview Result'}
514
  </button>
515
  </div>
516
  </div>
 
519
  {!previewMode ? (
520
  <div className="flex w-full h-full">
521
  <div className="flex-1 flex flex-col border-r border-gray-800 bg-gray-950 p-6 relative">
522
+ <div className="mb-3 text-[10px] font-black uppercase tracking-[0.2em] text-gray-500 text-center">Target Canvas (Side A)</div>
523
  <div className="relative flex-1 bg-black/40 rounded-xl border border-gray-800 overflow-hidden select-none flex items-center justify-center shadow-inner" onMouseMove={(e) => handleMouseMove(e, 'A')} onMouseDown={() => setSelectedRegionId(null)}>
524
  {imgA ? (
525
  <div className="relative" style={{ width: 'fit-content', height: 'fit-content', maxWidth: '100%', maxHeight: '100%' }}>
 
527
  {renderOverlay('A')}
528
  {renderMagnifier('A', imgA, mousePos.rect)}
529
  </div>
530
+ ) : <div className="text-gray-700 text-sm italic">Please upload Image A</div>}
531
  </div>
532
  </div>
533
+
534
  <div className="flex-1 flex flex-col bg-gray-950 p-6 relative">
535
  <div className="mb-3 text-[10px] font-black uppercase tracking-[0.2em] text-gray-500 text-center flex items-center justify-center gap-4">
536
+ Texture Source (Side B)
537
  <label className="flex items-center gap-2 cursor-pointer normal-case tracking-normal text-gray-400 hover:text-gray-200 transition-colors ml-4 border border-gray-800 px-2 py-0.5 rounded bg-gray-900/50">
538
  <input type="checkbox" className="w-3 h-3 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-0" checked={hideUnselected} onChange={(e) => setHideUnselected(e.target.checked)} />
539
+ <span className="text-[10px] font-medium">Selection Only</span>
540
  </label>
541
  </div>
542
  <div className="relative flex-1 bg-black/40 rounded-xl border border-gray-800 overflow-hidden select-none flex items-center justify-center shadow-inner" onMouseMove={(e) => handleMouseMove(e, 'B')} onMouseDown={() => setSelectedRegionId(null)}>
 
546
  {renderOverlay('B')}
547
  {renderMagnifier('B', imgB, mousePos.rect)}
548
  </div>
549
+ ) : <div className="text-gray-700 text-sm italic">Please upload Image B</div>}
550
  </div>
551
  </div>
552
  </div>
 
554
  <div className="absolute inset-0 z-20 bg-gray-950 flex flex-col items-center justify-center p-8 overflow-auto">
555
  <div className="mb-6 flex gap-4">
556
  <button type="button" onClick={downloadPreview} className="flex items-center gap-2 px-6 py-2 bg-emerald-600 hover:bg-emerald-500 text-white rounded-full font-bold shadow-xl transition-all active:scale-95">
557
+ <Download size={18} /> Download Result (.png)
558
  </button>
559
  <button onClick={() => setPreviewMode(false)} className="flex items-center gap-2 px-6 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-full font-bold transition-all">
560
+ Back to Edit
561
  </button>
562
  </div>
563
  <div className="relative shadow-2xl rounded-2xl overflow-hidden border border-gray-800 bg-black max-w-full max-h-[80vh]">