@broberg/bodymap 0.10.0 → 0.12.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.d.cts CHANGED
@@ -260,6 +260,26 @@ declare function seamWeight(seamEnabled: boolean, blend: number): number;
260
260
  * Returns true when it computed (the model had none, and would otherwise render
261
261
  * flat), false when it left the authored normals alone.
262
262
  */
263
+ /**
264
+ * F052.32 — is this pointer-up a TAP, or the end of a drag?
265
+ *
266
+ * DISTANCE IS THE WHOLE TEST. A drag moves; a tap does not. There used to be a
267
+ * 450 ms ceiling beside it, and it did not help tell those apart — what it did
268
+ * was rule that a SLOW press is not a tap.
269
+ *
270
+ * A slow press is exactly what a CAREFUL tap is. Aiming at a small body part on
271
+ * a phone, at a mark you are trying to hit again, takes longer than a casual
272
+ * poke — so the ceiling discarded precisely the taps a user was trying hardest
273
+ * to make, on a health app whose users are in pain.
274
+ *
275
+ * And it discarded them in SILENCE: the guard returned from the whole handler,
276
+ * so there was no pick, no clear, no feedback and no repaint. The owner reported
277
+ * it twice, as "the remove button is gone" and as "the sound does not work". One
278
+ * swallowed tap, two sightings, each explained away separately.
279
+ */
280
+ declare function isTap(dx: number, dy: number, maxDistance?: number): boolean;
281
+ /** Pixels of travel a tap may have. Beyond this it was a rotation. */
282
+ declare const TAP_MAX_DISTANCE = 6;
263
283
  declare function ensureNormals(geo: {
264
284
  getAttribute(name: string): unknown;
265
285
  computeVertexNormals(): void;
@@ -351,6 +371,56 @@ interface BodyMap3DProps {
351
371
  haptics?: boolean;
352
372
  className?: string;
353
373
  }
374
+ /**
375
+ * F052.30 — the controls now READ THE PALETTE.
376
+ *
377
+ * These were module-level constants with the colours written in: #e2e8f0 border,
378
+ * #fff surface, #0e8f8a accent. Three of those already existed as `chrome`
379
+ * fields and were simply not used, so a consumer who set `ui.border` saw it on
380
+ * the card and NOT on the buttons inside the card — in a component whose whole
381
+ * selling point is that the consuming app themes it.
382
+ *
383
+ * They take `chrome` now, which is why they are functions rather than
384
+ * constants: there is no correct value to compute before the palette is known.
385
+ */
386
+ /**
387
+ * The intensity circle — 46px, and every one of the eleven takes it.
388
+ *
389
+ * THE SIZE IS NOT A DESIGN CHOICE, it is the sibling's. Measured while making
390
+ * these round: the 2D renderer's `.bmap__i` is 38px and **46px on touch**; this
391
+ * renderer's were **30px with no touch override at all**. F052.8 set a >=44px
392
+ * floor for this component because a phone is its primary surface, and F052.11
393
+ * made accessibility a legal duty here — so the 3D half had been under the floor
394
+ * since it shipped, and the two halves had quietly diverged.
395
+ *
396
+ * Same shape as F052.32's tap guard: 2D correct, 3D not, nobody looking. Asking
397
+ * for circles is what surfaced it — the second time this week that making
398
+ * something explicit uncovered a defect that was already there (F052.30's accent
399
+ * was the first).
400
+ *
401
+ * 46 rather than 44 so the two renderers carry ONE number instead of two that
402
+ * both happen to pass. The widest label («10») fits at the shipped font size,
403
+ * and all eleven take the same box so none is an ellipse among circles.
404
+ */
405
+ declare const INTENSITY_SIZE = 46;
406
+ /**
407
+ * F052.33 — should the help note be on screen at all?
408
+ *
409
+ * Extracted because the renderer's own answer is unreachable from a unit test:
410
+ * happy-dom has no WebGL, so no region can be selected, and a test written
411
+ * against the component asserts the note is PRESENT twice and never that it
412
+ * disappears. I wrote that test and it was green and vacuous.
413
+ *
414
+ * `openRegion` wins over everything. A note reading «tap a body part» while the
415
+ * picker is open is wrong advice, not a layering problem — and it was covering
416
+ * the panel's close control, the third exit hidden under something this week.
417
+ */
418
+ declare function shouldShowHint(opts: {
419
+ openRegion: string | null;
420
+ hint: false | BodyMap3DHint | undefined;
421
+ anySelectable: boolean;
422
+ hasExplicitHoverHint: boolean;
423
+ }): boolean;
354
424
  declare function BodyMap3D(props: BodyMap3DProps): react.JSX.Element;
355
425
 
356
- export { BodyMap3D, type BodyMap3DHint, type BodyMap3DModels, type BodyMap3DProps, type BodyMap3DSex, type BodyMap3DUiLabels, ensureNormals, seamBlend, seamWeight, serializeReport };
426
+ export { BodyMap3D, type BodyMap3DHint, type BodyMap3DModels, type BodyMap3DProps, type BodyMap3DSex, type BodyMap3DUiLabels, INTENSITY_SIZE, TAP_MAX_DISTANCE, ensureNormals, isTap, seamBlend, seamWeight, serializeReport, shouldShowHint };
package/dist/three.d.ts CHANGED
@@ -260,6 +260,26 @@ declare function seamWeight(seamEnabled: boolean, blend: number): number;
260
260
  * Returns true when it computed (the model had none, and would otherwise render
261
261
  * flat), false when it left the authored normals alone.
262
262
  */
263
+ /**
264
+ * F052.32 — is this pointer-up a TAP, or the end of a drag?
265
+ *
266
+ * DISTANCE IS THE WHOLE TEST. A drag moves; a tap does not. There used to be a
267
+ * 450 ms ceiling beside it, and it did not help tell those apart — what it did
268
+ * was rule that a SLOW press is not a tap.
269
+ *
270
+ * A slow press is exactly what a CAREFUL tap is. Aiming at a small body part on
271
+ * a phone, at a mark you are trying to hit again, takes longer than a casual
272
+ * poke — so the ceiling discarded precisely the taps a user was trying hardest
273
+ * to make, on a health app whose users are in pain.
274
+ *
275
+ * And it discarded them in SILENCE: the guard returned from the whole handler,
276
+ * so there was no pick, no clear, no feedback and no repaint. The owner reported
277
+ * it twice, as "the remove button is gone" and as "the sound does not work". One
278
+ * swallowed tap, two sightings, each explained away separately.
279
+ */
280
+ declare function isTap(dx: number, dy: number, maxDistance?: number): boolean;
281
+ /** Pixels of travel a tap may have. Beyond this it was a rotation. */
282
+ declare const TAP_MAX_DISTANCE = 6;
263
283
  declare function ensureNormals(geo: {
264
284
  getAttribute(name: string): unknown;
265
285
  computeVertexNormals(): void;
@@ -351,6 +371,56 @@ interface BodyMap3DProps {
351
371
  haptics?: boolean;
352
372
  className?: string;
353
373
  }
374
+ /**
375
+ * F052.30 — the controls now READ THE PALETTE.
376
+ *
377
+ * These were module-level constants with the colours written in: #e2e8f0 border,
378
+ * #fff surface, #0e8f8a accent. Three of those already existed as `chrome`
379
+ * fields and were simply not used, so a consumer who set `ui.border` saw it on
380
+ * the card and NOT on the buttons inside the card — in a component whose whole
381
+ * selling point is that the consuming app themes it.
382
+ *
383
+ * They take `chrome` now, which is why they are functions rather than
384
+ * constants: there is no correct value to compute before the palette is known.
385
+ */
386
+ /**
387
+ * The intensity circle — 46px, and every one of the eleven takes it.
388
+ *
389
+ * THE SIZE IS NOT A DESIGN CHOICE, it is the sibling's. Measured while making
390
+ * these round: the 2D renderer's `.bmap__i` is 38px and **46px on touch**; this
391
+ * renderer's were **30px with no touch override at all**. F052.8 set a >=44px
392
+ * floor for this component because a phone is its primary surface, and F052.11
393
+ * made accessibility a legal duty here — so the 3D half had been under the floor
394
+ * since it shipped, and the two halves had quietly diverged.
395
+ *
396
+ * Same shape as F052.32's tap guard: 2D correct, 3D not, nobody looking. Asking
397
+ * for circles is what surfaced it — the second time this week that making
398
+ * something explicit uncovered a defect that was already there (F052.30's accent
399
+ * was the first).
400
+ *
401
+ * 46 rather than 44 so the two renderers carry ONE number instead of two that
402
+ * both happen to pass. The widest label («10») fits at the shipped font size,
403
+ * and all eleven take the same box so none is an ellipse among circles.
404
+ */
405
+ declare const INTENSITY_SIZE = 46;
406
+ /**
407
+ * F052.33 — should the help note be on screen at all?
408
+ *
409
+ * Extracted because the renderer's own answer is unreachable from a unit test:
410
+ * happy-dom has no WebGL, so no region can be selected, and a test written
411
+ * against the component asserts the note is PRESENT twice and never that it
412
+ * disappears. I wrote that test and it was green and vacuous.
413
+ *
414
+ * `openRegion` wins over everything. A note reading «tap a body part» while the
415
+ * picker is open is wrong advice, not a layering problem — and it was covering
416
+ * the panel's close control, the third exit hidden under something this week.
417
+ */
418
+ declare function shouldShowHint(opts: {
419
+ openRegion: string | null;
420
+ hint: false | BodyMap3DHint | undefined;
421
+ anySelectable: boolean;
422
+ hasExplicitHoverHint: boolean;
423
+ }): boolean;
354
424
  declare function BodyMap3D(props: BodyMap3DProps): react.JSX.Element;
355
425
 
356
- export { BodyMap3D, type BodyMap3DHint, type BodyMap3DModels, type BodyMap3DProps, type BodyMap3DSex, type BodyMap3DUiLabels, ensureNormals, seamBlend, seamWeight, serializeReport };
426
+ export { BodyMap3D, type BodyMap3DHint, type BodyMap3DModels, type BodyMap3DProps, type BodyMap3DSex, type BodyMap3DUiLabels, INTENSITY_SIZE, TAP_MAX_DISTANCE, ensureNormals, isTap, seamBlend, seamWeight, serializeReport, shouldShowHint };
package/dist/three.js CHANGED
@@ -260,6 +260,10 @@ function seamBlend(nearestSq, secondSq) {
260
260
  function seamWeight(seamEnabled, blend) {
261
261
  return seamEnabled ? blend : 0;
262
262
  }
263
+ function isTap(dx, dy, maxDistance = TAP_MAX_DISTANCE) {
264
+ return Math.hypot(dx, dy) <= maxDistance;
265
+ }
266
+ var TAP_MAX_DISTANCE = 6;
263
267
  function ensureNormals(geo) {
264
268
  if (geo.getAttribute("normal")) return false;
265
269
  geo.computeVertexNormals();
@@ -285,6 +289,12 @@ function webglAvailable() {
285
289
  return false;
286
290
  }
287
291
  }
292
+ var INTENSITY_SIZE = 46;
293
+ function shouldShowHint(opts) {
294
+ if (opts.openRegion) return false;
295
+ if (opts.hint === false) return false;
296
+ return opts.anySelectable || opts.hasExplicitHoverHint;
297
+ }
288
298
  var btn = (c) => ({
289
299
  font: "inherit",
290
300
  cursor: "pointer",
@@ -573,12 +583,11 @@ function BodyMap3D(props) {
573
583
  }
574
584
  return best;
575
585
  };
576
- let downX = 0, downY = 0, downT = 0;
586
+ let downX = 0, downY = 0;
577
587
  const canvas = renderer.domElement;
578
588
  const onDown = (e) => {
579
589
  downX = e.clientX;
580
590
  downY = e.clientY;
581
- downT = Date.now();
582
591
  };
583
592
  const onMove = (e) => {
584
593
  if ((e.buttons || 0) !== 0) return;
@@ -590,7 +599,7 @@ function BodyMap3D(props) {
590
599
  renderFrame();
591
600
  };
592
601
  const onUp = (e) => {
593
- if (Math.hypot(e.clientX - downX, e.clientY - downY) > 6 || Date.now() - downT > 450) return;
602
+ if (!isTap(e.clientX - downX, e.clientY - downY)) return;
594
603
  const k = pick(e.clientX, e.clientY);
595
604
  if (!k) return;
596
605
  const outcome = pickRef.current(k);
@@ -680,7 +689,12 @@ function BodyMap3D(props) {
680
689
  const region = selected ? REGIONS.find((r) => r.key === selected) : null;
681
690
  const current = selected ? pointOf(selected) : void 0;
682
691
  const anySelectable = REGIONS.some((r) => isSelectable(r.key, config ?? {}));
683
- const showEmptyHint = hint === false ? false : anySelectable || ui?.hoverHint !== void 0;
692
+ const showEmptyHint = shouldShowHint({
693
+ openRegion: selected,
694
+ hint,
695
+ anySelectable,
696
+ hasExplicitHoverHint: ui?.hoverHint !== void 0
697
+ });
684
698
  const hintText = (hint ? hint.text : void 0) ?? UI.hoverHint;
685
699
  const hintAtStage = isFullscreen && !!hint && hint.placement === "stage-bottom";
686
700
  return /* @__PURE__ */ jsxs(
@@ -788,13 +802,23 @@ function BodyMap3D(props) {
788
802
  "data-testid": "bodymap3d-close",
789
803
  "aria-label": L.close,
790
804
  onClick: () => setSelected(null),
791
- style: { ...btn(chrome), width: 32, height: 32, padding: 0, fontSize: 20, lineHeight: 1, color: chrome.mutedText, borderColor: chrome.border, background: chrome.panelBg },
805
+ style: { ...btn(chrome), width: 40, height: 40, minWidth: 40, padding: 0, borderRadius: "50%", fontSize: 20, lineHeight: 1, color: chrome.mutedText, borderColor: chrome.border, background: chrome.panelBg },
792
806
  children: "\xD7"
793
807
  }
794
808
  )
795
809
  ] }),
796
810
  /* @__PURE__ */ jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.intensity }),
797
- /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 4, marginBottom: 12 }, children: Array.from({ length: 11 }, (_, i) => /* @__PURE__ */ jsx("button", { "data-testid": `bodymap3d-intensity-${i}`, onClick: () => setPain(region.key, i, current?.type), style: { ...btn(chrome), display: "inline-flex", alignItems: "center", justifyContent: "center", minWidth: 30, height: 30, padding: 0, background: current?.intensity === i ? chrome.accent : chrome.panelBg, color: current?.intensity === i ? chrome.accentText : chrome.text }, children: i }, i)) }),
811
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 4, marginBottom: 12 }, children: Array.from({ length: 11 }, (_, i) => (
812
+ /* F052.34 — CIRCLES, and all eleven the same size.
813
+ Dimensioned by «10», the widest label: sizing by the average
814
+ would make the two-digit one an ellipse among circles, which
815
+ is worse than eleven rounded squares because the odd one out
816
+ becomes a different SHAPE rather than slightly wider.
817
+ 46px is the SIBLING's touch size, not a new number: the 2D
818
+ renderer already used it and this one was on 30px, under the
819
+ >=44px floor F052.8 set. See INTENSITY_SIZE. */
820
+ /* @__PURE__ */ jsx("button", { "data-testid": `bodymap3d-intensity-${i}`, onClick: () => setPain(region.key, i, current?.type), style: { ...btn(chrome), display: "inline-flex", alignItems: "center", justifyContent: "center", width: INTENSITY_SIZE, height: INTENSITY_SIZE, minWidth: INTENSITY_SIZE, padding: 0, borderRadius: "50%", background: current?.intensity === i ? chrome.accent : chrome.panelBg, color: current?.intensity === i ? chrome.accentText : chrome.text }, children: i }, i)
821
+ )) }),
798
822
  /* @__PURE__ */ jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.quality }),
