@broberg/bodymap 0.9.0 → 0.11.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
@@ -205,6 +205,16 @@ interface BodyMap3DUiLabels {
205
205
  female: string;
206
206
  hoverHint: string;
207
207
  }
208
+ interface BodyMap3DHint {
209
+ /** Overrides the localized default. `ui.hoverHint` still works too. */
210
+ text?: string;
211
+ /**
212
+ * `after-canvas` (default) keeps today's flow position.
213
+ * `stage-bottom` pins it inside the fullscreen surface, above the home
214
+ * indicator — and falls back to the flow position when not fullscreen.
215
+ */
216
+ placement?: "after-canvas" | "stage-bottom";
217
+ }
208
218
  /**
209
219
  * How much of the RUNNER-UP region bleeds into this vertex (F052.21).
210
220
  *
@@ -250,6 +260,26 @@ declare function seamWeight(seamEnabled: boolean, blend: number): number;
250
260
  * Returns true when it computed (the model had none, and would otherwise render
251
261
  * flat), false when it left the authored normals alone.
252
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;
253
283
  declare function ensureNormals(geo: {
254
284
  getAttribute(name: string): unknown;
255
285
  computeVertexNormals(): void;
@@ -265,6 +295,24 @@ interface BodyMap3DProps {
265
295
  palette?: BodymapPalette;
266
296
  locale?: BodyMapLocale;
267
297
  labels?: Partial<BodyMapLabels>;
298
+ /**
299
+ * The help note under the body. F052.31.
300
+ *
301
+ * `false` renders NOTHING — no box, no border, no empty element. That path did
302
+ * not exist before: `ui.hoverHint = ""` produced an empty bordered box, which
303
+ * is worse than the sentence it replaced.
304
+ *
305
+ * `placement` is what fixes the defect rather than moving it. In the boxed
306
+ * layout the note belongs after the canvas; in FULLSCREEN that column wraps
307
+ * beneath a full-height canvas and the note lands past the bottom edge —
308
+ * measured by a consumer at 858-912 in an 852px window, i.e. entirely
309
+ * offscreen. `stage-bottom` anchors it inside the fullscreen surface instead,
310
+ * clear of the home indicator.
311
+ *
312
+ * A consumer cannot make that call in CSS without knowing our internal
313
+ * structure, which is exactly why two of them had to write wedges against it.
314
+ */
315
+ hint?: false | BodyMap3DHint;
268
316
  /** Override the 3D-only control strings (male/female/hoverHint). */
269
317
  ui?: Partial<BodyMap3DUiLabels>;
270
318
  defaultSex?: BodyMap3DSex;
@@ -325,4 +373,4 @@ interface BodyMap3DProps {
325
373
  }
326
374
  declare function BodyMap3D(props: BodyMap3DProps): react.JSX.Element;
327
375
 
328
- export { BodyMap3D, type BodyMap3DModels, type BodyMap3DProps, type BodyMap3DSex, type BodyMap3DUiLabels, ensureNormals, seamBlend, seamWeight, serializeReport };
376
+ export { BodyMap3D, type BodyMap3DHint, type BodyMap3DModels, type BodyMap3DProps, type BodyMap3DSex, type BodyMap3DUiLabels, TAP_MAX_DISTANCE, ensureNormals, isTap, seamBlend, seamWeight, serializeReport };
package/dist/three.d.ts CHANGED
@@ -205,6 +205,16 @@ interface BodyMap3DUiLabels {
205
205
  female: string;
206
206
  hoverHint: string;
207
207
  }
