Spaces:
Running
Running
MAO
commited on
Commit
·
ccd2cb2
1
Parent(s):
548ea5f
Translate UI to English and refine interaction logic
Browse files- .DS_Store +0 -0
- drawingPaperTemplate.html +303 -0
- 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
|
| 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("
|
| 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"
|
| 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
|
| 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
|
| 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} />
|
|
|
|
| 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
|
| 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} />
|
| 512 |
</div>
|
| 513 |
-
|
|
|
|
|
|
|
| 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"
|
| 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"
|
| 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 |
-
|
| 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"
|
| 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"
|
| 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} />
|
| 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]">
|