@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/README.md +17 -0
- package/dist/three.cjs +34 -6
- package/dist/three.cjs.map +1 -1
- package/dist/three.d.cts +71 -1
- package/dist/three.d.ts +71 -1
- package/dist/three.js +31 -7
- package/dist/three.js.map +1 -1
- package/package.json +1 -1
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
|
|
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 (
|
|
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 =
|
|
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:
|
|
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) =>
|
|
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
|