diff --git a/examples/abyss-framework/playtests/replay.playtest.json b/examples/abyss-framework/playtests/replay.playtest.json index d45884306e..f1dc346e84 100644 --- a/examples/abyss-framework/playtests/replay.playtest.json +++ b/examples/abyss-framework/playtests/replay.playtest.json @@ -17,7 +17,7 @@ "world": { "runtime": { "agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36", - "core": "0.3.13", + "core": "0.3.14", "randomState": 90210, "rapier": null, "step": 0.016666666666666666 diff --git a/packages/animation-mcp/package.json b/packages/animation-mcp/package.json index 28898406e3..bf226652da 100644 --- a/packages/animation-mcp/package.json +++ b/packages/animation-mcp/package.json @@ -1,6 +1,6 @@ { "name": "threenative-animation-mcp", - "version": "0.1.10", + "version": "0.1.11", "license": "MIT", "description": "Author, repair and verify character animation through one MCP: inspect rigs, attach props, edit and verify clips, and open a calibrator view.", "keywords": [ diff --git a/packages/animation-mcp/src/index.ts b/packages/animation-mcp/src/index.ts index 05a6e4117b..2e623bb1d9 100644 --- a/packages/animation-mcp/src/index.ts +++ b/packages/animation-mcp/src/index.ts @@ -280,7 +280,7 @@ function openView(input: z.output) { } export function createAnimationServer(): McpServer { - const server = new McpServer({ name: "threenative-animation", version: "0.1.10" }); + const server = new McpServer({ name: "threenative-animation", version: "0.1.11" }); server.registerTool( "animation_inspect", diff --git a/packages/assets/package.json b/packages/assets/package.json index 4b41a8b025..901b4ad9e4 100644 --- a/packages/assets/package.json +++ b/packages/assets/package.json @@ -1,6 +1,6 @@ { "name": "@threenative/assets", - "version": "0.3.14", + "version": "0.3.15", "license": "MIT", "description": "Content-addressed asset compile step for ThreeNative games.", "keywords": ["threenative", "three", "threejs", "assets", "gltf", "asset-pipeline", "webgpu"], diff --git a/packages/blender-bridge/package.json b/packages/blender-bridge/package.json index 982b2b756e..7cd2edae62 100644 --- a/packages/blender-bridge/package.json +++ b/packages/blender-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@threenative/blender-bridge", - "version": "0.2.6", + "version": "0.2.7", "license": "MIT", "description": "Runs an installed Blender headlessly so a downloaded .fbx becomes a GLB a ThreeNative game loads. Ships no Blender.", "keywords": ["threenative", "blender", "fbx", "gltf"], diff --git a/packages/core/mcp/animation-server.mjs b/packages/core/mcp/animation-server.mjs index 78004eb8b4..0d364aba9d 100755 --- a/packages/core/mcp/animation-server.mjs +++ b/packages/core/mcp/animation-server.mjs @@ -252,7 +252,7 @@ void main() { }`,$e={meshphysical_vert:yk,meshphysical_frag:Sk},ae={common:{diffuse:{value:new ft(16777215)},opacity:{value:1},map:{value:null},mapTransform:{value:new Ce},alphaMap:{value:null},alphaMapTransform:{value:new Ce},alphaTest:{value:0}},specularmap:{specularMap:{value:null},specularMapTransform:{value:new Ce}},envmap:{envMap:{value:null},envMapRotation:{value:new Ce},reflectivity:{value:1},ior:{value:1.5},refractionRatio:{value:.98},dfgLUT:{value:null}},aomap:{aoMap:{value:null},aoMapIntensity:{value:1},aoMapTransform:{value:new Ce}},lightmap:{lightMap:{value:null},lightMapIntensity:{value:1},lightMapTransform:{value:new Ce}},bumpmap:{bumpMap:{value:null},bumpMapTransform:{value:new Ce},bumpScale:{value:1}},normalmap:{normalMap:{value:null},normalMapTransform:{value:new Ce},normalScale:{value:new mn(1,1)}},displacementmap:{displacementMap:{value:null},displacementMapTransform:{value:new Ce},displacementScale:{value:1},displacementBias:{value:0}},emissivemap:{emissiveMap:{value:null},emissiveMapTransform:{value:new Ce}},metalnessmap:{metalnessMap:{value:null},metalnessMapTransform:{value:new Ce}},roughnessmap:{roughnessMap:{value:null},roughnessMapTransform:{value:new Ce}},gradientmap:{gradientMap:{value:null}},fog:{fogDensity:{value:25e-5},fogNear:{value:1},fogFar:{value:2e3},fogColor:{value:new ft(16777215)}},lights:{ambientLightColor:{value:[]},lightProbe:{value:[]},sunLights:{value:[],properties:{direction:{},color:{}}},sunLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},sunShadowMatrix:{value:[]},sunShadowCascade:{value:[]},directionalLights:{value:[],properties:{direction:{},color:{}}},directionalLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},directionalShadowMatrix:{value:[]},spotLights:{value:[],properties:{color:{},position:{},direction:{},distance:{},coneCos:{},penumbraCos:{},decay:{}}},spotLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},spotLightMap:{value:[]},spotLightMatrix:{value:[]},pointLights:{value:[],properties:{color:{},position:{},decay:{},distance:{}}},pointLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{},shadowCameraNear:{},shadowCameraFar:{}}},pointShadowMatrix:{value:[]},hemisphereLights:{value:[],properties:{direction:{},skyColor:{},groundColor:{}}},rectAreaLights:{value:[],properties:{color:{},position:{},width:{},height:{}}},ltc_1:{value:null},ltc_2:{value:null},probesSH:{value:[]},probesMin:{value:[]},probesMax:{value:[]},probesResolution:{value:[]}},points:{diffuse:{value:new ft(16777215)},opacity:{value:1},size:{value:1},scale:{value:1},map:{value:null},alphaMap:{value:null},alphaMapTransform:{value:new Ce},alphaTest:{value:0},uvTransform:{value:new Ce}},sprite:{diffuse:{value:new ft(16777215)},opacity:{value:1},center:{value:new mn(.5,.5)},rotation:{value:0},map:{value:null},mapTransform:{value:new Ce},alphaMap:{value:null},alphaMapTransform:{value:new Ce},alphaTest:{value:0}}},VE={basic:{uniforms:Nt([ae.common,ae.specularmap,ae.envmap,ae.aomap,ae.lightmap,ae.fog])},lambert:{uniforms:Nt([ae.common,ae.specularmap,ae.envmap,ae.aomap,ae.lightmap,ae.emissivemap,ae.bumpmap,ae.normalmap,ae.displacementmap,ae.fog,ae.lights,{emissive:{value:new ft(0)},envMapIntensity:{value:1}}])},phong:{uniforms:Nt([ae.common,ae.specularmap,ae.envmap,ae.aomap,ae.lightmap,ae.emissivemap,ae.bumpmap,ae.normalmap,ae.displacementmap,ae.fog,ae.lights,{emissive:{value:new ft(0)},specular:{value:new ft(1118481)},shininess:{value:30},envMapIntensity:{value:1}}])},standard:{uniforms:Nt([ae.common,ae.envmap,ae.aomap,ae.lightmap,ae.emissivemap,ae.bumpmap,ae.normalmap,ae.displacementmap,ae.roughnessmap,ae.metalnessmap,ae.fog,ae.lights,{emissive:{value:new ft(0)},roughness:{value:1},metalness:{value:0},envMapIntensity:{value:1}}])},toon:{uniforms:Nt([ae.common,ae.aomap,ae.lightmap,ae.emissivemap,ae.bumpmap,ae.normalmap,ae.displacementmap,ae.gradientmap,ae.fog,ae.lights,{emissive:{value:new ft(0)}}])},matcap:{uniforms:Nt([ae.common,ae.bumpmap,ae.normalmap,ae.displacementmap,ae.fog,{matcap:{value:null}}])},points:{uniforms:Nt([ae.points,ae.fog])},dashed:{uniforms:Nt([ae.common,ae.fog,{scale:{value:1},dashSize:{value:1},totalSize:{value:2}}])},depth:{uniforms:Nt([ae.common,ae.displacementmap])},normal:{uniforms:Nt([ae.common,ae.bumpmap,ae.normalmap,ae.displacementmap,{opacity:{value:1}}])},sprite:{uniforms:Nt([ae.sprite,ae.fog])},background:{uniforms:{uvTransform:{value:new Ce}}},backgroundCube:{uniforms:{backgroundRotation:{value:new Ce}}},distance:{uniforms:Nt([ae.common,ae.displacementmap,{referencePosition:{value:new qe},nearDistance:{value:1},farDistance:{value:1e3}}])},shadow:{uniforms:Nt([ae.lights,ae.fog,{color:{value:new ft(0)},opacity:{value:1}}])}};VE.physical={uniforms:Nt([VE.standard.uniforms,{clearcoat:{value:0},clearcoatMap:{value:null},clearcoatMapTransform:{value:new Ce},clearcoatNormalMap:{value:null},clearcoatNormalMapTransform:{value:new Ce},clearcoatNormalScale:{value:new mn(1,1)},clearcoatRoughness:{value:0},clearcoatRoughnessMap:{value:null},clearcoatRoughnessMapTransform:{value:new Ce},diffuseRoughness:{value:0},diffuseRoughnessMap:{value:null},diffuseRoughnessMapTransform:{value:new Ce},dispersion:{value:0},retroreflectivity:{value:0},iridescence:{value:0},iridescenceMap:{value:null},iridescenceMapTransform:{value:new Ce},iridescenceIOR:{value:1.3},iridescenceThicknessMinimum:{value:100},iridescenceThicknessMaximum:{value:400},iridescenceThicknessMap:{value:null},iridescenceThicknessMapTransform:{value:new Ce},sheen:{value:0},sheenColor:{value:new ft(0)},sheenColorMap:{value:null},sheenColorMapTransform:{value:new Ce},sheenRoughness:{value:1},sheenRoughnessMap:{value:null},sheenRoughnessMapTransform:{value:new Ce},transmission:{value:0},transmissionMap:{value:null},transmissionMapTransform:{value:new Ce},transmissionSamplerSize:{value:new mn},transmissionSamplerMap:{value:null},thickness:{value:0},thicknessMap:{value:null},thicknessMapTransform:{value:new Ce},attenuationDistance:{value:0},attenuationColor:{value:new ft(0)},specularColor:{value:new ft(1,1,1)},specularColorMap:{value:null},specularColorMapTransform:{value:new Ce},specularIntensity:{value:1},specularIntensityMap:{value:null},specularIntensityMapTransform:{value:new Ce},anisotropyVector:{value:new mn},anisotropyMap:{value:null},anisotropyMapTransform:{value:new Ce}}]),vertexShader:$e.meshphysical_vert,fragmentShader:$e.meshphysical_frag};var Ik=new Ce;Ik.set(-1,0,0,0,1,0,0,0,1);var Ck=new Ce;Ck.set(-1,0,0,0,1,0,0,0,1);var Pk=t=>{let e=t.toLowerCase();if(e==="left")return t==="LEFT"?"RIGHT":"Right";if(e==="right")return t==="RIGHT"?"LEFT":"Left"};function jE(t){let e=t.replace(/left|right/giu,i=>Pk(i)??i);if(e!==t)return e;let r=t.replace(/(^|[._\s-])([lrLR])(?=$|[._\s-]|\d)/gu,(i,n,s)=>`${n}${s==="L"?"R":s==="R"?"L":s==="l"?"r":"l"}`);return r===t?void 0:r}function GE(t,e){let r=[],i=[];return t.forEach((n,s)=>{let o=e(n);o!==void 0&&(r.push(s),i.push(o));}),{source:r,times:i}}function HE(t,e){let r=new qe(t[0]??0,t[1]??0,t[2]??0).applyQuaternion(new Pt(e[0]??0,e[1]??0,e[2]??0,e[3]??1).invert());return [r.x,r.y,r.z]}var Nk=()=>new Jt().registerExtensions(Qt);function gn(t,e,r){let{times:i,values:n}=t,s=i.length,o=t.interpolation==="CUBICSPLINE",a=o?e*3:e,c=(_,y)=>n[_*a+(o?e:0)+y]??0,l=i[0]??0,u=i[s-1]??l,d=_=>Array.from({length:e},(y,R)=>c(_,R));if(s===0)return new Array(e).fill(0);if(r<=l)return d(0);if(r>=u)return d(s-1);let f=1;for(;f_+((v[y]??0)-_)*h)}function Ok(t,e,r){let i=e.get(t.getName()),n=[...t.getTranslation()],s=[...t.getRotation()],o=[...t.getScale()],a=i?.get("translation");a!==void 0&&n.splice(0,3,...gn(a,3,r));let c=i?.get("rotation");c!==void 0&&s.splice(0,4,...gn(c,4,r));let l=i?.get("scale");return l!==void 0&&o.splice(0,3,...gn(l,3,r)),{matrix:new nn().compose(new qe(n[0]??0,n[1]??0,n[2]??0),new Pt(s[0]??0,s[1]??0,s[2]??0,s[3]??1),new qe(o[0]??1,o[1]??1,o[2]??1)),translation:n}}function qE(t,e){let r=0;for(let i of e.get(t.getName())?.values()??[])r=Math.max(r,i.times.at(-1)??0);for(let i of t.listChildren())r=Math.max(r,qE(i,e));return r}async function jh(t,e,r=30){return v0(await Nk().read(t),e,r)}function v0(t,e,r=30){let i=t.getRoot().listAnimations().find(p=>p.getName()===e);if(i===void 0){let p=t.getRoot().listAnimations().map(g=>g.getName());throw new Error(`The GLB has no clip '${e}'. It carries: ${p.length===0?"none":p.join(", ")}.`)}let n=new Map;for(let p of i.listChannels()){let g=p.getTargetNode(),m=p.getSampler(),S=p.getTargetPath();if(g===null||m===null||S===null)continue;let h=m.getInput()?.getArray(),v=m.getOutput()?.getArray();if(h==null||v===null||v===void 0)continue;let _=n.get(g.getName())??new Map;_.set(S,{interpolation:m.getInterpolation(),times:h,values:v}),n.set(g.getName(),_);}let s=t.getRoot().listScenes().flatMap(p=>p.listChildren()),o=s.reduce((p,g)=>Math.max(p,qE(g,n)),0),a=t.getRoot().listSkins().flatMap(p=>p.listJoints()).map(p=>p.getName()),c=new Set(a.length>0?a:t.getRoot().listNodes().map(p=>p.getName()).filter(p=>p.length>0)),l=[...c],u={},d=Math.max(1,Math.round(o*r)),f=[];for(let p=0;p<=d;p+=1){let g=Math.min(o,p/r),m={},S={},h={},v=(_,y,R)=>{let T=y.clone().multiply(Ok(_,n,g).matrix),b=_.getName();if(c.has(b)){let E=new qe().setFromMatrixPosition(T),M=new Pt().setFromRotationMatrix(T),N=new Pt().setFromRotationMatrix(y);m[b]=[E.x,E.y,E.z],S[b]=[M.x,M.y,M.z,M.w],h[b]=[N.x,N.y,N.z,N.w],R!==void 0&&c.has(R)&&(u[b]=R);}for(let E of _.listChildren())v(E,T,b);};for(let _ of s)v(_,new nn);f.push({parents:h,positions:m,rotations:S,time:g});}return {bones:l,duration:o,fps:r,frames:f,parentsOf:u}}var zk=()=>new Jt().registerExtensions(Qt),Gh=t=>t==="rotation"?4:3;function xi(t){let e=t.getSampler(),r=e?.getInput()?.getArray(),i=e?.getOutput()?.getArray();if(!(e===null||r===null||r===void 0||i===null||i===void 0))return {interpolation:e.getInterpolation(),times:r,values:i}}function Fa(t,e,r,i,n){let s=e.getSampler();if(s===null)return;let o=t.getRoot().listBuffers()[0]??t.createBuffer(),a=t.createAccessor(`${e.getTargetNode()?.getName()}.time`).setType("SCALAR").setArray(new Float32Array(r)).setBuffer(o),c=t.createAccessor(`${e.getTargetNode()?.getName()}.${e.getTargetPath()}`).setType(n===4?"VEC4":"VEC3").setArray(new Float32Array(i)).setBuffer(o);s.setInput(a).setOutput(c).setInterpolation("LINEAR");}function WE(t,e,r,i){for(let n of e.listChannels()){let s=xi(n);if(s===void 0)continue;let o=n.getTargetPath()??"translation",a=Gh(o),c=s.interpolation==="CUBICSPLINE",l=c?a*3:a,{source:u,times:d}=GE([...s.times],r);if(d.length===0)throw new Error(`'${e.getName()}' keeps no keyframes in that window; the edit would leave an empty clip.`);let f=[];for(let p of u)for(let g=0;gv>g),S=m===-1?d.length:m;d.splice(S,0,g);let h=c?Array.from({length:3*a},(v,_)=>_>=a&&_=i)){f.push(m-r);for(let S=0;SMath.min(n,c/r));for(let a of i){let c=xi(a);if(c===void 0)continue;let l=Gh(a.getTargetPath()??"translation"),u=o.flatMap(d=>gn(c,l,d));Fa(t,a,o,u,l);}}function kk(t,e){let r=new Map(t.getRoot().listNodes().map(i=>[i.getName(),i]));for(let i of e.listChannels()){let n=i.getTargetNode(),s=xi(i);if(n===null||s===void 0)continue;let o=i.getTargetPath()??"translation",a=Gh(o),c=[];for(let d=0;d+a<=s.values.length;d+=a){let f=Array.from({length:a},(p,g)=>s.values[d+g]??0);o==="translation"&&(f[0]=-(f[0]??0)),o==="rotation"&&(f[1]=-(f[1]??0),f[2]=-(f[2]??0)),c.push(...f);}Fa(t,i,[...s.times],c,a);let l=jE(n.getName()),u=l===void 0?void 0:r.get(l);u!==void 0&&i.setTargetNode(u);}}function Uk(t,e,r){let i=v0(t,e.getName()),n=r.feet.length>0?r.feet:i.bones.filter(s=>/foot/iu.test(s));if(n.length===0)throw new Error("No foot bones to lock: name them with 'feet'.");for(let s of n){let o=i.frames.map(g=>g.positions[s]?.[1]??0),a=Math.min(...o),c=e.listChannels().find(g=>g.getTargetNode()?.getName()===s&&g.getTargetPath()==="translation");if(c===void 0)continue;let l=xi(c);if(l===void 0)continue;let u=[],d=[],f,p=false;i.frames.forEach((g,m)=>{let S=g.positions[s];if(S===void 0)return;(S[1]??0)<=a+r.contact?(f??=S,p=true):(p=false,f=void 0);let v=p&&f!==void 0?[(f[0]??0)-(S[0]??0),0,(f[2]??0)-(S[2]??0)]:[0,0,0],_=HE(v,g.parents[s]??[0,0,0,1]);u.push(g.time),d.push((gn(l,3,g.time)[0]??0)+_[0],(gn(l,3,g.time)[1]??0)+_[1],(gn(l,3,g.time)[2]??0)+_[2]);}),Fa(t,c,u,d,3);}}function Fk(t,e,r){switch(r.kind){case "trim":{if(r.end<=r.start)throw new Error("trim needs an end after its start.");let i=$k(e);if(r.start>=i||r.end>i+1e-6)throw new Error(`trim asks for ${r.start}\u2013${r.end}s but '${e.getName()}' is ${i}s long.`);Dk(t,e,r.start,r.end);return}case "retime":if(!(r.speed>0))throw new Error("retime needs a speed above zero.");WE(t,e,i=>i/r.speed);return;case "loop-point":WE(t,e,i=>i<=r.time?i:void 0,r.time);return;case "resample":if(!(r.fps>0))throw new Error("resample needs an fps above zero.");Lk(t,e,r.fps);return;case "mirror":kk(t,e);return;case "foot-lock":Uk(t,e,r);return}}function KE(t,e,r){let i=t.getRoot().listAnimations().filter(n=>n.getName()===e);if(i.length===0)throw new Error(`The GLB has no clip '${e}' to edit.`);for(let n of r)for(let s of i)Fk(t,s,n);}async function ZE(t){return zk().read(t)}function Bk(t){let e=new Map;for(let r of t)r.op==="modify"&&e.set(r.clip,[...e.get(r.clip)??[],r.edit]);return e}async function XE(t,e){let r=await ZE(t.host),i=t.ops.some(o=>o.op==="attach")?await Iy(r,t):void 0,n=Bk(t.ops);for(let[o,a]of n)KE(r,o,a);return await new Jt().registerExtensions(Qt).write(e,r),{clips:i?.clips??[],edits:[...n.values()].reduce((o,a)=>o+a.length,0),file:e,modified:[...n.keys()]}}var ks=Object.freeze({contact:.03,jitter:.05,penetration:.01,seam:5,skate:.02,stretch:.02}),y0=(t,e)=>Math.hypot((t[0]??0)-(e[0]??0),(t[1]??0)-(e[1]??0),(t[2]??0)-(e[2]??0)),Vk=(t,e)=>Math.hypot((t[0]??0)-(e[0]??0),(t[2]??0)-(e[2]??0));function jk(t,e){let r=n=>Math.hypot(n[0]??0,n[1]??0,n[2]??0,n[3]??1)||1,i=((t[0]??0)*(e[0]??0)+(t[1]??0)*(e[1]??0)+(t[2]??0)*(e[2]??0)+(t[3]??1)*(e[3]??1))/(r(t)*r(e));return 2*Math.acos(Math.min(1,Math.abs(i)))*180/Math.PI}function S0(t,e=2){let r=[],i,n;for(let s of t)(i===void 0||n===void 0||s-n>e)&&(i!==void 0&&n!==void 0&&r.push({from:i,to:n}),i=s),n=s;return i!==void 0&&n!==void 0&&r.push({from:i,to:n}),r}function Gk(t,e,r,i){let n=[];for(let s of e){let o=t.frames.map(p=>p.positions[s]?.[1]??0),a=Math.min(...o),c=t.frames.map((p,g)=>({index:g,frame:p})).filter(({frame:p})=>(p.positions[s]?.[1]??0)<=a+i),l,u,d=()=>{u!==void 0&&(u.worst>r&&n.push({bone:s,detail:`the planted foot slid ${(u.worst*1e3).toFixed(0)} mm between frames ${u.first} and ${u.last}`,fromFrame:u.first,fromTime:t.frames[u.first]?.time??0,kind:"foot-skate",limit:r,measured:u.worst,toFrame:u.last,toTime:t.frames[u.last]?.time??0}),u=void 0,l=void 0);},f;for(let{index:p,frame:g}of c){f!==void 0&&p-f>1&&d(),l??=g;let m=Vk(g.positions[s]??[0,0,0],l.positions[s]??[0,0,0]);u??={first:p,last:p,worst:0},u.last=p,u.worst=Math.max(u.worst,m),f=p;}d();}return n}function Hk(t,e,r){let i=[];for(let n of e){let s=t.frames.map((o,a)=>({index:a,y:o.positions[n]?.[1]})).filter(({y:o})=>o!==void 0&&o<-r);for(let o of S0(s.map(({index:a})=>a))){let a=s.filter(({index:c})=>c>=o.from&&c<=o.to).reduce((c,{y:l})=>Math.min(c,l??0),0);i.push({bone:n,detail:`reaches ${(Math.abs(a)*1e3).toFixed(0)} mm below the floor`,fromFrame:o.from,fromTime:t.frames[o.from]?.time??0,kind:"ground-penetration",limit:r,measured:Math.abs(a),toFrame:o.to,toTime:t.frames[o.to]?.time??0});}}return i}function qk(t,e){let r=t.frames[0],i=t.frames.at(-1);if(r===void 0||i===void 0||t.frames.length<3)return [];let n,s=0;for(let o of t.bones){let a=r.rotations[o],c=i.rotations[o];if(a===void 0||c===void 0)continue;let l=jk(a,c);l<=e||(s+=1,(n===void 0||l>n.angle)&&(n={angle:l,bone:o}));}return n===void 0?[]:[{bone:n.bone,detail:`the clip ends ${n.angle.toFixed(1)}\xB0 from where it started${s>1?`, and ${s-1} more bones with it`:""}`,fromFrame:0,fromTime:r.time,kind:"loop-seam",limit:e,measured:n.angle,toFrame:t.frames.length-1,toTime:i.time}]}function Wk(t,e,r){let i=[];for(let n of e){let s=t.frames.map(l=>l.positions[n]),o=[],a=new Map,c=0;for(let l=1;lr&&(o.push(l),a.set(l,Math.max(a.get(l)??0,p)),c=Math.max(c,p));}for(let l of S0(o.map(u=>u)))i.push({bone:n,detail:`a frame-direction spike of ${((a.get(l.from)??c)*1e3).toFixed(0)} mm`,fromFrame:l.from,fromTime:t.frames[l.from]?.time??0,kind:"jitter",limit:r,measured:c,toFrame:l.to,toTime:t.frames[l.to]?.time??0});}return i}function Kk(t,e){let r=t.frames[0];if(r===void 0)return [];let i=[];for(let[n,s]of Object.entries(t.parentsOf)){let o=y0(r.positions[n]??[0,0,0],r.positions[s]??[0,0,0]);if(o<=1e-6)continue;let a=t.frames.map((l,u)=>({index:u,ratio:Math.abs(y0(l.positions[n]??[0,0,0],l.positions[s]??[0,0,0])/o-1)})).filter(({ratio:l})=>l>e);if(a.length===0)continue;let c=a.reduce((l,{ratio:u})=>Math.max(l,u),0);for(let l of S0(a.map(({index:u})=>u)))i.push({bone:n,detail:`distance to its parent '${s}' changes by ${(c*100).toFixed(1)}%`,fromFrame:l.from,fromTime:t.frames[l.from]?.time??0,kind:"bone-stretch",limit:e,measured:c,toFrame:l.to,toTime:t.frames[l.to]?.time??0});}return i}var Zk=new Set(["left-foot","right-foot"]);async function JE(t,e,r={}){let i=r.scale??1,n={contact:(r.thresholds?.contact??ks.contact)/i,jitter:(r.thresholds?.jitter??ks.jitter)/i,penetration:(r.thresholds?.penetration??ks.penetration)/i,seam:r.thresholds?.seam??ks.seam,skate:(r.thresholds?.skate??ks.skate)/i,stretch:r.thresholds?.stretch??ks.stretch},s=await jh(t,e,r.fps??30),o=Sh(s.bones).filter(c=>Zk.has(c.id)).map(c=>c.node),a=[...Gk(s,o,n.skate,n.contact),...Hk(s,s.bones,n.penetration),...qk(s,n.seam),...Wk(s,s.bones,n.jitter),...Kk(s,n.stretch)];return {bones:s.bones.length,clip:e,defects:a,duration:s.duration,file:t,fps:s.fps,frames:s.frames.length,passed:a.length===0,scale:i,thresholds:n}}var Jk=(()=>{let t=new Uint32Array(256);for(let e=0;e<256;e+=1){let r=e;for(let i=0;i<8;i+=1)r=r&1?3988292384^r>>>1:r>>>1;t[e]=r>>>0;}return t})();function Yk(t){let e=4294967295;for(let r of t)e=(Jk[(e^r)&255]??0)^e>>>8;return (e^4294967295)>>>0}function x0(t,e){let r=Buffer.alloc(4);r.writeUInt32BE(e.length,0);let i=Buffer.concat([Buffer.from(t,"ascii"),Buffer.from(e)]),n=Buffer.alloc(4);return n.writeUInt32BE(Yk(i),0),Buffer.concat([r,i,n])}function YE(t,e,r){let i=Buffer.alloc(13);i.writeUInt32BE(t,0),i.writeUInt32BE(e,4),i[8]=8,i[9]=6,i[10]=0,i[11]=0,i[12]=0;let n=t*4,s=Buffer.alloc((n+1)*e);for(let o=0;o=t.width||d>=t.height)continue;let f=t.width*d+u<<2;t.data[f]=i[0]??0,t.data[f+1]=i[1]??0,t.data[f+2]=i[2]??0,t.data[f+3]=255;}}async function tw(t){let e=await jh(t.file,t.clip,t.fps??30),r=Math.max(1,t.columns??6),i=Math.max(48,t.height??160),n=Math.min(r,e.frames.length),s=Math.max(1,Math.floor((e.frames.length-1)/Math.max(1,n-1))),o=Array.from({length:n},(_,y)=>Math.min(e.frames.length-1,y*s)),a={data:new Uint8Array(i*n*i*4),height:i,width:i*n};for(let _=0;_e.bones.map(y=>e.frames[_]?.positions[y]).filter(y=>y!==void 0)),l=c.map(_=>_[0]),u=c.map(_=>_[1]),d=Math.min(...l,0),f=Math.max(...l,1),p=Math.min(...u,0),g=Math.max(...u,1),m=Math.max(1e-6,Math.max(f-d,g-p)),S=i*.12,h=(_,y,R)=>[Math.round(R*i+S+(_-d)/m*(i-S*2)),Math.round(i-S-(y-p)/m*(i-S*2))];o.forEach((_,y)=>{let R=e.frames[_];if(R!==void 0){ew(a,h(d,p,y),h(f,p,y),nU);for(let T of e.bones){let b=R.positions[T];b!==void 0&&ew(a,h(b[0],0,y),h(b[0],b[1],y),tU);}}});let v=YE(a.width,a.height,a.data);return await mkdir(Gr.dirname(Gr.resolve(t.output)),{recursive:true}),await writeFile(t.output,v),{bytes:v.length,columns:n,frames:e.frames.length}}var rU=String.raw` :root{color-scheme:dark;font-family:"IBM Plex Mono","Azeret Mono",ui-monospace,monospace;background:#080b0a;color:#dce4de;--line:#27302c;--line2:#38433e;--panel:#111613;--panel2:#171d1a;--acid:#b7f34a;--cyan:#68d9d0;--muted:#77837c;--red:#ff705d}*{box-sizing:border-box}body{margin:0;overflow:hidden;background:#080b0a}button{cursor:pointer}.lab{height:100vh;display:grid;grid-template-rows:50px 1fr 26px}.bar{display:flex;align-items:center;gap:14px;padding:0 14px;border-bottom:1px solid var(--line);background:#0d120f}.brand{font:800 13px sans-serif;letter-spacing:.18em}.brand b{display:inline-grid;place-items:center;width:24px;height:24px;margin-right:9px;background:var(--acid);color:#101808}.bar small,.status{color:var(--muted)}.grow{flex:1}.bar button{border:1px solid #789e3f;border-radius:2px;background:var(--acid);color:#111908;padding:8px 14px;font:800 10px inherit}.body{min-height:0;display:grid;grid-template-columns:230px minmax(0,1fr) 285px}.side{overflow:auto;background:var(--panel)}.left{border-right:1px solid var(--line)}.right{border-left:1px solid var(--line)}.section{padding:13px;border-bottom:1px solid var(--line)}h2{margin:0 0 10px;color:var(--muted);font-size:9px;letter-spacing:.16em;text-transform:uppercase}label{display:grid;gap:5px;margin-bottom:10px;color:#91a097;font-size:9px;text-transform:uppercase}select,input{width:100%;border:1px solid var(--line);border-radius:2px;background:#090d0b;color:#dce4de;padding:8px;font:10px inherit;outline:none}select:focus,input:focus{border-color:var(--acid)}input:disabled{opacity:.38}.workspace{min-width:0;min-height:0;display:grid;grid-template-rows:minmax(280px,1fr) 224px;background:#090d0b}.viewport{position:relative;min-width:0;min-height:0}.viewport canvas{display:block;width:100%;height:100%}.title{position:absolute;top:18px;left:18px;pointer-events:none;font:800 23px sans-serif}.title small{display:block;margin-top:6px;color:var(--acid);font:9px ui-monospace;letter-spacing:.14em}.tools{position:absolute;bottom:16px;left:50%;display:flex;transform:translateX(-50%);border:1px solid var(--line2);background:#090d0be8}.tools button,.preset button{border:0;background:none;color:var(--muted);padding:9px 15px;font:9px inherit;text-transform:uppercase}.tools button.on{background:#263328;color:var(--acid)}.tools button:disabled{opacity:.28;cursor:not-allowed}.release-note{position:absolute;top:18px;right:18px;padding:7px 10px;border:1px solid #8b493d;background:#2b1713d9;color:#ff9b86;font-size:9px;letter-spacing:.13em}.landmarks{display:grid;gap:5px}.landmark{display:grid;grid-template-columns:8px 1fr auto;gap:8px;align-items:center;width:100%;padding:8px;border:1px solid transparent;border-radius:2px;background:#0c110f;color:var(--muted);font:10px inherit;text-align:left}.landmark.on{border-color:#668c34;color:#dce4de}.landmark i{width:6px;height:6px;border-radius:50%;background:#71b9ff}.landmark.detected i{background:var(--acid)}.landmark small{font-size:8px;color:#69756f}.axis{display:grid;grid-template-columns:repeat(3,1fr);gap:6px}.axis label{margin:0}.preset{display:grid;grid-template-columns:repeat(4,1fr);margin-top:8px}.preset button{padding:7px 2px;border:1px solid var(--line)}.wide,.editor-bar button,.segmented button{border:1px solid var(--line2);border-radius:2px;background:#1a211d;color:#dce4de;padding:8px;font:10px inherit}.wide{width:100%}.secondary{color:var(--muted)!important;background:#0d1210!important}.segmented{display:grid;grid-template-columns:1fr 1fr;gap:3px}.segmented button.on{border-color:var(--acid);color:var(--acid);background:#243021}.segmented button.danger.on{border-color:var(--red);color:#ff9b86;background:#321a16}.hint{margin:8px 0 0;color:var(--muted);font-size:9px;line-height:1.5}.status{display:flex;justify-content:space-between;align-items:center;padding:0 12px;border-top:1px solid var(--line);font-size:9px}.status b{color:var(--acid)}.error{position:absolute;top:18px;right:18px;max-width:360px;padding:10px;border:1px solid #81452f;background:#321a12;color:#ffb08a;font-size:10px}.saved{color:var(--acid);font-size:9px}.dope-editor{min-width:0;border-top:1px solid #46504b;background:#101512;box-shadow:0 -12px 30px #0008}.editor-bar{height:44px;display:flex;align-items:center;gap:7px;padding:6px 9px;border-bottom:1px solid var(--line2);background:#171c19}.editor-bar select{width:180px;padding:7px}.editor-bar button{white-space:nowrap;padding:7px 10px}.editor-bar button:disabled{opacity:.3;cursor:not-allowed}.editor-bar .transport{width:34px;border-color:#738d47;color:var(--acid);font-size:13px}.timecode{min-width:90px;color:#e1e7e3;font-size:11px;letter-spacing:.08em}.timecode i{color:var(--muted);font-style:normal}.compact{display:flex}.sheet{position:relative;height:179px;display:grid;grid-template-columns:132px 1fr;grid-template-rows:27px repeat(4,38px);overflow:hidden;background:repeating-linear-gradient(90deg,transparent 0,transparent calc(10% - 1px),#252d29 calc(10% - 1px),#252d29 10%)}.ruler-label{grid-column:1;grid-row:1}.ruler{position:relative;grid-column:2;grid-row:1;border-bottom:1px solid var(--line2);background:#0c100e}.ruler span{position:absolute;bottom:5px;transform:translateX(-50%);color:#68736d;font-size:8px}.lane-row{display:contents}.lane-label{z-index:3;display:flex;align-items:center;gap:8px;padding-left:13px;border-right:1px solid var(--line2);border-bottom:1px solid #222a26;background:#151a17;color:#87938c;font-size:9px;letter-spacing:.1em}.lane-label i{width:6px;height:6px;border:1px solid var(--cyan);transform:rotate(45deg)}.lane-track{position:relative;border-bottom:1px solid #222a26;background:#0d120f66}.key-diamond{position:absolute;z-index:4;top:14px;width:10px;height:10px;padding:0;transform:translate(-50%,-50%) rotate(45deg);border:1px solid #e2ffae;background:var(--acid);box-shadow:0 0 0 2px #0a0e0c}.attachment-lane .lane-label{grid-column:1;grid-row:5}.event-clip{align-self:center;grid-column:2;grid-row:5;height:23px;margin:0 8px;padding:6px 10px;border-left:3px solid var(--cyan);background:#17312d;color:#8ce7df;font-size:9px;letter-spacing:.08em}.event-clip.released{border-left-color:var(--red);background:#321a16;color:#ff9b86}.playhead{position:absolute;z-index:6;top:0;bottom:0;width:1px;background:#f3f7f4;pointer-events:none}.playhead b{position:absolute;top:0;left:-4px;width:9px;height:9px;background:#f3f7f4;clip-path:polygon(0 0,100% 0,50% 100%)}.scrubber{position:absolute;z-index:7;top:0;right:0;bottom:0;left:132px;width:calc(100% - 132px);height:100%;margin:0;padding:0;opacity:0;cursor:ew-resize}.ruler{grid-column:2;grid-row:1}.lane-row:nth-of-type(1) .lane-label,.lane-row:nth-of-type(1) .lane-track{grid-row:2}.lane-row:nth-of-type(2) .lane-label,.lane-row:nth-of-type(2) .lane-track{grid-row:3}.lane-row:nth-of-type(3) .lane-label,.lane-row:nth-of-type(3) .lane-track{grid-row:4}.lane-row .lane-label{grid-column:1}.lane-row .lane-track{grid-column:2}@media(max-width:980px){.body{grid-template-columns:210px 1fr}.right{display:none}.workspace{grid-template-rows:minmax(260px,1fr) 224px}.compact{display:none}} `;function nw(t){return t.replaceAll("&","&").replaceAll("<","<").replaceAll('"',""")}function rw(t){return `Pose Lab \u2014 ${nw(t)}
`}var aU=new URL("./client/app.js",import.meta.url),cU=256e3;function iw(t,e){if(e.length===0||Gr.isAbsolute(e)||e.split(/[\\/]/u).some(n=>n.length===0||n.startsWith(".")))return;let r=Gr.resolve(t),i=Gr.resolve(r,e);return i.startsWith(`${r}${Gr.sep}`)?i:void 0}async function lU(t){let e=[],r=new Set([".git","build","dist","node_modules"]),i=async(n,s,o)=>{if(o>6||e.length>=2e3)return;let a=await readdir(n,{withFileTypes:true}).catch(()=>[]);for(let c of a){if(c.name.startsWith(".")||r.has(c.name))continue;let l=s.length===0?c.name:`${s}/${c.name}`;c.isDirectory()?await i(Gr.join(n,c.name),l,o+1):c.name.toLowerCase().endsWith(".glb")&&e.push(l);}};return await i(t,"",0),e.sort((n,s)=>n.localeCompare(s))}async function uU(t){let e=[],r=0;for await(let i of t){let n=Buffer.from(i);if(r+=n.length,r>cU)throw new Error("Request exceeds 256 KB.");e.push(n);}return JSON.parse(Buffer.concat(e).toString("utf8"))}function Hh(t,e,r,i){t.writeHead(e,{"cache-control":"no-store","content-type":i,"x-content-type-options":"nosniff"}),t.end(r);}function Rn(t,e,r){Hh(t,e,JSON.stringify(r),"application/json; charset=utf-8");}async function ow(t={}){let e=Gr.resolve(t.project??process.cwd()),r=t.port??4192;await readdir(e).catch(()=>{throw new Error(`Project directory does not exist: ${e}`)});let i=createServer(async(s,o)=>{try{let a=new URL(s.url??"/","http://pose-lab");if(s.method==="GET"&&(a.pathname==="/"||a.pathname==="/pose"))return Hh(o,200,rw(Gr.basename(e)),"text/html; charset=utf-8");if(s.method==="GET"&&a.pathname==="/app.js")return Hh(o,200,await readFile(aU),"text/javascript; charset=utf-8");if(s.method==="GET"&&a.pathname==="/favicon.ico")return o.writeHead(204),o.end();if(s.method==="GET"&&a.pathname==="/api/models")return Rn(o,200,{models:await lU(e)});if(s.method==="GET"&&a.pathname==="/api/model"){let c=a.searchParams.get("path")??"",l=c.endsWith(".glb")?iw(e,c):void 0;if(l===void 0)return Rn(o,404,{error:"GLB not found."});let u=await readFile(l).catch(()=>{});return u===void 0?Rn(o,404,{error:"GLB not found."}):Hh(o,200,u,"model/gltf-binary")}if(a.pathname==="/api/recipe"){let c=a.searchParams.get("path")??"",l=c.endsWith(".json")?iw(e,c):void 0;if(l===void 0)return Rn(o,404,{error:"Recipe not found."});if(s.method==="GET"){let u=await readFile(l,"utf8").catch(()=>{});return u===void 0?Rn(o,404,{error:"Recipe not found."}):Rn(o,200,{recipe:JSON.parse(u),path:c})}if(s.method==="PUT"){let u=await uU(s);return typeof u.recipe!="object"||u.recipe===null?Rn(o,400,{error:"Body must contain a recipe object."}):(await mkdir(Gr.dirname(l),{recursive:!0}),await writeFile(l,`${JSON.stringify(u.recipe,null,2)} -`,"utf8"),Rn(o,200,{path:c,saved:!0}))}}return Rn(o,404,{error:"Not found."})}catch(a){return Rn(o,500,{error:a instanceof Error?a.message:String(a)})}});await new Promise((s,o)=>{i.once("error",o),i.listen(r,"127.0.0.1",s);});let n=i.address();return {port:typeof n=="object"&&n!==null?n.port:r,close:()=>new Promise((s,o)=>i.close(a=>a?o(a):s()))}}var fU=B.object({position:B.tuple([B.number(),B.number(),B.number()]),rotation:B.tuple([B.number(),B.number(),B.number()]),scale:B.tuple([B.number(),B.number(),B.number()])}),_w=B.enum(yh.map(t=>t.id)),pU=B.object({attachment:B.enum(["attached","detached"]).optional(),clip:B.string().min(1).max(200),time:B.number().min(0).max(600),transform:fU}),mU=B.object({file:B.string().min(1)}),cw=B.object({bones:B.array(B.string()),bytes:B.number(),clips:B.array(B.object({channels:B.number(),duration:B.number(),horizontalTravel:B.number(),inPlace:B.boolean(),name:B.string()})),extensionsUsed:B.array(B.string()),file:B.string(),generator:B.string(),inPlace:B.boolean(),landmarks:B.array(B.object({id:B.string(),label:B.string(),node:B.string(),source:B.enum(["detected","estimated"])})),meshes:B.number(),rootBone:B.string().nullable(),skins:B.number(),warnings:B.array(B.string())}),lw=B.object({anchor:_w.default("right-hand"),attached:B.string().min(1).optional(),bone:B.string().min(1).optional(),host:B.string().min(1).optional(),keys:B.array(pU).max(2e3).optional(),log:B.string().min(1).optional(),output:B.string().min(1).optional(),recipe:B.string().min(1).optional()}),uw=B.object({anchor:B.string(),attached:B.string(),clips:B.array(B.string()),detached:B.array(B.string()),file:B.string(),log:B.string(),node:B.string()}),dw=B.object({clip:B.string().min(1).max(200),file:B.string().min(1),filmstrip:B.string().min(1).optional(),fps:B.number().int().min(1).max(240).default(30),scale:B.number().positive().default(1)}),hw=B.object({bones:B.number(),clip:B.string(),filmstrip:B.string().nullable(),defects:B.array(B.object({bone:B.string(),detail:B.string(),fromFrame:B.number(),fromTime:B.number(),kind:B.enum(["bone-stretch","foot-skate","ground-penetration","jitter","loop-seam"]),limit:B.number(),measured:B.number(),toFrame:B.number(),toTime:B.number()})),duration:B.number(),file:B.string(),fps:B.number(),frames:B.number(),passed:B.boolean(),scale:B.number(),thresholds:B.object({contact:B.number(),jitter:B.number(),penetration:B.number(),seam:B.number(),skate:B.number(),stretch:B.number()})}),gU=B.discriminatedUnion("kind",[B.object({end:B.number().min(0),kind:B.literal("trim"),start:B.number().min(0)}),B.object({kind:B.literal("retime"),speed:B.number().positive()}),B.object({kind:B.literal("loop-point"),time:B.number().min(0)}),B.object({fps:B.number().positive(),kind:B.literal("resample")}),B.object({kind:B.literal("mirror")}),B.object({contact:B.number().min(0).default(.03),feet:B.array(B.string().min(1)).max(8).default([]),kind:B.literal("foot-lock")})]),fw=B.object({clip:B.string().min(1).max(200),edits:B.array(gU).min(1).max(64),file:B.string().min(1),log:B.string().min(1).optional(),output:B.string().min(1).optional()}),pw=B.object({clips:B.array(B.string()),edits:B.number(),file:B.string(),log:B.string(),modified:B.array(B.string())}),R0=B.object({anchor:_w.default("right-hand"),attach:B.string().min(1).optional(),host:B.string().min(1).optional(),output:B.string().min(1).optional(),port:B.number().int().min(0).max(65535).optional(),project:B.string().min(1).optional()}),mw=B.object({port:B.number(),url:B.string()}),Ba=t=>({destructiveHint:false,idempotentHint:true,openWorldHint:false,readOnlyHint:t}),En=t=>[{type:"text",text:JSON.stringify(t)}];function _U(t,e,r){if(r!==void 0){let s=yh.find(o=>o.id===e);if(s===void 0)throw new Error(`Unknown anchor '${e}'.`);return {id:e,label:s.label,node:r,source:"detected"}}let i=t.find(s=>s.id===e);if(i!==void 0)return i;let n=t.map(s=>`${s.id} \u2192 ${s.node}`);throw new Error(`The host names no '${e}' landmark. It answers to: ${n.length===0?"none":n.join(", ")}. Pass 'bone' to name one directly.`)}function vU(t){return {position:t.position,rotation:t.rotation,scale:t.scale}}function vw(t){let e=Gr.parse(t);return Gr.join(e.dir,`${e.name}.anim.glb`)}async function yU(t){return t.filmstrip===void 0?null:(await tw({clip:t.clip,file:t.file,fps:t.fps,output:t.filmstrip}),Gr.resolve(t.filmstrip))}async function SU(t){let e=(t.keys??[]).map(s=>({attachment:s.attachment,clip:s.clip,op:"key",time:s.time,transform:vU(s.transform)})),r;if(t.recipe!==void 0){r=Ry(JSON.parse(await readFile(t.recipe,"utf8")));for(let s of e)r=Ey(r,s);}else {if(t.host===void 0||t.attached===void 0)throw new Error("Pass 'host' and 'attached', or a 'recipe' that names both.");let s=await Py(t.host);r={host:t.host,ops:[{anchor:_U(s.landmarks,t.anchor,t.bone),attached:t.attached,op:"attach"},...e],version:4};}let i=t.log??wy(r.host);return await My(i,r),{...await hE(r,t.output??vw(r.host)),log:Gr.resolve(i)}}var gw;function yw(t){return gw??=(async()=>{let e=await ow({...t.port===void 0?{}:{port:t.port},...t.project===void 0?{}:{project:t.project}}),r=new URLSearchParams;return t.host!==void 0&&r.set("host",t.host),t.attach!==void 0&&r.set("attach",t.attach),t.output!==void 0&&r.set("output",t.output),r.set("anchor",t.anchor),{port:e.port,url:`http://127.0.0.1:${e.port}/pose?${r.toString()}`}})(),gw}function xU(){let t=new Aa({name:"threenative-animation",version:"0.1.10"});return t.registerTool("animation_inspect",{annotations:Ba(true),description:"Inspect a local rigged GLB: bones, skeleton landmarks, clips with durations, root-motion travel and in-place detection, meshes, skins and extensions. Reads one local file and writes nothing.",inputSchema:mU,outputSchema:cw,title:"Inspect a rig and its clips"},async e=>{try{let r=cw.parse(await Py(e.file));return {content:En(r),structuredContent:r}}catch(r){return {content:En({message:r instanceof Error?r.message:String(r)}),isError:true}}}),t.registerTool("animation_attach",{annotations:Ba(false),description:"Attach a prop GLB to a bone, with per-clip keyed offsets, and bake it into an ordinary GLB beside the source. Writes the v4 op log (.anim.json) next to the source GLB and the baked GLB; a version 3 pose recipe loads as the same ops and still bakes.",inputSchema:lw,outputSchema:uw,title:"Attach a prop to a bone and bake it"},async e=>{try{let r=uw.parse(await SU(lw.parse(e)));return {content:En(r),structuredContent:r}}catch(r){return {content:En({message:r instanceof Error?r.message:String(r)}),isError:true}}}),t.registerTool("animation_verify",{annotations:Ba(true),description:"Measure one clip for the defects a still frame cannot show: a planted foot that slides (2 cm), ground penetration (1 cm), a loop seam (5\xB0), a one-frame jitter spike, and bones whose length changes. Every defect names the bone and the frame range it spans. Thresholds are in metres; pass `scale` for a model that is not (Mixamo and Quaternius are centimetres: 0.01).",inputSchema:dw,outputSchema:hw,title:"Verify a clip's motion"},async e=>{try{let r=dw.parse(e),i=hw.parse({...await JE(r.file,r.clip,{fps:r.fps,scale:r.scale}),filmstrip:await yU(r)});return {content:En(i),structuredContent:i}}catch(r){return {content:En({message:r instanceof Error?r.message:String(r)}),isError:true}}}),t.registerTool("animation_modify",{annotations:Ba(false),description:"Edit one clip \u2014 trim, retime, move the loop point, resample, mirror, lock planted feet \u2014 writing each edit into the op log and rendering the result from the source GLB. Rendering twice from one log gives the same bytes, so the log is the change and the GLB is derived.",inputSchema:fw,outputSchema:pw,title:"Edit a clip and render it from the log"},async e=>{try{let r=fw.parse(e),i=r.log??wy(r.file),s=await lE(i).catch(async()=>({host:r.file,ops:[],version:4}));for(let c of r.edits)s=Ey(s,{clip:r.clip,edit:c,op:"modify"});await My(i,s);let o=await XE(s,r.output??vw(r.file)),a=pw.parse({...o,log:Gr.resolve(i)});return {content:En(a),structuredContent:a}}catch(r){return {content:En({message:r instanceof Error?r.message:String(r)}),isError:true}}}),t.registerTool("animation_open_view",{annotations:Ba(false),description:"Start the animation calibrator page on 127.0.0.1 and return its URL. A human drags the prop into place there; the same operations an agent calls are what the page records.",inputSchema:R0,outputSchema:mw,title:"Open the animation calibrator"},async e=>{try{let r=mw.parse(await yw(R0.parse(e)));return {content:En(r),structuredContent:r}}catch(r){return {content:En({message:r instanceof Error?r.message:String(r)}),isError:true}}}),t}function bU(){nR(()=>xU());}function TU(){let t=process.argv[1];if(t===void 0)return false;try{return realpathSync(t)===realpathSync(fileURLToPath(import.meta.url))}catch{return false}}TU()&&(process.argv[2]==="--view"?yw(R0.parse({project:process.cwd()})).then(({url:t})=>process.stdout.write(`Animation view: ${t} +`,"utf8"),Rn(o,200,{path:c,saved:!0}))}}return Rn(o,404,{error:"Not found."})}catch(a){return Rn(o,500,{error:a instanceof Error?a.message:String(a)})}});await new Promise((s,o)=>{i.once("error",o),i.listen(r,"127.0.0.1",s);});let n=i.address();return {port:typeof n=="object"&&n!==null?n.port:r,close:()=>new Promise((s,o)=>i.close(a=>a?o(a):s()))}}var fU=B.object({position:B.tuple([B.number(),B.number(),B.number()]),rotation:B.tuple([B.number(),B.number(),B.number()]),scale:B.tuple([B.number(),B.number(),B.number()])}),_w=B.enum(yh.map(t=>t.id)),pU=B.object({attachment:B.enum(["attached","detached"]).optional(),clip:B.string().min(1).max(200),time:B.number().min(0).max(600),transform:fU}),mU=B.object({file:B.string().min(1)}),cw=B.object({bones:B.array(B.string()),bytes:B.number(),clips:B.array(B.object({channels:B.number(),duration:B.number(),horizontalTravel:B.number(),inPlace:B.boolean(),name:B.string()})),extensionsUsed:B.array(B.string()),file:B.string(),generator:B.string(),inPlace:B.boolean(),landmarks:B.array(B.object({id:B.string(),label:B.string(),node:B.string(),source:B.enum(["detected","estimated"])})),meshes:B.number(),rootBone:B.string().nullable(),skins:B.number(),warnings:B.array(B.string())}),lw=B.object({anchor:_w.default("right-hand"),attached:B.string().min(1).optional(),bone:B.string().min(1).optional(),host:B.string().min(1).optional(),keys:B.array(pU).max(2e3).optional(),log:B.string().min(1).optional(),output:B.string().min(1).optional(),recipe:B.string().min(1).optional()}),uw=B.object({anchor:B.string(),attached:B.string(),clips:B.array(B.string()),detached:B.array(B.string()),file:B.string(),log:B.string(),node:B.string()}),dw=B.object({clip:B.string().min(1).max(200),file:B.string().min(1),filmstrip:B.string().min(1).optional(),fps:B.number().int().min(1).max(240).default(30),scale:B.number().positive().default(1)}),hw=B.object({bones:B.number(),clip:B.string(),filmstrip:B.string().nullable(),defects:B.array(B.object({bone:B.string(),detail:B.string(),fromFrame:B.number(),fromTime:B.number(),kind:B.enum(["bone-stretch","foot-skate","ground-penetration","jitter","loop-seam"]),limit:B.number(),measured:B.number(),toFrame:B.number(),toTime:B.number()})),duration:B.number(),file:B.string(),fps:B.number(),frames:B.number(),passed:B.boolean(),scale:B.number(),thresholds:B.object({contact:B.number(),jitter:B.number(),penetration:B.number(),seam:B.number(),skate:B.number(),stretch:B.number()})}),gU=B.discriminatedUnion("kind",[B.object({end:B.number().min(0),kind:B.literal("trim"),start:B.number().min(0)}),B.object({kind:B.literal("retime"),speed:B.number().positive()}),B.object({kind:B.literal("loop-point"),time:B.number().min(0)}),B.object({fps:B.number().positive(),kind:B.literal("resample")}),B.object({kind:B.literal("mirror")}),B.object({contact:B.number().min(0).default(.03),feet:B.array(B.string().min(1)).max(8).default([]),kind:B.literal("foot-lock")})]),fw=B.object({clip:B.string().min(1).max(200),edits:B.array(gU).min(1).max(64),file:B.string().min(1),log:B.string().min(1).optional(),output:B.string().min(1).optional()}),pw=B.object({clips:B.array(B.string()),edits:B.number(),file:B.string(),log:B.string(),modified:B.array(B.string())}),R0=B.object({anchor:_w.default("right-hand"),attach:B.string().min(1).optional(),host:B.string().min(1).optional(),output:B.string().min(1).optional(),port:B.number().int().min(0).max(65535).optional(),project:B.string().min(1).optional()}),mw=B.object({port:B.number(),url:B.string()}),Ba=t=>({destructiveHint:false,idempotentHint:true,openWorldHint:false,readOnlyHint:t}),En=t=>[{type:"text",text:JSON.stringify(t)}];function _U(t,e,r){if(r!==void 0){let s=yh.find(o=>o.id===e);if(s===void 0)throw new Error(`Unknown anchor '${e}'.`);return {id:e,label:s.label,node:r,source:"detected"}}let i=t.find(s=>s.id===e);if(i!==void 0)return i;let n=t.map(s=>`${s.id} \u2192 ${s.node}`);throw new Error(`The host names no '${e}' landmark. It answers to: ${n.length===0?"none":n.join(", ")}. Pass 'bone' to name one directly.`)}function vU(t){return {position:t.position,rotation:t.rotation,scale:t.scale}}function vw(t){let e=Gr.parse(t);return Gr.join(e.dir,`${e.name}.anim.glb`)}async function yU(t){return t.filmstrip===void 0?null:(await tw({clip:t.clip,file:t.file,fps:t.fps,output:t.filmstrip}),Gr.resolve(t.filmstrip))}async function SU(t){let e=(t.keys??[]).map(s=>({attachment:s.attachment,clip:s.clip,op:"key",time:s.time,transform:vU(s.transform)})),r;if(t.recipe!==void 0){r=Ry(JSON.parse(await readFile(t.recipe,"utf8")));for(let s of e)r=Ey(r,s);}else {if(t.host===void 0||t.attached===void 0)throw new Error("Pass 'host' and 'attached', or a 'recipe' that names both.");let s=await Py(t.host);r={host:t.host,ops:[{anchor:_U(s.landmarks,t.anchor,t.bone),attached:t.attached,op:"attach"},...e],version:4};}let i=t.log??wy(r.host);return await My(i,r),{...await hE(r,t.output??vw(r.host)),log:Gr.resolve(i)}}var gw;function yw(t){return gw??=(async()=>{let e=await ow({...t.port===void 0?{}:{port:t.port},...t.project===void 0?{}:{project:t.project}}),r=new URLSearchParams;return t.host!==void 0&&r.set("host",t.host),t.attach!==void 0&&r.set("attach",t.attach),t.output!==void 0&&r.set("output",t.output),r.set("anchor",t.anchor),{port:e.port,url:`http://127.0.0.1:${e.port}/pose?${r.toString()}`}})(),gw}function xU(){let t=new Aa({name:"threenative-animation",version:"0.1.11"});return t.registerTool("animation_inspect",{annotations:Ba(true),description:"Inspect a local rigged GLB: bones, skeleton landmarks, clips with durations, root-motion travel and in-place detection, meshes, skins and extensions. Reads one local file and writes nothing.",inputSchema:mU,outputSchema:cw,title:"Inspect a rig and its clips"},async e=>{try{let r=cw.parse(await Py(e.file));return {content:En(r),structuredContent:r}}catch(r){return {content:En({message:r instanceof Error?r.message:String(r)}),isError:true}}}),t.registerTool("animation_attach",{annotations:Ba(false),description:"Attach a prop GLB to a bone, with per-clip keyed offsets, and bake it into an ordinary GLB beside the source. Writes the v4 op log (.anim.json) next to the source GLB and the baked GLB; a version 3 pose recipe loads as the same ops and still bakes.",inputSchema:lw,outputSchema:uw,title:"Attach a prop to a bone and bake it"},async e=>{try{let r=uw.parse(await SU(lw.parse(e)));return {content:En(r),structuredContent:r}}catch(r){return {content:En({message:r instanceof Error?r.message:String(r)}),isError:true}}}),t.registerTool("animation_verify",{annotations:Ba(true),description:"Measure one clip for the defects a still frame cannot show: a planted foot that slides (2 cm), ground penetration (1 cm), a loop seam (5\xB0), a one-frame jitter spike, and bones whose length changes. Every defect names the bone and the frame range it spans. Thresholds are in metres; pass `scale` for a model that is not (Mixamo and Quaternius are centimetres: 0.01).",inputSchema:dw,outputSchema:hw,title:"Verify a clip's motion"},async e=>{try{let r=dw.parse(e),i=hw.parse({...await JE(r.file,r.clip,{fps:r.fps,scale:r.scale}),filmstrip:await yU(r)});return {content:En(i),structuredContent:i}}catch(r){return {content:En({message:r instanceof Error?r.message:String(r)}),isError:true}}}),t.registerTool("animation_modify",{annotations:Ba(false),description:"Edit one clip \u2014 trim, retime, move the loop point, resample, mirror, lock planted feet \u2014 writing each edit into the op log and rendering the result from the source GLB. Rendering twice from one log gives the same bytes, so the log is the change and the GLB is derived.",inputSchema:fw,outputSchema:pw,title:"Edit a clip and render it from the log"},async e=>{try{let r=fw.parse(e),i=r.log??wy(r.file),s=await lE(i).catch(async()=>({host:r.file,ops:[],version:4}));for(let c of r.edits)s=Ey(s,{clip:r.clip,edit:c,op:"modify"});await My(i,s);let o=await XE(s,r.output??vw(r.file)),a=pw.parse({...o,log:Gr.resolve(i)});return {content:En(a),structuredContent:a}}catch(r){return {content:En({message:r instanceof Error?r.message:String(r)}),isError:true}}}),t.registerTool("animation_open_view",{annotations:Ba(false),description:"Start the animation calibrator page on 127.0.0.1 and return its URL. A human drags the prop into place there; the same operations an agent calls are what the page records.",inputSchema:R0,outputSchema:mw,title:"Open the animation calibrator"},async e=>{try{let r=mw.parse(await yw(R0.parse(e)));return {content:En(r),structuredContent:r}}catch(r){return {content:En({message:r instanceof Error?r.message:String(r)}),isError:true}}}),t}function bU(){nR(()=>xU());}function TU(){let t=process.argv[1];if(t===void 0)return false;try{return realpathSync(t)===realpathSync(fileURLToPath(import.meta.url))}catch{return false}}TU()&&(process.argv[2]==="--view"?yw(R0.parse({project:process.cwd()})).then(({url:t})=>process.stdout.write(`Animation view: ${t} `),t=>{process.stderr.write(`${t instanceof Error?t.message:String(t)} `),process.exitCode=1;}):bU());/*! Bundled license information: diff --git a/packages/core/mcp/engine-server.mjs b/packages/core/mcp/engine-server.mjs index ecf5e6c21f..cb34e9072b 100755 --- a/packages/core/mcp/engine-server.mjs +++ b/packages/core/mcp/engine-server.mjs @@ -1359,7 +1359,7 @@ async function handleLine(line2, manifestFile) { capabilities: { tools: { listChanged: false } }, instructions: AUTHORING_INSTRUCTIONS, protocolVersion: "2025-06-18", - serverInfo: { name: "threenative-engine-mcp", version: "0.2.13" } + serverInfo: { name: "threenative-engine-mcp", version: "0.2.14" } }); } if (request.method === "tools/list") { diff --git a/packages/core/mcp/servers.mjs b/packages/core/mcp/servers.mjs index 05f77ea109..fed2210cd2 100644 --- a/packages/core/mcp/servers.mjs +++ b/packages/core/mcp/servers.mjs @@ -52,10 +52,10 @@ export const MCP_SERVERS = Object.freeze({ * checkout whose bundle has not been built. */ export const MCP_PACKAGES = Object.freeze({ assets: Object.freeze({ name: "threenative-asset-mcp", version: "0.16.0" }), - terrain: Object.freeze({ name: "@threenative/terrain", version: "0.1.9" }), + terrain: Object.freeze({ name: "@threenative/terrain", version: "0.1.10" }), sculpt: Object.freeze({ name: "threenative-sculpt-mcp", version: "0.2.2" }), - engine: Object.freeze({ name: "threenative-engine-mcp", version: "0.2.13" }), - animation: Object.freeze({ name: "threenative-animation-mcp", version: "0.1.10" }), + engine: Object.freeze({ name: "threenative-engine-mcp", version: "0.2.14" }), + animation: Object.freeze({ name: "threenative-animation-mcp", version: "0.1.11" }), // `args` fixes the launch for every host. The Chrome flags are `WEBGPU_BROWSER_ARGS` from // `@threenative/playtest`, so a DevTools trace profiles the same adapter a playtest does and not // SwiftShader. An isolated profile keeps the user's own Chrome profile out, and the two opt-outs diff --git a/packages/core/package.json b/packages/core/package.json index 31ac8bf50b..f1cb35977f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@threenative/core", - "version": "0.3.13", + "version": "0.3.14", "license": "MIT AND GPL-2.0-or-later", "description": "The small vanilla Three.js game runtime foundation.", "keywords": [ @@ -122,7 +122,7 @@ "@typescript/native": "catalog:" }, "peerDependencies": { - "@threenative/playtest": ">=0.3.13 <0.4.0", + "@threenative/playtest": ">=0.3.14 <0.4.0", "react": ">=19.2.0 <20", "react-reconciler": ">=0.33.0 <0.34" }, diff --git a/packages/core/src/version.ts b/packages/core/src/version.ts index 5e84da91f2..ce5b2cdeb8 100644 --- a/packages/core/src/version.ts +++ b/packages/core/src/version.ts @@ -1,2 +1,2 @@ // Generated by scripts/generate-version.mjs - do not edit. Source: package.json. -export const CORE_VERSION = "0.3.13"; +export const CORE_VERSION = "0.3.14"; diff --git a/packages/create-threenative/__tests__/build.spec.ts b/packages/create-threenative/__tests__/build.spec.ts index 4c23172c35..794b73b6c9 100644 --- a/packages/create-threenative/__tests__/build.spec.ts +++ b/packages/create-threenative/__tests__/build.spec.ts @@ -1495,7 +1495,7 @@ cpSync("public", out, { recursive: true }); await published("create-threenative"), ); expect(manifest.optionalDependencies?.["@threenative/runtime-native"], template).toBe( - await published("runtime-native"), + `>=${await published("runtime-native")}`, ); expect(manifest.pnpm?.onlyBuiltDependencies, template).toContain( "@threenative/runtime-native", diff --git a/packages/create-threenative/__tests__/core-pair.spec.ts b/packages/create-threenative/__tests__/core-pair.spec.ts index caba17a9a0..bf16b95ea0 100644 --- a/packages/create-threenative/__tests__/core-pair.spec.ts +++ b/packages/create-threenative/__tests__/core-pair.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { CORE_TEMPLATE_FLOOR, pairsWithCore } from "../src/core-pair.js"; +import { pairsWithCore } from "../src/core-pair.js"; import { CORE_PACKAGE_VERSION } from "../src/mcp-servers.js"; const [MAJOR, MINOR, PATCH] = CORE_PACKAGE_VERSION.split(".").map(Number) as [ @@ -15,10 +15,6 @@ function previousRelease(): string { } describe("the core a game may pair with", () => { - it("ships the same floor the templates declare", () => { - expect(CORE_TEMPLATE_FLOOR).toBe(`>=${CORE_PACKAGE_VERSION}`); - }); - it("accepts the release this create-threenative was built against", () => { expect(pairsWithCore(CORE_PACKAGE_VERSION)).toBe(true); }); diff --git a/packages/create-threenative/__tests__/publication.spec.ts b/packages/create-threenative/__tests__/publication.spec.ts index db06f83b33..93a2de066e 100644 --- a/packages/create-threenative/__tests__/publication.spec.ts +++ b/packages/create-threenative/__tests__/publication.spec.ts @@ -7,6 +7,7 @@ import path from "node:path"; import { pathToFileURL } from "node:url"; import { promisify } from "node:util"; import { describe, expect, it } from "vitest"; +import { templateSpecifier } from "../../../scripts/version-range.js"; import { publicWorkspacePackages } from "../../../scripts/workspace-packages.js"; import { makeTempDir } from "../../../test-support/temp-dir.js"; @@ -185,9 +186,11 @@ describe("registry publication", () => { "utf8", ), ) as { dependencies: Record }; - // And a template must pin whatever that version is, exactly — a scaffold pinning a - // version the workspace no longer publishes cannot install. - expect(templateManifest.dependencies["@threenative/ui"], template).toBe(packageJson.version); + // And a template's floor must be that version — a floor the workspace does not publish + // cannot install. + expect(templateManifest.dependencies["@threenative/ui"], template).toBe( + templateSpecifier("@threenative/ui", packageJson.version), + ); } }); @@ -208,7 +211,9 @@ describe("registry publication", () => { for (const [name, version] of Object.entries(template[field] ?? {})) { const workspaceVersion = versions.get(name); if (workspaceVersion === undefined) continue; - expect(version, `${entry.name} ${field}.${name}`).toBe(workspaceVersion); + expect(version, `${entry.name} ${field}.${name}`).toBe( + templateSpecifier(name, workspaceVersion), + ); } } } diff --git a/packages/create-threenative/__tests__/scaffold.spec.ts b/packages/create-threenative/__tests__/scaffold.spec.ts index 22c1efad43..93fc061a06 100644 --- a/packages/create-threenative/__tests__/scaffold.spec.ts +++ b/packages/create-threenative/__tests__/scaffold.spec.ts @@ -116,68 +116,68 @@ const PACK_GENERATED_PAGES: readonly string[] = ["capability-reference.md"]; const SCAFFOLD_TREE_HASHES: Readonly> = { // scaffold-pin:begin "action-rpg": { - shared: "89aa333d822a6627d2ad23f96521abf4ea7ba92bcec1ed55f9c30a8ee83ff250", - kit: "6cc2f1560f2f68724630383b3db8dadfe96636b84143ff1e26f2c64a51272b6a", + shared: "1da79b069ff7d69c2ade4fcdd085d90cdea5feb45e32080f2a4387bfc4da4142", + kit: "eb6d822002840052a347d7f2f89dd7344d77586fc94a8de9282a4a7ef4622683", }, adventure: { - shared: "ddac540bdf78253f87db7432d1b80a2a4dc7e20809cd1006b3cc261492b7d6fa", - kit: "b7f39b0565b23e13e1d5452cf1ac450d31c5ef0a1e628e62d5b79d5a0a13b03d", + shared: "a62c06e9f4ac0a1cd0b7bc37dc2c7b51f45d6fffba151dc6b526a56073c7e0d9", + kit: "029c6b6abd71ee3f1859a38686083b21519ddaec41d11aac8e69729dc3080c6e", }, collectathon: { - shared: "3a0df8488b8d94deb01464e10fabde2d6ac4e65647e98f39f5afb235ec71ffae", - kit: "6c1a27670ecb528be120fef09851e7e0d05883e0453bf993161dfbd4b5d0a038", + shared: "ca1fed0952b59d4e4a95ae954c088ab1de3d24feb28f479227de41a63d7d78be", + kit: "02a3a047755a35a43e313d4bc9366e561344c68cf74fd5401e397ac9773adac6", }, kart: { - shared: "676434e30b0ed4e2dfe698e5d6ab393e47fe7779ef7df617ca8f12f1f24c7748", - kit: "88430371871175bf3ab95086f4450089e8df119ef823911dfa049f5437b4add7", + shared: "0d37501cd26b8b776bdc9a06872d371209fa9d3716781a98881d90bd2b73c0ab", + kit: "616b99f3bb89b01e3286dfea85dd09054a0c6516df2f0bc4ad3084f671c89d1f", }, minimal: { - shared: "f4b43e245b3a9810947d7a3b5652b409ffb097cafb7834ed215567f83d3379af", - kit: "7b8213f03fcb0282bb1de3de5cfa44bfca55efce6a53f6a9e5800ff10c07f495", + shared: "c3ec6d627ae7179dff4da43ba55224b40be020ef97ec8369db8f6485c518fe4d", + kit: "6a9913e467973cf435d2186c6659d84870f8258942734944a8bcd8bbf5aafb61", }, platformer: { - shared: "0720f0c8f3374f5d41e7540dd3fe16f4ceeb2b5717fe2b163bafd4d7742983a8", - kit: "3e102d862444eac65584dca4dd550268652a01f349e7eefea994a9a09fc65bb1", + shared: "dc214fb679ee15d67275f6a1e81c286e707c72f69d4edba642fef15bda651c7c", + kit: "76bfa5d4e3c5893ba2b6ada9674d5ad164552bc6939b6519050731445b31a6dd", }, puzzle: { - shared: "865623ec3097b295dfc67012b3e61eaba0d7a322e2b6ec59620478863fdbdae6", - kit: "e237631bdc0e72156381d699cb3e0d1b89a0ecfe3f00d65b101e1d58b6e602a4", + shared: "7145f06e3428cda0165af08063ea815c642608884a481a85c7649d3c16b02cfe", + kit: "c7a46ea4961d8e285179c3d2701f128d4b5e7666731dc41f70563ae060c12a2a", }, racing: { - shared: "8463fb44077a88e0c5938db571af259c59f668eedca9d477a0bc4b70239c3c60", - kit: "75fe644366e883be5b9f98f2901208d6b1fbf84b9522a61b00de94b0ca91696e", + shared: "bc31777801c8aec0fdd7fa178f9fcd39cf42cb92759bb705242b1a1c60299403", + kit: "4167e639c4b8361ab7c9dae8caee765aa7487384f8d7de0657fc7ce5092c7137", }, rain: { - shared: "f3d68ab1eb2759202e15499046cc80d362d3586be314f68610eed0bf2c20367e", - kit: "f36b4ae9fc412a295749096fcd03c2e37b14aeda3dcdf144a2e536001b1380e1", + shared: "761d21aebc00bbe8b75a37c0a8dd67b17dd7fcf78ab3a29a9a82ad7bf71119d3", + kit: "2030c9113d1088e23b35398275f177829a305ff398047a5259529334bd4a3431", }, rts: { - shared: "9e7c20d1b7eceb245eb9b1db1e30d1294f6533a9b69ef0f3e3bfad1c67d86b38", - kit: "bf48b027dcc68e5be5e5fd18cb67f1b28ddc558b6fb2aaae7d927feab10c1cde", + shared: "40898821275b3b391859efdb96a2b223eadf0c7b5e4091a6184affae49e6697e", + kit: "fc002302f30c5038e01cf7a4f6a58856b0381da4b55318aa55a53a2a011dfbbd", }, runner: { - shared: "cd43eda17fbdc91bedb7680441daf0a30dee0f3b1e19bacebb124ab4471ff1ba", - kit: "3e7b332420e36bad39c659b1e9bf933f0a489891a9bffa9f6dc41ea623822b5b", + shared: "5d3f7fde13c73426ab0320e96777ba660c1e5c844f38d41e11248d9d6246e861", + kit: "805231f689753c53618fd1d6d87e44d3b99d2f64d8a8da1b5e0461426fd6844b", }, sailing: { - shared: "6e39c4c67676a2c1d4ae7fdf148a0e7cc4dde3b6207b49da84701a4266e720c0", - kit: "91a786b88aef46b7a943cf16dcf7b3f26bc6a6fefddb2e2827c76fadf97dd8b8", + shared: "9c036afebb6db487a4a69a52c2c08be24555b4c66804b605f0a0a87604739391", + kit: "c699544738f583389987430e35605e7251dad9aa0a212dc13a6a2e56f0a4389c", }, shooter: { - shared: "dbe1eb0f2872275fc9759deea95c4c624f7fa82fb7adccab3d22a20889cabaac", - kit: "572809c688d58403bdda547c90e8bcd01d9ab39ee5dd452c36647e7d149855cd", + shared: "ede46ec4e52905a0829b87a2b9d1820a9889f8106322635203e47b89602d3a1f", + kit: "5587118a1f7401b5f339f7d4717c89b8c454b590980666e2c0abcf731e8da643", }, snow: { - shared: "130389a38fb48663bd0cd391751ab3d2eb0c47e0d535e236b09366fd1fc93099", - kit: "aad24bfe38ba18bdb701216ac9734a5330b8fc3c142ff091a3137e2d309630c7", + shared: "29918a5b10ee296d903b10c6741ef8d78663e54be191de4350b5524248446b80", + kit: "4d3e9c38d2696e890bc759af60253094f6ba0d5aeb4eae714e4442ffe92caa85", }, starter: { - shared: "000bd75177222e5b3ab03beee4118b7c63a8dd59fdc316487d61ff7b3758376d", - kit: "d81a3f22192640f60b7780330e8b121e2b07ca309eeca1ac13fd7eba7d13a6b6", + shared: "20636d0769fdab2f5054781a879f7d2eb24873db6bd9f324828fe342d32e12a4", + kit: "0678edb73f1932af31d477eb7da3a29497e3c30d10a42c2196dc1fef5abfec00", }, "tower-defense": { - shared: "119e88ddb3fdcf2f3f4c0aade6e62e4b480e2b6a1292d69552040fb6e8083470", - kit: "ebf8cfa6e2a0ed443eebdc81573528eda9bbea5cb5a878b99bdcccf9dce7c90e", + shared: "d804c83f9bce5a20a42bc4e4b4c01646437b34556336d884425e7a890ded5856", + kit: "5e71000a474c978d7e1aa8ee944e9cc196cf4c174e566cfdb8589db9032015c6", }, // scaffold-pin:end }; diff --git a/packages/create-threenative/animation-mcp-tools.json b/packages/create-threenative/animation-mcp-tools.json index 0da06875c9..099692c3ba 100644 --- a/packages/create-threenative/animation-mcp-tools.json +++ b/packages/create-threenative/animation-mcp-tools.json @@ -1,9 +1,9 @@ { - "comment": "The animation MCP surface a generated project actually gets. `tools` is set-equal to the live tools/list response of version 0.1.10, packed from this workspace and installed from that tarball into a clean directory — never read from src/. `recommended` is the ordered loop the shipped agent docs describe. Regenerate with `pnpm tsx scripts/capture-mcp-tools.ts`.", - "version": "0.1.10", + "comment": "The animation MCP surface a generated project actually gets. `tools` is set-equal to the live tools/list response of version 0.1.11, packed from this workspace and installed from that tarball into a clean directory — never read from src/. `recommended` is the ordered loop the shipped agent docs describe. Regenerate with `pnpm tsx scripts/capture-mcp-tools.ts`.", + "version": "0.1.11", "serverInfo": { "name": "threenative-animation", - "version": "0.1.10" + "version": "0.1.11" }, "recommended": ["animation_inspect", "animation_attach", "animation_open_view"], "tools": [ diff --git a/packages/create-threenative/engine-mcp-tools.json b/packages/create-threenative/engine-mcp-tools.json index 498e2874e7..fa85c0272d 100644 --- a/packages/create-threenative/engine-mcp-tools.json +++ b/packages/create-threenative/engine-mcp-tools.json @@ -1,6 +1,6 @@ { "comment": "The engine capability MCP surface shipped through @threenative/core. The golden path also calls a mechanically explicit cross-system request so a packaged consumer proves full-request discovery without repository source.", - "version": "0.2.13", + "version": "0.2.14", "tools": [ "engine_capability_detail", "engine_reference_detail", diff --git a/packages/create-threenative/package.json b/packages/create-threenative/package.json index 01ca96b8ee..297ce0bf15 100644 --- a/packages/create-threenative/package.json +++ b/packages/create-threenative/package.json @@ -1,6 +1,6 @@ { "name": "create-threenative", - "version": "0.2.18", + "version": "0.2.19", "license": "MIT", "description": "Scaffold a readable ThreeNative game project.", "keywords": [ @@ -108,6 +108,6 @@ "@types/estree": "1.0.8" }, "peerDependencies": { - "@threenative/assets": ">=0.3.14 <0.4.0" + "@threenative/assets": ">=0.3.15 <0.4.0" } } diff --git a/packages/create-threenative/src/core-pair.ts b/packages/create-threenative/src/core-pair.ts index 274ddc54e3..461a707ddd 100644 --- a/packages/create-threenative/src/core-pair.ts +++ b/packages/create-threenative/src/core-pair.ts @@ -8,12 +8,6 @@ function parts(value: string): readonly [number, number, number] | undefined { return parsed.every(Number.isSafeInteger) ? parsed : undefined; } -/** - * `0.3.13` -> `>=0.3.13`, matching what the templates ship. The scaffolder writes the specifier and - * the gates compare against it, so both spellings live in one place. - */ -export const CORE_TEMPLATE_FLOOR = `>=${CORE_PACKAGE_VERSION}`; - /** * Whether a game's installed core can carry the authoring layer this create-threenative writes. * diff --git a/packages/create-threenative/templates/action-rpg/package.json b/packages/create-threenative/templates/action-rpg/package.json index bb11d87806..d99b06c666 100644 --- a/packages/create-threenative/templates/action-rpg/package.json +++ b/packages/create-threenative/templates/action-rpg/package.json @@ -19,9 +19,9 @@ "format:check": "biome format src" }, "dependencies": { - "@threenative/core": ">=0.3.13", - "@threenative/physics": ">=0.3.13", - "@threenative/ui": ">=0.3.13", + "@threenative/core": ">=0.3.14", + "@threenative/physics": ">=0.3.14", + "@threenative/ui": ">=0.3.14", "react": "19.2.0", "react-dom": "19.2.0", "three": "0.187.0" @@ -29,13 +29,13 @@ "devDependencies": { "@gltf-transform/cli": "4.4.2", "@tailwindcss/vite": "4.3.3", - "@threenative/assets": ">=0.3.14", - "@threenative/playtest": ">=0.3.13", + "@threenative/assets": ">=0.3.15", + "@threenative/playtest": ">=0.3.14", "@types/react": "19.2.0", "@types/react-dom": "19.2.0", "@types/three": "0.187.0", "@vitejs/plugin-react": "6.0.5", - "create-threenative": "0.2.18", + "create-threenative": "0.2.19", "esbuild": "0.27.0", "playwright": "1.62.1", "tailwindcss": "4.3.3", @@ -45,7 +45,7 @@ "eslint": "10.13.0" }, "optionalDependencies": { - "@threenative/runtime-native": ">=0.3.13" + "@threenative/runtime-native": ">=0.3.14" }, "overrides": { "sharp": ">=0.35.4", diff --git a/packages/create-threenative/templates/adventure/package.json b/packages/create-threenative/templates/adventure/package.json index 964b26f0bd..0e15e7b9bc 100644 --- a/packages/create-threenative/templates/adventure/package.json +++ b/packages/create-threenative/templates/adventure/package.json @@ -19,9 +19,9 @@ "format:check": "biome format src" }, "dependencies": { - "@threenative/core": ">=0.3.13", - "@threenative/physics": ">=0.3.13", - "@threenative/ui": ">=0.3.13", + "@threenative/core": ">=0.3.14", + "@threenative/physics": ">=0.3.14", + "@threenative/ui": ">=0.3.14", "react": "19.2.0", "react-dom": "19.2.0", "three": "0.187.0" @@ -30,11 +30,11 @@ "@tailwindcss/vite": "4.3.3", "@vitejs/plugin-react": "6.0.5", "@types/three": "0.187.0", - "@threenative/assets": ">=0.3.14", - "@threenative/playtest": ">=0.3.13", + "@threenative/assets": ">=0.3.15", + "@threenative/playtest": ">=0.3.14", "@types/react": "19.2.0", "@types/react-dom": "19.2.0", - "create-threenative": "0.2.18", + "create-threenative": "0.2.19", "esbuild": "0.27.0", "playwright": "1.62.1", "tailwindcss": "4.3.3", @@ -45,7 +45,7 @@ "eslint": "10.13.0" }, "optionalDependencies": { - "@threenative/runtime-native": ">=0.3.13" + "@threenative/runtime-native": ">=0.3.14" }, "overrides": { "sharp": ">=0.35.4", diff --git a/packages/create-threenative/templates/collectathon/package.json b/packages/create-threenative/templates/collectathon/package.json index 964b26f0bd..0e15e7b9bc 100644 --- a/packages/create-threenative/templates/collectathon/package.json +++ b/packages/create-threenative/templates/collectathon/package.json @@ -19,9 +19,9 @@ "format:check": "biome format src" }, "dependencies": { - "@threenative/core": ">=0.3.13", - "@threenative/physics": ">=0.3.13", - "@threenative/ui": ">=0.3.13", + "@threenative/core": ">=0.3.14", + "@threenative/physics": ">=0.3.14", + "@threenative/ui": ">=0.3.14", "react": "19.2.0", "react-dom": "19.2.0", "three": "0.187.0" @@ -30,11 +30,11 @@ "@tailwindcss/vite": "4.3.3", "@vitejs/plugin-react": "6.0.5", "@types/three": "0.187.0", - "@threenative/assets": ">=0.3.14", - "@threenative/playtest": ">=0.3.13", + "@threenative/assets": ">=0.3.15", + "@threenative/playtest": ">=0.3.14", "@types/react": "19.2.0", "@types/react-dom": "19.2.0", - "create-threenative": "0.2.18", + "create-threenative": "0.2.19", "esbuild": "0.27.0", "playwright": "1.62.1", "tailwindcss": "4.3.3", @@ -45,7 +45,7 @@ "eslint": "10.13.0" }, "optionalDependencies": { - "@threenative/runtime-native": ">=0.3.13" + "@threenative/runtime-native": ">=0.3.14" }, "overrides": { "sharp": ">=0.35.4", diff --git a/packages/create-threenative/templates/kart/package.json b/packages/create-threenative/templates/kart/package.json index 9a7312bef6..a4064c5e84 100644 --- a/packages/create-threenative/templates/kart/package.json +++ b/packages/create-threenative/templates/kart/package.json @@ -19,8 +19,8 @@ "format:check": "biome format src" }, "dependencies": { - "@threenative/core": ">=0.3.13", - "@threenative/ui": ">=0.3.13", + "@threenative/core": ">=0.3.14", + "@threenative/ui": ">=0.3.14", "react": "19.2.0", "react-dom": "19.2.0", "three": "0.187.0" @@ -29,11 +29,11 @@ "@tailwindcss/vite": "4.3.3", "@vitejs/plugin-react": "6.0.5", "@types/three": "0.187.0", - "@threenative/assets": ">=0.3.14", - "@threenative/playtest": ">=0.3.13", + "@threenative/assets": ">=0.3.15", + "@threenative/playtest": ">=0.3.14", "@types/react": "19.2.0", "@types/react-dom": "19.2.0", - "create-threenative": "0.2.18", + "create-threenative": "0.2.19", "esbuild": "0.27.0", "playwright": "1.62.1", "tailwindcss": "4.3.3", @@ -44,7 +44,7 @@ "eslint": "10.13.0" }, "optionalDependencies": { - "@threenative/runtime-native": ">=0.3.13" + "@threenative/runtime-native": ">=0.3.14" }, "overrides": { "sharp": ">=0.35.4", diff --git a/packages/create-threenative/templates/minimal/package.json b/packages/create-threenative/templates/minimal/package.json index 05b3e960db..17f202768f 100644 --- a/packages/create-threenative/templates/minimal/package.json +++ b/packages/create-threenative/templates/minimal/package.json @@ -19,15 +19,15 @@ "format:check": "biome format src" }, "dependencies": { - "@threenative/core": ">=0.3.13", - "@threenative/physics": ">=0.3.13", + "@threenative/core": ">=0.3.14", + "@threenative/physics": ">=0.3.14", "three": "0.187.0" }, "devDependencies": { - "@threenative/assets": ">=0.3.14", - "@threenative/playtest": ">=0.3.13", + "@threenative/assets": ">=0.3.15", + "@threenative/playtest": ">=0.3.14", "@types/three": "0.187.0", - "create-threenative": "0.2.18", + "create-threenative": "0.2.19", "esbuild": "0.27.0", "playwright": "1.62.1", "typescript": "7.0.2", @@ -37,7 +37,7 @@ "eslint": "10.13.0" }, "optionalDependencies": { - "@threenative/runtime-native": ">=0.3.13" + "@threenative/runtime-native": ">=0.3.14" }, "overrides": { "sharp": ">=0.35.4", diff --git a/packages/create-threenative/templates/platformer/package.json b/packages/create-threenative/templates/platformer/package.json index 964b26f0bd..0e15e7b9bc 100644 --- a/packages/create-threenative/templates/platformer/package.json +++ b/packages/create-threenative/templates/platformer/package.json @@ -19,9 +19,9 @@ "format:check": "biome format src" }, "dependencies": { - "@threenative/core": ">=0.3.13", - "@threenative/physics": ">=0.3.13", - "@threenative/ui": ">=0.3.13", + "@threenative/core": ">=0.3.14", + "@threenative/physics": ">=0.3.14", + "@threenative/ui": ">=0.3.14", "react": "19.2.0", "react-dom": "19.2.0", "three": "0.187.0" @@ -30,11 +30,11 @@ "@tailwindcss/vite": "4.3.3", "@vitejs/plugin-react": "6.0.5", "@types/three": "0.187.0", - "@threenative/assets": ">=0.3.14", - "@threenative/playtest": ">=0.3.13", + "@threenative/assets": ">=0.3.15", + "@threenative/playtest": ">=0.3.14", "@types/react": "19.2.0", "@types/react-dom": "19.2.0", - "create-threenative": "0.2.18", + "create-threenative": "0.2.19", "esbuild": "0.27.0", "playwright": "1.62.1", "tailwindcss": "4.3.3", @@ -45,7 +45,7 @@ "eslint": "10.13.0" }, "optionalDependencies": { - "@threenative/runtime-native": ">=0.3.13" + "@threenative/runtime-native": ">=0.3.14" }, "overrides": { "sharp": ">=0.35.4", diff --git a/packages/create-threenative/templates/puzzle/package.json b/packages/create-threenative/templates/puzzle/package.json index 7c5d95e6d0..58af89f002 100644 --- a/packages/create-threenative/templates/puzzle/package.json +++ b/packages/create-threenative/templates/puzzle/package.json @@ -18,9 +18,9 @@ "format:check": "biome format src" }, "dependencies": { - "@threenative/core": ">=0.3.13", - "@threenative/physics": ">=0.3.13", - "@threenative/ui": ">=0.3.13", + "@threenative/core": ">=0.3.14", + "@threenative/physics": ">=0.3.14", + "@threenative/ui": ">=0.3.14", "react": "19.2.0", "react-dom": "19.2.0", "three": "0.187.0" @@ -28,13 +28,13 @@ "devDependencies": { "@gltf-transform/cli": "4.4.2", "@tailwindcss/vite": "4.3.3", - "@threenative/assets": ">=0.3.14", - "@threenative/playtest": ">=0.3.13", + "@threenative/assets": ">=0.3.15", + "@threenative/playtest": ">=0.3.14", "@types/react": "19.2.0", "@types/react-dom": "19.2.0", "@types/three": "0.187.0", "@vitejs/plugin-react": "6.0.5", - "create-threenative": "0.2.18", + "create-threenative": "0.2.19", "esbuild": "0.27.0", "playwright": "1.62.1", "tailwindcss": "4.3.3", @@ -44,7 +44,7 @@ "eslint": "10.13.0" }, "optionalDependencies": { - "@threenative/runtime-native": ">=0.3.13" + "@threenative/runtime-native": ">=0.3.14" }, "overrides": { "sharp": ">=0.35.4", diff --git a/packages/create-threenative/templates/racing/package.json b/packages/create-threenative/templates/racing/package.json index f33a603e14..3cc5a6d3f7 100644 --- a/packages/create-threenative/templates/racing/package.json +++ b/packages/create-threenative/templates/racing/package.json @@ -19,9 +19,9 @@ "format:check": "biome format src" }, "dependencies": { - "@threenative/core": ">=0.3.13", - "@threenative/physics": ">=0.3.13", - "@threenative/ui": ">=0.3.13", + "@threenative/core": ">=0.3.14", + "@threenative/physics": ">=0.3.14", + "@threenative/ui": ">=0.3.14", "react": "19.2.0", "react-dom": "19.2.0", "three": "0.187.0" @@ -29,13 +29,13 @@ "devDependencies": { "@gltf-transform/cli": "4.4.2", "@tailwindcss/vite": "4.3.3", - "@threenative/assets": ">=0.3.14", - "@threenative/playtest": ">=0.3.13", + "@threenative/assets": ">=0.3.15", + "@threenative/playtest": ">=0.3.14", "@types/react": "19.2.0", "@types/react-dom": "19.2.0", "@types/three": "0.187.0", "@vitejs/plugin-react": "6.0.5", - "create-threenative": "0.2.18", + "create-threenative": "0.2.19", "esbuild": "0.27.0", "playwright": "1.62.1", "tailwindcss": "4.3.3", @@ -45,7 +45,7 @@ "eslint": "10.13.0" }, "optionalDependencies": { - "@threenative/runtime-native": ">=0.3.13" + "@threenative/runtime-native": ">=0.3.14" }, "overrides": { "sharp": ">=0.35.4", diff --git a/packages/create-threenative/templates/rain/package.json b/packages/create-threenative/templates/rain/package.json index 29dae69c6e..57f39277a7 100644 --- a/packages/create-threenative/templates/rain/package.json +++ b/packages/create-threenative/templates/rain/package.json @@ -18,8 +18,8 @@ "format:check": "biome format src" }, "dependencies": { - "@threenative/core": ">=0.3.13", - "@threenative/ui": ">=0.3.13", + "@threenative/core": ">=0.3.14", + "@threenative/ui": ">=0.3.14", "react": "19.2.0", "react-dom": "19.2.0", "react-reconciler": "0.33.0", @@ -27,13 +27,13 @@ }, "devDependencies": { "@tailwindcss/vite": "4.3.3", - "@threenative/assets": ">=0.3.14", - "@threenative/playtest": ">=0.3.13", + "@threenative/assets": ">=0.3.15", + "@threenative/playtest": ">=0.3.14", "@types/react": "19.2.0", "@types/react-dom": "19.2.0", "@types/three": "0.187.0", "@vitejs/plugin-react": "6.0.5", - "create-threenative": "0.2.18", + "create-threenative": "0.2.19", "esbuild": "0.27.0", "playwright": "1.62.1", "tailwindcss": "4.3.3", @@ -45,7 +45,7 @@ "eslint": "10.13.0" }, "optionalDependencies": { - "@threenative/runtime-native": ">=0.3.13" + "@threenative/runtime-native": ">=0.3.14" }, "overrides": { "sharp": ">=0.35.4", diff --git a/packages/create-threenative/templates/rts/package.json b/packages/create-threenative/templates/rts/package.json index e46ade3c05..ed2d9f20ab 100644 --- a/packages/create-threenative/templates/rts/package.json +++ b/packages/create-threenative/templates/rts/package.json @@ -19,8 +19,8 @@ "format:check": "biome format src" }, "dependencies": { - "@threenative/core": ">=0.3.13", - "@threenative/ui": ">=0.3.13", + "@threenative/core": ">=0.3.14", + "@threenative/ui": ">=0.3.14", "react": "19.2.0", "react-dom": "19.2.0", "three": "0.187.0" @@ -28,13 +28,13 @@ "devDependencies": { "@gltf-transform/cli": "4.4.2", "@tailwindcss/vite": "4.3.3", - "@threenative/assets": ">=0.3.14", - "@threenative/playtest": ">=0.3.13", + "@threenative/assets": ">=0.3.15", + "@threenative/playtest": ">=0.3.14", "@types/react": "19.2.0", "@types/react-dom": "19.2.0", "@types/three": "0.187.0", "@vitejs/plugin-react": "6.0.5", - "create-threenative": "0.2.18", + "create-threenative": "0.2.19", "esbuild": "0.27.0", "playwright": "1.62.1", "tailwindcss": "4.3.3", @@ -44,7 +44,7 @@ "eslint": "10.13.0" }, "optionalDependencies": { - "@threenative/runtime-native": ">=0.3.13" + "@threenative/runtime-native": ">=0.3.14" }, "overrides": { "sharp": ">=0.35.4", diff --git a/packages/create-threenative/templates/runner/package.json b/packages/create-threenative/templates/runner/package.json index 7c5d95e6d0..58af89f002 100644 --- a/packages/create-threenative/templates/runner/package.json +++ b/packages/create-threenative/templates/runner/package.json @@ -18,9 +18,9 @@ "format:check": "biome format src" }, "dependencies": { - "@threenative/core": ">=0.3.13", - "@threenative/physics": ">=0.3.13", - "@threenative/ui": ">=0.3.13", + "@threenative/core": ">=0.3.14", + "@threenative/physics": ">=0.3.14", + "@threenative/ui": ">=0.3.14", "react": "19.2.0", "react-dom": "19.2.0", "three": "0.187.0" @@ -28,13 +28,13 @@ "devDependencies": { "@gltf-transform/cli": "4.4.2", "@tailwindcss/vite": "4.3.3", - "@threenative/assets": ">=0.3.14", - "@threenative/playtest": ">=0.3.13", + "@threenative/assets": ">=0.3.15", + "@threenative/playtest": ">=0.3.14", "@types/react": "19.2.0", "@types/react-dom": "19.2.0", "@types/three": "0.187.0", "@vitejs/plugin-react": "6.0.5", - "create-threenative": "0.2.18", + "create-threenative": "0.2.19", "esbuild": "0.27.0", "playwright": "1.62.1", "tailwindcss": "4.3.3", @@ -44,7 +44,7 @@ "eslint": "10.13.0" }, "optionalDependencies": { - "@threenative/runtime-native": ">=0.3.13" + "@threenative/runtime-native": ">=0.3.14" }, "overrides": { "sharp": ">=0.35.4", diff --git a/packages/create-threenative/templates/sailing/package.json b/packages/create-threenative/templates/sailing/package.json index 12411c9f7b..ccf3b9346a 100644 --- a/packages/create-threenative/templates/sailing/package.json +++ b/packages/create-threenative/templates/sailing/package.json @@ -19,9 +19,9 @@ "format:check": "biome format src" }, "dependencies": { - "@threenative/core": ">=0.3.13", - "@threenative/physics": ">=0.3.13", - "@threenative/ui": ">=0.3.13", + "@threenative/core": ">=0.3.14", + "@threenative/physics": ">=0.3.14", + "@threenative/ui": ">=0.3.14", "react": "19.2.0", "react-dom": "19.2.0", "react-reconciler": "0.33.0", @@ -29,13 +29,13 @@ }, "devDependencies": { "@tailwindcss/vite": "4.3.3", - "@threenative/assets": ">=0.3.14", - "@threenative/playtest": ">=0.3.13", + "@threenative/assets": ">=0.3.15", + "@threenative/playtest": ">=0.3.14", "@types/react": "19.2.0", "@types/react-dom": "19.2.0", "@types/three": "0.187.0", "@vitejs/plugin-react": "6.0.5", - "create-threenative": "0.2.18", + "create-threenative": "0.2.19", "esbuild": "0.27.0", "playwright": "1.62.1", "tailwindcss": "4.3.3", @@ -46,7 +46,7 @@ "eslint": "10.13.0" }, "optionalDependencies": { - "@threenative/runtime-native": ">=0.3.13" + "@threenative/runtime-native": ">=0.3.14" }, "overrides": { "sharp": ">=0.35.4", diff --git a/packages/create-threenative/templates/shooter/package.json b/packages/create-threenative/templates/shooter/package.json index 77ee91fa1c..36d4e35cae 100644 --- a/packages/create-threenative/templates/shooter/package.json +++ b/packages/create-threenative/templates/shooter/package.json @@ -19,9 +19,9 @@ "format:check": "biome format src" }, "dependencies": { - "@threenative/core": ">=0.3.13", - "@threenative/physics": ">=0.3.13", - "@threenative/ui": ">=0.3.13", + "@threenative/core": ">=0.3.14", + "@threenative/physics": ">=0.3.14", + "@threenative/ui": ">=0.3.14", "react": "19.2.0", "react-dom": "19.2.0", "three": "0.187.0" @@ -30,12 +30,12 @@ "@tailwindcss/vite": "4.3.3", "@types/three": "0.187.0", "@vitejs/plugin-react": "6.0.5", - "@threenative/assets": ">=0.3.14", - "@threenative/playtest": ">=0.3.13", + "@threenative/assets": ">=0.3.15", + "@threenative/playtest": ">=0.3.14", "@types/react": "19.2.0", "@types/react-dom": "19.2.0", "playwright": "1.62.1", - "create-threenative": "0.2.18", + "create-threenative": "0.2.19", "esbuild": "0.27.0", "tailwindcss": "4.3.3", "typescript": "7.0.2", @@ -45,7 +45,7 @@ "eslint": "10.13.0" }, "optionalDependencies": { - "@threenative/runtime-native": ">=0.3.13" + "@threenative/runtime-native": ">=0.3.14" }, "overrides": { "sharp": ">=0.35.4", diff --git a/packages/create-threenative/templates/snow/package.json b/packages/create-threenative/templates/snow/package.json index fbfa67fd75..67136cad21 100644 --- a/packages/create-threenative/templates/snow/package.json +++ b/packages/create-threenative/templates/snow/package.json @@ -19,9 +19,9 @@ "format:check": "biome format src" }, "dependencies": { - "@threenative/core": ">=0.3.13", - "@threenative/physics": ">=0.3.13", - "@threenative/ui": ">=0.3.13", + "@threenative/core": ">=0.3.14", + "@threenative/physics": ">=0.3.14", + "@threenative/ui": ">=0.3.14", "react": "19.2.0", "react-dom": "19.2.0", "react-reconciler": "0.33.0", @@ -29,13 +29,13 @@ }, "devDependencies": { "@tailwindcss/vite": "4.3.3", - "@threenative/assets": ">=0.3.14", - "@threenative/playtest": ">=0.3.13", + "@threenative/assets": ">=0.3.15", + "@threenative/playtest": ">=0.3.14", "@types/react": "19.2.0", "@types/react-dom": "19.2.0", "@types/three": "0.187.0", "@vitejs/plugin-react": "6.0.5", - "create-threenative": "0.2.18", + "create-threenative": "0.2.19", "esbuild": "0.27.0", "playwright": "1.62.1", "tailwindcss": "4.3.3", @@ -46,7 +46,7 @@ "eslint": "10.13.0" }, "optionalDependencies": { - "@threenative/runtime-native": ">=0.3.13" + "@threenative/runtime-native": ">=0.3.14" }, "overrides": { "sharp": ">=0.35.4", diff --git a/packages/create-threenative/templates/starter/package.json b/packages/create-threenative/templates/starter/package.json index 14f4f8254c..bef6340497 100644 --- a/packages/create-threenative/templates/starter/package.json +++ b/packages/create-threenative/templates/starter/package.json @@ -20,9 +20,9 @@ "format:check": "biome format src" }, "dependencies": { - "@threenative/core": ">=0.3.13", - "@threenative/physics": ">=0.3.13", - "@threenative/ui": ">=0.3.13", + "@threenative/core": ">=0.3.14", + "@threenative/physics": ">=0.3.14", + "@threenative/ui": ">=0.3.14", "react": "19.2.0", "react-dom": "19.2.0", "react-reconciler": "0.33.0", @@ -30,13 +30,13 @@ }, "devDependencies": { "@tailwindcss/vite": "4.3.3", - "@threenative/assets": ">=0.3.14", - "@threenative/playtest": ">=0.3.13", + "@threenative/assets": ">=0.3.15", + "@threenative/playtest": ">=0.3.14", "@types/react": "19.2.0", "@types/react-dom": "19.2.0", "@types/three": "0.187.0", "@vitejs/plugin-react": "6.0.5", - "create-threenative": "0.2.18", + "create-threenative": "0.2.19", "esbuild": "0.27.0", "playwright": "1.62.1", "tailwindcss": "4.3.3", @@ -47,7 +47,7 @@ "eslint": "10.13.0" }, "optionalDependencies": { - "@threenative/runtime-native": ">=0.3.13" + "@threenative/runtime-native": ">=0.3.14" }, "overrides": { "sharp": ">=0.35.4", diff --git a/packages/create-threenative/templates/tower-defense/package.json b/packages/create-threenative/templates/tower-defense/package.json index 7c5d95e6d0..58af89f002 100644 --- a/packages/create-threenative/templates/tower-defense/package.json +++ b/packages/create-threenative/templates/tower-defense/package.json @@ -18,9 +18,9 @@ "format:check": "biome format src" }, "dependencies": { - "@threenative/core": ">=0.3.13", - "@threenative/physics": ">=0.3.13", - "@threenative/ui": ">=0.3.13", + "@threenative/core": ">=0.3.14", + "@threenative/physics": ">=0.3.14", + "@threenative/ui": ">=0.3.14", "react": "19.2.0", "react-dom": "19.2.0", "three": "0.187.0" @@ -28,13 +28,13 @@ "devDependencies": { "@gltf-transform/cli": "4.4.2", "@tailwindcss/vite": "4.3.3", - "@threenative/assets": ">=0.3.14", - "@threenative/playtest": ">=0.3.13", + "@threenative/assets": ">=0.3.15", + "@threenative/playtest": ">=0.3.14", "@types/react": "19.2.0", "@types/react-dom": "19.2.0", "@types/three": "0.187.0", "@vitejs/plugin-react": "6.0.5", - "create-threenative": "0.2.18", + "create-threenative": "0.2.19", "esbuild": "0.27.0", "playwright": "1.62.1", "tailwindcss": "4.3.3", @@ -44,7 +44,7 @@ "eslint": "10.13.0" }, "optionalDependencies": { - "@threenative/runtime-native": ">=0.3.13" + "@threenative/runtime-native": ">=0.3.14" }, "overrides": { "sharp": ">=0.35.4", diff --git a/packages/engine-mcp/__tests__/server.spec.ts b/packages/engine-mcp/__tests__/server.spec.ts index f02e8b53e0..ae95c932e4 100644 --- a/packages/engine-mcp/__tests__/server.spec.ts +++ b/packages/engine-mcp/__tests__/server.spec.ts @@ -32,7 +32,7 @@ describe("threenative-engine-mcp stdio contract", async () => { capabilities: { tools: { listChanged: false } }, instructions: expect.stringContaining("threenative-capabilities"), protocolVersion: "2025-06-18", - serverInfo: { name: "threenative-engine-mcp", version: "0.2.13" }, + serverInfo: { name: "threenative-engine-mcp", version: "0.2.14" }, }, }); }); diff --git a/packages/engine-mcp/package.json b/packages/engine-mcp/package.json index 31feb15fd2..17e6ce70e5 100644 --- a/packages/engine-mcp/package.json +++ b/packages/engine-mcp/package.json @@ -1,6 +1,6 @@ { "name": "threenative-engine-mcp", - "version": "0.2.13", + "version": "0.2.14", "license": "MIT", "description": "Offline MCP capability discovery for ThreeNative authoring agents.", "keywords": [ diff --git a/packages/engine-mcp/src/index.ts b/packages/engine-mcp/src/index.ts index f5a650d279..38b66e8837 100644 --- a/packages/engine-mcp/src/index.ts +++ b/packages/engine-mcp/src/index.ts @@ -1467,7 +1467,7 @@ export async function handleLine(line: string, manifestFile: string): Promise=0.3.13 <0.4.0", + "@threenative/core": ">=0.3.14 <0.4.0", "three": ">=0.187.0 <0.188.0" }, "scripts": { diff --git a/packages/playtest/package.json b/packages/playtest/package.json index 41d37556dd..e139b2cd8d 100644 --- a/packages/playtest/package.json +++ b/packages/playtest/package.json @@ -1,6 +1,6 @@ { "name": "@threenative/playtest", - "version": "0.3.13", + "version": "0.3.14", "license": "MIT", "description": "Scenario-driven playtest harness for Three.js games. Runs on plain Three.js with zero ThreeNative dependencies.", "keywords": [ diff --git a/packages/procedural-animals/package.json b/packages/procedural-animals/package.json index 67c10ca1c2..19a6e48264 100644 --- a/packages/procedural-animals/package.json +++ b/packages/procedural-animals/package.json @@ -1,6 +1,6 @@ { "name": "@threenative/procedural-animals", - "version": "0.1.9", + "version": "0.1.10", "license": "MIT", "description": "Optional, bake-first procedural animal content for ThreeNative.", "homepage": "https://github.com/ThreeNativeHQ/threenative#readme", diff --git a/packages/raw-unreal/package.json b/packages/raw-unreal/package.json index 3c1b766c33..7b8f16be4a 100644 --- a/packages/raw-unreal/package.json +++ b/packages/raw-unreal/package.json @@ -1,6 +1,6 @@ { "name": "@threenative/raw-unreal", - "version": "0.1.13", + "version": "0.1.14", "license": "MIT", "description": "Loads raw Unreal editor .uasset static meshes directly into Three.js geometry \u2014 UE5 FMeshDescription and UE4.18 FRawMesh layouts, with no interchange conversion.", "keywords": ["threenative", "unreal", "uasset", "static-mesh", "three", "threejs", "loader"], diff --git a/packages/runtime-native/package.json b/packages/runtime-native/package.json index ea454ec435..226d9218eb 100644 --- a/packages/runtime-native/package.json +++ b/packages/runtime-native/package.json @@ -1,6 +1,6 @@ { "name": "@threenative/runtime-native", - "version": "0.3.13", + "version": "0.3.14", "description": "ThreeNative-owned native host for desktop and Android", "keywords": [ "threenative", diff --git a/packages/terrain/package.json b/packages/terrain/package.json index 2ac7c3e7e6..32c3ed9817 100644 --- a/packages/terrain/package.json +++ b/packages/terrain/package.json @@ -1,6 +1,6 @@ { "name": "@threenative/terrain", - "version": "0.1.9", + "version": "0.1.10", "license": "MIT", "description": "Headless terrain authoring \u2014 noise, erosion, sculpting, roads, rivers, material splats and scatter \u2014 evaluated into ordinary typed arrays and indexed Three.js geometry.", "keywords": ["threenative", "terrain", "heightfield", "three", "threejs", "procedural", "webgpu"], @@ -84,8 +84,8 @@ "@threenative/physics": "workspace:*" }, "peerDependencies": { - "@threenative/core": ">=0.3.13 <0.4.0", - "@threenative/physics": ">=0.3.13 <0.4.0", + "@threenative/core": ">=0.3.14 <0.4.0", + "@threenative/physics": ">=0.3.14 <0.4.0", "three": ">=0.187.0 <0.188.0", "vite": ">=7.0.0 <9.0.0" }, diff --git a/packages/terrain/src/core/io.ts b/packages/terrain/src/core/io.ts index b6cf68cafd..6c8cd1ba3a 100644 --- a/packages/terrain/src/core/io.ts +++ b/packages/terrain/src/core/io.ts @@ -405,7 +405,7 @@ export function encodeGLB(input: IBakedMesh | readonly IBakedMesh[]) { } const binary = join(chunks); const doc = { - asset: { version: "2.0", generator: "@threenative/terrain 0.1.9" }, + asset: { version: "2.0", generator: "@threenative/terrain 0.1.10" }, scene: 0, scenes: [{ nodes: meshes.map((_, i) => i) }], nodes: meshes.map((m, i) => ({ mesh: i, name: m.name ?? `Terrain ${i}` })), diff --git a/packages/three-native/__tests__/bridge-crossings.spec.ts b/packages/three-native/__tests__/bridge-crossings.spec.ts index bd7aad055d..ded80a1941 100644 --- a/packages/three-native/__tests__/bridge-crossings.spec.ts +++ b/packages/three-native/__tests__/bridge-crossings.spec.ts @@ -308,6 +308,67 @@ function growableModule() { } describe("Wasm memory growth under the bridge", () => { + // The threaded build's memory is shared, and a pool worker can grow it. Emscripten refreshes a + // thread's views only inside its own glue, so this thread's HEAPU8 stays at the old length while the + // engine hands back pointers past it (CI: "Offset is outside the bounds of the DataView"). A shared + // buffer never detaches, so a crossing inside the old length must not pay a `memory.buffer` read. + it("reads a result the engine placed past the views after another thread grew shared memory", () => { + const { module } = growableModule(); + const real = new WebAssembly.Memory({ initial: 2, maximum: 4, shared: true }); + let bufferReads = 0; + const memory = { + get buffer() { + bufferReads += 1; + return real.buffer; + }, + }; + Object.assign(module, { + wasmMemory: memory, + HEAPU8: new Uint8Array(real.buffer), + HEAPF64: new Float64Array(real.buffer), + }); + const runtime = createWasmRuntime(module); + const self = { key: "5:0:1:1", type: 5 }; + const past = real.buffer.byteLength + 64; + let grow = false; + (module as unknown as Record)._tn_invoke = ( + _handle: number, + _name: number, + _args: number, + _count: number, + out: number, + ) => { + const v = new DataView(real.buffer); + if (!grow) { + v.setUint32(out, 1, true); // KIND.number + v.setFloat64(out + 8, 3, true); + return 0; + } + real.grow(1); // another thread's malloc; this thread's glue never runs + const grown = new DataView(real.buffer); + grown.setUint32(out, 6, true); // KIND.array of one number, stored past the old end + grown.setUint32(out + 48, past, true); + grown.setBigUint64(out + 40, 1n, true); + grown.setUint32(past, 1, true); + grown.setFloat64(past + 8, 7.5, true); + return 0; + }; + for (let crossing = 0; crossing < 20; crossing += 1) + expect(runtime.invoke(self, "count", [1.5, "x", [2, 3]])).toBe(3); + runtime.writeDouble?.(800, 1.5); + expect(runtime.readDoubles?.(800, 1)).toBe(1.5); + expect(runtime.heapF64?.(808)[100]).toBe(1.5); + expect(bufferReads).toBe(0); + grow = true; + expect(runtime.invoke(self, "children", [])).toEqual([7.5]); + runtime.writeDouble?.(past + 16, 2.5); + expect(runtime.readDoubles?.(past + 8, 2)).toEqual([7.5, 2.5]); + expect(runtime.heapF64?.(past + 24)[(past + 16) / 8]).toBe(2.5); + // The fresh views went onto the module, so the renderer's and loader's readers see them too. + expect(module.HEAPU8.byteLength).toBe(real.buffer.byteLength); + expect(module.HEAPF64.byteLength).toBe(real.buffer.byteLength); + }); + it("lands a transform write in the memory as it is after the engine grew it", () => { const { module, grow } = growableModule(); const runtime = createWasmRuntime(module); diff --git a/packages/three-native/package.json b/packages/three-native/package.json index cb547af42e..943b072dad 100644 --- a/packages/three-native/package.json +++ b/packages/three-native/package.json @@ -1,6 +1,6 @@ { "name": "@threenative/three-native", - "version": "0.3.13", + "version": "0.3.14", "license": "MIT", "description": "The native engine's machine-readable compatibility catalog and the generators that adapt it to TypeScript declarations and the C binding ABI.", "keywords": ["threenative", "three", "native", "abi", "gamedev"], diff --git a/packages/three-native/src/browser-abi-types.ts b/packages/three-native/src/browser-abi-types.ts index 0f54a82045..30c5ce9d63 100644 --- a/packages/three-native/src/browser-abi-types.ts +++ b/packages/three-native/src/browser-abi-types.ts @@ -83,10 +83,10 @@ export interface IBrowserRuntime { /** Doubles at `address` in the engine's memory (one as a number, more as an array); Wasm only. */ readDoubles?(address: number, count: number): number | number[]; /** - * The engine's memory as doubles, current: Emscripten replaces the view when the memory grows, so a caller - * reads it per operation and never keeps it. Wasm only. + * The engine's memory as doubles, current and reaching at least byte `end`: the view is replaced + * when the memory grows, so a caller reads it per operation and never keeps it. Wasm only. */ - heapF64?(): Float64Array; + heapF64?(end: number): Float64Array; /** The byte at `address` in the engine's memory; Wasm only. A registry field `[offset, 0]` is a byte flag. */ readByte?(address: number): number; /** The 32-bit word at `address` in the engine's memory (the hierarchy counter's low word); Wasm only. */ @@ -169,6 +169,28 @@ export type TnAbiModule = Record number> & lengthBytesUTF8(text: string): number; }; +/** + * The engine's memory as bytes, reaching at least `end`. Shared memory (the threaded build) grows from + * any thread, and Emscripten refreshes a thread's views only inside its own glue: after a pool worker + * grew it, HEAPU8 here stayed short and a pointer past it threw "Offset is outside the bounds of the + * DataView". A shared buffer never detaches, so a short view is right below its end: only an access + * past it reads `memory.buffer`, and the fresh views go onto the module for every other reader. + */ +export function heapFor(abi: TnAbiModule, end: number): Uint8Array { + if (end <= abi.HEAPU8.byteLength) return abi.HEAPU8; + const buffer = (abi as { wasmMemory?: WebAssembly.Memory }).wasmMemory?.buffer; + if ( + buffer !== undefined && + buffer.byteLength > abi.HEAPU8.byteLength && + typeof SharedArrayBuffer !== "undefined" && + buffer instanceof SharedArrayBuffer + ) { + abi.HEAPU8 = new Uint8Array(buffer); + abi.HEAPF64 = new Float64Array(buffer); + } + return abi.HEAPU8; +} + // wasm32 layouts of tn_abi.h, measured with offsetof under emcc: tn_handle_t 12 bytes; tn_value_t // 56 bytes (kind 0, boolean 4, number 8, handle 16, text 32, count 40, numbers 48); tn_diagnostic_t // 8 bytes (message 0, code 4); tn_version_info_t 32 bytes. A struct passed by value goes by pointer. @@ -226,7 +248,8 @@ export interface IAbiHelpers { check(status: number, diag: number, what: string): void; handleOf(ref: IEngineRef): number; writeHandle(pointer: number, ref: IEngineRef): void; - view(): DataView; + /** The heap's DataView, reaching at least byte `end` (pass it for a pointer `alloc` did not make). */ + view(end?: number): DataView; } /** `tnw_web_load_gltf`'s handles: the default scene, the clips, then every scene in file order. */ @@ -508,7 +531,7 @@ export function tslOf( const out = abi._malloc(size) >>> 0; try { if (readStorageTake(id, out, size) === 1) - return abi.HEAPU8.slice(out, out + size).buffer; + return heapFor(abi, out + size).slice(out, out + size).buffer; } finally { abi._free(out); } @@ -550,7 +573,7 @@ export function tslOf( candidateAt = abi._malloc(candidateRoom * 12) >>> 0; candidatesWritten = 0; } - const ids = h.view(); + const ids = h.view(candidateAt + candidateRoom * 12); for (; candidatesWritten < candidates.length; candidatesWritten += 1) ids.setBigUint64( candidateAt + candidatesWritten * 8, diff --git a/packages/three-native/src/browser-backend.ts b/packages/three-native/src/browser-backend.ts index 0008c24023..4ab4cc4cbd 100644 --- a/packages/three-native/src/browser-backend.ts +++ b/packages/three-native/src/browser-backend.ts @@ -1069,8 +1069,9 @@ export function defineBrowserClasses( Object.getPrototypeOf(other) !== prototype ) return engineCopy.call(this, other); - const from = (addressOf(other) + offset) / 8; - heapF64().copyWithin((addressOf(this) + offset) / 8, from, from + count); + const to = addressOf(this) + offset; + const from = addressOf(other) + offset; + heapF64(Math.max(to, from) + count * 8).copyWithin(to / 8, from / 8, from / 8 + count); return this; }, }, @@ -1084,9 +1085,9 @@ export function defineBrowserClasses( Object.getPrototypeOf(other) !== prototype ) return engineEquals.call(this, other); - const heap = heapF64(); const a = (addressOf(this) + offset) / 8; const b = (addressOf(other) + offset) / 8; + const heap = heapF64((Math.max(a, b) + count) * 8); for (let i = 0; i < count; i++) if (heap[a + i] !== heap[b + i]) return false; return true; }, diff --git a/packages/three-native/src/browser-renderer.ts b/packages/three-native/src/browser-renderer.ts index c53aa96c03..d76b0068ee 100644 --- a/packages/three-native/src/browser-renderer.ts +++ b/packages/three-native/src/browser-renderer.ts @@ -4,7 +4,7 @@ * surface and every draw; this class turns three's calls into host calls and reports what the * engine did. What it does not implement throws its diagnostic rather than doing nothing. */ -import { packedRef } from "./browser-abi-types.js"; +import { heapFor, packedRef } from "./browser-abi-types.js"; import { TSL_NODE, type TnAbiModule, engineRef } from "./browser-backend.js"; import { type ITslRuntime, defineTsl, isTslNode } from "./browser-tsl.js"; import { debugTelemetry } from "./debug-telemetry.js"; @@ -189,7 +189,7 @@ export function defineWebRenderer( if (ref === undefined) throw new TypeError(`TN_WASM_RENDER: the ${what} is not an engine object.`); const { w0, w1, w2 } = packedRef(ref); - const view = new DataView(module.HEAPU8.buffer); + const view = new DataView(heapFor(module, handles + offset + 12).buffer); view.setUint32(handles + offset, w0, true); view.setUint32(handles + offset + 4, w1, true); view.setUint32(handles + offset + 8, w2, true); @@ -1193,7 +1193,7 @@ export function defineWebRenderer( for (;;) { module._tnw_web_poll(); const status = module._tnw_web_read_target_take(id, out, size); - if (status === 1) return module.HEAPU8.slice(out, out + size); + if (status === 1) return heapFor(module, out + size).slice(out, out + size); // A read the lost device dropped says so, so the caller's report is read as that one loss. if (status < 0) throw new Error( @@ -1263,7 +1263,9 @@ export function defineWebRenderer( module._tnw_web_poll(); const status = module._tnw_web_read_target_take(id, out, bytes); if (status === 1) - return new Float32Array(module.HEAPU8.slice(out, out + bytes).buffer); + return new Float32Array( + heapFor(module, out + bytes).slice(out, out + bytes).buffer, + ); if (status < 0) throw new Error("TN_REFLECTION_CAPTURE_READ"); await new Promise((resolve) => setTimeout(resolve, 0)); } diff --git a/packages/three-native/src/browser-value-views.ts b/packages/three-native/src/browser-value-views.ts index b413c414f6..cc2a8ed050 100644 --- a/packages/three-native/src/browser-value-views.ts +++ b/packages/three-native/src/browser-value-views.ts @@ -311,7 +311,7 @@ export function defineValueViews(host: IValueViewHost): IValueViews { if (read === undefined || write === undefined) return; const heap = runtime.heapF64; const lane = heap - ? (address: number): number => heap()[address / 8] as number + ? (address: number): number => heap(address + 8)[address / 8] as number : (address: number): number => read(address, 1) as number; if (name === "Quaternion" && binding.fields !== undefined) { const offsets = ["x", "y", "z", "w"].map((key) => binding.fields?.[key]?.[0] ?? 0); @@ -458,7 +458,7 @@ export function defineValueViews(host: IValueViewHost): IValueViews { // a frame, and a copy call allocates two arrays a read. const refresh = heap ? (address: number, values: number[]): void => { - const memory = heap(); + const memory = heap(address + count * 8); for (let i = 0, at = address / 8; i < count; i++) values[i] = memory[at + i] as number; } : (address: number, values: number[]): void => { @@ -472,7 +472,7 @@ export function defineValueViews(host: IValueViewHost): IValueViews { configurable: true, get(this: object): number[] { const address = addressOf(this) + offset; - const memory = heap?.(); + const memory = heap?.(address + count * 8); if ( typeof SharedArrayBuffer !== "undefined" && memory?.buffer instanceof SharedArrayBuffer diff --git a/packages/three-native/src/browser-wasm-runtime.ts b/packages/three-native/src/browser-wasm-runtime.ts index 6c81887ba6..5c64c43932 100644 --- a/packages/three-native/src/browser-wasm-runtime.ts +++ b/packages/three-native/src/browser-wasm-runtime.ts @@ -13,6 +13,7 @@ import { type TnAbiModule, VALUE, gltfOf, + heapFor, isRef, packedRef, tslOf, @@ -37,24 +38,30 @@ export function createWasmRuntime(abi: TnAbiModule): IBrowserRuntime { let encoding = "arguments"; // The heap's views, made again only when the memory grows: Emscripten replaces HEAPU8 then, so one // identity test per use stands in for reading two `.buffer`s, which every crossing paid (PRD-748). + // Shared memory can grow from another thread with no replacement here, but it never detaches: an + // access past `heapBytes` (an engine pointer's `end`) refreshes through `heapFor`. let heap = abi.HEAPU8; let heapBytes = heap.byteLength; let dataView = new DataView(heap.buffer); let words = new Uint32Array(heap.buffer); - const grown = () => { - debug?.add("wasmGrowthEvents", 1, "bridge"); - debug?.add("wasmGrowthBytes", Math.max(0, abi.HEAPU8.byteLength - heapBytes), "memory"); - heap = abi.HEAPU8; + const grown = (end: number) => { + const before = heapBytes; + heap = heapFor(abi, end); heapBytes = heap.byteLength; dataView = new DataView(heap.buffer); words = new Uint32Array(heap.buffer); + debug?.add("wasmGrowthEvents", 1, "bridge"); + debug?.add("wasmGrowthBytes", Math.max(0, heapBytes - before), "memory"); }; - const view = () => { - if (abi.HEAPU8 !== heap) grown(); + const reach = (end: number) => { + if (end > heapBytes) grown(end); + }; + const view = (end = 0) => { + if (end > heapBytes || abi.HEAPU8 !== heap) grown(end); return dataView; }; - const wordView = () => { - if (abi.HEAPU8 !== heap) grown(); + const wordView = (end = 0) => { + if (end > heapBytes || abi.HEAPU8 !== heap) grown(end); return words; }; // Each call frees what it allocated, and only that: a callback can run a nested call. @@ -80,8 +87,8 @@ export function createWasmRuntime(abi: TnAbiModule): IBrowserRuntime { debug?.add("bridgeOverflowBytes", aligned, "bridge"); } // A call's scratch is a few words (a handle, a value, a diagnostic): word stores beat a fill call. - // `wordView` first: a `_malloc` above may have grown the memory. - const w = wordView(); + // `wordView` first: a `_malloc` above may have grown the memory, on this thread or another. + const w = wordView(pointer + aligned); if (aligned > 64) heap.fill(0, pointer, pointer + aligned); else for (let at = pointer >>> 2, end = (pointer + aligned) >>> 2; at < end; at++) w[at] = 0; return pointer; @@ -228,7 +235,7 @@ export function createWasmRuntime(abi: TnAbiModule): IBrowserRuntime { return pointer; }; const readValue = (pointer: number): EngineValue => { - const v = view(); + const v = view(pointer + VALUE); switch (v.getUint32(pointer, true)) { case KIND.undefined: return undefined; @@ -261,9 +268,9 @@ export function createWasmRuntime(abi: TnAbiModule): IBrowserRuntime { } case KIND.numbers: { const at = v.getUint32(pointer + 48, true) / 8; - return Array.from( - abi.HEAPF64.subarray(at, at + Number(v.getBigUint64(pointer + 40, true))), - ); + const count = Number(v.getBigUint64(pointer + 40, true)); + reach((at + count) * 8); + return Array.from(abi.HEAPF64.subarray(at, at + count)); } default: return null; @@ -323,6 +330,7 @@ export function createWasmRuntime(abi: TnAbiModule): IBrowserRuntime { const Typed = SCALARS[scalar ?? -1]; if (Typed === undefined) throw new TypeError(`TN_NATIVE_UNSUPPORTED array: scalar ${String(scalar)}`); + reach((address ?? 0) + (count ?? 0) * Typed.BYTES_PER_ELEMENT); return use(new Typed(abi.HEAPU8.buffer as ArrayBuffer, address ?? 0, count ?? 0)); } finally { abi._tnw_attribute_view_release?.(lease); @@ -423,16 +431,25 @@ export function createWasmRuntime(abi: TnAbiModule): IBrowserRuntime { batch.status[i] = after.getFloat64(status + i * 8, true); }); }, - readByte: (address) => abi.HEAPU8[address] as number, - readWord: (address) => view().getUint32(address, true), + readByte: (address) => { + reach(address + 1); + return abi.HEAPU8[address] as number; + }, + readWord: (address) => view(address + 4).getUint32(address, true), writeDouble: (address, value) => { + reach(address + 8); abi.HEAPF64[address / 8] = value; }, writeByte: (address, value) => { + reach(address + 1); abi.HEAPU8[address] = value; }, - heapF64: () => abi.HEAPF64, + heapF64: (end) => { + reach(end); + return abi.HEAPF64; + }, readDoubles: (address, count) => { + reach(address + count * 8); const at = address / 8; return count === 1 ? (abi.HEAPF64[at] as number) diff --git a/packages/ueformat/package.json b/packages/ueformat/package.json index 9d7a513ac5..645be67671 100644 --- a/packages/ueformat/package.json +++ b/packages/ueformat/package.json @@ -1,6 +1,6 @@ { "name": "@threenative/ueformat", - "version": "0.1.13", + "version": "0.1.14", "license": "MIT", "description": "UEFormat v10 .uemodel parser and Three.js loader for meshes exported from Unreal packages by CUE4Parse/FModel.", "keywords": ["threenative", "unreal", "ueformat", "uemodel", "three", "threejs", "loader"], diff --git a/packages/ui/package.json b/packages/ui/package.json index 37386119cc..528e45cc51 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@threenative/ui", - "version": "0.3.13", + "version": "0.3.14", "license": "MIT", "description": "React bindings for mounting a ThreeNative game and reading throttled state.", "keywords": ["threenative", "react", "tailwind", "hud", "three", "threejs", "gamedev"], @@ -30,7 +30,7 @@ }, "sideEffects": false, "peerDependencies": { - "@threenative/core": ">=0.3.13 <0.4.0", + "@threenative/core": ">=0.3.14 <0.4.0", "react": ">=19.2.0 <20", "react-dom": ">=19.2.0 <20" }, diff --git a/scripts/check-publish-state.ts b/scripts/check-publish-state.ts index 3135b7e027..28bb4b6e86 100644 --- a/scripts/check-publish-state.ts +++ b/scripts/check-publish-state.ts @@ -31,7 +31,7 @@ const { platformKey, releaseManifestUrl } = (await import( readonly platformKey: (platform?: string, arch?: string) => string; readonly releaseManifestUrl: (version?: string) => string; }; -import { rangeFloor, satisfiesSpecifier } from "./version-range.js"; +import { rangeFloor } from "./version-range.js"; import { publicWorkspacePackages, workspacePackages } from "./workspace-packages.js"; const REPO = path.resolve(import.meta.dirname, "..");