799
823
  /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 14 }, children: PAIN_TYPES.map((t) => /* @__PURE__ */ jsx("button", { "data-testid": `bodymap3d-type-${t}`, onClick: () => setPain(region.key, current?.intensity ?? 5, t), style: { ...btn(chrome), borderRadius: 999, padding: "6px 12px", background: current?.type === t ? chrome.text : chrome.panelBg, color: current?.type === t ? chrome.panelBg : chrome.mutedText }, children: L.qualities[t] ?? t }, t)) }),
800
824
  current && /* @__PURE__ */ jsx("button", { "data-testid": "bodymap3d-remove", onClick: () => removePain(region.key), style: { ...btn(chrome), color: chrome.danger, borderColor: chrome.dangerBorder }, children: L.remove })
@@ -830,6 +854,6 @@ function BodyMap3D(props) {
830
854
  );
831
855
  }
832
856
 
833
- export { BodyMap3D, ensureNormals, seamBlend, seamWeight, serializeReport };
857
+ export { BodyMap3D, INTENSITY_SIZE, TAP_MAX_DISTANCE, ensureNormals, isTap, seamBlend, seamWeight, serializeReport, shouldShowHint };
834
858
  //# sourceMappingURL=three.js.map
835
859
  //# sourceMappingURL=three.js.map