@hology/core 0.0.240 → 0.0.242

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/gameplay/actors/builtin/navmesh-actor.js +1 -1
  2. package/dist/gameplay/actors/camera/first-person-camera-component.d.ts +0 -1
  3. package/dist/gameplay/actors/camera/first-person-camera-component.js +1 -1
  4. package/dist/gameplay/actors/camera/third-person-camera-component.d.ts +0 -1
  5. package/dist/gameplay/actors/camera/third-person-camera-component.js +1 -1
  6. package/dist/gameplay/ai/build-tile.js +1 -1
  7. package/dist/gameplay/ai/dynamic-tiled-navmesh.d.ts +2 -1
  8. package/dist/gameplay/ai/dynamic-tiled-navmesh.js +1 -1
  9. package/dist/gameplay/ai/dynamic-tiled-navmesh.worker.js +1 -1
  10. package/dist/gameplay/services/physics/abstract-physics-system.d.ts +4 -0
  11. package/dist/gameplay/services/physics/physics-system.d.ts +4 -8
  12. package/dist/gameplay/services/physics/physics-system.js +1 -1
  13. package/dist/rendering/webgpu-experimental-backend.d.ts +2 -0
  14. package/dist/rendering/webgpu-experimental-backend.js +1 -1
  15. package/dist/scene/asset-resource-loader.js +1 -1
  16. package/dist/scene/materializer.js +1 -1
  17. package/dist/scene/model.d.ts +51 -0
  18. package/dist/scene/runtime-backend-service.js +1 -1
  19. package/dist/scene/runtime-bundled-backend-service.js +1 -1
  20. package/dist/scene/storage/packed-json.d.ts +7 -0
  21. package/dist/scene/storage/packed-json.js +4 -0
  22. package/dist/scene/storage/storage.d.ts +1 -1
  23. package/dist/scene/storage/storage.js +1 -1
  24. package/dist/secondary-motion/data.d.ts +28 -0
  25. package/dist/secondary-motion/data.js +4 -0
  26. package/dist/secondary-motion/generation.d.ts +22 -0
  27. package/dist/secondary-motion/generation.js +4 -0
  28. package/dist/secondary-motion/runtime.d.ts +111 -0
  29. package/dist/secondary-motion/runtime.js +4 -0
  30. package/dist/test/asset-resource-loader-converted-fbx.test.d.ts +2 -0
  31. package/dist/test/asset-resource-loader-converted-fbx.test.js +4 -0
  32. package/dist/test/build-tile.test.d.ts +2 -0
  33. package/dist/test/build-tile.test.js +4 -0
  34. package/dist/test/first-person-camera-component.test.js +1 -1
  35. package/dist/test/materializer-dynamic-transform.test.d.ts +2 -0
  36. package/dist/test/materializer-dynamic-transform.test.js +4 -0
  37. package/dist/test/navmesh-worker-pool.test.js +1 -1
  38. package/dist/test/packed-json.test.d.ts +2 -0
  39. package/dist/test/packed-json.test.js +4 -0
  40. package/dist/test/secondary-motion.test.d.ts +2 -0
  41. package/dist/test/secondary-motion.test.js +4 -0
  42. package/package.json +6 -2
  43. package/tsconfig.tsbuildinfo +1 -1
@@ -149,6 +149,56 @@ export type ModelColliderData = {
149
149
  radius: number;
150
150
  height: number;
151
151
  };
152
+ export type SecondaryMotionMode = 'strand' | 'cloth';
153
+ export type SecondaryMotionQuality = 'low' | 'medium' | 'high' | 'hero';
154
+ export interface SecondaryMotionRegion {
155
+ id: string;
156
+ name: string;
157
+ /** Stable path from the imported model root to the source mesh. */
158
+ meshPath: string;
159
+ mode: SecondaryMotionMode;
160
+ attachment: {
161
+ type: 'bone';
162
+ boneName: string;
163
+ } | {
164
+ type: 'world';
165
+ };
166
+ preset?: string;
167
+ enabled: boolean;
168
+ quality: SecondaryMotionQuality;
169
+ stiffness: number;
170
+ bendStiffness: number;
171
+ animationStiffness: number;
172
+ damping: number;
173
+ gravityScale: number;
174
+ drag: number;
175
+ windResponse: number;
176
+ thickness: number;
177
+ maxDistance?: number;
178
+ /** Sparse editor-authored influence mask. Generated binary data supersedes this at cook time. */
179
+ paintedVertices?: Array<[vertexIndex: number, influence: number]>;
180
+ }
181
+ export interface SecondaryMotionCollider {
182
+ id: string;
183
+ name: string;
184
+ boneName: string;
185
+ type: 'sphere' | 'capsule';
186
+ position: [number, number, number];
187
+ rotation?: [number, number, number, number];
188
+ radius: number;
189
+ length?: number;
190
+ }
191
+ /** Editable secondary-motion metadata. Generated arrays live in resourceKey. */
192
+ export interface SecondaryMotionAssetData {
193
+ version: 1;
194
+ resourceKey: string;
195
+ regions: SecondaryMotionRegion[];
196
+ colliders: SecondaryMotionCollider[];
197
+ groundPlane?: {
198
+ enabled: boolean;
199
+ height: number;
200
+ };
201
+ }
152
202
  export type TextureSettings = {
153
203
  flipY?: boolean;
154
204
  wrapS?: WrapMode;
@@ -364,6 +414,7 @@ export type Asset = {
364
414
  shapeType?: CollisionShapeType;
365
415
  };
366
416
  colliders?: ModelColliderData[];
417
+ secondaryMotion?: SecondaryMotionAssetData;
367
418
  /** Animation rig this mesh is compatible with. */
368
419
  animationRigId?: AssetId;
369
420
  };
@@ -1,4 +1,4 @@
1
- import{ObjectStorage as e,defaultSerializer as t}from"./storage/storage.js";export class LegacyRuntimeBackendService{constructor(s){this.sceneStorage=new e("scene"),this.sceneBlobStorage=new e("scene-blob",void 0,e=>{const s={...e};return Array.isArray(s.data)&&(s.data=[...s.data].sort((e,t)=>e.id.localeCompare(t.id))),t(s)}),this.assetStorage=new e("asset"),this.sceneStorage.setBasePath(s),this.sceneBlobStorage.setBasePath(s),this.assetStorage.setBasePath(s)}getAssets(){return this.assetStorage.getAll()}getAsset(e){return this.assetStorage.get(e)}getScenes(){return this.sceneStorage.getAll()}getScene(e){return this.sceneStorage.get(e)}async getSceneData(e){const t=await this.sceneBlobStorage.get(e);return null==t?null:"string"==typeof t.data?JSON.parse(t.data):t.data}}/*
1
+ import{ObjectStorage as e,defaultSerializer as t}from"./storage/storage.js";import{parseStorageJson as s}from"./storage/packed-json.js";export class LegacyRuntimeBackendService{constructor(s){this.sceneStorage=new e("scene"),this.sceneBlobStorage=new e("scene-blob",void 0,e=>{const s={...e};return Array.isArray(s.data)&&(s.data=[...s.data].sort((e,t)=>e.id.localeCompare(t.id))),t(s)}),this.assetStorage=new e("asset"),this.sceneStorage.setBasePath(s),this.sceneBlobStorage.setBasePath(s),this.assetStorage.setBasePath(s)}getAssets(){return this.assetStorage.getAll()}getAsset(e){return this.assetStorage.get(e)}getScenes(){return this.sceneStorage.getAll()}getScene(e){return this.sceneStorage.get(e)}async getSceneData(e){const t=await this.sceneBlobStorage.get(e);return null==t?null:"string"==typeof t.data?s(t.data):t.data}}/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -1,4 +1,4 @@
1
- export class RuntimeBundledBackendService{constructor(){this.assets=new Map,this.scenes=new Map,this.sceneBlobs=new Map}getAssets(){return Promise.resolve(Array.from(this.assets.values()))}getAsset(e){return Promise.resolve(this.assets.get(e))}getScenes(){return Promise.resolve(Array.from(this.scenes.values()))}getScene(e){return Promise.resolve(this.scenes.get(e))}async getSceneData(e){const s=this.sceneBlobs.get(e);return"string"==typeof s.data?JSON.parse(s.data):s.data}static tryCreate(){}async preloadData(){const e=new URL("../hology_data.json",import.meta.url),s=await(await fetch(e)).json()??{};for(const e of s.asset??[])this.assets.set(e.id,e);for(const e of s.scene??[])this.scenes.set(e.id,e),this.scenes.set(e.name,e);for(const e of s["scene-blob"]??[])this.sceneBlobs.set(e.id,e)}}/*
1
+ import{decodeStorageValue as e,parseStorageJson as s}from"./storage/packed-json.js";export class RuntimeBundledBackendService{constructor(){this.assets=new Map,this.scenes=new Map,this.sceneBlobs=new Map}getAssets(){return Promise.resolve(Array.from(this.assets.values()))}getAsset(e){return Promise.resolve(this.assets.get(e))}getScenes(){return Promise.resolve(Array.from(this.scenes.values()))}getScene(e){return Promise.resolve(this.scenes.get(e))}async getSceneData(e){const t=this.sceneBlobs.get(e);return"string"==typeof t.data?s(t.data):t.data}static tryCreate(){}async preloadData(){const s=new URL("../hology_data.json",import.meta.url),t=await(await fetch(s)).json()??{};for(const s of t.asset??[]){const t=e(s);this.assets.set(t.id,t)}for(const e of t.scene??[])this.scenes.set(e.id,e),this.scenes.set(e.name,e);for(const s of t["scene-blob"]??[]){const t=e(s);this.sceneBlobs.set(t.id,t)}}}/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -0,0 +1,7 @@
1
+ /** Serializes storage data while compacting explicitly selected, machine-generated fields. */
2
+ export declare function serializeStorageJson(value: unknown): string;
3
+ /** Parses storage JSON and transparently restores packed fields. */
4
+ export declare function parseStorageJson<T = unknown>(text: string): T;
5
+ /** Restores packed fields in data which has already been parsed, such as a runtime bundle. */
6
+ export declare function decodeStorageValue<T>(value: T): T;
7
+ //# sourceMappingURL=packed-json.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{gzipSync as r,gunzipSync as e,strFromU8 as t,strToU8 as n}from"fflate";const o="@hology/packed-json:1:gzip:base64:";export function serializeStorageJson(r){return JSON.stringify(a(r),null,2)}export function parseStorageJson(r){return JSON.parse(r,(r,e)=>f(e))}export function decodeStorageValue(r){const e=f(r);return e!==r?decodeStorageValue(e):Array.isArray(r)?r.map(r=>decodeStorageValue(r)):y(r)?Object.fromEntries(Object.entries(r).map(([r,e])=>[r,decodeStorageValue(e)])):r}function a(r){if(Array.isArray(r))return r.map(a);if(!y(r))return r;const e={};for(const[t,n]of Object.entries(r))"vertexMaterials"===t&&Array.isArray(n)?e[t]=l(n):"landscape"===t&&y(n)?e[t]=s(n):"paintedScatter"===t&&y(n)?e[t]=i(n):"secondaryMotion"===t&&y(n)?e[t]=c(n):e[t]=a(n);return e}function s(r){const e=u(r);return Array.isArray(r.heightMaps)&&(e.heightMaps=l(r.heightMaps)),Array.isArray(r.holes)&&(e.holes=l(r.holes)),e}function i(r){const e=u(r);return Array.isArray(r.cells)&&(e.cells=r.cells.map(r=>{if(!y(r))return a(r);const e=u(r);return Array.isArray(r.instances)&&(e.instances=l(r.instances)),e})),e}function c(r){const e=u(r);return Array.isArray(r.regions)&&(e.regions=r.regions.map(r=>{if(!y(r))return a(r);const e=u(r);return Array.isArray(r.paintedVertices)&&(e.paintedVertices=l(r.paintedVertices)),e})),e}function u(r){return Object.fromEntries(Object.entries(r).map(([r,e])=>[r,a(e)]))}function l(e){const t=JSON.stringify(e);return t.length<128?a(e):o+function(r){let e="";const t=32768;for(let n=0;n<r.length;n+=t)e+=String.fromCharCode(...r.subarray(n,n+t));return btoa(e)}(r(n(t),{level:9}))}function f(r){if("string"!=typeof r||!r.startsWith(o))return r;const n=r.slice(34);try{return JSON.parse(t(e(function(r){const e=atob(r),t=new Uint8Array(e.length);for(let r=0;r<e.length;r++)t[r]=e.charCodeAt(r);return t}(n))))}catch(r){throw Object.assign(new Error("Failed to decode packed Hology storage data"),{cause:r})}}function y(r){return null!=r&&"object"==typeof r&&!Array.isArray(r)}/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -1,4 +1,5 @@
1
1
  import { Observable } from 'rxjs';
