From 1c13fc65fd94e93d2919e5a6142c2ace497d0073 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Thu, 12 Nov 2026 10:09:04 -0700 Subject: [PATCH 1/4] chore(create-threenative): drop the floor constant nothing reads and an unused import Review of e64ab4af3: CORE_TEMPLATE_FLOOR claimed the scaffolder writes it, but only its own test read it; check-publish-state imported satisfiesSpecifier and never called it. --- packages/create-threenative/__tests__/core-pair.spec.ts | 6 +----- packages/create-threenative/src/core-pair.ts | 6 ------ scripts/check-publish-state.ts | 2 +- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/create-threenative/__tests__/core-pair.spec.ts b/packages/create-threenative/__tests__/core-pair.spec.ts index caba17a9a..bf16b95ea 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/src/core-pair.ts b/packages/create-threenative/src/core-pair.ts index 274ddc54e..461a707dd 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/scripts/check-publish-state.ts b/scripts/check-publish-state.ts index 3135b7e02..28bb4b6e8 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, ".."); From 056269fa0e63cc358cb3975fe3d82a00e08d9bf8 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Thu, 12 Nov 2026 10:23:28 -0700 Subject: [PATCH 2/4] test(create-threenative): template pins expect the engine floor, not an exact version e64ab4af3 moved templates to `>=` for @threenative/*, but build.spec and publication.spec still compared each pin to the exact workspace version, so three tests went red on main. They now read the expected form from scripts/version-range.ts, the one owner of it. --- packages/create-threenative/__tests__/build.spec.ts | 2 +- .../__tests__/publication.spec.ts | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/create-threenative/__tests__/build.spec.ts b/packages/create-threenative/__tests__/build.spec.ts index 4c23172c3..794b73b6c 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__/publication.spec.ts b/packages/create-threenative/__tests__/publication.spec.ts index db06f83b3..93a2de066 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), + ); } } } From 0f83b3240781077207566460de061f83eb93cdb4 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Thu, 12 Nov 2026 10:23:28 -0700 Subject: [PATCH 3/4] chore(release): prepare the 0.3.14 cohort The floating-template change (e64ab4af3) reaches new games only once create-threenative publishes again, and 0.2.18 is already on npm. release:prepare bumped the cohort (core 0.3.14, create-threenative 0.2.19), regenerated the MCP tool surfaces and re-pinned the scaffold hashes. Its publish-state check passed. --- .../playtests/replay.playtest.json | 2 +- packages/animation-mcp/package.json | 2 +- packages/animation-mcp/src/index.ts | 2 +- packages/assets/package.json | 2 +- packages/blender-bridge/package.json | 2 +- packages/core/mcp/animation-server.mjs | 2 +- packages/core/mcp/engine-server.mjs | 2 +- packages/core/mcp/servers.mjs | 6 +- packages/core/package.json | 4 +- packages/core/src/version.ts | 2 +- .../__tests__/scaffold.spec.ts | 64 +++++++++---------- .../animation-mcp-tools.json | 6 +- .../create-threenative/engine-mcp-tools.json | 2 +- packages/create-threenative/package.json | 4 +- .../templates/action-rpg/package.json | 14 ++-- .../templates/adventure/package.json | 14 ++-- .../templates/collectathon/package.json | 14 ++-- .../templates/kart/package.json | 12 ++-- .../templates/minimal/package.json | 12 ++-- .../templates/platformer/package.json | 14 ++-- .../templates/puzzle/package.json | 14 ++-- .../templates/racing/package.json | 14 ++-- .../templates/rain/package.json | 12 ++-- .../templates/rts/package.json | 12 ++-- .../templates/runner/package.json | 14 ++-- .../templates/sailing/package.json | 14 ++-- .../templates/shooter/package.json | 14 ++-- .../templates/snow/package.json | 14 ++-- .../templates/starter/package.json | 14 ++-- .../templates/tower-defense/package.json | 14 ++-- packages/engine-mcp/__tests__/server.spec.ts | 2 +- packages/engine-mcp/package.json | 2 +- packages/engine-mcp/src/index.ts | 2 +- packages/metahuman/package.json | 2 +- packages/physics/package.json | 4 +- packages/playtest/package.json | 2 +- packages/procedural-animals/package.json | 2 +- packages/raw-unreal/package.json | 2 +- packages/runtime-native/package.json | 2 +- packages/terrain/package.json | 6 +- packages/terrain/src/core/io.ts | 2 +- packages/three-native/package.json | 2 +- packages/ueformat/package.json | 2 +- packages/ui/package.json | 4 +- 44 files changed, 177 insertions(+), 177 deletions(-) diff --git a/examples/abyss-framework/playtests/replay.playtest.json b/examples/abyss-framework/playtests/replay.playtest.json index d45884306..f1dc346e8 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 28898406e..bf226652d 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 05a6e4117..2e623bb1d 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 4b41a8b02..901b4ad9e 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 982b2b756..7cd2edae6 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 78004eb8b..0d364aba9 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 ecf5e6c21..cb34e9072 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 05f77ea10..fed2210cd 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 31ac8bf50..f1cb35977 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 5e84da91f..ce5b2cdeb 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__/scaffold.spec.ts b/packages/create-threenative/__tests__/scaffold.spec.ts index 22c1efad4..93fc061a0 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 0da06875c..099692c3b 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 498e2874e..fa85c0272 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 01ca96b8e..297ce0bf1 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/templates/action-rpg/package.json b/packages/create-threenative/templates/action-rpg/package.json index bb11d8780..d99b06c66 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 964b26f0b..0e15e7b9b 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 964b26f0b..0e15e7b9b 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 9a7312bef..a4064c5e8 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 05b3e960d..17f202768 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 964b26f0b..0e15e7b9b 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 7c5d95e6d..58af89f00 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 f33a603e1..3cc5a6d3f 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 29dae69c6..57f39277a 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 e46ade3c0..ed2d9f20a 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 7c5d95e6d..58af89f00 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 12411c9f7..ccf3b9346 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 77ee91fa1..36d4e35ca 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 fbfa67fd7..67136cad2 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 14f4f8254..bef634049 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 7c5d95e6d..58af89f00 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 f02e8b53e..ae95c932e 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 31feb15fd..17e6ce70e 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 f5a650d27..38b66e883 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 41d37556d..e139b2cd8 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 67c10ca1c..19a6e4826 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 3c1b766c3..7b8f16be4 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 ea454ec43..226d9218e 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 2ac7c3e7e..32c3ed981 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 b6cf68caf..6c8cd1ba3 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/package.json b/packages/three-native/package.json index cb547af42..943b072da 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/ueformat/package.json b/packages/ueformat/package.json index 9d7a513ac..645be6767 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 37386119c..528e45cc5 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" }, From 88389d95f8c54e68b0cde59d107ff9e830482edc Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Thu, 12 Nov 2026 11:12:44 -0700 Subject: [PATCH 4/4] fix(three-native): heap reads follow shared memory a pool worker grew The threaded Wasm build runs on shared memory, which any thread can grow. Emscripten refreshes a thread's views only inside its own glue (growMemViews), so after a pool worker grew the memory the game thread's HEAPU8 kept the old length. A pointer the engine handed back past it threw "Offset is outside the bounds of the DataView" in readValue: the intermittent template-nonvisual (shooter) boot red. A shared buffer never detaches, so a short view stays right below its end. The bridge now refreshes only when an access passes its cached heapBytes (an integer compare, no memory.buffer read per crossing): view(end), wordView(end) and reach(end) at readValue, alloc's zeroing, attribute views, readDoubles/writeDouble/readByte/writeByte/readWord and heapF64(end). The refresh (heapFor in browser-abi-types) rebuilds from memory.buffer and puts the fresh HEAPU8/HEAPF64 on the module, and the cold readers that take a raw _malloc pointer (renderer handles and read-backs, storage read-back, TSL uniform-read cache, matrix copy/equals) pass their end to it. The identity test for non-shared memory is unchanged. --- .../__tests__/bridge-crossings.spec.ts | 61 +++++++++++++++++++ .../three-native/src/browser-abi-types.ts | 35 +++++++++-- packages/three-native/src/browser-backend.ts | 7 ++- packages/three-native/src/browser-renderer.ts | 10 +-- .../three-native/src/browser-value-views.ts | 6 +- .../three-native/src/browser-wasm-runtime.ts | 51 ++++++++++------ 6 files changed, 137 insertions(+), 33 deletions(-) diff --git a/packages/three-native/__tests__/bridge-crossings.spec.ts b/packages/three-native/__tests__/bridge-crossings.spec.ts index bd7aad055..ded80a194 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/src/browser-abi-types.ts b/packages/three-native/src/browser-abi-types.ts index 0f54a8204..30c5ce9d6 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 0008c2402..4ab4cc4cb 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 c53aa96c0..d76b0068e 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 b413c414f..cc2a8ed05 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 6c81887ba..5c64c4393 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)