@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/README.md +68 -0
- package/dist/index.cjs +13 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -2
- package/dist/index.d.ts +38 -2
- package/dist/index.js +13 -2
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +3 -3
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +28 -1
- package/dist/react.d.ts +28 -1
- package/dist/react.js +3 -3
- package/dist/react.js.map +1 -1
- package/dist/three.cjs +65 -17
- package/dist/three.cjs.map +1 -1
- package/dist/three.d.cts +63 -2
- package/dist/three.d.ts +63 -2
- package/dist/three.js +64 -18
- package/dist/three.js.map +1 -1
- package/package.json +1 -1
package/dist/three.d.cts
CHANGED
|
@@ -97,14 +97,41 @@ interface BodymapUiColors {
|
|
|
97
97
|
text?: string;
|
|
98
98
|
/** Secondary text — section labels, the empty-state hint. */
|
|
99
99
|
mutedText?: string;
|
|
100
|
-
/** Panel background. */
|
|
100
|
+
/** Panel background — the card, its buttons, the empty hint. */
|
|
101
101
|
panelBg?: string;
|
|
102
|
+
/**
|
|
103
|
+
* THE STAGE: the fullscreen backdrop, the canvas frame, and the three.js
|
|
104
|
+
* scene behind the body. A different surface from `panelBg` with an opposite
|
|
105
|
+
* need — see F052.29, where one field paints both and no value is right for
|
|
106
|
+
* either.
|
|
107
|
+
*
|
|
108
|
+
* 3D only. The 2D renderer has no canvas and no fullscreen, so it ignores
|
|
109
|
+
* this; it is declared here because a stage is a shared idea, not because
|
|
110
|
+
* every renderer has one.
|
|
111
|
+
*
|
|
112
|
+
* Defaults to {@link STAGE_BG}. Pass `'#fff'` for the pre-0.8.0 white
|
|
113
|
+
* fullscreen backdrop.
|
|
114
|
+
*/
|
|
115
|
+
stageBg?: string;
|
|
102
116
|
/** Panel + control borders. */
|
|
103
117
|
border?: string;
|
|
104
118
|
/** Background behind the region-code badge. */
|
|
105
119
|
badgeBg?: string;
|
|
106
120
|
/** The destructive action (remove a marked region). */
|
|
107
121
|
danger?: string;
|
|
122
|
+
/** Border on the destructive control. Its TEXT was already themeable and its
|
|
123
|
+
* border was not — half a control. (F052.30) */
|
|
124
|
+
dangerBorder?: string;
|
|
125
|
+
/**
|
|
126
|
+
* THE ACTIVE CONTROL colour — the chosen intensity, the active sex toggle.
|
|
127
|
+
*
|
|
128
|
+
* NOT `palette.selected`. That is the colour of a MARK ON SKIN; this is the
|
|
129
|
+
* colour of an active button. Collapsing them would stop a consumer having a
|
|
130
|
+
* teal mark and a navy button, which is a normal thing to want. (F052.30)
|
|
131
|
+
*/
|
|
132
|
+
accent?: string;
|
|
133
|
+
/** Text ON the accent. Check it against your accent — see the README. */
|
|
134
|
+
accentText?: string;
|
|
108
135
|
}
|
|
109
136
|
type BodyView = "front" | "back" | "left" | "right";
|
|
110
137
|
/** Side in the serialized report — a midline region (no side) becomes "center". */
|
|
@@ -193,6 +220,40 @@ interface BodyMap3DUiLabels {
|
|
|
193
220
|
* PainReport is unchanged.
|
|
194
221
|
*/
|
|
195
222
|
declare function seamBlend(nearestSq: number, secondSq: number): number;
|
|
223
|
+
/**
|
|
224
|
+
* F052.23 — how much of the NEIGHBOUR region's colour a vertex takes.
|
|
225
|
+
*
|
|
226
|
+
* Extracted from the paint loop for one reason: happy-dom cannot run WebGL, so
|
|
227
|
+
* the fix that put the seam blend behind an opt-in had no automated guard at
|
|
228
|
+
* all. A regression here is a WCAG failure on a public-health surface (measured:
|
|
229
|
+
* 4.71:1 without the blend, 3.59:1 with it, averaged over a small mark), which
|
|
230
|
+
* is not the kind of thing that should rely on someone re-measuring by hand.
|
|
231
|
+
*
|
|
232
|
+
* The invariant worth asserting is not "off returns 0 for this blend value" —
|
|
233
|
+
* it is that off returns 0 for EVERY blend value, so the shipped default can
|
|
234
|
+
* never mix body colour into a mark.
|
|
235
|
+
*/
|
|
236
|
+
declare function seamWeight(seamEnabled: boolean, blend: number): number;
|
|
237
|
+
/**
|
|
238
|
+
* F052.25 — recompute vertex normals ONLY when the file shipped none.
|
|
239
|
+
*
|
|
240
|
+
* Same reason as above: this single condition WAS the "lines on the body" bug,
|
|
241
|
+
* and it lived on the one code path the unit suite cannot execute. Takes a
|
|
242
|
+
* structural slice rather than a THREE.BufferGeometry so it is testable without
|
|
243
|
+
* a GL context.
|
|
244
|
+
*
|
|
245
|
+
* It performs the call as well as the check, deliberately. A predicate alone
|
|
246
|
+
* would let a test pass while the call site did the opposite with it — and what
|
|
247
|
+
* the acceptance criterion asks for is the EFFECT: a model that ships normals
|
|
248
|
+
* must come out the other side with those same normals, untouched.
|
|
249
|
+
*
|
|
250
|
+
* Returns true when it computed (the model had none, and would otherwise render
|
|
251
|
+
* flat), false when it left the authored normals alone.
|
|
252
|
+
*/
|
|
253
|
+
declare function ensureNormals(geo: {
|
|
254
|
+
getAttribute(name: string): unknown;
|
|
255
|
+
computeVertexNormals(): void;
|
|
256
|
+
}): boolean;
|
|
196
257
|
interface BodyMap3DProps {
|
|
197
258
|
/** URLs of the male/female body GLBs (you host them; reference GLBs ship under `@broberg/bodymap/models/`). */
|
|
198
259
|
models: BodyMap3DModels;
|
|
@@ -264,4 +325,4 @@ interface BodyMap3DProps {
|
|
|
264
325
|
}
|
|
265
326
|
declare function BodyMap3D(props: BodyMap3DProps): react.JSX.Element;
|
|
266
327
|
|
|
267
|
-
export { BodyMap3D, type BodyMap3DModels, type BodyMap3DProps, type BodyMap3DSex, type BodyMap3DUiLabels, seamBlend, serializeReport };
|
|
328
|
+
export { BodyMap3D, type BodyMap3DModels, type BodyMap3DProps, type BodyMap3DSex, type BodyMap3DUiLabels, ensureNormals, seamBlend, seamWeight, serializeReport };
|
package/dist/three.d.ts
CHANGED
|
@@ -97,14 +97,41 @@ interface BodymapUiColors {
|
|
|
97
97
|
text?: string;
|
|
98
98
|
/** Secondary text — section labels, the empty-state hint. */
|
|
99
99
|
mutedText?: string;
|
|
100
|
-
/** Panel background. */
|
|
100
|
+
/** Panel background — the card, its buttons, the empty hint. */
|
|
101
101
|
panelBg?: string;
|
|
102
|
+
/**
|
|
103
|
+
* THE STAGE: the fullscreen backdrop, the canvas frame, and the three.js
|
|
104
|
+
* scene behind the body. A different surface from `panelBg` with an opposite
|
|
105
|
+
* need — see F052.29, where one field paints both and no value is right for
|
|
106
|
+
* either.
|
|
107
|
+
*
|
|
108
|
+
* 3D only. The 2D renderer has no canvas and no fullscreen, so it ignores
|
|
109
|
+
* this; it is declared here because a stage is a shared idea, not because
|
|
110
|
+
* every renderer has one.
|
|
111
|
+
*
|
|
112
|
+
* Defaults to {@link STAGE_BG}. Pass `'#fff'` for the pre-0.8.0 white
|
|
113
|
+
* fullscreen backdrop.
|
|
114
|
+
*/
|
|
115
|
+
stageBg?: string;
|
|
102
116
|
/** Panel + control borders. */
|
|
103
117
|
border?: string;
|
|
104
118
|
/** Background behind the region-code badge. */
|
|
105
119
|
badgeBg?: string;
|
|
106
120
|
/** The destructive action (remove a marked region). */
|
|
107
121
|
danger?: string;
|
|
122
|
+
/** Border on the destructive control. Its TEXT was already themeable and its
|
|
123
|
+
* border was not — half a control. (F052.30) */
|
|
124
|
+
dangerBorder?: string;
|
|
125
|
+
/**
|
|
126
|
+
* THE ACTIVE CONTROL colour — the chosen intensity, the active sex toggle.
|
|
127
|
+
*
|
|
128
|
+
* NOT `palette.selected`. That is the colour of a MARK ON SKIN; this is the
|
|
129
|
+
* colour of an active button. Collapsing them would stop a consumer having a
|
|
130
|
+
* teal mark and a navy button, which is a normal thing to want. (F052.30)
|
|
131
|
+
*/
|
|
132
|
+
accent?: string;
|
|
133
|
+
/** Text ON the accent. Check it against your accent — see the README. */
|
|
134
|
+
accentText?: string;
|
|
108
135
|
}
|
|
109
136
|
type BodyView = "front" | "back" | "left" | "right";
|
|
110
137
|
/** Side in the serialized report — a midline region (no side) becomes "center". */
|
|
@@ -193,6 +220,40 @@ interface BodyMap3DUiLabels {
|
|
|
193
220
|
* PainReport is unchanged.
|
|
194
221
|
*/
|
|
195
222
|
declare function seamBlend(nearestSq: number, secondSq: number): number;
|
|
223
|
+
/**
|
|
224
|
+
* F052.23 — how much of the NEIGHBOUR region's colour a vertex takes.
|
|
225
|
+
*
|
|
226
|
+
* Extracted from the paint loop for one reason: happy-dom cannot run WebGL, so
|
|
227
|
+
* the fix that put the seam blend behind an opt-in had no automated guard at
|
|
228
|
+
* all. A regression here is a WCAG failure on a public-health surface (measured:
|
|
229
|
+
* 4.71:1 without the blend, 3.59:1 with it, averaged over a small mark), which
|
|
230
|
+
* is not the kind of thing that should rely on someone re-measuring by hand.
|
|
231
|
+
*
|
|
232
|
+
* The invariant worth asserting is not "off returns 0 for this blend value" —
|
|
233
|
+
* it is that off returns 0 for EVERY blend value, so the shipped default can
|
|
234
|
+
* never mix body colour into a mark.
|
|
235
|
+
*/
|
|
236
|
+
declare function seamWeight(seamEnabled: boolean, blend: number): number;
|
|
237
|
+
/**
|
|
238
|
+
* F052.25 — recompute vertex normals ONLY when the file shipped none.
|
|
239
|
+
*
|
|
240
|
+
* Same reason as above: this single condition WAS the "lines on the body" bug,
|
|
241
|
+
* and it lived on the one code path the unit suite cannot execute. Takes a
|
|
242
|
+
* structural slice rather than a THREE.BufferGeometry so it is testable without
|
|
243
|
+
* a GL context.
|
|
244
|
+
*
|
|
245
|
+
* It performs the call as well as the check, deliberately. A predicate alone
|
|
246
|
+
* would let a test pass while the call site did the opposite with it — and what
|
|
247
|
+
* the acceptance criterion asks for is the EFFECT: a model that ships normals
|
|
248
|
+
* must come out the other side with those same normals, untouched.
|
|
249
|
+
*
|
|
250
|
+
* Returns true when it computed (the model had none, and would otherwise render
|
|
251
|
+
* flat), false when it left the authored normals alone.
|
|
252
|
+
*/
|
|
253
|
+
declare function ensureNormals(geo: {
|
|
254
|
+
getAttribute(name: string): unknown;
|
|
255
|
+
computeVertexNormals(): void;
|
|
256
|
+
}): boolean;
|
|
196
257
|
interface BodyMap3DProps {
|
|
197
258
|
/** URLs of the male/female body GLBs (you host them; reference GLBs ship under `@broberg/bodymap/models/`). */
|
|
198
259
|
models: BodyMap3DModels;
|
|
@@ -264,4 +325,4 @@ interface BodyMap3DProps {
|
|
|
264
325
|
}
|
|
265
326
|
declare function BodyMap3D(props: BodyMap3DProps): react.JSX.Element;
|
|
266
327
|
|
|
267
|
-
export { BodyMap3D, type BodyMap3DModels, type BodyMap3DProps, type BodyMap3DSex, type BodyMap3DUiLabels, seamBlend, serializeReport };
|
|
328
|
+
export { BodyMap3D, type BodyMap3DModels, type BodyMap3DProps, type BodyMap3DSex, type BodyMap3DUiLabels, ensureNormals, seamBlend, seamWeight, serializeReport };
|
package/dist/three.js
CHANGED
|
@@ -81,13 +81,24 @@ function emitFeedback(outcome, region, opts = {}) {
|
|
|
81
81
|
if (opts.haptics === false) return "skipped";
|
|
82
82
|
return requestVibration(VIBRATION_PATTERNS[outcome], opts.nav);
|
|
83
83
|
}
|
|
84
|
+
var STAGE_BG = "#0e1424";
|
|
84
85
|
var defaultUi = {
|
|
85
86
|
text: "#1e293b",
|
|
86
87
|
mutedText: "#475569",
|
|
87
88
|
panelBg: "#fff",
|
|
89
|
+
stageBg: STAGE_BG,
|
|
88
90
|
border: "#e2e8f0",
|
|
89
91
|
badgeBg: "#f1f5f9",
|
|
90
|
-
danger: "#dc2626"
|
|
92
|
+
danger: "#dc2626",
|
|
93
|
+
dangerBorder: "#f6c9c9",
|
|
94
|
+
// F052.30 — was #0e8f8a, which measured 3.95:1 against white. AA needs 4.5,
|
|
95
|
+
// and this is the SELECTED intensity button on a surface where AA is a legal
|
|
96
|
+
// duty. It shipped from 0.2.0 and no check could see it: the colour was an
|
|
97
|
+
// inline literal, and the F052.19 contrast sweep only read this object.
|
|
98
|
+
// #0c7d77 is the same teal a shade deeper — 4.98:1, with headroom rather than
|
|
99
|
+
// sitting on the 4.5 boundary where any later tweak reopens it.
|
|
100
|
+
accent: "#0c7d77",
|
|
101
|
+
accentText: "#fff"
|
|
91
102
|
};
|
|
92
103
|
function uiColors(palette) {
|
|
93
104
|
return { ...defaultUi, ...palette?.ui ?? {} };
|
|
@@ -246,6 +257,14 @@ function seamBlend(nearestSq, secondSq) {
|
|
|
246
257
|
if (t >= 1) return 0.5;
|
|
247
258
|
return t * t * (3 - 2 * t) * 0.5;
|
|
248
259
|
}
|
|
260
|
+
function seamWeight(seamEnabled, blend) {
|
|
261
|
+
return seamEnabled ? blend : 0;
|
|
262
|
+
}
|
|
263
|
+
function ensureNormals(geo) {
|
|
264
|
+
if (geo.getAttribute("normal")) return false;
|
|
265
|
+
geo.computeVertexNormals();
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
249
268
|
var ANCHOR_KEYS = Object.keys(ANCHORS);
|
|
250
269
|
for (const k of ANCHOR_KEYS) ANCHORS[k][0] = -ANCHORS[k][0];
|
|
251
270
|
function mergeLabels(locale, overrides) {
|
|
@@ -266,8 +285,20 @@ function webglAvailable() {
|
|
|
266
285
|
return false;
|
|
267
286
|
}
|
|
268
287
|
}
|
|
269
|
-
var btn =
|
|
270
|
-
|
|
288
|
+
var btn = (c) => ({
|
|
289
|
+
font: "inherit",
|
|
290
|
+
cursor: "pointer",
|
|
291
|
+
borderRadius: 8,
|
|
292
|
+
border: `1px solid ${c.border}`,
|
|
293
|
+
background: c.panelBg,
|
|
294
|
+
padding: "6px 9px"
|
|
295
|
+
});
|
|
296
|
+
var seg = (c, on) => ({
|
|
297
|
+
...btn(c),
|
|
298
|
+
background: on ? c.accent : c.panelBg,
|
|
299
|
+
color: on ? c.accentText : c.text,
|
|
300
|
+
fontWeight: 600
|
|
301
|
+
});
|
|
271
302
|
function BodyMap3D(props) {
|
|
272
303
|
const {
|
|
273
304
|
models,
|
|
@@ -331,6 +362,8 @@ function BodyMap3D(props) {
|
|
|
331
362
|
setReadyRef.current = setReady;
|
|
332
363
|
const seamRef = useRef(seam);
|
|
333
364
|
seamRef.current = seam;
|
|
365
|
+
const stageRef = useRef(chrome.stageBg);
|
|
366
|
+
stageRef.current = chrome.stageBg;
|
|
334
367
|
const [internalFs, setInternalFs] = useState(false);
|
|
335
368
|
const isFullscreen = fullscreenProp ?? internalFs;
|
|
336
369
|
const setFullscreen = (v) => {
|
|
@@ -347,7 +380,7 @@ function BodyMap3D(props) {
|
|
|
347
380
|
if (!el) return;
|
|
348
381
|
let W = el.clientWidth || 520, H = el.clientHeight || 600;
|
|
349
382
|
const scene = new THREE.Scene();
|
|
350
|
-
scene.background = new THREE.Color(
|
|
383
|
+
scene.background = new THREE.Color(stageRef.current);
|
|
351
384
|
const camera = new THREE.PerspectiveCamera(32, W / H, 0.1, 100);
|
|
352
385
|
camera.position.set(0, 1.05, 4.4);
|
|
353
386
|
let renderer;
|
|
@@ -430,7 +463,7 @@ function BodyMap3D(props) {
|
|
|
430
463
|
};
|
|
431
464
|
for (let i = 0; i < vertexRegion.length; i++) {
|
|
432
465
|
tmp.copy(col(vertexRegion[i]));
|
|
433
|
-
const w = seamRef.current
|
|
466
|
+
const w = seamWeight(seamRef.current, vertexBlend[i]);
|
|
434
467
|
if (w > 0) {
|
|
435
468
|
tmp2.copy(col(vertexNeighbour[i]));
|
|
436
469
|
tmp.lerp(tmp2, w);
|
|
@@ -440,6 +473,7 @@ function BodyMap3D(props) {
|
|
|
440
473
|
colorAttr.needsUpdate = true;
|
|
441
474
|
};
|
|
442
475
|
const refresh = () => {
|
|
476
|
+
scene.background = new THREE.Color(stageRef.current);
|
|
443
477
|
paint();
|
|
444
478
|
renderFrame();
|
|
445
479
|
};
|
|
@@ -464,7 +498,7 @@ function BodyMap3D(props) {
|
|
|
464
498
|
});
|
|
465
499
|
if (bodyMesh) {
|
|
466
500
|
const geo = bodyMesh.geometry;
|
|
467
|
-
|
|
501
|
+
ensureNormals(geo);
|
|
468
502
|
const pos = geo.getAttribute("position");
|
|
469
503
|
const n = pos.count;
|
|
470
504
|
const cols = new Float32Array(n * 3);
|
|
@@ -654,7 +688,7 @@ function BodyMap3D(props) {
|
|
|
654
688
|
"data-fullscreen": isFullscreen ? "true" : void 0,
|
|
655
689
|
style: {
|
|
656
690
|
fontFamily: "system-ui, sans-serif",
|
|
657
|
-
color:
|
|
691
|
+
color: chrome.text,
|
|
658
692
|
// A viewport fill, not the Fullscreen API — see the `fullscreen` prop.
|
|
659
693
|
// `fixed`+`inset:0` behaves the same on iOS Safari, where element
|
|
660
694
|
// fullscreen does not exist at all, as it does everywhere else.
|
|
@@ -662,7 +696,12 @@ function BodyMap3D(props) {
|
|
|
662
696
|
position: "fixed",
|
|
663
697
|
inset: 0,
|
|
664
698
|
zIndex: 2147483e3,
|
|
665
|
-
|
|
699
|
+
// THE STAGE, not the panel. This used to be `chrome.panelBg`,
|
|
700
|
+
// the same field that paints the picker card — so a consumer with
|
|
701
|
+
// a dark canvas got a white void around a dark body in fullscreen,
|
|
702
|
+
// and could not fix it: darkening panelBg hid the 0-10 buttons.
|
|
703
|
+
// One field, two surfaces, opposite needs. (F052.29)
|
|
704
|
+
background: chrome.stageBg,
|
|
666
705
|
// F052.27 — a fixed inset:0 surface in a standalone/Capacitor
|
|
667
706
|
// webview covers the STATUS BAR too, so a control laid out at the
|
|
668
707
|
// top lands on the clock and the battery. Measured by fd-sundhed
|
|
@@ -681,8 +720,8 @@ function BodyMap3D(props) {
|
|
|
681
720
|
},
|
|
682
721
|
children: [
|
|
683
722
|
showSexToggle && /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 16, flexWrap: "wrap", marginBottom: 12, fontSize: 13 }, children: /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6, alignItems: "center" }, children: [
|
|
684
|
-
/* @__PURE__ */ jsx("button", { "data-testid": "bodymap3d-sex-male", onClick: () => changeSex("male"), style: seg(sex === "male"), children: UI.male }),
|
|
685
|
-
/* @__PURE__ */ jsx("button", { "data-testid": "bodymap3d-sex-female", onClick: () => changeSex("female"), style: seg(sex === "female"), children: UI.female })
|
|
723
|
+
/* @__PURE__ */ jsx("button", { "data-testid": "bodymap3d-sex-male", onClick: () => changeSex("male"), style: seg(chrome, sex === "male"), children: UI.male }),
|
|
724
|
+
/* @__PURE__ */ jsx("button", { "data-testid": "bodymap3d-sex-female", onClick: () => changeSex("female"), style: seg(chrome, sex === "female"), children: UI.female })
|
|
686
725
|
] }) }),
|
|
687
726
|
/* @__PURE__ */ jsx("span", { ref: loadedRef, "data-testid": "bodymap3d-loaded", style: { display: "none" } }),
|
|
688
727
|
ready && /* @__PURE__ */ jsx("span", { "data-testid": "bodymap3d-ready", style: { position: "absolute", width: 1, height: 1, opacity: 0, pointerEvents: "none" } }),
|
|
@@ -695,7 +734,7 @@ function BodyMap3D(props) {
|
|
|
695
734
|
"aria-label": isFullscreen ? UI.collapse ?? "Close" : UI.expand ?? "Expand",
|
|
696
735
|
onClick: () => setFullscreen(!isFullscreen),
|
|
697
736
|
style: {
|
|
698
|
-
...btn,
|
|
737
|
+
...btn(chrome),
|
|
699
738
|
color: chrome.text,
|
|
700
739
|
borderColor: chrome.border,
|
|
701
740
|
background: chrome.panelBg,
|
|
@@ -705,7 +744,14 @@ function BodyMap3D(props) {
|
|
|
705
744
|
}
|
|
706
745
|
) }),
|
|
707
746
|
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 18, alignItems: "flex-start", flexWrap: "wrap", ...isFullscreen ? { flex: "1 1 auto", minHeight: 0 } : null }, children: [
|
|
708
|
-
unsupported ? /* @__PURE__ */ jsx(
|
|
747
|
+
unsupported ? /* @__PURE__ */ jsx(
|
|
748
|
+
"div",
|
|
749
|
+
{
|
|
750
|
+
"data-testid": "bodymap3d-unsupported",
|
|
751
|
+
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 },
|
|
752
|
+
children: "3D kr\xE6ver WebGL, som ikke er tilg\xE6ngeligt her."
|
|
753
|
+
}
|
|
754
|
+
) : /* @__PURE__ */ jsx(
|
|
709
755
|
"div",
|
|
710
756
|
{
|
|
711
757
|
ref: mountRef,
|
|
@@ -720,7 +766,7 @@ function BodyMap3D(props) {
|
|
|
720
766
|
minHeight: isFullscreen ? 0 : void 0,
|
|
721
767
|
borderRadius: 16,
|
|
722
768
|
overflow: "hidden",
|
|
723
|
-
background:
|
|
769
|
+
background: chrome.stageBg,
|
|
724
770
|
touchAction: "none"
|
|
725
771
|
}
|
|
726
772
|
}
|
|
@@ -739,16 +785,16 @@ function BodyMap3D(props) {
|
|
|
739
785
|
"data-testid": "bodymap3d-close",
|
|
740
786
|
"aria-label": L.close,
|
|
741
787
|
onClick: () => setSelected(null),
|
|
742
|
-
style: { ...btn, width: 32, height: 32, padding: 0, fontSize: 20, lineHeight: 1, color: chrome.mutedText, borderColor: chrome.border, background: chrome.panelBg },
|
|
788
|
+
style: { ...btn(chrome), width: 32, height: 32, padding: 0, fontSize: 20, lineHeight: 1, color: chrome.mutedText, borderColor: chrome.border, background: chrome.panelBg },
|
|
743
789
|
children: "\xD7"
|
|
744
790
|
}
|
|
745
791
|
)
|
|
746
792
|
] }),
|
|
747
793
|
/* @__PURE__ */ jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.intensity }),
|
|
748
|
-
/* @__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, display: "inline-flex", alignItems: "center", justifyContent: "center", minWidth: 30, height: 30, padding: 0, background: current?.intensity === i ?
|
|
794
|
+
/* @__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)) }),
|
|
749
795
|
/* @__PURE__ */ jsx("div", { style: { fontSize: 11, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: chrome.mutedText, marginBottom: 7 }, children: L.quality }),
|
|
750
|
-
/* @__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, borderRadius: 999, padding: "6px 12px", background: current?.type === t ?
|
|
751
|
-
current && /* @__PURE__ */ jsx("button", { "data-testid": "bodymap3d-remove", onClick: () => removePain(region.key), style: { ...btn, color: chrome.danger, borderColor:
|
|
796
|
+
/* @__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
|
+
current && /* @__PURE__ */ jsx("button", { "data-testid": "bodymap3d-remove", onClick: () => removePain(region.key), style: { ...btn(chrome), color: chrome.danger, borderColor: chrome.dangerBorder }, children: L.remove })
|
|
752
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 })
|
|
753
799
|
] })
|
|
754
800
|
]
|
|
@@ -756,6 +802,6 @@ function BodyMap3D(props) {
|
|
|
756
802
|
);
|
|
757
803
|
}
|
|
758
804
|
|
|
759
|
-
export { BodyMap3D, seamBlend, serializeReport };
|
|
805
|
+
export { BodyMap3D, ensureNormals, seamBlend, seamWeight, serializeReport };
|
|
760
806
|
//# sourceMappingURL=three.js.map
|
|
761
807
|
//# sourceMappingURL=three.js.map
|