2
+ export { decodeStorageValue, parseStorageJson, serializeStorageJson } from './packed-json.js';
2
3
  export interface StorageEntity {
3
4
  id: string;
4
5
  name: string;
@@ -90,5 +91,4 @@ export type StorageWatchEvent<T> = {
90
91
  filename: string;
91
92
  };
92
93
  export declare function defaultSerializer<T>(obj: Partial<T>): string;
93
- export {};
94
94
  //# sourceMappingURL=storage.d.ts.map
@@ -1,4 +1,4 @@
1
- import{randomUUID as e}from"../../utils/uuid.js";import{pathJoin as t}from"../../utils/files.js";import{EMPTY as a,filter as i,firstValueFrom as n,from as s,map as r,merge as h,mergeAll as o,mergeMap as l,Observable as c,of as d,startWith as u,Subject as p,switchMap as f,tap as w}from"rxjs";import{sleepDelay as m}from"../../utils/async.js";const y={},v={},x={},b={};null==y.read&&window.require&&(Object.assign(y,window.require("fs")),Object.assign(v,y.promises),Object.assign(x,window.require("path")),Object.assign(b,window.require("chokidar")));const j=null!=y.existsSync;let g=0,I=0;const P=[];async function F(e){await function(){if(g<32)return g++,Promise.resolve();return new Promise(e=>P.push(e))}();try{return await e()}finally{!function(){const e=P[I];if(null==e)return P.length=0,I=0,void g--;I++,I>64&&2*I>P.length&&(P.splice(0,I),I=0);e()}()}}function O(e){return F(()=>v.readFile(e))}function S(){if(j){const e="--path=",t=window.process.argv.find(t=>t.startsWith(e));return t?t.substring(e.length):""}return""}const C=/^[A-Z]:/;function E(...e){return 0===e.length?"":C.test(e[0])||j?x.join(...e):t(...e)}export class ObjectStorage{get pathResources(){return E(this.path+"-resources")}constructor(e,t,a=defaultSerializer){this.name=e,this.filePathFn=t,this.serializer=a,this.basePathUpdates=new p,this.basePath=this.basePathUpdates.pipe(r(e=>E(S(),e)),w(e=>{this.path=E(e,this.name),this.cachedIndex=null,this.loadingIndex=null,k(this.path),this.determineIfMetaFileShouldBeCreated()})),this.loaded=n(this.basePath),this.cachedIndex=null,this.loadingIndex=null,this.shouldCreateIndex=!1}setBasePath(e){this.basePathUpdates.next(e)}async determineIfMetaFileShouldBeCreated(){if(j)try{const e=["vite.renderer.config.ts","vite.config.ts"];for(const t of e){const e=E(S(),t);if(!await z(e))continue;if((await O(e)).toString().includes("hologyBuild"))return void(this.shouldCreateIndex=!1)}this.shouldCreateIndex=!0}catch(e){console.warn("Failed to read vite config to determine if meta files should be created")}}async createFolder(e,t=""){D(),await v.mkdir(x.join(this.path,t,e),{recursive:!0})}async deleteFolderForceDangerous(e){await v.rm(x.join(this.path,e),{recursive:!0,force:!0})}async moveFolder(e,t){if(D(),""==e)return void console.warn("Can not move a folder in root");const a=x.resolve(x.join(this.path,t),x.basename(e));if(await U(a)){if(a!==x.resolve(this.path,e))throw new Error("Can not move to directory as a file already exists with the same name")}else{if(function(e,t){const a=x.resolve(e),i=x.resolve(t),n=x.normalize(a)+x.sep;return(x.normalize(i)+x.sep).startsWith(n)}(x.join(this.path,e),x.join(this.path,t)))throw new Error("Can not move a folder into a folder it contains");await v.rename(x.join(this.path,e),a)}}async renameFolder(e,t){await v.rename(x.join(this.path,e),x.resolve(x.dirname(x.join(this.path,e)),t))}async moveToFolder(e,t){if(D(),(e.path??"")===t)return e;const a={...e,path:t};await this.assertObjectFilePathAvailable(a,e.id);const i=this.privateObjectPath(a);return await v.rename(this.privateObjectPath(e),i),await this.addOrUpdateIndexEntry(a),a}getAbsolutePath(e=""){return x.join(this.path,e)}getResourceAbsolutePath(e=""){return x.join(this.pathResources,e)}watchFolders(){return s(this.loaded).pipe(f(()=>s(k(this.path))),f(()=>this.watchDir(this.path).pipe(i(e=>!e.filename.endsWith(".json")),u(null),f(()=>s(v.readdir(this.path,{recursive:!0,withFileTypes:!0}))),r(e=>Array.from(new Set(e.filter(e=>e.isDirectory()).map(e=>x.relative(this.path,x.join(e.parentPath,e.name)).replace(/^\.$/,"")).values()))))))}watch(){D();const e=s(this.loaded).pipe(f(()=>s(k(this.pathResources))),f(()=>this.watchDir(this.pathResources)),l(e=>{if(null==e||!["add","change","unlink"].includes(e.event))return a;const t=x.basename(e.filename);return s(this.getAll()).pipe(l(e=>{const i=e.filter(e=>e.fileKey===t);return 0===i.length?a:s(i).pipe(l(e=>{const t=this.privateObjectRelativePath(e);return s(this.readFileIfExists(t)).pipe(r(a=>({event:"change",object:a,path:e.path,filename:t})))}))}))})),t=s(this.loaded).pipe(f(()=>this.watchDir(this.path)),i(e=>"_meta.json"!==e.filename),l(e=>{const t={event:e.event,path:x.dirname(e.filename).replace(/^\.$/,""),filename:x.basename(e.filename)};return e.filename.endsWith(".json")?"unlink"!==e.event?s(this.readFileIfExists(e.filename)).pipe(w(e=>{null!=e&&(null==this.cachedIndex&&(this.cachedIndex={}),this.cachedIndex[e.id]={id:e.id,name:e.name??e.id,path:e.path},this.persistIndex())}),r(e=>({object:e,...t}))):d({object:null,...t}).pipe(w(()=>{const t=Object.keys(this.cachedIndex??{}).find(t=>this.privateObjectRelativePath(this.cachedIndex[t])===e.filename);t&&this.removeIndexEntry(t)})):"change"===e.event?s(this.reloadSubdirectory(e.filename)).pipe(o(),r(e=>({object:e,...t}))):a}));return h(t,e)}async reloadSubdirectory(e){return(await this.getAll(e)).filter(t=>t.path.startsWith(e))}async readFileIfExists(e){const t=E(this.path,e);for(let a=0;a<3;a++)try{const a=await O(t);return{...JSON.parse(a.toString()),path:x.dirname(e).replace(/^\.$/,""),filename:x.basename(e)}}catch(e){if(2===a)return console.error("Could not read file at "+t,e),null;await m(50*(a+1))}return null}async getAll(e){if(j){await this.loaded,await k(this.path);const t=await this.getObjectFilePaths(e??""),a=null==e||""===e,i=[];for(let e=0;e<t.length;e+=32){const a=Math.min(32,t.length-e),n=await Promise.all(Array.from({length:a},(a,i)=>{const n=t[e+i];return O(E(this.path,n)).then(e=>({...JSON.parse(e.toString()),path:x.dirname(n).replace(/^\.$/,""),filename:x.basename(n)}))}));i.push(...n)}return a&&(this.cachedIndex=R(i),await this.persistIndex()),i}const t=await this.loadIndex();return Promise.all(Object.keys(t).map(e=>this.get(e)))}async getObjectFilePaths(e){const t=this.path,a=x.join(t,e),i=[];return await async function e(a){const n=await v.readdir(a,{withFileTypes:!0});for(const s of n){const n=x.join(a,s.name);s.isDirectory()?await e(n):s.isFile()&&s.name.endsWith(".json")&&!/^[\._]/.test(s.name)&&i.push(x.relative(t,n))}}(a),i}async get(e){const t=await this.loadIndex(),a=t[e]??Object.values(t).find(t=>t.name===e);if(null==a)return;const i=this.privateObjectPath(a);if(!j)return(await fetch(i)).json();return await z(i)?{...JSON.parse((await O(i)).toString()),path:a.path??"",filename:x.basename(this.privateObjectRelativePath(a))}:null}async save(e){if(D(),await this.loaded,null==e.path){const t=(await this.loadIndex())[e.id];null!=t?.path&&(e={...e,path:t.path})}return await this.assertObjectFilePathAvailable(e,e.id),await v.writeFile(this.privateObjectPath(e),this.serialize(e)),await this.addOrUpdateIndexEntry(e),e}async rename(e,t){const a={...e,name:t};await this.assertObjectFilePathAvailable(a,e.id);const i=this.privateObjectPath(e),n=this.privateObjectPath(a);try{await v.rename(i,n)}catch(e){console.error(e),console.warn("Rename failed, retrying",{currentPath:i,newPath:n}),await m(400),await v.rename(i,n)}return await this.save(a),a}async delete(e){await v.unlink(this.privateObjectPath(e)),this.removeIndexEntry(e.id)}async create(t){D(),await this.loaded,t.id=e();const a=this.privateObjectPath(t);if(await this.assertObjectFilePathAvailable(t),await U(a))throw Error(`Can not create because a file already exists at ${a}`);return await v.writeFile(a,this.serialize(t)),await this.addOrUpdateIndexEntry(t),t}prepareCreate(t){return D(),t.id=e(),t}serialize(e){return this.serializer(e)}async addOrUpdateIndexEntry(e){null==this.cachedIndex&&await this.loadIndex(),this.cachedIndex[e.id]={id:e.id,name:e.name??e.id,path:e.path},await this.persistIndex()}removeIndexEntry(e){null!=this.cachedIndex&&delete this.cachedIndex[e],this.persistIndex()}async persistIndex(){j&&this.shouldCreateIndex&&null!=this.cachedIndex&&await v.writeFile(this.indexFilePath,JSON.stringify(this.cachedIndex,null,2))}get indexFilePath(){return E(this.path,"_meta.json")}async loadIndex(){if(null!=this.cachedIndex)return this.cachedIndex;this.loadingIndex??(this.loadingIndex=this.createIndex());try{return await this.loadingIndex}finally{this.loadingIndex=null}}async createIndex(){if(j){const e=await this.getAll();this.cachedIndex=R(e),await this.persistIndex()}else this.cachedIndex=await(await fetch(this.indexFilePath)).json();return this.cachedIndex}async ensureResourceDir(){await k(E(this.path+"-resources"))}async saveFile(e,t){return D(),await k(E(this.path+"-resources")),v.copyFile(t.path,E(this.path+"-resources",e.fileKey))}async saveExtraFile(e,t){return D(),await k(E(this.path+"-resources")),v.copyFile(e,E(this.path+"-resources",t))}getAssetPath(e){return window&&"function"==typeof window.require?window.require("path").join(this.path+"-resources",e.fileKey):E(this.path+"-resources",e.fileKey)}async replaceFile(e,t){if(await z(t))return v.copyFile(t,E(this.path+"-resources",e.fileKey));console.error("Failed to replace file using path "+t)}async deleteFile(e){if(null==e)return;D();const t=E(this.path+"-resources",e);return await z(t)?v.unlink(t):void 0}privateObjectPath(e){return E(this.path,this.privateObjectRelativePath(e))}privateObjectRelativePath(e){return this.filePathFn?E(e.path??"",this.filePathFn(e)):E(e.path??"",tokenizeName(e.name??e.id)+".json")}async assertObjectFilePathAvailable(e,t){const a=await this.findObjectFilePathCollision(e,t);if(null!=a)throw Error(`Can not save "${e.name??e.id}" because "${a.name}" already uses the same storage file name. Use a name that differs beyond letter casing.`)}async findObjectFilePathCollision(e,t){const a=A(this.privateObjectRelativePath(e)),i=await this.loadIndex();return Object.values(i).find(e=>e.id!==t&&A(this.privateObjectRelativePath(e))===a)??null}watchDir(e){return new c(t=>{if("win32"===process.platform&&j){const a=y.watch(e,{recursive:!0},(a,i)=>{if(i){const n=x.join(e,i);y.access(n,y.constants.F_OK,e=>{const n=e?"unlink":"rename"===a?"add":"change";t.next({event:n,filename:i})})}});return()=>a.close()}const a=b.watch(e,{cwd:e,ignoreInitial:!0,disableGlobbing:!0});return a.on("all",(e,a,i)=>{t.next({event:e,filename:a})}),a.on("error",()=>{}),()=>{a.close()}})}}export function tokenizeName(e){return e.trim().replace(/\s/g,"_").replace(/[^a-z0-9_\-\.]/gi,"")}function A(e){return e.replace(/\\/g,"/").toLowerCase()}function R(e){const t={};for(const a of e)t[a.id]={id:a.id,name:a.name??a.id,path:a.path};return t}async function k(e){j&&(await z(e)||await v.mkdir(e,{recursive:!0}))}function z(e){return!!j&&new Promise(function(t,a){y.exists(e,function(e){t(e)})})}function D(){if(!j)throw new Error("Must have direct access to filesystem")}async function U(e){try{await v.access(e,v.constants.F_OK)}catch(e){return!1}return!0}export function defaultSerializer(e){const t={...e};return delete t.path,delete t.filename,JSON.stringify(t,null,2)}/*
1
+ import{randomUUID as e}from"../../utils/uuid.js";import{pathJoin as t}from"../../utils/files.js";import{EMPTY as a,filter as i,firstValueFrom as n,from as s,map as r,merge as h,mergeAll as o,mergeMap as l,Observable as c,of as d,startWith as u,Subject as p,switchMap as f,tap as w}from"rxjs";import{sleepDelay as m}from"../../utils/async.js";import{parseStorageJson as y,serializeStorageJson as v}from"./packed-json.js";export{decodeStorageValue,parseStorageJson,serializeStorageJson}from"./packed-json.js";const x={},j={},b={},g={};null==x.read&&window.require&&(Object.assign(x,window.require("fs")),Object.assign(j,x.promises),Object.assign(b,window.require("path")),Object.assign(g,window.require("chokidar")));const I=null!=x.existsSync;let P=0,F=0;const O=[];async function S(e){await function(){if(P<32)return P++,Promise.resolve();return new Promise(e=>O.push(e))}();try{return await e()}finally{!function(){const e=O[F];if(null==e)return O.length=0,F=0,void P--;F++,F>64&&2*F>O.length&&(O.splice(0,F),F=0);e()}()}}function C(e){return S(()=>j.readFile(e))}function E(){if(I){const e="--path=",t=window.process.argv.find(t=>t.startsWith(e));return t?t.substring(e.length):""}return""}const A=/^[A-Z]:/;function k(...e){return 0===e.length?"":A.test(e[0])||I?b.join(...e):t(...e)}export class ObjectStorage{get pathResources(){return k(this.path+"-resources")}constructor(e,t,a=defaultSerializer){this.name=e,this.filePathFn=t,this.serializer=a,this.basePathUpdates=new p,this.basePath=this.basePathUpdates.pipe(r(e=>k(E(),e)),w(e=>{this.path=k(e,this.name),this.cachedIndex=null,this.loadingIndex=null,D(this.path),this.determineIfMetaFileShouldBeCreated()})),this.loaded=n(this.basePath),this.cachedIndex=null,this.loadingIndex=null,this.shouldCreateIndex=!1}setBasePath(e){this.basePathUpdates.next(e)}async determineIfMetaFileShouldBeCreated(){if(I)try{const e=["vite.renderer.config.ts","vite.config.ts"];for(const t of e){const e=k(E(),t);if(!await U(e))continue;if((await C(e)).toString().includes("hologyBuild"))return void(this.shouldCreateIndex=!1)}this.shouldCreateIndex=!0}catch(e){console.warn("Failed to read vite config to determine if meta files should be created")}}async createFolder(e,t=""){K(),await j.mkdir(b.join(this.path,t,e),{recursive:!0})}async deleteFolderForceDangerous(e){await j.rm(b.join(this.path,e),{recursive:!0,force:!0})}async moveFolder(e,t){if(K(),""==e)return void console.warn("Can not move a folder in root");const a=b.resolve(b.join(this.path,t),b.basename(e));if(await $(a)){if(a!==b.resolve(this.path,e))throw new Error("Can not move to directory as a file already exists with the same name")}else{if(function(e,t){const a=b.resolve(e),i=b.resolve(t),n=b.normalize(a)+b.sep;return(b.normalize(i)+b.sep).startsWith(n)}(b.join(this.path,e),b.join(this.path,t)))throw new Error("Can not move a folder into a folder it contains");await j.rename(b.join(this.path,e),a)}}async renameFolder(e,t){await j.rename(b.join(this.path,e),b.resolve(b.dirname(b.join(this.path,e)),t))}async moveToFolder(e,t){if(K(),(e.path??"")===t)return e;const a={...e,path:t};await this.assertObjectFilePathAvailable(a,e.id);const i=this.privateObjectPath(a);return await j.rename(this.privateObjectPath(e),i),await this.addOrUpdateIndexEntry(a),a}getAbsolutePath(e=""){return b.join(this.path,e)}getResourceAbsolutePath(e=""){return b.join(this.pathResources,e)}watchFolders(){return s(this.loaded).pipe(f(()=>s(D(this.path))),f(()=>this.watchDir(this.path).pipe(i(e=>!e.filename.endsWith(".json")),u(null),f(()=>s(j.readdir(this.path,{recursive:!0,withFileTypes:!0}))),r(e=>Array.from(new Set(e.filter(e=>e.isDirectory()).map(e=>b.relative(this.path,b.join(e.parentPath,e.name)).replace(/^\.$/,"")).values()))))))}watch(){K();const e=s(this.loaded).pipe(f(()=>s(D(this.pathResources))),f(()=>this.watchDir(this.pathResources)),l(e=>{if(null==e||!["add","change","unlink"].includes(e.event))return a;const t=b.basename(e.filename);return s(this.getAll()).pipe(l(e=>{const i=e.filter(e=>e.fileKey===t);return 0===i.length?a:s(i).pipe(l(e=>{const t=this.privateObjectRelativePath(e);return s(this.readFileIfExists(t)).pipe(r(a=>({event:"change",object:a,path:e.path,filename:t})))}))}))})),t=s(this.loaded).pipe(f(()=>this.watchDir(this.path)),i(e=>"_meta.json"!==e.filename),l(e=>{const t={event:e.event,path:b.dirname(e.filename).replace(/^\.$/,""),filename:b.basename(e.filename)};return e.filename.endsWith(".json")?"unlink"!==e.event?s(this.readFileIfExists(e.filename)).pipe(w(e=>{null!=e&&(null==this.cachedIndex&&(this.cachedIndex={}),this.cachedIndex[e.id]={id:e.id,name:e.name??e.id,path:e.path},this.persistIndex())}),r(e=>({object:e,...t}))):d({object:null,...t}).pipe(w(()=>{const t=Object.keys(this.cachedIndex??{}).find(t=>this.privateObjectRelativePath(this.cachedIndex[t])===e.filename);t&&this.removeIndexEntry(t)})):"change"===e.event?s(this.reloadSubdirectory(e.filename)).pipe(o(),r(e=>({object:e,...t}))):a}));return h(t,e)}async reloadSubdirectory(e){return(await this.getAll(e)).filter(t=>t.path.startsWith(e))}async readFileIfExists(e){const t=k(this.path,e);for(let a=0;a<3;a++)try{const a=await C(t);return{...y(a.toString()),path:b.dirname(e).replace(/^\.$/,""),filename:b.basename(e)}}catch(e){if(2===a)return console.error("Could not read file at "+t,e),null;await m(50*(a+1))}return null}async getAll(e){if(I){await this.loaded,await D(this.path);const t=await this.getObjectFilePaths(e??""),a=null==e||""===e,i=[];for(let e=0;e<t.length;e+=32){const a=Math.min(32,t.length-e),n=await Promise.all(Array.from({length:a},(a,i)=>{const n=t[e+i];return C(k(this.path,n)).then(e=>({...y(e.toString()),path:b.dirname(n).replace(/^\.$/,""),filename:b.basename(n)}))}));i.push(...n)}return a&&(this.cachedIndex=z(i),await this.persistIndex()),i}const t=await this.loadIndex();return Promise.all(Object.keys(t).map(e=>this.get(e)))}async getObjectFilePaths(e){const t=this.path,a=b.join(t,e),i=[];return await async function e(a){const n=await j.readdir(a,{withFileTypes:!0});for(const s of n){const n=b.join(a,s.name);s.isDirectory()?await e(n):s.isFile()&&s.name.endsWith(".json")&&!/^[\._]/.test(s.name)&&i.push(b.relative(t,n))}}(a),i}async get(e){const t=await this.loadIndex(),a=t[e]??Object.values(t).find(t=>t.name===e);if(null==a)return;const i=this.privateObjectPath(a);if(!I)return y(await(await fetch(i)).text());return await U(i)?{...y((await C(i)).toString()),path:a.path??"",filename:b.basename(this.privateObjectRelativePath(a))}:null}async save(e){if(K(),await this.loaded,null==e.path){const t=(await this.loadIndex())[e.id];null!=t?.path&&(e={...e,path:t.path})}return await this.assertObjectFilePathAvailable(e,e.id),await j.writeFile(this.privateObjectPath(e),this.serialize(e)),await this.addOrUpdateIndexEntry(e),e}async rename(e,t){const a={...e,name:t};await this.assertObjectFilePathAvailable(a,e.id);const i=this.privateObjectPath(e),n=this.privateObjectPath(a);try{await j.rename(i,n)}catch(e){console.error(e),console.warn("Rename failed, retrying",{currentPath:i,newPath:n}),await m(400),await j.rename(i,n)}return await this.save(a),a}async delete(e){await j.unlink(this.privateObjectPath(e)),this.removeIndexEntry(e.id)}async create(t){K(),await this.loaded,t.id=e();const a=this.privateObjectPath(t);if(await this.assertObjectFilePathAvailable(t),await $(a))throw Error(`Can not create because a file already exists at ${a}`);return await j.writeFile(a,this.serialize(t)),await this.addOrUpdateIndexEntry(t),t}prepareCreate(t){return K(),t.id=e(),t}serialize(e){return this.serializer(e)}async addOrUpdateIndexEntry(e){null==this.cachedIndex&&await this.loadIndex(),this.cachedIndex[e.id]={id:e.id,name:e.name??e.id,path:e.path},await this.persistIndex()}removeIndexEntry(e){null!=this.cachedIndex&&delete this.cachedIndex[e],this.persistIndex()}async persistIndex(){I&&this.shouldCreateIndex&&null!=this.cachedIndex&&await j.writeFile(this.indexFilePath,JSON.stringify(this.cachedIndex,null,2))}get indexFilePath(){return k(this.path,"_meta.json")}async loadIndex(){if(null!=this.cachedIndex)return this.cachedIndex;this.loadingIndex??(this.loadingIndex=this.createIndex());try{return await this.loadingIndex}finally{this.loadingIndex=null}}async createIndex(){if(I){const e=await this.getAll();this.cachedIndex=z(e),await this.persistIndex()}else this.cachedIndex=await(await fetch(this.indexFilePath)).json();return this.cachedIndex}async ensureResourceDir(){await D(k(this.path+"-resources"))}async saveFile(e,t){return K(),await D(k(this.path+"-resources")),j.copyFile(t.path,k(this.path+"-resources",e.fileKey))}async saveExtraFile(e,t){return K(),await D(k(this.path+"-resources")),j.copyFile(e,k(this.path+"-resources",t))}getAssetPath(e){return window&&"function"==typeof window.require?window.require("path").join(this.path+"-resources",e.fileKey):k(this.path+"-resources",e.fileKey)}async replaceFile(e,t){if(await U(t))return j.copyFile(t,k(this.path+"-resources",e.fileKey));console.error("Failed to replace file using path "+t)}async deleteFile(e){if(null==e)return;K();const t=k(this.path+"-resources",e);return await U(t)?j.unlink(t):void 0}privateObjectPath(e){return k(this.path,this.privateObjectRelativePath(e))}privateObjectRelativePath(e){return this.filePathFn?k(e.path??"",this.filePathFn(e)):k(e.path??"",tokenizeName(e.name??e.id)+".json")}async assertObjectFilePathAvailable(e,t){const a=await this.findObjectFilePathCollision(e,t);if(null!=a)throw Error(`Can not save "${e.name??e.id}" because "${a.name}" already uses the same storage file name. Use a name that differs beyond letter casing.`)}async findObjectFilePathCollision(e,t){const a=R(this.privateObjectRelativePath(e)),i=await this.loadIndex();return Object.values(i).find(e=>e.id!==t&&R(this.privateObjectRelativePath(e))===a)??null}watchDir(e){return new c(t=>{if("win32"===process.platform&&I){const a=x.watch(e,{recursive:!0},(a,i)=>{if(i){const n=b.join(e,i);x.access(n,x.constants.F_OK,e=>{const n=e?"unlink":"rename"===a?"add":"change";t.next({event:n,filename:i})})}});return()=>a.close()}const a=g.watch(e,{cwd:e,ignoreInitial:!0,disableGlobbing:!0});return a.on("all",(e,a,i)=>{t.next({event:e,filename:a})}),a.on("error",()=>{}),()=>{a.close()}})}}export function tokenizeName(e){return e.trim().replace(/\s/g,"_").replace(/[^a-z0-9_\-\.]/gi,"")}function R(e){return e.replace(/\\/g,"/").toLowerCase()}function z(e){const t={};for(const a of e)t[a.id]={id:a.id,name:a.name??a.id,path:a.path};return t}async function D(e){I&&(await U(e)||await j.mkdir(e,{recursive:!0}))}function U(e){return!!I&&new Promise(function(t,a){x.exists(e,function(e){t(e)})})}function K(){if(!I)throw new Error("Must have direct access to filesystem")}async function $(e){try{await j.access(e,j.constants.F_OK)}catch(e){return!1}return!0}export function defaultSerializer(e){const t={...e};return delete t.path,delete t.filename,v(t)}/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -0,0 +1,28 @@
1
+ export declare const SECONDARY_MOTION_BINARY_VERSION = 1;
2
+ export declare const enum SecondaryMotionConstraintType {
3
+ Distance = 0,
4
+ Bend = 1,
5
+ Area = 2,
6
+ Attachment = 3
7
+ }
8
+ export interface SecondaryMotionGeneratedRegion {
9
+ id: string;
10
+ topologyHash: string;
11
+ particlePositions: Float32Array;
12
+ particleInverseMass: Float32Array;
13
+ particleSkinIndices: Uint16Array;
14
+ particleSkinWeights: Float32Array;
15
+ constraints: Uint32Array;
16
+ constraintRest: Float32Array;
17
+ constraintColors: Uint16Array;
18
+ renderVertexIndices: Uint32Array;
19
+ renderBindings: Float32Array;
20
+ influence: Float32Array;
21
+ }
22
+ export interface SecondaryMotionGeneratedData {
23
+ version: typeof SECONDARY_MOTION_BINARY_VERSION;
24
+ regions: SecondaryMotionGeneratedRegion[];
25
+ }
26
+ export declare function encodeSecondaryMotionData(data: SecondaryMotionGeneratedData): ArrayBuffer;
27
+ export declare function decodeSecondaryMotionData(buffer: ArrayBuffer): SecondaryMotionGeneratedData;
28
+ //# sourceMappingURL=data.d.ts.map
@@ -0,0 +1,4 @@
1
+ export const SECONDARY_MOTION_BINARY_VERSION=1;const n=1213026131;export function encodeSecondaryMotionData(r){if(1!==r.version)throw new Error(`Unsupported secondary motion version ${r.version}`);const o=new TextEncoder,i=r.regions.map(n=>({region:n,id:o.encode(n.id),hash:o.encode(n.topologyHash)}));let s=16;for(const{region:n,id:e,hash:r}of i)s+=56+t(e.length)+t(r.length),s+=n.particlePositions.byteLength+n.particleInverseMass.byteLength,s+=n.particleSkinIndices.byteLength+n.particleSkinWeights.byteLength,s+=n.constraints.byteLength+n.constraintRest.byteLength+n.constraintColors.byteLength,s+=n.renderVertexIndices.byteLength+n.renderBindings.byteLength+n.influence.byteLength;const a=new ArrayBuffer(s),c=new DataView(a);let l=0;[n,r.version,r.regions.length,s].forEach(n=>{c.setUint32(l,n,!0),l+=4});for(const{region:n,id:r,hash:o}of i){const i=[r.length,o.length,n.particlePositions.length,n.particleInverseMass.length,n.particleSkinIndices.length,n.particleSkinWeights.length,n.constraints.length,n.constraintRest.length,n.constraintColors.length,n.renderVertexIndices.length,n.renderBindings.length,n.influence.length,0,0];for(const n of i)c.setUint32(l,n,!0),l+=4;new Uint8Array(a,l,r.length).set(r),l+=t(r.length),new Uint8Array(a,l,o.length).set(o),l+=t(o.length),l=e(a,l,n.particlePositions),l=e(a,l,n.particleInverseMass),l=e(a,l,n.particleSkinIndices),l=e(a,l,n.particleSkinWeights),l=e(a,l,n.constraints),l=e(a,l,n.constraintRest),l=e(a,l,n.constraintColors),l=e(a,l,n.renderVertexIndices),l=e(a,l,n.renderBindings),l=e(a,l,n.influence)}return a}export function decodeSecondaryMotionData(e){const o=new DataView(e);if(e.byteLength<16||o.getUint32(0,!0)!==n)throw new Error("Invalid secondary motion binary");const i=o.getUint32(4,!0);if(1!==i)throw new Error(`Unsupported secondary motion version ${i}`);const s=o.getUint32(8,!0);if(o.getUint32(12,!0)!==e.byteLength)throw new Error("Truncated secondary motion binary");const a=new TextDecoder,c=[];let l=16;for(let n=0;n<s;n++){r(e,l,56);const n=[];for(let e=0;e<14;e++)n.push(o.getUint32(l,!0)),l+=4;const i=n=>{r(e,l,t(n));const o=a.decode(new Uint8Array(e,l,n));return l+=t(n),o},s=i(n[0]),h=i(n[1]),d=g(Float32Array,n[2]),y=g(Float32Array,n[3]),f=g(Uint16Array,n[4]),p=g(Float32Array,n[5]),w=g(Uint32Array,n[6]),u=g(Float32Array,n[7]),b=g(Uint16Array,n[8]),U=g(Uint32Array,n[9]),A=g(Float32Array,n[10]),L=g(Float32Array,n[11]);c.push({id:s,topologyHash:h,particlePositions:d,particleInverseMass:y,particleSkinIndices:f,particleSkinWeights:p,constraints:w,constraintRest:u,constraintColors:b,renderVertexIndices:U,renderBindings:A,influence:L})}if(l!==e.byteLength)throw new Error("Unexpected trailing secondary motion data");return{version:1,regions:c};function g(n,t){const o=t*n.BYTES_PER_ELEMENT;r(e,l,o);const i=e.slice(l,l+o);return l+=o,new n(i)}}function e(n,e,t){return new Uint8Array(n,e,t.byteLength).set(new Uint8Array(t.buffer,t.byteOffset,t.byteLength)),e+t.byteLength}function t(n){return n+3&-4}function r(n,e,t){if(e<0||t<0||e+t>n.byteLength)throw new Error("Truncated secondary motion binary")}/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -0,0 +1,22 @@
1
+ import type { SecondaryMotionGeneratedRegion } from './data.js';
2
+ export interface MeshTopology {
3
+ positions: Float32Array;
4
+ indices: Uint32Array | Uint16Array;
5
+ }
6
+ export interface PaintedIsland {
7
+ vertices: Uint32Array;
8
+ triangles: Uint32Array;
9
+ rootVertices: Uint32Array;
10
+ }
11
+ /** Builds welded logical cloth particles and maps split render vertices back to them. */
12
+ export declare function generateClothSecondaryMotionRegion(id: string, topology: MeshTopology, influence: Float32Array, particleSkinIndices?: Uint16Array, particleSkinWeights?: Float32Array): SecondaryMotionGeneratedRegion;
13
+ /** Stable FNV-1a fingerprint of topology and quantized rest positions. */
14
+ export declare function secondaryMotionTopologyHash(topology: MeshTopology): string;
15
+ /** Extracts triangle-connected painted islands. Roots are painted boundary vertices at or below rootThreshold. */
16
+ export declare function extractPaintedIslands(topology: MeshTopology, influence: Float32Array, threshold?: number, rootThreshold?: number): PaintedIsland[];
17
+ /** Generates a centerline by weighted shortest paths from the painted root and resamples it uniformly. */
18
+ export declare function generateStrandCenterline(topology: MeshTopology, island: PaintedIsland, particleCount: number): Float32Array;
19
+ export declare function resamplePolyline(points: Float32Array, count: number): Float32Array;
20
+ /** Greedy coloring; constraints contain fixed-width particle indices. */
21
+ export declare function colorConstraints(constraints: Uint32Array, arity?: number): Uint16Array;
22
+ //# sourceMappingURL=generation.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{Vector3 as t}from"three";export function generateClothSecondaryMotionRegion(t,e,n,o,r){const s=e.positions.length/3;if(!Number.isInteger(s)||n.length!==s)throw new Error("Secondary-motion influence count must match mesh vertex count");const i=new Map,a=new Uint32Array(s),c=[],l=[],h=[];for(let t=0;t<s;t++){const o=`${Math.round(1e4*e.positions[3*t])}|${Math.round(1e4*e.positions[3*t+1])}|${Math.round(1e4*e.positions[3*t+2])}`;let r=i.get(o);null==r&&(r=c.length/3,i.set(o,r),h.push(t),c.push(e.positions[3*t],e.positions[3*t+1],e.positions[3*t+2]),l.push(0)),a[t]=r,l[r]=Math.max(l[r],Math.max(0,Math.min(1,n[t])))}const f=l.length,d=Float32Array.from(c),g=Float32Array.from(l),p=new Map,u=new Map;for(let t=0;t+2<e.indices.length;t+=3){const n=[e.indices[t],e.indices[t+1],e.indices[t+2]];for(let t=0;t<3;t++){const e=n[t],o=n[(t+1)%3];if(e>=s||o>=s)throw new Error("Mesh index is outside the position attribute");const r=a[e],i=a[o];if(r===i)continue;if(Math.max(g[r],g[i])<=.001)continue;const c=Math.min(r,i),l=Math.max(r,i);p.set(`${c}|${l}`,[c,l]);const h=a[n[(t+2)%3]],f=`${c}|${l}`,d=u.get(f);if(null==d)u.set(f,h);else if(d!==h&&Math.max(g[d],g[h])>.001){const t=Math.min(d,h),e=Math.max(d,h);p.set(`${t}|${e}`,[t,e])}}}const w=new Uint32Array(2*p.size),m=new Float32Array(p.size);let y=0;for(const[t,e]of p.values()){w[2*y]=t,w[2*y+1]=e;const n=d[3*t],o=d[3*t+1],r=d[3*t+2],s=d[3*e]-n,i=d[3*e+1]-o,a=d[3*e+2]-r;m[y]=Math.sqrt(s*s+i*i+a*a),y++}const M=new Float32Array(4*s);for(let t=0;t<s;t++)M[4*t]=a[t];const A=new Uint16Array(4*f),x=new Float32Array(4*f);for(let t=0;t<f;t++){const e=h[t];o&&A.set(o.subarray(4*e,4*e+4),4*t),r&&x.set(r.subarray(4*e,4*e+4),4*t)}return{id:t,topologyHash:secondaryMotionTopologyHash(e),particlePositions:d,particleInverseMass:g,particleSkinIndices:A,particleSkinWeights:x,constraints:w,constraintRest:m,constraintColors:colorConstraints(w),renderVertexIndices:a,renderBindings:M,influence:g.slice()}}export function secondaryMotionTopologyHash(t){let e=2166136261;const n=t=>{e^=255&t,e=Math.imul(e,16777619)>>>0};for(const e of t.indices)n(e),n(e>>>8),n(e>>>16),n(e>>>24);for(const e of t.positions){const t=Math.round(1e5*e);n(t),n(t>>>8),n(t>>>16),n(t>>>24)}return e.toString(16).padStart(8,"0")}export function extractPaintedIslands(t,e,n=.001,o=.15){const r=t.positions.length/3;if(e.length!==r)throw new Error("Secondary motion influence count must match mesh vertex count");const s=Array.from({length:r},()=>new Set),i=[];for(let o=0;o+2<t.indices.length;o+=3){const a=t.indices[o],c=t.indices[o+1],l=t.indices[o+2];if(a>=r||c>=r||l>=r)throw new Error("Mesh index is outside the position attribute");Math.max(e[a],e[c],e[l])<=n||(i.push([a,c,l]),s[a].add(c).add(l),s[c].add(a).add(l),s[l].add(a).add(c))}const a=new Uint8Array(r),c=[];for(let t=0;t<r;t++){if(a[t]||e[t]<=n||0===s[t].size)continue;const r=[t],l=[];for(a[t]=1;r.length;){const t=r.pop();l.push(t);for(const o of s[t])!a[o]&&e[o]>n&&(a[o]=1,r.push(o))}const h=new Set(l),f=i.filter(t=>t.some(t=>h.has(t))).flat(),d=l.filter(t=>e[t]<=o||Array.from(s[t]).some(t=>e[t]<=n));0===d.length&&d.push(l.reduce((t,n)=>e[n]<e[t]?n:t,l[0])),c.push({vertices:Uint32Array.from(l.sort((t,e)=>t-e)),triangles:Uint32Array.from(f),rootVertices:Uint32Array.from(d)})}return c.sort((t,e)=>e.vertices.length-t.vertices.length)}export function generateStrandCenterline(e,n,o){if(o<2)throw new Error("A strand requires at least two particles");const r=new Set(n.vertices),s=new Map,i=(t,n)=>n.fromArray(e.positions,3*t),a=new t,c=new t;for(let t=0;t+2<n.triangles.length;t+=3){const e=[n.triangles[t],n.triangles[t+1],n.triangles[t+2]];for(let t=0;t<3;t++){const n=e[t],o=e[(t+1)%3];if(!r.has(n)||!r.has(o))continue;const l=i(n,a).distanceTo(i(o,c));s.has(n)||s.set(n,new Map),s.has(o)||s.set(o,new Map),s.get(n).set(o,Math.min(s.get(n).get(o)??1/0,l)),s.get(o).set(n,Math.min(s.get(o).get(n)??1/0,l))}}const l=Array.from(n.rootVertices),h=new Map,f=new Map;for(const t of n.vertices)h.set(t,1/0);for(const t of l)h.set(t,0);const d=new Set(n.vertices);for(;d.size;){let t=-1,e=1/0;for(const n of d)h.get(n)<e&&(t=n,e=h.get(n));if(t<0)break;d.delete(t);for(const[n,o]of s.get(t)??[]){if(!d.has(n))continue;const r=e+o;r<h.get(n)&&(h.set(n,r),f.set(n,t))}}let g=n.vertices[0];for(const t of n.vertices)(h.get(t)??-1)>(h.get(g)??-1)&&(g=t);const p=[g];for(;f.has(p[p.length-1]);)p.push(f.get(p[p.length-1]));if(p.reverse(),p.length<2)throw new Error("Unable to find a strand path through the painted island");return resamplePolyline(Float32Array.from(p.flatMap(t=>Array.from(e.positions.slice(3*t,3*t+3)))),o)}export function resamplePolyline(e,n){if(e.length<6||e.length%3!=0||n<2)throw new Error("Invalid strand polyline");const o=[0],r=new t,s=new t;for(let t=1;t<e.length/3;t++)o.push(o[t-1]+r.fromArray(e,3*(t-1)).distanceTo(s.fromArray(e,3*t)));const i=o[o.length-1];if(i<=1e-8)throw new Error("Strand polyline has zero length");const a=new Float32Array(3*n);let c=0;for(let t=0;t<n;t++){const l=i*t/(n-1);for(;c+1<o.length-1&&o[c+1]<l;)c++;const h=o[c+1]-o[c],f=h>0?(l-o[c])/h:0;r.fromArray(e,3*c).lerp(s.fromArray(e,3*(c+1)),f).toArray(a,3*t)}return a}export function colorConstraints(t,e=2){if(e<1||t.length%e!==0)throw new Error("Invalid constraint arity");const n=new Uint16Array(t.length/e),o=new Map;for(let r=0;r<n.length;r++){const s=new Set;for(let n=0;n<e;n++)for(const i of o.get(t[r*e+n])??[])s.add(i);let i=0;for(;s.has(i);)i++;if(i>65535)throw new Error("Too many secondary motion constraint colors");n[r]=i;for(let n=0;n<e;n++){const s=t[r*e+n];o.has(s)||o.set(s,new Set),o.get(s).add(i)}}return n}/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -0,0 +1,111 @@
1
+ import { type RendererCustomPassContext } from '@hology/webgpu-renderer';
2
+ import type { BufferGeometry } from 'three';
3
+ import type { ExperimentalWebGpuRenderGraphExtension } from '../rendering/webgpu-experimental-backend.js';
4
+ import type { SecondaryMotionGeneratedRegion } from './data.js';
5
+ export interface SecondaryMotionComputeRuntime {
6
+ execute(context: RendererCustomPassContext): void;
7
+ dispose(): void;
8
+ }
9
+ export interface WebGpuSecondaryMotionOptions extends SecondaryMotionFixedStepOptions {
10
+ gravity?: readonly [number, number, number];
11
+ damping?: number;
12
+ attachment?: number;
13
+ constraintIterations?: number;
14
+ constraintStiffness?: number;
15
+ autoAdvance?: boolean;
16
+ }
17
+ export interface SecondaryMotionFrameMotion {
18
+ linearAcceleration?: readonly [number, number, number];
19
+ angularVelocity?: readonly [number, number, number];
20
+ angularAcceleration?: readonly [number, number, number];
21
+ }
22
+ export interface SecondaryMotionSphereCollider {
23
+ readonly center: readonly [number, number, number];
24
+ readonly radius: number;
25
+ }
26
+ export interface SecondaryMotionEllipsoidCollider {
27
+ readonly center: readonly [number, number, number];
28
+ readonly radii: readonly [number, number, number];
29
+ readonly rotation?: readonly [number, number, number, number];
30
+ }
31
+ export interface WebGpuSecondaryMotionRegionHandle {
32
+ readonly id: string;
33
+ readonly particleCount: number;
34
+ readonly renderVertexCount: number;
35
+ readonly positionBuffer: GPUBuffer;
36
+ setRenderGeometry(geometry: BufferGeometry | null): void;
37
+ updateTargets(positions: Float32Array): void;
38
+ reset(positions?: Float32Array): void;
39
+ dispose(): void;
40
+ }
41
+ /**
42
+ * First-stage GPU secondary-motion solver. Positions use vec4 storage so the
43
+ * same buffers can later be consumed directly by render/skinning pipelines.
44
+ * Distance constraints are dispatched one graph color at a time, avoiding
45
+ * atomics and write races between constraints sharing a particle.
46
+ */
47
+ export declare class WebGpuSecondaryMotionRuntime implements SecondaryMotionComputeRuntime {
48
+ private readonly device;
49
+ private readonly fixedStep;
50
+ private readonly integrationKernel;
51
+ private readonly constraintKernel;
52
+ private readonly packKernel;
53
+ private readonly finalizeKernel;
54
+ private readonly collisionKernel;
55
+ private readonly integrationParams;
56
+ private readonly collisionSpheres;
57
+ private readonly previousCollisionSpheres;
58
+ private readonly collisionParams;
59
+ private readonly collisionColliderData;
60
+ private collisionCollidersInitialized;
61
+ private readonly collisionBindGroups;
62
+ private readonly integrationParamsData;
63
+ private readonly integrationParamsView;
64
+ private readonly regions;
65
+ private pendingSteps;
66
+ private disposed;
67
+ private readonly constraintIterations;
68
+ private readonly constraintStiffness;
69
+ private readonly autoAdvance;
70
+ private lastExecuteSeconds;
71
+ constructor(device: GPUDevice, options?: WebGpuSecondaryMotionOptions);
72
+ /** Updates non-inertial reference-frame motion before the next render. */
73
+ setFrameMotion(motion: SecondaryMotionFrameMotion): void;
74
+ /** Sets up to eight mesh-local spheres used to keep simulated particles outside the body. */
75
+ setSphereColliders(colliders: readonly SecondaryMotionSphereCollider[]): void;
76
+ /** Sets up to eight mesh-local ellipsoids and enables swept particle collision. */
77
+ setEllipsoidColliders(colliders: readonly SecondaryMotionEllipsoidCollider[]): void;
78
+ /** Queues fixed simulation steps; call once with the frame delta before rendering. */
79
+ advance(deltaSeconds: number): number;
80
+ addRegion(data: SecondaryMotionGeneratedRegion): WebGpuSecondaryMotionRegionHandle;
81
+ execute(context: RendererCustomPassContext): void;
82
+ dispose(): void;
83
+ private resetRegion;
84
+ private writeTargets;
85
+ private removeRegion;
86
+ private createStorageBuffer;
87
+ private getCollisionBindGroup;
88
+ private assertActive;
89
+ private writeFrameVector;
90
+ }
91
+ /** Installs secondary-motion compute before every geometry pass in the WebGPU frame graph. */
92
+ export declare function createSecondaryMotionRenderGraphExtension(runtime: SecondaryMotionComputeRuntime, passName?: string): ExperimentalWebGpuRenderGraphExtension;
93
+ export interface SecondaryMotionFixedStepOptions {
94
+ frequency?: 30 | 60 | 120;
95
+ maxSubsteps?: number;
96
+ maxFrameDelta?: number;
97
+ }
98
+ /** Allocation-free fixed-step state shared by editor preview and gameplay. */
99
+ export declare class SecondaryMotionFixedStep {
100
+ private accumulator;
101
+ readonly step: number;
102
+ readonly maxSubsteps: number;
103
+ readonly maxFrameDelta: number;
104
+ constructor(options?: SecondaryMotionFixedStepOptions);
105
+ consume(deltaSeconds: number): number;
106
+ reset(): void;
107
+ }
108
+ export type SecondaryMotionLod = 'hero' | 'near' | 'mid' | 'far' | 'sleep';
109
+ export declare function selectSecondaryMotionLod(distance: number, visible: boolean, hero: boolean): SecondaryMotionLod;
110
+ export declare function secondaryMotionTransformDiscontinuous(previous: ArrayLike<number>, current: ArrayLike<number>, translationThreshold?: number, rotationDotThreshold?: number): boolean;
111
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{ComputeKernel as e}from"@hology/webgpu-renderer";const t=64;export class WebGpuSecondaryMotionRuntime{constructor(t,n={}){this.device=t,this.collisionColliderData=new Float32Array(96),this.collisionCollidersInitialized=!1,this.collisionBindGroups=new Map,this.integrationParamsData=new ArrayBuffer(80),this.integrationParamsView=new DataView(this.integrationParamsData),this.regions=new Map,this.pendingSteps=0,this.disposed=!1,this.lastExecuteSeconds=null,this.fixedStep=new SecondaryMotionFixedStep(n),this.constraintIterations=Math.max(0,Math.trunc(n.constraintIterations??4)),this.constraintStiffness=Math.max(0,Math.min(1,n.constraintStiffness??.9)),this.autoAdvance=n.autoAdvance??!0,this.integrationKernel=new e(t,{label:"secondary-motion-integrate",code:o,bindGroupLayoutEntries:[i(0,!0),i(1,!1),i(2,!1),i(3,!0),i(4,!0),{binding:5,visibility:GPUShaderStage.COMPUTE,buffer:{type:"uniform"}}]}),this.constraintKernel=new e(t,{label:"secondary-motion-distance-constraints",code:s,bindGroupLayoutEntries:[i(0,!1),i(1,!0),i(2,!0),i(3,!0),{binding:4,visibility:GPUShaderStage.COMPUTE,buffer:{type:"uniform"}}]}),this.packKernel=new e(t,{label:"secondary-motion-pack-render-positions",code:a,bindGroupLayoutEntries:[i(0,!0),i(1,!0),i(2,!1)]}),this.finalizeKernel=new e(t,{label:"secondary-motion-finalize-velocity",code:l,bindGroupLayoutEntries:[i(0,!0),i(1,!0),i(2,!1),{binding:3,visibility:GPUShaderStage.COMPUTE,buffer:{type:"uniform"}}]}),this.collisionKernel=new e(t,{label:"secondary-motion-sphere-collision",code:c,bindGroupLayoutEntries:[i(0,!1),i(1,!0),i(2,!0),i(3,!0),i(4,!0),i(5,!1),{binding:6,visibility:GPUShaderStage.COMPUTE,buffer:{type:"uniform"}}]}),this.integrationParams=t.createBuffer({label:"secondary-motion-integration-params",size:80,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),this.collisionSpheres=t.createBuffer({label:"secondary-motion-collision-spheres",size:384,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),this.previousCollisionSpheres=t.createBuffer({label:"secondary-motion-previous-collision-spheres",size:384,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),this.collisionParams=t.createBuffer({label:"secondary-motion-collision-params",size:16,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),t.queue.writeBuffer(this.collisionParams,0,new Uint32Array(4));const r=this.integrationParamsView,d=n.gravity??[0,-9.81,0];r.setFloat32(0,this.fixedStep.step,!0),r.setFloat32(4,Math.max(0,Math.min(1,n.damping??.985)),!0),r.setFloat32(8,Math.max(0,n.attachment??12),!0),r.setFloat32(16,d[0],!0),r.setFloat32(20,d[1],!0),r.setFloat32(24,d[2],!0),r.setFloat32(76,Math.max(1,4*Math.hypot(d[0],d[1],d[2])),!0),t.queue.writeBuffer(this.integrationParams,0,this.integrationParamsData)}setFrameMotion(e){this.writeFrameVector(32,e.linearAcceleration),this.writeFrameVector(48,e.angularVelocity),this.writeFrameVector(64,e.angularAcceleration),this.device.queue.writeBuffer(this.integrationParams,32,this.integrationParamsData,32,48)}setSphereColliders(e){this.setEllipsoidColliders(e.map(e=>({center:e.center,radii:[e.radius,e.radius,e.radius]})))}setEllipsoidColliders(e){const t=Math.min(e.length,8),i=new Float32Array(96);for(let n=0;n<t;n++){const t=e[n],r=12*n;i[r]=t.center[0],i[r+1]=t.center[1],i[r+2]=t.center[2],i[r+4]=Math.max(0,t.radii[0]),i[r+5]=Math.max(0,t.radii[1]),i[r+6]=Math.max(0,t.radii[2]),i[r+8]=t.rotation?.[0]??0,i[r+9]=t.rotation?.[1]??0,i[r+10]=t.rotation?.[2]??0,i[r+11]=t.rotation?.[3]??1}this.collisionCollidersInitialized?this.device.queue.writeBuffer(this.previousCollisionSpheres,0,this.collisionColliderData):(this.device.queue.writeBuffer(this.previousCollisionSpheres,0,i),this.collisionCollidersInitialized=!0),this.collisionColliderData.set(i),this.device.queue.writeBuffer(this.collisionSpheres,0,this.collisionColliderData),this.device.queue.writeBuffer(this.collisionParams,0,new Uint32Array([t,0,0,0]))}advance(e){const t=this.fixedStep.consume(e);return this.pendingSteps=Math.min(this.pendingSteps+t,this.fixedStep.maxSubsteps),t}addRegion(e){if(this.assertActive(),this.regions.has(e.id))throw new Error(`Secondary-motion region "${e.id}" is already registered`);const t=e.particleInverseMass.length,i=e.renderVertexIndices.length;if(e.particlePositions.length!==3*t)throw new Error("Secondary-motion particle positions must contain three values per particle");const o=r(e.particlePositions),s=[this.createStorageBuffer(`${e.id}-positions-a`,o),this.createStorageBuffer(`${e.id}-positions-b`,o)],a=this.createStorageBuffer(`${e.id}-velocities`,new Float32Array(4*t)),c=this.createStorageBuffer(`${e.id}-targets`,e.particlePositions),l=this.createStorageBuffer(`${e.id}-inverse-mass`,e.particleInverseMass),d=this.createStorageBuffer(`${e.id}-render-positions`,new Float32Array(3*i)),u=this.createStorageBuffer(`${e.id}-render-vertex-indices`,e.renderVertexIndices),p=[0,1].map(t=>this.integrationKernel.createBindGroup(this.device,[n(0,s[t]),n(1,a),n(2,s[1-t]),n(3,c),n(4,l),n(5,this.integrationParams)],`${e.id}-integration-${t}`)),f=s.map(e=>this.packKernel.createBindGroup(this.device,[n(0,e),n(1,u),n(2,d)])),h=[0,1].map(e=>this.finalizeKernel.createBindGroup(this.device,[n(0,s[1-e]),n(1,s[e]),n(2,a),n(3,this.integrationParams)])),g=function(e,t){if(0===e.constraints.length)return null;if(e.constraints.length%2!=0)throw new Error("Secondary-motion distance constraints must contain particle pairs");const i=e.constraints.length/2;if(e.constraintRest.length!==i||e.constraintColors.length!==i)throw new Error("Secondary-motion constraint arrays have inconsistent lengths");const n=Array.from({length:i},(e,t)=>t).sort((t,i)=>e.constraintColors[t]-e.constraintColors[i]),r=new Uint32Array(e.constraints.length),o=new Float32Array(i),s=[];let a=-1;for(let c=0;c<i;c++){const i=n[c],l=e.constraintColors[i],d=e.constraints[2*i],u=e.constraints[2*i+1];if(d>=t||u>=t)throw new Error("Secondary-motion constraint references an invalid particle");r[2*c]=d,r[2*c+1]=u,o[c]=e.constraintRest[i],l!==a?(s.push({offset:c,count:1}),a=l):s[s.length-1].count++}return{pairs:r,rest:o,batches:s}}(e,t),m=g?this.createStorageBuffer(`${e.id}-constraints`,g.pairs):null,v=g?this.createStorageBuffer(`${e.id}-constraint-rest`,g.rest):null,y=[];if(g&&m&&v)for(const t of g.batches){const i=this.device.createBuffer({label:`${e.id}-constraint-params`,size:16,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),r=new ArrayBuffer(16),o=new DataView(r);o.setUint32(0,t.offset,!0),o.setUint32(4,t.count,!0),o.setFloat32(8,this.constraintStiffness,!0),this.device.queue.writeBuffer(i,0,r),y.push({count:t.count,params:i,bindGroups:s.map(e=>this.constraintKernel.createBindGroup(this.device,[n(0,e),n(1,l),n(2,m),n(3,v),n(4,i)]))})}const x={id:e.id,particleCount:t,renderVertexCount:i,positions:s,velocities:a,targets:c,inverseMass:l,renderPositions:d,renderVertexIndices:u,integrationBindGroups:p,packBindGroups:f,finalizeBindGroups:h,constraints:m,constraintRest:v,constraintBatches:y,state:0,disposed:!1,renderGeometry:null};this.regions.set(e.id,x);const P=this;return{id:e.id,particleCount:t,renderVertexCount:i,get positionBuffer(){return x.positions[x.state]},setRenderGeometry(e){x.renderGeometry=e},updateTargets(e){P.writeTargets(x.targets,e,t)},reset(t=e.particlePositions){P.resetRegion(x,t)},dispose(){P.removeRegion(x)}}}execute(e){if(this.assertActive(),e.device!==this.device)throw new Error("Secondary-motion runtime was created for a different GPU device");if(this.autoAdvance){const e=.001*performance.now();null!=this.lastExecuteSeconds&&this.advance(e-this.lastExecuteSeconds),this.lastExecuteSeconds=e}const i=this.pendingSteps;if(this.pendingSteps=0,0===this.regions.size)return;const n=e.encoder.beginComputePass({label:"secondary-motion-simulation"});for(let e=0;e<i;e++)for(const e of this.regions.values())if(!e.disposed&&0!==e.particleCount){if(n.setPipeline(this.integrationKernel.pipeline),n.setBindGroup(0,e.integrationBindGroups[e.state]),n.dispatchWorkgroups(Math.ceil(e.particleCount/t)),e.state=0===e.state?1:0,e.constraintBatches.length>0){n.setPipeline(this.constraintKernel.pipeline);for(let i=0;i<this.constraintIterations;i++)for(const i of e.constraintBatches)n.setBindGroup(0,i.bindGroups[e.state]),n.dispatchWorkgroups(Math.ceil(i.count/t))}n.setPipeline(this.finalizeKernel.pipeline),n.setBindGroup(0,e.finalizeBindGroups[e.state]),n.dispatchWorkgroups(Math.ceil(e.particleCount/t)),n.setPipeline(this.collisionKernel.pipeline),n.setBindGroup(0,this.getCollisionBindGroup(e)),n.dispatchWorkgroups(Math.ceil(e.particleCount/t))}for(const e of this.regions.values())e.disposed||0===e.particleCount||null==e.renderGeometry||(n.setPipeline(this.packKernel.pipeline),n.setBindGroup(0,e.packBindGroups[e.state]),n.dispatchWorkgroups(Math.ceil(e.renderVertexCount/t)));n.end();for(const t of this.regions.values())if(!t.disposed&&null!=t.renderGeometry){const i=e.getGeometryVertexBuffer(t.renderGeometry,"position");e.encoder.copyBufferToBuffer(t.renderPositions,0,i,0,12*t.renderVertexCount)}}dispose(){if(!this.disposed){for(const e of Array.from(this.regions.values()))this.removeRegion(e);this.integrationParams.destroy(),this.collisionSpheres.destroy(),this.previousCollisionSpheres.destroy(),this.collisionParams.destroy(),this.collisionBindGroups.clear(),this.disposed=!0}}resetRegion(e,t){this.writeTargets(e.targets,t,e.particleCount);const i=function(e,t){if(e.length!==3*t)throw new Error(`Expected ${3*t} secondary-motion position values, received ${e.length}`);return r(e)}(t,e.particleCount);for(const t of e.positions)this.device.queue.writeBuffer(t,0,i);this.device.queue.writeBuffer(e.velocities,0,new Float32Array(4*e.particleCount)),e.state=0,this.fixedStep.reset(),this.pendingSteps=0}writeTargets(e,t,i){if(t.length!==3*i)throw new Error(`Expected ${3*i} secondary-motion target values, received ${t.length}`);this.device.queue.writeBuffer(e,0,t)}removeRegion(e){if(!e.disposed){e.disposed=!0,this.regions.delete(e.id);for(const t of e.positions)this.collisionBindGroups.delete(t);for(const t of e.positions)t.destroy();e.velocities.destroy(),e.targets.destroy(),e.inverseMass.destroy(),e.renderPositions.destroy(),e.renderVertexIndices.destroy(),e.constraints?.destroy(),e.constraintRest?.destroy();for(const t of e.constraintBatches)t.params.destroy()}}createStorageBuffer(e,t){const i=this.device.createBuffer({label:e,size:Math.max(4,t.byteLength+3&-4),usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST|GPUBufferUsage.COPY_SRC});return t.byteLength>0&&this.device.queue.writeBuffer(i,0,t.buffer,t.byteOffset,t.byteLength),i}getCollisionBindGroup(e){const t=e.positions[e.state];let i=this.collisionBindGroups.get(t);return null==i&&(i=this.collisionKernel.createBindGroup(this.device,[n(0,t),n(1,e.positions[1-e.state]),n(2,e.inverseMass),n(3,this.collisionSpheres),n(4,this.previousCollisionSpheres),n(5,e.velocities),n(6,this.collisionParams)],`${e.id}-sphere-collision-${e.state}`),this.collisionBindGroups.set(t,i)),i}assertActive(){if(this.disposed)throw new Error("Secondary-motion runtime is disposed")}writeFrameVector(e,t){this.integrationParamsView.setFloat32(e,t?.[0]??0,!0),this.integrationParamsView.setFloat32(e+4,t?.[1]??0,!0),this.integrationParamsView.setFloat32(e+8,t?.[2]??0,!0)}}export function createSecondaryMotionRenderGraphExtension(e,t="SecondaryMotionSimulation"){const i=i=>{i.addPass({name:t,execute:t=>e.execute(t)})};return i.stage="beforeScene",i}export class SecondaryMotionFixedStep{constructor(e={}){this.accumulator=0,this.step=1/(e.frequency??60),this.maxSubsteps=e.maxSubsteps??4,this.maxFrameDelta=e.maxFrameDelta??.1}consume(e){if(!Number.isFinite(e)||e<0||e>this.maxFrameDelta)return this.accumulator=0,0;this.accumulator=Math.min(this.accumulator+e,this.step*this.maxSubsteps);const t=Math.min(Math.floor(this.accumulator/this.step),this.maxSubsteps);return this.accumulator-=t*this.step,t}reset(){this.accumulator=0}}export function selectSecondaryMotionLod(e,t,i){return t?i?"hero":e<8?"near":e<20?"mid":e<40?"far":"sleep":"sleep"}export function secondaryMotionTransformDiscontinuous(e,t,i=2,n=.5){if(e.length<7||t.length<7)return!0;const r=t[0]-e[0],o=t[1]-e[1],s=t[2]-e[2];if(r*r+o*o+s*s>i*i)return!0;return Math.abs(e[3]*t[3]+e[4]*t[4]+e[5]*t[5]+e[6]*t[6])<n}function i(e,t){return{binding:e,visibility:GPUShaderStage.COMPUTE,buffer:{type:t?"read-only-storage":"storage"}}}function n(e,t){return{binding:e,resource:{buffer:t}}}function r(e){const t=new Float32Array(e.length/3*4);for(let i=0,n=0;i<e.length;i+=3,n+=4)t[n]=e[i],t[n+1]=e[i+1],t[n+2]=e[i+2],t[n+3]=1;return t}const o="\nstruct IntegrationParams {\n deltaTime: f32,\n damping: f32,\n attachment: f32,\n _padding: f32,\n gravity: vec3<f32>,\n _padding2: f32,\n frameLinearAcceleration: vec3<f32>,\n _padding3: f32,\n frameAngularVelocity: vec3<f32>,\n _padding4: f32,\n frameAngularAcceleration: vec3<f32>,\n maxFrameAcceleration: f32,\n}\n@group(0) @binding(0) var<storage, read> currentPositions: array<vec4<f32>>;\n@group(0) @binding(1) var<storage, read_write> velocities: array<vec4<f32>>;\n@group(0) @binding(2) var<storage, read_write> outputPositions: array<vec4<f32>>;\nstruct PackedPosition { x: f32, y: f32, z: f32 }\n@group(0) @binding(3) var<storage, read> targetPositions: array<PackedPosition>;\n@group(0) @binding(4) var<storage, read> inverseMass: array<f32>;\n@group(0) @binding(5) var<uniform> params: IntegrationParams;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n let index = invocation.x;\n if (index >= arrayLength(&inverseMass)) { return; }\n let packedTarget = targetPositions[index];\n let targetPosition = vec3<f32>(packedTarget.x, packedTarget.y, packedTarget.z);\n if (inverseMass[index] <= 0.0) {\n velocities[index] = vec4(0.0);\n outputPositions[index] = vec4(targetPosition, 1.0);\n return;\n }\n let current = currentPositions[index].xyz;\n // Fully simulated cloth should be driven through its structural connection\n // to the animated roots. Pulling every free particle toward its skinned rest\n // pose makes collision sandwich it against the character like shrink-wrap.\n let animationInfluence = 1.0 - clamp(inverseMass[index], 0.0, 1.0);\n let targetAcceleration = (targetPosition - current) * params.attachment * animationInfluence;\n let previousVelocity = velocities[index].xyz;\n let rotationalAcceleration =\n cross(params.frameAngularAcceleration, current) +\n cross(params.frameAngularVelocity, cross(params.frameAngularVelocity, current)) +\n 2.0 * cross(params.frameAngularVelocity, previousVelocity);\n var frameAcceleration = params.frameLinearAcceleration + rotationalAcceleration;\n let frameAccelerationLength = length(frameAcceleration);\n if (frameAccelerationLength > params.maxFrameAcceleration) {\n frameAcceleration *= params.maxFrameAcceleration / frameAccelerationLength;\n }\n let acceleration = params.gravity + targetAcceleration - frameAcceleration;\n let velocity = previousVelocity * params.damping + acceleration * params.deltaTime;\n let next = current + velocity * params.deltaTime;\n velocities[index] = vec4(velocity, 0.0);\n outputPositions[index] = vec4(next, 1.0);\n}\n",s="\nstruct ConstraintParams {\n offset: u32,\n count: u32,\n stiffness: f32,\n _padding: f32,\n}\n@group(0) @binding(0) var<storage, read_write> positions: array<vec4<f32>>;\n@group(0) @binding(1) var<storage, read> inverseMass: array<f32>;\n@group(0) @binding(2) var<storage, read> pairs: array<vec2<u32>>;\n@group(0) @binding(3) var<storage, read> restLengths: array<f32>;\n@group(0) @binding(4) var<uniform> params: ConstraintParams;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n if (invocation.x >= params.count) { return; }\n let constraintIndex = params.offset + invocation.x;\n let pair = pairs[constraintIndex];\n let weightA = inverseMass[pair.x];\n let weightB = inverseMass[pair.y];\n let weight = weightA + weightB;\n if (weight <= 0.0) { return; }\n let delta = positions[pair.y].xyz - positions[pair.x].xyz;\n let length = length(delta);\n if (length <= 0.000001) { return; }\n let correction = delta * ((length - restLengths[constraintIndex]) / length) * params.stiffness;\n positions[pair.x] = vec4(positions[pair.x].xyz + correction * (weightA / weight), 1.0);\n positions[pair.y] = vec4(positions[pair.y].xyz - correction * (weightB / weight), 1.0);\n}\n",a="\nstruct PackedPosition { x: f32, y: f32, z: f32 }\n@group(0) @binding(0) var<storage, read> positions: array<vec4<f32>>;\n@group(0) @binding(1) var<storage, read> particleByRenderVertex: array<u32>;\n@group(0) @binding(2) var<storage, read_write> output: array<PackedPosition>;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n let index = invocation.x;\n if (index >= arrayLength(&particleByRenderVertex)) { return; }\n let position = positions[particleByRenderVertex[index]];\n output[index].x = position.x; output[index].y = position.y; output[index].z = position.z;\n}\n",c="\nstruct CollisionParams { count: u32, _padding1: u32, _padding2: u32, _padding3: u32 }\n@group(0) @binding(0) var<storage, read_write> positions: array<vec4<f32>>;\nstruct Ellipsoid { center: vec4<f32>, radii: vec4<f32>, rotation: vec4<f32> }\n@group(0) @binding(1) var<storage, read> previousPositions: array<vec4<f32>>;\n@group(0) @binding(2) var<storage, read> inverseMass: array<f32>;\n@group(0) @binding(3) var<storage, read> ellipsoids: array<Ellipsoid>;\n@group(0) @binding(4) var<storage, read> previousEllipsoids: array<Ellipsoid>;\n@group(0) @binding(5) var<storage, read_write> velocities: array<vec4<f32>>;\n@group(0) @binding(6) var<uniform> params: CollisionParams;\n\nfn rotateByQuaternion(value: vec3<f32>, rotation: vec4<f32>) -> vec3<f32> {\n return value + 2.0 * cross(rotation.xyz, cross(rotation.xyz, value) + rotation.w * value);\n}\n\nfn toEllipsoidSpace(value: vec3<f32>, shape: Ellipsoid) -> vec3<f32> {\n return rotateByQuaternion(value - shape.center.xyz, vec4<f32>(-shape.rotation.xyz, shape.rotation.w)) / shape.radii.xyz;\n}\n\nfn fromEllipsoidSpace(value: vec3<f32>, shape: Ellipsoid) -> vec3<f32> {\n return shape.center.xyz + rotateByQuaternion(value * shape.radii.xyz, shape.rotation);\n}\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n let index = invocation.x;\n if (index >= arrayLength(&inverseMass) || inverseMass[index] <= 0.0) { return; }\n var position = positions[index].xyz;\n var velocity = velocities[index].xyz;\n for (var collider = 0u; collider < params.count; collider++) {\n let shape = ellipsoids[collider];\n if (all(shape.radii.xyz > vec3<f32>(0.0))) {\n let scaled = toEllipsoidSpace(position, shape);\n let scaledLengthSquared = dot(scaled, scaled);\n if (scaledLengthSquared < 1.0) {\n let direction = select(vec3<f32>(0.0, 0.0, 1.0), scaled * inverseSqrt(scaledLengthSquared), scaledLengthSquared > 0.00000001);\n position = fromEllipsoidSpace(direction * 1.02, shape);\n let normal = normalize(rotateByQuaternion(direction / shape.radii.xyz, shape.rotation));\n velocity -= normal * min(dot(velocity, normal), 0.0);\n } else {\n let previousShape = previousEllipsoids[collider];\n let start = toEllipsoidSpace(previousPositions[index].xyz, previousShape);\n let travel = scaled - start;\n let a = dot(travel, travel);\n let b = 2.0 * dot(start, travel);\n let c = dot(start, start) - 1.0;\n let discriminant = b * b - 4.0 * a * c;\n if (a > 0.00000001 && discriminant >= 0.0) {\n let hit = (-b - sqrt(discriminant)) / (2.0 * a);\n if (hit > 0.0001 && hit <= 1.0) {\n let direction = normalize(start + travel * hit);\n position = fromEllipsoidSpace(direction * 1.02, shape);\n let normal = normalize(rotateByQuaternion(direction / shape.radii.xyz, shape.rotation));\n velocity -= normal * min(dot(velocity, normal), 0.0);\n }\n }\n }\n }\n }\n positions[index] = vec4(position, 1.0);\n velocities[index] = vec4(velocity, 0.0);\n}\n",l="\nstruct IntegrationParams {\n deltaTime: f32,\n damping: f32,\n attachment: f32,\n _padding: f32,\n gravity: vec3<f32>,\n _padding2: f32,\n}\n@group(0) @binding(0) var<storage, read> previousPositions: array<vec4<f32>>;\n@group(0) @binding(1) var<storage, read> constrainedPositions: array<vec4<f32>>;\n@group(0) @binding(2) var<storage, read_write> velocities: array<vec4<f32>>;\n@group(0) @binding(3) var<uniform> params: IntegrationParams;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n let index = invocation.x;\n if (index >= arrayLength(&velocities)) { return; }\n velocities[index] = vec4(\n (constrainedPositions[index].xyz - previousPositions[index].xyz) / params.deltaTime,\n 0.0,\n );\n}\n";/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=asset-resource-loader-converted-fbx.test.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{describe as e,expect as t,it as n,vi as o}from"vitest";import{Group as a,Scene as s}from"three";import{AssetResourceLoader as r}from"../scene/asset-resource-loader.js";o.mock("../scene/materializer.js",()=>({applyMaterial:o.fn(),getConvertedFbxToGlbAssignedMaterialTextureFlipY:o.fn()})),o.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=o.fn()),e[t]),set:(e,t,n)=>(e[t]=n,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),e("AssetResourceLoader converted FBX hierarchy",()=>{n("unwraps the synthetic glTF scene around a Three-converted FBX root",async()=>{const e=new r;e.setDataDir("");const n=new s,i=new a;i.name="AuthoredFbxRoot",n.add(i),e.glbLoader={loadAsync:o.fn().mockResolvedValue({scene:n,animations:[]})};const l=await e.loadByAsset({id:"converted-fbx",name:"Converted FBX",type:"mesh",fileKey:"model.glb",fileFormat:"glb",originalFileKey:"model.fbx"});t(l.scene).toBe(i),t(i.parent).toBeNull()}),n("keeps the synthetic scene for an ordinary GLB",async()=>{const e=new r;e.setDataDir("");const n=new s;n.add(new a),e.glbLoader={loadAsync:o.fn().mockResolvedValue({scene:n,animations:[]})};const i=await e.loadByAsset({id:"native-glb",name:"Native GLB",type:"mesh",fileKey:"model.glb",fileFormat:"glb"});t(i.scene).toBe(n)})});/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=build-tile.test.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{beforeAll as e,describe as a,expect as i,it as t}from"vitest";import{init as n,recastConfigDefaults as o}from"recast-navigation";import{buildTile as l}from"../gameplay/ai/build-tile.js";a("buildTile",()=>{e(async()=>{await n()}),t("builds Detour data for a walkable plane",()=>{const e=l({positions:new Float32Array([-8,0,-8,8,0,-8,-8,0,8,8,0,8]),indices:new Uint32Array([0,2,1,1,2,3]),navMeshBounds:[[-10,-2,-10],[10,2,10]],tileBoundsMin:[-10,-2,-10],tileBoundsMax:[10,2,10],tileX:0,tileY:0,keepIntermediates:!1,recastConfig:{...o,cs:.2,ch:.2,tileSize:100,walkableHeight:10,walkableRadius:2,walkableClimb:1,minRegionArea:0,mergeRegionArea:0}});i(e.success).toBe(!0),i("data"in e?e.data:void 0).toBeDefined(),"data"in e&&e.data?.destroy()})});/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -1,4 +1,4 @@
1
- import"reflect-metadata";import{afterEach as e,expect as t,test as o,vi as n}from"vitest";n.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&n.fn()),e[t]),set:(e,t,o)=>(e[t]=o,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}),n.mock("../gameplay/services/render.js",()=>({ViewController:class{}})),n.mock("../gameplay/services/physics/physics-system.js",()=>({PhysicsSystem:class{},RayTestResult:class{},ShapeCastResult:class{}})),n.mock("../gameplay/services/world.js",()=>({World:class{}})),n.mock("@hology/nebula",()=>({ease:{easeInOutCubic:e=>e}}));import{Subject as a}from"rxjs";import{Container as s}from"typedi";import{Object3D as r,PerspectiveCamera as i,Ray as c,Scene as l,Vector3 as p}from"three";import{BaseActor as m}from"../gameplay/actors/actor.js";import{FirstPersonCameraComponent as d}from"../gameplay/actors/camera/first-person-camera-component.js";import{ThirdPersonCameraComponent as u}from"../gameplay/actors/camera/third-person-camera-component.js";import{PhysicsSystem as h}from"../gameplay/services/physics/physics-system.js";import{ViewController as y}from"../gameplay/services/render.js";import{World as f}from"../gameplay/services/world.js";function w(e=new C){const t=v(),o=new d;return o.actor=e,{camera:o,actor:e,element:t}}function v(){const e=document.createElement("div"),t=document.createElement("canvas");return e.appendChild(t),Object.defineProperty(e,"clientWidth",{configurable:!0,value:1280}),Object.defineProperty(e,"clientHeight",{configurable:!0,value:720}),s.set(y,{htmlElement:e,setCamera:n.fn(),onUpdate:()=>new a,onLateUpdate:()=>new a}),s.set(f,{scene:new l}),s.set(h,{sphereCast:n.fn()}),e}function B(){Object.defineProperty(document.body,"requestPointerLock",{configurable:!0,value:n.fn()}),Object.defineProperty(document,"pointerLockElement",{configurable:!0,value:null})}function g(e,t){return e.mock.calls.filter(e=>e[0]===t).length}e(()=>{n.restoreAllMocks(),s.reset()}),o("first-person camera clamps pitch input",()=>{const{camera:e}=w();e.rotationInput.rotateX(Math.PI),t(e.rotationInput.rotation.x).toBeCloseTo(Math.PI/2-.01),e.rotationInput.rotateX(2*-Math.PI),t(e.rotationInput.rotation.x).toBeCloseTo(-Math.PI/2+.01)}),o("first-person camera follows actor eye offset",async()=>{const e=new C;e.position.set(1,2,3),e.rotation.y=Math.PI/2,e.object.updateMatrixWorld(!0);const{camera:o}=w(e);o.eyeHeight=1.5,o.offsetZ=.25,o.autoActivate=!1,await o.onInit(),o.updateCameraTransform(),t(o.camera.position.x).toBeCloseTo(1.25),t(o.camera.position.y).toBeCloseTo(3.5),t(o.camera.position.z).toBeCloseTo(3)}),o("first-person aim helpers write to caller-provided objects",async()=>{const e=new C;e.position.set(0,1,0);const{camera:o}=w(e);o.autoActivate=!1,await o.onInit();const n=new p,a=new p,s=new c;t(o.getAimOrigin(n)).toBe(n),t(o.getAimDirection(a)).toBe(a),t(o.getAimRay(s)).toBe(s),t(s.origin).toEqual(n),t(s.direction).toEqual(a),t(a.x).toBeCloseTo(0),t(a.y).toBeCloseTo(0),t(a.z).toBeCloseTo(1),t(a.length()).toBeCloseTo(1)}),o("first-person camera pitch input tilts aim downward when positive",async()=>{const{camera:e}=w();e.autoActivate=!1,await e.onInit(),e.rotationInput.rotateX(.25),e.updateCameraTransform();const o=e.getAimDirection(new p);t(o.y).toBeLessThan(0),t(o.z).toBeGreaterThan(0),t(o.length()).toBeCloseTo(1)}),o("first-person hidden objects restore their previous visibility",()=>{const{camera:e}=w(),o=new r,n=new r;n.visible=!1,e.hideObjects(o,n),t(o.visible).toBe(!1),t(n.visible).toBe(!1),e.restoreHiddenObjects(),t(o.visible).toBe(!0),t(n.visible).toBe(!1)}),o("first-person activation is idempotent",async()=>{const{camera:e,element:o}=w();e.autoActivate=!1,await e.onInit(),B();const a=n.spyOn(o,"addEventListener"),s=n.spyOn(o,"removeEventListener"),r=n.spyOn(document,"addEventListener"),i=n.spyOn(document,"removeEventListener");e.activate(),e.activate(),t(g(a,"pointerdown")).toBe(1),t(g(a,"keydown")).toBe(1),t(g(r,"pointerlockchange")).toBe(1),e.deactivate(),e.deactivate(),t(g(s,"pointerdown")).toBe(1),t(g(s,"keydown")).toBe(1),t(g(i,"pointerlockchange")).toBe(1)}),o("third-person activation remains idempotent",async()=>{const e=new C,o=v(),a=new u;a.actor=e,a.autoActivate=!1,await a.onInit(),B();const s=n.spyOn(o,"addEventListener"),r=n.spyOn(o,"removeEventListener"),i=n.spyOn(document,"addEventListener"),c=n.spyOn(document,"removeEventListener");a.activate(),a.activate(),t(g(s,"pointerdown")).toBe(1),t(g(s,"keydown")).toBe(1),t(g(i,"pointerlockchange")).toBe(1),a.deactivate(),a.deactivate(),t(g(r,"pointerdown")).toBe(1),t(g(r,"keydown")).toBe(1),t(g(c,"pointerlockchange")).toBe(1)}),o("third-person camera hides the cursor only after pointer lock is active",async()=>{const e=new C,o=v(),a=o.querySelector("canvas"),s=new u;s.actor=e,s.autoActivate=!1,await s.onInit(),B();const r=n.fn(()=>Promise.resolve());Object.defineProperty(a,"requestPointerLock",{configurable:!0,value:r});let i=null;Object.defineProperty(document,"pointerLockElement",{configurable:!0,get:()=>i}),s.activate(),s.hideCursor({ignoreRecentUnlock:!0}),t(r).toHaveBeenCalledWith(),t(s.isMouseLocked).toBe(!1),t(o.style.cursor).not.toBe("none"),i=a,document.dispatchEvent(new Event("pointerlockchange")),t(s.isMouseLocked).toBe(!0),t(o.style.cursor).toBe("none")}),o("third-person camera does not treat sustained fast movement as a teleport",async()=>{const e=new C;v();const o=new u;o.actor=e,o.autoActivate=!0,o.collision=!1,o.fixedBehind=!1,o.smoothCamera=!0,o.smoothSpeed=10,await o.onInit(),o.onLateUpdate(1/60);for(let t=0;t<12;t++)e.position.x+=.2,o.onLateUpdate(1/60);t(e.position.x-o.camera.position.x).toBeGreaterThan(.7)}),o("third-person camera still snaps after a target teleport",async()=>{const e=new C;v();const o=new u;o.actor=e,o.autoActivate=!0,o.collision=!1,o.fixedBehind=!1,o.smoothCamera=!0,o.teleportSnapDistance=5,await o.onInit(),o.onLateUpdate(1/60),e.position.x=10,o.onLateUpdate(1/60),t(o.camera.position.x).toBeCloseTo(10)}),o("third-person collision casts along the current frame camera arm",async()=>{const e=new C;v();const o=s.get(h);n.mocked(o.sphereCast).mockReturnValue({hasHit:!1});const a=new u;a.actor=e,a.autoActivate=!0,a.fixedBehind=!1,await a.onInit(),a.rotationInput.rotation.y=Math.PI/2,a.onLateUpdate(1/60);const[,,r,i]=n.mocked(o.sphereCast).mock.calls[0],c=Math.sqrt(a.distance**2+(a.offsetY-a.height)**2);t(r.x).toBeCloseTo(-a.distance/c),t(r.y).toBeCloseTo((a.offsetY-a.height)/c),t(i).toBeCloseTo(c)}),o("third-person collision preserves the safe camera arm and recovers smoothly",async()=>{const e=new C;v();const o=s.get(h);n.mocked(o.sphereCast).mockReturnValueOnce({hasHit:!0,distance:4}).mockReturnValue({hasHit:!1});const a=new u;a.actor=e,a.autoActivate=!0,a.fixedBehind=!1,await a.onInit();const r=new p(0,a.height,0);a.onLateUpdate(1/60),t(a.camera.position.distanceTo(r)).toBeCloseTo(3.95),a.onLateUpdate(.1);const i=a.camera.position.distanceTo(r);t(i).toBeGreaterThan(3.95),t(i).toBeLessThan(Math.sqrt(a.distance**2+(a.offsetY-a.height)**2))}),o("third-person camera blends temporary control over the gameplay pose",async()=>{const e=new C;v();const o=new u;o.actor=e,o.autoActivate=!0,o.collision=!1,o.fixedBehind=!1,o.smoothCamera=!1,await o.onInit(),o.onLateUpdate(1/60);const n=o.camera.position.clone(),a=new p(10,4,2),s=new p(1,2,3);o.setTemporaryControl(a,s,{blendInDuration:1}),o.onLateUpdate(.5),t(o.camera.position.x).toBeCloseTo((n.x+a.x)/2),t(o.camera.position.y).toBeCloseTo((n.y+a.y)/2),o.onLateUpdate(.5),t(o.camera.position).toEqual(a)}),o("third-person camera blends temporary control back to the current gameplay pose",async()=>{const e=new C;v();const o=new u;o.actor=e,o.autoActivate=!0,o.collision=!1,o.fixedBehind=!1,o.smoothCamera=!1,await o.onInit();const n=new p(10,4,2);o.setTemporaryControl(n,new p(1,2,3),{blendInDuration:0}),o.onLateUpdate(1/60),e.position.x=2,o.clearTemporaryControl(1),o.onLateUpdate(.5);const a=new p(2,o.offsetY,-o.distance);t(o.camera.position.x).toBeCloseTo((n.x+a.x)/2),t(o.camera.position.y).toBeCloseTo((n.y+a.y)/2)}),o("third-person camera applies collision to temporary control",async()=>{const e=new C;v();const o=s.get(h);n.mocked(o.sphereCast).mockReturnValue({hasHit:!0,distance:2});const a=new u;a.actor=e,a.autoActivate=!0,a.collision=!0,a.fixedBehind=!1,a.smoothCamera=!1,await a.onInit(),a.setTemporaryControl(new p(0,0,10),new p,{blendInDuration:0}),a.onLateUpdate(1/60),t(a.camera.position.z).toBeCloseTo(1.95)}),o("third-person camera pans toward a temporary look-at target without repositioning",async()=>{const e=new C;v();const o=new u;o.actor=e,o.autoActivate=!0,o.collision=!1,o.fixedBehind=!1,o.smoothCamera=!1,await o.onInit(),o.onLateUpdate(1/60);const n=o.camera.position.clone(),a=o.camera.quaternion.clone(),s=new p(0,2,-20),r=new i;r.position.copy(n),r.lookAt(s);const c=a.angleTo(r.quaternion);o.setTemporaryLookAt(s,{blendInDuration:1}),o.onLateUpdate(.5),t(o.camera.position).toEqual(n),t(a.angleTo(o.camera.quaternion)).toBeCloseTo(c/2)}),o("third-person camera blends a temporary field of view and restores it",async()=>{const e=new C;v();const o=new u;o.actor=e,o.autoActivate=!0,o.collision=!1,await o.onInit();const n=o.camera.fov;o.setTemporaryLookAt(new p(0,2,10),{blendInDuration:1,blendOutDuration:1,fieldOfView:35}),o.onLateUpdate(1),t(o.camera.fov).toBe(35),o.clearTemporaryControl(),o.onLateUpdate(.5),t(o.camera.fov).toBeCloseTo((35+n)/2),o.onLateUpdate(.5),t(o.camera.fov).toBe(n)});class C extends m{constructor(){super(),this.__isInitialised=!0}}/*
1
+ import"reflect-metadata";import{afterEach as e,expect as t,test as o,vi as n}from"vitest";n.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&n.fn()),e[t]),set:(e,t,o)=>(e[t]=o,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}),n.mock("../gameplay/services/render.js",()=>({ViewController:class{}})),n.mock("../gameplay/services/physics/physics-system.js",()=>({PhysicsSystem:class{},RayTestResult:class{},ShapeCastResult:class{}})),n.mock("../gameplay/services/world.js",()=>({World:class{}})),n.mock("@hology/nebula",()=>({ease:{easeInOutCubic:e=>e}}));import{Subject as a}from"rxjs";import{Container as s}from"typedi";import{Object3D as r,PerspectiveCamera as i,Ray as c,Scene as l,Vector3 as p}from"three";import{BaseActor as m}from"../gameplay/actors/actor.js";import{FirstPersonCameraComponent as d}from"../gameplay/actors/camera/first-person-camera-component.js";import{ThirdPersonCameraComponent as u}from"../gameplay/actors/camera/third-person-camera-component.js";import{PhysicsSystem as h}from"../gameplay/services/physics/physics-system.js";import{ViewController as y}from"../gameplay/services/render.js";import{World as f}from"../gameplay/services/world.js";function w(e=new C){const t=v(),o=new d;return o.actor=e,{camera:o,actor:e,element:t}}function v(){const e=document.createElement("div"),t=document.createElement("canvas");return e.appendChild(t),Object.defineProperty(e,"clientWidth",{configurable:!0,value:1280}),Object.defineProperty(e,"clientHeight",{configurable:!0,value:720}),s.set(y,{htmlElement:e,setCamera:n.fn(),onUpdate:()=>new a,onLateUpdate:()=>new a}),s.set(f,{scene:new l}),s.set(h,{sphereCast:n.fn()}),e}function B(){Object.defineProperty(document.body,"requestPointerLock",{configurable:!0,value:n.fn()}),Object.defineProperty(document,"pointerLockElement",{configurable:!0,value:null})}function g(e,t){return e.mock.calls.filter(e=>e[0]===t).length}e(()=>{n.restoreAllMocks(),s.reset()}),o("first-person camera clamps pitch input",()=>{const{camera:e}=w();e.rotationInput.rotateX(Math.PI),t(e.rotationInput.rotation.x).toBeCloseTo(Math.PI/2-.01),e.rotationInput.rotateX(2*-Math.PI),t(e.rotationInput.rotation.x).toBeCloseTo(-Math.PI/2+.01)}),o("first-person camera follows actor eye offset",async()=>{const e=new C;e.position.set(1,2,3),e.rotation.y=Math.PI/2,e.object.updateMatrixWorld(!0);const{camera:o}=w(e);o.eyeHeight=1.5,o.offsetZ=.25,o.autoActivate=!1,await o.onInit(),o.updateCameraTransform(),t(o.camera.position.x).toBeCloseTo(1.25),t(o.camera.position.y).toBeCloseTo(3.5),t(o.camera.position.z).toBeCloseTo(3)}),o("first-person aim helpers write to caller-provided objects",async()=>{const e=new C;e.position.set(0,1,0);const{camera:o}=w(e);o.autoActivate=!1,await o.onInit();const n=new p,a=new p,s=new c;t(o.getAimOrigin(n)).toBe(n),t(o.getAimDirection(a)).toBe(a),t(o.getAimRay(s)).toBe(s),t(s.origin).toEqual(n),t(s.direction).toEqual(a),t(a.x).toBeCloseTo(0),t(a.y).toBeCloseTo(0),t(a.z).toBeCloseTo(1),t(a.length()).toBeCloseTo(1)}),o("first-person camera pitch input tilts aim downward when positive",async()=>{const{camera:e}=w();e.autoActivate=!1,await e.onInit(),e.rotationInput.rotateX(.25),e.updateCameraTransform();const o=e.getAimDirection(new p);t(o.y).toBeLessThan(0),t(o.z).toBeGreaterThan(0),t(o.length()).toBeCloseTo(1)}),o("first-person hidden objects restore their previous visibility",()=>{const{camera:e}=w(),o=new r,n=new r;n.visible=!1,e.hideObjects(o,n),t(o.visible).toBe(!1),t(n.visible).toBe(!1),e.restoreHiddenObjects(),t(o.visible).toBe(!0),t(n.visible).toBe(!1)}),o("first-person activation is idempotent",async()=>{const{camera:e,element:o}=w();e.autoActivate=!1,await e.onInit(),B();const a=n.spyOn(o,"addEventListener"),s=n.spyOn(o,"removeEventListener"),r=n.spyOn(document,"addEventListener"),i=n.spyOn(document,"removeEventListener");e.activate(),e.activate(),t(g(a,"pointerdown")).toBe(1),t(g(a,"keydown")).toBe(1),t(g(r,"pointerlockchange")).toBe(1),e.deactivate(),e.deactivate(),t(g(s,"pointerdown")).toBe(1),t(g(s,"keydown")).toBe(1),t(g(i,"pointerlockchange")).toBe(1)}),o("third-person activation remains idempotent",async()=>{const e=new C,o=v(),a=new u;a.actor=e,a.autoActivate=!1,await a.onInit(),B();const s=n.spyOn(o,"addEventListener"),r=n.spyOn(o,"removeEventListener"),i=n.spyOn(document,"addEventListener"),c=n.spyOn(document,"removeEventListener");a.activate(),a.activate(),t(g(s,"pointerdown")).toBe(1),t(g(s,"keydown")).toBe(1),t(g(i,"pointerlockchange")).toBe(1),a.deactivate(),a.deactivate(),t(g(r,"pointerdown")).toBe(1),t(g(r,"keydown")).toBe(1),t(g(c,"pointerlockchange")).toBe(1)}),o("third-person camera hides the cursor only after pointer lock is active",async()=>{const e=new C,o=v();document.body.appendChild(o);const a=o.querySelector("canvas"),s=new u;s.actor=e,s.autoActivate=!1,await s.onInit(),B();const r=n.fn(()=>Promise.resolve());Object.defineProperty(a,"requestPointerLock",{configurable:!0,value:r});let i=null;Object.defineProperty(document,"pointerLockElement",{configurable:!0,get:()=>i}),s.activate(),s.hideCursor({ignoreRecentUnlock:!0}),t(r).toHaveBeenCalledWith({unadjustedMovement:!0}),t(s.isMouseLocked).toBe(!1),t(o.style.cursor).not.toBe("none"),i=a,document.dispatchEvent(new Event("pointerlockchange")),t(s.isMouseLocked).toBe(!0),t(o.style.cursor).toBe("none"),o.remove()}),o("third-person camera does not treat sustained fast movement as a teleport",async()=>{const e=new C;v();const o=new u;o.actor=e,o.autoActivate=!0,o.collision=!1,o.fixedBehind=!1,o.smoothCamera=!0,o.smoothSpeed=10,await o.onInit(),o.onLateUpdate(1/60);for(let t=0;t<12;t++)e.position.x+=.2,o.onLateUpdate(1/60);t(e.position.x-o.camera.position.x).toBeGreaterThan(.7)}),o("third-person camera still snaps after a target teleport",async()=>{const e=new C;v();const o=new u;o.actor=e,o.autoActivate=!0,o.collision=!1,o.fixedBehind=!1,o.smoothCamera=!0,o.teleportSnapDistance=5,await o.onInit(),o.onLateUpdate(1/60),e.position.x=10,o.onLateUpdate(1/60),t(o.camera.position.x).toBeCloseTo(10)}),o("third-person collision casts along the current frame camera arm",async()=>{const e=new C;v();const o=s.get(h);n.mocked(o.sphereCast).mockReturnValue({hasHit:!1});const a=new u;a.actor=e,a.autoActivate=!0,a.fixedBehind=!1,await a.onInit(),a.rotationInput.rotation.y=Math.PI/2,a.onLateUpdate(1/60);const[,,r,i]=n.mocked(o.sphereCast).mock.calls[0],c=Math.sqrt(a.distance**2+(a.offsetY-a.height)**2);t(r.x).toBeCloseTo(-a.distance/c),t(r.y).toBeCloseTo((a.offsetY-a.height)/c),t(i).toBeCloseTo(c)}),o("third-person collision preserves the safe camera arm and recovers smoothly",async()=>{const e=new C;v();const o=s.get(h);n.mocked(o.sphereCast).mockReturnValueOnce({hasHit:!0,distance:4}).mockReturnValue({hasHit:!1});const a=new u;a.actor=e,a.autoActivate=!0,a.fixedBehind=!1,await a.onInit();const r=new p(0,a.height,0);a.onLateUpdate(1/60),t(a.camera.position.distanceTo(r)).toBeCloseTo(3.95),a.onLateUpdate(.1);const i=a.camera.position.distanceTo(r);t(i).toBeGreaterThan(3.95),t(i).toBeLessThan(Math.sqrt(a.distance**2+(a.offsetY-a.height)**2))}),o("third-person camera blends temporary control over the gameplay pose",async()=>{const e=new C;v();const o=new u;o.actor=e,o.autoActivate=!0,o.collision=!1,o.fixedBehind=!1,o.smoothCamera=!1,await o.onInit(),o.onLateUpdate(1/60);const n=o.camera.position.clone(),a=new p(10,4,2),s=new p(1,2,3);o.setTemporaryControl(a,s,{blendInDuration:1}),o.onLateUpdate(.5),t(o.camera.position.x).toBeCloseTo((n.x+a.x)/2),t(o.camera.position.y).toBeCloseTo((n.y+a.y)/2),o.onLateUpdate(.5),t(o.camera.position).toEqual(a)}),o("third-person camera blends temporary control back to the current gameplay pose",async()=>{const e=new C;v();const o=new u;o.actor=e,o.autoActivate=!0,o.collision=!1,o.fixedBehind=!1,o.smoothCamera=!1,await o.onInit();const n=new p(10,4,2);o.setTemporaryControl(n,new p(1,2,3),{blendInDuration:0}),o.onLateUpdate(1/60),e.position.x=2,o.clearTemporaryControl(1),o.onLateUpdate(.5);const a=new p(2,o.offsetY,-o.distance);t(o.camera.position.x).toBeCloseTo((n.x+a.x)/2),t(o.camera.position.y).toBeCloseTo((n.y+a.y)/2)}),o("third-person camera applies collision to temporary control",async()=>{const e=new C;v();const o=s.get(h);n.mocked(o.sphereCast).mockReturnValue({hasHit:!0,distance:2});const a=new u;a.actor=e,a.autoActivate=!0,a.collision=!0,a.fixedBehind=!1,a.smoothCamera=!1,await a.onInit(),a.setTemporaryControl(new p(0,0,10),new p,{blendInDuration:0}),a.onLateUpdate(1/60),t(a.camera.position.z).toBeCloseTo(1.95)}),o("third-person camera pans toward a temporary look-at target without repositioning",async()=>{const e=new C;v();const o=new u;o.actor=e,o.autoActivate=!0,o.collision=!1,o.fixedBehind=!1,o.smoothCamera=!1,await o.onInit(),o.onLateUpdate(1/60);const n=o.camera.position.clone(),a=o.camera.quaternion.clone(),s=new p(0,2,-20),r=new i;r.position.copy(n),r.lookAt(s);const c=a.angleTo(r.quaternion);o.setTemporaryLookAt(s,{blendInDuration:1}),o.onLateUpdate(.5),t(o.camera.position).toEqual(n),t(a.angleTo(o.camera.quaternion)).toBeCloseTo(c/2)}),o("third-person camera blends a temporary field of view and restores it",async()=>{const e=new C;v();const o=new u;o.actor=e,o.autoActivate=!0,o.collision=!1,await o.onInit();const n=o.camera.fov;o.setTemporaryLookAt(new p(0,2,10),{blendInDuration:1,blendOutDuration:1,fieldOfView:35}),o.onLateUpdate(1),t(o.camera.fov).toBe(35),o.clearTemporaryControl(),o.onLateUpdate(.5),t(o.camera.fov).toBeCloseTo((35+n)/2),o.onLateUpdate(.5),t(o.camera.fov).toBe(n)});class C extends m{constructor(){super(),this.__isInitialised=!0}}/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=materializer-dynamic-transform.test.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{Subject as e}from"rxjs";import{Group as t,Object3D as n,Quaternion as s,Scene as o,Vector3 as a}from"three";import{describe as i,expect as r,it as c,vi as d}from"vitest";d.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&d.fn()),e[t]),set:(e,t,n)=>(e[t]=n,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),d.mock("../gameplay/actors/builtin/index.js",()=>({default:{}})),d.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),d.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),d.mock("../rendering.js",()=>({RenderingView:class{},setRenderingPaused:d.fn()}));import{PhysicsBodyType as f}from"../gameplay/services/physics/physics-system.js";import{SceneMaterializer as l}from"../scene/materializer.js";function p(e){return{name:e.id,...e}}i("SceneMaterializer dynamic transforms",()=>{c("preserves a nested dynamic object world transform when moving it to the scene root",async()=>{const i=new o,c=function(t){const n={getObjects:()=>[],onCreate:()=>{},onUpdate:()=>{},onRemove:()=>{}},s={onCreate:new e,onDelete:new e,onUpdate:new e,getAsset:d.fn(async()=>null),getAssets:d.fn(async()=>[])};return new l(t,n,s,{},{renderer:{shadowMap:{needsUpdate:!1}}},[],[],{create:async()=>null,initActor:async()=>{}})}(i);c.inEditor=!1;const m=[.2,.7,-.1],y=[0,0,0],u=p({id:"parent",type:"group",position:[10,4,-7],rotation:m,scale:[2,3,4],children:[p({id:"dynamic-child",type:"shape_mesh",shape:"box",position:[3,-2,5],rotation:y,scale:[.5,1.5,2],physics:{type:f.dynamic}})]});await c.materialize(u);const g=i.getObjectByName("dynamic-child");r(g?.parent).toBe(i);const h=new t;h.position.set(10,4,-7),h.rotation.set(...m),h.scale.set(2,3,4);const w=new n;w.position.set(3,-2,5),w.rotation.set(...y),w.scale.set(.5,1.5,2),h.add(w);const v=w.getWorldPosition(new a),j=w.getWorldQuaternion(new s),b=w.getWorldScale(new a);r(g.position.distanceTo(v)).toBeLessThan(1e-6),r(1-Math.abs(g.quaternion.dot(j))).toBeLessThan(1e-6),r(g.scale.distanceTo(b)).toBeLessThan(1e-6)})});/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -1,4 +1,4 @@
1
- import{describe as e,expect as t,it as s,vi as a}from"vitest";import{NavMeshWorkerPool as n}from"../gameplay/ai/dynamic-tiled-navmesh.js";e("NavMeshWorkerPool",()=>{s("creates workers lazily and caps concurrent work",()=>{const e=[],s=new n(()=>{const t=new o;return e.push(t),t}),l={},r=a.fn();t(e).toHaveLength(0);for(let e=0;e<8;e++)s.schedule(l,{tileX:e},void 0,r,24);t(e).toHaveLength(4),t(e.flatMap(e=>e.messages)).toHaveLength(4),e[0].complete(0),t(e).toHaveLength(4),t(e.flatMap(e=>e.messages)).toHaveLength(5),t(r).toHaveBeenCalledTimes(1)}),s("does not deliver queued or active results after an owner is cancelled",()=>{const e=[],s=new n(()=>{const t=new o;return e.push(t),t}),l={},r=a.fn();s.schedule(l,{tileX:0},void 0,r,1),s.schedule(l,{tileX:1},void 0,r,1),s.cancel(l),e[0].complete(0),t(r).not.toHaveBeenCalled(),t(e[0].messages).toHaveLength(1)})});class o{constructor(){this.onmessage=null,this.onerror=null,this.messages=[],this.terminate=a.fn()}postMessage(e){this.messages.push(e)}complete(e){this.onmessage?.({data:{tileX:e,tileY:0,navMeshData:new Uint8Array}})}}/*
1
+ import{describe as e,expect as t,it as s,vi as a}from"vitest";import*as n from"three";import{getTileBounds as o,NavMeshWorkerPool as l}from"../gameplay/ai/dynamic-tiled-navmesh.js";e("getTileBounds",()=>{s("creates adjacent fixed-size tiles relative to the navmesh minimum",()=>{const e=new n.Vector3(-100,-5,-200),s=new n.Vector3(300,25,400);t(o(e,s,2,3,10)).toEqual([[-80,-5,-170],[-70,25,-160]])})}),e("NavMeshWorkerPool",()=>{s("creates workers lazily and caps concurrent work",()=>{const e=[],s=new l(()=>{const t=new r;return e.push(t),t}),n={},o=a.fn();t(e).toHaveLength(0);for(let e=0;e<8;e++)s.schedule(n,{tileX:e},void 0,o,24);t(e).toHaveLength(4),t(e.flatMap(e=>e.messages)).toHaveLength(4),e[0].complete(0),t(e).toHaveLength(4),t(e.flatMap(e=>e.messages)).toHaveLength(5),t(o).toHaveBeenCalledTimes(1)}),s("does not deliver queued or active results after an owner is cancelled",()=>{const e=[],s=new l(()=>{const t=new r;return e.push(t),t}),n={},o=a.fn();s.schedule(n,{tileX:0},void 0,o,1),s.schedule(n,{tileX:1},void 0,o,1),s.cancel(n),e[0].complete(0),t(o).not.toHaveBeenCalled(),t(e[0].messages).toHaveLength(1)})});class r{constructor(){this.onmessage=null,this.onerror=null,this.messages=[],this.terminate=a.fn()}postMessage(e){this.messages.push(e)}complete(e){this.onmessage?.({data:{tileX:e,tileY:0,navMeshData:new Uint8Array}})}}/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=packed-json.test.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{describe as e,expect as a,test as t}from"vitest";import{decodeStorageValue as s,parseStorageJson as r,serializeStorageJson as n}from"../scene/storage/packed-json.js";const o="@hology/packed-json:1:gzip:base64:";e("packed storage JSON",()=>{t("packs dense scene fields and restores their original runtime shapes",()=>{const e=Array.from({length:100},(e,a)=>({i:a,y:Math.sin(a)})),t=Array.from({length:100},(e,a)=>({i:a,w:[0,.25,.75]})),s={id:"scene",data:[{id:"landscape",name:"Landscape",landscape:{options:{},heightMaps:[{x:0,y:0,points:e}],holes:t},vertexMaterials:t,paintedScatter:{palette:[],brush:{radius:1,density:1},cells:[{key:"0:0",revision:1,instances:Array.from({length:50},(e,a)=>({id:`instance-${a}`,paletteId:"trees",position:[a,0,a],rotation:[0,0,0],scale:[1,1,1]}))}]}}]},i=n(s),l=JSON.parse(i);a(l.data[0].landscape.heightMaps).toMatch(o),a(l.data[0].landscape.holes).toMatch(o),a(l.data[0].vertexMaterials).toMatch(o),a(l.data[0].paintedScatter.cells[0].instances).toMatch(o),a(r(i)).toEqual(s)}),t("packs secondary-motion paint and decodes already-parsed bundle data",()=>{const e={mesh:{secondaryMotion:{version:1,regions:[{id:"cloth",paintedVertices:Array.from({length:100},(e,a)=>[a,a/100])}]}}},t=JSON.parse(n(e));a(t.mesh.secondaryMotion.regions[0].paintedVertices).toMatch(o),a(s(t)).toEqual(e)}),t("leaves small selected values readable",()=>{const e={vertexMaterials:[{i:2,w:[1,0,0]}]},t=n(e);a(JSON.parse(t).vertexMaterials).toEqual(e.vertexMaterials),a(r(t)).toEqual(e)}),t("leaves unrelated large arrays readable",()=>{const e={children:Array.from({length:100},(e,a)=>({id:`${a}`,name:`Object ${a}`}))},t=JSON.parse(n(e));a(t.children).toEqual(e.children)})});/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=secondary-motion.test.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{describe as e,expect as r,test as t}from"vitest";import{decodeSecondaryMotionData as n,encodeSecondaryMotionData as o,SECONDARY_MOTION_BINARY_VERSION as s}from"../secondary-motion/data.js";import{colorConstraints as a,extractPaintedIslands as i,generateClothSecondaryMotionRegion as c,generateStrandCenterline as l,secondaryMotionTopologyHash as d}from"../secondary-motion/generation.js";import{SecondaryMotionFixedStep as y,secondaryMotionTransformDiscontinuous as p,selectSecondaryMotionLod as m}from"../secondary-motion/runtime.js";const A={positions:new Float32Array([0,0,0,1,0,0,0,1,0,1,1,0,0,2,0,1,2,0]),indices:new Uint16Array([0,1,2,1,3,2,2,3,4,3,5,4])};e("secondary motion generation",()=>{t("extracts islands and generates a root-to-tip strand",()=>{const e=i(A,new Float32Array([.05,.05,.5,.5,1,1]));r(e).toHaveLength(1),r(Array.from(e[0].rootVertices)).toEqual(r.arrayContaining([0,1]));const t=l(A,e[0],5);r(t).toHaveLength(15),r(t[1]).toBeCloseTo(0),r(t[13]).toBeCloseTo(2)}),t("fingerprints topology deterministically and colors conflicts",()=>{r(d(A)).toBe(d(A)),r(Array.from(a(new Uint32Array([0,1,1,2,2,3,0,3])))).toEqual([0,1,0,1])}),t("builds colored cloth edges and pins unpainted particles",()=>{const e=c("cape",A,new Float32Array([0,0,.5,.5,1,1]));r(e.particleInverseMass[0]).toBe(0),r(e.particleInverseMass[5]).toBe(1),r(e.constraints.length).toBeGreaterThan(0);for(let t=0;t<e.constraintColors.length;t++)for(let n=t+1;n<e.constraintColors.length;n++){if(e.constraintColors[t]!==e.constraintColors[n])continue;const o=[e.constraints[2*t],e.constraints[2*t+1]],s=[e.constraints[2*n],e.constraints[2*n+1]];r(o.some(e=>s.includes(e))).toBe(!1)}}),t("maps coincident render vertices to one particle across imported mesh seams",()=>{const e={positions:new Float32Array([0,0,0,1,0,0,0,1,0,0,0,0,0,1,0,-1,0,0]),indices:new Uint16Array([0,1,2,3,4,5])},t=c("seam",e,new Float32Array([1,1,1,1,1,1]));r(t.particleInverseMass).toHaveLength(4),r(t.renderVertexIndices[0]).toBe(t.renderVertexIndices[3]),r(t.renderVertexIndices[2]).toBe(t.renderVertexIndices[4])}),t("round trips generated binary arrays",()=>{const e=o({version:s,regions:[{id:"hair",topologyHash:"abcd",particlePositions:new Float32Array([0,1,2]),particleInverseMass:new Float32Array([0]),particleSkinIndices:new Uint16Array([1,2,3,4]),particleSkinWeights:new Float32Array([1,0,0,0]),constraints:new Uint32Array([0,1]),constraintRest:new Float32Array([1]),constraintColors:new Uint16Array([0]),renderVertexIndices:new Uint32Array([2]),renderBindings:new Float32Array([0,1,0,0]),influence:new Float32Array([.5])}]}),t=n(e);r(t.regions[0].id).toBe("hair"),r(Array.from(t.regions[0].particleSkinIndices)).toEqual([1,2,3,4]),r(Array.from(t.regions[0].influence)).toEqual([.5])}),t("bounds fixed steps, detects teleports, and selects lod",()=>{const e=new y({frequency:60,maxSubsteps:3});r(e.consume(1/30)).toBe(2),r(e.consume(1)).toBe(0),r(p([0,0,0,0,0,0,1],[3,0,0,0,0,0,1])).toBe(!0),r(m(12,!0,!1)).toBe("mid"),r(m(1,!1,!0)).toBe("sleep")})});/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hology/core",
3
- "version": "0.0.240",
3
+ "version": "0.0.242",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -31,6 +31,9 @@
31
31
  "./scene/sky": "./dist/scene/sky.js",
32
32
  "./scene/storage/storage": "./dist/scene/storage/storage.js",
33
33
  "./scene/asset-resource-loader": "./dist/scene/asset-resource-loader.js",
34
+ "./secondary-motion/data": "./dist/secondary-motion/data.js",
35
+ "./secondary-motion/generation": "./dist/secondary-motion/generation.js",
36
+ "./secondary-motion/runtime": "./dist/secondary-motion/runtime.js",
34
37
  "./scene/assets-provider": "./dist/scene/assets-provider.js",
35
38
  "./scene/materials/utils/material-painting": "./dist/scene/materials/utils/material-painting.js",
36
39
  "./scene/landscape/utils": "./dist/scene/landscape/utils.js",
@@ -213,6 +216,7 @@
213
216
  "@hology/nebula": "^0.0.161",
214
217
  "@plumier/reflect": "^1.1.0",
215
218
  "@recast-navigation/three": "0.39.0",
219
+ "fflate": "^0.8.3",
216
220
  "recast-navigation": "0.39.0",
217
221
  "rxjs": "7.8.1",
218
222
  "three-mesh-bvh": "^0.7.5",
@@ -235,6 +239,6 @@
235
239
  "terser": "^5.25.0",
236
240
  "typescript": "~5.8.2",
237
241
  "vitest": "^4.1.9",
238
- "@hology/webgpu-renderer": "^0.1.7"
242
+ "@hology/webgpu-renderer": "^0.1.8"
239
243
  }
240
244
  }