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 { 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(); }
}