208
+ interface BodyMap3DHint {
209
+ /** Overrides the localized default. `ui.hoverHint` still works too. */
210
+ text?: string;
211
+ /**
212
+ * `after-canvas` (default) keeps today's flow position.
213
+ * `stage-bottom` pins it inside the fullscreen surface, above the home
214
+ * indicator — and falls back to the flow position when not fullscreen.
215
+ */
216
+ placement?: "after-canvas" | "stage-bottom";
217
+ }
208
218
  /**
209
219
  * How much of the RUNNER-UP region bleeds into this vertex (F052.21).
210
220
  *
@@ -250,6 +260,26 @@ declare function seamWeight(seamEnabled: boolean, blend: number): number;
250
260
  * Returns true when it computed (the model had none, and would otherwise render
251
261
  * flat), false when it left the authored normals alone.
252
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;
253
283
  declare function ensureNormals(geo: {
254
284
  getAttribute(name: string): unknown;
255
285
  computeVertexNormals(): void;
@@ -265,6 +295,24 @@ interface BodyMap3DProps {
265
295
  palette?: BodymapPalette;
266
296
  locale?: BodyMapLocale;
267
297
  labels?: Partial<BodyMapLabels>;
298
+ /**
299
+ * The help note under the body. F052.31.
300
+ *
301
+ * `false` renders NOTHING — no box, no border, no empty element. That path did
302
+ * not exist before: `ui.hoverHint = ""` produced an empty bordered box, which
303
+ * is worse than the sentence it replaced.
304
+ *
305
+ * `placement` is what fixes the defect rather than moving it. In the boxed
306
+ * layout the note belongs after the canvas; in FULLSCREEN that column wraps
307
+ * beneath a full-height canvas and the note lands past the bottom edge —
308
+ * measured by a consumer at 858-912 in an 852px window, i.e. entirely
309
+ * offscreen. `stage-bottom` anchors it inside the fullscreen surface instead,
310
+ * clear of the home indicator.
311
+ *
312
+ * A consumer cannot make that call in CSS without knowing our internal
313
+ * structure, which is exactly why two of them had to write wedges against it.
314
+ */
315
+ hint?: false | BodyMap3DHint;
268
316
  /** Override the 3D-only control strings (male/female/hoverHint). */
269
317
  ui?: Partial<BodyMap3DUiLabels>;
270
318
  defaultSex?: BodyMap3DSex;
@@ -325,4 +373,4 @@ interface BodyMap3DProps {
325
373
  }
326
374
  declare function BodyMap3D(props: BodyMap3DProps): react.JSX.Element;
327
375
 
328
- export { BodyMap3D, type BodyMap3DModels, type BodyMap3DProps, type BodyMap3DSex, type BodyMap3DUiLabels, ensureNormals, seamBlend, seamWeight, serializeReport };
376
+ export { BodyMap3D, type BodyMap3DHint, type BodyMap3DModels, type BodyMap3DProps, type BodyMap3DSex, type BodyMap3DUiLabels, TAP_MAX_DISTANCE, ensureNormals, isTap, seamBlend, seamWeight, serializeReport };
package/dist/three.js CHANGED
@@ -212,8 +212,8 @@ var LABELS_EN = {
212
212
  zoomOut: "Zoom out",
213
213
  zoomReset: "Reset zoom"
214
214
  };
