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, Float32BufferAttribute, Matrix4, MeshStandardMaterial, Quaternion, SkinnedMesh, Sphere, Triangle, Uint16BufferAttribute, Vector3 } from 'three';
import type { Group, Intersection, Skeleton } from 'three';
import type { AtlasData, AtlasPart } from './atlas-data.js';
import type { AnatomyChoice } from './anatomy-contract.js';
import type { Mark, Point3 } from './case.js';
/** Reference soft-tissue geometry, sharing the body rig with blended vertex weights. */
export class AtlasModel {
private items: { mesh: SkinnedMesh; info: AtlasPart; rest: Float32Array; bounds: Map<number, Sphere> }[] = [];
private materials = new Map<string, MeshStandardMaterial>();
private shown = false;
private anatomy: 'male' | 'female' | 'none' = 'none';
private applicable(info: AtlasPart): boolean { return !info.sex || info.sex === this.anatomy; }
select(choice: AnatomyChoice, sex: string): void {
this.anatomy = choice === 'penis' ? 'male' : choice === 'vulva' ? 'female' : choice === 'auto' && sex === 'Male' ? 'male' : choice === 'auto' && sex === 'Female' ? 'female' : 'none';
this.visible = this.shown;
}
constructor(private data: AtlasData, skeleton: Skeleton, owner: Group, private referenceHeads: Float32Array, private parents: number[]) {
for (const info of data.parts) {
const rest = new Float32Array(data.buffer, data.start + info.positions, info.vertices * 3);
const geometry = new BufferGeometry();
geometry.setAttribute('position', new Float32BufferAttribute(rest.slice(), 3));
geometry.setIndex(new Uint16BufferAttribute(new Uint16Array(data.buffer, data.start + info.indices, info.triangles * 3), 1));
geometry.setAttribute('skinIndex', new Uint16BufferAttribute(new Uint16Array(data.buffer, data.start + info.skinIndices, info.vertices * 4), 4));
geometry.setAttribute('skinWeight', new Float32BufferAttribute(new Float32Array(data.buffer, data.start + info.skinWeights, info.vertices * 4), 4));
geometry.computeVertexNormals();
if (!this.materials.has(info.color)) this.materials.set(info.color, new MeshStandardMaterial({ color: info.color, roughness: .85 }));
const mesh = new SkinnedMesh(geometry, this.materials.get(info.color));
mesh.name = info.id; mesh.userData.anatomicalPart = info.part; mesh.frustumCulled = false;
const item = { mesh, info, rest, bounds: new Map<number, Sphere>() };
const transform = new Matrix4(), sphere = new Sphere();
mesh.boundingSphere = new Sphere();
// A sphere enclosing every bone-transformed contribution also encloses
// their weighted blend. Tight bounds avoid skinning every muscle on each
// pointer move; the final hit still tests the actual deformed triangles.
mesh.raycast = function(raycaster, hits) {
this.boundingSphere!.makeEmpty();
for(const [bone, bound] of item.bounds) {
transform.fromArray(skeleton.boneMatrices!, bone * 16).premultiply(this.bindMatrixInverse).multiply(this.bindMatrix);
this.boundingSphere!.union(sphere.copy(bound).applyMatrix4(transform));
}
SkinnedMesh.prototype.raycast.call(this, raycaster, hits);
};
owner.add(mesh); mesh.bind(skeleton, new Matrix4());
this.items.push(item);
}
this.visible = false;
}
set visible(value: boolean) { this.shown = value; for (const { mesh, info } of this.items) mesh.visible = value && this.applicable(info); }
get surfaces(): SkinnedMesh[] { return this.items.filter(item => this.applicable(item.info)).map(item => item.mesh); }
shape(heads: Float32Array): void {
const transforms = this.parents.map((_, i) => {
const a = new Vector3().fromArray(this.referenceHeads, i * 3), nextA = new Vector3().fromArray(heads, i * 3);
// Axial organs retain atlas proportions. A spine's first child may be
// the left clavicle; using it as a length axis sheared the chest.
const child = [0,1,2,3,4,43,44].includes(i) ? -1 : i===8 ? 12 : i===27 ? 31 : this.parents.indexOf(i);
const transform = new Matrix4().makeTranslation(nextA.x, nextA.y, nextA.z);
if (child >= 0) {
const ref = new Vector3().fromArray(this.referenceHeads, child * 3).sub(a);
const next = new Vector3().fromArray(heads, child * 3).sub(nextA);
if (ref.lengthSq() > 1e-6 && next.lengthSq() > 1e-6) {
const axis = ref.clone().normalize(), frame = new Matrix4().makeRotationFromQuaternion(new Quaternion().setFromUnitVectors(new Vector3(0, 1, 0), axis));
const stretch = frame.clone().multiply(new Matrix4().makeScale(1, next.length() / ref.length(), 1)).multiply(frame.clone().invert());
transform.multiply(new Matrix4().makeRotationFromQuaternion(new Quaternion().setFromUnitVectors(axis, next.normalize()))).multiply(stretch);
}
}
return transform.multiply(new Matrix4().makeTranslation(-a.x, -a.y, -a.z));
});
const p = new Vector3(), result = new Vector3();
for (const { mesh, rest, info, bounds } of this.items) {
const positions = mesh.geometry.getAttribute('position'), indices = mesh.geometry.getAttribute('skinIndex'), weights = mesh.geometry.getAttribute('skinWeight');
const boxes = new Map<number, Box3>();
for (let i = 0; i < info.vertices; i++) {
result.set(0, 0, 0);
for (let k = 0; k < 4; k++) {
const w = weights.array[i * 4 + k]; if (!w) continue;
result.addScaledVector(p.fromArray(rest, i * 3).applyMatrix4(transforms[indices.array[i * 4 + k]]), w);
}
positions.setXYZ(i, result.x, result.y, result.z);
for(let k=0;k<4;k++) if(weights.array[i*4+k]>0) {
const bone=indices.array[i*4+k];if(!boxes.has(bone))boxes.set(bone,new Box3());boxes.get(bone)!.expandByPoint(result);
}
}
bounds.clear();for(const [bone,box] of boxes)bounds.set(bone,box.getBoundingSphere(new Sphere()));
positions.needsUpdate = true; mesh.geometry.computeVertexNormals();
}
}
anchor(mark: Mark): { point: Vector3; normal: Vector3 } | undefined {
const surface = mark.surface, item = this.items.find(item => item.info.id === surface?.zone);
if (!surface || surface.model !== this.data.id || !item || !this.applicable(item.info) || surface.triangle < 0 || surface.triangle >= item.info.triangles) return;
const vertices = this.triangle(item.mesh, surface.triangle);
return { point: vertices.reduce((p, v, i) => p.addScaledVector(v, surface.barycentric[i]), new Vector3()), normal: Triangle.getNormal(...vertices, new Vector3()) };
}
mark(hit: Intersection, size: number, stroke: number): { mark: Mark; label: string } | undefined {
const item = this.items.find(item => item.mesh === hit.object);
if (!item || !this.applicable(item.info) || hit.faceIndex == null) return;
const { mesh, info, rest } = item, vertices = this.triangle(mesh, hit.faceIndex);
const bary = Triangle.getBarycoord(mesh.worldToLocal(hit.point.clone()), ...vertices, new Vector3()); if (!bary) return;
const weights = bary.toArray(), point = new Vector3();
for (let i = 0; i < 3; i++) point.addScaledVector(new Vector3().fromArray(rest, mesh.geometry.index!.getX(hit.faceIndex * 3 + i) * 3), weights[i]);
return { label: info.label, mark: { part: info.part, point: point.toArray() as Point3, normal: Triangle.getNormal(...vertices, new Vector3()).toArray() as Point3, size, stroke,
surface: { model: this.data.id, triangle: hit.faceIndex, barycentric: weights as Point3, uv: [0, 0], zone: info.id, label: info.label } } };
}
private triangle(mesh: SkinnedMesh, index: number): [Vector3, Vector3, Vector3] {
return [0, 1, 2].map(i => mesh.getVertexPosition(mesh.geometry.index!.getX(index * 3 + i), new Vector3())) as [Vector3, Vector3, Vector3];
}
dispose(): void {
for (const { mesh } of this.items) { mesh.removeFromParent(); mesh.geometry.dispose(); }
for (const material of this.materials.values()) material.dispose();
}
}
