Spaces:
Sleeping
Sleeping
MAO
commited on
Commit
·
0d52f5b
1
Parent(s):
d504d0f
Enhance: Improve Triangulation Logic and Magnifier UI v2.7
Browse files- src/App.jsx +81 -37
src/App.jsx
CHANGED
|
@@ -27,19 +27,17 @@ const getRandomColor = () => {
|
|
| 27 |
};
|
| 28 |
|
| 29 |
// --- Magnifier Component ---
|
| 30 |
-
function Magnifier({ imgUrl, regions, selectedId, mousePos, side, imgSize, focusModeB }) {
|
| 31 |
if (!imgUrl || !mousePos || mousePos.side !== side) return null;
|
| 32 |
|
| 33 |
const zoom = 2.5;
|
| 34 |
const size = 180;
|
| 35 |
const r = size / 2;
|
| 36 |
|
| 37 |
-
// Filtering regions based on side and Focus Mode
|
| 38 |
const visibleRegions = (side === 'B' && focusModeB)
|
| 39 |
? regions.filter(reg => reg.id === selectedId)
|
| 40 |
: regions;
|
| 41 |
|
| 42 |
-
// Background offset logic to keep mouse pointer at center
|
| 43 |
const bgPosX = -mousePos.x * imgSize.width * zoom + r;
|
| 44 |
const bgPosY = -mousePos.y * imgSize.height * zoom + r;
|
| 45 |
|
|
@@ -53,7 +51,6 @@ function Magnifier({ imgUrl, regions, selectedId, mousePos, side, imgSize, focus
|
|
| 53 |
top: mousePos.py - size - 40,
|
| 54 |
}}
|
| 55 |
>
|
| 56 |
-
{/* Background Image Source */}
|
| 57 |
<div
|
| 58 |
className="absolute inset-0 bg-[#111]"
|
| 59 |
style={{
|
|
@@ -64,7 +61,6 @@ function Magnifier({ imgUrl, regions, selectedId, mousePos, side, imgSize, focus
|
|
| 64 |
}}
|
| 65 |
/>
|
| 66 |
|
| 67 |
-
{/* SVG Overlay inside Magnifier */}
|
| 68 |
<svg className="absolute inset-0 w-full h-full overflow-visible" viewBox={`0 0 ${size} ${size}`}>
|
| 69 |
{visibleRegions.map(reg => {
|
| 70 |
const isSelected = reg.id === selectedId;
|
|
@@ -88,22 +84,24 @@ function Magnifier({ imgUrl, regions, selectedId, mousePos, side, imgSize, focus
|
|
| 88 |
strokeWidth: 1.5,
|
| 89 |
}}
|
| 90 |
/>
|
| 91 |
-
{localPoints.map((p, idx) =>
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
| 101 |
</g>
|
| 102 |
);
|
| 103 |
})}
|
| 104 |
</svg>
|
| 105 |
|
| 106 |
-
{/* Reticle */}
|
| 107 |
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
| 108 |
<div className="w-full h-[1px] bg-white/30 absolute" />
|
| 109 |
<div className="h-full w-[1px] bg-white/30 absolute" />
|
|
@@ -126,7 +124,6 @@ export default function App() {
|
|
| 126 |
const [previewMode, setPreviewMode] = useState(false);
|
| 127 |
const [focusModeB, setFocusModeB] = useState(false);
|
| 128 |
|
| 129 |
-
// Interaction & Keys State
|
| 130 |
const [hoveredEdge, setHoveredEdge] = useState(null);
|
| 131 |
const [hoveredPoint, setHoveredPoint] = useState(null);
|
| 132 |
const [draggingPoint, setDraggingPoint] = useState(null);
|
|
@@ -326,21 +323,67 @@ export default function App() {
|
|
| 326 |
};
|
| 327 |
}, [hoveredEdge, hoveredPoint]);
|
| 328 |
|
| 329 |
-
// --- Texture Mapping Core ---
|
| 330 |
const triangulate = (points) => {
|
|
|
|
| 331 |
const indices = points.map((_, i) => i);
|
| 332 |
const result = [];
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 337 |
for (let i = 0; i < indices.length; i++) {
|
| 338 |
-
|
| 339 |
-
indices
|
| 340 |
-
|
| 341 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
}
|
| 343 |
-
if (!
|
| 344 |
}
|
| 345 |
if (indices.length === 3) result.push(...indices);
|
| 346 |
return result;
|
|
@@ -421,10 +464,11 @@ export default function App() {
|
|
| 421 |
}}
|
| 422 |
/>
|
| 423 |
{points.map((p, idx) => {
|
| 424 |
-
const
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
|
|
|
| 428 |
|
| 429 |
return (
|
| 430 |
<circle
|
|
@@ -434,9 +478,9 @@ export default function App() {
|
|
| 434 |
className="pointer-events-auto cursor-move"
|
| 435 |
onMouseDown={(e) => handlePointMouseDown(e, side, r.id, idx)}
|
| 436 |
style={{
|
| 437 |
-
fill: isSelected ? '#fff' : r.color,
|
| 438 |
-
stroke:
|
| 439 |
-
strokeWidth:
|
| 440 |
vectorEffect: 'non-scaling-stroke'
|
| 441 |
}}
|
| 442 |
/>
|
|
@@ -461,7 +505,7 @@ export default function App() {
|
|
| 461 |
<div className="p-2.5 bg-indigo-600 rounded-xl shadow-lg shadow-indigo-500/20"><Target size={22} /></div>
|
| 462 |
<div>
|
| 463 |
<h1 className="text-lg font-black tracking-tighter leading-none italic">UV WRAPPER</h1>
|
| 464 |
-
<span className="text-[9px] text-indigo-400 font-bold uppercase tracking-[0.3em]">Mapping Engine v2.
|
| 465 |
</div>
|
| 466 |
</div>
|
| 467 |
|
|
@@ -512,7 +556,7 @@ export default function App() {
|
|
| 512 |
>
|
| 513 |
<img ref={imgRefA} src={imgA} className="max-w-full max-h-[72vh] block select-none rounded-sm" draggable={false} />
|
| 514 |
{renderSVG('A')}
|
| 515 |
-
{isZPressed && <Magnifier imgUrl={imgA} regions={regions} selectedId={selectedRegionId} mousePos={magnifierPos} side="A" imgSize={imgSizeA} focusModeB={focusModeB} />}
|
| 516 |
</div>
|
| 517 |
)}
|
| 518 |
</div>
|
|
@@ -543,7 +587,7 @@ export default function App() {
|
|
| 543 |
>
|
| 544 |
<img ref={imgRefB} src={imgB} className="max-w-full max-h-[72vh] block select-none rounded-sm" draggable={false} />
|
| 545 |
{renderSVG('B')}
|
| 546 |
-
{isZPressed && <Magnifier imgUrl={imgB} regions={regions} selectedId={selectedRegionId} mousePos={magnifierPos} side="B" imgSize={imgSizeB} focusModeB={focusModeB} />}
|
| 547 |
</div>
|
| 548 |
)}
|
| 549 |
</div>
|
|
|
|
| 27 |
};
|
| 28 |
|
| 29 |
// --- Magnifier Component ---
|
| 30 |
+
function Magnifier({ imgUrl, regions, selectedId, mousePos, side, imgSize, focusModeB, hoveredPoint }) {
|
| 31 |
if (!imgUrl || !mousePos || mousePos.side !== side) return null;
|
| 32 |
|
| 33 |
const zoom = 2.5;
|
| 34 |
const size = 180;
|
| 35 |
const r = size / 2;
|
| 36 |
|
|
|
|
| 37 |
const visibleRegions = (side === 'B' && focusModeB)
|
| 38 |
? regions.filter(reg => reg.id === selectedId)
|
| 39 |
: regions;
|
| 40 |
|
|
|
|
| 41 |
const bgPosX = -mousePos.x * imgSize.width * zoom + r;
|
| 42 |
const bgPosY = -mousePos.y * imgSize.height * zoom + r;
|
| 43 |
|
|
|
|
| 51 |
top: mousePos.py - size - 40,
|
| 52 |
}}
|
| 53 |
>
|
|
|
|
| 54 |
<div
|
| 55 |
className="absolute inset-0 bg-[#111]"
|
| 56 |
style={{
|
|
|
|
| 61 |
}}
|
| 62 |
/>
|
| 63 |
|
|
|
|
| 64 |
<svg className="absolute inset-0 w-full h-full overflow-visible" viewBox={`0 0 ${size} ${size}`}>
|
| 65 |
{visibleRegions.map(reg => {
|
| 66 |
const isSelected = reg.id === selectedId;
|
|
|
|
| 84 |
strokeWidth: 1.5,
|
| 85 |
}}
|
| 86 |
/>
|
| 87 |
+
{localPoints.map((p, idx) => {
|
| 88 |
+
const isHovered = hoveredPoint?.regionId === reg.id && hoveredPoint?.index === idx;
|
| 89 |
+
return (
|
| 90 |
+
<circle
|
| 91 |
+
key={idx}
|
| 92 |
+
cx={p.x} cy={p.y}
|
| 93 |
+
r={isSelected ? 4 : 2}
|
| 94 |
+
fill={isHovered ? '#00ffff' : (isSelected ? '#fff' : reg.color)}
|
| 95 |
+
stroke="#000"
|
| 96 |
+
strokeWidth={0.5}
|
| 97 |
+
/>
|
| 98 |
+
);
|
| 99 |
+
})}
|
| 100 |
</g>
|
| 101 |
);
|
| 102 |
})}
|
| 103 |
</svg>
|
| 104 |
|
|
|
|
| 105 |
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
| 106 |
<div className="w-full h-[1px] bg-white/30 absolute" />
|
| 107 |
<div className="h-full w-[1px] bg-white/30 absolute" />
|
|
|
|
| 124 |
const [previewMode, setPreviewMode] = useState(false);
|
| 125 |
const [focusModeB, setFocusModeB] = useState(false);
|
| 126 |
|
|
|
|
| 127 |
const [hoveredEdge, setHoveredEdge] = useState(null);
|
| 128 |
const [hoveredPoint, setHoveredPoint] = useState(null);
|
| 129 |
const [draggingPoint, setDraggingPoint] = useState(null);
|
|
|
|
| 323 |
};
|
| 324 |
}, [hoveredEdge, hoveredPoint]);
|
| 325 |
|
| 326 |
+
// --- Texture Mapping Core (Improved Ear Clipping) ---
|
| 327 |
const triangulate = (points) => {
|
| 328 |
+
if (points.length < 3) return [];
|
| 329 |
const indices = points.map((_, i) => i);
|
| 330 |
const result = [];
|
| 331 |
+
|
| 332 |
+
// Signed area for orientation
|
| 333 |
+
let area = 0;
|
| 334 |
+
for (let i = 0; i < points.length; i++) {
|
| 335 |
+
const p1 = points[i];
|
| 336 |
+
const p2 = points[(i + 1) % points.length];
|
| 337 |
+
area += (p2.x - p1.x) * (p2.y + p1.y);
|
| 338 |
+
}
|
| 339 |
+
const clockwise = area > 0;
|
| 340 |
+
|
| 341 |
+
const isPointInTriangle = (p, a, b, c) => {
|
| 342 |
+
const areaOrig = Math.abs((b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y));
|
| 343 |
+
const area1 = Math.abs((a.x - p.x) * (b.y - p.y) - (b.x - p.x) * (a.y - p.y));
|
| 344 |
+
const area2 = Math.abs((b.x - p.x) * (c.y - p.y) - (c.x - p.x) * (b.y - p.y));
|
| 345 |
+
const area3 = Math.abs((c.x - p.x) * (a.y - p.y) - (a.x - p.x) * (c.y - p.y));
|
| 346 |
+
return Math.abs(area1 + area2 + area3 - areaOrig) < 0.0001;
|
| 347 |
+
};
|
| 348 |
+
|
| 349 |
+
const isConvex = (pPrev, pCurr, pNext) => {
|
| 350 |
+
const val = (pCurr.x - pPrev.x) * (pNext.y - pPrev.y) - (pCurr.y - pPrev.y) * (pNext.x - pPrev.x);
|
| 351 |
+
return clockwise ? val < 0 : val > 0;
|
| 352 |
+
};
|
| 353 |
+
|
| 354 |
+
let limit = points.length * 10;
|
| 355 |
+
while (indices.length > 3 && limit > 0) {
|
| 356 |
+
limit--;
|
| 357 |
+
let earFound = false;
|
| 358 |
for (let i = 0; i < indices.length; i++) {
|
| 359 |
+
const prevIdx = indices[(i + indices.length - 1) % indices.length];
|
| 360 |
+
const currIdx = indices[i];
|
| 361 |
+
const nextIdx = indices[(i + 1) % indices.length];
|
| 362 |
+
|
| 363 |
+
const pPrev = points[prevIdx];
|
| 364 |
+
const pCurr = points[currIdx];
|
| 365 |
+
const pNext = points[nextIdx];
|
| 366 |
+
|
| 367 |
+
if (!isConvex(pPrev, pCurr, pNext)) continue;
|
| 368 |
+
|
| 369 |
+
let hasPointInside = false;
|
| 370 |
+
for (let j = 0; j < indices.length; j++) {
|
| 371 |
+
const pIdx = indices[j];
|
| 372 |
+
if (pIdx === prevIdx || pIdx === currIdx || pIdx === nextIdx) continue;
|
| 373 |
+
if (isPointInTriangle(points[pIdx], pPrev, pCurr, pNext)) {
|
| 374 |
+
hasPointInside = true;
|
| 375 |
+
break;
|
| 376 |
+
}
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
if (!hasPointInside) {
|
| 380 |
+
result.push(prevIdx, currIdx, nextIdx);
|
| 381 |
+
indices.splice(i, 1);
|
| 382 |
+
earFound = true;
|
| 383 |
+
break;
|
| 384 |
+
}
|
| 385 |
}
|
| 386 |
+
if (!earFound) break;
|
| 387 |
}
|
| 388 |
if (indices.length === 3) result.push(...indices);
|
| 389 |
return result;
|
|
|
|
| 464 |
}}
|
| 465 |
/>
|
| 466 |
{points.map((p, idx) => {
|
| 467 |
+
const isHoveredOrDragging = (hoveredPoint?.regionId === r.id && hoveredPoint?.index === idx) ||
|
| 468 |
+
(draggingPoint?.regionId === r.id && draggingPoint?.index === idx && draggingPoint.side === side);
|
| 469 |
+
|
| 470 |
+
// Point size logic: increased selected point size by 0.25
|
| 471 |
+
const finalRadius = isSelected ? 0.75 : 0.25;
|
| 472 |
|
| 473 |
return (
|
| 474 |
<circle
|
|
|
|
| 478 |
className="pointer-events-auto cursor-move"
|
| 479 |
onMouseDown={(e) => handlePointMouseDown(e, side, r.id, idx)}
|
| 480 |
style={{
|
| 481 |
+
fill: isHoveredOrDragging ? '#00ffff' : (isSelected ? '#fff' : r.color),
|
| 482 |
+
stroke: isHoveredOrDragging ? '#fff' : '#000',
|
| 483 |
+
strokeWidth: 0.2,
|
| 484 |
vectorEffect: 'non-scaling-stroke'
|
| 485 |
}}
|
| 486 |
/>
|
|
|
|
| 505 |
<div className="p-2.5 bg-indigo-600 rounded-xl shadow-lg shadow-indigo-500/20"><Target size={22} /></div>
|
| 506 |
<div>
|
| 507 |
<h1 className="text-lg font-black tracking-tighter leading-none italic">UV WRAPPER</h1>
|
| 508 |
+
<span className="text-[9px] text-indigo-400 font-bold uppercase tracking-[0.3em]">Mapping Engine v2.7</span>
|
| 509 |
</div>
|
| 510 |
</div>
|
| 511 |
|
|
|
|
| 556 |
>
|
| 557 |
<img ref={imgRefA} src={imgA} className="max-w-full max-h-[72vh] block select-none rounded-sm" draggable={false} />
|
| 558 |
{renderSVG('A')}
|
| 559 |
+
{isZPressed && <Magnifier imgUrl={imgA} regions={regions} selectedId={selectedRegionId} mousePos={magnifierPos} side="A" imgSize={imgSizeA} focusModeB={focusModeB} hoveredPoint={hoveredPoint} />}
|
| 560 |
</div>
|
| 561 |
)}
|
| 562 |
</div>
|
|
|
|
| 587 |
>
|
| 588 |
<img ref={imgRefB} src={imgB} className="max-w-full max-h-[72vh] block select-none rounded-sm" draggable={false} />
|
| 589 |
{renderSVG('B')}
|
| 590 |
+
{isZPressed && <Magnifier imgUrl={imgB} regions={regions} selectedId={selectedRegionId} mousePos={magnifierPos} side="B" imgSize={imgSizeB} focusModeB={focusModeB} hoveredPoint={hoveredPoint} />}
|
| 591 |
</div>
|
| 592 |
)}
|
| 593 |
</div>
|