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.

scripts/prepare-reproductive.py
"""Append sex-filtered reproductive anatomy without changing existing triangles.
Blender --background --factory-startup --python scripts/prepare-reproductive.py -- ISA_ZIP
Female shapes are original MIT schematics; male structures are BodyParts3D CC-BY-4.0.
"""
from pathlib import Path
import sys,json,struct,hashlib,zipfile,re
import numpy as np
import bpy,bmesh
sys.path.insert(0,str(Path(__file__).resolve().parent))
from anatomy_fitting import assert_stable_topology
ROOT=Path(__file__).resolve().parents[1];archive=zipfile.ZipFile(sys.argv[sys.argv.index('--')+1])
path=ROOT/'assets/anatomy/organs.bin';raw=path.read_bytes();start=12+struct.unpack('<I',raw[8:12])[0];meta=json.loads(raw[12:start]);blob=bytearray(raw[start:]);parts=meta['parts'];existing={p['id'] for p in parts};sources={}
catalog_path=ROOT/'src/atlas-catalog.ts';catalog_text=catalog_path.read_text();catalog=json.loads(catalog_text.split('export const organCatalog = ')[1].split(';')[0])
# Rebuilding owns only the appended structures; the original prefix is immutable.
appended=[p for p in parts if p.get('sex')]
if appended:
 blob=blob[:min(p['positions'] for p in appended)];removed={p['id'] for p in appended};parts[:]=[p for p in parts if p['id'] not in removed];catalog=[p for p in catalog if p['id'] not in removed];existing={p['id'] for p in parts}
def write(a,t):
 while len(blob)%4:blob.append(0)
 offset=len(blob);blob.extend(np.asarray(a,dtype=t).tobytes());return offset

def add(id,label,p,f,sex,color,source):
 if id in existing:return
 # Every original vertex/triangle before this append retains its address.
 p=np.asarray(p);f=np.asarray(f);v=len(p);ix=np.zeros((v,4),dtype='<u2');ix[:,0]=1;w=np.zeros((v,4),dtype='<f4');w[:,0]=1
 info={'id':id,'label':label,'part':'pelvis','color':color,'sex':sex,'vertices':v,'triangles':len(f),'positions':write(p,'<f4'),'indices':write(f,'<u2'),'skinIndices':write(ix,'<u2'),'skinWeights':write(w,'<f4')}
 parts.append(info);catalog.append({'id':id,'label':label,'triangles':len(f),'part':'pelvis','sources':[source]});existing.add(id)
 print(label,np.round(p.min(0),3),np.round(p.max(0),3),flush=True)
for number in range(3132,3144):
 file=f'FJ{number}.obj';text=archive.read('isa_BP3D_4.0_obj_99/'+file).decode();sources[file]=hashlib.sha256(text.encode()).hexdigest()
 header=lambda n:re.search(r'^# '+n+r' : ([^\r\n]*)',text,re.M).group(1).strip()
 p=[];f=[]
 for line in text.splitlines():
  if line.startswith('v '):p.append([float(v) for v in line.split()[1:4]])
  elif line.startswith('f '):
   face=[int(v.split('/')[0])-1 for v in line.split()[1:]];f.extend([[face[0],face[i],face[i+1]] for i in range(1,len(face)-1)])
 p=np.array(p)/1000;p=p[:,[0,2,1]];p[:,1]+=.08;p[:,2]=-p[:,2]-.075
 mesh=bpy.data.meshes.new(file);mesh.from_pydata(p.tolist(),[],f);mesh.update();bm=bmesh.new();bm.from_mesh(mesh);bmesh.ops.remove_doubles(bm,verts=list(bm.verts),dist=1e-6);bm.to_mesh(mesh);bm.free()
 obj=bpy.data.objects.new(file,mesh);bpy.context.scene.collection.objects.link(obj);bpy.context.view_layer.objects.active=obj;obj.select_set(True)
 if len(mesh.polygons)>900:
  mod=obj.modifiers.new('Web reduction','DECIMATE');mod.ratio=900/len(mesh.polygons);bpy.ops.object.modifier_apply(modifier=mod.name)
 mesh.calc_loop_triangles();p=np.array([v.co[:] for v in mesh.vertices]);f=np.array([t.vertices[:] for t in mesh.loop_triangles]);used,inverse=np.unique(f,return_inverse=True)
 add(header('Concept ID'),header('English name'),p[used],inverse.reshape(-1,3),'male','#bd8581',file)
 bpy.data.objects.remove(obj,do_unlink=True);bpy.data.meshes.remove(mesh)

