@broberg/bodymap 0.2.8 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/three.cjs CHANGED
@@ -84,6 +84,25 @@ function isSelectable(key, config = {}) {
84
84
  if (s?.visible === false) return false;
85
85
  return s?.selectable ?? true;
86
86
  }
87
+ var VIBRATION_PATTERNS = {
88
+ select: [12],
89
+ clear: [8, 40, 8],
90
+ ignore: []
91
+ };
92
+ function requestVibration(pattern, nav = globalThis.navigator) {
93
+ if (pattern.length === 0) return "skipped";
94
+ if (typeof nav?.vibrate !== "function") return "unsupported";
95
+ try {
96
+ return nav.vibrate([...pattern]) ? "requested" : "declined";
97
+ } catch {
98
+ return "declined";
99
+ }
100
+ }
101
+ function emitFeedback(outcome, region, opts = {}) {
102
+ opts.onFeedback?.({ outcome, region });
103
+ if (opts.haptics === false) return "skipped";
104
+ return requestVibration(VIBRATION_PATTERNS[outcome], opts.nav);
105
+ }
87
106
  var defaultUi = {
88
107
  text: "#1e293b",
89
108
  mutedText: "#475569",
@@ -236,6 +255,17 @@ var ANCHORS = {
236
255
  foot_left: [-0.1, 0.02, 0.11],
237
256
  foot_right: [0.1, 0.02, 0.11]
238
257
  };
258
+ var SEAM = 0.18;
259
+ function seamBlend(nearestSq, secondSq) {
260
+ const d1 = Math.sqrt(nearestSq), d2 = Math.sqrt(secondSq);
261
+ const sum = d1 + d2;
262
+ if (!(sum > 0)) return 0;
263
+ const ratio = d1 / sum;
264
+ const t = (ratio - (0.5 - SEAM)) / SEAM;
265
+ if (t <= 0) return 0;
266
+ if (t >= 1) return 0.5;
267
+ return t * t * (3 - 2 * t) * 0.5;
268
+ }
239
269
  var ANCHOR_KEYS = Object.keys(ANCHORS);
240
270
  for (const k of ANCHOR_KEYS) ANCHORS[k][0] = -ANCHORS[k][0];
241
271
  function mergeLabels(locale, overrides) {
@@ -276,6 +306,8 @@ function BodyMap3D(props) {
276
306
  autoRotate = true,
277
307
  canvasHeight = "60vh",
278
308
  showRegionCode = true,
309
+ onFeedback,
310
+ haptics,
279
311
  className
280
312
  } = props;
281
313
  const chrome = uiColors(palette);
@@ -378,6 +410,8 @@ function BodyMap3D(props) {
378
410
  let modelRoot = null;
379
411
  let bodyMesh = null;
380
412
  let vertexRegion = [];
413
+ let vertexNeighbour = [];
414
+ let vertexBlend = new Float32Array(0);
381
415
  let colorAttr = null;
382
416
  let hovered = null;
383
417
  const loader = new GLTFLoader_js.GLTFLoader();
@@ -389,14 +423,32 @@ function BodyMap3D(props) {
389
423
  if (selectedRef.current === key) return paletteRef.current.selected;
390
424
  return baseColorFor(key, paletteRef.current);
391
425
  };
392
- const colorRegion = (key, hex) => {
426
+ const tmp2 = new THREE__namespace.Color();
427
+ const colourOf = (key) => hovered === key ? paletteRef.current.hover : restingHex(key);
428
+ const paint = () => {
393
429
  if (!colorAttr) return;
394
- tmp.set(hex);
395
- for (let i = 0; i < vertexRegion.length; i++) if (vertexRegion[i] === key) colorAttr.setXYZ(i, tmp.r, tmp.g, tmp.b);
430
+ const cache = /* @__PURE__ */ new Map();
431
+ const col = (k) => {
432
+ let c = cache.get(k);
433
+ if (!c) {
434
+ c = new THREE__namespace.Color(colourOf(k));
435
+ cache.set(k, c);
436
+ }
437
+ return c;
438
+ };
439
+ for (let i = 0; i < vertexRegion.length; i++) {
440
+ tmp.copy(col(vertexRegion[i]));
441
+ const w = vertexBlend[i];
442
+ if (w > 0) {
443
+ tmp2.copy(col(vertexNeighbour[i]));
444
+ tmp.lerp(tmp2, w);
445
+ }
446
+ colorAttr.setXYZ(i, tmp.r, tmp.g, tmp.b);
447
+ }
396
448
  colorAttr.needsUpdate = true;
397
449
  };
398
450
  const refresh = () => {
399
- for (const k of ANCHOR_KEYS) colorRegion(k, hovered === k ? paletteRef.current.hover : restingHex(k));
451
+ paint();
400
452
  renderFrame();
401
453
  };
402
454
  const loadModel = (which) => {
@@ -425,18 +477,27 @@ function BodyMap3D(props) {
425
477
  const n = pos.count;
426
478
  const cols = new Float32Array(n * 3);
427
479
  vertexRegion = new Array(n);
480
+ vertexNeighbour = new Array(n);
481
+ vertexBlend = new Float32Array(n);
428
482
  const v = new THREE__namespace.Vector3();
429
483
  for (let i = 0; i < n; i++) {
430
484
  v.fromBufferAttribute(pos, i).applyMatrix4(bodyMesh.matrixWorld);
431
- let best = 0, bd = Infinity;
485
+ let best = 0, second = 0, bd = Infinity, sd = Infinity;
432
486
  for (let a = 0; a < anchorVecs.length; a++) {
433
487
  const d = v.distanceToSquared(anchorVecs[a]);
434
488
  if (d < bd) {
489
+ sd = bd;
490
+ second = best;
435
491
  bd = d;
436
492
  best = a;
493
+ } else if (d < sd) {
494
+ sd = d;
495
+ second = a;
437
496
  }
438
497
  }
439
498
  vertexRegion[i] = ANCHOR_KEYS[best];
499
+ vertexNeighbour[i] = ANCHOR_KEYS[second];
500
+ vertexBlend[i] = seamBlend(bd, sd);
440
501
  }
441
502
  colorAttr = new THREE__namespace.BufferAttribute(cols, 3);
442
503
  geo.setAttribute("color", colorAttr);
@@ -496,10 +557,8 @@ function BodyMap3D(props) {
496
557
  if ((e.buttons || 0) !== 0) return;
497
558
  const k = pick(e.clientX, e.clientY);
498
559
  if (k === hovered) return;
499
- const prev = hovered;
500
560
  hovered = k;
501
- if (prev) colorRegion(prev, restingHex(prev));
502
- if (k) colorRegion(k, paletteRef.current.hover);
561
+ paint();
503
562
  canvas.style.cursor = k ? "pointer" : "default";
504
563
  renderFrame();
505
564
  };
@@ -569,6 +628,7 @@ function BodyMap3D(props) {
569
628
  };
570
629
  pickRef.current = (k) => {
571
630
  const what = decidePick(k, reportRef.current, configRef.current ?? {});
631
+ emitFeedback(what, k, { onFeedback, haptics });
572
632
  if (what === "clear") removePain(k);
573
633
  else if (what === "select") setSelected(k);
574
634
  return what;
@@ -609,6 +669,7 @@ function BodyMap3D(props) {
609
669
  }
610
670
 
611
671
  exports.BodyMap3D = BodyMap3D;
672
+ exports.seamBlend = seamBlend;
612
673
  exports.serializeReport = serializeReport;
613
674
  //# sourceMappingURL=three.cjs.map
614
675
  //# sourceMappingURL=three.cjs.map