@broberg/bodymap 0.4.0 → 0.5.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 CHANGED
@@ -211,6 +211,47 @@ had *arrived* on a device that never *showed* it.)
211
211
  `VIBRATION_PATTERNS.ignore` is empty on purpose: a tap that changed nothing must
212
212
  not feel like it did.
213
213
 
214
+ ## `seam` — soft region edges are OPT-IN, and here is why
215
+
216
+ ```tsx
217
+ <BodyMap3D seam /> // soft colour transition between regions
218
+ <BodyMap3D /> // default: flat, full-contrast regions
219
+ ```
220
+
221
+ **Default off, because the default has to be the accessible one.** The blend mixes
222
+ the *body* colour into a marked region near its boundary — and on a **small** mark,
223
+ the boundary is most of the mark.
224
+
225
+ Measured on a phone (`iphone-15`), a consumer's real palette, one knee marked,
226
+ averaged over every pixel of the mark:
227
+
228
+ | | mark colour | contrast vs body |
229
+ |---|---|---|
230
+ | `seam` on | `rgb(114, 58, 79)` | **3.59:1** — fails WCAG AA |
231
+ | `seam` off (default) | `rgb(111, 22, 56)` | **4.71:1** — passes |
232
+
233
+ At the single strongest pixel the cost is small (5.12 → 4.78), which is why a
234
+ spot-check misses it. The average is what a person sees.
235
+
236
+ **Turn it on** when marked areas are large and you have no accessibility duty — it
237
+ genuinely looks better there. **Leave it off** for anything a public authority,
238
+ a clinician or a partially-sighted user will read.
239
+
240
+ ## Smooth skin: we do not recompute the model's normals
241
+
242
+ If your `.glb` ships normals, the renderer uses them. It only calls
243
+ `computeVertexNormals()` when the file has none.
244
+
245
+ This matters more than it sounds. `computeVertexNormals()` averages face normals
246
+ **per vertex index**, and a body mesh has split vertices along its UV seams — two
247
+ copies at the same position averaging different faces. The two sides then light
248
+ differently, drawing a visible crease along every seam: a line down the torso, and
249
+ across the chest, waist, shoulders, knees, elbows and ankles. The figure looks
250
+ assembled from parts. Measured side by side: with the call, every line is there;
251
+ without it, none are.
252
+
253
+ If you supply your own model, **ship it with normals.**
254
+
214
255
  ## Colour control — `BodymapPalette`
215
256
 
216
257
  Both renderers theme off one palette (consumer-defined):
package/dist/three.cjs CHANGED
@@ -308,6 +308,7 @@ function BodyMap3D(props) {
308
308
  showRegionCode = true,
309
309
  onFeedback,
310
310
  haptics,
311
+ seam = false,
311
312
  className
312
313
  } = props;
313
314
  const chrome = uiColors(palette);
@@ -345,6 +346,8 @@ function BodyMap3D(props) {
345
346
  const pickRef = react.useRef(() => "ignore");
346
347
  const setReadyRef = react.useRef(setReady);
347
348
  setReadyRef.current = setReady;
349
+ const seamRef = react.useRef(seam);
350
+ seamRef.current = seam;
348
351
  const apiRef = react.useRef(null);
349
352
  react.useEffect(() => {
350
353
  if (!webglAvailable()) {
@@ -438,7 +441,7 @@ function BodyMap3D(props) {
438
441
  };
439
442
  for (let i = 0; i < vertexRegion.length; i++) {
440
443
  tmp.copy(col(vertexRegion[i]));
441
- const w = vertexBlend[i];
444
+ const w = seamRef.current ? vertexBlend[i] : 0;
442
445
  if (w > 0) {
443
446
  tmp2.copy(col(vertexNeighbour[i]));
444
447
  tmp.lerp(tmp2, w);
@@ -472,7 +475,7 @@ function BodyMap3D(props) {
472
475
  });
473
476
  if (bodyMesh) {
474
477
  const geo = bodyMesh.geometry;
475
- geo.computeVertexNormals();
478
+ if (!geo.getAttribute("normal")) geo.computeVertexNormals();
476
479
  const pos = geo.getAttribute("position");
477
480
  const n = pos.count;
478
481
  const cols = new Float32Array(n * 3);
@@ -609,7 +612,7 @@ function BodyMap3D(props) {
609
612
  }, []);
610
613
  react.useEffect(() => {
611
614
  apiRef.current?.refresh();
612
- }, [selected, report, palette]);
615
+ }, [selected, report, palette, seam]);
613
616
  const sexInited = react.useRef(false);
614
617
  react.useEffect(() => {
615
618
  if (!sexInited.current) {