def sphere(id,label,center,scale,tilt=0,taper=0):
 p=[];f=[];n=32;m=20
 # Ring topology is stable; poles are singular rings, like the existing skin schematic.
 for i in range(m+1):
  theta=np.pi*i/m;y=np.cos(theta);r=np.sin(theta)
  for j in range(n):
   phi=2*np.pi*j/n;x=r*np.cos(phi)*(1+taper*y);z=r*np.sin(phi)
   v=np.array([x,y,z])*scale;c=np.cos(tilt);s=np.sin(tilt);v=np.array([v[0],v[1]*c-v[2]*s,v[1]*s+v[2]*c])+center;p.append(v)
 for i in range(m):
  for j in range(n):
   a=i*n+j;b=i*n+(j+1)%n;c=(i+1)*n+j;d=(i+1)*n+(j+1)%n
   if i>0:f.append([a,b,c])
   if i<m-1:f.append([b,d,c])
 add(id,label,p,f,'female','#c28c91','original MIT schematic')

def tube(id,label,path,radii):
 path=np.array(path);p=[];f=[];n=16
 for i,center in enumerate(path):
  direction=path[min(i+1,len(path)-1)]-path[max(i-1,0)];direction/=np.linalg.norm(direction)
  x=np.cross(direction,[0,0,1]);x/=np.linalg.norm(x);y=np.cross(direction,x)
  for j in range(n):p.append(center+radii[i]*(np.cos(j*2*np.pi/n)*x+np.sin(j*2*np.pi/n)*y))
 for i in range(len(path)-1):
  for j in range(n):
   a=i*n+j;b=i*n+(j+1)%n;c=(i+1)*n+j;d=(i+1)*n+(j+1)%n;f.extend([[a,b,c],[b,d,c]])
 add(id,label,p,f,'female','#c28c91','original MIT schematic')
sphere('schematic-uterus','Uterus · schematic',[0,.937,-.005],[.025,.027,.015],.35,.25)
sphere('schematic-cervix','Cervix · schematic',[0,.903,-.016],[.012,.015,.011],.25)
tube('schematic-vagina','Vagina · schematic',[[0,.9,-.014],[0,.884,0],[0,.865,.025],[0,.843,.05],[0,.823,.073]],[.011,.012,.013,.011,.008])
for s,side in [(1,'Left'),(-1,'Right')]:
 sphere('schematic-ovary-'+side.lower(),side+' ovary · schematic',[s*.047,.931,-.012],[.015,.010,.007],.2)
 points=[[s*x,y,z] for x,y,z in [[.019,.95,.002],[.03,.956,0],[.045,.957,-.004],[.06,.95,-.008],[.062,.939,-.01],[.057,.932,-.012]]]
 tube('schematic-tube-'+side.lower(),side+' uterine tube · schematic',points,[.003,.003,.0035,.004,.005,.006])
 sphere('schematic-labium-'+side.lower(),side+' labium majus · schematic',[s*.014,.84,.096],[.013,.031,.012],.65)
sphere('schematic-clitoris','Clitoral glans · schematic',[0,.866,.115],[.004,.005,.004])
# Bundle metadata and original per-part offsets; earlier parts remain byte-for-byte.
assert_stable_topology(path,parts,blob)
header=json.dumps(meta,separators=(',',':')).encode();header+=b' '*((-len(header))%4);output=b'BJATLS01'+struct.pack('<I',len(header))+header+blob;path.write_bytes(output)
catalog_path.write_text(catalog_text.split('export const organCatalog = ')[0]+'export const organCatalog = '+json.dumps(catalog,separators=(',',':'))+';\n')
provpath=ROOT/'assets/anatomy/organs-provenance.json';prov=json.loads(provpath.read_text());prov.update(sha256=hashlib.sha256(output).hexdigest(),bytes=len(output),structures=len(parts),triangles=sum(p['triangles'] for p in parts));prov['sourceFiles'].update(sources)
prov['reproductiveAnatomy']={'male':'12 BodyParts3D structures, retained in the atlas coordinate frame.','female':'10 original schematic meshes, MIT. Approximate non-pregnant adult layout; no clinical validation.','references':['https://openstax.org/books/anatomy-and-physiology-2e/pages/27-2-anatomy-and-physiology-of-the-female-reproductive-system'],'topology':'Existing 70 structures unchanged. New stable IDs appended.'}
provpath.write_text(json.dumps(prov,indent=2)+'\n');print('organs',len(parts),'structures',len(output),'bytes')