215
- var UI_DA = { male: "Mand", female: "Kvinde", hoverHint: "Hover for at fremh\xE6ve \xB7 klik en kropsdel for at markere smerte.", expand: "Vis stor", collapse: "Luk stor visning" };
216
- var UI_EN = { male: "Male", female: "Female", hoverHint: "Hover to highlight \xB7 tap a body part to mark pain.", expand: "Expand", collapse: "Close" };
215
+ var UI_DA = { male: "Mand", female: "Kvinde", hoverHint: "Tr\xE6k for at dreje \xB7 tryk en kropsdel for at markere smerte.", expand: "Vis stor", collapse: "Luk stor visning" };
216
+ var UI_EN = { male: "Male", female: "Female", hoverHint: "Drag to rotate \xB7 tap a body part to mark pain.", expand: "Expand", collapse: "Close" };
217
217
  var ANCHORS = {
218
218
  head: [0, 1.79, 0.02],
219
219
  neck: [0, 1.57, 0],
@@ -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();
@@ -320,6 +324,7 @@ function BodyMap3D(props) {
320
324
  onFeedback,
321
325
  haptics,
322
326
  seam = false,
327
+ hint,
323
328
  fullscreen: fullscreenProp,
324
329
  onFullscreenChange,
325
330
  showFullscreenButton = true,
@@ -572,12 +577,11 @@ function BodyMap3D(props) {
572
577
  }
573
578
  return best;
574
579
  };
575
- let downX = 0, downY = 0, downT = 0;
580
+ let downX = 0, downY = 0;
576
581
  const canvas = renderer.domElement;
577
582
  const onDown = (e) => {
578
583
  downX = e.clientX;
579
584
  downY = e.clientY;
580
- downT = Date.now();
581
585
  };
582
586
  const onMove = (e) => {
583
587
  if ((e.buttons || 0) !== 0) return;
@@ -589,7 +593,7 @@ function BodyMap3D(props) {
589
593
  renderFrame();
590
594
  };
591
595
  const onUp = (e) => {
592
- if (Math.hypot(e.clientX - downX, e.clientY - downY) > 6 || Date.now() - downT > 450) return;
596
+ if (!isTap(e.clientX - downX, e.clientY - downY)) return;
593
597
  const k = pick(e.clientX, e.clientY);
594
598
  if (!k) return;
595
599
  const outcome = pickRef.current(k);
@@ -679,7 +683,9 @@ function BodyMap3D(props) {
679
683
  const region = selected ? REGIONS.find((r) => r.key === selected) : null;
680
684
  const current = selected ? pointOf(selected) : void 0;
681
685
  const anySelectable = REGIONS.some((r) => isSelectable(r.key, config ?? {}));
682
- const showEmptyHint = anySelectable || ui?.hoverHint !== void 0;
686
+ const showEmptyHint = hint === false ? false : anySelectable || ui?.hoverHint !== void 0;
687
+ const hintText = (hint ? hint.text : void 0) ?? UI.hoverHint;
688
+ const hintAtStage = isFullscreen && !!hint && hint.placement === "stage-bottom";
683
689
  return /* @__PURE__ */ jsxs(
684
690
  "div",
685
691
  {
@@ -795,13 +801,38 @@ function BodyMap3D(props) {
795
801
  /* @__PURE__ */ jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.quality }),
796
802
  /* @__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)) }),
797
803
  current && /* @__PURE__ */ jsx("button", { "data-testid": "bodymap3d-remove", onClick: () => removePain(region.key), style: { ...btn(chrome), color: chrome.danger, borderColor: chrome.dangerBorder }, children: L.remove })
798
- ] }) : showEmptyHint ? /* @__PURE__ */ jsx("div", { "data-testid": "bodymap3d-empty", style: { border: `1px solid ${chrome.border}`, borderRadius: 14, padding: 16, background: chrome.panelBg, color: chrome.mutedText, fontSize: 13.5 }, children: UI.hoverHint }) : null })
799
- ] })
804
+ ] }) : showEmptyHint && !hintAtStage ? /* @__PURE__ */ jsx("div", { "data-testid": "bodymap3d-empty", style: { border: `1px solid ${chrome.border}`, borderRadius: 14, padding: 16, background: chrome.panelBg, color: chrome.mutedText, fontSize: 13.5 }, children: hintText }) : null })
805
+ ] }),
806
+ showEmptyHint && hintAtStage && /* F052.31 — pinned INSIDE the fullscreen surface, not in the column that
807
+ wraps beneath a full-height canvas. That wrap is what put the note at
808
+ 858-912 in an 852px window on a real iPhone: entirely offscreen, and
809
+ it reads as a bug in the consumer's app rather than in ours.
810
+ The bottom inset clears the home indicator, exactly as the exit
811
+ control's does (F052.27). */
812
+ /* @__PURE__ */ jsx(
813
+ "div",
814
+ {
815
+ "data-testid": "bodymap3d-empty",
816
+ "data-placement": "stage-bottom",
817
+ style: {
818
+ flex: "0 0 auto",
819
+ marginTop: 12,
820
+ marginBottom: "env(safe-area-inset-bottom)",
821
+ border: `1px solid ${chrome.border}`,
822
+ borderRadius: 14,
823
+ padding: 16,
824
+ background: chrome.panelBg,
825
+ color: chrome.mutedText,
826
+ fontSize: 13.5
827
+ },
828
+ children: hintText
829
+ }
830
+ )
800
831
  ]
801
832
  }
802
833
  );
803
834
  }
804
835
 
805
- export { BodyMap3D, ensureNormals, seamBlend, seamWeight, serializeReport };
836
+ export { BodyMap3D, TAP_MAX_DISTANCE, ensureNormals, isTap, seamBlend, seamWeight, serializeReport };
806
837
  //# sourceMappingURL=three.js.map
807
838
  //# sourceMappingURL=three.js.map