synthda-demo / scripts /test.mjs
evelynsaltt's picture
Add interactive SynthDA motion showcase
f108d90 verified
Raw History Blame Contribute Delete
3.05 kB
import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';import {fileURLToPath} from 'node:url';import {validateMotion,validateMeshes,project,boundsFor,renderStage} from '../motion.js';
const root=fileURLToPath(new URL('../',import.meta.url)),manifest=JSON.parse(fs.readFileSync(root+'assets/manifest.json'));
for(const example of manifest.examples){const data=JSON.parse(fs.readFileSync(root+example.data));
test(example.id+': saved data and provenance',()=>{validateMotion(data,example.frames);assert.equal(example.provenance.revision.length,40);for(let k=1;k<=9;k++)for(let f=0;f<example.frames;f++)for(let j=0;j<22;j++)for(let c=0;c<3;c++){const a=data.sourceA[f][j][c],b=data.sourceB[f][j][c],expected=a+.5*(k/10*a-(1-k/10)*b);assert.ok(Math.abs(expected-data.variants[k][f][j][c])<.000025);}});
test(example.id+': rotation and fit bounds contain all frames',()=>{for(const yaw of [-180,-90,0,20,90,180]){const b=boundsFor(data,yaw);for(const seq of [data.sourceA,data.sourceB,...Object.values(data.variants)])for(const frame of seq)for(const p of frame){const[x,y]=project(p,yaw);assert.ok(x>=b.minX&&x<=b.maxX&&y>=b.minY&&y<=b.maxY);}assert.ok(b.maxX>b.minX&&b.maxY>b.minY);}});
test(example.id+': all render modes use finite geometry',()=>{let strokes=0;const context=new Proxy({},{get:(t,k)=>t[k]??((...args)=>{for(const arg of args)if(typeof arg==='number')assert.ok(Number.isFinite(arg),k);if(k==='stroke')strokes++;}),set:(t,k,v)=>(t[k]=v,true)});const canvas={clientWidth:340,clientHeight:280,width:0,height:0,getContext:()=>context};for(const frame of [0,Math.floor(example.frames/2),example.frames-1])for(const yaw of [-180,0,180]){const bounds=boundsFor(data,yaw);for(const overlay of [false,true])renderStage(canvas,[{key:'sourceA',sequence:data.sourceA,color:'#587997'},{key:'variant',sequence:data.variants[5],color:'#76b900'},{key:'sourceB',sequence:data.sourceB,color:'#a07957'}],data.parents,frame,yaw,1,bounds,{overlay});renderStage(canvas,[1,3,5,7,9].map(i=>({key:String(i),sequence:data.variants[i],color:'#76b900'})),data.parents,frame,yaw,1,bounds);}assert.ok(strokes>100);});
}
test('Invalid poses fail clearly',()=>{assert.throws(()=>validateMotion({parents:[]},10));const data=JSON.parse(fs.readFileSync(root+manifest.examples[0].data));data.sourceA[0][0][0]=NaN;assert.throws(()=>validateMotion(data,manifest.examples[0].frames),/Invalid/);});
test('Mesh frames and topology must match',()=>{const mesh={faces:[[0,1,2]],vertices:[[[0,0,0],[1,0,0],[0,1,0]],[[0,0,0],[1,0,0],[0,1,0]]]};assert.equal(validateMeshes({sourceA:mesh},2).sourceA,mesh);assert.throws(()=>validateMeshes({sourceA:mesh},3));assert.throws(()=>validateMeshes({sourceA:{...mesh,faces:[[0,1,9]]}},2));});
test('No unsupported class or recognition claims in initial release',()=>{assert.deepEqual([...new Set(manifest.examples.map(e=>e.action))],['Fall']);assert.ok(manifest.examples.every(e=>e.recognition===null&&e.meshes===null));assert.equal(manifest.archive.reduce((s,a)=>s+Object.keys(a.clips).length,0),33);});