@broberg/bodymap 0.7.1 → 0.9.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
@@ -103,13 +103,24 @@ function emitFeedback(outcome, region, opts = {}) {
103
103
  if (opts.haptics === false) return "skipped";
104
104
  return requestVibration(VIBRATION_PATTERNS[outcome], opts.nav);
105
105
  }
106
+ var STAGE_BG = "#0e1424";
106
107
  var defaultUi = {
107
108
  text: "#1e293b",
108
109
  mutedText: "#475569",
109
110
  panelBg: "#fff",
111
+ stageBg: STAGE_BG,
110
112
  border: "#e2e8f0",
111
113
  badgeBg: "#f1f5f9",
112
- danger: "#dc2626"
114
+ danger: "#dc2626",
115
+ dangerBorder: "#f6c9c9",
116
+ // F052.30 — was #0e8f8a, which measured 3.95:1 against white. AA needs 4.5,
117
+ // and this is the SELECTED intensity button on a surface where AA is a legal
118
+ // duty. It shipped from 0.2.0 and no check could see it: the colour was an
119
+ // inline literal, and the F052.19 contrast sweep only read this object.
120
+ // #0c7d77 is the same teal a shade deeper — 4.98:1, with headroom rather than
121
+ // sitting on the 4.5 boundary where any later tweak reopens it.
122
+ accent: "#0c7d77",
123
+ accentText: "#fff"
113
124
  };
