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/body-inspection.ts
import { Box3, BufferGeometry, Mesh, SkinnedMesh, Uint16BufferAttribute, Vector3 } from 'three';
import type { Intersection } from 'three';
import type { SkinModel } from './skin-model.js';
import type { Mark } from './case.js';
import { anatomyTree, anatomyLeaves, descendants, flattenAnatomy, skinLeafForBone, type AnatomyNode } from './anatomy-tree.js';
import type { FocusOption } from './inspection-record.js';

/** Camera selection and display filtering are independent. Paint addresses never change. */
export class BodyInspection {
  target: string | null = null;
  private hiddenLeaves = new Set<string>();
  private triangleLeaves: string[];
  private focusVertices: number[] = [];
  private focusMeshes: Mesh[] = [];
  private overlay?: SkinnedMesh;
  private originals = new Map<Mesh, boolean>();
  constructor(private model: SkinModel) {
    const data = model.data;
    this.triangleLeaves = Array.from({length:data.indices.length / 3}, (_, t) => {
      const scores = new Float32Array(model.bones.length);
      for (let j = 0; j < 3; j++) {
        const v = data.indices[t * 3 + j];
        for (let k = 0; k < 4; k++) scores[data.skinIndices[v * 4 + k]] += data.skinWeights[v * 4 + k];
      }
      return skinLeafForBone(data.meta.bones[scores.indexOf(Math.max(...scores))].name);
    });
  }
  tree(): AnatomyNode {
    const names = new Set(this.model.surfaces.map(m => `structure:${m.name}`));
    const prune = (node: AnatomyNode): AnatomyNode | undefined => {
      if (!node.children) return node.id.startsWith('structure:') && !names.has(node.id) ? undefined : node;
      const children = node.children.map(prune).filter((n): n is AnatomyNode => !!n);
      return children.length ? {...node, children} : undefined;
    };
    return prune(anatomyTree(this.model.layer)) || {id:'region:body',label:'All anatomy',children:[]};
  }
  options(): FocusOption[] { return flattenAnatomy(this.tree()).filter(n => n.id !== 'outline' && n.id !== 'region:body').map(({id,label}) => ({id,label})); }
  get hidden(): string[] { return [...this.hiddenLeaves].sort(); }
  private restoreDisplay(): void {
    for (const [mesh, visible] of this.originals) mesh.visible = visible;
    this.originals.clear(); this.overlay?.removeFromParent(); this.overlay?.geometry.dispose(); this.overlay = undefined;
    this.model.markVisibility = undefined; this.model.showOutline(true);
  }
  clear(): void { this.restoreDisplay(); this.hiddenLeaves.clear(); this.target = null; this.focusVertices = []; this.focusMeshes = []; }
  /** Focus changes no mesh visibility or picking policy. */
  set(target: string | null): boolean {
    if (target && !this.options().some(p => p.id === target)) return false;
    this.target = target; this.focusVertices = []; this.focusMeshes = [];
    if (!target) return true;
    const selected = new Set(descendants(this.model.layer, target));
    this.focusMeshes = this.model.surfaces.filter(mesh => selected.has(`structure:${mesh.name}`));
    // Region pivots use the posed skin envelope, including hand muscles whose
    // tendons extend into the forearm. Named structures use their own geometry.
    if (target.startsWith('region:')) {
      const skinLeaves = new Set(descendants('skin', target)), vertices = new Set<number>();
      this.triangleLeaves.forEach((id,t) => { if (skinLeaves.has(id)) for (let j=0;j<3;j++) vertices.add(this.model.data.indices[t*3+j]); });
      this.focusVertices = [...vertices];
    }
    return true;
  }
  setHidden(ids: readonly string[]): void {
    this.restoreDisplay();
    const known = new Set(anatomyLeaves(this.tree()));
    this.hiddenLeaves = new Set(ids.filter(id => known.has(id)));
    if (!this.hiddenLeaves.size) return;
    const source = this.model.mesh;
    for (const mesh of this.model.surfaces) {
      this.originals.set(mesh, mesh.visible);
      if (mesh !== source) mesh.visible = !this.hiddenLeaves.has(`structure:${mesh.name}`);
    }
    if (this.model.layer === 'skin' && this.triangleLeaves.some(id => this.hiddenLeaves.has(id))) {
      source.visible = false;
      const indices: number[] = [];
      this.triangleLeaves.forEach((id,t) => { if (!this.hiddenLeaves.has(id)) for(let j=0;j<3;j++) indices.push(this.model.data.indices[t*3+j]); });
      if (indices.length) {
        const geometry = new BufferGeometry();
        for (const [name, attribute] of Object.entries(source.geometry.attributes)) geometry.setAttribute(name, attribute);
        geometry.setIndex(new Uint16BufferAttribute(indices, 1));
        this.overlay = new SkinnedMesh(geometry, source.material); this.overlay.frustumCulled = false;
        this.overlay.bind(source.skeleton, source.bindMatrix); this.model.group.add(this.overlay);
      }
    }
    this.model.showOutline(!this.hiddenLeaves.has('outline'));
    this.model.markVisibility = mark => this.includesMark(mark);
  }
  setVisible(id: string, visible: boolean): void {
    const next = new Set(this.hiddenLeaves);
    for (const leaf of descendants(this.model.layer, id)) { if (visible) next.delete(leaf); else next.add(leaf); }
    this.setHidden([...next]);
  }
  get surfaces(): Mesh[] {
    return this.model.surfaces.filter(m => m === this.model.mesh ? this.triangleLeaves.some(id => !this.hiddenLeaves.has(id)) : !this.hiddenLeaves.has(`structure:${m.name}`));
  }
  accepts(hit: Intersection): boolean {
    return hit.object === this.model.mesh ? !this.hiddenLeaves.has(this.triangleLeaves[hit.faceIndex ?? -1]) : !this.hiddenLeaves.has(`structure:${hit.object.name}`);
  }
  includesMark(mark: Mark): boolean {
    return mark.surface?.model === this.model.data.meta.id ? !this.hiddenLeaves.has(this.triangleLeaves[mark.surface.triangle]) : !this.hiddenLeaves.has(`structure:${mark.surface?.zone}`);
  }
  bounds(): Box3 | undefined {
    if (!this.target) return;
    this.model.update(); const box = new Box3(), point = new Vector3();
    if (this.focusVertices.length) {
      for (const v of this.focusVertices) box.expandByPoint(this.model.mesh.localToWorld(this.model.mesh.getVertexPosition(v,point)));
    } else for (const mesh of this.focusMeshes) for (let v=0;v<mesh.geometry.getAttribute('position').count;v++) box.expandByPoint(mesh.localToWorld(mesh.getVertexPosition(v,point)));
    return box.isEmpty() ? undefined : box;
  }
  dispose(): void { this.clear(); }
}