MAO commited on
Commit
0d52f5b
·
1 Parent(s): d504d0f

Enhance: Improve Triangulation Logic and Magnifier UI v2.7

Browse files
Files changed (1) hide show
  1. 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
- <circle
93
- key={idx}
94
- cx={p.x} cy={p.y}
95
- r={isSelected ? 4 : 2}
96
- fill={isSelected ? '#fff' : reg.color}
97
- stroke="#000"
98
- strokeWidth={0.5}
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
- let count = 0;
334
- while (indices.length > 3 && count < 500) {
335
- count++;
336
- let found = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  for (let i = 0; i < indices.length; i++) {
338
- result.push(indices[(i + indices.length - 1) % indices.length], indices[i], indices[(i + 1) % indices.length]);
339
- indices.splice(i, 1);
340
- found = true;
341
- break;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
  }
343
- if (!found) break;
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 isHovered = hoveredPoint?.regionId === r.id && hoveredPoint?.index === idx;
425
- const isDrag = draggingPoint?.regionId === r.id && draggingPoint?.index === idx && draggingPoint.side === side;
426
- const baseRadius = isSelected ? 1.0 : 0.5;
427
- const finalRadius = isHovered || isDrag ? 1.8 : baseRadius;
 
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: (isHovered || isDrag) ? '#00e5ff' : '#000',
439
- strokeWidth: isSelected ? 0.4 : 0.2,
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.5</span>
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>