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.
import { Group, Matrix3, Mesh, MeshStandardMaterial, SphereGeometry, Triangle, Vector3 } from 'three';
import type { Intersection } from 'three';
import type { Mark, Point3 } from './case.js';
import { ANATOMY_MODEL, ANATOMY_TRIANGLES, anatomyLabels, type AnatomyChoice } from './anatomy-contract.js';
export { ANATOMY_MODEL, type AnatomyChoice } from './anatomy-contract.js';
const hanging:Record<string,{p:Point3;scale:Point3;tilt:number}>={
shaft:{p:[0,-.043,-.012],scale:[.014,.047,.015],tilt:-.12},
glans:{p:[0,-.082,-.007],scale:[.016,.018,.017],tilt:-.12},
'scrotum-left':{p:[.013,-.060,-.040],scale:[.022,.036,.027],tilt:.12},
'scrotum-right':{p:[-.013,-.063,-.040],scale:[.022,.036,.027],tilt:.12},
};
const axis=new Vector3(1,0,0);
export class ExternalAnatomy {
readonly group = new Group();
private meshes = new Map<string, Mesh>();
private selected: AnatomyChoice = 'none';
private geometry = new SphereGeometry(1,32,20);
private detail: MeshStandardMaterial;
constructor(material: MeshStandardMaterial) {
this.detail = material.clone(); this.detail.color.set('#737780');
const add = (id:string, kind:'penis'|'vulva', p:Point3, scale:Point3, detail=false, tilt=0) => {
let geometry=this.geometry;
if(id.startsWith('labium-majus') || id==='shaft') {
geometry=this.geometry.clone();const positions=geometry.getAttribute('position');
for(let i=0;i<positions.count;i++) {
const y=positions.getY(i);
const inset=id==='shaft'?Math.max(0,y-.3)**2*1.5:Math.pow(y,4);
positions.setZ(i,positions.getZ(i)-inset);
}
geometry.computeVertexNormals();
}
const mesh = new Mesh(geometry, detail ? this.detail : material);
mesh.name=id; mesh.userData.anatomy=kind; mesh.position.set(...p); mesh.scale.set(...scale); mesh.rotation.x=tilt;
this.meshes.set(id,mesh); this.group.add(mesh);
};
// Neutral adult, flaccid schematic. No organ/pathology inference from shape.
for(const [id,spec] of Object.entries(hanging)) add(id,'penis',spec.p,spec.scale,false,spec.tilt);
add('vestibule','vulva',[0,-.030,0],[.009,.031,.007],true);
add('labium-majus-left','vulva',[.016,-.027,-.005],[.016,.043,.018]);
add('labium-majus-right','vulva',[-.016,-.027,-.005],[.016,.043,.018]);
add('labium-minus-left','vulva',[.007,-.028,.004],[.005,.028,.009]);
add('labium-minus-right','vulva',[-.007,-.028,.004],[.005,.028,.009]);
add('hood','vulva',[0,.002,.006],[.0045,.006,.006]);
add('urethral-opening','vulva',[0,-.018,.007],[.002,.003,.0008],true);
add('vaginal-opening','vulva',[0,-.040,.007],[.005,.008,.0008],true);
}
select(choice:AnatomyChoice,sex:string):void {
this.selected = choice==='auto' ? sex==='Male'?'penis':sex==='Female'?'vulva':'none' : choice;
for(const mesh of this.meshes.values()) mesh.visible=mesh.userData.anatomy===this.selected;
}
get surfaces():Mesh[] { return [...this.meshes.values()].filter(m=>m.userData.anatomy===this.selected); }
fit(pubic:Vector3,perineal:Vector3):void {
const slope=pubic.clone().sub(perineal);
const tilt=Math.atan2(slope.z,slope.y);
this.group.position.copy(pubic);
this.group.rotation.x=tilt;
// Labial surfaces follow the downward/backward pubic contour. Embed their
// rear halves into the skin instead of placing a vertical plate in front.
const length=slope.length()/.091;
// A flaccid shaft hangs vertically; the scrotal roots sit farther back under
// the pubis. Counter-rotate these components against the fitted surface.
for(const [id,spec] of Object.entries(hanging)) {
const mesh=this.meshes.get(id)!;
mesh.position.set(...spec.p).applyAxisAngle(axis,-tilt);
mesh.rotation.x=spec.tilt-tilt;
}
// Scale along the surface only, preserving the original component topology
// and barycentric mark addresses across changes in body shape.
this.group.scale.set(1,length,1);
}
anchor(mark:Mark, owner:Group):{point:Vector3;normal:Vector3}|undefined {
const s=mark.surface,mesh=s && this.meshes.get(s.zone);
if(!s || s.model!==ANATOMY_MODEL || !mesh || mesh.userData.anatomy!==this.selected || s.triangle<0 || s.triangle>=ANATOMY_TRIANGLES) return;
const [a,b,c]=this.vertices(mesh,s.triangle),point=a.clone().multiplyScalar(s.barycentric[0]).addScaledVector(b,s.barycentric[1]).addScaledVector(c,s.barycentric[2]);
const normal=Triangle.getNormal(a,b,c,new Vector3());
// Account for nonuniform component and surface scaling.
const worldNormal=normal.applyNormalMatrix(new Matrix3().getNormalMatrix(mesh.matrixWorld));
const localNormal=worldNormal.transformDirection(owner.matrixWorld.clone().invert());
return {point:owner.worldToLocal(mesh.localToWorld(point)),normal:localNormal};
}
private vertices(mesh:Mesh,triangle:number):Vector3[] {
const index=mesh.geometry.index!,p=mesh.geometry.getAttribute('position');
return [0,1,2].map(j=>new Vector3().fromBufferAttribute(p,index.getX(triangle*3+j)));
}
mark(hit:Intersection,size:number,stroke:number,owner:Group):{mark:Mark;label:string}|undefined {
const mesh=this.meshes.get(hit.object.name); if(!mesh || hit.faceIndex==null) return;
const [a,b,c]=this.vertices(mesh,hit.faceIndex),p=mesh.worldToLocal(hit.point.clone());
const bary=Triangle.getBarycoord(p,a,b,c,new Vector3()); if(!bary) return;
const label=anatomyLabels[mesh.name];
const mark:Mark={part:'pelvis',point:[0,.86,.12],normal:[0,0,1],size,stroke,
surface:{model:ANATOMY_MODEL,triangle:hit.faceIndex,barycentric:bary.toArray() as Point3,uv:[0,0],zone:mesh.name,label}};
const anchor=this.anchor(mark,owner); if(anchor) mark.normal=anchor.normal.toArray() as Point3;
return {mark,label};
}
dispose():void { for(const geometry of new Set([this.geometry,...[...this.meshes.values()].map(m=>m.geometry)])) geometry.dispose(); this.detail.dispose(); }
}
