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.

src/external-anatomy.ts
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(); }
}