114
125
  function uiColors(palette) {
115
126
  return { ...defaultUi, ...palette?.ui ?? {} };
@@ -268,6 +279,14 @@ function seamBlend(nearestSq, secondSq) {
268
279
  if (t >= 1) return 0.5;
269
280
  return t * t * (3 - 2 * t) * 0.5;
270
281
  }
282
+ function seamWeight(seamEnabled, blend) {
283
+ return seamEnabled ? blend : 0;
284
+ }
285
+ function ensureNormals(geo) {
286
+ if (geo.getAttribute("normal")) return false;
287
+ geo.computeVertexNormals();
288
+ return true;
289
+ }
271
290
  var ANCHOR_KEYS = Object.keys(ANCHORS);
272
291
  for (const k of ANCHOR_KEYS) ANCHORS[k][0] = -ANCHORS[k][0];
273
292
  function mergeLabels(locale, overrides) {
@@ -288,8 +307,20 @@ function webglAvailable() {
288
307
  return false;
289
308
  }
290
309
  }
291
- var btn = { font: "inherit", cursor: "pointer", borderRadius: 8, border: "1px solid #e2e8f0", background: "#fff", padding: "6px 9px" };
292
- var seg = (on) => ({ ...btn, background: on ? "#0e8f8a" : "#fff", color: on ? "#fff" : "#1e293b", fontWeight: 600 });
310
+ var btn = (c) => ({
311
+ font: "inherit",
312
+ cursor: "pointer",
313
+ borderRadius: 8,
314
+ border: `1px solid ${c.border}`,
315
+ background: c.panelBg,
316
+ padding: "6px 9px"
317
+ });
318
+ var seg = (c, on) => ({
319
+ ...btn(c),
320
+ background: on ? c.accent : c.panelBg,
321
+ color: on ? c.accentText : c.text,
322
+ fontWeight: 600
323
+ });
293
324
  function BodyMap3D(props) {
294
325
  const {
295
326
  models,
@@ -353,6 +384,8 @@ function BodyMap3D(props) {
353
384
  setReadyRef.current = setReady;
354
385
  const seamRef = react.useRef(seam);
355
386
  seamRef.current = seam;
387
+ const stageRef = react.useRef(chrome.stageBg);
388
+ stageRef.current = chrome.stageBg;
356
389
  const [internalFs, setInternalFs] = react.useState(false);
357
390
  const isFullscreen = fullscreenProp ?? internalFs;
358
391
  const setFullscreen = (v) => {
@@ -369,7 +402,7 @@ function BodyMap3D(props) {
369
402
  if (!el) return;
370
403
  let W = el.clientWidth || 520, H = el.clientHeight || 600;
371
404
  const scene = new THREE__namespace.Scene();
372
- scene.background = new THREE__namespace.Color(922660);
405
+ scene.background = new THREE__namespace.Color(stageRef.current);
373
406
  const camera = new THREE__namespace.PerspectiveCamera(32, W / H, 0.1, 100);
374
407
  camera.position.set(0, 1.05, 4.4);
375
408
  let renderer;
@@ -452,7 +485,7 @@ function BodyMap3D(props) {
452
485
  };
453
486
  for (let i = 0; i < vertexRegion.length; i++) {
454
487
  tmp.copy(col(vertexRegion[i]));
455
- const w = seamRef.current ? vertexBlend[i] : 0;
488
+ const w = seamWeight(seamRef.current, vertexBlend[i]);
456
489
  if (w > 0) {
457
490
  tmp2.copy(col(vertexNeighbour[i]));
458
491
  tmp.lerp(tmp2, w);
@@ -462,6 +495,7 @@ function BodyMap3D(props) {
462
495
  colorAttr.needsUpdate = true;
463
496
  };
464
497
  const refresh = () => {
498
+ scene.background = new THREE__namespace.Color(stageRef.current);
465
499
  paint();
466
500
  renderFrame();
467
501
  };
@@ -486,7 +520,7 @@ function BodyMap3D(props) {
486
520
  });
487
521
  if (bodyMesh) {
488
522
  const geo = bodyMesh.geometry;
489
- if (!geo.getAttribute("normal")) geo.computeVertexNormals();
523
+ ensureNormals(geo);
490
524
  const pos = geo.getAttribute("position");
491
525
  const n = pos.count;
492
526
  const cols = new Float32Array(n * 3);
@@ -676,7 +710,7 @@ function BodyMap3D(props) {
676
710
  "data-fullscreen": isFullscreen ? "true" : void 0,
677
711
  style: {
678
712
  fontFamily: "system-ui, sans-serif",
679
- color: "#1e293b",
713
+ color: chrome.text,
680
714
  // A viewport fill, not the Fullscreen API — see the `fullscreen` prop.
681
715
  // `fixed`+`inset:0` behaves the same on iOS Safari, where element
682
716
  // fullscreen does not exist at all, as it does everywhere else.
@@ -684,7 +718,12 @@ function BodyMap3D(props) {
684
718
  position: "fixed",
685
719
  inset: 0,
686
720
  zIndex: 2147483e3,
687
- background: chrome.panelBg,
721
+ // THE STAGE, not the panel. This used to be `chrome.panelBg`,
722
+ // the same field that paints the picker card — so a consumer with
723
+ // a dark canvas got a white void around a dark body in fullscreen,
724
+ // and could not fix it: darkening panelBg hid the 0-10 buttons.
725
+ // One field, two surfaces, opposite needs. (F052.29)
726
+ background: chrome.stageBg,
688
727
  // F052.27 — a fixed inset:0 surface in a standalone/Capacitor
689
728
  // webview covers the STATUS BAR too, so a control laid out at the
690
729
  // top lands on the clock and the battery. Measured by fd-sundhed
@@ -703,8 +742,8 @@ function BodyMap3D(props) {
703
742
  },
704
743
  children: [
705
744
  showSexToggle && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", gap: 16, flexWrap: "wrap", marginBottom: 12, fontSize: 13 }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 6, alignItems: "center" }, children: [
706
- /* @__PURE__ */ jsxRuntime.jsx("button", { "data-testid": "bodymap3d-sex-male", onClick: () => changeSex("male"), style: seg(sex === "male"), children: UI.male }),
707
- /* @__PURE__ */ jsxRuntime.jsx("button", { "data-testid": "bodymap3d-sex-female", onClick: () => changeSex("female"), style: seg(sex === "female"), children: UI.female })
745
+ /* @__PURE__ */ jsxRuntime.jsx("button", { "data-testid": "bodymap3d-sex-male", onClick: () => changeSex("male"), style: seg(chrome, sex === "male"), children: UI.male }),
746
+ /* @__PURE__ */ jsxRuntime.jsx("button", { "data-testid": "bodymap3d-sex-female", onClick: () => changeSex("female"), style: seg(chrome, sex === "female"), children: UI.female })
708
747
  ] }) }),
709
748
  /* @__PURE__ */ jsxRuntime.jsx("span", { ref: loadedRef, "data-testid": "bodymap3d-loaded", style: { display: "none" } }),
710
749
  ready && /* @__PURE__ */ jsxRuntime.jsx("span", { "data-testid": "bodymap3d-ready", style: { position: "absolute", width: 1, height: 1, opacity: 0, pointerEvents: "none" } }),
@@ -717,7 +756,7 @@ function BodyMap3D(props) {
717
756
  "aria-label": isFullscreen ? UI.collapse ?? "Close" : UI.expand ?? "Expand",
718
757
  onClick: () => setFullscreen(!isFullscreen),
719
758
  style: {
720
- ...btn,
759
+ ...btn(chrome),
721
760
  color: chrome.text,
722
761
  borderColor: chrome.border,
723
762
  background: chrome.panelBg,
@@ -727,7 +766,14 @@ function BodyMap3D(props) {
727
766
  }
728
767
  ) }),
729
768
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 18, alignItems: "flex-start", flexWrap: "wrap", ...isFullscreen ? { flex: "1 1 auto", minHeight: 0 } : null }, children: [
730
- unsupported ? /* @__PURE__ */ jsxRuntime.jsx("div", { "data-testid": "bodymap3d-unsupported", style: { flex: "1 1 520px", minWidth: 320, height: canvasH, borderRadius: 16, background: "#0e1424", color: "#cbd5e1", display: "flex", alignItems: "center", justifyContent: "center", padding: 24, textAlign: "center", fontSize: 14 }, children: "3D kr\xE6ver WebGL, som ikke er tilg\xE6ngeligt her." }) : /* @__PURE__ */ jsxRuntime.jsx(
769
+ unsupported ? /* @__PURE__ */ jsxRuntime.jsx(
770
+ "div",
771
+ {
772
+ "data-testid": "bodymap3d-unsupported",
773
+ style: { flex: "1 1 520px", minWidth: 320, height: canvasH, borderRadius: 16, background: chrome.panelBg, color: chrome.mutedText, border: `1px solid ${chrome.border}`, display: "flex", alignItems: "center", justifyContent: "center", padding: 24, textAlign: "center", fontSize: 14 },
774
+ children: "3D kr\xE6ver WebGL, som ikke er tilg\xE6ngeligt her."
775
+ }
776
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
731
777
  "div",
732
778
  {
733
779
  ref: mountRef,
@@ -742,7 +788,7 @@ function BodyMap3D(props) {
742
788
  minHeight: isFullscreen ? 0 : void 0,
743
789
  borderRadius: 16,
744
790
  overflow: "hidden",
745
- background: "#0e1424",
791
+ background: chrome.stageBg,
746
792
  touchAction: "none"
747
793
  }
748
794
  }
@@ -761,16 +807,16 @@ function BodyMap3D(props) {
761
807
  "data-testid": "bodymap3d-close",
762
808
  "aria-label": L.close,
763
809
  onClick: () => setSelected(null),
764
- style: { ...btn, width: 32, height: 32, padding: 0, fontSize: 20, lineHeight: 1, color: chrome.mutedText, borderColor: chrome.border, background: chrome.panelBg },
810
+ style: { ...btn(chrome), width: 32, height: 32, padding: 0, fontSize: 20, lineHeight: 1, color: chrome.mutedText, borderColor: chrome.border, background: chrome.panelBg },
765
811
  children: "\xD7"
766
812
  }
767
813
  )
768
814
  ] }),
769
815
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.intensity }),
770
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 4, marginBottom: 12 }, children: Array.from({ length: 11 }, (_, i) => /* @__PURE__ */ jsxRuntime.jsx("button", { "data-testid": `bodymap3d-intensity-${i}`, onClick: () => setPain(region.key, i, current?.type), style: { ...btn, display: "inline-flex", alignItems: "center", justifyContent: "center", minWidth: 30, height: 30, padding: 0, background: current?.intensity === i ? "#0e8f8a" : "#fff", color: current?.intensity === i ? "#fff" : "#1e293b" }, children: i }, i)) }),
816
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 4, marginBottom: 12 }, children: Array.from({ length: 11 }, (_, i) => /* @__PURE__ */ jsxRuntime.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)) }),
771
817
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.quality }),
772
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 14 }, children: PAIN_TYPES.map((t) => /* @__PURE__ */ jsxRuntime.jsx("button", { "data-testid": `bodymap3d-type-${t}`, onClick: () => setPain(region.key, current?.intensity ?? 5, t), style: { ...btn, borderRadius: 999, padding: "6px 12px", background: current?.type === t ? "#1e293b" : "#fff", color: current?.type === t ? "#fff" : "#64748b" }, children: L.qualities[t] ?? t }, t)) }),
773
- current && /* @__PURE__ */ jsxRuntime.jsx("button", { "data-testid": "bodymap3d-remove", onClick: () => removePain(region.key), style: { ...btn, color: chrome.danger, borderColor: "#f6c9c9" }, children: L.remove })
818
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 14 }, children: PAIN_TYPES.map((t) => /* @__PURE__ */ jsxRuntime.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)) }),
819
+ current && /* @__PURE__ */ jsxRuntime.jsx("button", { "data-testid": "bodymap3d-remove", onClick: () => removePain(region.key), style: { ...btn(chrome), color: chrome.danger, borderColor: chrome.dangerBorder }, children: L.remove })
774
820
  ] }) : showEmptyHint ? /* @__PURE__ */ jsxRuntime.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 })
775
821
  ] })
776
822
  ]
@@ -779,7 +825,9 @@ function BodyMap3D(props) {
779
825
  }
780
826
 
781
827
  exports.BodyMap3D = BodyMap3D;
828
+ exports.ensureNormals = ensureNormals;
782
829
  exports.seamBlend = seamBlend;
830
+ exports.seamWeight = seamWeight;
783
831
  exports.serializeReport = serializeReport;
784
832
  //# sourceMappingURL=three.cjs.map
785
833
  //# sourceMappingURL=three.cjs.map