@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/README.md +45 -0
- package/dist/three.cjs +41 -8
- package/dist/three.cjs.map +1 -1
- package/dist/three.d.cts +49 -1
- package/dist/three.d.ts +49 -1
- package/dist/three.js +40 -9
- package/dist/three.js.map +1 -1
- package/package.json +1 -1
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: "
|
|
216
|
-
var UI_EN = { male: "Male", female: "Female", hoverHint: "
|
|
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
|
|
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 (
|
|
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:
|
|
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
|