Back to Body Journal
SOURCE / PINNED RELEASE

Made of little things.

Body Journal

Release
e8049f8da4e2…
Author-recorded commit
61c04bf0e8aa…
License
LICENSE
Author’s source reference
nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-6c4554844c7

Archive hash verified: 4369d778da046a8b…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

tests/fingers.test.mjs
import assert from 'node:assert/strict';
import test from 'node:test';
import {readFileSync} from 'node:fs';
import {resolve,dirname} from 'node:path';
import {runInNewContext} from 'node:vm';
import ts from 'typescript';
import * as three from 'three';
const cache=new Map();
function load(file){
 const path=resolve(file);if(cache.has(path))return cache.get(path).exports;
 const module={exports:{}};cache.set(path,module);
 runInNewContext(ts.transpileModule(readFileSync(path,'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText,{module,exports:module.exports,TextEncoder,TextDecoder,require:name=>name==='three'?three:load(resolve(dirname(path),name.replace(/\.js$/,'.ts')))});return module.exports;
}
const {decodeAtlas}=load('src/atlas-data.ts');
const {SkinModel,decodeSkin}=load('src/skin-model.ts'),{decodeSkeleton}=load('src/skeleton-data.ts');
const {draftString,parseDraft,casePayload}=load('src/case-record.ts'),{parseSession,sessionString}=load('src/session-record.ts');
const {profile,makeConcern}=load('src/case.ts');
const bytes=file=>{const b=readFileSync(file);return b.buffer.slice(b.byteOffset,b.byteOffset+b.byteLength);};
const data=decodeSkin(bytes('assets/skin/body-skin.bin')),skeleton=decodeSkeleton(bytes('assets/skeleton/skeleton.bin'));
const muscles=decodeAtlas(bytes('assets/anatomy/muscles.bin'),'bp3d-muscles-1'),organs=decodeAtlas(bytes('assets/anatomy/organs.bin'),'bp3d-organs-1');
const {MeshStandardMaterial,Vector3,Raycaster}=three;
function model(){const m=new SkinModel(data,new MeshStandardMaterial(),skeleton,[muscles,organs]);m.setLayer('skeleton');m.update();return m;}
const {parseFingerPose,describeFingers,digits}=load('src/finger-pose.ts');
const {BodyInspection}=load('src/body-inspection.ts');
const point=(m,name)=>m.bones.find(b=>b.name===name).getWorldPosition(new Vector3());

test('each finger bends toward its own palm, without moving adjacent fingers or its base',()=>{
 const m=model();
 for(const [side,suffix,sign] of [['left','l',-1],['right','r',1]]) for(const digit of digits){
  m.fingers.set({});m.update();
  const root=point(m,`${digit}_01_${suffix}`),tip=point(m,`${digit}_03_${suffix}`);
  const other=digit==='index'?'middle':'index',neighbor=point(m,`${other}_03_${suffix}`);
  m.fingers.set({[`${side}-${digit}`]:[.5,.5,.5]});m.update();
  const moved=point(m,`${digit}_03_${suffix}`);
  assert.ok(sign*(moved.x-tip.x)>.005,`${side} ${digit} curls inward`);
  assert.ok(point(m,`${digit}_01_${suffix}`).distanceTo(root)<1e-8);
  assert.ok(point(m,`${other}_03_${suffix}`).distanceTo(neighbor)<1e-8);
  m.fingers.set({[`${side}-${digit}`]:[0,.7,0]});m.update();
  assert.ok(point(m,`${digit}_03_${suffix}`).distanceTo(tip)>.005);
  assert.ok(point(m,`${digit}_01_${suffix}`).distanceTo(root)<1e-8);
 }
 m.dispose();
});
test('all named hand phalanges follow the correct digit and segment',()=>{
 const m=model();
 for(const part of skeleton.parts){
  const match=part.label.toLowerCase().match(/(proximal|middle|distal) phalanx of (left|right) (thumb|index|middle|ring|little)(?: finger)?$/);
  if(!match)continue;
  const [,segment,side,finger]=match,digit=finger==='little'?'pinky':finger;
  const n=segment==='distal'?3:segment==='middle'?2:digit==='thumb'?2:1;
  assert.equal(data.meta.bones[part.bone].name,`${digit}_0${n}_${side==='left'?'l':'r'}`,part.label);
  const mesh=m.surfaces.find(s=>s.name===part.id);assert.equal(mesh.parent,m.bones[part.bone]);
 }
 m.dispose();
});
test('finger surface anchors deform with joints and survive body shape and limb posing',()=>{
 const m=model();m.setLayer('skin');
 const inspect=new BodyInspection(m);inspect.set('region:left-index');inspect.setVisible('region:body',false);inspect.setVisible('region:left-index',true);
 const center=inspect.bounds().getCenter(new Vector3());
 const hit=new Raycaster(center.clone().add(new Vector3(0,0,1)),new Vector3(0,0,-1)).intersectObjects(inspect.surfaces,false).find(h=>inspect.accepts(h));assert.ok(hit);
 const {mark}=m.mark(hit,.003,1),serialized=JSON.stringify(mark),before=m.anchor(mark).point.clone();
 m.fingers.set({'left-index':[.6,.8,.4]});m.update();
 assert.ok(m.anchor(mark).point.distanceTo(before)>.015);
 assert.ok(inspect.bounds().containsPoint(m.anchor(mark).point));
 const pose=JSON.stringify(m.fingers.snapshot());m.joints.get('left-elbow').rotation.x=-1.2;m.shape(190,125,'Female');
 assert.equal(JSON.stringify(m.fingers.snapshot()),pose);assert.equal(JSON.stringify(mark),serialized);
 assert.ok(inspect.bounds().containsPoint(m.anchor(mark).point));
 inspect.clear();m.setLayer('skeleton');assert.ok(inspect.set('region:left-hand'));inspect.setVisible('region:body',false);inspect.setVisible('region:left-hand',true);assert.equal(inspect.surfaces.length,27);
 inspect.clear();m.setLayer('muscles');assert.ok(inspect.set('region:right-hand'));assert.ok(inspect.surfaces.length>5);
 inspect.dispose();m.dispose();
});
test('finger records are bounded, independent, backwards compatible and included in text/revisions',()=>{
 const c=makeConcern(1,'Left hand','standing'),second=makeConcern(2,'Right hand','sitting');
 c.fingers={'left-index':[.2,.9,.4]};second.fingers={'right-thumb':[.3,.4,.5]};
 const draft=parseDraft(draftString(profile,[c,second]));
 assert.equal(JSON.stringify(draft.concerns[0].fingers),JSON.stringify(c.fingers));assert.equal(JSON.stringify(draft.concerns[1].fingers),JSON.stringify(second.fingers));
 const session={version:1,draft,view:{page:'map',panel:'body',selectedId:1,pose:'standing',layer:'skin',exampleAttachment:false,newObservation:true,fingers:{'left-pinky':[.5,0,0]}}};
 assert.equal(JSON.stringify(parseSession(sessionString(session)).view.fingers),JSON.stringify(session.view.fingers));
 assert.equal(parseSession(sessionString(session)).view.newObservation,true);
 assert.throws(()=>parseSession(JSON.stringify({...session,view:{...session.view,newObservation:'yes'}})));
 const initial=casePayload(profile,[c]);assert.match(initial.concerns[0].illustratedPose,/left index finger bent at base \/ middle \/ tip/);
 c.fingers['left-index'][1]=.5;assert.notEqual(initial.bodyMapRevision,casePayload(profile,[c]).bodyMapRevision);
 for(const invalid of [null,[],{'left-eye':[0,0,0]},{'left-index':[0,0]},{'left-index':[0,-.1,0]},{'right-thumb':[0,1.1,0]},{'left-pinky':[0,NaN,0]},{'left-ring':['1',0,0]}])assert.throws(()=>parseFingerPose(invalid));
 const snapshot=parseFingerPose(c.fingers);snapshot['left-index'][0]=1;assert.equal(c.fingers['left-index'][0],.2);
 delete c.fingers;assert.equal(parseDraft(draftString(profile,[c])).concerns[0].fingers,undefined);assert.equal(describeFingers().length,0);
});

test('fingertip IK reaches bent targets on both posed hands without disturbing neighbors',()=>{
 const m=model();m.shape(185,92,'Male');m.joints.get('left-arm').rotation.set(.25,.2,-.4);m.joints.get('right-arm').rotation.set(-.4,.15,.2);
 for(const side of ['left','right'])for(const digit of digits){
  const id=`${side}-${digit}`,other=`${side}-${digit==='index'?'ring':'index'}`;
  m.fingers.set({[id]:[.4,.7,.45],[other]:[.2,.3,.2]});m.update();const target=m.fingers.endpoint(id);
  m.fingers.set({[other]:[.2,.3,.2]});m.update();const neighbor=m.fingers.endpoint(other),before=m.fingers.endpoint(id).distanceTo(target);
  m.fingers.solve(id,target);m.update();
  assert.ok(m.fingers.endpoint(id).distanceTo(target)<.002,`${id} reaches target within 2 mm (was ${before})`);
  assert.ok(m.fingers.endpoint(other).distanceTo(neighbor)<1e-8,'neighbor stays fixed');
  m.fingers.solve(id,new Vector3(100,100,100));m.update();
  assert.ok(m.fingers.snapshot()[id].every(n=>Number.isFinite(n)&&n>=0&&n<=1));
  const saved=JSON.stringify(m.fingers.snapshot());m.fingers.solve(id,new Vector3(NaN,0,0));assert.equal(JSON.stringify(m.fingers.snapshot()),saved);
 }
 m.dispose();
});