diff --git a/.gitignore b/.gitignore index 249ab8c2d5..7b036d513c 100644 --- a/.gitignore +++ b/.gitignore @@ -38,7 +38,8 @@ playwright-report/ # Claude Code files .claude/ CLAUDE.md - +docs/superpowers +notes/ .husky/post-checkout .husky/post-commit .husky/pre-push diff --git a/docs/en/graphics/material/examples/shaderlab-05-advance.mdx b/docs/en/graphics/material/examples/shaderlab-05-advance.mdx index 4a21ce1c25..4e8c257ea1 100644 --- a/docs/en/graphics/material/examples/shaderlab-05-advance.mdx +++ b/docs/en/graphics/material/examples/shaderlab-05-advance.mdx @@ -190,7 +190,6 @@ gl_FragColor = vec4(lighting, 1.0); The engine provides a rich set of built-in variables that can be used directly, such as transformation matrix variables: ```glsl -mat4 renderer_LocalMat; // Local transformation matrix mat4 renderer_ModelMat; // Model matrix mat4 renderer_MVMat; // Model-view matrix mat4 renderer_MVPMat; // MVP matrix diff --git a/docs/en/graphics/material/shaderAPI.mdx b/docs/en/graphics/material/shaderAPI.mdx index f546447e19..63e646c37e 100644 --- a/docs/en/graphics/material/shaderAPI.mdx +++ b/docs/en/graphics/material/shaderAPI.mdx @@ -27,7 +27,6 @@ vec4 fog(vec4 color, vec3 positionVS); Provides system variables for model space, view space, world space, and camera coordinates: ```glsl -mat4 renderer_LocalMat; mat4 renderer_ModelMat; mat4 camera_ViewMat; mat4 camera_ProjMat; diff --git a/docs/en/graphics/material/variables.mdx b/docs/en/graphics/material/variables.mdx index f64f02b3a3..779f86130b 100644 --- a/docs/en/graphics/material/variables.mdx +++ b/docs/en/graphics/material/variables.mdx @@ -25,7 +25,6 @@ Below are the engine's built-in variables for reference when writing Shaders: | Name | Type | Description | | :---------------- | :--- | :--------------------------- | -| renderer_LocalMat | mat4 | Model local coordinate matrix | | renderer_ModelMat | mat4 | Model world coordinate matrix | | renderer_MVMat | mat4 | Model view matrix | | renderer_MVPMat | mat4 | Model view projection matrix | diff --git a/docs/en/script/edit.mdx b/docs/en/script/edit.mdx index 60feec35e5..95c8561a11 100644 --- a/docs/en/script/edit.mdx +++ b/docs/en/script/edit.mdx @@ -24,14 +24,14 @@ For more information about the code editor, please check [Code Editor](/en/docs/ After creating a script asset in the scene editor, double-click the script to open the code editor. Scripts in Galacean need to be written in [Typescript](https://www.typescriptlang.org/), and new scripts are created based on built-in templates by default. Additionally, the Galacean code editor is based on Monaco, with shortcuts similar to VSCode. After modifying the script, press `Ctrl/⌘ + S` to save, and the real-time preview area on the right will show the latest scene effects. -> Tip: The Galacean code editor currently supports `.ts`, `.gs`, and `.glsl` file editing. +> Tip: The Galacean code editor currently supports `.ts`, `.shader`, and `.glsl` file editing. ## File Preview 1. **File Search** Quickly search for files in the project -2. **Code Filter** Whether to display only code files ( `.ts`, `.gs`, `.glsl` ) in the file tree +2. **Code Filter** Whether to display only code files ( `.ts`, `.shader`, `.glsl` ) in the file tree 3. **Built-in Files** Used to display which files are non-editable internal files 4. **Expand/Hide** Toggle the expansion or hiding of folders 5. **Code Files** Editable code files will display corresponding file type thumbnails diff --git a/docs/zh/graphics/material/examples/shaderlab-05-advance.mdx b/docs/zh/graphics/material/examples/shaderlab-05-advance.mdx index c2d4d779dc..a9490581ac 100644 --- a/docs/zh/graphics/material/examples/shaderlab-05-advance.mdx +++ b/docs/zh/graphics/material/examples/shaderlab-05-advance.mdx @@ -193,7 +193,6 @@ void frag() { 引擎提供了丰富的内置变量,直接声明使用即可,比如变换矩阵相关变量: ```glsl -mat4 renderer_LocalMat; // 本地变换矩阵 mat4 renderer_ModelMat; // 模型矩阵 mat4 renderer_MVMat; // 模型视图矩阵 mat4 renderer_MVPMat; // MVP矩阵 diff --git a/docs/zh/graphics/material/shaderAPI.mdx b/docs/zh/graphics/material/shaderAPI.mdx index d6fcd4ac25..0c0879054b 100644 --- a/docs/zh/graphics/material/shaderAPI.mdx +++ b/docs/zh/graphics/material/shaderAPI.mdx @@ -29,7 +29,6 @@ vec4 fog(vec4 color, vec3 positionVS); 提供了模型空间、视图空间、世界空间、相机坐标等[系统变量](/docs/graphics/material/variables/): ```glsl -mat4 renderer_LocalMat; mat4 renderer_ModelMat; mat4 camera_ViewMat; mat4 camera_ProjMat; diff --git a/docs/zh/graphics/material/variables.mdx b/docs/zh/graphics/material/variables.mdx index 6059e0d0e1..48deab814b 100644 --- a/docs/zh/graphics/material/variables.mdx +++ b/docs/zh/graphics/material/variables.mdx @@ -25,7 +25,6 @@ Shader 代码中会经常用到内置变量,一种是**逐顶点**的 `attribu | 名字 | 类型 | 解释 | | :----------------- | :--- | ------------------ | -| renderer_LocalMat | mat4 | 模型本地坐标系矩阵 | | renderer_ModelMat | mat4 | 模型世界坐标系矩阵 | | renderer_MVMat | mat4 | 模型视口矩阵 | | renderer_MVPMat | mat4 | 模型视口投影矩阵 | diff --git a/docs/zh/script/edit.mdx b/docs/zh/script/edit.mdx index 28cdd108a5..4cb0255eff 100644 --- a/docs/zh/script/edit.mdx +++ b/docs/zh/script/edit.mdx @@ -24,14 +24,14 @@ Galacean Editor 提供了一个功能强大的代码编辑器,提供了代码 在场景编辑器中创建脚本资产后,双击该脚本即可打开代码编辑器。Galacean 中的脚本需使用 [Typescript](https://www.typescriptlang.org/) 语言编写,同时新脚本默认基于内置模板创建。另外,Galacean 的代码编辑器基于 Monaco,快捷键与 VSCode 类似。修改脚本后,按 `Ctrl/⌘ + S` 保存,右侧实时预览区展现最新场景效果。 -> 提示:Galacean 代码编辑器目前支持 `.ts` `.gs` 和 `.glsl` 的文件编辑 +> 提示:Galacean 代码编辑器目前支持 `.ts` `.shader` 和 `.glsl` 的文件编辑 ## 文件预览 1. **文件搜索** 可快速搜索项目中的文件 -2. **代码筛选** 文件树是否仅显示代码文件 ( `.ts` `.gs` `.glsl` ) +2. **代码筛选** 文件树是否仅显示代码文件 ( `.ts` `.shader` `.glsl` ) 3. **内置文件** 用来显示哪些文件是不可编辑的内部文件 4. **展开/隐藏** 可切换文件夹的展开或隐藏 5. **代码文件** 可编辑的代码文件会显示对应的文件类型的缩略图标 diff --git a/e2e/.dev/physx.release.js b/e2e/.dev/physx.release.js index 426e4ca185..f816e9dce3 100644 --- a/e2e/.dev/physx.release.js +++ b/e2e/.dev/physx.release.js @@ -1,2 +1,2 @@ -var PHYSX=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};if(typeof __filename!="undefined"){_scriptName=__filename}else if(ENVIRONMENT_IS_WORKER){_scriptName=self.location.href}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{if(isFileURI(url)){return new Promise((resolve,reject)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){resolve(xhr.response);return}reject(xhr.status)};xhr.onerror=reject;xhr.send(null)})}var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var EXITSTATUS;var isFileURI=filename=>filename.startsWith("file://");var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["P"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){return "https://mdn.alipayobjects.com/rms/afts/file/A*RkDQSaUOhxsAAAAAgCAAAAgAehQnAQ/physx.release.wasm"}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!isFileURI(binaryFile)&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var __abort_js=()=>abort("");var createNamedFunction=(name,func)=>Object.defineProperty(func,"name",{value:name});var emval_freelist=[];var emval_handles=[0,1,,1,null,1,true,1,false,1];var BindingError=class BindingError extends Error{constructor(message){super(message);this.name="BindingError"}};var throwBindingError=message=>{throw new BindingError(message)};var Emval={toValue:handle=>{if(!handle){throwBindingError(`Cannot use deleted val. handle = ${handle}`)}return emval_handles[handle]},toHandle:value=>{switch(value){case undefined:return 2;case null:return 4;case true:return 6;case false:return 8;default:{const handle=emval_freelist.pop()||emval_handles.length;emval_handles[handle]=value;emval_handles[handle+1]=1;return handle}}}};class PureVirtualError extends Error{}var AsciiToString=ptr=>{var str="";while(1){var ch=HEAPU8[ptr++];if(!ch)return str;str+=String.fromCharCode(ch)}};var registeredInstances={};var getBasestPointer=(class_,ptr)=>{if(ptr===undefined){throwBindingError("ptr should not be undefined")}while(class_.baseClass){ptr=class_.upcast(ptr);class_=class_.baseClass}return ptr};var registerInheritedInstance=(class_,ptr,instance)=>{ptr=getBasestPointer(class_,ptr);if(registeredInstances.hasOwnProperty(ptr)){throwBindingError(`Tried to register registered instance: ${ptr}`)}else{registeredInstances[ptr]=instance}};var registeredTypes={};var getTypeName=type=>{var ptr=___getTypeName(type);var rv=AsciiToString(ptr);_free(ptr);return rv};var requireRegisteredType=(rawType,humanName)=>{var impl=registeredTypes[rawType];if(undefined===impl){throwBindingError(`${humanName} has unknown type ${getTypeName(rawType)}`)}return impl};var unregisterInheritedInstance=(class_,ptr)=>{ptr=getBasestPointer(class_,ptr);if(registeredInstances.hasOwnProperty(ptr)){delete registeredInstances[ptr]}else{throwBindingError(`Tried to unregister unregistered instance: ${ptr}`)}};var detachFinalizer=handle=>{};var finalizationRegistry=false;var runDestructor=$$=>{if($$.smartPtr){$$.smartPtrType.rawDestructor($$.smartPtr)}else{$$.ptrType.registeredClass.rawDestructor($$.ptr)}};var releaseClassHandle=$$=>{$$.count.value-=1;var toDelete=0===$$.count.value;if(toDelete){runDestructor($$)}};var attachFinalizer=handle=>{if(!globalThis.FinalizationRegistry){attachFinalizer=handle=>handle;return handle}finalizationRegistry=new FinalizationRegistry(info=>{releaseClassHandle(info.$$)});attachFinalizer=handle=>{var $$=handle.$$;var hasSmartPtr=!!$$.smartPtr;if(hasSmartPtr){var info={$$};finalizationRegistry.register(handle,info,handle)}return handle};detachFinalizer=handle=>finalizationRegistry.unregister(handle);return attachFinalizer(handle)};var __embind_create_inheriting_constructor=(constructorName,wrapperType,properties)=>{constructorName=AsciiToString(constructorName);wrapperType=requireRegisteredType(wrapperType,"wrapper");properties=Emval.toValue(properties);var registeredClass=wrapperType.registeredClass;var wrapperPrototype=registeredClass.instancePrototype;var baseClass=registeredClass.baseClass;var baseClassPrototype=baseClass.instancePrototype;var baseConstructor=registeredClass.baseClass.constructor;var ctor=createNamedFunction(constructorName,function(...args){for(var name of registeredClass.baseClass.pureVirtualFunctions){if(this[name]===baseClassPrototype[name]){throw new PureVirtualError(`Pure virtual function ${name} must be implemented in JavaScript`)}}Object.defineProperty(this,"__parent",{value:wrapperPrototype});this["__construct"](...args)});wrapperPrototype["__construct"]=function __construct(...args){if(this===wrapperPrototype){throwBindingError("Pass correct 'this' to __construct")}var inner=baseConstructor["implement"](this,...args);detachFinalizer(inner);var $$=inner.$$;inner["notifyOnDestruction"]();$$.preservePointerOnDelete=true;Object.defineProperties(this,{$$:{value:$$}});attachFinalizer(this);registerInheritedInstance(registeredClass,$$.ptr,this)};wrapperPrototype["__destruct"]=function __destruct(){if(this===wrapperPrototype){throwBindingError("Pass correct 'this' to __destruct")}detachFinalizer(this);unregisterInheritedInstance(registeredClass,this.$$.ptr)};ctor.prototype=Object.create(wrapperPrototype);Object.assign(ctor.prototype,properties);return Emval.toHandle(ctor)};var structRegistrations={};var runDestructors=destructors=>{while(destructors.length){var ptr=destructors.pop();var del=destructors.pop();del(ptr)}};function readPointer(pointer){return this.fromWireType(HEAPU32[pointer>>2])}var awaitingDependencies={};var typeDependencies={};var InternalError=class InternalError extends Error{constructor(message){super(message);this.name="InternalError"}};var throwInternalError=message=>{throw new InternalError(message)};var whenDependentTypesAreResolved=(myTypes,dependentTypes,getTypeConverters)=>{myTypes.forEach(type=>typeDependencies[type]=dependentTypes);function onComplete(typeConverters){var myTypeConverters=getTypeConverters(typeConverters);if(myTypeConverters.length!==myTypes.length){throwInternalError("Mismatched type converter count")}for(var i=0;i{typeConverters[i]=registeredTypes[dt];++registered;if(registered===unregisteredTypes.length){onComplete(typeConverters)}})}}if(0===unregisteredTypes.length){onComplete(typeConverters)}};var __embind_finalize_value_object=structType=>{var reg=structRegistrations[structType];delete structRegistrations[structType];var rawConstructor=reg.rawConstructor;var rawDestructor=reg.rawDestructor;var fieldRecords=reg.fields;var fieldTypes=fieldRecords.map(field=>field.getterReturnType).concat(fieldRecords.map(field=>field.setterArgumentType));whenDependentTypesAreResolved([structType],fieldTypes,fieldTypes=>{var fields={};for(var[i,field]of fieldRecords.entries()){const getterReturnType=fieldTypes[i];const getter=field.getter;const getterContext=field.getterContext;const setterArgumentType=fieldTypes[i+fieldRecords.length];const setter=field.setter;const setterContext=field.setterContext;fields[field.fieldName]={read:ptr=>getterReturnType.fromWireType(getter(getterContext,ptr)),write:(ptr,o)=>{var destructors=[];setter(setterContext,ptr,setterArgumentType.toWireType(destructors,o));runDestructors(destructors)},optional:getterReturnType.optional}}return[{name:reg.name,fromWireType:ptr=>{var rv={};for(var i in fields){rv[i]=fields[i].read(ptr)}rawDestructor(ptr);return rv},toWireType:(destructors,o)=>{for(var fieldName in fields){if(!(fieldName in o)&&!fields[fieldName].optional){throw new TypeError(`Missing field: "${fieldName}"`)}}var ptr=rawConstructor();for(fieldName in fields){fields[fieldName].write(ptr,o[fieldName])}if(destructors!==null){destructors.push(rawDestructor,ptr)}return ptr},readValueFromPointer:readPointer,destructorFunction:rawDestructor}]})};function sharedRegisterType(rawType,registeredInstance,options={}){var name=registeredInstance.name;if(!rawType){throwBindingError(`type "${name}" must have a positive integer typeid pointer`)}if(registeredTypes.hasOwnProperty(rawType)){if(options.ignoreDuplicateRegistrations){return}else{throwBindingError(`Cannot register type '${name}' twice`)}}registeredTypes[rawType]=registeredInstance;delete typeDependencies[rawType];if(awaitingDependencies.hasOwnProperty(rawType)){var callbacks=awaitingDependencies[rawType];delete awaitingDependencies[rawType];callbacks.forEach(cb=>cb())}}function registerType(rawType,registeredInstance,options={}){return sharedRegisterType(rawType,registeredInstance,options)}var integerReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?pointer=>HEAP8[pointer]:pointer=>HEAPU8[pointer];case 2:return signed?pointer=>HEAP16[pointer>>1]:pointer=>HEAPU16[pointer>>1];case 4:return signed?pointer=>HEAP32[pointer>>2]:pointer=>HEAPU32[pointer>>2];case 8:return signed?pointer=>HEAP64[pointer>>3]:pointer=>HEAPU64[pointer>>3];default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};var __embind_register_bigint=(primitiveType,name,size,minRange,maxRange)=>{name=AsciiToString(name);const isUnsignedType=minRange===0n;let fromWireType=value=>value;if(isUnsignedType){const bitSize=size*8;fromWireType=value=>BigInt.asUintN(bitSize,value);maxRange=fromWireType(maxRange)}registerType(primitiveType,{name,fromWireType,toWireType:(destructors,value)=>{if(typeof value=="number"){value=BigInt(value)}return value},readValueFromPointer:integerReadValueFromPointer(name,size,!isUnsignedType),destructorFunction:null})};var __embind_register_bool=(rawType,name,trueValue,falseValue)=>{name=AsciiToString(name);registerType(rawType,{name,fromWireType:function(wt){return!!wt},toWireType:function(destructors,o){return o?trueValue:falseValue},readValueFromPointer:function(pointer){return this.fromWireType(HEAPU8[pointer])},destructorFunction:null})};var shallowCopyInternalPointer=o=>({count:o.count,deleteScheduled:o.deleteScheduled,preservePointerOnDelete:o.preservePointerOnDelete,ptr:o.ptr,ptrType:o.ptrType,smartPtr:o.smartPtr,smartPtrType:o.smartPtrType});var throwInstanceAlreadyDeleted=obj=>{function getInstanceTypeName(handle){return handle.$$.ptrType.registeredClass.name}throwBindingError(getInstanceTypeName(obj)+" instance already deleted")};var deletionQueue=[];var flushPendingDeletes=()=>{while(deletionQueue.length){var obj=deletionQueue.pop();obj.$$.deleteScheduled=false;obj["delete"]()}};var delayFunction;var init_ClassHandle=()=>{let proto=ClassHandle.prototype;Object.assign(proto,{isAliasOf(other){if(!(this instanceof ClassHandle)){return false}if(!(other instanceof ClassHandle)){return false}var leftClass=this.$$.ptrType.registeredClass;var left=this.$$.ptr;other.$$=other.$$;var rightClass=other.$$.ptrType.registeredClass;var right=other.$$.ptr;while(leftClass.baseClass){left=leftClass.upcast(left);leftClass=leftClass.baseClass}while(rightClass.baseClass){right=rightClass.upcast(right);rightClass=rightClass.baseClass}return leftClass===rightClass&&left===right},clone(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.preservePointerOnDelete){this.$$.count.value+=1;return this}else{var clone=attachFinalizer(Object.create(Object.getPrototypeOf(this),{$$:{value:shallowCopyInternalPointer(this.$$)}}));clone.$$.count.value+=1;clone.$$.deleteScheduled=false;return clone}},delete(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}detachFinalizer(this);releaseClassHandle(this.$$);if(!this.$$.preservePointerOnDelete){this.$$.smartPtr=undefined;this.$$.ptr=undefined}},isDeleted(){return!this.$$.ptr},deleteLater(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}deletionQueue.push(this);if(deletionQueue.length===1&&delayFunction){delayFunction(flushPendingDeletes)}this.$$.deleteScheduled=true;return this}});const symbolDispose=Symbol.dispose;if(symbolDispose){proto[symbolDispose]=proto["delete"]}};function ClassHandle(){}var registeredPointers={};var ensureOverloadTable=(proto,methodName,humanName)=>{if(undefined===proto[methodName].overloadTable){var prevFunc=proto[methodName];proto[methodName]=function(...args){if(!proto[methodName].overloadTable.hasOwnProperty(args.length)){throwBindingError(`Function '${humanName}' called with an invalid number of arguments (${args.length}) - expects one of (${proto[methodName].overloadTable})!`)}return proto[methodName].overloadTable[args.length].apply(this,args)};proto[methodName].overloadTable=[];proto[methodName].overloadTable[prevFunc.argCount]=prevFunc}};var exposePublicSymbol=(name,value,numArguments)=>{if(Module.hasOwnProperty(name)){if(undefined===numArguments||undefined!==Module[name].overloadTable&&undefined!==Module[name].overloadTable[numArguments]){throwBindingError(`Cannot register public name '${name}' twice`)}ensureOverloadTable(Module,name,name);if(Module[name].overloadTable.hasOwnProperty(numArguments)){throwBindingError(`Cannot register multiple overloads of a function with the same number of arguments (${numArguments})!`)}Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}};var char_0=48;var char_9=57;var makeLegalFunctionName=name=>{name=name.replace(/[^a-zA-Z0-9_]/g,"$");var f=name.charCodeAt(0);if(f>=char_0&&f<=char_9){return`_${name}`}return name};function RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast){this.name=name;this.constructor=constructor;this.instancePrototype=instancePrototype;this.rawDestructor=rawDestructor;this.baseClass=baseClass;this.getActualType=getActualType;this.upcast=upcast;this.downcast=downcast;this.pureVirtualFunctions=[]}var upcastPointer=(ptr,ptrClass,desiredClass)=>{while(ptrClass!==desiredClass){if(!ptrClass.upcast){throwBindingError(`Expected null or instance of ${desiredClass.name}, got an instance of ${ptrClass.name}`)}ptr=ptrClass.upcast(ptr);ptrClass=ptrClass.baseClass}return ptr};var embindRepr=v=>{if(v===null){return"null"}var t=typeof v;if(t==="object"||t==="array"||t==="function"){return v.toString()}else{return""+v}};function constNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}function genericPointerToWireType(destructors,handle){var ptr;if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}if(this.isSmartPointer){ptr=this.rawConstructor();if(destructors!==null){destructors.push(this.rawDestructor,ptr)}return ptr}else{return 0}}if(!handle||!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(!this.isConst&&handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);if(this.isSmartPointer){if(undefined===handle.$$.smartPtr){throwBindingError("Passing raw pointer to smart pointer is illegal")}switch(this.sharingPolicy){case 0:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}break;case 1:ptr=handle.$$.smartPtr;break;case 2:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{var clonedHandle=handle["clone"]();ptr=this.rawShare(ptr,Emval.toHandle(()=>clonedHandle["delete"]()));if(destructors!==null){destructors.push(this.rawDestructor,ptr)}}break;default:throwBindingError("Unsupported sharing policy")}}return ptr}function nonConstNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}var downcastPointer=(ptr,ptrClass,desiredClass)=>{if(ptrClass===desiredClass){return ptr}if(undefined===desiredClass.baseClass){return null}var rv=downcastPointer(ptr,ptrClass,desiredClass.baseClass);if(rv===null){return null}return desiredClass.downcast(rv)};var getInheritedInstance=(class_,ptr)=>{ptr=getBasestPointer(class_,ptr);return registeredInstances[ptr]};var makeClassHandle=(prototype,record)=>{if(!record.ptrType||!record.ptr){throwInternalError("makeClassHandle requires ptr and ptrType")}var hasSmartPtrType=!!record.smartPtrType;var hasSmartPtr=!!record.smartPtr;if(hasSmartPtrType!==hasSmartPtr){throwInternalError("Both smartPtrType and smartPtr must be specified")}record.count={value:1};return attachFinalizer(Object.create(prototype,{$$:{value:record,writable:true}}))};function RegisteredPointer_fromWireType(ptr){var rawPointer=this.getPointee(ptr);if(!rawPointer){this.destructor(ptr);return null}var registeredInstance=getInheritedInstance(this.registeredClass,rawPointer);if(undefined!==registeredInstance){if(0===registeredInstance.$$.count.value){registeredInstance.$$.ptr=rawPointer;registeredInstance.$$.smartPtr=ptr;return registeredInstance["clone"]()}else{var rv=registeredInstance["clone"]();this.destructor(ptr);return rv}}function makeDefaultHandle(){if(this.isSmartPointer){return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this.pointeeType,ptr:rawPointer,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this,ptr})}}var actualType=this.registeredClass.getActualType(rawPointer);var registeredPointerRecord=registeredPointers[actualType];if(!registeredPointerRecord){return makeDefaultHandle.call(this)}var toType;if(this.isConst){toType=registeredPointerRecord.constPointerType}else{toType=registeredPointerRecord.pointerType}var dp=downcastPointer(rawPointer,this.registeredClass,toType.registeredClass);if(dp===null){return makeDefaultHandle.call(this)}if(this.isSmartPointer){return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp})}}var init_RegisteredPointer=()=>{Object.assign(RegisteredPointer.prototype,{getPointee(ptr){if(this.rawGetPointee){ptr=this.rawGetPointee(ptr)}return ptr},destructor(ptr){this.rawDestructor?.(ptr)},readValueFromPointer:readPointer,fromWireType:RegisteredPointer_fromWireType})};function RegisteredPointer(name,registeredClass,isReference,isConst,isSmartPointer,pointeeType,sharingPolicy,rawGetPointee,rawConstructor,rawShare,rawDestructor){this.name=name;this.registeredClass=registeredClass;this.isReference=isReference;this.isConst=isConst;this.isSmartPointer=isSmartPointer;this.pointeeType=pointeeType;this.sharingPolicy=sharingPolicy;this.rawGetPointee=rawGetPointee;this.rawConstructor=rawConstructor;this.rawShare=rawShare;this.rawDestructor=rawDestructor;if(!isSmartPointer&®isteredClass.baseClass===undefined){if(isConst){this.toWireType=constNoSmartPtrRawPointerToWireType;this.destructorFunction=null}else{this.toWireType=nonConstNoSmartPtrRawPointerToWireType;this.destructorFunction=null}}else{this.toWireType=genericPointerToWireType}}var replacePublicSymbol=(name,value,numArguments)=>{if(!Module.hasOwnProperty(name)){throwInternalError("Replacing nonexistent public symbol")}if(undefined!==Module[name].overloadTable&&undefined!==numArguments){Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}};var wasmTableMirror=[];var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var embind__requireFunction=(signature,rawFunction,isAsync=false)=>{signature=AsciiToString(signature);function makeDynCaller(){var rtn=getWasmTableEntry(rawFunction);return rtn}var fp=makeDynCaller();if(typeof fp!="function"){throwBindingError(`unknown function pointer with signature ${signature}: ${rawFunction}`)}return fp};class UnboundTypeError extends Error{}var throwUnboundTypeError=(message,types)=>{var unboundTypes=[];var seen={};function visit(type){if(seen[type]){return}if(registeredTypes[type]){return}if(typeDependencies[type]){typeDependencies[type].forEach(visit);return}unboundTypes.push(type);seen[type]=true}types.forEach(visit);throw new UnboundTypeError(`${message}: `+unboundTypes.map(getTypeName).join([", "]))};var __embind_register_class=(rawType,rawPointerType,rawConstPointerType,baseClassRawType,getActualTypeSignature,getActualType,upcastSignature,upcast,downcastSignature,downcast,name,destructorSignature,rawDestructor)=>{name=AsciiToString(name);getActualType=embind__requireFunction(getActualTypeSignature,getActualType);upcast&&=embind__requireFunction(upcastSignature,upcast);downcast&&=embind__requireFunction(downcastSignature,downcast);rawDestructor=embind__requireFunction(destructorSignature,rawDestructor);var legalFunctionName=makeLegalFunctionName(name);exposePublicSymbol(legalFunctionName,function(){throwUnboundTypeError(`Cannot construct ${name} due to unbound types`,[baseClassRawType])});whenDependentTypesAreResolved([rawType,rawPointerType,rawConstPointerType],baseClassRawType?[baseClassRawType]:[],base=>{base=base[0];var baseClass;var basePrototype;if(baseClassRawType){baseClass=base.registeredClass;basePrototype=baseClass.instancePrototype}else{basePrototype=ClassHandle.prototype}var constructor=createNamedFunction(name,function(...args){if(Object.getPrototypeOf(this)!==instancePrototype){throw new BindingError(`Use 'new' to construct ${name}`)}if(undefined===registeredClass.constructor_body){throw new BindingError(`${name} has no accessible constructor`)}var body=registeredClass.constructor_body[args.length];if(undefined===body){throw new BindingError(`Tried to invoke ctor of ${name} with invalid number of parameters (${args.length}) - expected (${Object.keys(registeredClass.constructor_body).toString()}) parameters instead!`)}return body.apply(this,args)});var instancePrototype=Object.create(basePrototype,{constructor:{value:constructor}});constructor.prototype=instancePrototype;var registeredClass=new RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast);if(registeredClass.baseClass){registeredClass.baseClass.__derivedClasses??=[];registeredClass.baseClass.__derivedClasses.push(registeredClass)}var referenceConverter=new RegisteredPointer(name,registeredClass,true,false,false);var pointerConverter=new RegisteredPointer(name+"*",registeredClass,false,false,false);var constPointerConverter=new RegisteredPointer(name+" const*",registeredClass,false,true,false);registeredPointers[rawType]={pointerType:pointerConverter,constPointerType:constPointerConverter};replacePublicSymbol(legalFunctionName,constructor);return[referenceConverter,pointerConverter,constPointerConverter]})};function usesDestructorStack(argTypes){for(var i=1;i{var array=[];for(var i=0;i>2])}return array};var getFunctionName=signature=>{signature=signature.trim();const argsIndex=signature.indexOf("(");if(argsIndex===-1)return signature;return signature.slice(0,argsIndex)};var __embind_register_class_class_function=(rawClassType,methodName,argCount,rawArgTypesAddr,invokerSignature,rawInvoker,fn,isAsync,isNonnullReturn)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);methodName=AsciiToString(methodName);methodName=getFunctionName(methodName);rawInvoker=embind__requireFunction(invokerSignature,rawInvoker,isAsync);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`${classType.name}.${methodName}`;function unboundTypesHandler(){throwUnboundTypeError(`Cannot call ${humanName} due to unbound types`,rawArgTypes)}if(methodName.startsWith("@@")){methodName=Symbol[methodName.substring(2)]}var proto=classType.registeredClass.constructor;if(undefined===proto[methodName]){unboundTypesHandler.argCount=argCount-1;proto[methodName]=unboundTypesHandler}else{ensureOverloadTable(proto,methodName,humanName);proto[methodName].overloadTable[argCount-1]=unboundTypesHandler}whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{var invokerArgsArray=[argTypes[0],null].concat(argTypes.slice(1));var func=craftInvokerFunction(humanName,invokerArgsArray,null,rawInvoker,fn,isAsync);if(undefined===proto[methodName].overloadTable){func.argCount=argCount-1;proto[methodName]=func}else{proto[methodName].overloadTable[argCount-1]=func}if(classType.registeredClass.__derivedClasses){for(const derivedClass of classType.registeredClass.__derivedClasses){if(!derivedClass.constructor.hasOwnProperty(methodName)){derivedClass.constructor[methodName]=func}}}return[]});return[]})};var __embind_register_class_constructor=(rawClassType,argCount,rawArgTypesAddr,invokerSignature,invoker,rawConstructor)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);invoker=embind__requireFunction(invokerSignature,invoker);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`constructor ${classType.name}`;if(undefined===classType.registeredClass.constructor_body){classType.registeredClass.constructor_body=[]}if(undefined!==classType.registeredClass.constructor_body[argCount-1]){throw new BindingError(`Cannot register multiple constructors with identical number of parameters (${argCount-1}) for class '${classType.name}'! Overload resolution is currently only performed using the parameter count, not actual type info!`)}classType.registeredClass.constructor_body[argCount-1]=()=>{throwUnboundTypeError(`Cannot construct ${classType.name} due to unbound types`,rawArgTypes)};whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{argTypes.splice(1,0,null);classType.registeredClass.constructor_body[argCount-1]=craftInvokerFunction(humanName,argTypes,null,invoker,rawConstructor);return[]});return[]})};var __embind_register_class_function=(rawClassType,methodName,argCount,rawArgTypesAddr,invokerSignature,rawInvoker,context,isPureVirtual,isAsync,isNonnullReturn)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);methodName=AsciiToString(methodName);methodName=getFunctionName(methodName);rawInvoker=embind__requireFunction(invokerSignature,rawInvoker,isAsync);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`${classType.name}.${methodName}`;if(methodName.startsWith("@@")){methodName=Symbol[methodName.substring(2)]}if(isPureVirtual){classType.registeredClass.pureVirtualFunctions.push(methodName)}function unboundTypesHandler(){throwUnboundTypeError(`Cannot call ${humanName} due to unbound types`,rawArgTypes)}var proto=classType.registeredClass.instancePrototype;var method=proto[methodName];if(undefined===method||undefined===method.overloadTable&&method.className!==classType.name&&method.argCount===argCount-2){unboundTypesHandler.argCount=argCount-2;unboundTypesHandler.className=classType.name;proto[methodName]=unboundTypesHandler}else{ensureOverloadTable(proto,methodName,humanName);proto[methodName].overloadTable[argCount-2]=unboundTypesHandler}whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{var memberFunction=craftInvokerFunction(humanName,argTypes,classType,rawInvoker,context,isAsync);if(undefined===proto[methodName].overloadTable){memberFunction.argCount=argCount-2;proto[methodName]=memberFunction}else{proto[methodName].overloadTable[argCount-2]=memberFunction}return[]});return[]})};var validateThis=(this_,classType,humanName)=>{if(!(this_ instanceof Object)){throwBindingError(`${humanName} with invalid "this": ${this_}`)}if(!(this_ instanceof classType.registeredClass.constructor)){throwBindingError(`${humanName} incompatible with "this" of type ${this_.constructor.name}`)}if(!this_.$$.ptr){throwBindingError(`cannot call emscripten binding method ${humanName} on deleted object`)}return upcastPointer(this_.$$.ptr,this_.$$.ptrType.registeredClass,classType.registeredClass)};var __embind_register_class_property=(classType,fieldName,getterReturnType,getterSignature,getter,getterContext,setterArgumentType,setterSignature,setter,setterContext)=>{fieldName=AsciiToString(fieldName);getter=embind__requireFunction(getterSignature,getter);whenDependentTypesAreResolved([],[classType],classType=>{classType=classType[0];var humanName=`${classType.name}.${fieldName}`;var desc={get(){throwUnboundTypeError(`Cannot access ${humanName} due to unbound types`,[getterReturnType,setterArgumentType])},enumerable:true,configurable:true};if(setter){desc.set=()=>throwUnboundTypeError(`Cannot access ${humanName} due to unbound types`,[getterReturnType,setterArgumentType])}else{desc.set=v=>throwBindingError(humanName+" is a read-only property")}Object.defineProperty(classType.registeredClass.instancePrototype,fieldName,desc);whenDependentTypesAreResolved([],setter?[getterReturnType,setterArgumentType]:[getterReturnType],types=>{var getterReturnType=types[0];var desc={get(){var ptr=validateThis(this,classType,humanName+" getter");return getterReturnType.fromWireType(getter(getterContext,ptr))},enumerable:true};if(setter){setter=embind__requireFunction(setterSignature,setter);var setterArgumentType=types[1];desc.set=function(v){var ptr=validateThis(this,classType,humanName+" setter");var destructors=[];setter(setterContext,ptr,setterArgumentType.toWireType(destructors,v));runDestructors(destructors)}}Object.defineProperty(classType.registeredClass.instancePrototype,fieldName,desc);return[]});return[]})};var __embind_register_constant=(name,type,value)=>{name=AsciiToString(name);whenDependentTypesAreResolved([],[type],type=>{type=type[0];Module[name]=type.fromWireType(value);return[]})};var __emval_decref=handle=>{if(handle>9&&0===--emval_handles[handle+1]){emval_handles[handle]=undefined;emval_freelist.push(handle)}};var EmValType={name:"emscripten::val",fromWireType:handle=>{var rv=Emval.toValue(handle);__emval_decref(handle);return rv},toWireType:(destructors,value)=>Emval.toHandle(value),readValueFromPointer:readPointer,destructorFunction:null};var __embind_register_emval=rawType=>registerType(rawType,EmValType);var enumReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?function(pointer){return this.fromWireType(HEAP8[pointer])}:function(pointer){return this.fromWireType(HEAPU8[pointer])};case 2:return signed?function(pointer){return this.fromWireType(HEAP16[pointer>>1])}:function(pointer){return this.fromWireType(HEAPU16[pointer>>1])};case 4:return signed?function(pointer){return this.fromWireType(HEAP32[pointer>>2])}:function(pointer){return this.fromWireType(HEAPU32[pointer>>2])};default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};function getEnumValueType(rawValueType){return rawValueType===0?"object":rawValueType===1?"number":"string"}var __embind_register_enum=(rawType,name,size,isSigned,rawValueType)=>{name=AsciiToString(name);const valueType=getEnumValueType(rawValueType);switch(valueType){case"object":{function ctor(){}ctor.values={};registerType(rawType,{name,constructor:ctor,valueType,fromWireType:function(c){return this.constructor.values[c]},toWireType:(destructors,c)=>c.value,readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,ctor);break}case"number":{var keysMap={};registerType(rawType,{name,keysMap,valueType,fromWireType:c=>c,toWireType:(destructors,c)=>c,readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,keysMap);delete Module[name].argCount;break}case"string":{var valuesMap={};var reverseMap={};var keysMap={};registerType(rawType,{name,valuesMap,reverseMap,keysMap,valueType,fromWireType:function(c){return this.reverseMap[c]},toWireType:function(destructors,c){return this.valuesMap[c]},readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,keysMap);delete Module[name].argCount;break}}};var __embind_register_enum_value=(rawEnumType,name,enumValue)=>{var enumType=requireRegisteredType(rawEnumType,"enum");name=AsciiToString(name);switch(enumType.valueType){case"object":{var Enum=enumType.constructor;var Value=Object.create(enumType.constructor.prototype,{value:{value:enumValue},constructor:{value:createNamedFunction(`${enumType.name}_${name}`,function(){})}});Enum.values[enumValue]=Value;Enum[name]=Value;break}case"number":{enumType.keysMap[name]=enumValue;break}case"string":{enumType.valuesMap[name]=enumValue;enumType.reverseMap[enumValue]=name;enumType.keysMap[name]=name;break}}};var floatReadValueFromPointer=(name,width)=>{switch(width){case 4:return function(pointer){return this.fromWireType(HEAPF32[pointer>>2])};case 8:return function(pointer){return this.fromWireType(HEAPF64[pointer>>3])};default:throw new TypeError(`invalid float width (${width}): ${name}`)}};var __embind_register_float=(rawType,name,size)=>{name=AsciiToString(name);registerType(rawType,{name,fromWireType:value=>value,toWireType:(destructors,value)=>value,readValueFromPointer:floatReadValueFromPointer(name,size),destructorFunction:null})};var __embind_register_function=(name,argCount,rawArgTypesAddr,signature,rawInvoker,fn,isAsync,isNonnullReturn)=>{var argTypes=heap32VectorToArray(argCount,rawArgTypesAddr);name=AsciiToString(name);name=getFunctionName(name);rawInvoker=embind__requireFunction(signature,rawInvoker,isAsync);exposePublicSymbol(name,function(){throwUnboundTypeError(`Cannot call ${name} due to unbound types`,argTypes)},argCount-1);whenDependentTypesAreResolved([],argTypes,argTypes=>{var invokerArgsArray=[argTypes[0],null].concat(argTypes.slice(1));replacePublicSymbol(name,craftInvokerFunction(name,invokerArgsArray,null,rawInvoker,fn,isAsync),argCount-1);return[]})};var __embind_register_integer=(primitiveType,name,size,minRange,maxRange)=>{name=AsciiToString(name);const isUnsignedType=minRange===0;let fromWireType=value=>value;if(isUnsignedType){var bitshift=32-8*size;fromWireType=value=>value<>>bitshift;maxRange=fromWireType(maxRange)}registerType(primitiveType,{name,fromWireType,toWireType:(destructors,value)=>value,readValueFromPointer:integerReadValueFromPointer(name,size,minRange!==0),destructorFunction:null})};var installIndexedIterator=(proto,sizeMethodName,getMethodName)=>{const makeIterator=(size,getValue)=>{let index=0;return{next(){if(index>=size){return{done:true}}const current=index;index++;const value=getValue(current);return{value,done:false}},[Symbol.iterator](){return this}}};if(!proto[Symbol.iterator]){proto[Symbol.iterator]=function(){const size=this[sizeMethodName]();return makeIterator(size,i=>this[getMethodName](i))}}};var __embind_register_iterable=(rawClassType,rawElementType,sizeMethodName,getMethodName)=>{sizeMethodName=AsciiToString(sizeMethodName);getMethodName=AsciiToString(getMethodName);whenDependentTypesAreResolved([],[rawClassType,rawElementType],types=>{const classType=types[0];installIndexedIterator(classType.registeredClass.instancePrototype,sizeMethodName,getMethodName);return[]})};var __embind_register_memory_view=(rawType,dataTypeIndex,name)=>{var typeMapping=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array,BigInt64Array,BigUint64Array];var TA=typeMapping[dataTypeIndex];function decodeMemoryView(handle){var size=HEAPU32[handle>>2];var data=HEAPU32[handle+4>>2];return new TA(HEAP8.buffer,data,size)}name=AsciiToString(name);registerType(rawType,{name,fromWireType:decodeMemoryView,readValueFromPointer:decodeMemoryView},{ignoreDuplicateRegistrations:true})};var EmValOptionalType=Object.assign({optional:true},EmValType);var __embind_register_optional=(rawOptionalType,rawType)=>{registerType(rawOptionalType,EmValOptionalType)};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var __embind_register_std_string=(rawType,name)=>{name=AsciiToString(name);var stdStringIsUTF8=true;registerType(rawType,{name,fromWireType(value){var length=HEAPU32[value>>2];var payload=value+4;var str;if(stdStringIsUTF8){str=UTF8ToString(payload,length,true)}else{str="";for(var i=0;i>2]=length;if(valueIsOfTypeString){if(stdStringIsUTF8){stringToUTF8(value,ptr,length+1)}else{for(var i=0;i255){_free(base);throwBindingError("String has UTF-16 code units that do not fit in 8 bits")}HEAPU8[ptr+i]=charCode}}}else{HEAPU8.set(value,ptr)}if(destructors!==null){destructors.push(_free,base)}return base},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var UTF16Decoder=globalThis.TextDecoder?new TextDecoder("utf-16le"):undefined;var UTF16ToString=(ptr,maxBytesToRead,ignoreNul)=>{var idx=ptr>>1;var endIdx=findStringEnd(HEAPU16,idx,maxBytesToRead/2,ignoreNul);if(endIdx-idx>16&&UTF16Decoder)return UTF16Decoder.decode(HEAPU16.subarray(idx,endIdx));var str="";for(var i=idx;i{maxBytesToWrite??=2147483647;if(maxBytesToWrite<2)return 0;maxBytesToWrite-=2;var startPtr=outPtr;var numCharsToWrite=maxBytesToWrite>1]=codeUnit;outPtr+=2}HEAP16[outPtr>>1]=0;return outPtr-startPtr};var lengthBytesUTF16=str=>str.length*2;var UTF32ToString=(ptr,maxBytesToRead,ignoreNul)=>{var str="";var startIdx=ptr>>2;for(var i=0;!(i>=maxBytesToRead/4);i++){var utf32=HEAPU32[startIdx+i];if(!utf32&&!ignoreNul)break;str+=String.fromCodePoint(utf32)}return str};var stringToUTF32=(str,outPtr,maxBytesToWrite)=>{maxBytesToWrite??=2147483647;if(maxBytesToWrite<4)return 0;var startPtr=outPtr;var endPtr=startPtr+maxBytesToWrite-4;for(var i=0;i65535){i++}HEAP32[outPtr>>2]=codePoint;outPtr+=4;if(outPtr+4>endPtr)break}HEAP32[outPtr>>2]=0;return outPtr-startPtr};var lengthBytesUTF32=str=>{var len=0;for(var i=0;i65535){i++}len+=4}return len};var __embind_register_std_wstring=(rawType,charSize,name)=>{name=AsciiToString(name);var decodeString,encodeString,lengthBytesUTF;if(charSize===2){decodeString=UTF16ToString;encodeString=stringToUTF16;lengthBytesUTF=lengthBytesUTF16}else{decodeString=UTF32ToString;encodeString=stringToUTF32;lengthBytesUTF=lengthBytesUTF32}registerType(rawType,{name,fromWireType:value=>{var length=HEAPU32[value>>2];var str=decodeString(value+4,length*charSize,true);_free(value);return str},toWireType:(destructors,value)=>{if(!(typeof value=="string")){throwBindingError(`Cannot pass non-string to C++ string type ${name}`)}var length=lengthBytesUTF(value);var ptr=_malloc(4+length+charSize);HEAPU32[ptr>>2]=length/charSize;encodeString(value,ptr+4,length+charSize);if(destructors!==null){destructors.push(_free,ptr)}return ptr},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var __embind_register_value_object=(rawType,name,constructorSignature,rawConstructor,destructorSignature,rawDestructor)=>{structRegistrations[rawType]={name:AsciiToString(name),rawConstructor:embind__requireFunction(constructorSignature,rawConstructor),rawDestructor:embind__requireFunction(destructorSignature,rawDestructor),fields:[]}};var __embind_register_value_object_field=(structType,fieldName,getterReturnType,getterSignature,getter,getterContext,setterArgumentType,setterSignature,setter,setterContext)=>{structRegistrations[structType].fields.push({fieldName:AsciiToString(fieldName),getterReturnType,getter:embind__requireFunction(getterSignature,getter),getterContext,setterArgumentType,setter:embind__requireFunction(setterSignature,setter),setterContext})};var __embind_register_void=(rawType,name)=>{name=AsciiToString(name);registerType(rawType,{isVoid:true,name,fromWireType:()=>undefined,toWireType:(destructors,o)=>undefined})};var runtimeKeepaliveCounter=0;var __emscripten_runtime_keepalive_clear=()=>{noExitRuntime=false;runtimeKeepaliveCounter=0};var emval_methodCallers=[];var emval_addMethodCaller=caller=>{var id=emval_methodCallers.length;emval_methodCallers.push(caller);return id};var emval_lookupTypes=(argCount,argTypes)=>{var a=new Array(argCount);for(var i=0;i>2],`parameter ${i}`)}return a};var emval_returnValue=(toReturnWire,destructorsRef,handle)=>{var destructors=[];var result=toReturnWire(destructors,handle);if(destructors.length){HEAPU32[destructorsRef>>2]=Emval.toHandle(destructors)}return result};var emval_symbols={};var getStringOrSymbol=address=>{var symbol=emval_symbols[address];if(symbol===undefined){return AsciiToString(address)}return symbol};var __emval_create_invoker=(argCount,argTypesPtr,kind)=>{var GenericWireTypeSize=8;var[retType,...argTypes]=emval_lookupTypes(argCount,argTypesPtr);var toReturnWire=retType.toWireType.bind(retType);var argFromPtr=argTypes.map(type=>type.readValueFromPointer.bind(type));argCount--;var argN=new Array(argCount);var invokerFunction=(handle,methodName,destructorsRef,args)=>{var offset=0;for(var i=0;it.name)}) => ${retType.name}>`;return emval_addMethodCaller(createNamedFunction(functionName,invokerFunction))};var __emval_invoke=(caller,handle,methodName,destructorsRef,args)=>emval_methodCallers[caller](handle,methodName,destructorsRef,args);var __emval_new_cstring=v=>Emval.toHandle(getStringOrSymbol(v));var __emval_new_object=()=>Emval.toHandle({});var __emval_run_destructors=handle=>{var destructors=Emval.toValue(handle);runDestructors(destructors);__emval_decref(handle)};var __emval_set_property=(handle,key,value)=>{handle=Emval.toValue(handle);key=Emval.toValue(key);value=Emval.toValue(value);handle[key]=value};var timers={};var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;var maybeExit=()=>{if(!keepRuntimeAlive()){try{_exit(EXITSTATUS)}catch(e){handleException(e)}}};var callUserCallback=func=>{if(ABORT){return}try{return func()}catch(e){handleException(e)}finally{maybeExit()}};var _emscripten_get_now=()=>performance.now();var __setitimer_js=(which,timeout_ms)=>{if(timers[which]){clearTimeout(timers[which].id);delete timers[which]}if(!timeout_ms)return 0;var id=setTimeout(()=>{delete timers[which];callUserCallback(()=>__emscripten_timeout(which,_emscripten_get_now()))},timeout_ms);timers[which]={id,timeout_ms};return 0};var _emscripten_date_now=()=>Date.now();var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var printCharBuffers=[null,[],[]];var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(curr===0||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer));buffer.length=0}else{buffer.push(curr)}};var _fd_write=(fd,iov,iovcnt,pnum)=>{var num=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j>2]=num;return 0};init_ClassHandle();init_RegisteredPointer();{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}var ___getTypeName,_free,_malloc,__emscripten_timeout,memory,__indirect_function_table,wasmMemory,wasmTable;function assignWasmExports(wasmExports){___getTypeName=wasmExports["Q"];_free=Module["_free"]=wasmExports["R"];_malloc=Module["_malloc"]=wasmExports["S"];__emscripten_timeout=wasmExports["U"];memory=wasmMemory=wasmExports["O"];__indirect_function_table=wasmTable=wasmExports["T"]}var wasmImports={J:__abort_js,x:__embind_create_inheriting_constructor,s:__embind_finalize_value_object,z:__embind_register_bigint,A:__embind_register_bool,b:__embind_register_class,p:__embind_register_class_class_function,e:__embind_register_class_constructor,a:__embind_register_class_function,d:__embind_register_class_property,K:__embind_register_constant,M:__embind_register_emval,l:__embind_register_enum,f:__embind_register_enum_value,y:__embind_register_float,g:__embind_register_function,o:__embind_register_integer,q:__embind_register_iterable,h:__embind_register_memory_view,r:__embind_register_optional,N:__embind_register_std_string,u:__embind_register_std_wstring,t:__embind_register_value_object,m:__embind_register_value_object_field,B:__embind_register_void,D:__emscripten_runtime_keepalive_clear,k:__emval_create_invoker,n:__emval_decref,j:__emval_invoke,v:__emval_new_cstring,L:__emval_new_object,i:__emval_run_destructors,w:__emval_set_property,E:__setitimer_js,I:_emscripten_date_now,c:_emscripten_get_now,F:_emscripten_resize_heap,G:_exit,H:_fd_write,C:_proc_exit};function run(){preRun();function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} +var PHYSX=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};if(typeof __filename!="undefined"){_scriptName=__filename}else if(ENVIRONMENT_IS_WORKER){_scriptName=self.location.href}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{if(isFileURI(url)){return new Promise((resolve,reject)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){resolve(xhr.response);return}reject(xhr.status)};xhr.onerror=reject;xhr.send(null)})}var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var EXITSTATUS;var isFileURI=filename=>filename.startsWith("file://");var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["P"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("physx.release.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!isFileURI(binaryFile)&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var __abort_js=()=>abort("");var createNamedFunction=(name,func)=>Object.defineProperty(func,"name",{value:name});var emval_freelist=[];var emval_handles=[0,1,,1,null,1,true,1,false,1];var BindingError=class BindingError extends Error{constructor(message){super(message);this.name="BindingError"}};var throwBindingError=message=>{throw new BindingError(message)};var Emval={toValue:handle=>{if(!handle){throwBindingError(`Cannot use deleted val. handle = ${handle}`)}return emval_handles[handle]},toHandle:value=>{switch(value){case undefined:return 2;case null:return 4;case true:return 6;case false:return 8;default:{const handle=emval_freelist.pop()||emval_handles.length;emval_handles[handle]=value;emval_handles[handle+1]=1;return handle}}}};class PureVirtualError extends Error{}var AsciiToString=ptr=>{var str="";while(1){var ch=HEAPU8[ptr++];if(!ch)return str;str+=String.fromCharCode(ch)}};var registeredInstances={};var getBasestPointer=(class_,ptr)=>{if(ptr===undefined){throwBindingError("ptr should not be undefined")}while(class_.baseClass){ptr=class_.upcast(ptr);class_=class_.baseClass}return ptr};var registerInheritedInstance=(class_,ptr,instance)=>{ptr=getBasestPointer(class_,ptr);if(registeredInstances.hasOwnProperty(ptr)){throwBindingError(`Tried to register registered instance: ${ptr}`)}else{registeredInstances[ptr]=instance}};var registeredTypes={};var getTypeName=type=>{var ptr=___getTypeName(type);var rv=AsciiToString(ptr);_free(ptr);return rv};var requireRegisteredType=(rawType,humanName)=>{var impl=registeredTypes[rawType];if(undefined===impl){throwBindingError(`${humanName} has unknown type ${getTypeName(rawType)}`)}return impl};var unregisterInheritedInstance=(class_,ptr)=>{ptr=getBasestPointer(class_,ptr);if(registeredInstances.hasOwnProperty(ptr)){delete registeredInstances[ptr]}else{throwBindingError(`Tried to unregister unregistered instance: ${ptr}`)}};var detachFinalizer=handle=>{};var finalizationRegistry=false;var runDestructor=$$=>{if($$.smartPtr){$$.smartPtrType.rawDestructor($$.smartPtr)}else{$$.ptrType.registeredClass.rawDestructor($$.ptr)}};var releaseClassHandle=$$=>{$$.count.value-=1;var toDelete=0===$$.count.value;if(toDelete){runDestructor($$)}};var attachFinalizer=handle=>{if(!globalThis.FinalizationRegistry){attachFinalizer=handle=>handle;return handle}finalizationRegistry=new FinalizationRegistry(info=>{releaseClassHandle(info.$$)});attachFinalizer=handle=>{var $$=handle.$$;var hasSmartPtr=!!$$.smartPtr;if(hasSmartPtr){var info={$$};finalizationRegistry.register(handle,info,handle)}return handle};detachFinalizer=handle=>finalizationRegistry.unregister(handle);return attachFinalizer(handle)};var __embind_create_inheriting_constructor=(constructorName,wrapperType,properties)=>{constructorName=AsciiToString(constructorName);wrapperType=requireRegisteredType(wrapperType,"wrapper");properties=Emval.toValue(properties);var registeredClass=wrapperType.registeredClass;var wrapperPrototype=registeredClass.instancePrototype;var baseClass=registeredClass.baseClass;var baseClassPrototype=baseClass.instancePrototype;var baseConstructor=registeredClass.baseClass.constructor;var ctor=createNamedFunction(constructorName,function(...args){for(var name of registeredClass.baseClass.pureVirtualFunctions){if(this[name]===baseClassPrototype[name]){throw new PureVirtualError(`Pure virtual function ${name} must be implemented in JavaScript`)}}Object.defineProperty(this,"__parent",{value:wrapperPrototype});this["__construct"](...args)});wrapperPrototype["__construct"]=function __construct(...args){if(this===wrapperPrototype){throwBindingError("Pass correct 'this' to __construct")}var inner=baseConstructor["implement"](this,...args);detachFinalizer(inner);var $$=inner.$$;inner["notifyOnDestruction"]();$$.preservePointerOnDelete=true;Object.defineProperties(this,{$$:{value:$$}});attachFinalizer(this);registerInheritedInstance(registeredClass,$$.ptr,this)};wrapperPrototype["__destruct"]=function __destruct(){if(this===wrapperPrototype){throwBindingError("Pass correct 'this' to __destruct")}detachFinalizer(this);unregisterInheritedInstance(registeredClass,this.$$.ptr)};ctor.prototype=Object.create(wrapperPrototype);Object.assign(ctor.prototype,properties);return Emval.toHandle(ctor)};var structRegistrations={};var runDestructors=destructors=>{while(destructors.length){var ptr=destructors.pop();var del=destructors.pop();del(ptr)}};function readPointer(pointer){return this.fromWireType(HEAPU32[pointer>>2])}var awaitingDependencies={};var typeDependencies={};var InternalError=class InternalError extends Error{constructor(message){super(message);this.name="InternalError"}};var throwInternalError=message=>{throw new InternalError(message)};var whenDependentTypesAreResolved=(myTypes,dependentTypes,getTypeConverters)=>{myTypes.forEach(type=>typeDependencies[type]=dependentTypes);function onComplete(typeConverters){var myTypeConverters=getTypeConverters(typeConverters);if(myTypeConverters.length!==myTypes.length){throwInternalError("Mismatched type converter count")}for(var i=0;i{typeConverters[i]=registeredTypes[dt];++registered;if(registered===unregisteredTypes.length){onComplete(typeConverters)}})}}if(0===unregisteredTypes.length){onComplete(typeConverters)}};var __embind_finalize_value_object=structType=>{var reg=structRegistrations[structType];delete structRegistrations[structType];var rawConstructor=reg.rawConstructor;var rawDestructor=reg.rawDestructor;var fieldRecords=reg.fields;var fieldTypes=fieldRecords.map(field=>field.getterReturnType).concat(fieldRecords.map(field=>field.setterArgumentType));whenDependentTypesAreResolved([structType],fieldTypes,fieldTypes=>{var fields={};for(var[i,field]of fieldRecords.entries()){const getterReturnType=fieldTypes[i];const getter=field.getter;const getterContext=field.getterContext;const setterArgumentType=fieldTypes[i+fieldRecords.length];const setter=field.setter;const setterContext=field.setterContext;fields[field.fieldName]={read:ptr=>getterReturnType.fromWireType(getter(getterContext,ptr)),write:(ptr,o)=>{var destructors=[];setter(setterContext,ptr,setterArgumentType.toWireType(destructors,o));runDestructors(destructors)},optional:getterReturnType.optional}}return[{name:reg.name,fromWireType:ptr=>{var rv={};for(var i in fields){rv[i]=fields[i].read(ptr)}rawDestructor(ptr);return rv},toWireType:(destructors,o)=>{for(var fieldName in fields){if(!(fieldName in o)&&!fields[fieldName].optional){throw new TypeError(`Missing field: "${fieldName}"`)}}var ptr=rawConstructor();for(fieldName in fields){fields[fieldName].write(ptr,o[fieldName])}if(destructors!==null){destructors.push(rawDestructor,ptr)}return ptr},readValueFromPointer:readPointer,destructorFunction:rawDestructor}]})};function sharedRegisterType(rawType,registeredInstance,options={}){var name=registeredInstance.name;if(!rawType){throwBindingError(`type "${name}" must have a positive integer typeid pointer`)}if(registeredTypes.hasOwnProperty(rawType)){if(options.ignoreDuplicateRegistrations){return}else{throwBindingError(`Cannot register type '${name}' twice`)}}registeredTypes[rawType]=registeredInstance;delete typeDependencies[rawType];if(awaitingDependencies.hasOwnProperty(rawType)){var callbacks=awaitingDependencies[rawType];delete awaitingDependencies[rawType];callbacks.forEach(cb=>cb())}}function registerType(rawType,registeredInstance,options={}){return sharedRegisterType(rawType,registeredInstance,options)}var integerReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?pointer=>HEAP8[pointer]:pointer=>HEAPU8[pointer];case 2:return signed?pointer=>HEAP16[pointer>>1]:pointer=>HEAPU16[pointer>>1];case 4:return signed?pointer=>HEAP32[pointer>>2]:pointer=>HEAPU32[pointer>>2];case 8:return signed?pointer=>HEAP64[pointer>>3]:pointer=>HEAPU64[pointer>>3];default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};var __embind_register_bigint=(primitiveType,name,size,minRange,maxRange)=>{name=AsciiToString(name);const isUnsignedType=minRange===0n;let fromWireType=value=>value;if(isUnsignedType){const bitSize=size*8;fromWireType=value=>BigInt.asUintN(bitSize,value);maxRange=fromWireType(maxRange)}registerType(primitiveType,{name,fromWireType,toWireType:(destructors,value)=>{if(typeof value=="number"){value=BigInt(value)}return value},readValueFromPointer:integerReadValueFromPointer(name,size,!isUnsignedType),destructorFunction:null})};var __embind_register_bool=(rawType,name,trueValue,falseValue)=>{name=AsciiToString(name);registerType(rawType,{name,fromWireType:function(wt){return!!wt},toWireType:function(destructors,o){return o?trueValue:falseValue},readValueFromPointer:function(pointer){return this.fromWireType(HEAPU8[pointer])},destructorFunction:null})};var shallowCopyInternalPointer=o=>({count:o.count,deleteScheduled:o.deleteScheduled,preservePointerOnDelete:o.preservePointerOnDelete,ptr:o.ptr,ptrType:o.ptrType,smartPtr:o.smartPtr,smartPtrType:o.smartPtrType});var throwInstanceAlreadyDeleted=obj=>{function getInstanceTypeName(handle){return handle.$$.ptrType.registeredClass.name}throwBindingError(getInstanceTypeName(obj)+" instance already deleted")};var deletionQueue=[];var flushPendingDeletes=()=>{while(deletionQueue.length){var obj=deletionQueue.pop();obj.$$.deleteScheduled=false;obj["delete"]()}};var delayFunction;var init_ClassHandle=()=>{let proto=ClassHandle.prototype;Object.assign(proto,{isAliasOf(other){if(!(this instanceof ClassHandle)){return false}if(!(other instanceof ClassHandle)){return false}var leftClass=this.$$.ptrType.registeredClass;var left=this.$$.ptr;other.$$=other.$$;var rightClass=other.$$.ptrType.registeredClass;var right=other.$$.ptr;while(leftClass.baseClass){left=leftClass.upcast(left);leftClass=leftClass.baseClass}while(rightClass.baseClass){right=rightClass.upcast(right);rightClass=rightClass.baseClass}return leftClass===rightClass&&left===right},clone(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.preservePointerOnDelete){this.$$.count.value+=1;return this}else{var clone=attachFinalizer(Object.create(Object.getPrototypeOf(this),{$$:{value:shallowCopyInternalPointer(this.$$)}}));clone.$$.count.value+=1;clone.$$.deleteScheduled=false;return clone}},delete(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}detachFinalizer(this);releaseClassHandle(this.$$);if(!this.$$.preservePointerOnDelete){this.$$.smartPtr=undefined;this.$$.ptr=undefined}},isDeleted(){return!this.$$.ptr},deleteLater(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}deletionQueue.push(this);if(deletionQueue.length===1&&delayFunction){delayFunction(flushPendingDeletes)}this.$$.deleteScheduled=true;return this}});const symbolDispose=Symbol.dispose;if(symbolDispose){proto[symbolDispose]=proto["delete"]}};function ClassHandle(){}var registeredPointers={};var ensureOverloadTable=(proto,methodName,humanName)=>{if(undefined===proto[methodName].overloadTable){var prevFunc=proto[methodName];proto[methodName]=function(...args){if(!proto[methodName].overloadTable.hasOwnProperty(args.length)){throwBindingError(`Function '${humanName}' called with an invalid number of arguments (${args.length}) - expects one of (${proto[methodName].overloadTable})!`)}return proto[methodName].overloadTable[args.length].apply(this,args)};proto[methodName].overloadTable=[];proto[methodName].overloadTable[prevFunc.argCount]=prevFunc}};var exposePublicSymbol=(name,value,numArguments)=>{if(Module.hasOwnProperty(name)){if(undefined===numArguments||undefined!==Module[name].overloadTable&&undefined!==Module[name].overloadTable[numArguments]){throwBindingError(`Cannot register public name '${name}' twice`)}ensureOverloadTable(Module,name,name);if(Module[name].overloadTable.hasOwnProperty(numArguments)){throwBindingError(`Cannot register multiple overloads of a function with the same number of arguments (${numArguments})!`)}Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}};var char_0=48;var char_9=57;var makeLegalFunctionName=name=>{name=name.replace(/[^a-zA-Z0-9_]/g,"$");var f=name.charCodeAt(0);if(f>=char_0&&f<=char_9){return`_${name}`}return name};function RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast){this.name=name;this.constructor=constructor;this.instancePrototype=instancePrototype;this.rawDestructor=rawDestructor;this.baseClass=baseClass;this.getActualType=getActualType;this.upcast=upcast;this.downcast=downcast;this.pureVirtualFunctions=[]}var upcastPointer=(ptr,ptrClass,desiredClass)=>{while(ptrClass!==desiredClass){if(!ptrClass.upcast){throwBindingError(`Expected null or instance of ${desiredClass.name}, got an instance of ${ptrClass.name}`)}ptr=ptrClass.upcast(ptr);ptrClass=ptrClass.baseClass}return ptr};var embindRepr=v=>{if(v===null){return"null"}var t=typeof v;if(t==="object"||t==="array"||t==="function"){return v.toString()}else{return""+v}};function constNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}function genericPointerToWireType(destructors,handle){var ptr;if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}if(this.isSmartPointer){ptr=this.rawConstructor();if(destructors!==null){destructors.push(this.rawDestructor,ptr)}return ptr}else{return 0}}if(!handle||!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(!this.isConst&&handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);if(this.isSmartPointer){if(undefined===handle.$$.smartPtr){throwBindingError("Passing raw pointer to smart pointer is illegal")}switch(this.sharingPolicy){case 0:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}break;case 1:ptr=handle.$$.smartPtr;break;case 2:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{var clonedHandle=handle["clone"]();ptr=this.rawShare(ptr,Emval.toHandle(()=>clonedHandle["delete"]()));if(destructors!==null){destructors.push(this.rawDestructor,ptr)}}break;default:throwBindingError("Unsupported sharing policy")}}return ptr}function nonConstNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}var downcastPointer=(ptr,ptrClass,desiredClass)=>{if(ptrClass===desiredClass){return ptr}if(undefined===desiredClass.baseClass){return null}var rv=downcastPointer(ptr,ptrClass,desiredClass.baseClass);if(rv===null){return null}return desiredClass.downcast(rv)};var getInheritedInstance=(class_,ptr)=>{ptr=getBasestPointer(class_,ptr);return registeredInstances[ptr]};var makeClassHandle=(prototype,record)=>{if(!record.ptrType||!record.ptr){throwInternalError("makeClassHandle requires ptr and ptrType")}var hasSmartPtrType=!!record.smartPtrType;var hasSmartPtr=!!record.smartPtr;if(hasSmartPtrType!==hasSmartPtr){throwInternalError("Both smartPtrType and smartPtr must be specified")}record.count={value:1};return attachFinalizer(Object.create(prototype,{$$:{value:record,writable:true}}))};function RegisteredPointer_fromWireType(ptr){var rawPointer=this.getPointee(ptr);if(!rawPointer){this.destructor(ptr);return null}var registeredInstance=getInheritedInstance(this.registeredClass,rawPointer);if(undefined!==registeredInstance){if(0===registeredInstance.$$.count.value){registeredInstance.$$.ptr=rawPointer;registeredInstance.$$.smartPtr=ptr;return registeredInstance["clone"]()}else{var rv=registeredInstance["clone"]();this.destructor(ptr);return rv}}function makeDefaultHandle(){if(this.isSmartPointer){return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this.pointeeType,ptr:rawPointer,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this,ptr})}}var actualType=this.registeredClass.getActualType(rawPointer);var registeredPointerRecord=registeredPointers[actualType];if(!registeredPointerRecord){return makeDefaultHandle.call(this)}var toType;if(this.isConst){toType=registeredPointerRecord.constPointerType}else{toType=registeredPointerRecord.pointerType}var dp=downcastPointer(rawPointer,this.registeredClass,toType.registeredClass);if(dp===null){return makeDefaultHandle.call(this)}if(this.isSmartPointer){return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp})}}var init_RegisteredPointer=()=>{Object.assign(RegisteredPointer.prototype,{getPointee(ptr){if(this.rawGetPointee){ptr=this.rawGetPointee(ptr)}return ptr},destructor(ptr){this.rawDestructor?.(ptr)},readValueFromPointer:readPointer,fromWireType:RegisteredPointer_fromWireType})};function RegisteredPointer(name,registeredClass,isReference,isConst,isSmartPointer,pointeeType,sharingPolicy,rawGetPointee,rawConstructor,rawShare,rawDestructor){this.name=name;this.registeredClass=registeredClass;this.isReference=isReference;this.isConst=isConst;this.isSmartPointer=isSmartPointer;this.pointeeType=pointeeType;this.sharingPolicy=sharingPolicy;this.rawGetPointee=rawGetPointee;this.rawConstructor=rawConstructor;this.rawShare=rawShare;this.rawDestructor=rawDestructor;if(!isSmartPointer&®isteredClass.baseClass===undefined){if(isConst){this.toWireType=constNoSmartPtrRawPointerToWireType;this.destructorFunction=null}else{this.toWireType=nonConstNoSmartPtrRawPointerToWireType;this.destructorFunction=null}}else{this.toWireType=genericPointerToWireType}}var replacePublicSymbol=(name,value,numArguments)=>{if(!Module.hasOwnProperty(name)){throwInternalError("Replacing nonexistent public symbol")}if(undefined!==Module[name].overloadTable&&undefined!==numArguments){Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}};var wasmTableMirror=[];var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var embind__requireFunction=(signature,rawFunction,isAsync=false)=>{signature=AsciiToString(signature);function makeDynCaller(){var rtn=getWasmTableEntry(rawFunction);return rtn}var fp=makeDynCaller();if(typeof fp!="function"){throwBindingError(`unknown function pointer with signature ${signature}: ${rawFunction}`)}return fp};class UnboundTypeError extends Error{}var throwUnboundTypeError=(message,types)=>{var unboundTypes=[];var seen={};function visit(type){if(seen[type]){return}if(registeredTypes[type]){return}if(typeDependencies[type]){typeDependencies[type].forEach(visit);return}unboundTypes.push(type);seen[type]=true}types.forEach(visit);throw new UnboundTypeError(`${message}: `+unboundTypes.map(getTypeName).join([", "]))};var __embind_register_class=(rawType,rawPointerType,rawConstPointerType,baseClassRawType,getActualTypeSignature,getActualType,upcastSignature,upcast,downcastSignature,downcast,name,destructorSignature,rawDestructor)=>{name=AsciiToString(name);getActualType=embind__requireFunction(getActualTypeSignature,getActualType);upcast&&=embind__requireFunction(upcastSignature,upcast);downcast&&=embind__requireFunction(downcastSignature,downcast);rawDestructor=embind__requireFunction(destructorSignature,rawDestructor);var legalFunctionName=makeLegalFunctionName(name);exposePublicSymbol(legalFunctionName,function(){throwUnboundTypeError(`Cannot construct ${name} due to unbound types`,[baseClassRawType])});whenDependentTypesAreResolved([rawType,rawPointerType,rawConstPointerType],baseClassRawType?[baseClassRawType]:[],base=>{base=base[0];var baseClass;var basePrototype;if(baseClassRawType){baseClass=base.registeredClass;basePrototype=baseClass.instancePrototype}else{basePrototype=ClassHandle.prototype}var constructor=createNamedFunction(name,function(...args){if(Object.getPrototypeOf(this)!==instancePrototype){throw new BindingError(`Use 'new' to construct ${name}`)}if(undefined===registeredClass.constructor_body){throw new BindingError(`${name} has no accessible constructor`)}var body=registeredClass.constructor_body[args.length];if(undefined===body){throw new BindingError(`Tried to invoke ctor of ${name} with invalid number of parameters (${args.length}) - expected (${Object.keys(registeredClass.constructor_body).toString()}) parameters instead!`)}return body.apply(this,args)});var instancePrototype=Object.create(basePrototype,{constructor:{value:constructor}});constructor.prototype=instancePrototype;var registeredClass=new RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast);if(registeredClass.baseClass){registeredClass.baseClass.__derivedClasses??=[];registeredClass.baseClass.__derivedClasses.push(registeredClass)}var referenceConverter=new RegisteredPointer(name,registeredClass,true,false,false);var pointerConverter=new RegisteredPointer(name+"*",registeredClass,false,false,false);var constPointerConverter=new RegisteredPointer(name+" const*",registeredClass,false,true,false);registeredPointers[rawType]={pointerType:pointerConverter,constPointerType:constPointerConverter};replacePublicSymbol(legalFunctionName,constructor);return[referenceConverter,pointerConverter,constPointerConverter]})};function usesDestructorStack(argTypes){for(var i=1;i{var array=[];for(var i=0;i>2])}return array};var getFunctionName=signature=>{signature=signature.trim();const argsIndex=signature.indexOf("(");if(argsIndex===-1)return signature;return signature.slice(0,argsIndex)};var __embind_register_class_class_function=(rawClassType,methodName,argCount,rawArgTypesAddr,invokerSignature,rawInvoker,fn,isAsync,isNonnullReturn)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);methodName=AsciiToString(methodName);methodName=getFunctionName(methodName);rawInvoker=embind__requireFunction(invokerSignature,rawInvoker,isAsync);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`${classType.name}.${methodName}`;function unboundTypesHandler(){throwUnboundTypeError(`Cannot call ${humanName} due to unbound types`,rawArgTypes)}if(methodName.startsWith("@@")){methodName=Symbol[methodName.substring(2)]}var proto=classType.registeredClass.constructor;if(undefined===proto[methodName]){unboundTypesHandler.argCount=argCount-1;proto[methodName]=unboundTypesHandler}else{ensureOverloadTable(proto,methodName,humanName);proto[methodName].overloadTable[argCount-1]=unboundTypesHandler}whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{var invokerArgsArray=[argTypes[0],null].concat(argTypes.slice(1));var func=craftInvokerFunction(humanName,invokerArgsArray,null,rawInvoker,fn,isAsync);if(undefined===proto[methodName].overloadTable){func.argCount=argCount-1;proto[methodName]=func}else{proto[methodName].overloadTable[argCount-1]=func}if(classType.registeredClass.__derivedClasses){for(const derivedClass of classType.registeredClass.__derivedClasses){if(!derivedClass.constructor.hasOwnProperty(methodName)){derivedClass.constructor[methodName]=func}}}return[]});return[]})};var __embind_register_class_constructor=(rawClassType,argCount,rawArgTypesAddr,invokerSignature,invoker,rawConstructor)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);invoker=embind__requireFunction(invokerSignature,invoker);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`constructor ${classType.name}`;if(undefined===classType.registeredClass.constructor_body){classType.registeredClass.constructor_body=[]}if(undefined!==classType.registeredClass.constructor_body[argCount-1]){throw new BindingError(`Cannot register multiple constructors with identical number of parameters (${argCount-1}) for class '${classType.name}'! Overload resolution is currently only performed using the parameter count, not actual type info!`)}classType.registeredClass.constructor_body[argCount-1]=()=>{throwUnboundTypeError(`Cannot construct ${classType.name} due to unbound types`,rawArgTypes)};whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{argTypes.splice(1,0,null);classType.registeredClass.constructor_body[argCount-1]=craftInvokerFunction(humanName,argTypes,null,invoker,rawConstructor);return[]});return[]})};var __embind_register_class_function=(rawClassType,methodName,argCount,rawArgTypesAddr,invokerSignature,rawInvoker,context,isPureVirtual,isAsync,isNonnullReturn)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);methodName=AsciiToString(methodName);methodName=getFunctionName(methodName);rawInvoker=embind__requireFunction(invokerSignature,rawInvoker,isAsync);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`${classType.name}.${methodName}`;if(methodName.startsWith("@@")){methodName=Symbol[methodName.substring(2)]}if(isPureVirtual){classType.registeredClass.pureVirtualFunctions.push(methodName)}function unboundTypesHandler(){throwUnboundTypeError(`Cannot call ${humanName} due to unbound types`,rawArgTypes)}var proto=classType.registeredClass.instancePrototype;var method=proto[methodName];if(undefined===method||undefined===method.overloadTable&&method.className!==classType.name&&method.argCount===argCount-2){unboundTypesHandler.argCount=argCount-2;unboundTypesHandler.className=classType.name;proto[methodName]=unboundTypesHandler}else{ensureOverloadTable(proto,methodName,humanName);proto[methodName].overloadTable[argCount-2]=unboundTypesHandler}whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{var memberFunction=craftInvokerFunction(humanName,argTypes,classType,rawInvoker,context,isAsync);if(undefined===proto[methodName].overloadTable){memberFunction.argCount=argCount-2;proto[methodName]=memberFunction}else{proto[methodName].overloadTable[argCount-2]=memberFunction}return[]});return[]})};var validateThis=(this_,classType,humanName)=>{if(!(this_ instanceof Object)){throwBindingError(`${humanName} with invalid "this": ${this_}`)}if(!(this_ instanceof classType.registeredClass.constructor)){throwBindingError(`${humanName} incompatible with "this" of type ${this_.constructor.name}`)}if(!this_.$$.ptr){throwBindingError(`cannot call emscripten binding method ${humanName} on deleted object`)}return upcastPointer(this_.$$.ptr,this_.$$.ptrType.registeredClass,classType.registeredClass)};var __embind_register_class_property=(classType,fieldName,getterReturnType,getterSignature,getter,getterContext,setterArgumentType,setterSignature,setter,setterContext)=>{fieldName=AsciiToString(fieldName);getter=embind__requireFunction(getterSignature,getter);whenDependentTypesAreResolved([],[classType],classType=>{classType=classType[0];var humanName=`${classType.name}.${fieldName}`;var desc={get(){throwUnboundTypeError(`Cannot access ${humanName} due to unbound types`,[getterReturnType,setterArgumentType])},enumerable:true,configurable:true};if(setter){desc.set=()=>throwUnboundTypeError(`Cannot access ${humanName} due to unbound types`,[getterReturnType,setterArgumentType])}else{desc.set=v=>throwBindingError(humanName+" is a read-only property")}Object.defineProperty(classType.registeredClass.instancePrototype,fieldName,desc);whenDependentTypesAreResolved([],setter?[getterReturnType,setterArgumentType]:[getterReturnType],types=>{var getterReturnType=types[0];var desc={get(){var ptr=validateThis(this,classType,humanName+" getter");return getterReturnType.fromWireType(getter(getterContext,ptr))},enumerable:true};if(setter){setter=embind__requireFunction(setterSignature,setter);var setterArgumentType=types[1];desc.set=function(v){var ptr=validateThis(this,classType,humanName+" setter");var destructors=[];setter(setterContext,ptr,setterArgumentType.toWireType(destructors,v));runDestructors(destructors)}}Object.defineProperty(classType.registeredClass.instancePrototype,fieldName,desc);return[]});return[]})};var __embind_register_constant=(name,type,value)=>{name=AsciiToString(name);whenDependentTypesAreResolved([],[type],type=>{type=type[0];Module[name]=type.fromWireType(value);return[]})};var __emval_decref=handle=>{if(handle>9&&0===--emval_handles[handle+1]){emval_handles[handle]=undefined;emval_freelist.push(handle)}};var EmValType={name:"emscripten::val",fromWireType:handle=>{var rv=Emval.toValue(handle);__emval_decref(handle);return rv},toWireType:(destructors,value)=>Emval.toHandle(value),readValueFromPointer:readPointer,destructorFunction:null};var __embind_register_emval=rawType=>registerType(rawType,EmValType);var enumReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?function(pointer){return this.fromWireType(HEAP8[pointer])}:function(pointer){return this.fromWireType(HEAPU8[pointer])};case 2:return signed?function(pointer){return this.fromWireType(HEAP16[pointer>>1])}:function(pointer){return this.fromWireType(HEAPU16[pointer>>1])};case 4:return signed?function(pointer){return this.fromWireType(HEAP32[pointer>>2])}:function(pointer){return this.fromWireType(HEAPU32[pointer>>2])};default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};function getEnumValueType(rawValueType){return rawValueType===0?"object":rawValueType===1?"number":"string"}var __embind_register_enum=(rawType,name,size,isSigned,rawValueType)=>{name=AsciiToString(name);const valueType=getEnumValueType(rawValueType);switch(valueType){case"object":{function ctor(){}ctor.values={};registerType(rawType,{name,constructor:ctor,valueType,fromWireType:function(c){return this.constructor.values[c]},toWireType:(destructors,c)=>c.value,readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,ctor);break}case"number":{var keysMap={};registerType(rawType,{name,keysMap,valueType,fromWireType:c=>c,toWireType:(destructors,c)=>c,readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,keysMap);delete Module[name].argCount;break}case"string":{var valuesMap={};var reverseMap={};var keysMap={};registerType(rawType,{name,valuesMap,reverseMap,keysMap,valueType,fromWireType:function(c){return this.reverseMap[c]},toWireType:function(destructors,c){return this.valuesMap[c]},readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,keysMap);delete Module[name].argCount;break}}};var __embind_register_enum_value=(rawEnumType,name,enumValue)=>{var enumType=requireRegisteredType(rawEnumType,"enum");name=AsciiToString(name);switch(enumType.valueType){case"object":{var Enum=enumType.constructor;var Value=Object.create(enumType.constructor.prototype,{value:{value:enumValue},constructor:{value:createNamedFunction(`${enumType.name}_${name}`,function(){})}});Enum.values[enumValue]=Value;Enum[name]=Value;break}case"number":{enumType.keysMap[name]=enumValue;break}case"string":{enumType.valuesMap[name]=enumValue;enumType.reverseMap[enumValue]=name;enumType.keysMap[name]=name;break}}};var floatReadValueFromPointer=(name,width)=>{switch(width){case 4:return function(pointer){return this.fromWireType(HEAPF32[pointer>>2])};case 8:return function(pointer){return this.fromWireType(HEAPF64[pointer>>3])};default:throw new TypeError(`invalid float width (${width}): ${name}`)}};var __embind_register_float=(rawType,name,size)=>{name=AsciiToString(name);registerType(rawType,{name,fromWireType:value=>value,toWireType:(destructors,value)=>value,readValueFromPointer:floatReadValueFromPointer(name,size),destructorFunction:null})};var __embind_register_function=(name,argCount,rawArgTypesAddr,signature,rawInvoker,fn,isAsync,isNonnullReturn)=>{var argTypes=heap32VectorToArray(argCount,rawArgTypesAddr);name=AsciiToString(name);name=getFunctionName(name);rawInvoker=embind__requireFunction(signature,rawInvoker,isAsync);exposePublicSymbol(name,function(){throwUnboundTypeError(`Cannot call ${name} due to unbound types`,argTypes)},argCount-1);whenDependentTypesAreResolved([],argTypes,argTypes=>{var invokerArgsArray=[argTypes[0],null].concat(argTypes.slice(1));replacePublicSymbol(name,craftInvokerFunction(name,invokerArgsArray,null,rawInvoker,fn,isAsync),argCount-1);return[]})};var __embind_register_integer=(primitiveType,name,size,minRange,maxRange)=>{name=AsciiToString(name);const isUnsignedType=minRange===0;let fromWireType=value=>value;if(isUnsignedType){var bitshift=32-8*size;fromWireType=value=>value<>>bitshift;maxRange=fromWireType(maxRange)}registerType(primitiveType,{name,fromWireType,toWireType:(destructors,value)=>value,readValueFromPointer:integerReadValueFromPointer(name,size,minRange!==0),destructorFunction:null})};var installIndexedIterator=(proto,sizeMethodName,getMethodName)=>{const makeIterator=(size,getValue)=>{let index=0;return{next(){if(index>=size){return{done:true}}const current=index;index++;const value=getValue(current);return{value,done:false}},[Symbol.iterator](){return this}}};if(!proto[Symbol.iterator]){proto[Symbol.iterator]=function(){const size=this[sizeMethodName]();return makeIterator(size,i=>this[getMethodName](i))}}};var __embind_register_iterable=(rawClassType,rawElementType,sizeMethodName,getMethodName)=>{sizeMethodName=AsciiToString(sizeMethodName);getMethodName=AsciiToString(getMethodName);whenDependentTypesAreResolved([],[rawClassType,rawElementType],types=>{const classType=types[0];installIndexedIterator(classType.registeredClass.instancePrototype,sizeMethodName,getMethodName);return[]})};var __embind_register_memory_view=(rawType,dataTypeIndex,name)=>{var typeMapping=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array,BigInt64Array,BigUint64Array];var TA=typeMapping[dataTypeIndex];function decodeMemoryView(handle){var size=HEAPU32[handle>>2];var data=HEAPU32[handle+4>>2];return new TA(HEAP8.buffer,data,size)}name=AsciiToString(name);registerType(rawType,{name,fromWireType:decodeMemoryView,readValueFromPointer:decodeMemoryView},{ignoreDuplicateRegistrations:true})};var EmValOptionalType=Object.assign({optional:true},EmValType);var __embind_register_optional=(rawOptionalType,rawType)=>{registerType(rawOptionalType,EmValOptionalType)};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var __embind_register_std_string=(rawType,name)=>{name=AsciiToString(name);var stdStringIsUTF8=true;registerType(rawType,{name,fromWireType(value){var length=HEAPU32[value>>2];var payload=value+4;var str;if(stdStringIsUTF8){str=UTF8ToString(payload,length,true)}else{str="";for(var i=0;i>2]=length;if(valueIsOfTypeString){if(stdStringIsUTF8){stringToUTF8(value,ptr,length+1)}else{for(var i=0;i255){_free(base);throwBindingError("String has UTF-16 code units that do not fit in 8 bits")}HEAPU8[ptr+i]=charCode}}}else{HEAPU8.set(value,ptr)}if(destructors!==null){destructors.push(_free,base)}return base},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var UTF16Decoder=globalThis.TextDecoder?new TextDecoder("utf-16le"):undefined;var UTF16ToString=(ptr,maxBytesToRead,ignoreNul)=>{var idx=ptr>>1;var endIdx=findStringEnd(HEAPU16,idx,maxBytesToRead/2,ignoreNul);if(endIdx-idx>16&&UTF16Decoder)return UTF16Decoder.decode(HEAPU16.subarray(idx,endIdx));var str="";for(var i=idx;i{maxBytesToWrite??=2147483647;if(maxBytesToWrite<2)return 0;maxBytesToWrite-=2;var startPtr=outPtr;var numCharsToWrite=maxBytesToWrite>1]=codeUnit;outPtr+=2}HEAP16[outPtr>>1]=0;return outPtr-startPtr};var lengthBytesUTF16=str=>str.length*2;var UTF32ToString=(ptr,maxBytesToRead,ignoreNul)=>{var str="";var startIdx=ptr>>2;for(var i=0;!(i>=maxBytesToRead/4);i++){var utf32=HEAPU32[startIdx+i];if(!utf32&&!ignoreNul)break;str+=String.fromCodePoint(utf32)}return str};var stringToUTF32=(str,outPtr,maxBytesToWrite)=>{maxBytesToWrite??=2147483647;if(maxBytesToWrite<4)return 0;var startPtr=outPtr;var endPtr=startPtr+maxBytesToWrite-4;for(var i=0;i65535){i++}HEAP32[outPtr>>2]=codePoint;outPtr+=4;if(outPtr+4>endPtr)break}HEAP32[outPtr>>2]=0;return outPtr-startPtr};var lengthBytesUTF32=str=>{var len=0;for(var i=0;i65535){i++}len+=4}return len};var __embind_register_std_wstring=(rawType,charSize,name)=>{name=AsciiToString(name);var decodeString,encodeString,lengthBytesUTF;if(charSize===2){decodeString=UTF16ToString;encodeString=stringToUTF16;lengthBytesUTF=lengthBytesUTF16}else{decodeString=UTF32ToString;encodeString=stringToUTF32;lengthBytesUTF=lengthBytesUTF32}registerType(rawType,{name,fromWireType:value=>{var length=HEAPU32[value>>2];var str=decodeString(value+4,length*charSize,true);_free(value);return str},toWireType:(destructors,value)=>{if(!(typeof value=="string")){throwBindingError(`Cannot pass non-string to C++ string type ${name}`)}var length=lengthBytesUTF(value);var ptr=_malloc(4+length+charSize);HEAPU32[ptr>>2]=length/charSize;encodeString(value,ptr+4,length+charSize);if(destructors!==null){destructors.push(_free,ptr)}return ptr},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var __embind_register_value_object=(rawType,name,constructorSignature,rawConstructor,destructorSignature,rawDestructor)=>{structRegistrations[rawType]={name:AsciiToString(name),rawConstructor:embind__requireFunction(constructorSignature,rawConstructor),rawDestructor:embind__requireFunction(destructorSignature,rawDestructor),fields:[]}};var __embind_register_value_object_field=(structType,fieldName,getterReturnType,getterSignature,getter,getterContext,setterArgumentType,setterSignature,setter,setterContext)=>{structRegistrations[structType].fields.push({fieldName:AsciiToString(fieldName),getterReturnType,getter:embind__requireFunction(getterSignature,getter),getterContext,setterArgumentType,setter:embind__requireFunction(setterSignature,setter),setterContext})};var __embind_register_void=(rawType,name)=>{name=AsciiToString(name);registerType(rawType,{isVoid:true,name,fromWireType:()=>undefined,toWireType:(destructors,o)=>undefined})};var runtimeKeepaliveCounter=0;var __emscripten_runtime_keepalive_clear=()=>{noExitRuntime=false;runtimeKeepaliveCounter=0};var emval_methodCallers=[];var emval_addMethodCaller=caller=>{var id=emval_methodCallers.length;emval_methodCallers.push(caller);return id};var emval_lookupTypes=(argCount,argTypes)=>{var a=new Array(argCount);for(var i=0;i>2],`parameter ${i}`)}return a};var emval_returnValue=(toReturnWire,destructorsRef,handle)=>{var destructors=[];var result=toReturnWire(destructors,handle);if(destructors.length){HEAPU32[destructorsRef>>2]=Emval.toHandle(destructors)}return result};var emval_symbols={};var getStringOrSymbol=address=>{var symbol=emval_symbols[address];if(symbol===undefined){return AsciiToString(address)}return symbol};var __emval_create_invoker=(argCount,argTypesPtr,kind)=>{var GenericWireTypeSize=8;var[retType,...argTypes]=emval_lookupTypes(argCount,argTypesPtr);var toReturnWire=retType.toWireType.bind(retType);var argFromPtr=argTypes.map(type=>type.readValueFromPointer.bind(type));argCount--;var argN=new Array(argCount);var invokerFunction=(handle,methodName,destructorsRef,args)=>{var offset=0;for(var i=0;it.name)}) => ${retType.name}>`;return emval_addMethodCaller(createNamedFunction(functionName,invokerFunction))};var __emval_invoke=(caller,handle,methodName,destructorsRef,args)=>emval_methodCallers[caller](handle,methodName,destructorsRef,args);var __emval_new_cstring=v=>Emval.toHandle(getStringOrSymbol(v));var __emval_new_object=()=>Emval.toHandle({});var __emval_run_destructors=handle=>{var destructors=Emval.toValue(handle);runDestructors(destructors);__emval_decref(handle)};var __emval_set_property=(handle,key,value)=>{handle=Emval.toValue(handle);key=Emval.toValue(key);value=Emval.toValue(value);handle[key]=value};var timers={};var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;var maybeExit=()=>{if(!keepRuntimeAlive()){try{_exit(EXITSTATUS)}catch(e){handleException(e)}}};var callUserCallback=func=>{if(ABORT){return}try{return func()}catch(e){handleException(e)}finally{maybeExit()}};var _emscripten_get_now=()=>performance.now();var __setitimer_js=(which,timeout_ms)=>{if(timers[which]){clearTimeout(timers[which].id);delete timers[which]}if(!timeout_ms)return 0;var id=setTimeout(()=>{delete timers[which];callUserCallback(()=>__emscripten_timeout(which,_emscripten_get_now()))},timeout_ms);timers[which]={id,timeout_ms};return 0};var _emscripten_date_now=()=>Date.now();var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var printCharBuffers=[null,[],[]];var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(curr===0||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer));buffer.length=0}else{buffer.push(curr)}};var _fd_write=(fd,iov,iovcnt,pnum)=>{var num=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j>2]=num;return 0};init_ClassHandle();init_RegisteredPointer();{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}var ___getTypeName,_free,_malloc,__emscripten_timeout,memory,__indirect_function_table,wasmMemory,wasmTable;function assignWasmExports(wasmExports){___getTypeName=wasmExports["Q"];_free=Module["_free"]=wasmExports["R"];_malloc=Module["_malloc"]=wasmExports["S"];__emscripten_timeout=wasmExports["U"];memory=wasmMemory=wasmExports["O"];__indirect_function_table=wasmTable=wasmExports["T"]}var wasmImports={J:__abort_js,x:__embind_create_inheriting_constructor,s:__embind_finalize_value_object,z:__embind_register_bigint,A:__embind_register_bool,b:__embind_register_class,p:__embind_register_class_class_function,e:__embind_register_class_constructor,a:__embind_register_class_function,d:__embind_register_class_property,K:__embind_register_constant,M:__embind_register_emval,l:__embind_register_enum,f:__embind_register_enum_value,y:__embind_register_float,g:__embind_register_function,o:__embind_register_integer,q:__embind_register_iterable,h:__embind_register_memory_view,r:__embind_register_optional,N:__embind_register_std_string,u:__embind_register_std_wstring,t:__embind_register_value_object,m:__embind_register_value_object_field,B:__embind_register_void,D:__emscripten_runtime_keepalive_clear,k:__emval_create_invoker,n:__emval_decref,j:__emval_invoke,v:__emval_new_cstring,L:__emval_new_object,i:__emval_run_destructors,w:__emval_set_property,E:__setitimer_js,I:_emscripten_date_now,c:_emscripten_get_now,F:_emscripten_resize_heap,G:_exit,H:_fd_write,C:_proc_exit};function run(){preRun();function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} ;return moduleRtn}})();if(typeof exports==="object"&&typeof module==="object"){module.exports=PHYSX;module.exports.default=PHYSX}else if(typeof define==="function"&&define["amd"])define([],()=>PHYSX); diff --git a/e2e/.dev/physx.release.simd.js b/e2e/.dev/physx.release.simd.js index bdb1d70990..45843d369b 100644 --- a/e2e/.dev/physx.release.simd.js +++ b/e2e/.dev/physx.release.simd.js @@ -1,2 +1,2 @@ -var PHYSX=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};if(typeof __filename!="undefined"){_scriptName=__filename}else if(ENVIRONMENT_IS_WORKER){_scriptName=self.location.href}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{if(isFileURI(url)){return new Promise((resolve,reject)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){resolve(xhr.response);return}reject(xhr.status)};xhr.onerror=reject;xhr.send(null)})}var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var EXITSTATUS;var isFileURI=filename=>filename.startsWith("file://");var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["P"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){return "https://mdn.alipayobjects.com/rms/afts/file/A*6vi1SJaSJ6IAAAAAgDAAAAgAehQnAQ/physx.release.simd.wasm"}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!isFileURI(binaryFile)&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var __abort_js=()=>abort("");var createNamedFunction=(name,func)=>Object.defineProperty(func,"name",{value:name});var emval_freelist=[];var emval_handles=[0,1,,1,null,1,true,1,false,1];var BindingError=class BindingError extends Error{constructor(message){super(message);this.name="BindingError"}};var throwBindingError=message=>{throw new BindingError(message)};var Emval={toValue:handle=>{if(!handle){throwBindingError(`Cannot use deleted val. handle = ${handle}`)}return emval_handles[handle]},toHandle:value=>{switch(value){case undefined:return 2;case null:return 4;case true:return 6;case false:return 8;default:{const handle=emval_freelist.pop()||emval_handles.length;emval_handles[handle]=value;emval_handles[handle+1]=1;return handle}}}};class PureVirtualError extends Error{}var AsciiToString=ptr=>{var str="";while(1){var ch=HEAPU8[ptr++];if(!ch)return str;str+=String.fromCharCode(ch)}};var registeredInstances={};var getBasestPointer=(class_,ptr)=>{if(ptr===undefined){throwBindingError("ptr should not be undefined")}while(class_.baseClass){ptr=class_.upcast(ptr);class_=class_.baseClass}return ptr};var registerInheritedInstance=(class_,ptr,instance)=>{ptr=getBasestPointer(class_,ptr);if(registeredInstances.hasOwnProperty(ptr)){throwBindingError(`Tried to register registered instance: ${ptr}`)}else{registeredInstances[ptr]=instance}};var registeredTypes={};var getTypeName=type=>{var ptr=___getTypeName(type);var rv=AsciiToString(ptr);_free(ptr);return rv};var requireRegisteredType=(rawType,humanName)=>{var impl=registeredTypes[rawType];if(undefined===impl){throwBindingError(`${humanName} has unknown type ${getTypeName(rawType)}`)}return impl};var unregisterInheritedInstance=(class_,ptr)=>{ptr=getBasestPointer(class_,ptr);if(registeredInstances.hasOwnProperty(ptr)){delete registeredInstances[ptr]}else{throwBindingError(`Tried to unregister unregistered instance: ${ptr}`)}};var detachFinalizer=handle=>{};var finalizationRegistry=false;var runDestructor=$$=>{if($$.smartPtr){$$.smartPtrType.rawDestructor($$.smartPtr)}else{$$.ptrType.registeredClass.rawDestructor($$.ptr)}};var releaseClassHandle=$$=>{$$.count.value-=1;var toDelete=0===$$.count.value;if(toDelete){runDestructor($$)}};var attachFinalizer=handle=>{if(!globalThis.FinalizationRegistry){attachFinalizer=handle=>handle;return handle}finalizationRegistry=new FinalizationRegistry(info=>{releaseClassHandle(info.$$)});attachFinalizer=handle=>{var $$=handle.$$;var hasSmartPtr=!!$$.smartPtr;if(hasSmartPtr){var info={$$};finalizationRegistry.register(handle,info,handle)}return handle};detachFinalizer=handle=>finalizationRegistry.unregister(handle);return attachFinalizer(handle)};var __embind_create_inheriting_constructor=(constructorName,wrapperType,properties)=>{constructorName=AsciiToString(constructorName);wrapperType=requireRegisteredType(wrapperType,"wrapper");properties=Emval.toValue(properties);var registeredClass=wrapperType.registeredClass;var wrapperPrototype=registeredClass.instancePrototype;var baseClass=registeredClass.baseClass;var baseClassPrototype=baseClass.instancePrototype;var baseConstructor=registeredClass.baseClass.constructor;var ctor=createNamedFunction(constructorName,function(...args){for(var name of registeredClass.baseClass.pureVirtualFunctions){if(this[name]===baseClassPrototype[name]){throw new PureVirtualError(`Pure virtual function ${name} must be implemented in JavaScript`)}}Object.defineProperty(this,"__parent",{value:wrapperPrototype});this["__construct"](...args)});wrapperPrototype["__construct"]=function __construct(...args){if(this===wrapperPrototype){throwBindingError("Pass correct 'this' to __construct")}var inner=baseConstructor["implement"](this,...args);detachFinalizer(inner);var $$=inner.$$;inner["notifyOnDestruction"]();$$.preservePointerOnDelete=true;Object.defineProperties(this,{$$:{value:$$}});attachFinalizer(this);registerInheritedInstance(registeredClass,$$.ptr,this)};wrapperPrototype["__destruct"]=function __destruct(){if(this===wrapperPrototype){throwBindingError("Pass correct 'this' to __destruct")}detachFinalizer(this);unregisterInheritedInstance(registeredClass,this.$$.ptr)};ctor.prototype=Object.create(wrapperPrototype);Object.assign(ctor.prototype,properties);return Emval.toHandle(ctor)};var structRegistrations={};var runDestructors=destructors=>{while(destructors.length){var ptr=destructors.pop();var del=destructors.pop();del(ptr)}};function readPointer(pointer){return this.fromWireType(HEAPU32[pointer>>2])}var awaitingDependencies={};var typeDependencies={};var InternalError=class InternalError extends Error{constructor(message){super(message);this.name="InternalError"}};var throwInternalError=message=>{throw new InternalError(message)};var whenDependentTypesAreResolved=(myTypes,dependentTypes,getTypeConverters)=>{myTypes.forEach(type=>typeDependencies[type]=dependentTypes);function onComplete(typeConverters){var myTypeConverters=getTypeConverters(typeConverters);if(myTypeConverters.length!==myTypes.length){throwInternalError("Mismatched type converter count")}for(var i=0;i{typeConverters[i]=registeredTypes[dt];++registered;if(registered===unregisteredTypes.length){onComplete(typeConverters)}})}}if(0===unregisteredTypes.length){onComplete(typeConverters)}};var __embind_finalize_value_object=structType=>{var reg=structRegistrations[structType];delete structRegistrations[structType];var rawConstructor=reg.rawConstructor;var rawDestructor=reg.rawDestructor;var fieldRecords=reg.fields;var fieldTypes=fieldRecords.map(field=>field.getterReturnType).concat(fieldRecords.map(field=>field.setterArgumentType));whenDependentTypesAreResolved([structType],fieldTypes,fieldTypes=>{var fields={};for(var[i,field]of fieldRecords.entries()){const getterReturnType=fieldTypes[i];const getter=field.getter;const getterContext=field.getterContext;const setterArgumentType=fieldTypes[i+fieldRecords.length];const setter=field.setter;const setterContext=field.setterContext;fields[field.fieldName]={read:ptr=>getterReturnType.fromWireType(getter(getterContext,ptr)),write:(ptr,o)=>{var destructors=[];setter(setterContext,ptr,setterArgumentType.toWireType(destructors,o));runDestructors(destructors)},optional:getterReturnType.optional}}return[{name:reg.name,fromWireType:ptr=>{var rv={};for(var i in fields){rv[i]=fields[i].read(ptr)}rawDestructor(ptr);return rv},toWireType:(destructors,o)=>{for(var fieldName in fields){if(!(fieldName in o)&&!fields[fieldName].optional){throw new TypeError(`Missing field: "${fieldName}"`)}}var ptr=rawConstructor();for(fieldName in fields){fields[fieldName].write(ptr,o[fieldName])}if(destructors!==null){destructors.push(rawDestructor,ptr)}return ptr},readValueFromPointer:readPointer,destructorFunction:rawDestructor}]})};function sharedRegisterType(rawType,registeredInstance,options={}){var name=registeredInstance.name;if(!rawType){throwBindingError(`type "${name}" must have a positive integer typeid pointer`)}if(registeredTypes.hasOwnProperty(rawType)){if(options.ignoreDuplicateRegistrations){return}else{throwBindingError(`Cannot register type '${name}' twice`)}}registeredTypes[rawType]=registeredInstance;delete typeDependencies[rawType];if(awaitingDependencies.hasOwnProperty(rawType)){var callbacks=awaitingDependencies[rawType];delete awaitingDependencies[rawType];callbacks.forEach(cb=>cb())}}function registerType(rawType,registeredInstance,options={}){return sharedRegisterType(rawType,registeredInstance,options)}var integerReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?pointer=>HEAP8[pointer]:pointer=>HEAPU8[pointer];case 2:return signed?pointer=>HEAP16[pointer>>1]:pointer=>HEAPU16[pointer>>1];case 4:return signed?pointer=>HEAP32[pointer>>2]:pointer=>HEAPU32[pointer>>2];case 8:return signed?pointer=>HEAP64[pointer>>3]:pointer=>HEAPU64[pointer>>3];default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};var __embind_register_bigint=(primitiveType,name,size,minRange,maxRange)=>{name=AsciiToString(name);const isUnsignedType=minRange===0n;let fromWireType=value=>value;if(isUnsignedType){const bitSize=size*8;fromWireType=value=>BigInt.asUintN(bitSize,value);maxRange=fromWireType(maxRange)}registerType(primitiveType,{name,fromWireType,toWireType:(destructors,value)=>{if(typeof value=="number"){value=BigInt(value)}return value},readValueFromPointer:integerReadValueFromPointer(name,size,!isUnsignedType),destructorFunction:null})};var __embind_register_bool=(rawType,name,trueValue,falseValue)=>{name=AsciiToString(name);registerType(rawType,{name,fromWireType:function(wt){return!!wt},toWireType:function(destructors,o){return o?trueValue:falseValue},readValueFromPointer:function(pointer){return this.fromWireType(HEAPU8[pointer])},destructorFunction:null})};var shallowCopyInternalPointer=o=>({count:o.count,deleteScheduled:o.deleteScheduled,preservePointerOnDelete:o.preservePointerOnDelete,ptr:o.ptr,ptrType:o.ptrType,smartPtr:o.smartPtr,smartPtrType:o.smartPtrType});var throwInstanceAlreadyDeleted=obj=>{function getInstanceTypeName(handle){return handle.$$.ptrType.registeredClass.name}throwBindingError(getInstanceTypeName(obj)+" instance already deleted")};var deletionQueue=[];var flushPendingDeletes=()=>{while(deletionQueue.length){var obj=deletionQueue.pop();obj.$$.deleteScheduled=false;obj["delete"]()}};var delayFunction;var init_ClassHandle=()=>{let proto=ClassHandle.prototype;Object.assign(proto,{isAliasOf(other){if(!(this instanceof ClassHandle)){return false}if(!(other instanceof ClassHandle)){return false}var leftClass=this.$$.ptrType.registeredClass;var left=this.$$.ptr;other.$$=other.$$;var rightClass=other.$$.ptrType.registeredClass;var right=other.$$.ptr;while(leftClass.baseClass){left=leftClass.upcast(left);leftClass=leftClass.baseClass}while(rightClass.baseClass){right=rightClass.upcast(right);rightClass=rightClass.baseClass}return leftClass===rightClass&&left===right},clone(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.preservePointerOnDelete){this.$$.count.value+=1;return this}else{var clone=attachFinalizer(Object.create(Object.getPrototypeOf(this),{$$:{value:shallowCopyInternalPointer(this.$$)}}));clone.$$.count.value+=1;clone.$$.deleteScheduled=false;return clone}},delete(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}detachFinalizer(this);releaseClassHandle(this.$$);if(!this.$$.preservePointerOnDelete){this.$$.smartPtr=undefined;this.$$.ptr=undefined}},isDeleted(){return!this.$$.ptr},deleteLater(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}deletionQueue.push(this);if(deletionQueue.length===1&&delayFunction){delayFunction(flushPendingDeletes)}this.$$.deleteScheduled=true;return this}});const symbolDispose=Symbol.dispose;if(symbolDispose){proto[symbolDispose]=proto["delete"]}};function ClassHandle(){}var registeredPointers={};var ensureOverloadTable=(proto,methodName,humanName)=>{if(undefined===proto[methodName].overloadTable){var prevFunc=proto[methodName];proto[methodName]=function(...args){if(!proto[methodName].overloadTable.hasOwnProperty(args.length)){throwBindingError(`Function '${humanName}' called with an invalid number of arguments (${args.length}) - expects one of (${proto[methodName].overloadTable})!`)}return proto[methodName].overloadTable[args.length].apply(this,args)};proto[methodName].overloadTable=[];proto[methodName].overloadTable[prevFunc.argCount]=prevFunc}};var exposePublicSymbol=(name,value,numArguments)=>{if(Module.hasOwnProperty(name)){if(undefined===numArguments||undefined!==Module[name].overloadTable&&undefined!==Module[name].overloadTable[numArguments]){throwBindingError(`Cannot register public name '${name}' twice`)}ensureOverloadTable(Module,name,name);if(Module[name].overloadTable.hasOwnProperty(numArguments)){throwBindingError(`Cannot register multiple overloads of a function with the same number of arguments (${numArguments})!`)}Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}};var char_0=48;var char_9=57;var makeLegalFunctionName=name=>{name=name.replace(/[^a-zA-Z0-9_]/g,"$");var f=name.charCodeAt(0);if(f>=char_0&&f<=char_9){return`_${name}`}return name};function RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast){this.name=name;this.constructor=constructor;this.instancePrototype=instancePrototype;this.rawDestructor=rawDestructor;this.baseClass=baseClass;this.getActualType=getActualType;this.upcast=upcast;this.downcast=downcast;this.pureVirtualFunctions=[]}var upcastPointer=(ptr,ptrClass,desiredClass)=>{while(ptrClass!==desiredClass){if(!ptrClass.upcast){throwBindingError(`Expected null or instance of ${desiredClass.name}, got an instance of ${ptrClass.name}`)}ptr=ptrClass.upcast(ptr);ptrClass=ptrClass.baseClass}return ptr};var embindRepr=v=>{if(v===null){return"null"}var t=typeof v;if(t==="object"||t==="array"||t==="function"){return v.toString()}else{return""+v}};function constNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}function genericPointerToWireType(destructors,handle){var ptr;if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}if(this.isSmartPointer){ptr=this.rawConstructor();if(destructors!==null){destructors.push(this.rawDestructor,ptr)}return ptr}else{return 0}}if(!handle||!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(!this.isConst&&handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);if(this.isSmartPointer){if(undefined===handle.$$.smartPtr){throwBindingError("Passing raw pointer to smart pointer is illegal")}switch(this.sharingPolicy){case 0:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}break;case 1:ptr=handle.$$.smartPtr;break;case 2:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{var clonedHandle=handle["clone"]();ptr=this.rawShare(ptr,Emval.toHandle(()=>clonedHandle["delete"]()));if(destructors!==null){destructors.push(this.rawDestructor,ptr)}}break;default:throwBindingError("Unsupported sharing policy")}}return ptr}function nonConstNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}var downcastPointer=(ptr,ptrClass,desiredClass)=>{if(ptrClass===desiredClass){return ptr}if(undefined===desiredClass.baseClass){return null}var rv=downcastPointer(ptr,ptrClass,desiredClass.baseClass);if(rv===null){return null}return desiredClass.downcast(rv)};var getInheritedInstance=(class_,ptr)=>{ptr=getBasestPointer(class_,ptr);return registeredInstances[ptr]};var makeClassHandle=(prototype,record)=>{if(!record.ptrType||!record.ptr){throwInternalError("makeClassHandle requires ptr and ptrType")}var hasSmartPtrType=!!record.smartPtrType;var hasSmartPtr=!!record.smartPtr;if(hasSmartPtrType!==hasSmartPtr){throwInternalError("Both smartPtrType and smartPtr must be specified")}record.count={value:1};return attachFinalizer(Object.create(prototype,{$$:{value:record,writable:true}}))};function RegisteredPointer_fromWireType(ptr){var rawPointer=this.getPointee(ptr);if(!rawPointer){this.destructor(ptr);return null}var registeredInstance=getInheritedInstance(this.registeredClass,rawPointer);if(undefined!==registeredInstance){if(0===registeredInstance.$$.count.value){registeredInstance.$$.ptr=rawPointer;registeredInstance.$$.smartPtr=ptr;return registeredInstance["clone"]()}else{var rv=registeredInstance["clone"]();this.destructor(ptr);return rv}}function makeDefaultHandle(){if(this.isSmartPointer){return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this.pointeeType,ptr:rawPointer,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this,ptr})}}var actualType=this.registeredClass.getActualType(rawPointer);var registeredPointerRecord=registeredPointers[actualType];if(!registeredPointerRecord){return makeDefaultHandle.call(this)}var toType;if(this.isConst){toType=registeredPointerRecord.constPointerType}else{toType=registeredPointerRecord.pointerType}var dp=downcastPointer(rawPointer,this.registeredClass,toType.registeredClass);if(dp===null){return makeDefaultHandle.call(this)}if(this.isSmartPointer){return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp})}}var init_RegisteredPointer=()=>{Object.assign(RegisteredPointer.prototype,{getPointee(ptr){if(this.rawGetPointee){ptr=this.rawGetPointee(ptr)}return ptr},destructor(ptr){this.rawDestructor?.(ptr)},readValueFromPointer:readPointer,fromWireType:RegisteredPointer_fromWireType})};function RegisteredPointer(name,registeredClass,isReference,isConst,isSmartPointer,pointeeType,sharingPolicy,rawGetPointee,rawConstructor,rawShare,rawDestructor){this.name=name;this.registeredClass=registeredClass;this.isReference=isReference;this.isConst=isConst;this.isSmartPointer=isSmartPointer;this.pointeeType=pointeeType;this.sharingPolicy=sharingPolicy;this.rawGetPointee=rawGetPointee;this.rawConstructor=rawConstructor;this.rawShare=rawShare;this.rawDestructor=rawDestructor;if(!isSmartPointer&®isteredClass.baseClass===undefined){if(isConst){this.toWireType=constNoSmartPtrRawPointerToWireType;this.destructorFunction=null}else{this.toWireType=nonConstNoSmartPtrRawPointerToWireType;this.destructorFunction=null}}else{this.toWireType=genericPointerToWireType}}var replacePublicSymbol=(name,value,numArguments)=>{if(!Module.hasOwnProperty(name)){throwInternalError("Replacing nonexistent public symbol")}if(undefined!==Module[name].overloadTable&&undefined!==numArguments){Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}};var wasmTableMirror=[];var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var embind__requireFunction=(signature,rawFunction,isAsync=false)=>{signature=AsciiToString(signature);function makeDynCaller(){var rtn=getWasmTableEntry(rawFunction);return rtn}var fp=makeDynCaller();if(typeof fp!="function"){throwBindingError(`unknown function pointer with signature ${signature}: ${rawFunction}`)}return fp};class UnboundTypeError extends Error{}var throwUnboundTypeError=(message,types)=>{var unboundTypes=[];var seen={};function visit(type){if(seen[type]){return}if(registeredTypes[type]){return}if(typeDependencies[type]){typeDependencies[type].forEach(visit);return}unboundTypes.push(type);seen[type]=true}types.forEach(visit);throw new UnboundTypeError(`${message}: `+unboundTypes.map(getTypeName).join([", "]))};var __embind_register_class=(rawType,rawPointerType,rawConstPointerType,baseClassRawType,getActualTypeSignature,getActualType,upcastSignature,upcast,downcastSignature,downcast,name,destructorSignature,rawDestructor)=>{name=AsciiToString(name);getActualType=embind__requireFunction(getActualTypeSignature,getActualType);upcast&&=embind__requireFunction(upcastSignature,upcast);downcast&&=embind__requireFunction(downcastSignature,downcast);rawDestructor=embind__requireFunction(destructorSignature,rawDestructor);var legalFunctionName=makeLegalFunctionName(name);exposePublicSymbol(legalFunctionName,function(){throwUnboundTypeError(`Cannot construct ${name} due to unbound types`,[baseClassRawType])});whenDependentTypesAreResolved([rawType,rawPointerType,rawConstPointerType],baseClassRawType?[baseClassRawType]:[],base=>{base=base[0];var baseClass;var basePrototype;if(baseClassRawType){baseClass=base.registeredClass;basePrototype=baseClass.instancePrototype}else{basePrototype=ClassHandle.prototype}var constructor=createNamedFunction(name,function(...args){if(Object.getPrototypeOf(this)!==instancePrototype){throw new BindingError(`Use 'new' to construct ${name}`)}if(undefined===registeredClass.constructor_body){throw new BindingError(`${name} has no accessible constructor`)}var body=registeredClass.constructor_body[args.length];if(undefined===body){throw new BindingError(`Tried to invoke ctor of ${name} with invalid number of parameters (${args.length}) - expected (${Object.keys(registeredClass.constructor_body).toString()}) parameters instead!`)}return body.apply(this,args)});var instancePrototype=Object.create(basePrototype,{constructor:{value:constructor}});constructor.prototype=instancePrototype;var registeredClass=new RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast);if(registeredClass.baseClass){registeredClass.baseClass.__derivedClasses??=[];registeredClass.baseClass.__derivedClasses.push(registeredClass)}var referenceConverter=new RegisteredPointer(name,registeredClass,true,false,false);var pointerConverter=new RegisteredPointer(name+"*",registeredClass,false,false,false);var constPointerConverter=new RegisteredPointer(name+" const*",registeredClass,false,true,false);registeredPointers[rawType]={pointerType:pointerConverter,constPointerType:constPointerConverter};replacePublicSymbol(legalFunctionName,constructor);return[referenceConverter,pointerConverter,constPointerConverter]})};function usesDestructorStack(argTypes){for(var i=1;i{var array=[];for(var i=0;i>2])}return array};var getFunctionName=signature=>{signature=signature.trim();const argsIndex=signature.indexOf("(");if(argsIndex===-1)return signature;return signature.slice(0,argsIndex)};var __embind_register_class_class_function=(rawClassType,methodName,argCount,rawArgTypesAddr,invokerSignature,rawInvoker,fn,isAsync,isNonnullReturn)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);methodName=AsciiToString(methodName);methodName=getFunctionName(methodName);rawInvoker=embind__requireFunction(invokerSignature,rawInvoker,isAsync);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`${classType.name}.${methodName}`;function unboundTypesHandler(){throwUnboundTypeError(`Cannot call ${humanName} due to unbound types`,rawArgTypes)}if(methodName.startsWith("@@")){methodName=Symbol[methodName.substring(2)]}var proto=classType.registeredClass.constructor;if(undefined===proto[methodName]){unboundTypesHandler.argCount=argCount-1;proto[methodName]=unboundTypesHandler}else{ensureOverloadTable(proto,methodName,humanName);proto[methodName].overloadTable[argCount-1]=unboundTypesHandler}whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{var invokerArgsArray=[argTypes[0],null].concat(argTypes.slice(1));var func=craftInvokerFunction(humanName,invokerArgsArray,null,rawInvoker,fn,isAsync);if(undefined===proto[methodName].overloadTable){func.argCount=argCount-1;proto[methodName]=func}else{proto[methodName].overloadTable[argCount-1]=func}if(classType.registeredClass.__derivedClasses){for(const derivedClass of classType.registeredClass.__derivedClasses){if(!derivedClass.constructor.hasOwnProperty(methodName)){derivedClass.constructor[methodName]=func}}}return[]});return[]})};var __embind_register_class_constructor=(rawClassType,argCount,rawArgTypesAddr,invokerSignature,invoker,rawConstructor)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);invoker=embind__requireFunction(invokerSignature,invoker);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`constructor ${classType.name}`;if(undefined===classType.registeredClass.constructor_body){classType.registeredClass.constructor_body=[]}if(undefined!==classType.registeredClass.constructor_body[argCount-1]){throw new BindingError(`Cannot register multiple constructors with identical number of parameters (${argCount-1}) for class '${classType.name}'! Overload resolution is currently only performed using the parameter count, not actual type info!`)}classType.registeredClass.constructor_body[argCount-1]=()=>{throwUnboundTypeError(`Cannot construct ${classType.name} due to unbound types`,rawArgTypes)};whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{argTypes.splice(1,0,null);classType.registeredClass.constructor_body[argCount-1]=craftInvokerFunction(humanName,argTypes,null,invoker,rawConstructor);return[]});return[]})};var __embind_register_class_function=(rawClassType,methodName,argCount,rawArgTypesAddr,invokerSignature,rawInvoker,context,isPureVirtual,isAsync,isNonnullReturn)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);methodName=AsciiToString(methodName);methodName=getFunctionName(methodName);rawInvoker=embind__requireFunction(invokerSignature,rawInvoker,isAsync);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`${classType.name}.${methodName}`;if(methodName.startsWith("@@")){methodName=Symbol[methodName.substring(2)]}if(isPureVirtual){classType.registeredClass.pureVirtualFunctions.push(methodName)}function unboundTypesHandler(){throwUnboundTypeError(`Cannot call ${humanName} due to unbound types`,rawArgTypes)}var proto=classType.registeredClass.instancePrototype;var method=proto[methodName];if(undefined===method||undefined===method.overloadTable&&method.className!==classType.name&&method.argCount===argCount-2){unboundTypesHandler.argCount=argCount-2;unboundTypesHandler.className=classType.name;proto[methodName]=unboundTypesHandler}else{ensureOverloadTable(proto,methodName,humanName);proto[methodName].overloadTable[argCount-2]=unboundTypesHandler}whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{var memberFunction=craftInvokerFunction(humanName,argTypes,classType,rawInvoker,context,isAsync);if(undefined===proto[methodName].overloadTable){memberFunction.argCount=argCount-2;proto[methodName]=memberFunction}else{proto[methodName].overloadTable[argCount-2]=memberFunction}return[]});return[]})};var validateThis=(this_,classType,humanName)=>{if(!(this_ instanceof Object)){throwBindingError(`${humanName} with invalid "this": ${this_}`)}if(!(this_ instanceof classType.registeredClass.constructor)){throwBindingError(`${humanName} incompatible with "this" of type ${this_.constructor.name}`)}if(!this_.$$.ptr){throwBindingError(`cannot call emscripten binding method ${humanName} on deleted object`)}return upcastPointer(this_.$$.ptr,this_.$$.ptrType.registeredClass,classType.registeredClass)};var __embind_register_class_property=(classType,fieldName,getterReturnType,getterSignature,getter,getterContext,setterArgumentType,setterSignature,setter,setterContext)=>{fieldName=AsciiToString(fieldName);getter=embind__requireFunction(getterSignature,getter);whenDependentTypesAreResolved([],[classType],classType=>{classType=classType[0];var humanName=`${classType.name}.${fieldName}`;var desc={get(){throwUnboundTypeError(`Cannot access ${humanName} due to unbound types`,[getterReturnType,setterArgumentType])},enumerable:true,configurable:true};if(setter){desc.set=()=>throwUnboundTypeError(`Cannot access ${humanName} due to unbound types`,[getterReturnType,setterArgumentType])}else{desc.set=v=>throwBindingError(humanName+" is a read-only property")}Object.defineProperty(classType.registeredClass.instancePrototype,fieldName,desc);whenDependentTypesAreResolved([],setter?[getterReturnType,setterArgumentType]:[getterReturnType],types=>{var getterReturnType=types[0];var desc={get(){var ptr=validateThis(this,classType,humanName+" getter");return getterReturnType.fromWireType(getter(getterContext,ptr))},enumerable:true};if(setter){setter=embind__requireFunction(setterSignature,setter);var setterArgumentType=types[1];desc.set=function(v){var ptr=validateThis(this,classType,humanName+" setter");var destructors=[];setter(setterContext,ptr,setterArgumentType.toWireType(destructors,v));runDestructors(destructors)}}Object.defineProperty(classType.registeredClass.instancePrototype,fieldName,desc);return[]});return[]})};var __embind_register_constant=(name,type,value)=>{name=AsciiToString(name);whenDependentTypesAreResolved([],[type],type=>{type=type[0];Module[name]=type.fromWireType(value);return[]})};var __emval_decref=handle=>{if(handle>9&&0===--emval_handles[handle+1]){emval_handles[handle]=undefined;emval_freelist.push(handle)}};var EmValType={name:"emscripten::val",fromWireType:handle=>{var rv=Emval.toValue(handle);__emval_decref(handle);return rv},toWireType:(destructors,value)=>Emval.toHandle(value),readValueFromPointer:readPointer,destructorFunction:null};var __embind_register_emval=rawType=>registerType(rawType,EmValType);var enumReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?function(pointer){return this.fromWireType(HEAP8[pointer])}:function(pointer){return this.fromWireType(HEAPU8[pointer])};case 2:return signed?function(pointer){return this.fromWireType(HEAP16[pointer>>1])}:function(pointer){return this.fromWireType(HEAPU16[pointer>>1])};case 4:return signed?function(pointer){return this.fromWireType(HEAP32[pointer>>2])}:function(pointer){return this.fromWireType(HEAPU32[pointer>>2])};default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};function getEnumValueType(rawValueType){return rawValueType===0?"object":rawValueType===1?"number":"string"}var __embind_register_enum=(rawType,name,size,isSigned,rawValueType)=>{name=AsciiToString(name);const valueType=getEnumValueType(rawValueType);switch(valueType){case"object":{function ctor(){}ctor.values={};registerType(rawType,{name,constructor:ctor,valueType,fromWireType:function(c){return this.constructor.values[c]},toWireType:(destructors,c)=>c.value,readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,ctor);break}case"number":{var keysMap={};registerType(rawType,{name,keysMap,valueType,fromWireType:c=>c,toWireType:(destructors,c)=>c,readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,keysMap);delete Module[name].argCount;break}case"string":{var valuesMap={};var reverseMap={};var keysMap={};registerType(rawType,{name,valuesMap,reverseMap,keysMap,valueType,fromWireType:function(c){return this.reverseMap[c]},toWireType:function(destructors,c){return this.valuesMap[c]},readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,keysMap);delete Module[name].argCount;break}}};var __embind_register_enum_value=(rawEnumType,name,enumValue)=>{var enumType=requireRegisteredType(rawEnumType,"enum");name=AsciiToString(name);switch(enumType.valueType){case"object":{var Enum=enumType.constructor;var Value=Object.create(enumType.constructor.prototype,{value:{value:enumValue},constructor:{value:createNamedFunction(`${enumType.name}_${name}`,function(){})}});Enum.values[enumValue]=Value;Enum[name]=Value;break}case"number":{enumType.keysMap[name]=enumValue;break}case"string":{enumType.valuesMap[name]=enumValue;enumType.reverseMap[enumValue]=name;enumType.keysMap[name]=name;break}}};var floatReadValueFromPointer=(name,width)=>{switch(width){case 4:return function(pointer){return this.fromWireType(HEAPF32[pointer>>2])};case 8:return function(pointer){return this.fromWireType(HEAPF64[pointer>>3])};default:throw new TypeError(`invalid float width (${width}): ${name}`)}};var __embind_register_float=(rawType,name,size)=>{name=AsciiToString(name);registerType(rawType,{name,fromWireType:value=>value,toWireType:(destructors,value)=>value,readValueFromPointer:floatReadValueFromPointer(name,size),destructorFunction:null})};var __embind_register_function=(name,argCount,rawArgTypesAddr,signature,rawInvoker,fn,isAsync,isNonnullReturn)=>{var argTypes=heap32VectorToArray(argCount,rawArgTypesAddr);name=AsciiToString(name);name=getFunctionName(name);rawInvoker=embind__requireFunction(signature,rawInvoker,isAsync);exposePublicSymbol(name,function(){throwUnboundTypeError(`Cannot call ${name} due to unbound types`,argTypes)},argCount-1);whenDependentTypesAreResolved([],argTypes,argTypes=>{var invokerArgsArray=[argTypes[0],null].concat(argTypes.slice(1));replacePublicSymbol(name,craftInvokerFunction(name,invokerArgsArray,null,rawInvoker,fn,isAsync),argCount-1);return[]})};var __embind_register_integer=(primitiveType,name,size,minRange,maxRange)=>{name=AsciiToString(name);const isUnsignedType=minRange===0;let fromWireType=value=>value;if(isUnsignedType){var bitshift=32-8*size;fromWireType=value=>value<>>bitshift;maxRange=fromWireType(maxRange)}registerType(primitiveType,{name,fromWireType,toWireType:(destructors,value)=>value,readValueFromPointer:integerReadValueFromPointer(name,size,minRange!==0),destructorFunction:null})};var installIndexedIterator=(proto,sizeMethodName,getMethodName)=>{const makeIterator=(size,getValue)=>{let index=0;return{next(){if(index>=size){return{done:true}}const current=index;index++;const value=getValue(current);return{value,done:false}},[Symbol.iterator](){return this}}};if(!proto[Symbol.iterator]){proto[Symbol.iterator]=function(){const size=this[sizeMethodName]();return makeIterator(size,i=>this[getMethodName](i))}}};var __embind_register_iterable=(rawClassType,rawElementType,sizeMethodName,getMethodName)=>{sizeMethodName=AsciiToString(sizeMethodName);getMethodName=AsciiToString(getMethodName);whenDependentTypesAreResolved([],[rawClassType,rawElementType],types=>{const classType=types[0];installIndexedIterator(classType.registeredClass.instancePrototype,sizeMethodName,getMethodName);return[]})};var __embind_register_memory_view=(rawType,dataTypeIndex,name)=>{var typeMapping=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array,BigInt64Array,BigUint64Array];var TA=typeMapping[dataTypeIndex];function decodeMemoryView(handle){var size=HEAPU32[handle>>2];var data=HEAPU32[handle+4>>2];return new TA(HEAP8.buffer,data,size)}name=AsciiToString(name);registerType(rawType,{name,fromWireType:decodeMemoryView,readValueFromPointer:decodeMemoryView},{ignoreDuplicateRegistrations:true})};var EmValOptionalType=Object.assign({optional:true},EmValType);var __embind_register_optional=(rawOptionalType,rawType)=>{registerType(rawOptionalType,EmValOptionalType)};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var __embind_register_std_string=(rawType,name)=>{name=AsciiToString(name);var stdStringIsUTF8=true;registerType(rawType,{name,fromWireType(value){var length=HEAPU32[value>>2];var payload=value+4;var str;if(stdStringIsUTF8){str=UTF8ToString(payload,length,true)}else{str="";for(var i=0;i>2]=length;if(valueIsOfTypeString){if(stdStringIsUTF8){stringToUTF8(value,ptr,length+1)}else{for(var i=0;i255){_free(base);throwBindingError("String has UTF-16 code units that do not fit in 8 bits")}HEAPU8[ptr+i]=charCode}}}else{HEAPU8.set(value,ptr)}if(destructors!==null){destructors.push(_free,base)}return base},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var UTF16Decoder=globalThis.TextDecoder?new TextDecoder("utf-16le"):undefined;var UTF16ToString=(ptr,maxBytesToRead,ignoreNul)=>{var idx=ptr>>1;var endIdx=findStringEnd(HEAPU16,idx,maxBytesToRead/2,ignoreNul);if(endIdx-idx>16&&UTF16Decoder)return UTF16Decoder.decode(HEAPU16.subarray(idx,endIdx));var str="";for(var i=idx;i{maxBytesToWrite??=2147483647;if(maxBytesToWrite<2)return 0;maxBytesToWrite-=2;var startPtr=outPtr;var numCharsToWrite=maxBytesToWrite>1]=codeUnit;outPtr+=2}HEAP16[outPtr>>1]=0;return outPtr-startPtr};var lengthBytesUTF16=str=>str.length*2;var UTF32ToString=(ptr,maxBytesToRead,ignoreNul)=>{var str="";var startIdx=ptr>>2;for(var i=0;!(i>=maxBytesToRead/4);i++){var utf32=HEAPU32[startIdx+i];if(!utf32&&!ignoreNul)break;str+=String.fromCodePoint(utf32)}return str};var stringToUTF32=(str,outPtr,maxBytesToWrite)=>{maxBytesToWrite??=2147483647;if(maxBytesToWrite<4)return 0;var startPtr=outPtr;var endPtr=startPtr+maxBytesToWrite-4;for(var i=0;i65535){i++}HEAP32[outPtr>>2]=codePoint;outPtr+=4;if(outPtr+4>endPtr)break}HEAP32[outPtr>>2]=0;return outPtr-startPtr};var lengthBytesUTF32=str=>{var len=0;for(var i=0;i65535){i++}len+=4}return len};var __embind_register_std_wstring=(rawType,charSize,name)=>{name=AsciiToString(name);var decodeString,encodeString,lengthBytesUTF;if(charSize===2){decodeString=UTF16ToString;encodeString=stringToUTF16;lengthBytesUTF=lengthBytesUTF16}else{decodeString=UTF32ToString;encodeString=stringToUTF32;lengthBytesUTF=lengthBytesUTF32}registerType(rawType,{name,fromWireType:value=>{var length=HEAPU32[value>>2];var str=decodeString(value+4,length*charSize,true);_free(value);return str},toWireType:(destructors,value)=>{if(!(typeof value=="string")){throwBindingError(`Cannot pass non-string to C++ string type ${name}`)}var length=lengthBytesUTF(value);var ptr=_malloc(4+length+charSize);HEAPU32[ptr>>2]=length/charSize;encodeString(value,ptr+4,length+charSize);if(destructors!==null){destructors.push(_free,ptr)}return ptr},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var __embind_register_value_object=(rawType,name,constructorSignature,rawConstructor,destructorSignature,rawDestructor)=>{structRegistrations[rawType]={name:AsciiToString(name),rawConstructor:embind__requireFunction(constructorSignature,rawConstructor),rawDestructor:embind__requireFunction(destructorSignature,rawDestructor),fields:[]}};var __embind_register_value_object_field=(structType,fieldName,getterReturnType,getterSignature,getter,getterContext,setterArgumentType,setterSignature,setter,setterContext)=>{structRegistrations[structType].fields.push({fieldName:AsciiToString(fieldName),getterReturnType,getter:embind__requireFunction(getterSignature,getter),getterContext,setterArgumentType,setter:embind__requireFunction(setterSignature,setter),setterContext})};var __embind_register_void=(rawType,name)=>{name=AsciiToString(name);registerType(rawType,{isVoid:true,name,fromWireType:()=>undefined,toWireType:(destructors,o)=>undefined})};var runtimeKeepaliveCounter=0;var __emscripten_runtime_keepalive_clear=()=>{noExitRuntime=false;runtimeKeepaliveCounter=0};var emval_methodCallers=[];var emval_addMethodCaller=caller=>{var id=emval_methodCallers.length;emval_methodCallers.push(caller);return id};var emval_lookupTypes=(argCount,argTypes)=>{var a=new Array(argCount);for(var i=0;i>2],`parameter ${i}`)}return a};var emval_returnValue=(toReturnWire,destructorsRef,handle)=>{var destructors=[];var result=toReturnWire(destructors,handle);if(destructors.length){HEAPU32[destructorsRef>>2]=Emval.toHandle(destructors)}return result};var emval_symbols={};var getStringOrSymbol=address=>{var symbol=emval_symbols[address];if(symbol===undefined){return AsciiToString(address)}return symbol};var __emval_create_invoker=(argCount,argTypesPtr,kind)=>{var GenericWireTypeSize=8;var[retType,...argTypes]=emval_lookupTypes(argCount,argTypesPtr);var toReturnWire=retType.toWireType.bind(retType);var argFromPtr=argTypes.map(type=>type.readValueFromPointer.bind(type));argCount--;var argN=new Array(argCount);var invokerFunction=(handle,methodName,destructorsRef,args)=>{var offset=0;for(var i=0;it.name)}) => ${retType.name}>`;return emval_addMethodCaller(createNamedFunction(functionName,invokerFunction))};var __emval_invoke=(caller,handle,methodName,destructorsRef,args)=>emval_methodCallers[caller](handle,methodName,destructorsRef,args);var __emval_new_cstring=v=>Emval.toHandle(getStringOrSymbol(v));var __emval_new_object=()=>Emval.toHandle({});var __emval_run_destructors=handle=>{var destructors=Emval.toValue(handle);runDestructors(destructors);__emval_decref(handle)};var __emval_set_property=(handle,key,value)=>{handle=Emval.toValue(handle);key=Emval.toValue(key);value=Emval.toValue(value);handle[key]=value};var timers={};var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;var maybeExit=()=>{if(!keepRuntimeAlive()){try{_exit(EXITSTATUS)}catch(e){handleException(e)}}};var callUserCallback=func=>{if(ABORT){return}try{return func()}catch(e){handleException(e)}finally{maybeExit()}};var _emscripten_get_now=()=>performance.now();var __setitimer_js=(which,timeout_ms)=>{if(timers[which]){clearTimeout(timers[which].id);delete timers[which]}if(!timeout_ms)return 0;var id=setTimeout(()=>{delete timers[which];callUserCallback(()=>__emscripten_timeout(which,_emscripten_get_now()))},timeout_ms);timers[which]={id,timeout_ms};return 0};var _emscripten_date_now=()=>Date.now();var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var printCharBuffers=[null,[],[]];var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(curr===0||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer));buffer.length=0}else{buffer.push(curr)}};var _fd_write=(fd,iov,iovcnt,pnum)=>{var num=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j>2]=num;return 0};init_ClassHandle();init_RegisteredPointer();{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}var ___getTypeName,_free,_malloc,__emscripten_timeout,memory,__indirect_function_table,wasmMemory,wasmTable;function assignWasmExports(wasmExports){___getTypeName=wasmExports["Q"];_free=Module["_free"]=wasmExports["R"];_malloc=Module["_malloc"]=wasmExports["S"];__emscripten_timeout=wasmExports["U"];memory=wasmMemory=wasmExports["O"];__indirect_function_table=wasmTable=wasmExports["T"]}var wasmImports={J:__abort_js,x:__embind_create_inheriting_constructor,s:__embind_finalize_value_object,z:__embind_register_bigint,A:__embind_register_bool,b:__embind_register_class,p:__embind_register_class_class_function,e:__embind_register_class_constructor,a:__embind_register_class_function,d:__embind_register_class_property,K:__embind_register_constant,M:__embind_register_emval,l:__embind_register_enum,f:__embind_register_enum_value,y:__embind_register_float,g:__embind_register_function,o:__embind_register_integer,q:__embind_register_iterable,h:__embind_register_memory_view,r:__embind_register_optional,N:__embind_register_std_string,u:__embind_register_std_wstring,t:__embind_register_value_object,m:__embind_register_value_object_field,B:__embind_register_void,D:__emscripten_runtime_keepalive_clear,k:__emval_create_invoker,n:__emval_decref,j:__emval_invoke,v:__emval_new_cstring,L:__emval_new_object,i:__emval_run_destructors,w:__emval_set_property,E:__setitimer_js,I:_emscripten_date_now,c:_emscripten_get_now,F:_emscripten_resize_heap,G:_exit,H:_fd_write,C:_proc_exit};function run(){preRun();function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} +var PHYSX=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};if(typeof __filename!="undefined"){_scriptName=__filename}else if(ENVIRONMENT_IS_WORKER){_scriptName=self.location.href}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{if(isFileURI(url)){return new Promise((resolve,reject)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){resolve(xhr.response);return}reject(xhr.status)};xhr.onerror=reject;xhr.send(null)})}var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var EXITSTATUS;var isFileURI=filename=>filename.startsWith("file://");var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["P"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("physx.release.simd.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!isFileURI(binaryFile)&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var __abort_js=()=>abort("");var createNamedFunction=(name,func)=>Object.defineProperty(func,"name",{value:name});var emval_freelist=[];var emval_handles=[0,1,,1,null,1,true,1,false,1];var BindingError=class BindingError extends Error{constructor(message){super(message);this.name="BindingError"}};var throwBindingError=message=>{throw new BindingError(message)};var Emval={toValue:handle=>{if(!handle){throwBindingError(`Cannot use deleted val. handle = ${handle}`)}return emval_handles[handle]},toHandle:value=>{switch(value){case undefined:return 2;case null:return 4;case true:return 6;case false:return 8;default:{const handle=emval_freelist.pop()||emval_handles.length;emval_handles[handle]=value;emval_handles[handle+1]=1;return handle}}}};class PureVirtualError extends Error{}var AsciiToString=ptr=>{var str="";while(1){var ch=HEAPU8[ptr++];if(!ch)return str;str+=String.fromCharCode(ch)}};var registeredInstances={};var getBasestPointer=(class_,ptr)=>{if(ptr===undefined){throwBindingError("ptr should not be undefined")}while(class_.baseClass){ptr=class_.upcast(ptr);class_=class_.baseClass}return ptr};var registerInheritedInstance=(class_,ptr,instance)=>{ptr=getBasestPointer(class_,ptr);if(registeredInstances.hasOwnProperty(ptr)){throwBindingError(`Tried to register registered instance: ${ptr}`)}else{registeredInstances[ptr]=instance}};var registeredTypes={};var getTypeName=type=>{var ptr=___getTypeName(type);var rv=AsciiToString(ptr);_free(ptr);return rv};var requireRegisteredType=(rawType,humanName)=>{var impl=registeredTypes[rawType];if(undefined===impl){throwBindingError(`${humanName} has unknown type ${getTypeName(rawType)}`)}return impl};var unregisterInheritedInstance=(class_,ptr)=>{ptr=getBasestPointer(class_,ptr);if(registeredInstances.hasOwnProperty(ptr)){delete registeredInstances[ptr]}else{throwBindingError(`Tried to unregister unregistered instance: ${ptr}`)}};var detachFinalizer=handle=>{};var finalizationRegistry=false;var runDestructor=$$=>{if($$.smartPtr){$$.smartPtrType.rawDestructor($$.smartPtr)}else{$$.ptrType.registeredClass.rawDestructor($$.ptr)}};var releaseClassHandle=$$=>{$$.count.value-=1;var toDelete=0===$$.count.value;if(toDelete){runDestructor($$)}};var attachFinalizer=handle=>{if(!globalThis.FinalizationRegistry){attachFinalizer=handle=>handle;return handle}finalizationRegistry=new FinalizationRegistry(info=>{releaseClassHandle(info.$$)});attachFinalizer=handle=>{var $$=handle.$$;var hasSmartPtr=!!$$.smartPtr;if(hasSmartPtr){var info={$$};finalizationRegistry.register(handle,info,handle)}return handle};detachFinalizer=handle=>finalizationRegistry.unregister(handle);return attachFinalizer(handle)};var __embind_create_inheriting_constructor=(constructorName,wrapperType,properties)=>{constructorName=AsciiToString(constructorName);wrapperType=requireRegisteredType(wrapperType,"wrapper");properties=Emval.toValue(properties);var registeredClass=wrapperType.registeredClass;var wrapperPrototype=registeredClass.instancePrototype;var baseClass=registeredClass.baseClass;var baseClassPrototype=baseClass.instancePrototype;var baseConstructor=registeredClass.baseClass.constructor;var ctor=createNamedFunction(constructorName,function(...args){for(var name of registeredClass.baseClass.pureVirtualFunctions){if(this[name]===baseClassPrototype[name]){throw new PureVirtualError(`Pure virtual function ${name} must be implemented in JavaScript`)}}Object.defineProperty(this,"__parent",{value:wrapperPrototype});this["__construct"](...args)});wrapperPrototype["__construct"]=function __construct(...args){if(this===wrapperPrototype){throwBindingError("Pass correct 'this' to __construct")}var inner=baseConstructor["implement"](this,...args);detachFinalizer(inner);var $$=inner.$$;inner["notifyOnDestruction"]();$$.preservePointerOnDelete=true;Object.defineProperties(this,{$$:{value:$$}});attachFinalizer(this);registerInheritedInstance(registeredClass,$$.ptr,this)};wrapperPrototype["__destruct"]=function __destruct(){if(this===wrapperPrototype){throwBindingError("Pass correct 'this' to __destruct")}detachFinalizer(this);unregisterInheritedInstance(registeredClass,this.$$.ptr)};ctor.prototype=Object.create(wrapperPrototype);Object.assign(ctor.prototype,properties);return Emval.toHandle(ctor)};var structRegistrations={};var runDestructors=destructors=>{while(destructors.length){var ptr=destructors.pop();var del=destructors.pop();del(ptr)}};function readPointer(pointer){return this.fromWireType(HEAPU32[pointer>>2])}var awaitingDependencies={};var typeDependencies={};var InternalError=class InternalError extends Error{constructor(message){super(message);this.name="InternalError"}};var throwInternalError=message=>{throw new InternalError(message)};var whenDependentTypesAreResolved=(myTypes,dependentTypes,getTypeConverters)=>{myTypes.forEach(type=>typeDependencies[type]=dependentTypes);function onComplete(typeConverters){var myTypeConverters=getTypeConverters(typeConverters);if(myTypeConverters.length!==myTypes.length){throwInternalError("Mismatched type converter count")}for(var i=0;i{typeConverters[i]=registeredTypes[dt];++registered;if(registered===unregisteredTypes.length){onComplete(typeConverters)}})}}if(0===unregisteredTypes.length){onComplete(typeConverters)}};var __embind_finalize_value_object=structType=>{var reg=structRegistrations[structType];delete structRegistrations[structType];var rawConstructor=reg.rawConstructor;var rawDestructor=reg.rawDestructor;var fieldRecords=reg.fields;var fieldTypes=fieldRecords.map(field=>field.getterReturnType).concat(fieldRecords.map(field=>field.setterArgumentType));whenDependentTypesAreResolved([structType],fieldTypes,fieldTypes=>{var fields={};for(var[i,field]of fieldRecords.entries()){const getterReturnType=fieldTypes[i];const getter=field.getter;const getterContext=field.getterContext;const setterArgumentType=fieldTypes[i+fieldRecords.length];const setter=field.setter;const setterContext=field.setterContext;fields[field.fieldName]={read:ptr=>getterReturnType.fromWireType(getter(getterContext,ptr)),write:(ptr,o)=>{var destructors=[];setter(setterContext,ptr,setterArgumentType.toWireType(destructors,o));runDestructors(destructors)},optional:getterReturnType.optional}}return[{name:reg.name,fromWireType:ptr=>{var rv={};for(var i in fields){rv[i]=fields[i].read(ptr)}rawDestructor(ptr);return rv},toWireType:(destructors,o)=>{for(var fieldName in fields){if(!(fieldName in o)&&!fields[fieldName].optional){throw new TypeError(`Missing field: "${fieldName}"`)}}var ptr=rawConstructor();for(fieldName in fields){fields[fieldName].write(ptr,o[fieldName])}if(destructors!==null){destructors.push(rawDestructor,ptr)}return ptr},readValueFromPointer:readPointer,destructorFunction:rawDestructor}]})};function sharedRegisterType(rawType,registeredInstance,options={}){var name=registeredInstance.name;if(!rawType){throwBindingError(`type "${name}" must have a positive integer typeid pointer`)}if(registeredTypes.hasOwnProperty(rawType)){if(options.ignoreDuplicateRegistrations){return}else{throwBindingError(`Cannot register type '${name}' twice`)}}registeredTypes[rawType]=registeredInstance;delete typeDependencies[rawType];if(awaitingDependencies.hasOwnProperty(rawType)){var callbacks=awaitingDependencies[rawType];delete awaitingDependencies[rawType];callbacks.forEach(cb=>cb())}}function registerType(rawType,registeredInstance,options={}){return sharedRegisterType(rawType,registeredInstance,options)}var integerReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?pointer=>HEAP8[pointer]:pointer=>HEAPU8[pointer];case 2:return signed?pointer=>HEAP16[pointer>>1]:pointer=>HEAPU16[pointer>>1];case 4:return signed?pointer=>HEAP32[pointer>>2]:pointer=>HEAPU32[pointer>>2];case 8:return signed?pointer=>HEAP64[pointer>>3]:pointer=>HEAPU64[pointer>>3];default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};var __embind_register_bigint=(primitiveType,name,size,minRange,maxRange)=>{name=AsciiToString(name);const isUnsignedType=minRange===0n;let fromWireType=value=>value;if(isUnsignedType){const bitSize=size*8;fromWireType=value=>BigInt.asUintN(bitSize,value);maxRange=fromWireType(maxRange)}registerType(primitiveType,{name,fromWireType,toWireType:(destructors,value)=>{if(typeof value=="number"){value=BigInt(value)}return value},readValueFromPointer:integerReadValueFromPointer(name,size,!isUnsignedType),destructorFunction:null})};var __embind_register_bool=(rawType,name,trueValue,falseValue)=>{name=AsciiToString(name);registerType(rawType,{name,fromWireType:function(wt){return!!wt},toWireType:function(destructors,o){return o?trueValue:falseValue},readValueFromPointer:function(pointer){return this.fromWireType(HEAPU8[pointer])},destructorFunction:null})};var shallowCopyInternalPointer=o=>({count:o.count,deleteScheduled:o.deleteScheduled,preservePointerOnDelete:o.preservePointerOnDelete,ptr:o.ptr,ptrType:o.ptrType,smartPtr:o.smartPtr,smartPtrType:o.smartPtrType});var throwInstanceAlreadyDeleted=obj=>{function getInstanceTypeName(handle){return handle.$$.ptrType.registeredClass.name}throwBindingError(getInstanceTypeName(obj)+" instance already deleted")};var deletionQueue=[];var flushPendingDeletes=()=>{while(deletionQueue.length){var obj=deletionQueue.pop();obj.$$.deleteScheduled=false;obj["delete"]()}};var delayFunction;var init_ClassHandle=()=>{let proto=ClassHandle.prototype;Object.assign(proto,{isAliasOf(other){if(!(this instanceof ClassHandle)){return false}if(!(other instanceof ClassHandle)){return false}var leftClass=this.$$.ptrType.registeredClass;var left=this.$$.ptr;other.$$=other.$$;var rightClass=other.$$.ptrType.registeredClass;var right=other.$$.ptr;while(leftClass.baseClass){left=leftClass.upcast(left);leftClass=leftClass.baseClass}while(rightClass.baseClass){right=rightClass.upcast(right);rightClass=rightClass.baseClass}return leftClass===rightClass&&left===right},clone(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.preservePointerOnDelete){this.$$.count.value+=1;return this}else{var clone=attachFinalizer(Object.create(Object.getPrototypeOf(this),{$$:{value:shallowCopyInternalPointer(this.$$)}}));clone.$$.count.value+=1;clone.$$.deleteScheduled=false;return clone}},delete(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}detachFinalizer(this);releaseClassHandle(this.$$);if(!this.$$.preservePointerOnDelete){this.$$.smartPtr=undefined;this.$$.ptr=undefined}},isDeleted(){return!this.$$.ptr},deleteLater(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}deletionQueue.push(this);if(deletionQueue.length===1&&delayFunction){delayFunction(flushPendingDeletes)}this.$$.deleteScheduled=true;return this}});const symbolDispose=Symbol.dispose;if(symbolDispose){proto[symbolDispose]=proto["delete"]}};function ClassHandle(){}var registeredPointers={};var ensureOverloadTable=(proto,methodName,humanName)=>{if(undefined===proto[methodName].overloadTable){var prevFunc=proto[methodName];proto[methodName]=function(...args){if(!proto[methodName].overloadTable.hasOwnProperty(args.length)){throwBindingError(`Function '${humanName}' called with an invalid number of arguments (${args.length}) - expects one of (${proto[methodName].overloadTable})!`)}return proto[methodName].overloadTable[args.length].apply(this,args)};proto[methodName].overloadTable=[];proto[methodName].overloadTable[prevFunc.argCount]=prevFunc}};var exposePublicSymbol=(name,value,numArguments)=>{if(Module.hasOwnProperty(name)){if(undefined===numArguments||undefined!==Module[name].overloadTable&&undefined!==Module[name].overloadTable[numArguments]){throwBindingError(`Cannot register public name '${name}' twice`)}ensureOverloadTable(Module,name,name);if(Module[name].overloadTable.hasOwnProperty(numArguments)){throwBindingError(`Cannot register multiple overloads of a function with the same number of arguments (${numArguments})!`)}Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}};var char_0=48;var char_9=57;var makeLegalFunctionName=name=>{name=name.replace(/[^a-zA-Z0-9_]/g,"$");var f=name.charCodeAt(0);if(f>=char_0&&f<=char_9){return`_${name}`}return name};function RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast){this.name=name;this.constructor=constructor;this.instancePrototype=instancePrototype;this.rawDestructor=rawDestructor;this.baseClass=baseClass;this.getActualType=getActualType;this.upcast=upcast;this.downcast=downcast;this.pureVirtualFunctions=[]}var upcastPointer=(ptr,ptrClass,desiredClass)=>{while(ptrClass!==desiredClass){if(!ptrClass.upcast){throwBindingError(`Expected null or instance of ${desiredClass.name}, got an instance of ${ptrClass.name}`)}ptr=ptrClass.upcast(ptr);ptrClass=ptrClass.baseClass}return ptr};var embindRepr=v=>{if(v===null){return"null"}var t=typeof v;if(t==="object"||t==="array"||t==="function"){return v.toString()}else{return""+v}};function constNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}function genericPointerToWireType(destructors,handle){var ptr;if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}if(this.isSmartPointer){ptr=this.rawConstructor();if(destructors!==null){destructors.push(this.rawDestructor,ptr)}return ptr}else{return 0}}if(!handle||!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(!this.isConst&&handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);if(this.isSmartPointer){if(undefined===handle.$$.smartPtr){throwBindingError("Passing raw pointer to smart pointer is illegal")}switch(this.sharingPolicy){case 0:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}break;case 1:ptr=handle.$$.smartPtr;break;case 2:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{var clonedHandle=handle["clone"]();ptr=this.rawShare(ptr,Emval.toHandle(()=>clonedHandle["delete"]()));if(destructors!==null){destructors.push(this.rawDestructor,ptr)}}break;default:throwBindingError("Unsupported sharing policy")}}return ptr}function nonConstNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}var downcastPointer=(ptr,ptrClass,desiredClass)=>{if(ptrClass===desiredClass){return ptr}if(undefined===desiredClass.baseClass){return null}var rv=downcastPointer(ptr,ptrClass,desiredClass.baseClass);if(rv===null){return null}return desiredClass.downcast(rv)};var getInheritedInstance=(class_,ptr)=>{ptr=getBasestPointer(class_,ptr);return registeredInstances[ptr]};var makeClassHandle=(prototype,record)=>{if(!record.ptrType||!record.ptr){throwInternalError("makeClassHandle requires ptr and ptrType")}var hasSmartPtrType=!!record.smartPtrType;var hasSmartPtr=!!record.smartPtr;if(hasSmartPtrType!==hasSmartPtr){throwInternalError("Both smartPtrType and smartPtr must be specified")}record.count={value:1};return attachFinalizer(Object.create(prototype,{$$:{value:record,writable:true}}))};function RegisteredPointer_fromWireType(ptr){var rawPointer=this.getPointee(ptr);if(!rawPointer){this.destructor(ptr);return null}var registeredInstance=getInheritedInstance(this.registeredClass,rawPointer);if(undefined!==registeredInstance){if(0===registeredInstance.$$.count.value){registeredInstance.$$.ptr=rawPointer;registeredInstance.$$.smartPtr=ptr;return registeredInstance["clone"]()}else{var rv=registeredInstance["clone"]();this.destructor(ptr);return rv}}function makeDefaultHandle(){if(this.isSmartPointer){return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this.pointeeType,ptr:rawPointer,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this,ptr})}}var actualType=this.registeredClass.getActualType(rawPointer);var registeredPointerRecord=registeredPointers[actualType];if(!registeredPointerRecord){return makeDefaultHandle.call(this)}var toType;if(this.isConst){toType=registeredPointerRecord.constPointerType}else{toType=registeredPointerRecord.pointerType}var dp=downcastPointer(rawPointer,this.registeredClass,toType.registeredClass);if(dp===null){return makeDefaultHandle.call(this)}if(this.isSmartPointer){return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp})}}var init_RegisteredPointer=()=>{Object.assign(RegisteredPointer.prototype,{getPointee(ptr){if(this.rawGetPointee){ptr=this.rawGetPointee(ptr)}return ptr},destructor(ptr){this.rawDestructor?.(ptr)},readValueFromPointer:readPointer,fromWireType:RegisteredPointer_fromWireType})};function RegisteredPointer(name,registeredClass,isReference,isConst,isSmartPointer,pointeeType,sharingPolicy,rawGetPointee,rawConstructor,rawShare,rawDestructor){this.name=name;this.registeredClass=registeredClass;this.isReference=isReference;this.isConst=isConst;this.isSmartPointer=isSmartPointer;this.pointeeType=pointeeType;this.sharingPolicy=sharingPolicy;this.rawGetPointee=rawGetPointee;this.rawConstructor=rawConstructor;this.rawShare=rawShare;this.rawDestructor=rawDestructor;if(!isSmartPointer&®isteredClass.baseClass===undefined){if(isConst){this.toWireType=constNoSmartPtrRawPointerToWireType;this.destructorFunction=null}else{this.toWireType=nonConstNoSmartPtrRawPointerToWireType;this.destructorFunction=null}}else{this.toWireType=genericPointerToWireType}}var replacePublicSymbol=(name,value,numArguments)=>{if(!Module.hasOwnProperty(name)){throwInternalError("Replacing nonexistent public symbol")}if(undefined!==Module[name].overloadTable&&undefined!==numArguments){Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}};var wasmTableMirror=[];var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var embind__requireFunction=(signature,rawFunction,isAsync=false)=>{signature=AsciiToString(signature);function makeDynCaller(){var rtn=getWasmTableEntry(rawFunction);return rtn}var fp=makeDynCaller();if(typeof fp!="function"){throwBindingError(`unknown function pointer with signature ${signature}: ${rawFunction}`)}return fp};class UnboundTypeError extends Error{}var throwUnboundTypeError=(message,types)=>{var unboundTypes=[];var seen={};function visit(type){if(seen[type]){return}if(registeredTypes[type]){return}if(typeDependencies[type]){typeDependencies[type].forEach(visit);return}unboundTypes.push(type);seen[type]=true}types.forEach(visit);throw new UnboundTypeError(`${message}: `+unboundTypes.map(getTypeName).join([", "]))};var __embind_register_class=(rawType,rawPointerType,rawConstPointerType,baseClassRawType,getActualTypeSignature,getActualType,upcastSignature,upcast,downcastSignature,downcast,name,destructorSignature,rawDestructor)=>{name=AsciiToString(name);getActualType=embind__requireFunction(getActualTypeSignature,getActualType);upcast&&=embind__requireFunction(upcastSignature,upcast);downcast&&=embind__requireFunction(downcastSignature,downcast);rawDestructor=embind__requireFunction(destructorSignature,rawDestructor);var legalFunctionName=makeLegalFunctionName(name);exposePublicSymbol(legalFunctionName,function(){throwUnboundTypeError(`Cannot construct ${name} due to unbound types`,[baseClassRawType])});whenDependentTypesAreResolved([rawType,rawPointerType,rawConstPointerType],baseClassRawType?[baseClassRawType]:[],base=>{base=base[0];var baseClass;var basePrototype;if(baseClassRawType){baseClass=base.registeredClass;basePrototype=baseClass.instancePrototype}else{basePrototype=ClassHandle.prototype}var constructor=createNamedFunction(name,function(...args){if(Object.getPrototypeOf(this)!==instancePrototype){throw new BindingError(`Use 'new' to construct ${name}`)}if(undefined===registeredClass.constructor_body){throw new BindingError(`${name} has no accessible constructor`)}var body=registeredClass.constructor_body[args.length];if(undefined===body){throw new BindingError(`Tried to invoke ctor of ${name} with invalid number of parameters (${args.length}) - expected (${Object.keys(registeredClass.constructor_body).toString()}) parameters instead!`)}return body.apply(this,args)});var instancePrototype=Object.create(basePrototype,{constructor:{value:constructor}});constructor.prototype=instancePrototype;var registeredClass=new RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast);if(registeredClass.baseClass){registeredClass.baseClass.__derivedClasses??=[];registeredClass.baseClass.__derivedClasses.push(registeredClass)}var referenceConverter=new RegisteredPointer(name,registeredClass,true,false,false);var pointerConverter=new RegisteredPointer(name+"*",registeredClass,false,false,false);var constPointerConverter=new RegisteredPointer(name+" const*",registeredClass,false,true,false);registeredPointers[rawType]={pointerType:pointerConverter,constPointerType:constPointerConverter};replacePublicSymbol(legalFunctionName,constructor);return[referenceConverter,pointerConverter,constPointerConverter]})};function usesDestructorStack(argTypes){for(var i=1;i{var array=[];for(var i=0;i>2])}return array};var getFunctionName=signature=>{signature=signature.trim();const argsIndex=signature.indexOf("(");if(argsIndex===-1)return signature;return signature.slice(0,argsIndex)};var __embind_register_class_class_function=(rawClassType,methodName,argCount,rawArgTypesAddr,invokerSignature,rawInvoker,fn,isAsync,isNonnullReturn)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);methodName=AsciiToString(methodName);methodName=getFunctionName(methodName);rawInvoker=embind__requireFunction(invokerSignature,rawInvoker,isAsync);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`${classType.name}.${methodName}`;function unboundTypesHandler(){throwUnboundTypeError(`Cannot call ${humanName} due to unbound types`,rawArgTypes)}if(methodName.startsWith("@@")){methodName=Symbol[methodName.substring(2)]}var proto=classType.registeredClass.constructor;if(undefined===proto[methodName]){unboundTypesHandler.argCount=argCount-1;proto[methodName]=unboundTypesHandler}else{ensureOverloadTable(proto,methodName,humanName);proto[methodName].overloadTable[argCount-1]=unboundTypesHandler}whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{var invokerArgsArray=[argTypes[0],null].concat(argTypes.slice(1));var func=craftInvokerFunction(humanName,invokerArgsArray,null,rawInvoker,fn,isAsync);if(undefined===proto[methodName].overloadTable){func.argCount=argCount-1;proto[methodName]=func}else{proto[methodName].overloadTable[argCount-1]=func}if(classType.registeredClass.__derivedClasses){for(const derivedClass of classType.registeredClass.__derivedClasses){if(!derivedClass.constructor.hasOwnProperty(methodName)){derivedClass.constructor[methodName]=func}}}return[]});return[]})};var __embind_register_class_constructor=(rawClassType,argCount,rawArgTypesAddr,invokerSignature,invoker,rawConstructor)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);invoker=embind__requireFunction(invokerSignature,invoker);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`constructor ${classType.name}`;if(undefined===classType.registeredClass.constructor_body){classType.registeredClass.constructor_body=[]}if(undefined!==classType.registeredClass.constructor_body[argCount-1]){throw new BindingError(`Cannot register multiple constructors with identical number of parameters (${argCount-1}) for class '${classType.name}'! Overload resolution is currently only performed using the parameter count, not actual type info!`)}classType.registeredClass.constructor_body[argCount-1]=()=>{throwUnboundTypeError(`Cannot construct ${classType.name} due to unbound types`,rawArgTypes)};whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{argTypes.splice(1,0,null);classType.registeredClass.constructor_body[argCount-1]=craftInvokerFunction(humanName,argTypes,null,invoker,rawConstructor);return[]});return[]})};var __embind_register_class_function=(rawClassType,methodName,argCount,rawArgTypesAddr,invokerSignature,rawInvoker,context,isPureVirtual,isAsync,isNonnullReturn)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);methodName=AsciiToString(methodName);methodName=getFunctionName(methodName);rawInvoker=embind__requireFunction(invokerSignature,rawInvoker,isAsync);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`${classType.name}.${methodName}`;if(methodName.startsWith("@@")){methodName=Symbol[methodName.substring(2)]}if(isPureVirtual){classType.registeredClass.pureVirtualFunctions.push(methodName)}function unboundTypesHandler(){throwUnboundTypeError(`Cannot call ${humanName} due to unbound types`,rawArgTypes)}var proto=classType.registeredClass.instancePrototype;var method=proto[methodName];if(undefined===method||undefined===method.overloadTable&&method.className!==classType.name&&method.argCount===argCount-2){unboundTypesHandler.argCount=argCount-2;unboundTypesHandler.className=classType.name;proto[methodName]=unboundTypesHandler}else{ensureOverloadTable(proto,methodName,humanName);proto[methodName].overloadTable[argCount-2]=unboundTypesHandler}whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{var memberFunction=craftInvokerFunction(humanName,argTypes,classType,rawInvoker,context,isAsync);if(undefined===proto[methodName].overloadTable){memberFunction.argCount=argCount-2;proto[methodName]=memberFunction}else{proto[methodName].overloadTable[argCount-2]=memberFunction}return[]});return[]})};var validateThis=(this_,classType,humanName)=>{if(!(this_ instanceof Object)){throwBindingError(`${humanName} with invalid "this": ${this_}`)}if(!(this_ instanceof classType.registeredClass.constructor)){throwBindingError(`${humanName} incompatible with "this" of type ${this_.constructor.name}`)}if(!this_.$$.ptr){throwBindingError(`cannot call emscripten binding method ${humanName} on deleted object`)}return upcastPointer(this_.$$.ptr,this_.$$.ptrType.registeredClass,classType.registeredClass)};var __embind_register_class_property=(classType,fieldName,getterReturnType,getterSignature,getter,getterContext,setterArgumentType,setterSignature,setter,setterContext)=>{fieldName=AsciiToString(fieldName);getter=embind__requireFunction(getterSignature,getter);whenDependentTypesAreResolved([],[classType],classType=>{classType=classType[0];var humanName=`${classType.name}.${fieldName}`;var desc={get(){throwUnboundTypeError(`Cannot access ${humanName} due to unbound types`,[getterReturnType,setterArgumentType])},enumerable:true,configurable:true};if(setter){desc.set=()=>throwUnboundTypeError(`Cannot access ${humanName} due to unbound types`,[getterReturnType,setterArgumentType])}else{desc.set=v=>throwBindingError(humanName+" is a read-only property")}Object.defineProperty(classType.registeredClass.instancePrototype,fieldName,desc);whenDependentTypesAreResolved([],setter?[getterReturnType,setterArgumentType]:[getterReturnType],types=>{var getterReturnType=types[0];var desc={get(){var ptr=validateThis(this,classType,humanName+" getter");return getterReturnType.fromWireType(getter(getterContext,ptr))},enumerable:true};if(setter){setter=embind__requireFunction(setterSignature,setter);var setterArgumentType=types[1];desc.set=function(v){var ptr=validateThis(this,classType,humanName+" setter");var destructors=[];setter(setterContext,ptr,setterArgumentType.toWireType(destructors,v));runDestructors(destructors)}}Object.defineProperty(classType.registeredClass.instancePrototype,fieldName,desc);return[]});return[]})};var __embind_register_constant=(name,type,value)=>{name=AsciiToString(name);whenDependentTypesAreResolved([],[type],type=>{type=type[0];Module[name]=type.fromWireType(value);return[]})};var __emval_decref=handle=>{if(handle>9&&0===--emval_handles[handle+1]){emval_handles[handle]=undefined;emval_freelist.push(handle)}};var EmValType={name:"emscripten::val",fromWireType:handle=>{var rv=Emval.toValue(handle);__emval_decref(handle);return rv},toWireType:(destructors,value)=>Emval.toHandle(value),readValueFromPointer:readPointer,destructorFunction:null};var __embind_register_emval=rawType=>registerType(rawType,EmValType);var enumReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?function(pointer){return this.fromWireType(HEAP8[pointer])}:function(pointer){return this.fromWireType(HEAPU8[pointer])};case 2:return signed?function(pointer){return this.fromWireType(HEAP16[pointer>>1])}:function(pointer){return this.fromWireType(HEAPU16[pointer>>1])};case 4:return signed?function(pointer){return this.fromWireType(HEAP32[pointer>>2])}:function(pointer){return this.fromWireType(HEAPU32[pointer>>2])};default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};function getEnumValueType(rawValueType){return rawValueType===0?"object":rawValueType===1?"number":"string"}var __embind_register_enum=(rawType,name,size,isSigned,rawValueType)=>{name=AsciiToString(name);const valueType=getEnumValueType(rawValueType);switch(valueType){case"object":{function ctor(){}ctor.values={};registerType(rawType,{name,constructor:ctor,valueType,fromWireType:function(c){return this.constructor.values[c]},toWireType:(destructors,c)=>c.value,readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,ctor);break}case"number":{var keysMap={};registerType(rawType,{name,keysMap,valueType,fromWireType:c=>c,toWireType:(destructors,c)=>c,readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,keysMap);delete Module[name].argCount;break}case"string":{var valuesMap={};var reverseMap={};var keysMap={};registerType(rawType,{name,valuesMap,reverseMap,keysMap,valueType,fromWireType:function(c){return this.reverseMap[c]},toWireType:function(destructors,c){return this.valuesMap[c]},readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,keysMap);delete Module[name].argCount;break}}};var __embind_register_enum_value=(rawEnumType,name,enumValue)=>{var enumType=requireRegisteredType(rawEnumType,"enum");name=AsciiToString(name);switch(enumType.valueType){case"object":{var Enum=enumType.constructor;var Value=Object.create(enumType.constructor.prototype,{value:{value:enumValue},constructor:{value:createNamedFunction(`${enumType.name}_${name}`,function(){})}});Enum.values[enumValue]=Value;Enum[name]=Value;break}case"number":{enumType.keysMap[name]=enumValue;break}case"string":{enumType.valuesMap[name]=enumValue;enumType.reverseMap[enumValue]=name;enumType.keysMap[name]=name;break}}};var floatReadValueFromPointer=(name,width)=>{switch(width){case 4:return function(pointer){return this.fromWireType(HEAPF32[pointer>>2])};case 8:return function(pointer){return this.fromWireType(HEAPF64[pointer>>3])};default:throw new TypeError(`invalid float width (${width}): ${name}`)}};var __embind_register_float=(rawType,name,size)=>{name=AsciiToString(name);registerType(rawType,{name,fromWireType:value=>value,toWireType:(destructors,value)=>value,readValueFromPointer:floatReadValueFromPointer(name,size),destructorFunction:null})};var __embind_register_function=(name,argCount,rawArgTypesAddr,signature,rawInvoker,fn,isAsync,isNonnullReturn)=>{var argTypes=heap32VectorToArray(argCount,rawArgTypesAddr);name=AsciiToString(name);name=getFunctionName(name);rawInvoker=embind__requireFunction(signature,rawInvoker,isAsync);exposePublicSymbol(name,function(){throwUnboundTypeError(`Cannot call ${name} due to unbound types`,argTypes)},argCount-1);whenDependentTypesAreResolved([],argTypes,argTypes=>{var invokerArgsArray=[argTypes[0],null].concat(argTypes.slice(1));replacePublicSymbol(name,craftInvokerFunction(name,invokerArgsArray,null,rawInvoker,fn,isAsync),argCount-1);return[]})};var __embind_register_integer=(primitiveType,name,size,minRange,maxRange)=>{name=AsciiToString(name);const isUnsignedType=minRange===0;let fromWireType=value=>value;if(isUnsignedType){var bitshift=32-8*size;fromWireType=value=>value<>>bitshift;maxRange=fromWireType(maxRange)}registerType(primitiveType,{name,fromWireType,toWireType:(destructors,value)=>value,readValueFromPointer:integerReadValueFromPointer(name,size,minRange!==0),destructorFunction:null})};var installIndexedIterator=(proto,sizeMethodName,getMethodName)=>{const makeIterator=(size,getValue)=>{let index=0;return{next(){if(index>=size){return{done:true}}const current=index;index++;const value=getValue(current);return{value,done:false}},[Symbol.iterator](){return this}}};if(!proto[Symbol.iterator]){proto[Symbol.iterator]=function(){const size=this[sizeMethodName]();return makeIterator(size,i=>this[getMethodName](i))}}};var __embind_register_iterable=(rawClassType,rawElementType,sizeMethodName,getMethodName)=>{sizeMethodName=AsciiToString(sizeMethodName);getMethodName=AsciiToString(getMethodName);whenDependentTypesAreResolved([],[rawClassType,rawElementType],types=>{const classType=types[0];installIndexedIterator(classType.registeredClass.instancePrototype,sizeMethodName,getMethodName);return[]})};var __embind_register_memory_view=(rawType,dataTypeIndex,name)=>{var typeMapping=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array,BigInt64Array,BigUint64Array];var TA=typeMapping[dataTypeIndex];function decodeMemoryView(handle){var size=HEAPU32[handle>>2];var data=HEAPU32[handle+4>>2];return new TA(HEAP8.buffer,data,size)}name=AsciiToString(name);registerType(rawType,{name,fromWireType:decodeMemoryView,readValueFromPointer:decodeMemoryView},{ignoreDuplicateRegistrations:true})};var EmValOptionalType=Object.assign({optional:true},EmValType);var __embind_register_optional=(rawOptionalType,rawType)=>{registerType(rawOptionalType,EmValOptionalType)};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var __embind_register_std_string=(rawType,name)=>{name=AsciiToString(name);var stdStringIsUTF8=true;registerType(rawType,{name,fromWireType(value){var length=HEAPU32[value>>2];var payload=value+4;var str;if(stdStringIsUTF8){str=UTF8ToString(payload,length,true)}else{str="";for(var i=0;i>2]=length;if(valueIsOfTypeString){if(stdStringIsUTF8){stringToUTF8(value,ptr,length+1)}else{for(var i=0;i255){_free(base);throwBindingError("String has UTF-16 code units that do not fit in 8 bits")}HEAPU8[ptr+i]=charCode}}}else{HEAPU8.set(value,ptr)}if(destructors!==null){destructors.push(_free,base)}return base},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var UTF16Decoder=globalThis.TextDecoder?new TextDecoder("utf-16le"):undefined;var UTF16ToString=(ptr,maxBytesToRead,ignoreNul)=>{var idx=ptr>>1;var endIdx=findStringEnd(HEAPU16,idx,maxBytesToRead/2,ignoreNul);if(endIdx-idx>16&&UTF16Decoder)return UTF16Decoder.decode(HEAPU16.subarray(idx,endIdx));var str="";for(var i=idx;i{maxBytesToWrite??=2147483647;if(maxBytesToWrite<2)return 0;maxBytesToWrite-=2;var startPtr=outPtr;var numCharsToWrite=maxBytesToWrite>1]=codeUnit;outPtr+=2}HEAP16[outPtr>>1]=0;return outPtr-startPtr};var lengthBytesUTF16=str=>str.length*2;var UTF32ToString=(ptr,maxBytesToRead,ignoreNul)=>{var str="";var startIdx=ptr>>2;for(var i=0;!(i>=maxBytesToRead/4);i++){var utf32=HEAPU32[startIdx+i];if(!utf32&&!ignoreNul)break;str+=String.fromCodePoint(utf32)}return str};var stringToUTF32=(str,outPtr,maxBytesToWrite)=>{maxBytesToWrite??=2147483647;if(maxBytesToWrite<4)return 0;var startPtr=outPtr;var endPtr=startPtr+maxBytesToWrite-4;for(var i=0;i65535){i++}HEAP32[outPtr>>2]=codePoint;outPtr+=4;if(outPtr+4>endPtr)break}HEAP32[outPtr>>2]=0;return outPtr-startPtr};var lengthBytesUTF32=str=>{var len=0;for(var i=0;i65535){i++}len+=4}return len};var __embind_register_std_wstring=(rawType,charSize,name)=>{name=AsciiToString(name);var decodeString,encodeString,lengthBytesUTF;if(charSize===2){decodeString=UTF16ToString;encodeString=stringToUTF16;lengthBytesUTF=lengthBytesUTF16}else{decodeString=UTF32ToString;encodeString=stringToUTF32;lengthBytesUTF=lengthBytesUTF32}registerType(rawType,{name,fromWireType:value=>{var length=HEAPU32[value>>2];var str=decodeString(value+4,length*charSize,true);_free(value);return str},toWireType:(destructors,value)=>{if(!(typeof value=="string")){throwBindingError(`Cannot pass non-string to C++ string type ${name}`)}var length=lengthBytesUTF(value);var ptr=_malloc(4+length+charSize);HEAPU32[ptr>>2]=length/charSize;encodeString(value,ptr+4,length+charSize);if(destructors!==null){destructors.push(_free,ptr)}return ptr},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var __embind_register_value_object=(rawType,name,constructorSignature,rawConstructor,destructorSignature,rawDestructor)=>{structRegistrations[rawType]={name:AsciiToString(name),rawConstructor:embind__requireFunction(constructorSignature,rawConstructor),rawDestructor:embind__requireFunction(destructorSignature,rawDestructor),fields:[]}};var __embind_register_value_object_field=(structType,fieldName,getterReturnType,getterSignature,getter,getterContext,setterArgumentType,setterSignature,setter,setterContext)=>{structRegistrations[structType].fields.push({fieldName:AsciiToString(fieldName),getterReturnType,getter:embind__requireFunction(getterSignature,getter),getterContext,setterArgumentType,setter:embind__requireFunction(setterSignature,setter),setterContext})};var __embind_register_void=(rawType,name)=>{name=AsciiToString(name);registerType(rawType,{isVoid:true,name,fromWireType:()=>undefined,toWireType:(destructors,o)=>undefined})};var runtimeKeepaliveCounter=0;var __emscripten_runtime_keepalive_clear=()=>{noExitRuntime=false;runtimeKeepaliveCounter=0};var emval_methodCallers=[];var emval_addMethodCaller=caller=>{var id=emval_methodCallers.length;emval_methodCallers.push(caller);return id};var emval_lookupTypes=(argCount,argTypes)=>{var a=new Array(argCount);for(var i=0;i>2],`parameter ${i}`)}return a};var emval_returnValue=(toReturnWire,destructorsRef,handle)=>{var destructors=[];var result=toReturnWire(destructors,handle);if(destructors.length){HEAPU32[destructorsRef>>2]=Emval.toHandle(destructors)}return result};var emval_symbols={};var getStringOrSymbol=address=>{var symbol=emval_symbols[address];if(symbol===undefined){return AsciiToString(address)}return symbol};var __emval_create_invoker=(argCount,argTypesPtr,kind)=>{var GenericWireTypeSize=8;var[retType,...argTypes]=emval_lookupTypes(argCount,argTypesPtr);var toReturnWire=retType.toWireType.bind(retType);var argFromPtr=argTypes.map(type=>type.readValueFromPointer.bind(type));argCount--;var argN=new Array(argCount);var invokerFunction=(handle,methodName,destructorsRef,args)=>{var offset=0;for(var i=0;it.name)}) => ${retType.name}>`;return emval_addMethodCaller(createNamedFunction(functionName,invokerFunction))};var __emval_invoke=(caller,handle,methodName,destructorsRef,args)=>emval_methodCallers[caller](handle,methodName,destructorsRef,args);var __emval_new_cstring=v=>Emval.toHandle(getStringOrSymbol(v));var __emval_new_object=()=>Emval.toHandle({});var __emval_run_destructors=handle=>{var destructors=Emval.toValue(handle);runDestructors(destructors);__emval_decref(handle)};var __emval_set_property=(handle,key,value)=>{handle=Emval.toValue(handle);key=Emval.toValue(key);value=Emval.toValue(value);handle[key]=value};var timers={};var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;var maybeExit=()=>{if(!keepRuntimeAlive()){try{_exit(EXITSTATUS)}catch(e){handleException(e)}}};var callUserCallback=func=>{if(ABORT){return}try{return func()}catch(e){handleException(e)}finally{maybeExit()}};var _emscripten_get_now=()=>performance.now();var __setitimer_js=(which,timeout_ms)=>{if(timers[which]){clearTimeout(timers[which].id);delete timers[which]}if(!timeout_ms)return 0;var id=setTimeout(()=>{delete timers[which];callUserCallback(()=>__emscripten_timeout(which,_emscripten_get_now()))},timeout_ms);timers[which]={id,timeout_ms};return 0};var _emscripten_date_now=()=>Date.now();var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var printCharBuffers=[null,[],[]];var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(curr===0||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer));buffer.length=0}else{buffer.push(curr)}};var _fd_write=(fd,iov,iovcnt,pnum)=>{var num=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j>2]=num;return 0};init_ClassHandle();init_RegisteredPointer();{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}var ___getTypeName,_free,_malloc,__emscripten_timeout,memory,__indirect_function_table,wasmMemory,wasmTable;function assignWasmExports(wasmExports){___getTypeName=wasmExports["Q"];_free=Module["_free"]=wasmExports["R"];_malloc=Module["_malloc"]=wasmExports["S"];__emscripten_timeout=wasmExports["U"];memory=wasmMemory=wasmExports["O"];__indirect_function_table=wasmTable=wasmExports["T"]}var wasmImports={J:__abort_js,x:__embind_create_inheriting_constructor,s:__embind_finalize_value_object,z:__embind_register_bigint,A:__embind_register_bool,b:__embind_register_class,p:__embind_register_class_class_function,e:__embind_register_class_constructor,a:__embind_register_class_function,d:__embind_register_class_property,K:__embind_register_constant,M:__embind_register_emval,l:__embind_register_enum,f:__embind_register_enum_value,y:__embind_register_float,g:__embind_register_function,o:__embind_register_integer,q:__embind_register_iterable,h:__embind_register_memory_view,r:__embind_register_optional,N:__embind_register_std_string,u:__embind_register_std_wstring,t:__embind_register_value_object,m:__embind_register_value_object_field,B:__embind_register_void,D:__emscripten_runtime_keepalive_clear,k:__emval_create_invoker,n:__emval_decref,j:__emval_invoke,v:__emval_new_cstring,L:__emval_new_object,i:__emval_run_destructors,w:__emval_set_property,E:__setitimer_js,I:_emscripten_date_now,c:_emscripten_get_now,F:_emscripten_resize_heap,G:_exit,H:_fd_write,C:_proc_exit};function run(){preRun();function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} ;return moduleRtn}})();if(typeof exports==="object"&&typeof module==="object"){module.exports=PHYSX;module.exports.default=PHYSX}else if(typeof define==="function"&&define["amd"])define([],()=>PHYSX); diff --git a/e2e/.dev/physx.release.simd.wasm b/e2e/.dev/physx.release.simd.wasm index 91e5bab34b..f52b48110a 100755 Binary files a/e2e/.dev/physx.release.simd.wasm and b/e2e/.dev/physx.release.simd.wasm differ diff --git a/e2e/.dev/physx.release.wasm b/e2e/.dev/physx.release.wasm index d9e03eee6a..3efb26a288 100755 Binary files a/e2e/.dev/physx.release.wasm and b/e2e/.dev/physx.release.wasm differ diff --git a/e2e/.dev/public/spineboy.atlas b/e2e/.dev/public/spineboy.atlas new file mode 100644 index 0000000000..eca542b711 --- /dev/null +++ b/e2e/.dev/public/spineboy.atlas @@ -0,0 +1,94 @@ +spineboy.png + size: 1024, 256 + filter: Linear, Linear + scale: 0.5 +crosshair + bounds: 352, 7, 45, 45 +eye-indifferent + bounds: 862, 105, 47, 45 +eye-surprised + bounds: 505, 79, 47, 45 +front-bracer + bounds: 826, 66, 29, 40 +front-fist-closed + bounds: 786, 65, 38, 41 +front-fist-open + bounds: 710, 51, 43, 44 + rotate: 90 +front-foot + bounds: 210, 6, 63, 35 +front-shin + bounds: 665, 128, 41, 92 + rotate: 90 +front-thigh + bounds: 2, 2, 23, 56 + rotate: 90 +front-upper-arm + bounds: 250, 205, 23, 49 +goggles + bounds: 665, 171, 131, 83 +gun + bounds: 798, 152, 105, 102 +head + bounds: 2, 27, 136, 149 +hoverboard-board + bounds: 2, 178, 246, 76 +hoverboard-thruster + bounds: 722, 96, 30, 32 + rotate: 90 +hoverglow-small + bounds: 275, 81, 137, 38 +mouth-grind + bounds: 614, 97, 47, 30 +mouth-oooo + bounds: 612, 65, 47, 30 +mouth-smile + bounds: 661, 64, 47, 30 +muzzle-glow + bounds: 382, 54, 25, 25 +muzzle-ring + bounds: 275, 54, 25, 105 + rotate: 90 +muzzle01 + bounds: 911, 95, 67, 40 + rotate: 90 +muzzle02 + bounds: 792, 108, 68, 42 +muzzle03 + bounds: 956, 171, 83, 53 + rotate: 90 +muzzle04 + bounds: 275, 7, 75, 45 +muzzle05 + bounds: 140, 3, 68, 38 +neck + bounds: 250, 182, 18, 21 +portal-bg + bounds: 140, 43, 133, 133 +portal-flare1 + bounds: 554, 65, 56, 30 +portal-flare2 + bounds: 759, 112, 57, 31 + rotate: 90 +portal-flare3 + bounds: 554, 97, 58, 30 +portal-shade + bounds: 275, 121, 133, 133 +portal-streaks1 + bounds: 410, 126, 126, 128 +portal-streaks2 + bounds: 538, 129, 125, 125 +rear-bracer + bounds: 857, 67, 28, 36 +rear-foot + bounds: 663, 96, 57, 30 +rear-shin + bounds: 414, 86, 38, 89 + rotate: 90 +rear-thigh + bounds: 756, 63, 28, 47 +rear-upper-arm + bounds: 60, 5, 20, 44 + rotate: 90 +torso + bounds: 905, 164, 49, 90 diff --git a/e2e/.dev/public/spineboy.json b/e2e/.dev/public/spineboy.json new file mode 100644 index 0000000000..c0eb0ae927 --- /dev/null +++ b/e2e/.dev/public/spineboy.json @@ -0,0 +1,8723 @@ +{ + "skeleton": { + "hash": "dr3Kr/vMgPA", + "spine": "4.2.22", + "x": -188.63, + "y": -7.94, + "width": 418.45, + "height": 686.2, + "images": "./images/", + "audio": "" + }, + "bones": [ + { "name": "root", "rotation": 0.05 }, + { "name": "hip", "parent": "root", "y": 247.27 }, + { "name": "crosshair", "parent": "root", "x": 302.83, "y": 569.45, "color": "ff3f00ff", "icon": "circle" }, + { + "name": "aim-constraint-target", + "parent": "hip", + "length": 26.24, + "rotation": 19.61, + "x": 1.02, + "y": 5.62, + "color": "abe323ff" + }, + { "name": "rear-foot-target", "parent": "root", "x": 61.91, "y": 0.42, "color": "ff3f00ff", "icon": "ik" }, + { "name": "rear-leg-target", "parent": "rear-foot-target", "x": -33.91, "y": 37.34, "color": "ff3f00ff", "icon": "ik" }, + { + "name": "rear-thigh", + "parent": "hip", + "length": 85.72, + "rotation": -72.54, + "x": 8.91, + "y": -5.63, + "color": "ff000dff" + }, + { + "name": "rear-shin", + "parent": "rear-thigh", + "length": 121.88, + "rotation": -19.83, + "x": 86.1, + "y": -1.33, + "color": "ff000dff" + }, + { + "name": "rear-foot", + "parent": "rear-shin", + "length": 51.58, + "rotation": 45.78, + "x": 121.46, + "y": -0.76, + "color": "ff000dff" + }, + { + "name": "back-foot-tip", + "parent": "rear-foot", + "length": 50.3, + "rotation": -0.85, + "x": 51.17, + "y": 0.24, + "inherit": "noRotationOrReflection", + "color": "ff000dff" + }, + { "name": "board-ik", "parent": "root", "x": -131.78, "y": 69.09, "color": "4c56ffff", "icon": "arrows" }, + { "name": "clipping", "parent": "root" }, + { + "name": "hoverboard-controller", + "parent": "root", + "rotation": -0.28, + "x": -329.69, + "y": 69.82, + "color": "ff0004ff", + "icon": "arrowsB" + }, + { "name": "exhaust1", "parent": "hoverboard-controller", "rotation": 3.02, "x": -249.68, "y": 53.39 }, + { "name": "exhaust2", "parent": "hoverboard-controller", "rotation": 26.34, "x": -191.6, "y": -22.92 }, + { + "name": "exhaust3", + "parent": "hoverboard-controller", + "rotation": -12.34, + "x": -236.03, + "y": 80.54, + "scaleX": 0.7847, + "scaleY": 0.7847 + }, + { "name": "portal-root", "parent": "root", "x": 12.9, "y": 328.54, "scaleX": 2.0334, "scaleY": 2.0334 }, + { "name": "flare1", "parent": "portal-root", "x": -6.34, "y": -161.57, "icon": "particles" }, + { "name": "flare10", "parent": "portal-root", "x": -6.34, "y": -161.57, "icon": "particles" }, + { "name": "flare2", "parent": "portal-root", "x": -6.34, "y": -161.57, "icon": "particles" }, + { "name": "flare3", "parent": "portal-root", "x": -6.34, "y": -161.57, "icon": "particles" }, + { "name": "flare4", "parent": "portal-root", "x": -6.34, "y": -161.57, "icon": "particles" }, + { "name": "flare5", "parent": "portal-root", "x": -6.34, "y": -161.57, "icon": "particles" }, + { "name": "flare6", "parent": "portal-root", "x": -6.34, "y": -161.57, "icon": "particles" }, + { "name": "flare7", "parent": "portal-root", "x": -6.34, "y": -161.57, "icon": "particles" }, + { "name": "flare8", "parent": "portal-root", "x": -6.34, "y": -161.57, "icon": "particles" }, + { "name": "flare9", "parent": "portal-root", "x": -6.34, "y": -161.57, "icon": "particles" }, + { + "name": "torso", + "parent": "hip", + "length": 42.52, + "rotation": 103.82, + "x": -1.62, + "y": 4.9, + "color": "e0da19ff" + }, + { "name": "torso2", "parent": "torso", "length": 42.52, "x": 42.52, "color": "e0da19ff" }, + { "name": "torso3", "parent": "torso2", "length": 42.52, "x": 42.52, "color": "e0da19ff" }, + { "name": "front-shoulder", "parent": "torso3", "rotation": 255.89, "x": 18.72, "y": 19.33, "color": "00ff04ff" }, + { "name": "front-upper-arm", "parent": "front-shoulder", "length": 69.45, "rotation": -87.51, "color": "00ff04ff" }, + { + "name": "front-bracer", + "parent": "front-upper-arm", + "length": 40.57, + "rotation": 18.3, + "x": 68.8, + "y": -0.68, + "color": "00ff04ff" + }, + { + "name": "front-fist", + "parent": "front-bracer", + "length": 65.39, + "rotation": 12.43, + "x": 40.57, + "y": 0.2, + "color": "00ff04ff" + }, + { "name": "front-foot-target", "parent": "root", "x": -13.53, "y": 0.04, "color": "ff3f00ff", "icon": "ik" }, + { "name": "front-leg-target", "parent": "front-foot-target", "x": -28.4, "y": 29.06, "color": "ff3f00ff", "icon": "ik" }, + { + "name": "front-thigh", + "parent": "hip", + "length": 74.81, + "rotation": -95.51, + "x": -17.46, + "y": -11.64, + "color": "00ff04ff" + }, + { + "name": "front-shin", + "parent": "front-thigh", + "length": 128.77, + "rotation": -2.21, + "x": 78.69, + "y": 1.6, + "color": "00ff04ff" + }, + { + "name": "front-foot", + "parent": "front-shin", + "length": 41.01, + "rotation": 51.27, + "x": 128.76, + "y": -0.34, + "color": "00ff04ff" + }, + { + "name": "front-foot-tip", + "parent": "front-foot", + "length": 56.03, + "rotation": -1.68, + "x": 41.42, + "y": -0.09, + "inherit": "noRotationOrReflection", + "color": "00ff04ff" + }, + { "name": "back-shoulder", "parent": "torso3", "rotation": -104.11, "x": 7.32, "y": -19.22, "color": "ff000dff" }, + { "name": "rear-upper-arm", "parent": "back-shoulder", "length": 51.94, "rotation": -65.45, "color": "ff000dff" }, + { "name": "rear-bracer", "parent": "rear-upper-arm", "length": 34.56, "rotation": 23.15, "x": 51.36, "color": "ff000dff" }, + { + "name": "gun", + "parent": "rear-bracer", + "length": 43.11, + "rotation": -5.43, + "x": 34.42, + "y": -0.45, + "color": "ff000dff" + }, + { "name": "gun-tip", "parent": "gun", "rotation": 7.1, "x": 200.78, "y": 52.5, "color": "ff0000ff" }, + { + "name": "neck", + "parent": "torso3", + "length": 25.45, + "rotation": -31.54, + "x": 42.46, + "y": -0.31, + "color": "e0da19ff" + }, + { + "name": "head", + "parent": "neck", + "length": 131.79, + "rotation": 26.1, + "x": 27.66, + "y": -0.26, + "color": "e0da19ff" + }, + { + "name": "hair1", + "parent": "head", + "length": 47.23, + "rotation": -49.1, + "x": 149.83, + "y": -59.77, + "color": "e0da19ff" + }, + { + "name": "hair2", + "parent": "hair1", + "length": 55.57, + "rotation": 50.42, + "x": 47.23, + "y": 0.19, + "color": "e0da19ff" + }, + { + "name": "hair3", + "parent": "head", + "length": 62.22, + "rotation": -32.17, + "x": 164.14, + "y": 3.68, + "color": "e0da19ff" + }, + { + "name": "hair4", + "parent": "hair3", + "length": 80.28, + "rotation": 83.71, + "x": 62.22, + "y": -0.04, + "color": "e0da19ff" + }, + { "name": "hoverboard-thruster-front", "parent": "hoverboard-controller", "rotation": -29.2, "x": 95.77, "y": -2.99, "inherit": "noRotationOrReflection" }, + { "name": "hoverboard-thruster-rear", "parent": "hoverboard-controller", "rotation": -29.2, "x": -76.47, "y": -4.88, "inherit": "noRotationOrReflection" }, + { "name": "hoverglow-front", "parent": "hoverboard-thruster-front", "rotation": 0.17, "x": -1.78, "y": -37.79 }, + { "name": "hoverglow-rear", "parent": "hoverboard-thruster-rear", "rotation": 0.17, "x": 1.06, "y": -35.66 }, + { + "name": "muzzle", + "parent": "rear-bracer", + "rotation": 3.06, + "x": 242.34, + "y": 34.26, + "color": "ffb900ff", + "icon": "muzzleFlash" + }, + { "name": "muzzle-ring", "parent": "muzzle", "color": "ffb900ff" }, + { "name": "muzzle-ring2", "parent": "muzzle", "color": "ffb900ff" }, + { "name": "muzzle-ring3", "parent": "muzzle", "color": "ffb900ff" }, + { "name": "muzzle-ring4", "parent": "muzzle", "color": "ffb900ff" }, + { "name": "portal", "parent": "portal-root" }, + { "name": "portal-shade", "parent": "portal-root" }, + { "name": "portal-streaks1", "parent": "portal-root" }, + { "name": "portal-streaks2", "parent": "portal-root" }, + { "name": "side-glow1", "parent": "hoverboard-controller", "x": -110.56, "y": 2.62, "color": "000effff" }, + { + "name": "side-glow2", + "parent": "hoverboard-controller", + "x": -110.56, + "y": 2.62, + "scaleX": 0.738, + "scaleY": 0.738, + "color": "000effff" + }, + { "name": "head-control", "parent": "head", "x": 110.21, "color": "00a220ff", "icon": "arrows" } + ], + "slots": [ + { "name": "portal-bg", "bone": "portal" }, + { "name": "portal-shade", "bone": "portal-shade" }, + { "name": "portal-streaks2", "bone": "portal-streaks2", "blend": "additive" }, + { "name": "portal-streaks1", "bone": "portal-streaks1", "blend": "additive" }, + { "name": "portal-flare8", "bone": "flare8", "color": "c3cbffff", "blend": "additive" }, + { "name": "portal-flare9", "bone": "flare9", "color": "c3cbffff", "blend": "additive" }, + { "name": "portal-flare10", "bone": "flare10", "color": "c3cbffff", "blend": "additive" }, + { "name": "clipping", "bone": "clipping" }, + { "name": "exhaust3", "bone": "exhaust3", "color": "5eb4ffff", "blend": "additive" }, + { "name": "hoverboard-thruster-rear", "bone": "hoverboard-thruster-rear" }, + { "name": "hoverboard-thruster-front", "bone": "hoverboard-thruster-front" }, + { "name": "hoverboard-board", "bone": "hoverboard-controller" }, + { "name": "side-glow1", "bone": "side-glow1", "color": "ff8686ff", "blend": "additive" }, + { "name": "side-glow3", "bone": "side-glow1", "color": "ff8686ff", "blend": "additive" }, + { "name": "side-glow2", "bone": "side-glow2", "color": "ff8686ff", "blend": "additive" }, + { "name": "hoverglow-front", "bone": "hoverglow-front", "color": "5eb4ffff", "blend": "additive" }, + { "name": "hoverglow-rear", "bone": "hoverglow-rear", "color": "5eb4ffff", "blend": "additive" }, + { "name": "exhaust1", "bone": "exhaust2", "color": "5eb4ffff", "blend": "additive" }, + { "name": "exhaust2", "bone": "exhaust1", "color": "5eb4ffff", "blend": "additive" }, + { "name": "rear-upper-arm", "bone": "rear-upper-arm", "attachment": "rear-upper-arm" }, + { "name": "rear-bracer", "bone": "rear-bracer", "attachment": "rear-bracer" }, + { "name": "gun", "bone": "gun", "attachment": "gun" }, + { "name": "rear-foot", "bone": "rear-foot", "attachment": "rear-foot" }, + { "name": "rear-thigh", "bone": "rear-thigh", "attachment": "rear-thigh" }, + { "name": "rear-shin", "bone": "rear-shin", "attachment": "rear-shin" }, + { "name": "neck", "bone": "neck", "attachment": "neck" }, + { "name": "torso", "bone": "torso", "attachment": "torso" }, + { "name": "front-upper-arm", "bone": "front-upper-arm", "attachment": "front-upper-arm" }, + { "name": "head", "bone": "head", "attachment": "head" }, + { "name": "eye", "bone": "head", "attachment": "eye-indifferent" }, + { "name": "front-thigh", "bone": "front-thigh", "attachment": "front-thigh" }, + { "name": "front-foot", "bone": "front-foot", "attachment": "front-foot" }, + { "name": "front-shin", "bone": "front-shin", "attachment": "front-shin" }, + { "name": "mouth", "bone": "head", "attachment": "mouth-smile" }, + { "name": "goggles", "bone": "head", "attachment": "goggles" }, + { "name": "front-bracer", "bone": "front-bracer", "attachment": "front-bracer" }, + { "name": "front-fist", "bone": "front-fist", "attachment": "front-fist-closed" }, + { "name": "muzzle", "bone": "muzzle" }, + { "name": "head-bb", "bone": "head" }, + { "name": "portal-flare1", "bone": "flare1", "color": "c3cbffff", "blend": "additive" }, + { "name": "portal-flare2", "bone": "flare2", "color": "c3cbffff", "blend": "additive" }, + { "name": "portal-flare3", "bone": "flare3", "color": "c3cbffff", "blend": "additive" }, + { "name": "portal-flare4", "bone": "flare4", "color": "c3cbffff", "blend": "additive" }, + { "name": "portal-flare5", "bone": "flare5", "color": "c3cbffff", "blend": "additive" }, + { "name": "portal-flare6", "bone": "flare6", "color": "c3cbffff", "blend": "additive" }, + { "name": "portal-flare7", "bone": "flare7", "color": "c3cbffff", "blend": "additive" }, + { "name": "crosshair", "bone": "crosshair" }, + { "name": "muzzle-glow", "bone": "gun-tip", "color": "ffffff00", "blend": "additive" }, + { "name": "muzzle-ring", "bone": "muzzle-ring", "color": "d8baffff", "blend": "additive" }, + { "name": "muzzle-ring2", "bone": "muzzle-ring2", "color": "d8baffff", "blend": "additive" }, + { "name": "muzzle-ring3", "bone": "muzzle-ring3", "color": "d8baffff", "blend": "additive" }, + { "name": "muzzle-ring4", "bone": "muzzle-ring4", "color": "d8baffff", "blend": "additive" } + ], + "ik": [ + { + "name": "aim-ik", + "order": 13, + "bones": [ "rear-upper-arm" ], + "target": "crosshair", + "mix": 0 + }, + { + "name": "aim-torso-ik", + "order": 8, + "bones": [ "aim-constraint-target" ], + "target": "crosshair" + }, + { + "name": "board-ik", + "order": 1, + "bones": [ "hoverboard-controller" ], + "target": "board-ik" + }, + { + "name": "front-foot-ik", + "order": 6, + "bones": [ "front-foot" ], + "target": "front-foot-target" + }, + { + "name": "front-leg-ik", + "order": 4, + "bones": [ "front-thigh", "front-shin" ], + "target": "front-leg-target", + "bendPositive": false + }, + { + "name": "rear-foot-ik", + "order": 7, + "bones": [ "rear-foot" ], + "target": "rear-foot-target" + }, + { + "name": "rear-leg-ik", + "order": 5, + "bones": [ "rear-thigh", "rear-shin" ], + "target": "rear-leg-target", + "bendPositive": false + } + ], + "transform": [ + { + "name": "aim-front-arm-transform", + "order": 11, + "bones": [ "front-upper-arm" ], + "target": "aim-constraint-target", + "rotation": -180, + "mixRotate": 0, + "mixX": 0, + "mixScaleX": 0, + "mixShearY": 0 + }, + { + "name": "aim-head-transform", + "order": 10, + "bones": [ "head" ], + "target": "aim-constraint-target", + "rotation": 84.3, + "mixRotate": 0, + "mixX": 0, + "mixScaleX": 0, + "mixShearY": 0 + }, + { + "name": "aim-rear-arm-transform", + "order": 12, + "bones": [ "rear-upper-arm" ], + "target": "aim-constraint-target", + "x": 57.7, + "y": 56.4, + "mixRotate": 0, + "mixX": 0, + "mixScaleX": 0, + "mixShearY": 0 + }, + { + "name": "aim-torso-transform", + "order": 9, + "bones": [ "torso" ], + "target": "aim-constraint-target", + "rotation": 69.5, + "shearY": -36, + "mixRotate": 0, + "mixX": 0, + "mixScaleX": 0, + "mixShearY": 0 + }, + { + "name": "front-foot-board-transform", + "order": 2, + "bones": [ "front-foot-target" ], + "target": "hoverboard-controller", + "x": -69.8, + "y": 20.7, + "mixRotate": 0, + "mixX": 0, + "mixScaleX": 0, + "mixShearY": 0 + }, + { + "name": "rear-foot-board-transform", + "order": 3, + "bones": [ "rear-foot-target" ], + "target": "hoverboard-controller", + "x": 86.6, + "y": 21.3, + "mixRotate": 0, + "mixX": 0, + "mixScaleX": 0, + "mixShearY": 0 + }, + { + "name": "shoulder", + "bones": [ "back-shoulder" ], + "target": "front-shoulder", + "x": 40.17, + "y": -1.66, + "mixRotate": 0, + "mixX": -1, + "mixScaleX": 0, + "mixShearY": 0 + }, + { + "name": "toes-board", + "order": 14, + "bones": [ "front-foot-tip", "back-foot-tip" ], + "target": "hoverboard-controller", + "mixRotate": 0, + "mixX": 0, + "mixScaleX": 0, + "mixShearY": 0 + } + ], + "skins": [ + { + "name": "default", + "attachments": { + "clipping": { + "clipping": { + "type": "clipping", + "end": "head-bb", + "vertexCount": 9, + "vertices": [ 66.76, 509.48, 19.98, 434.54, 5.34, 336.28, 22.19, 247.93, 77.98, 159.54, 182.21, -97.56, 1452.26, -99.8, 1454.33, 843.61, 166.57, 841.02 ], + "color": "ce3a3aff" + } + }, + "crosshair": { + "crosshair": { "width": 89, "height": 89 } + }, + "exhaust1": { + "hoverglow-small": { "scaleX": 0.4629, "scaleY": 0.8129, "rotation": -83.07, "width": 274, "height": 75 } + }, + "exhaust2": { + "hoverglow-small": { + "x": 0.01, + "y": -0.76, + "scaleX": 0.4208, + "scaleY": 0.8403, + "rotation": -89.25, + "width": 274, + "height": 75 + } + }, + "exhaust3": { + "hoverglow-small": { "scaleX": 0.4629, "scaleY": 0.8129, "rotation": -83.07, "width": 274, "height": 75 } + }, + "eye": { + "eye-indifferent": { + "type": "mesh", + "uvs": [ 1, 1, 0, 1, 0, 0, 1, 0 ], + "triangles": [ 1, 3, 0, 1, 2, 3 ], + "vertices": [ 2, 66, -36.8, -91.35, 0.3, 46, 73.41, -91.35, 0.7, 2, 66, -87.05, -13.11, 0.70968, 46, 23.16, -13.11, 0.29032, 2, 66, -12.18, 34.99, 0.82818, 46, 98.03, 34.99, 0.17182, 2, 66, 38.07, -43.25, 0.59781, 46, 148.28, -43.25, 0.40219 ], + "hull": 4, + "edges": [ 0, 2, 2, 4, 4, 6, 0, 6 ], + "width": 93, + "height": 89 + }, + "eye-surprised": { + "type": "mesh", + "uvs": [ 1, 1, 0, 1, 0, 0, 1, 0 ], + "triangles": [ 1, 2, 3, 1, 3, 0 ], + "vertices": [ 2, 66, -46.74, -89.7, 0.3, 46, 63.47, -89.7, 0.7, 2, 66, -77.58, -1.97, 0.71, 46, 32.63, -1.97, 0.29, 2, 66, 6.38, 27.55, 0.83, 46, 116.59, 27.55, 0.17, 2, 66, 37.22, -60.19, 0.6, 46, 147.44, -60.19, 0.4 ], + "hull": 4, + "edges": [ 0, 2, 2, 4, 4, 6, 0, 6 ], + "width": 93, + "height": 89 + } + }, + "front-bracer": { + "front-bracer": { "x": 12.03, "y": -1.68, "rotation": 79.6, "width": 58, "height": 80 } + }, + "front-fist": { + "front-fist-closed": { "x": 35.5, "y": 6, "rotation": 67.16, "width": 75, "height": 82 }, + "front-fist-open": { "x": 39.57, "y": 7.76, "rotation": 67.16, "width": 86, "height": 87 } + }, + "front-foot": { + "front-foot": { + "type": "mesh", + "uvs": [ 0.59417, 0.23422, 0.62257, 0.30336, 0.6501, 0.37036, 0.67637, 0.38404, 0.72068, 0.4071, 0.76264, 0.42894, 1, 0.70375, 1, 1, 0.65517, 1, 0.46923, 0.99999, 0, 1, 0, 0.39197, 0.17846, 0, 0.49796, 0 ], + "triangles": [ 8, 9, 3, 4, 8, 3, 5, 8, 4, 6, 8, 5, 8, 6, 7, 11, 1, 10, 0, 12, 13, 0, 11, 12, 0, 1, 11, 9, 2, 3, 1, 2, 10, 9, 10, 2 ], + "vertices": [ 2, 38, 18.17, 41.57, 0.7896, 39, 12.46, 46.05, 0.2104, 2, 38, 24.08, 40.76, 0.71228, 39, 16.12, 41.34, 0.28772, 2, 38, 29.81, 39.98, 0.55344, 39, 19.67, 36.78, 0.44656, 2, 38, 32.81, 41.67, 0.38554, 39, 23, 35.89, 0.61446, 2, 38, 37.86, 44.52, 0.25567, 39, 28.61, 34.4, 0.74433, 2, 38, 42.65, 47.22, 0.17384, 39, 33.92, 32.99, 0.82616, 1, 39, 64.15, 14.56, 1, 1, 39, 64.51, -5.87, 1, 1, 39, 21.08, -6.64, 1, 2, 38, 44.67, -6.77, 0.5684, 39, -2.34, -6.97, 0.4316, 1, 38, 3.1, -48.81, 1, 1, 38, -26.73, -19.31, 1, 1, 38, -30.15, 15.69, 1, 1, 38, -1.84, 44.32, 1 ], + "hull": 14, + "edges": [ 14, 16, 16, 18, 18, 20, 4, 18, 20, 22, 24, 26, 22, 24, 12, 14, 10, 12, 2, 4, 2, 20, 4, 6, 6, 16, 2, 0, 0, 26, 6, 8, 8, 10 ], + "width": 126, + "height": 69 + } + }, + "front-shin": { + "front-shin": { + "type": "mesh", + "uvs": [ 0.90031, 0.05785, 1, 0.12828, 1, 0.21619, 0.9025, 0.31002, 0.78736, 0.35684, 0.78081, 0.39874, 0.77215, 0.45415, 0.77098, 0.51572, 0.84094, 0.63751, 0.93095, 0.7491, 0.95531, 0.7793, 0.78126, 0.87679, 0.5613, 1, 0.2687, 1, 0, 1, 0.00279, 0.96112, 0.01358, 0.81038, 0.02822, 0.60605, 0.08324, 0.45142, 0.18908, 0.31882, 0.29577, 0.2398, 0.30236, 0.14941, 0.37875, 0.05902, 0.53284, 0, 0.70538, 0, 0.41094, 0.71968, 0.40743, 0.54751, 0.41094, 0.4536, 0.4724, 0.35186, 0.33367, 0.27829, 0.50226, 0.31664, 0.65328, 0.67507, 0.60762, 0.52716, 0.6006, 0.45125, 0.62747, 0.37543, 0.6573, 0.3385, 0.27843, 0.32924, 0.18967, 0.45203, 0.16509, 0.58586, 0.18265, 0.7682, 0.50532, 0.24634, 0.59473, 0.17967, 0.60161, 0.10611, 0.51392, 0.04327, 0.72198, 0.28849, 0.82343, 0.20266, 0.86814, 0.11377, 0.79592, 0.04634, 0.44858, 0.15515, 0.25466, 0.96219, 0.53169, 0.9448, 0.7531, 0.8324 ], + "triangles": [ 24, 0, 47, 43, 23, 24, 47, 43, 24, 43, 22, 23, 42, 43, 47, 46, 47, 0, 42, 47, 46, 46, 0, 1, 48, 22, 43, 48, 43, 42, 21, 22, 48, 41, 48, 42, 45, 42, 46, 41, 42, 45, 46, 1, 2, 45, 46, 2, 40, 48, 41, 48, 20, 21, 29, 48, 40, 29, 20, 48, 44, 41, 45, 40, 41, 44, 3, 45, 2, 44, 45, 3, 30, 29, 40, 35, 30, 40, 36, 19, 20, 36, 20, 29, 44, 35, 40, 28, 29, 30, 4, 44, 3, 35, 44, 4, 34, 30, 35, 5, 35, 4, 34, 28, 30, 33, 28, 34, 37, 19, 36, 18, 19, 37, 27, 29, 28, 27, 28, 33, 36, 29, 27, 37, 36, 27, 5, 34, 35, 6, 34, 5, 33, 34, 6, 6, 32, 33, 7, 32, 6, 26, 37, 27, 38, 18, 37, 38, 37, 26, 17, 18, 38, 31, 32, 7, 31, 7, 8, 32, 25, 26, 38, 26, 25, 27, 33, 32, 32, 26, 27, 39, 38, 25, 17, 38, 39, 16, 17, 39, 51, 31, 8, 51, 8, 9, 11, 51, 9, 11, 9, 10, 31, 50, 25, 31, 25, 32, 50, 31, 51, 49, 39, 25, 49, 25, 50, 15, 16, 39, 49, 15, 39, 13, 49, 50, 14, 15, 49, 13, 14, 49, 12, 50, 51, 12, 51, 11, 13, 50, 12 ], + "vertices": [ -23.66, 19.37, -11.73, 28.98, 4.34, 30.83, 22.41, 24.87, 32.05, 16.48, 39.77, 16.83, 49.98, 17.3, 61.25, 18.5, 82.85, 26.78, 102.4, 36.46, 107.69, 39.09, 127.15, 26.97, 151.74, 11.65, 154.49, -12.18, 157.02, -34.07, 149.89, -34.66, 122.23, -36.97, 84.75, -40.09, 55.97, -38.88, 30.73, -33.05, 15.29, -26.03, -1.3, -27.41, -18.54, -23.09, -30.78, -11.79, -32.4, 2.27, 101.92, -6.52, 70.48, -10.44, 53.28, -12.14, 34.11, -9.28, 21.96, -22.13, 27.39, -7.59, 91.48, 12.28, 64.88, 5.44, 51.07, 3.26, 36.95, 3.85, 29.92, 5.5, 31.8, -25.56, 55.08, -30.19, 79.77, -29.37, 112.93, -24.09, 14.51, -8.83, 1.48, -2.95, -12.03, -3.94, -22.69, -12.41, 20.17, 9.71, 3.53, 16.16, -13.14, 17.93, -24.78, 10.62, -1.62, -15.37, 147.71, -14.13, 141.93, 8.07, 119.3, 23.74 ], + "hull": 25, + "edges": [ 8, 6, 6, 4, 4, 2, 2, 0, 0, 48, 46, 48, 46, 44, 44, 42, 42, 40, 40, 38, 38, 36, 36, 34, 32, 34, 50, 52, 52, 54, 54, 56, 40, 58, 58, 60, 8, 10, 20, 22, 22, 24, 62, 64, 64, 66, 66, 68, 8, 70, 70, 60, 68, 70, 58, 72, 72, 74, 74, 76, 76, 78, 24, 26, 26, 28, 58, 80, 80, 82, 82, 84, 84, 86, 86, 44, 70, 88, 88, 90, 90, 92, 92, 94, 94, 48, 80, 88, 88, 6, 82, 90, 90, 4, 84, 92, 92, 2, 86, 94, 94, 0, 56, 60, 10, 12, 12, 14, 14, 16, 28, 30, 30, 32, 26, 98, 98, 78, 30, 98, 24, 100, 100, 50, 98, 100, 22, 102, 102, 62, 100, 102, 16, 18, 18, 20, 102, 18 ], + "width": 82, + "height": 184 + } + }, + "front-thigh": { + "front-thigh": { "x": 42.48, "y": 4.45, "rotation": 84.87, "width": 45, "height": 112 } + }, + "front-upper-arm": { + "front-upper-arm": { "x": 28.31, "y": 7.37, "rotation": 97.9, "width": 46, "height": 97 } + }, + "goggles": { + "goggles": { + "type": "mesh", + "uvs": [ 0.53653, 0.04114, 0.72922, 0.16036, 0.91667, 0.33223, 0.97046, 0.31329, 1, 0.48053, 0.95756, 0.5733, 0.88825, 0.6328, 0.86878, 0.78962, 0.77404, 0.8675, 0.72628, 1, 0.60714, 0.93863, 0.49601, 0.88138, 0.41558, 0.75027, 0.32547, 0.70084, 0.2782, 0.58257, 0.1721, 0.63281, 0.17229, 0.75071, 0.10781, 0.79898, 0, 0.32304, 0, 0.12476, 0.07373, 0.07344, 0.15423, 0.10734, 0.23165, 0.13994, 0.30313, 0.02256, 0.34802, 0, 0.42979, 0.69183, 0.39476, 0.51042, 0.39488, 0.31512, 0.45878, 0.23198, 0.56501, 0.28109, 0.69961, 0.39216, 0.82039, 0.54204, 0.85738, 0.62343, 0.91107, 0.51407, 0.72639, 0.32147, 0.58764, 0.19609, 0.48075, 0.11269, 0.37823, 0.05501, 0.3287, 0.17866, 0.319, 0.305, 0.36036, 0.53799, 0.40327, 0.70072, 0.30059, 0.55838, 0.21957, 0.2815, 0.09963, 0.28943, 0.56863, 0.4368, 0.4911, 0.37156, 0.51185, 0.52093, 0.67018, 0.59304, 0.7619, 0.68575, 0.73296, 0.43355 ], + "triangles": [ 18, 44, 15, 21, 19, 20, 17, 18, 15, 44, 19, 21, 2, 3, 4, 18, 19, 44, 2, 33, 34, 33, 2, 4, 5, 33, 4, 5, 6, 33, 7, 32, 6, 31, 50, 33, 32, 31, 33, 6, 32, 33, 31, 49, 50, 49, 31, 32, 49, 32, 7, 8, 49, 7, 33, 50, 34, 17, 15, 16, 9, 48, 8, 49, 48, 50, 50, 48, 45, 47, 45, 48, 50, 45, 30, 45, 47, 46, 45, 46, 29, 30, 45, 29, 30, 29, 34, 30, 34, 50, 47, 26, 46, 25, 10, 11, 12, 25, 11, 41, 12, 42, 42, 44, 43, 43, 21, 22, 41, 40, 25, 41, 42, 40, 29, 35, 34, 40, 26, 25, 25, 26, 47, 37, 24, 0, 36, 37, 0, 42, 43, 39, 42, 39, 40, 28, 38, 36, 40, 39, 26, 28, 27, 38, 26, 39, 27, 37, 38, 23, 39, 43, 38, 38, 37, 36, 27, 39, 38, 43, 22, 38, 37, 23, 24, 22, 23, 38, 36, 0, 35, 28, 36, 35, 29, 28, 35, 27, 28, 46, 26, 27, 46, 35, 0, 1, 34, 35, 1, 12, 41, 25, 47, 10, 25, 44, 21, 43, 42, 14, 44, 14, 15, 44, 13, 14, 42, 12, 13, 42, 46, 28, 29, 47, 48, 10, 48, 9, 10, 49, 8, 48, 2, 34, 1 ], + "vertices": [ 2, 66, 61.88, 22.81, 0.832, 46, 172.09, 22.81, 0.168, 2, 66, 59.89, -31.19, 0.6855, 46, 170.1, -31.19, 0.3145, 2, 66, 49.2, -86.8, 0.32635, 46, 159.41, -86.8, 0.67365, 2, 66, 56.82, -99.01, 0.01217, 46, 167.03, -99.01, 0.98783, 1, 46, 143.4, -115.48, 1, 2, 66, 15, -110.14, 0.0041, 46, 125.21, -110.14, 0.9959, 2, 66, -0.32, -96.36, 0.07948, 46, 109.89, -96.36, 0.92052, 2, 66, -26.56, -100.19, 0.01905, 46, 83.65, -100.19, 0.98095, 2, 66, -46.96, -81.16, 0.4921, 46, 63.26, -81.16, 0.50791, 2, 66, -71.84, -76.69, 0.56923, 46, 38.37, -76.69, 0.43077, 2, 66, -72.54, -43.98, 0.74145, 46, 37.67, -43.98, 0.25855, 2, 66, -73.2, -13.47, 0.87929, 46, 37.01, -13.47, 0.12071, 2, 66, -59.63, 13.55, 0.864, 46, 50.58, 13.55, 0.136, 2, 66, -59.69, 38.45, 0.85289, 46, 50.52, 38.45, 0.14711, 2, 66, -45.26, 56.6, 0.74392, 46, 64.95, 56.6, 0.25608, 2, 66, -62.31, 79.96, 0.624, 46, 47.9, 79.96, 0.376, 2, 66, -80.76, 73.42, 0.616, 46, 29.45, 73.42, 0.384, 2, 66, -93.9, 86.64, 0.288, 46, 16.31, 86.64, 0.712, 1, 46, 81.51, 139.38, 1, 1, 46, 112.56, 150.3, 1, 2, 66, 16.76, 134.97, 0.02942, 46, 126.97, 134.97, 0.97058, 2, 66, 18.42, 113.28, 0.36147, 46, 128.63, 113.28, 0.63853, 2, 66, 20.02, 92.43, 0.7135, 46, 130.23, 92.43, 0.2865, 2, 66, 44.58, 81.29, 0.69603, 46, 154.79, 81.29, 0.30397, 2, 66, 52, 71.48, 0.848, 46, 162.21, 71.48, 0.152, 2, 66, -49.25, 13.27, 0.8, 46, 60.96, 13.27, 0.2, 2, 66, -23.88, 31.88, 0.896, 46, 86.33, 31.88, 0.104, 2, 66, 6.72, 42.6, 0.928, 46, 116.93, 42.6, 0.072, 2, 66, 25.26, 31.44, 0.8, 46, 135.47, 31.44, 0.2, 2, 66, 26.77, 2.59, 0.75, 46, 136.98, 2.59, 0.25, 2, 66, 21.02, -36.66, 0.54887, 46, 131.23, -36.66, 0.45113, 2, 66, 8.01, -74.65, 0.36029, 46, 118.22, -74.65, 0.63971, 2, 66, -1.52, -88.24, 0.1253, 46, 108.69, -88.24, 0.8747, 2, 66, 20.25, -95.44, 0.08687, 46, 130.46, -95.44, 0.91313, 2, 66, 34.42, -39.36, 0.72613, 46, 144.63, -39.36, 0.27387, 2, 66, 42.03, 1.7, 0.824, 46, 152.25, 1.7, 0.176, 2, 66, 45.85, 32.6, 0.856, 46, 156.06, 32.6, 0.144, 1, 66, 46.01, 61.02, 1, 1, 66, 22.35, 66.41, 1, 1, 66, 1.73, 61.84, 1, 2, 66, -31.17, 38.83, 0.928, 46, 79.04, 38.83, 0.072, 2, 66, -52.94, 19.31, 0.79073, 46, 57.27, 19.31, 0.20927, 2, 66, -39.54, 52.42, 0.912, 46, 70.67, 52.42, 0.088, 2, 66, -3.2, 87.61, 0.744, 46, 107.02, 87.61, 0.256, 2, 66, -14.82, 116.7, 0.6368, 46, 95.4, 116.7, 0.3632, 2, 66, 2.7, -6.87, 0.856, 46, 112.91, -6.87, 0.144, 2, 66, 6.21, 15.8, 0.744, 46, 116.42, 15.8, 0.256, 2, 66, -15.39, 2.47, 0.856, 46, 94.82, 2.47, 0.144, 2, 66, -12.98, -40.48, 0.72102, 46, 97.24, -40.48, 0.27898, 2, 66, -19.55, -68.16, 0.59162, 46, 90.66, -68.16, 0.40838, 2, 66, 17.44, -47.15, 0.53452, 46, 127.65, -47.15, 0.46548 ], + "hull": 25, + "edges": [ 36, 34, 34, 32, 32, 30, 30, 28, 28, 26, 26, 24, 24, 22, 18, 16, 16, 14, 14, 12, 12, 10, 10, 8, 8, 6, 6, 4, 4, 2, 2, 0, 0, 48, 48, 46, 46, 44, 36, 38, 40, 38, 24, 50, 50, 52, 52, 54, 54, 56, 56, 58, 58, 60, 62, 64, 64, 12, 8, 66, 66, 68, 68, 70, 70, 72, 72, 74, 74, 76, 76, 78, 78, 80, 80, 82, 82, 24, 24, 84, 84, 86, 86, 44, 40, 42, 42, 44, 42, 88, 88, 30, 58, 90, 90, 92, 92, 94, 18, 20, 20, 22, 94, 20, 18, 96, 96, 98, 60, 100, 100, 62, 98, 100 ], + "width": 261, + "height": 166 + } + }, + "gun": { + "gun": { "x": 77.3, "y": 16.4, "rotation": 60.83, "width": 210, "height": 203 } + }, + "head": { + "head": { + "type": "mesh", + "uvs": [ 0.75919, 0.06107, 0.88392, 0.17893, 0.90174, 0.30856, 0.94224, 0.1966, 1, 0.26584, 1, 0.422, 0.95864, 0.46993, 0.92118, 0.51333, 0.85957, 0.5347, 0.78388, 0.65605, 0.74384, 0.74838, 0.85116, 0.75151, 0.84828, 0.82564, 0.81781, 0.85367, 0.75599, 0.85906, 0.76237, 0.90468, 0.65875, 1, 0.38337, 1, 0.1858, 0.85404, 0.12742, 0.81091, 0.06025, 0.69209, 0, 0.58552, 0, 0.41021, 0.0853, 0.20692, 0.24243, 0.14504, 0.5, 0.1421, 0.50324, 0.07433, 0.41738, 0, 0.57614, 0, 0.85059, 0.36087, 0.73431, 0.43206, 0.68481, 0.31271, 0.72165, 0.16718, 0.55931, 0.04154, 0.44764, 0.22895, 0.23926, 0.26559, 0.71272, 0.44036, 0.56993, 0.383, 0.41678, 0.33511, 0.293, 0.31497, 0.70802, 0.44502, 0.56676, 0.38976, 0.41521, 0.34416, 0.28754, 0.33017, 0.88988, 0.50177, 0.30389, 0.73463, 0.2646, 0.65675, 0.21414, 0.61584, 0.14613, 0.62194, 0.10316, 0.66636, 0.10358, 0.72557, 0.14505, 0.79164, 0.20263, 0.81355, 0.27873, 0.80159, 0.34947, 0.7376, 0.23073, 0.57073, 0.08878, 0.60707, 0.29461, 0.8129, 0.73006, 0.87883, 0.69805, 0.87348, 0.66166, 0.79681, 0.22468, 0.69824, 0.14552, 0.67405 ], + "triangles": [ 50, 49, 62, 34, 25, 31, 39, 35, 34, 38, 39, 34, 37, 38, 34, 42, 39, 38, 43, 39, 42, 32, 2, 31, 31, 37, 34, 42, 38, 37, 41, 42, 37, 43, 22, 39, 30, 31, 29, 36, 37, 31, 30, 36, 31, 40, 41, 37, 36, 40, 37, 36, 30, 44, 55, 22, 43, 55, 48, 56, 47, 48, 55, 46, 55, 54, 42, 55, 43, 47, 55, 46, 62, 49, 48, 61, 47, 46, 62, 48, 47, 61, 62, 47, 46, 54, 45, 42, 41, 55, 61, 46, 45, 55, 41, 54, 61, 51, 50, 61, 50, 62, 60, 41, 40, 54, 41, 60, 53, 61, 45, 52, 51, 61, 57, 53, 45, 57, 45, 54, 53, 52, 61, 52, 19, 51, 57, 18, 52, 57, 52, 53, 17, 54, 60, 57, 54, 17, 18, 57, 17, 19, 50, 51, 33, 27, 28, 26, 27, 33, 0, 33, 28, 32, 33, 0, 32, 0, 1, 33, 25, 26, 33, 32, 25, 31, 25, 32, 2, 32, 1, 2, 3, 4, 29, 31, 2, 2, 4, 5, 29, 2, 5, 6, 29, 5, 30, 29, 6, 44, 30, 6, 18, 19, 52, 49, 56, 48, 34, 24, 25, 35, 23, 24, 35, 24, 34, 39, 22, 35, 22, 23, 35, 7, 44, 6, 8, 36, 44, 40, 36, 8, 8, 44, 7, 56, 21, 22, 55, 56, 22, 9, 40, 8, 20, 21, 56, 20, 56, 49, 9, 60, 40, 10, 60, 9, 20, 50, 19, 12, 10, 11, 13, 10, 12, 14, 60, 10, 13, 14, 10, 59, 60, 14, 58, 59, 14, 58, 14, 15, 16, 17, 60, 59, 16, 60, 15, 16, 59, 15, 59, 58, 20, 49, 50 ], + "vertices": [ 2, 50, 41.97, -41.8, 0.94074, 66, 165.41, -22.6, 0.05926, 4, 48, 73.47, 27.54, 0.26795, 50, -5.75, -51.71, 0.4738, 49, 112.99, -11.41, 0.12255, 66, 143.5, -66.13, 0.1357, 4, 48, 38.23, 10.99, 0.6831, 50, -41.01, -35.22, 0.07866, 49, 92.73, -44.66, 0.04872, 66, 108.65, -83.49, 0.18952, 2, 48, 73.35, 10.89, 0.8455, 66, 143.77, -82.78, 0.1545, 2, 48, 58.59, -10.38, 0.91607, 66, 129.5, -104.39, 0.08393, 3, 46, 195.82, -119.82, 0.104, 47, 75.49, -4.55, 0.09191, 48, 14.36, -24.8, 0.80409, 4, 46, 178.62, -113.98, 0.19022, 47, 59.82, -13.72, 0.33409, 48, -2.7, -18.57, 0.46643, 66, 68.41, -113.98, 0.00926, 4, 46, 163.06, -108.69, 0.18724, 47, 45.64, -22.03, 0.3133, 48, -18.14, -12.93, 0.47469, 66, 52.84, -108.69, 0.02477, 2, 46, 151.52, -95.05, 0.91122, 66, 41.31, -95.05, 0.08878, 2, 46, 110.61, -87.69, 0.70564, 66, 0.4, -87.69, 0.29436, 2, 46, 81.05, -86.58, 0.63951, 66, -29.16, -86.58, 0.36049, 2, 46, 89.82, -114.32, 0.57, 66, -20.39, -114.32, 0.43, 2, 46, 68.72, -120.91, 0.57, 66, -41.49, -120.91, 0.43, 2, 46, 58.1, -115.9, 0.57, 66, -52.11, -115.9, 0.43, 2, 46, 51.03, -100.63, 0.64242, 66, -59.18, -100.63, 0.35758, 2, 46, 38.79, -106.76, 0.81659, 66, -71.43, -106.76, 0.18341, 2, 46, 2.68, -89.7, 0.77801, 66, -107.53, -89.7, 0.22199, 2, 46, -22.07, -19.3, 0.823, 66, -132.28, -19.3, 0.177, 2, 46, 1.2, 45.63, 0.51204, 66, -109.01, 45.63, 0.48796, 2, 46, 8.07, 64.81, 0.60869, 66, -102.14, 64.81, 0.39131, 2, 46, 35.44, 93.73, 0.80009, 66, -74.77, 93.73, 0.19991, 2, 46, 59.98, 119.66, 0.93554, 66, -50.23, 119.66, 0.06446, 2, 46, 109.26, 136.99, 0.99895, 66, -0.95, 136.99, 0.00105, 1, 46, 174.07, 135.27, 1, 3, 46, 205.59, 101.22, 0.80778, 49, -16.84, 104.63, 0.15658, 66, 95.38, 101.22, 0.03564, 3, 50, 58.94, 30.5, 0.43491, 49, 38.36, 61.89, 0.28116, 66, 119.35, 35.65, 0.28393, 2, 50, 75.56, 19.01, 0.92164, 66, 138.68, 41.52, 0.07836, 1, 50, 106.7, 26.9, 1, 1, 50, 83.79, -9.51, 1, 5, 47, 44.51, 27.24, 0.15139, 48, 19.12, 19.33, 0.44847, 50, -46.82, -15.19, 0.05757, 49, 72.19, -48.24, 0.1149, 66, 89.35, -75.58, 0.22767, 3, 47, 7.42, 19.08, 0.37772, 49, 34.32, -45.24, 0.09918, 66, 58.9, -52.89, 0.52311, 2, 49, 45.94, -9.07, 0.4826, 66, 87.99, -28.45, 0.5174, 2, 50, 20.62, -16.35, 0.7435, 66, 132.21, -23.49, 0.2565, 2, 50, 75.74, 0.94, 0.97172, 66, 152.95, 30.42, 0.02828, 4, 46, 200.45, 40.46, 0.18809, 50, 44.6, 56.29, 0.05831, 49, 11.15, 50.46, 0.14366, 66, 90.24, 40.46, 0.60994, 2, 46, 171.41, 90.12, 0.48644, 66, 61.2, 90.12, 0.51356, 2, 46, 164.84, -48.18, 0.43217, 66, 54.62, -48.18, 0.56783, 4, 46, 168.13, -6.02, 0.01949, 47, -28.65, 49.02, 0.02229, 49, 8.54, -6.09, 0.12791, 66, 57.92, -6.02, 0.83031, 2, 46, 167.84, 37.87, 0.15, 66, 57.63, 37.87, 0.85, 2, 46, 162.36, 71.5, 0.24107, 66, 52.15, 71.5, 0.75893, 2, 46, 163.11, -47.44, 0.41951, 66, 52.9, -47.44, 0.58049, 2, 46, 165.94, -5.87, 0.16355, 66, 55.73, -5.87, 0.83645, 2, 46, 165.14, 37.38, 0.15, 66, 54.93, 37.38, 0.85, 2, 46, 157.6, 71.4, 0.21735, 66, 47.39, 71.4, 0.78265, 3, 46, 163.5, -99.54, 0.61812, 47, 39.01, -15.71, 0.30445, 66, 53.29, -99.54, 0.07744, 2, 46, 45.38, 27.24, 0.16741, 66, -64.83, 27.24, 0.83259, 2, 46, 63.74, 44.98, 0.15, 66, -46.47, 44.98, 0.85, 2, 46, 70.7, 61.92, 0.22175, 66, -39.51, 61.92, 0.77825, 2, 46, 62.88, 78.71, 0.38, 66, -47.34, 78.71, 0.62, 2, 46, 46.53, 85.3, 0.51, 66, -63.68, 85.3, 0.49, 2, 46, 29.92, 79.34, 0.388, 66, -80.29, 79.34, 0.612, 2, 46, 15.08, 62.21, 0.38, 66, -95.13, 62.21, 0.62, 2, 46, 14.09, 45.32, 0.41, 66, -96.12, 45.32, 0.59, 2, 46, 24.3, 27.06, 0.192, 66, -85.91, 27.06, 0.808, 1, 66, -61.57, 15.3, 1, 2, 46, 84.87, 62.14, 0.16757, 66, -25.34, 62.14, 0.83243, 2, 46, 61.9, 94.84, 0.68145, 66, -48.31, 94.84, 0.31855, 2, 46, 22.54, 21.88, 0.16, 66, -87.67, 21.88, 0.84, 2, 46, 43.15, -95.95, 0.73445, 66, -67.06, -95.95, 0.26555, 2, 46, 41.77, -87.24, 0.67858, 66, -68.44, -87.24, 0.32142, 2, 46, 60.05, -70.36, 0.50195, 66, -50.16, -70.36, 0.49805, 2, 46, 48.49, 51.09, 0.25, 66, -61.72, 51.09, 0.75, 2, 46, 48.17, 73.71, 0.15634, 66, -62.04, 73.71, 0.84366 ], + "hull": 29, + "edges": [ 10, 8, 8, 6, 6, 4, 4, 2, 2, 0, 0, 56, 54, 56, 54, 52, 52, 50, 50, 48, 48, 46, 46, 44, 42, 44, 32, 34, 4, 58, 58, 60, 62, 64, 64, 66, 66, 54, 50, 68, 68, 70, 70, 44, 60, 72, 62, 74, 72, 74, 74, 76, 76, 78, 78, 44, 16, 80, 80, 82, 82, 84, 84, 86, 86, 44, 14, 88, 88, 72, 14, 16, 10, 12, 12, 14, 12, 60, 90, 92, 92, 94, 94, 96, 96, 98, 98, 100, 100, 102, 102, 104, 104, 106, 106, 90, 108, 110, 110, 112, 38, 40, 40, 42, 112, 40, 34, 36, 36, 38, 36, 114, 114, 108, 30, 32, 30, 28, 24, 26, 28, 26, 22, 24, 22, 20, 20, 18, 18, 16, 28, 116, 116, 118, 118, 120, 120, 20 ], + "width": 271, + "height": 298 + } + }, + "head-bb": { + "head": { + "type": "boundingbox", + "vertexCount": 6, + "vertices": [ -19.14, -70.3, 40.8, -118.08, 257.78, -115.62, 285.17, 57.18, 120.77, 164.95, -5.07, 76.95 ] + } + }, + "hoverboard-board": { + "hoverboard-board": { + "type": "mesh", + "uvs": [ 0.13865, 0.56624, 0.11428, 0.51461, 0.07619, 0.52107, 0.02364, 0.52998, 0.01281, 0.53182, 0, 0.37979, 0, 0.2206, 0.00519, 0.10825, 0.01038, 0.10726, 0.03834, 0.10194, 0.05091, 0, 0.08326, 0, 0.10933, 0.04206, 0.1382, 0.08865, 0.18916, 0.24067, 0.22234, 0.4063, 0.23886, 0.44063, 0.83412, 0.44034, 0.88444, 0.38296, 0.92591, 0.32639, 0.95996, 0.28841, 0.98612, 0.28542, 1, 0.38675, 0.99494, 0.47104, 0.97883, 0.53251, 0.94409, 0.62135, 0.90206, 0.69492, 0.86569, 0.71094, 0.82822, 0.70791, 0.81286, 0.77127, 0.62931, 0.77266, 0.61364, 0.70645, 0.47166, 0.70664, 0.45901, 0.77827, 0.27747, 0.76986, 0.2658, 0.70372, 0.24976, 0.71381, 0.24601, 0.77827, 0.23042, 0.84931, 0.20926, 0.90956, 0.17299, 1, 0.15077, 0.99967, 0.12906, 0.90192, 0.10369, 0.73693, 0.10198, 0.62482, 0.09131, 0.47272, 0.09133, 0.41325, 0.15082, 0.41868, 0.21991, 0.51856, 0.06331, 0.10816, 0.08383, 0.21696, 0.08905, 0.37532, 0.15903, 0.58726, 0.17538, 0.65706, 0.20118, 0.8029, 0.17918, 0.55644, 0.22166, 0.5802, 0.86259, 0.57962, 0.92346, 0.48534, 0.96691, 0.36881, 0.0945, 0.13259, 0.12688, 0.17831, 0.15986, 0.24682, 0.18036, 0.31268, 0.20607, 0.4235, 0.16074, 0.85403, 0.13624, 0.70122, 0.12096, 0.64049, 0.02396, 0.21811, 0.02732, 0.37839, 0.02557, 0.4972, 0.14476, 0.45736, 0.18019, 0.51689, 0.19692, 0.56636 ], + "triangles": [ 10, 11, 12, 9, 10, 12, 49, 9, 12, 60, 49, 12, 13, 60, 12, 61, 60, 13, 50, 49, 60, 50, 60, 61, 68, 8, 9, 68, 9, 49, 68, 49, 50, 7, 8, 68, 6, 7, 68, 61, 13, 14, 62, 61, 14, 50, 61, 62, 63, 62, 14, 59, 20, 21, 19, 20, 59, 51, 50, 62, 51, 62, 63, 51, 69, 68, 51, 68, 50, 6, 68, 69, 5, 6, 69, 18, 19, 59, 15, 63, 14, 59, 21, 22, 47, 51, 63, 47, 46, 51, 47, 63, 64, 15, 64, 63, 64, 15, 16, 71, 46, 47, 23, 59, 22, 69, 51, 70, 45, 46, 71, 70, 51, 2, 58, 18, 59, 58, 59, 23, 17, 18, 58, 70, 5, 69, 2, 51, 46, 1, 45, 71, 47, 48, 71, 47, 64, 48, 48, 72, 71, 1, 71, 72, 16, 48, 64, 45, 2, 46, 2, 45, 1, 70, 4, 5, 3, 70, 2, 3, 4, 70, 24, 58, 23, 72, 0, 1, 73, 55, 72, 55, 0, 72, 48, 73, 72, 57, 17, 58, 25, 57, 58, 56, 48, 16, 73, 48, 56, 56, 16, 17, 56, 17, 57, 52, 0, 55, 24, 25, 58, 44, 0, 52, 67, 44, 52, 52, 56, 53, 73, 52, 55, 56, 52, 73, 67, 52, 53, 26, 57, 25, 66, 67, 53, 56, 32, 35, 53, 56, 35, 56, 57, 32, 28, 31, 57, 57, 31, 32, 57, 27, 28, 26, 27, 57, 36, 53, 35, 43, 44, 67, 43, 67, 66, 34, 35, 32, 29, 31, 28, 30, 31, 29, 53, 54, 66, 53, 36, 54, 33, 34, 32, 37, 54, 36, 65, 43, 66, 38, 54, 37, 54, 65, 66, 39, 65, 54, 42, 43, 65, 38, 39, 54, 40, 42, 65, 40, 41, 42, 65, 39, 40 ], + "vertices": [ -189.36, 15.62, -201.35, 23.47, -220.09, 22.49, -245.95, 21.13, -251.28, 20.86, -257.58, 43.96, -257.57, 68.16, -255.02, 85.24, -252.47, 85.39, -238.71, 86.2, -232.52, 101.69, -216.61, 101.69, -203.78, 95.3, -189.58, 88.21, -164.51, 65.1, -148.19, 39.93, -140.06, 34.71, 152.82, 34.73, 177.57, 43.45, 197.97, 52.05, 214.72, 57.82, 227.6, 58.27, 234.42, 42.87, 231.94, 30.06, 224.01, 20.72, 206.91, 7.21, 186.23, -3.97, 168.34, -6.4, 149.9, -5.94, 142.35, -15.57, 52.04, -15.77, 44.33, -5.71, -25.52, -5.73, -31.75, -16.62, -121.07, -15.34, -126.81, -5.28, -134.7, -6.81, -136.54, -16.61, -144.22, -27.41, -154.63, -36.57, -172.47, -50.31, -183.41, -50.26, -194.09, -35.4, -206.56, -10.32, -207.4, 6.72, -212.65, 29.84, -212.64, 38.88, -183.37, 38.05, -149.38, 22.86, -226.43, 85.25, -216.33, 68.71, -213.76, 44.64, -179.34, 12.42, -171.29, 1.81, -158.6, -20.36, -169.42, 17.11, -148.52, 13.49, 166.82, 13.56, 196.76, 27.89, 218.14, 45.6, -211.08, 81.54, -195.15, 74.59, -178.93, 64.17, -168.84, 54.16, -156.19, 37.31, -178.5, -28.13, -190.55, -4.9, -198.07, 4.33, -245.79, 68.54, -244.14, 44.18, -245, 26.12, -186.36, 32.17, -168.92, 23.12, -160.69, 15.6 ], + "hull": 45, + "edges": [ 0, 2, 8, 10, 10, 12, 12, 14, 18, 20, 20, 22, 26, 28, 28, 30, 30, 32, 32, 34, 34, 36, 36, 38, 38, 40, 40, 42, 42, 44, 44, 46, 46, 48, 48, 50, 50, 52, 52, 54, 54, 56, 56, 58, 58, 60, 60, 62, 62, 64, 64, 66, 66, 68, 68, 70, 70, 72, 72, 74, 80, 82, 82, 84, 84, 86, 86, 88, 0, 88, 2, 90, 90, 92, 92, 94, 94, 96, 96, 32, 18, 98, 98, 100, 100, 102, 2, 4, 102, 4, 92, 102, 0, 104, 104, 106, 106, 108, 78, 80, 108, 78, 74, 76, 76, 78, 62, 56, 64, 70, 0, 110, 112, 114, 114, 116, 116, 118, 118, 42, 50, 116, 114, 34, 98, 120, 120, 122, 22, 24, 24, 26, 120, 24, 122, 124, 124, 126, 126, 128, 128, 96, 80, 130, 130, 132, 132, 134, 134, 88, 14, 16, 16, 18, 136, 16, 136, 138, 138, 140, 4, 6, 6, 8, 140, 6, 96, 112, 92, 142, 142, 144, 110, 146, 146, 112, 144, 146 ], + "width": 492, + "height": 152 + } + }, + "hoverboard-thruster-front": { + "hoverboard-thruster": { "x": 0.02, "y": -7.08, "rotation": 0.17, "width": 60, "height": 64 } + }, + "hoverboard-thruster-rear": { + "hoverboard-thruster": { "x": 1.1, "y": -6.29, "rotation": 0.17, "width": 60, "height": 64 } + }, + "hoverglow-front": { + "hoverglow-small": { + "x": 2.13, + "y": -2, + "scaleX": 0.303, + "scaleY": 0.495, + "rotation": 0.15, + "width": 274, + "height": 75 + } + }, + "hoverglow-rear": { + "hoverglow-small": { + "x": 1.39, + "y": -2.09, + "scaleX": 0.303, + "scaleY": 0.495, + "rotation": 0.61, + "width": 274, + "height": 75 + } + }, + "mouth": { + "mouth-grind": { + "type": "mesh", + "uvs": [ 1, 1, 0, 1, 0, 0, 1, 0 ], + "triangles": [ 1, 3, 0, 1, 2, 3 ], + "vertices": [ 2, 66, -98.93, -85.88, 0.22, 46, 11.28, -85.88, 0.78, 2, 66, -129.77, 1.84, 0.6, 46, -19.56, 1.84, 0.4, 2, 66, -74.12, 21.41, 0.6, 46, 36.09, 21.41, 0.4, 2, 66, -43.28, -66.32, 0.4, 46, 66.93, -66.32, 0.6 ], + "hull": 4, + "edges": [ 0, 2, 2, 4, 4, 6, 0, 6 ], + "width": 93, + "height": 59 + }, + "mouth-oooo": { + "type": "mesh", + "uvs": [ 1, 1, 0, 1, 0, 0, 1, 0 ], + "triangles": [ 1, 3, 0, 1, 2, 3 ], + "vertices": [ 2, 46, 11.28, -85.89, 0.22, 66, -98.93, -85.89, 0.78, 2, 46, -19.56, 1.85, 0.6, 66, -129.78, 1.85, 0.4, 2, 46, 36.1, 21.42, 0.6, 66, -74.12, 21.42, 0.4, 2, 46, 66.94, -66.32, 0.4, 66, -43.27, -66.32, 0.6 ], + "hull": 4, + "edges": [ 0, 2, 2, 4, 4, 6, 0, 6 ], + "width": 93, + "height": 59 + }, + "mouth-smile": { + "type": "mesh", + "uvs": [ 1, 1, 0, 1, 0, 0, 1, 0 ], + "triangles": [ 1, 3, 0, 1, 2, 3 ], + "vertices": [ 2, 66, -98.93, -85.89, 0.21075, 46, 11.28, -85.89, 0.78925, 2, 66, -129.77, 1.85, 0.6, 46, -19.56, 1.85, 0.4, 2, 66, -74.11, 21.42, 0.6, 46, 36.1, 21.42, 0.4, 2, 66, -43.27, -66.32, 0.40772, 46, 66.94, -66.32, 0.59228 ], + "hull": 4, + "edges": [ 0, 2, 2, 4, 4, 6, 0, 6 ], + "width": 93, + "height": 59 + } + }, + "muzzle": { + "muzzle01": { + "x": 151.97, + "y": 5.81, + "scaleX": 3.7361, + "scaleY": 3.7361, + "rotation": 0.15, + "width": 133, + "height": 79 + }, + "muzzle02": { + "x": 187.25, + "y": 5.9, + "scaleX": 4.0623, + "scaleY": 4.0623, + "rotation": 0.15, + "width": 135, + "height": 84 + }, + "muzzle03": { + "x": 231.96, + "y": 6.02, + "scaleX": 4.1325, + "scaleY": 4.1325, + "rotation": 0.15, + "width": 166, + "height": 106 + }, + "muzzle04": { + "x": 231.96, + "y": 6.02, + "scaleX": 4.0046, + "scaleY": 4.0046, + "rotation": 0.15, + "width": 149, + "height": 90 + }, + "muzzle05": { + "x": 293.8, + "y": 6.19, + "scaleX": 4.4673, + "scaleY": 4.4673, + "rotation": 0.15, + "width": 135, + "height": 75 + } + }, + "muzzle-glow": { + "muzzle-glow": { "width": 50, "height": 50 } + }, + "muzzle-ring": { + "muzzle-ring": { "x": -1.3, "y": 0.32, "scaleX": 0.3147, "scaleY": 0.3147, "width": 49, "height": 209 } + }, + "muzzle-ring2": { + "muzzle-ring": { "x": -1.3, "y": 0.32, "scaleX": 0.3147, "scaleY": 0.3147, "width": 49, "height": 209 } + }, + "muzzle-ring3": { + "muzzle-ring": { "x": -1.3, "y": 0.32, "scaleX": 0.3147, "scaleY": 0.3147, "width": 49, "height": 209 } + }, + "muzzle-ring4": { + "muzzle-ring": { "x": -1.3, "y": 0.32, "scaleX": 0.3147, "scaleY": 0.3147, "width": 49, "height": 209 } + }, + "neck": { + "neck": { "x": 9.77, "y": -3.01, "rotation": -55.22, "width": 36, "height": 41 } + }, + "portal-bg": { + "portal-bg": { "x": -3.1, "y": 7.25, "scaleX": 1.0492, "scaleY": 1.0492, "width": 266, "height": 266 } + }, + "portal-flare1": { + "portal-flare1": { "width": 111, "height": 60 }, + "portal-flare2": { "width": 114, "height": 61 }, + "portal-flare3": { "width": 115, "height": 59 } + }, + "portal-flare2": { + "portal-flare1": { "width": 111, "height": 60 }, + "portal-flare2": { "width": 114, "height": 61 }, + "portal-flare3": { "width": 115, "height": 59 } + }, + "portal-flare3": { + "portal-flare1": { "width": 111, "height": 60 }, + "portal-flare2": { "width": 114, "height": 61 }, + "portal-flare3": { "width": 115, "height": 59 } + }, + "portal-flare4": { + "portal-flare1": { "width": 111, "height": 60 }, + "portal-flare2": { "width": 114, "height": 61 }, + "portal-flare3": { "width": 115, "height": 59 } + }, + "portal-flare5": { + "portal-flare1": { "width": 111, "height": 60 }, + "portal-flare2": { "width": 114, "height": 61 }, + "portal-flare3": { "width": 115, "height": 59 } + }, + "portal-flare6": { + "portal-flare1": { "width": 111, "height": 60 }, + "portal-flare2": { "width": 114, "height": 61 }, + "portal-flare3": { "width": 115, "height": 59 } + }, + "portal-flare7": { + "portal-flare1": { "width": 111, "height": 60 }, + "portal-flare2": { "width": 114, "height": 61 }, + "portal-flare3": { "width": 115, "height": 59 } + }, + "portal-flare8": { + "portal-flare1": { "width": 111, "height": 60 }, + "portal-flare2": { "width": 114, "height": 61 }, + "portal-flare3": { "width": 115, "height": 59 } + }, + "portal-flare9": { + "portal-flare1": { "width": 111, "height": 60 }, + "portal-flare2": { "width": 114, "height": 61 }, + "portal-flare3": { "width": 115, "height": 59 } + }, + "portal-flare10": { + "portal-flare1": { "width": 111, "height": 60 }, + "portal-flare2": { "width": 114, "height": 61 }, + "portal-flare3": { "width": 115, "height": 59 } + }, + "portal-shade": { + "portal-shade": { "width": 266, "height": 266 } + }, + "portal-streaks1": { + "portal-streaks1": { "scaleX": 0.9774, "scaleY": 0.9774, "width": 252, "height": 256 } + }, + "portal-streaks2": { + "portal-streaks2": { "x": -1.64, "y": 2.79, "width": 250, "height": 249 } + }, + "rear-bracer": { + "rear-bracer": { "x": 11.15, "y": -2.2, "rotation": 66.17, "width": 56, "height": 72 } + }, + "rear-foot": { + "rear-foot": { + "type": "mesh", + "uvs": [ 0.48368, 0.1387, 0.51991, 0.21424, 0.551, 0.27907, 0.58838, 0.29816, 0.63489, 0.32191, 0.77342, 0.39267, 1, 0.73347, 1, 1, 0.54831, 0.99883, 0.31161, 1, 0, 1, 0, 0.41397, 0.13631, 0, 0.41717, 0 ], + "triangles": [ 8, 3, 4, 8, 4, 5, 8, 5, 6, 8, 6, 7, 11, 1, 10, 3, 9, 2, 2, 10, 1, 12, 13, 0, 0, 11, 12, 1, 11, 0, 2, 9, 10, 3, 8, 9 ], + "vertices": [ 2, 8, 10.45, 29.41, 0.90802, 9, -6.74, 49.62, 0.09198, 2, 8, 16.56, 29.27, 0.84259, 9, -2.65, 45.09, 0.15741, 2, 8, 21.8, 29.15, 0.69807, 9, 0.85, 41.2, 0.30193, 2, 8, 25.53, 31.43, 0.52955, 9, 5.08, 40.05, 0.47045, 2, 8, 30.18, 34.27, 0.39303, 9, 10.33, 38.62, 0.60697, 2, 8, 44.02, 42.73, 0.27525, 9, 25.98, 34.36, 0.72475, 2, 8, 76.47, 47.28, 0.21597, 9, 51.56, 13.9, 0.78403, 2, 8, 88.09, 36.29, 0.28719, 9, 51.55, -2.09, 0.71281, 2, 8, 52.94, -0.73, 0.47576, 9, 0.52, -1.98, 0.52424, 2, 8, 34.63, -20.23, 0.68757, 9, -26.23, -2.03, 0.31243, 2, 8, 10.44, -45.81, 0.84141, 9, -61.43, -2, 0.15859, 2, 8, -15.11, -21.64, 0.93283, 9, -61.4, 33.15, 0.06717, 1, 8, -22.57, 6.61, 1, 1, 8, -0.76, 29.67, 1 ], + "hull": 14, + "edges": [ 14, 12, 10, 12, 14, 16, 16, 18, 18, 20, 4, 18, 20, 22, 24, 26, 22, 24, 4, 2, 2, 20, 4, 6, 6, 16, 6, 8, 8, 10, 2, 0, 0, 26 ], + "width": 113, + "height": 60 + } + }, + "rear-shin": { + "rear-shin": { "x": 58.29, "y": -2.75, "rotation": 92.37, "width": 75, "height": 178 } + }, + "rear-thigh": { + "rear-thigh": { "x": 33.11, "y": -4.11, "rotation": 72.54, "width": 55, "height": 94 } + }, + "rear-upper-arm": { + "rear-upper-arm": { "x": 21.13, "y": 4.09, "rotation": 89.33, "width": 40, "height": 87 } + }, + "side-glow1": { + "hoverglow-small": { "x": 2.09, "scaleX": 0.2353, "scaleY": 0.4132, "width": 274, "height": 75 } + }, + "side-glow2": { + "hoverglow-small": { "x": 2.09, "scaleX": 0.2353, "scaleY": 0.4132, "width": 274, "height": 75 } + }, + "side-glow3": { + "hoverglow-small": { "x": 2.09, "scaleX": 0.3586, "scaleY": 0.6297, "width": 274, "height": 75 } + }, + "torso": { + "torso": { + "type": "mesh", + "uvs": [ 0.6251, 0.12672, 1, 0.26361, 1, 0.28871, 1, 0.66021, 1, 0.68245, 0.92324, 0.69259, 0.95116, 0.84965, 0.77124, 1, 0.49655, 1, 0.27181, 1, 0.13842, 0.77196, 0.09886, 0.6817, 0.05635, 0.58471, 0, 0.45614, 0, 0.33778, 0, 0.19436, 0.14463, 0, 0.27802, 0, 0.72525, 0.27835, 0.76091, 0.46216, 0.84888, 0.67963, 0.68257, 0.63249, 0.53986, 0.3847, 0.25443, 0.3217, 0.30063, 0.55174, 0.39553, 0.79507, 0.26389, 0.17007, 0.5241, 0.18674, 0.71492, 0.76655, 0.82151, 0.72956, 0.27626, 0.4304, 0.62327, 0.52952, 0.3455, 0.66679, 0.53243, 0.2914 ], + "triangles": [ 18, 1, 2, 19, 2, 3, 18, 0, 1, 23, 15, 26, 27, 26, 16, 14, 15, 23, 15, 16, 26, 17, 27, 16, 13, 14, 23, 0, 27, 17, 13, 23, 30, 11, 12, 24, 21, 31, 19, 12, 13, 30, 24, 22, 31, 31, 22, 19, 12, 30, 24, 32, 24, 31, 24, 30, 22, 3, 20, 19, 32, 31, 21, 11, 24, 32, 4, 5, 3, 8, 28, 7, 7, 29, 6, 7, 28, 29, 9, 25, 8, 8, 25, 28, 9, 10, 25, 29, 5, 6, 10, 32, 25, 25, 21, 28, 25, 32, 21, 10, 11, 32, 28, 21, 29, 29, 20, 5, 29, 21, 20, 5, 20, 3, 20, 21, 19, 33, 26, 27, 22, 18, 19, 19, 18, 2, 33, 27, 18, 30, 23, 22, 22, 33, 18, 23, 33, 22, 33, 23, 26, 27, 0, 18 ], + "vertices": [ 2, 29, 44.59, -10.39, 0.88, 40, -17.65, 33.99, 0.12, 3, 28, 59.65, -45.08, 0.12189, 29, 17.13, -45.08, 0.26811, 40, 22.68, 15.82, 0.61, 3, 28, 55.15, -44.72, 0.1345, 29, 12.63, -44.72, 0.2555, 40, 23.43, 11.37, 0.61, 3, 27, 31.01, -39.45, 0.51133, 28, -11.51, -39.45, 0.30867, 40, 34.58, -54.57, 0.18, 3, 27, 27.01, -39.14, 0.53492, 28, -15.5, -39.14, 0.28508, 40, 35.25, -58.52, 0.18, 2, 27, 25.79, -31.5, 0.75532, 28, -16.73, -31.5, 0.24468, 1, 27, -2.61, -32, 1, 1, 27, -28.2, -12.29, 1, 1, 27, -26.08, 14.55, 1, 1, 27, -24.35, 36.5, 1, 2, 27, 17.6, 46.3, 0.8332, 28, -24.92, 46.3, 0.1668, 2, 27, 34.1, 48.89, 0.59943, 28, -8.42, 48.89, 0.40058, 3, 27, 51.83, 51.67, 0.29262, 28, 9.32, 51.67, 0.63181, 29, -33.2, 51.67, 0.07557, 3, 27, 75.34, 55.35, 0.06656, 28, 32.82, 55.35, 0.62298, 29, -9.7, 55.35, 0.31046, 2, 28, 54.06, 53.67, 0.37296, 29, 11.54, 53.67, 0.62704, 2, 28, 79.79, 51.64, 0.10373, 29, 37.27, 51.64, 0.89627, 1, 29, 71.04, 34.76, 1, 1, 29, 70.01, 21.72, 1, 1, 30, 36.74, 7.06, 1, 3, 30, 45.7, -24.98, 0.67, 28, 25.87, -18.9, 0.3012, 29, -16.65, -18.9, 0.0288, 2, 27, 28.69, -24.42, 0.77602, 28, -13.83, -24.42, 0.22398, 3, 30, 43.24, -56.49, 0.064, 27, 38.43, -8.84, 0.67897, 28, -4.09, -8.84, 0.25703, 3, 30, 22.02, -14.85, 0.29, 28, 41.48, 1.59, 0.53368, 29, -1.04, 1.59, 0.17632, 3, 30, -7.45, -8.33, 0.76, 28, 54.98, 28.59, 0.06693, 29, 12.46, 28.59, 0.17307, 3, 30, 3.91, -48.4, 0.25, 27, 55.87, 27.33, 0.15843, 28, 13.35, 27.33, 0.59157, 1, 27, 11.47, 21.51, 1, 2, 30, -11.09, 18.74, 0.416, 29, 39.6, 25.51, 0.584, 2, 30, 14.56, 20.03, 0.53, 29, 34.6, 0.33, 0.47, 1, 27, 14.12, -10.1, 1, 2, 27, 19.94, -21.03, 0.92029, 28, -22.58, -21.03, 0.07971, 3, 30, -2.08, -27.26, 0.29, 28, 35.31, 27.99, 0.49582, 29, -7.21, 27.99, 0.21418, 2, 30, 34.42, -39.19, 0.25, 28, 14.84, -4.5, 0.75, 2, 27, 34.87, 24.58, 0.67349, 28, -7.64, 24.58, 0.32651, 2, 30, 18.5, 1.59, 0.76, 29, 15.76, 1, 0.24 ], + "hull": 18, + "edges": [ 14, 12, 12, 10, 10, 8, 18, 20, 32, 34, 30, 32, 2, 4, 36, 4, 36, 38, 38, 40, 4, 6, 6, 8, 40, 6, 40, 42, 14, 16, 16, 18, 50, 16, 46, 52, 54, 36, 2, 0, 0, 34, 54, 0, 54, 32, 20, 50, 14, 56, 56, 42, 50, 56, 56, 58, 58, 40, 58, 10, 46, 60, 60, 48, 26, 60, 60, 44, 24, 26, 24, 48, 42, 62, 62, 44, 48, 62, 48, 64, 64, 50, 42, 64, 20, 22, 22, 24, 64, 22, 26, 28, 28, 30, 28, 46, 44, 66, 66, 54, 46, 66, 66, 36, 62, 38 ], + "width": 98, + "height": 180 + } + } + } + } + ], + "events": { + "footstep": {} + }, + "animations": { + "aim": { + "slots": { + "crosshair": { + "attachment": [ + { "name": "crosshair" } + ] + } + }, + "bones": { + "front-fist": { + "rotate": [ + { "value": 36.08 } + ] + }, + "rear-bracer": { + "rotate": [ + { "value": -26.55 } + ] + }, + "rear-upper-arm": { + "rotate": [ + { "value": 62.31 } + ] + }, + "front-bracer": { + "rotate": [ + { "value": 9.11 } + ] + }, + "gun": { + "rotate": [ + { "value": -0.31 } + ] + } + }, + "ik": { + "aim-ik": [ + { "mix": 0.995 } + ] + }, + "transform": { + "aim-front-arm-transform": [ + { "mixRotate": 0.784, "mixX": 0, "mixScaleX": 0, "mixShearY": 0 } + ], + "aim-head-transform": [ + { "mixRotate": 0.659, "mixX": 0, "mixScaleX": 0, "mixShearY": 0 } + ], + "aim-torso-transform": [ + { "mixRotate": 0.423, "mixX": 0, "mixScaleX": 0, "mixShearY": 0 } + ] + } + }, + "death": { + "slots": { + "eye": { + "attachment": [ + { "name": "eye-surprised" }, + { "time": 0.5333, "name": "eye-indifferent" }, + { "time": 2.2, "name": "eye-surprised" }, + { "time": 4.6, "name": "eye-indifferent" } + ] + }, + "front-fist": { + "attachment": [ + { "name": "front-fist-open" } + ] + }, + "mouth": { + "attachment": [ + { "name": "mouth-oooo" }, + { "time": 0.5333, "name": "mouth-grind" }, + { "time": 1.4, "name": "mouth-oooo" }, + { "time": 2.1667, "name": "mouth-grind" }, + { "time": 4.5333, "name": "mouth-oooo" } + ] + } + }, + "bones": { + "head": { + "rotate": [ + { + "value": -2.83, + "curve": [ 0.015, -2.83, 0.036, 12.72 ] + }, + { + "time": 0.0667, + "value": 12.19, + "curve": [ 0.096, 11.68, 0.119, -1.14 ] + }, + { + "time": 0.1333, + "value": -6.86, + "curve": [ 0.149, -13.27, 0.21, -37.28 ] + }, + { + "time": 0.3, + "value": -36.86, + "curve": [ 0.354, -36.61, 0.412, -32.35 ] + }, + { + "time": 0.4667, + "value": -23.49, + "curve": [ 0.49, -19.87, 0.512, -3.29 ] + }, + { + "time": 0.5333, + "value": -3.24, + "curve": [ 0.56, -3.39, 0.614, -67.25 ] + }, + { + "time": 0.6333, + "value": -74.4, + "curve": [ 0.652, -81.58, 0.702, -88.94 ] + }, + { + "time": 0.7333, + "value": -88.93, + "curve": [ 0.805, -88.91, 0.838, -80.87 ] + }, + { + "time": 0.8667, + "value": -81.03, + "curve": [ 0.922, -81.32, 0.976, -85.29 ] + }, + { "time": 1, "value": -85.29, "curve": "stepped" }, + { + "time": 2.2333, + "value": -85.29, + "curve": [ 2.314, -85.29, 2.382, -68.06 ] + }, + { + "time": 2.4667, + "value": -63.48, + "curve": [ 2.57, -57.87, 2.916, -55.24 ] + }, + { + "time": 3.2, + "value": -55.1, + "curve": [ 3.447, -54.98, 4.135, -56.61 ] + }, + { + "time": 4.2667, + "value": -58.23, + "curve": [ 4.672, -63.24, 4.646, -82.69 ] + }, + { "time": 4.9333, "value": -85.29 } + ], + "scale": [ + { + "time": 0.4667, + "curve": [ 0.469, 1.005, 0.492, 1.065, 0.475, 1.018, 0.492, 0.94 ] + }, + { + "time": 0.5, + "x": 1.065, + "y": 0.94, + "curve": [ 0.517, 1.065, 0.541, 0.991, 0.517, 0.94, 0.542, 1.026 ] + }, + { + "time": 0.5667, + "x": 0.99, + "y": 1.025, + "curve": [ 0.593, 0.988, 0.609, 1.002, 0.595, 1.024, 0.607, 1.001 ] + }, + { "time": 0.6333 } + ] + }, + "neck": { + "rotate": [ + { + "value": -2.83, + "curve": [ 0.114, 1.33, 0.195, 4.13 ] + }, + { + "time": 0.2667, + "value": 4.13, + "curve": [ 0.351, 4.14, 0.444, -24.5 ] + }, + { + "time": 0.5, + "value": -24.69, + "curve": [ 0.571, -23.89, 0.55, 34.22 ] + }, + { + "time": 0.6667, + "value": 35.13, + "curve": [ 0.713, 34.81, 0.756, 22.76 ] + }, + { + "time": 0.8333, + "value": 22.82, + "curve": [ 0.868, 22.84, 0.916, 47.95 ] + }, + { "time": 0.9667, "value": 47.95, "curve": "stepped" }, + { + "time": 2.2333, + "value": 47.95, + "curve": [ 2.3, 47.95, 2.617, 18.72 ] + }, + { + "time": 2.6667, + "value": 18.51, + "curve": [ 3.172, 16.58, 4.06, 16.79 ] + }, + { + "time": 4.5333, + "value": 18.51, + "curve": [ 4.707, 19.13, 4.776, 41.11 ] + }, + { "time": 4.8, "value": 47.95 } + ] + }, + "torso": { + "rotate": [ + { + "value": -8.62, + "curve": [ 0.01, -16.71, 0.032, -33.6 ] + }, + { + "time": 0.0667, + "value": -33.37, + "curve": [ 0.182, -32.61, 0.298, 123.07 ] + }, + { + "time": 0.4667, + "value": 122.77, + "curve": [ 0.511, 122.69, 0.52, 100.2 ] + }, + { + "time": 0.5667, + "value": 88.96, + "curve": [ 0.588, 83.89, 0.667, 75.34 ] + }, + { + "time": 0.7, + "value": 75.34, + "curve": [ 0.767, 75.34, 0.9, 76.03 ] + }, + { "time": 0.9667, "value": 76.03 } + ] + }, + "front-upper-arm": { + "rotate": [ + { + "value": -38.86, + "curve": [ 0.022, -40.38, 0.096, -41.92 ] + }, + { + "time": 0.1333, + "value": -41.92, + "curve": [ 0.176, -41.92, 0.216, -16.92 ] + }, + { + "time": 0.2333, + "value": -4.35, + "curve": [ 0.258, 13.69, 0.308, 60.35 ] + }, + { + "time": 0.4, + "value": 60.17, + "curve": [ 0.496, 59.98, 0.539, 33.63 ] + }, + { + "time": 0.5667, + "value": 23.06, + "curve": [ 0.595, 32.71, 0.675, 53.71 ] + }, + { + "time": 0.7333, + "value": 53.61, + "curve": [ 0.797, 53.51, 0.926, 30.98 ] + }, + { "time": 0.9333, "value": 19.57, "curve": "stepped" }, + { + "time": 1.9667, + "value": 19.57, + "curve": [ 2.245, 19.57, 2.702, 77.03 ] + }, + { + "time": 3.0667, + "value": 77.06, + "curve": [ 3.209, 77.33, 3.291, 67.99 ] + }, + { + "time": 3.4333, + "value": 67.96, + "curve": [ 3.608, 68.34, 3.729, 73.88 ] + }, + { + "time": 3.8333, + "value": 73.42, + "curve": [ 4.152, 73.91, 4.46, 71.98 ] + }, + { + "time": 4.6333, + "value": 64.77, + "curve": [ 4.688, 62.5, 4.847, 26.42 ] + }, + { "time": 4.8667, "value": 10.94 } + ] + }, + "rear-upper-arm": { + "rotate": [ + { + "value": -44.7, + "curve": [ 0.033, -44.7, 0.12, 54.89 ] + }, + { + "time": 0.1333, + "value": 64.62, + "curve": [ 0.154, 79.18, 0.214, 79.42 ] + }, + { + "time": 0.2667, + "value": 63.4, + "curve": [ 0.293, 55.19, 0.332, 30.13 ] + }, + { + "time": 0.3667, + "value": 30.13, + "curve": [ 0.4, 30.13, 0.441, 39.87 ] + }, + { + "time": 0.4667, + "value": 55.13, + "curve": [ 0.488, 68.18, 0.52, 100.72 ] + }, + { + "time": 0.5333, + "value": 111.96, + "curve": [ 0.551, 126.88, 0.627, 185.97 ] + }, + { + "time": 0.6667, + "value": 185.97, + "curve": [ 0.692, 185.97, 0.736, 162.43 ] + }, + { + "time": 0.8, + "value": 158.01, + "curve": [ 0.9, 151.12, 1.017, 144.01 ] + }, + { "time": 1.1, "value": 144.01, "curve": "stepped" }, + { + "time": 2.3667, + "value": 144.01, + "curve": [ 2.492, 144.01, 2.742, 138.63 ] + }, + { + "time": 2.8667, + "value": 138.63, + "curve": [ 3.067, 138.63, 3.467, 138.63 ] + }, + { + "time": 3.6667, + "value": 138.63, + "curve": [ 3.883, 138.63, 4.317, 135.18 ] + }, + { + "time": 4.5333, + "value": 135.18, + "curve": [ 4.575, 135.18, 4.692, 131.59 ] + }, + { + "time": 4.7333, + "value": 131.59, + "curve": [ 4.758, 131.59, 4.517, 144.01 ] + }, + { "time": 4.8333, "value": 144.01 } + ], + "translate": [ + { + "time": 0.4667, + "curve": [ 0.517, 0, 0.617, -34.96, 0.517, 0, 0.617, -16.59 ] + }, + { "time": 0.6667, "x": -35.02, "y": -16.62 } + ] + }, + "front-bracer": { + "rotate": [ + { + "value": 21.88, + "curve": [ 0.033, 21.88, 0.099, 20.44 ] + }, + { + "time": 0.1333, + "value": 9.43, + "curve": [ 0.164, -0.29, 0.162, -38.26 ] + }, + { + "time": 0.2, + "value": -38.05, + "curve": [ 0.24, -37.96, 0.228, -17.82 ] + }, + { + "time": 0.3333, + "value": -9.73, + "curve": [ 0.372, -6.76, 0.431, -0.74 ] + }, + { + "time": 0.4667, + "value": 6.47, + "curve": [ 0.489, 11.05, 0.503, 19.09 ] + }, + { + "time": 0.5333, + "value": 19.09, + "curve": [ 0.571, 19.09, 0.554, -42.67 ] + }, + { + "time": 0.6, + "value": -42.67, + "curve": [ 0.653, -42.67, 0.691, -13.8 ] + }, + { + "time": 0.7, + "value": -3.54, + "curve": [ 0.707, 3.8, 0.719, 24.94 ] + }, + { + "time": 0.8, + "value": 25.31, + "curve": [ 0.902, 24.75, 0.992, -0.34 ] + }, + { "time": 1, "value": -32.16, "curve": "stepped" }, + { + "time": 2.2333, + "value": -32.16, + "curve": [ 2.6, -32.16, 2.638, -5.3 ] + }, + { + "time": 2.7, + "value": -1.96, + "curve": [ 2.707, -1.56, 2.775, 1.67 ] + }, + { + "time": 2.8, + "value": 1.67, + "curve": [ 2.825, 1.67, 2.875, -0.39 ] + }, + { + "time": 2.9, + "value": -0.39, + "curve": [ 2.925, -0.39, 2.975, 0.26 ] + }, + { + "time": 3, + "value": 0.26, + "curve": [ 3.025, 0.26, 3.075, -1.81 ] + }, + { + "time": 3.1, + "value": -1.81, + "curve": [ 3.125, -1.81, 3.175, -0.52 ] + }, + { + "time": 3.2, + "value": -0.52, + "curve": [ 3.225, -0.52, 3.275, -2.41 ] + }, + { + "time": 3.3, + "value": -2.41, + "curve": [ 3.333, -2.41, 3.4, -0.38 ] + }, + { + "time": 3.4333, + "value": -0.38, + "curve": [ 3.467, -0.38, 3.533, -2.25 ] + }, + { + "time": 3.5667, + "value": -2.25, + "curve": [ 3.592, -2.25, 3.642, -0.33 ] + }, + { + "time": 3.6667, + "value": -0.33, + "curve": [ 3.7, -0.33, 3.767, -1.34 ] + }, + { + "time": 3.8, + "value": -1.34, + "curve": [ 3.825, -1.34, 3.862, -0.77 ] + }, + { + "time": 3.9, + "value": -0.77, + "curve": [ 3.942, -0.77, 3.991, -1.48 ] + }, + { + "time": 4, + "value": -1.87, + "curve": [ 4.167, -1.87, 4.5, -1.96 ] + }, + { + "time": 4.6667, + "value": -1.96, + "curve": [ 4.709, 18.05, 4.767, 34.55 ] + }, + { + "time": 4.8, + "value": 34.55, + "curve": [ 4.84, 34.24, 4.902, 12.03 ] + }, + { "time": 4.9333, "value": -18.75 } + ] + }, + "front-fist": { + "rotate": [ + { + "value": -2.33, + "curve": [ 0.019, 4.43, 0.069, 10.82 ] + }, + { + "time": 0.1, + "value": 10.6, + "curve": [ 0.148, 10.6, 0.123, -15.24 ] + }, + { + "time": 0.2, + "value": -15.35, + "curve": [ 0.266, -15.44, 0.316, -6.48 ] + }, + { + "time": 0.3333, + "value": -3.9, + "curve": [ 0.362, 0.43, 0.479, 22.36 ] + }, + { + "time": 0.5667, + "value": 22.01, + "curve": [ 0.61, 21.84, 0.627, 12.85 ] + }, + { + "time": 0.6333, + "value": 9.05, + "curve": [ 0.643, 2.77, 0.622, -39.43 ] + }, + { + "time": 0.7, + "value": -39.5, + "curve": [ 0.773, -39.57, 0.814, 14.77 ] + }, + { + "time": 0.8667, + "value": 14.81, + "curve": [ 0.965, 14.88, 1.1, 5.64 ] + }, + { "time": 1.1, "value": -6.08, "curve": "stepped" }, + { + "time": 2.2333, + "value": -6.08, + "curve": [ 2.307, -6.08, 2.427, -25.89 ] + }, + { + "time": 2.5333, + "value": -22.42, + "curve": [ 2.598, -20.38, 2.657, 5.73 ] + }, + { + "time": 2.7, + "value": 5.73, + "curve": [ 2.77, 5.73, 2.851, -5.38 ] + }, + { + "time": 2.9333, + "value": -5.38, + "curve": [ 3.008, -5.38, 3.087, -4.54 ] + }, + { + "time": 3.1667, + "value": -4.17, + "curve": [ 3.223, -3.91, 4.486, 5.73 ] + }, + { + "time": 4.6667, + "value": 5.73, + "curve": [ 4.733, 5.73, 4.886, -2.47 ] + }, + { "time": 4.9333, "value": -6.52 } + ] + }, + "rear-bracer": { + "rotate": [ + { + "value": 10.36, + "curve": [ 0.033, 10.36, 0.1, -32.89 ] + }, + { + "time": 0.1333, + "value": -32.89, + "curve": [ 0.183, -32.89, 0.283, -4.45 ] + }, + { + "time": 0.3333, + "value": -4.45, + "curve": [ 0.367, -4.45, 0.438, -6.86 ] + }, + { + "time": 0.4667, + "value": -8.99, + "curve": [ 0.529, -13.62, 0.605, -20.58 ] + }, + { + "time": 0.6333, + "value": -23.2, + "curve": [ 0.708, -30.18, 0.758, -35.56 ] + }, + { + "time": 0.8, + "value": -35.56, + "curve": [ 0.875, -35.56, 1.025, -23.2 ] + }, + { "time": 1.1, "value": -23.2 } + ] + }, + "gun": { + "rotate": [ + { + "value": -2.79, + "curve": [ 0.033, -2.79, 0.12, -7.22 ] + }, + { + "time": 0.1333, + "value": -8.52, + "curve": [ 0.168, -11.87, 0.29, -23.71 ] + }, + { + "time": 0.3333, + "value": -26.24, + "curve": [ 0.369, -28.31, 0.436, -29.75 ] + }, + { + "time": 0.5, + "value": -29.66, + "curve": [ 0.552, -29.58, 0.611, -25.47 ] + }, + { + "time": 0.6333, + "value": -22.68, + "curve": [ 0.656, -19.76, 0.68, -10.02 ] + }, + { + "time": 0.7, + "value": -6.49, + "curve": [ 0.722, -2.6, 0.75, -1.22 ] + }, + { + "time": 0.7667, + "value": -1.35, + "curve": [ 0.792, -1.55, 0.842, -19.74 ] + }, + { "time": 0.8667, "value": -19.8 } + ] + }, + "hip": { + "translate": [ + { + "curve": [ 0.098, -42.62, 0.166, -79.85, 0.029, 84.97, 0.109, 155.93 ] + }, + { + "time": 0.2667, + "x": -133.79, + "y": 152.44, + "curve": [ 0.361, -184.63, 0.392, -203.69, 0.42, 149.12, 0.467, -15.7 ] + }, + { + "time": 0.4667, + "x": -230.02, + "y": -113.87, + "curve": [ 0.523, -249.86, 0.565, -261.7, 0.473, -133.1, 0.583, -203.43 ] + }, + { + "time": 0.6, + "x": -268.57, + "y": -203.43, + "curve": [ 0.663, -280.98, 0.816, -290.05, 0.708, -203.43, 0.892, -203.5 ] + }, + { "time": 1, "x": -290.42, "y": -203.5 } + ] + }, + "front-thigh": { + "rotate": [ + { + "curve": [ 0.06, 1.02, 0.151, 45.23 ] + }, + { + "time": 0.1667, + "value": 54.01, + "curve": [ 0.19, 66.85, 0.358, 169.85 ] + }, + { + "time": 0.5, + "value": 169.51, + "curve": [ 0.628, 169.85, 0.692, 108.85 ] + }, + { + "time": 0.7, + "value": 97.74, + "curve": [ 0.723, 102.6, 0.805, 111.6 ] + }, + { + "time": 0.8667, + "value": 111.69, + "curve": [ 0.899, 111.83, 1.015, 109.15 ] + }, + { "time": 1.0667, "value": 95.8 } + ] + }, + "front-shin": { + "rotate": [ + { + "curve": [ 0.086, -0.02, 0.191, -24.25 ] + }, + { + "time": 0.2, + "value": -26.5, + "curve": [ 0.214, -29.92, 0.249, -40.51 ] + }, + { + "time": 0.3333, + "value": -40.57, + "curve": [ 0.431, -40.7, 0.459, -11.34 ] + }, + { + "time": 0.4667, + "value": -8.71, + "curve": [ 0.477, -5.16, 0.524, 17.13 ] + }, + { + "time": 0.6, + "value": 16.98, + "curve": [ 0.632, 17.09, 0.625, 2.76 ] + }, + { + "time": 0.6333, + "value": 2.76, + "curve": [ 0.648, 2.76, 0.653, 2.75 ] + }, + { + "time": 0.6667, + "value": 2.59, + "curve": [ 0.678, 2.39, 0.733, 2.53 ] + }, + { + "time": 0.7333, + "value": -9.43, + "curve": [ 0.745, -2.48, 0.782, 3.12 ] + }, + { + "time": 0.8, + "value": 4.28, + "curve": [ 0.832, 6.32, 0.895, 8.46 ] + }, + { + "time": 0.9333, + "value": 8.49, + "curve": [ 0.986, 8.53, 1.051, 6.38 ] + }, + { + "time": 1.0667, + "value": 2.28, + "curve": [ 1.078, 4.17, 1.103, 5.86 ] + }, + { + "time": 1.1333, + "value": 5.88, + "curve": [ 1.191, 5.93, 1.209, 4.56 ] + }, + { "time": 1.2333, "value": 2.52 } + ] + }, + "rear-thigh": { + "rotate": [ + { + "curve": [ 0.033, 0, 0.12, 50.26 ] + }, + { + "time": 0.1333, + "value": 57.3, + "curve": [ 0.164, 73.34, 0.274, 147.18 ] + }, + { + "time": 0.3333, + "value": 147.1, + "curve": [ 0.475, 146.45, 0.583, 95.72 ] + }, + { + "time": 0.6, + "value": 79.66, + "curve": [ 0.62, 94.74, 0.732, 103.15 ] + }, + { + "time": 0.7667, + "value": 103.02, + "curve": [ 0.812, 102.85, 0.897, 95.75 ] + }, + { "time": 0.9333, "value": 83.01 } + ] + }, + "rear-shin": { + "rotate": [ + { + "curve": [ 0.021, -16.65, 0.091, -54.82 ] + }, + { + "time": 0.1667, + "value": -55.29, + "curve": [ 0.187, -55.42, 0.213, -52.52 ] + }, + { + "time": 0.2333, + "value": -45.98, + "curve": [ 0.242, -43.1, 0.311, -12.73 ] + }, + { + "time": 0.3333, + "value": -6.32, + "curve": [ 0.356, 0.13, 0.467, 24.5 ] + }, + { + "time": 0.5, + "value": 24.5, + "curve": [ 0.543, 24.5, 0.56, 3.78 ] + }, + { + "time": 0.5667, + "value": -3.53, + "curve": [ 0.585, 3.86, 0.659, 16.63 ] + }, + { + "time": 0.7, + "value": 16.56, + "curve": [ 0.782, 16.43, 0.896, 8.44 ] + }, + { + "time": 0.9333, + "value": 4.04, + "curve": [ 0.956, 6.84, 1.008, 8.41 ] + }, + { + "time": 1.0333, + "value": 8.41, + "curve": [ 1.067, 8.41, 1.122, 8.14 ] + }, + { "time": 1.1667, "value": 5.8 } + ] + }, + "rear-foot": { + "rotate": [ + { + "value": -0.28, + "curve": [ 0.033, -0.28, 0.256, -66.71 ] + }, + { + "time": 0.3667, + "value": -66.84, + "curve": [ 0.418, -66.91, 0.499, -21.79 ] + }, + { + "time": 0.6, + "value": -21.52, + "curve": [ 0.652, -21.38, 0.665, -53.96 ] + }, + { + "time": 0.7, + "value": -54.26, + "curve": [ 0.757, -53.96, 0.843, -2.07 ] + }, + { + "time": 0.9333, + "value": -1.47, + "curve": [ 0.968, -2.07, 0.975, -19.96 ] + }, + { + "time": 1, + "value": -19.96, + "curve": [ 1.025, -19.96, 1.075, -12.42 ] + }, + { + "time": 1.1, + "value": -12.42, + "curve": [ 1.133, -12.42, 1.2, -18.34 ] + }, + { "time": 1.2333, "value": -18.34 } + ] + }, + "front-foot": { + "rotate": [ + { + "curve": [ 0.008, -11.33, 0.108, -57.71 ] + }, + { + "time": 0.1333, + "value": -57.71, + "curve": [ 0.175, -57.71, 0.229, 19.73 ] + }, + { + "time": 0.3, + "value": 19.34, + "curve": [ 0.354, 19.34, 0.4, -57.76 ] + }, + { + "time": 0.4333, + "value": -57.76, + "curve": [ 0.458, -57.76, 0.511, -3.56 ] + }, + { + "time": 0.5333, + "value": 3.7, + "curve": [ 0.563, 13.29, 0.633, 15.79 ] + }, + { + "time": 0.6667, + "value": 15.79, + "curve": [ 0.7, 15.79, 0.767, -48.75 ] + }, + { + "time": 0.8, + "value": -48.75, + "curve": [ 0.842, -48.75, 0.925, 4.7 ] + }, + { + "time": 0.9667, + "value": 4.7, + "curve": [ 1, 4.7, 1.067, -22.9 ] + }, + { + "time": 1.1, + "value": -22.9, + "curve": [ 1.142, -22.9, 1.225, -13.28 ] + }, + { "time": 1.2667, "value": -13.28 } + ] + }, + "rear-foot-target": { + "rotate": [ + { "value": -0.28 } + ] + }, + "front-foot-tip": { + "rotate": [ + { + "value": -0.28, + "curve": [ 0.008, -0.28, 0.003, -66.62 ] + }, + { + "time": 0.0667, + "value": -65.75, + "curve": [ 0.166, -64.42, 0.234, 14.35 ] + }, + { + "time": 0.2667, + "value": 38.25, + "curve": [ 0.294, 57.91, 0.392, 89.79 ] + }, + { + "time": 0.4667, + "value": 90.73, + "curve": [ 0.483, 90.73, 0.55, 177.66 ] + }, + { + "time": 0.5667, + "value": 177.66, + "curve": [ 0.733, 176.24, 0.75, 11.35 ] + }, + { + "time": 0.8, + "value": 11.35, + "curve": [ 0.886, 12.29, 0.911, 47.88 ] + }, + { + "time": 0.9333, + "value": 56.77, + "curve": [ 0.967, 70.59, 1.05, 86.46 ] + }, + { + "time": 1.1, + "value": 86.46, + "curve": [ 1.187, 86.46, 1.214, 66.44 ] + }, + { "time": 1.3333, "value": 64.55 } + ] + }, + "back-foot-tip": { + "rotate": [ + { + "value": -0.28, + "curve": [ 0, -7.97, 0.027, -18.69 ] + }, + { + "time": 0.0667, + "value": -19, + "curve": [ 0.166, -19.3, 0.208, 15.58 ] + }, + { + "time": 0.2667, + "value": 45.95, + "curve": [ 0.306, 66.24, 0.378, 99.08 ] + }, + { + "time": 0.4333, + "value": 99.08, + "curve": [ 0.497, 98.62, 0.488, -1.2 ] + }, + { + "time": 0.5667, + "value": -1.32, + "curve": [ 0.637, -0.84, 0.687, 94.41 ] + }, + { + "time": 0.7333, + "value": 94.33, + "curve": [ 0.832, 94.16, 0.895, 29.6 ] + }, + { + "time": 0.9667, + "value": 28.67, + "curve": [ 1.026, 28.67, 1.045, 53.14 ] + }, + { "time": 1.1, "value": 53.38 } + ] + }, + "hair4": { + "rotate": [ + { + "curve": [ 0.011, 4.5, 0.05, 11.42 ] + }, + { + "time": 0.0667, + "value": 11.42, + "curve": [ 0.1, 11.42, 0.136, -5.92 ] + }, + { + "time": 0.1667, + "value": -10.54, + "curve": [ 0.206, -16.51, 0.327, -22 ] + }, + { + "time": 0.3667, + "value": -24.47, + "curve": [ 0.413, -27.37, 0.467, -43.99 ] + }, + { + "time": 0.5, + "value": -43.99, + "curve": [ 0.533, -43.99, 0.552, 12.12 ] + }, + { + "time": 0.6333, + "value": 11.85, + "curve": [ 0.714, 11.59, 0.758, -34.13 ] + }, + { + "time": 0.8, + "value": -34.13, + "curve": [ 0.858, -34.13, 1.015, -12.47 ] + }, + { + "time": 1.0667, + "value": -8.85, + "curve": [ 1.121, -5.07, 1.219, -0.02 ] + }, + { + "time": 1.3333, + "value": 1.29, + "curve": [ 1.509, 3.3, 1.763, 2.75 ] + }, + { + "time": 1.8667, + "value": 2.78, + "curve": [ 1.974, 2.81, 2.108, 2.81 ] + }, + { + "time": 2.2, + "value": 2.78, + "curve": [ 2.315, 2.74, 2.374, 1.22 ] + }, + { + "time": 2.4667, + "value": 1.18, + "curve": [ 2.525, 1.18, 2.608, 10.79 ] + }, + { + "time": 2.6667, + "value": 10.79, + "curve": [ 2.725, 10.79, 2.893, 4.72 ] + }, + { + "time": 3.0333, + "value": 4.72, + "curve": [ 3.117, 4.72, 3.283, 7.93 ] + }, + { + "time": 3.3667, + "value": 7.93, + "curve": [ 3.492, 7.93, 3.775, 6.93 ] + }, + { + "time": 3.9, + "value": 6.93, + "curve": [ 3.981, 6.93, 4.094, 6.9 ] + }, + { + "time": 4.2, + "value": 8.44, + "curve": [ 4.267, 9.42, 4.401, 16.61 ] + }, + { + "time": 4.5, + "value": 16.33, + "curve": [ 4.582, 16.12, 4.709, 9.94 ] + }, + { + "time": 4.7333, + "value": 6.51, + "curve": [ 4.747, 4.57, 4.779, -1.76 ] + }, + { + "time": 4.8, + "value": -1.75, + "curve": [ 4.823, -1.73, 4.82, 4.47 ] + }, + { + "time": 4.8667, + "value": 6.04, + "curve": [ 4.899, 7.14, 4.913, 6.93 ] + }, + { "time": 4.9333, "value": 6.93 } + ] + }, + "hair2": { + "rotate": [ + { + "value": 10.61, + "curve": [ 0.075, 10.61, 0.05, 12.67 ] + }, + { + "time": 0.0667, + "value": 12.67, + "curve": [ 0.123, 12.67, 0.194, -16.51 ] + }, + { + "time": 0.2, + "value": -19.87, + "curve": [ 0.207, -23.48, 0.236, -31.68 ] + }, + { + "time": 0.3, + "value": -31.8, + "curve": [ 0.356, -31.9, 0.437, -25.61 ] + }, + { + "time": 0.4667, + "value": -19.29, + "curve": [ 0.485, -15.33, 0.529, 6.48 ] + }, + { + "time": 0.5667, + "value": 6.67, + "curve": [ 0.628, 6.97, 0.65, -46.39 ] + }, + { + "time": 0.7333, + "value": -46.3, + "curve": [ 0.843, -46.17, 0.941, -33.37 ] + }, + { + "time": 0.9667, + "value": -23.17, + "curve": [ 0.972, -20.98, 1.047, 15.21 ] + }, + { + "time": 1.1, + "value": 15.21, + "curve": [ 1.142, 15.21, 1.183, 10.73 ] + }, + { + "time": 1.2667, + "value": 10.61, + "curve": [ 1.45, 10.34, 1.817, 10.61 ] + }, + { + "time": 2, + "value": 10.61, + "curve": [ 2.075, 10.61, 2.225, 16.9 ] + }, + { + "time": 2.3, + "value": 16.9, + "curve": [ 2.327, 16.9, 2.347, 6.81 ] + }, + { + "time": 2.4, + "value": 6.83, + "curve": [ 2.492, 6.87, 2.602, 17.39 ] + }, + { + "time": 2.6667, + "value": 17.39, + "curve": [ 2.742, 17.39, 2.892, 10.67 ] + }, + { + "time": 2.9667, + "value": 10.64, + "curve": [ 3.187, 10.57, 3.344, 10.73 ] + }, + { + "time": 3.6, + "value": 11.4, + "curve": [ 3.766, 11.83, 3.874, 14.87 ] + }, + { + "time": 3.9333, + "value": 14.83, + "curve": [ 4.022, 14.76, 4.208, 9.49 ] + }, + { + "time": 4.3, + "value": 9.54, + "curve": [ 4.391, 9.58, 4.441, 14.82 ] + }, + { + "time": 4.5333, + "value": 14.84, + "curve": [ 4.642, 14.88, 4.692, 1.17 ] + }, + { + "time": 4.7667, + "value": 1.24, + "curve": [ 4.823, 1.3, 4.818, 18.35 ] + }, + { + "time": 4.8667, + "value": 18.38, + "curve": [ 4.905, 18.41, 4.901, 10.61 ] + }, + { "time": 4.9333, "value": 10.61 } + ] + }, + "torso2": { + "rotate": [ + { + "curve": [ 0.048, 0, 0.129, -12.73 ] + }, + { + "time": 0.1667, + "value": -15.95, + "curve": [ 0.221, -20.66, 0.254, -21.62 ] + }, + { + "time": 0.3, + "value": -21.59, + "curve": [ 0.458, -21.46, 0.46, -1.67 ] + }, + { + "time": 0.6333, + "value": -1.71, + "curve": [ 0.71, -1.73, 0.715, -4 ] + }, + { + "time": 0.7667, + "value": -3.97, + "curve": [ 0.866, -3.92, 0.84, 0.02 ] + }, + { "time": 1, "curve": "stepped" }, + { + "time": 2, + "curve": [ 2.275, 0, 2.867, -5.8 ] + }, + { + "time": 3.1, + "value": -6.44, + "curve": [ 3.327, -7.06, 3.71, -6.23 ] + }, + { + "time": 3.9333, + "value": -5.41, + "curve": [ 4.168, -4.53, 4.488, -2.83 ] + }, + { "time": 4.8 } + ] + }, + "torso3": { + "rotate": [ + { + "curve": [ 0.025, 0, 0.09, -3.66 ] + }, + { + "time": 0.1, + "value": -4.55, + "curve": [ 0.143, -8.4, 0.223, -17.07 ] + }, + { + "time": 0.2333, + "value": -18.31, + "curve": [ 0.282, -24.44, 0.35, -29 ] + }, + { + "time": 0.3667, + "value": -30.07, + "curve": [ 0.405, -32.58, 0.442, -33.03 ] + }, + { + "time": 0.4667, + "value": -32.99, + "curve": [ 0.491, -33.04, 0.505, -23.56 ] + }, + { + "time": 0.5333, + "value": -23.55, + "curve": [ 0.571, -23.67, 0.599, -27.21 ] + }, + { + "time": 0.6333, + "value": -27.21, + "curve": [ 0.669, -27.2, 0.742, -10.43 ] + }, + { + "time": 0.7667, + "value": -7.79, + "curve": [ 0.788, -5.53, 0.796, -4.42 ] + }, + { + "time": 0.8333, + "value": -2.9, + "curve": [ 0.875, -1.21, 0.933, 0 ] + }, + { "time": 0.9667, "curve": "stepped" }, + { + "time": 2.4333, + "curve": [ 2.517, 0, 2.683, 4.63 ] + }, + { + "time": 2.7667, + "value": 4.66, + "curve": [ 3.084, 4.76, 3.248, 4.37 ] + }, + { + "time": 3.4, + "value": 3.74, + "curve": [ 3.596, 2.92, 3.755, 2.18 ] + }, + { + "time": 3.8667, + "value": 1.72, + "curve": [ 4.136, 0.59, 4.471, 0 ] + }, + { "time": 4.8 } + ] + }, + "hair3": { + "rotate": [ + { + "curve": [ 0, 0, 0.041, 10.74 ] + }, + { + "time": 0.0667, + "value": 14.16, + "curve": [ 0.075, 15.22, 0.148, 18.04 ] + }, + { + "time": 0.2, + "value": 18.13, + "curve": [ 0.251, 18.23, 0.307, -4.75 ] + }, + { + "time": 0.3667, + "value": -5.06, + "curve": [ 0.412, -5.3, 0.47, -0.96 ] + }, + { + "time": 0.5, + "value": 2.21, + "curve": [ 0.512, 3.48, 0.595, 20.31 ] + }, + { + "time": 0.6333, + "value": 24.87, + "curve": [ 0.647, 26.53, 0.719, 29.33 ] + }, + { + "time": 0.8, + "value": 29.22, + "curve": [ 0.859, 29.14, 0.9, 28.48 ] + }, + { + "time": 0.9333, + "value": 26.11, + "curve": [ 0.981, 22.72, 0.998, 2.06 ] + }, + { "time": 1.1, "value": 2.21 } + ] + }, + "hair1": { + "rotate": [ + { + "curve": [ 0.047, -0.21, 0.048, 7.86 ] + }, + { + "time": 0.0667, + "value": 13.27, + "curve": [ 0.083, 18.05, 0.135, 24.44 ] + }, + { + "time": 0.2, + "value": 24.02, + "curve": [ 0.225, 24.02, 0.28, 6.32 ] + }, + { + "time": 0.3, + "value": 3.1, + "curve": [ 0.323, -0.58, 0.382, -7.12 ] + }, + { + "time": 0.4667, + "value": -7.45, + "curve": [ 0.512, -7.66, 0.538, 12.13 ] + }, + { + "time": 0.5667, + "value": 16.46, + "curve": [ 0.609, 22.72, 0.672, 27.4 ] + }, + { + "time": 0.7333, + "value": 27.55, + "curve": [ 0.827, 27.4, 0.933, 23.23 ] + }, + { + "time": 0.9667, + "value": 19.11, + "curve": [ 0.998, 15.27, 1.092, -2.53 ] + }, + { + "time": 1.1333, + "value": -2.53, + "curve": [ 1.158, -2.53, 1.208, 0 ] + }, + { "time": 1.2333, "curve": "stepped" }, + { + "time": 2, + "curve": [ 2.075, 0, 2.248, 0.35 ] + }, + { + "time": 2.3333, + "value": 0.78, + "curve": [ 2.585, 2.06, 2.805, 3.46 ] + }, + { + "time": 3.2, + "value": 3.5, + "curve": [ 3.593, 3.54, 3.979, 2.36 ] + }, + { + "time": 4.1667, + "value": 1.55, + "curve": [ 4.391, 0.59, 4.447, 0.04 ] + }, + { + "time": 4.6, + "value": 0.04, + "curve": [ 4.642, 0.04, 4.742, 0 ] + }, + { "time": 4.9333 } + ] + }, + "head-control": { + "translate": [ + { + "curve": [ 0.025, 0, 0.09, 1.43, 0.025, 0, 0.075, -34.76 ] + }, + { + "time": 0.1, + "x": 1.59, + "y": -34.76, + "curve": [ 0.214, 3.33, 0.375, 5.34, 0.192, -34.76, 0.441, -21.17 ] + }, + { + "time": 0.4667, + "x": 5.34, + "y": -12.57, + "curve": [ 0.492, 5.34, 0.55, 5.24, 0.482, -7.36, 0.504, 4.03 ] + }, + { + "time": 0.5667, + "x": 5.11, + "y": 4.01, + "curve": [ 0.658, 4.45, 0.679, 3.19, 0.649, 3.98, 0.642, -16.84 ] + }, + { + "time": 0.7, + "x": 2.8, + "y": -16.74, + "curve": [ 0.787, 1.15, 0.881, -1.29, 0.772, -16.62, 0.82, 8.95 ] + }, + { + "time": 0.9, + "x": -1.72, + "y": 8.91, + "curve": [ 0.961, -3.06, 1.025, -3.58, 0.975, 8.87, 0.951, -1.37 ] + }, + { + "time": 1.1, + "x": -3.58, + "y": -1.45, + "curve": [ 1.292, -3.58, 2.002, -2.4, 1.292, -1.56, 1.975, -1.45 ] + }, + { + "time": 2.1667, + "x": -1.39, + "y": -1.45, + "curve": [ 2.25, -0.88, 2.503, 1.38, 2.283, -1.45, 2.603, -12.44 ] + }, + { + "time": 2.6667, + "x": 2.13, + "y": -14.45, + "curve": [ 2.766, 2.59, 2.999, 2.81, 2.835, -19.73, 3.003, -25.2 ] + }, + { + "time": 3.1333, + "x": 2.91, + "y": -26.08, + "curve": [ 3.392, 3.1, 4.199, 4.05, 3.483, -28.44, 4.129, -27.23 ] + }, + { + "time": 4.3667, + "x": 4.81, + "y": -19.59, + "curve": [ 4.429, 5.1, 4.594, 8.54, 4.538, -14.08, 4.583, -7.88 ] + }, + { + "time": 4.6667, + "x": 8.65, + "y": -4.56, + "curve": [ 4.794, 8.86, 4.806, 5.93, 4.691, -3.59, 4.8, -1.61 ] + }, + { "time": 4.9333, "x": 5.8, "y": -1.99 } + ] + } + }, + "ik": { + "front-foot-ik": [ + { "mix": 0 } + ], + "front-leg-ik": [ + { "mix": 0, "bendPositive": false } + ], + "rear-foot-ik": [ + { "mix": 0.005 } + ], + "rear-leg-ik": [ + { "mix": 0.005, "bendPositive": false } + ] + } + }, + "hoverboard": { + "slots": { + "exhaust1": { + "attachment": [ + { "name": "hoverglow-small" } + ] + }, + "exhaust2": { + "attachment": [ + { "name": "hoverglow-small" } + ] + }, + "exhaust3": { + "attachment": [ + { "name": "hoverglow-small" } + ] + }, + "front-fist": { + "attachment": [ + { "name": "front-fist-open" } + ] + }, + "hoverboard-board": { + "attachment": [ + { "name": "hoverboard-board" } + ] + }, + "hoverboard-thruster-front": { + "attachment": [ + { "name": "hoverboard-thruster" } + ] + }, + "hoverboard-thruster-rear": { + "attachment": [ + { "name": "hoverboard-thruster" } + ] + }, + "hoverglow-front": { + "attachment": [ + { "name": "hoverglow-small" } + ] + }, + "hoverglow-rear": { + "attachment": [ + { "name": "hoverglow-small" } + ] + }, + "side-glow1": { + "attachment": [ + { "name": "hoverglow-small" }, + { "time": 0.9667 } + ] + }, + "side-glow2": { + "attachment": [ + { "time": 0.0667, "name": "hoverglow-small" }, + { "time": 1 } + ] + }, + "side-glow3": { + "attachment": [ + { "name": "hoverglow-small" }, + { "time": 0.9667 } + ] + } + }, + "bones": { + "hoverboard-controller": { + "translate": [ + { + "x": 319.55, + "y": -1.59, + "curve": [ 0.064, 319.55, 0.2, 347.85, 0.058, -1.2, 0.2, 23.11 ] + }, + { + "time": 0.2667, + "x": 347.66, + "y": 39.62, + "curve": [ 0.35, 347.41, 0.476, 341.47, 0.323, 53.58, 0.44, 85.82 ] + }, + { + "time": 0.5333, + "x": 338.47, + "y": 85.72, + "curve": [ 0.603, 334.83, 0.913, 319.65, 0.621, 85.62, 0.88, -1.53 ] + }, + { "time": 1, "x": 319.55, "y": -1.59 } + ] + }, + "hip": { + "translate": [ + { + "x": -53.49, + "y": 32.14, + "curve": [ 0.061, -53.77, 0.093, -51.81, 0.044, 16.34, 0.063, 9.67 ] + }, + { + "time": 0.1333, + "x": -49.31, + "y": 7.01, + "curve": [ 0.3, -35.27, 0.461, -20.06, 0.314, 9.52, 0.408, 121.09 ] + }, + { + "time": 0.5667, + "x": -20.06, + "y": 122.72, + "curve": [ 0.716, -20.09, 0.912, -53.29, 0.753, 121.8, 0.946, 51.85 ] + }, + { "time": 1, "x": -53.49, "y": 32.14 } + ] + }, + "exhaust1": { + "scale": [ + { + "x": 1.593, + "y": 0.964, + "curve": [ 0.033, 1.593, 0.1, 1, 0.033, 0.964, 0.1, 0.713 ] + }, + { + "time": 0.1333, + "y": 0.713, + "curve": [ 0.15, 1, 0.183, 1.774, 0.15, 0.713, 0.183, 0.883 ] + }, + { + "time": 0.2, + "x": 1.774, + "y": 0.883, + "curve": [ 0.242, 1.774, 0.325, 1.181, 0.242, 0.883, 0.325, 0.649 ] + }, + { + "time": 0.3667, + "x": 1.181, + "y": 0.649, + "curve": [ 0.408, 1.181, 0.492, 1.893, 0.408, 0.649, 0.492, 0.819 ] + }, + { + "time": 0.5333, + "x": 1.893, + "y": 0.819, + "curve": [ 0.558, 1.893, 0.608, 1.18, 0.558, 0.819, 0.608, 0.686 ] + }, + { + "time": 0.6333, + "x": 1.18, + "y": 0.686, + "curve": [ 0.658, 1.18, 0.708, 1.903, 0.658, 0.686, 0.708, 0.855 ] + }, + { + "time": 0.7333, + "x": 1.903, + "y": 0.855, + "curve": [ 0.767, 1.903, 0.833, 1.311, 0.767, 0.855, 0.833, 0.622 ] + }, + { + "time": 0.8667, + "x": 1.311, + "y": 0.622, + "curve": [ 0.9, 1.311, 0.967, 1.593, 0.9, 0.622, 0.967, 0.964 ] + }, + { "time": 1, "x": 1.593, "y": 0.964 } + ] + }, + "exhaust2": { + "scale": [ + { + "x": 1.88, + "y": 0.832, + "curve": [ 0.025, 1.88, 0.075, 1.311, 0.025, 0.832, 0.075, 0.686 ] + }, + { + "time": 0.1, + "x": 1.311, + "y": 0.686, + "curve": [ 0.133, 1.311, 0.2, 2.01, 0.133, 0.686, 0.208, 0.736 ] + }, + { + "time": 0.2333, + "x": 2.01, + "y": 0.769, + "curve": [ 0.267, 2.01, 0.333, 1, 0.282, 0.831, 0.333, 0.91 ] + }, + { + "time": 0.3667, + "y": 0.91, + "curve": [ 0.4, 1, 0.467, 1.699, 0.4, 0.91, 0.474, 0.891 ] + }, + { + "time": 0.5, + "x": 1.699, + "y": 0.86, + "curve": [ 0.517, 1.699, 0.55, 1.181, 0.54, 0.813, 0.55, 0.713 ] + }, + { + "time": 0.5667, + "x": 1.181, + "y": 0.713, + "curve": [ 0.617, 1.181, 0.717, 1.881, 0.617, 0.713, 0.717, 0.796 ] + }, + { + "time": 0.7667, + "x": 1.881, + "y": 0.796, + "curve": [ 0.8, 1.881, 0.867, 1.3, 0.8, 0.796, 0.867, 0.649 ] + }, + { + "time": 0.9, + "x": 1.3, + "y": 0.649, + "curve": [ 0.925, 1.3, 0.975, 1.88, 0.925, 0.649, 0.975, 0.832 ] + }, + { "time": 1, "x": 1.88, "y": 0.832 } + ] + }, + "hoverboard-thruster-front": { + "rotate": [ + { + "curve": [ 0.125, 0, 0.375, 24.06 ] + }, + { + "time": 0.5, + "value": 24.06, + "curve": [ 0.625, 24.06, 0.875, 0 ] + }, + { "time": 1 } + ] + }, + "hoverglow-front": { + "scale": [ + { + "x": 0.849, + "y": 1.764, + "curve": [ 0.017, 0.849, 0.05, 0.835, 0.017, 1.764, 0.05, 2.033 ] + }, + { + "time": 0.0667, + "x": 0.835, + "y": 2.033, + "curve": [ 0.092, 0.835, 0.142, 0.752, 0.092, 2.033, 0.142, 1.584 ] + }, + { + "time": 0.1667, + "x": 0.752, + "y": 1.584, + "curve": [ 0.183, 0.752, 0.217, 0.809, 0.183, 1.584, 0.217, 1.71 ] + }, + { + "time": 0.2333, + "x": 0.809, + "y": 1.71, + "curve": [ 0.25, 0.809, 0.283, 0.717, 0.25, 1.71, 0.283, 1.45 ] + }, + { + "time": 0.3, + "x": 0.717, + "y": 1.45, + "curve": [ 0.317, 0.717, 0.35, 0.777, 0.317, 1.45, 0.35, 1.698 ] + }, + { + "time": 0.3667, + "x": 0.777, + "y": 1.698, + "curve": [ 0.4, 0.781, 0.45, 0.685, 0.375, 1.698, 0.45, 1.173 ] + }, + { + "time": 0.4667, + "x": 0.685, + "y": 1.173, + "curve": [ 0.492, 0.685, 0.542, 0.825, 0.492, 1.173, 0.542, 1.572 ] + }, + { + "time": 0.5667, + "x": 0.825, + "y": 1.572, + "curve": [ 0.611, 0.816, 0.63, 0.727, 0.611, 1.577, 0.606, 1.255 ] + }, + { + "time": 0.6667, + "x": 0.725, + "y": 1.241, + "curve": [ 0.692, 0.725, 0.742, 0.895, 0.692, 1.241, 0.749, 1.799 ] + }, + { + "time": 0.7667, + "x": 0.895, + "y": 1.857, + "curve": [ 0.783, 0.895, 0.796, 0.892, 0.796, 1.955, 0.817, 1.962 ] + }, + { + "time": 0.8333, + "x": 0.845, + "y": 1.962, + "curve": [ 0.845, 0.831, 0.883, 0.802, 0.85, 1.962, 0.872, 1.704 ] + }, + { + "time": 0.9, + "x": 0.802, + "y": 1.491, + "curve": [ 0.917, 0.802, 0.95, 0.845, 0.907, 1.441, 0.936, 1.508 ] + }, + { + "time": 0.9667, + "x": 0.845, + "y": 1.627, + "curve": [ 0.975, 0.845, 0.992, 0.849, 0.973, 1.652, 0.992, 1.764 ] + }, + { "time": 1, "x": 0.849, "y": 1.764 } + ] + }, + "hoverboard-thruster-rear": { + "rotate": [ + { + "curve": [ 0.125, 0, 0.375, 24.06 ] + }, + { + "time": 0.5, + "value": 24.06, + "curve": [ 0.625, 24.06, 0.875, 0 ] + }, + { "time": 1 } + ] + }, + "hoverglow-rear": { + "scale": [ + { + "x": 0.845, + "y": 1.31, + "curve": [ 0.017, 0.845, 0.117, 0.899, 0.017, 1.31, 0.117, 2.033 ] + }, + { + "time": 0.1333, + "x": 0.899, + "y": 2.033, + "curve": [ 0.15, 0.899, 0.183, 0.752, 0.15, 2.033, 0.183, 1.574 ] + }, + { + "time": 0.2, + "x": 0.752, + "y": 1.574, + "curve": [ 0.225, 0.752, 0.275, 0.809, 0.225, 1.574, 0.275, 1.71 ] + }, + { + "time": 0.3, + "x": 0.809, + "y": 1.71, + "curve": [ 0.317, 0.809, 0.35, 0.717, 0.317, 1.71, 0.35, 1.397 ] + }, + { + "time": 0.3667, + "x": 0.717, + "y": 1.397, + "curve": [ 0.383, 0.717, 0.417, 0.777, 0.383, 1.397, 0.417, 1.45 ] + }, + { + "time": 0.4333, + "x": 0.777, + "y": 1.45, + "curve": [ 0.45, 0.777, 0.496, 0.689, 0.45, 1.45, 0.481, 1.168 ] + }, + { + "time": 0.5333, + "x": 0.685, + "y": 1.173, + "curve": [ 0.565, 0.682, 0.617, 0.758, 0.575, 1.177, 0.617, 1.297 ] + }, + { + "time": 0.6333, + "x": 0.758, + "y": 1.297, + "curve": [ 0.658, 0.758, 0.708, 0.725, 0.658, 1.297, 0.708, 1.241 ] + }, + { + "time": 0.7333, + "x": 0.725, + "y": 1.241, + "curve": [ 0.772, 0.732, 0.796, 0.893, 0.782, 1.238, 0.778, 1.854 ] + }, + { + "time": 0.8333, + "x": 0.895, + "y": 1.857, + "curve": [ 0.878, 0.9, 0.992, 0.845, 0.88, 1.86, 0.992, 1.31 ] + }, + { "time": 1, "x": 0.845, "y": 1.31 } + ] + }, + "front-upper-arm": { + "rotate": [ + { + "value": -85.92, + "curve": [ 0.08, -85.59, 0.284, -62.7 ] + }, + { + "time": 0.3667, + "value": -55.14, + "curve": [ 0.438, -48.65, 0.551, -43.21 ] + }, + { + "time": 0.6333, + "value": -43.21, + "curve": [ 0.716, -43.22, 0.908, -85.92 ] + }, + { "time": 1, "value": -85.92 } + ], + "translate": [ + { + "x": -0.59, + "y": -2.94, + "curve": [ 0.1, -1.21, 0.275, -1.74, 0.092, -2.94, 0.275, -6.39 ] + }, + { + "time": 0.3667, + "x": -1.74, + "y": -6.39, + "curve": [ 0.433, -1.74, 0.567, 0.72, 0.433, -6.39, 0.587, -4.48 ] + }, + { + "time": 0.6333, + "x": 0.72, + "y": -4.21, + "curve": [ 0.725, 0.72, 0.908, -0.08, 0.743, -3.57, 0.908, -2.94 ] + }, + { "time": 1, "x": -0.59, "y": -2.94 } + ] + }, + "front-fist": { + "rotate": [ + { + "value": 7.61, + "curve": [ 0.143, 7.62, 0.247, -23.17 ] + }, + { + "time": 0.2667, + "value": -26.56, + "curve": [ 0.281, -29.08, 0.351, -37.36 ] + }, + { + "time": 0.4333, + "value": -37.2, + "curve": [ 0.513, -37.05, 0.562, -29.88 ] + }, + { + "time": 0.6, + "value": -25.18, + "curve": [ 0.621, -22.58, 0.694, -3.98 ] + }, + { + "time": 0.8, + "value": 3.63, + "curve": [ 0.861, 8.03, 0.946, 7.57 ] + }, + { "time": 1, "value": 7.61 } + ], + "translate": [ + { + "curve": [ 0.117, 0, 0.35, 0.52, 0.117, 0, 0.35, -3.27 ] + }, + { + "time": 0.4667, + "x": 0.52, + "y": -3.27, + "curve": [ 0.6, 0.52, 0.867, 0, 0.6, -3.27, 0.867, 0 ] + }, + { "time": 1 } + ], + "shear": [ + { + "y": 19.83, + "curve": [ 0.117, 0, 0.35, 15.28, 0.117, 19.83, 0.35, 28.31 ] + }, + { + "time": 0.4667, + "x": 15.28, + "y": 28.31, + "curve": [ 0.6, 15.28, 0.867, 0, 0.6, 28.31, 0.867, 19.83 ] + }, + { "time": 1, "y": 19.83 } + ] + }, + "board-ik": { + "translate": [ + { + "x": 393.62, + "curve": [ 0.083, 393.62, 0.25, 393.48, 0.083, 0, 0.25, 117.69 ] + }, + { + "time": 0.3333, + "x": 393.48, + "y": 117.69, + "curve": [ 0.375, 393.48, 0.458, 393.62, 0.375, 117.69, 0.458, 83.82 ] + }, + { "time": 0.5, "x": 393.62, "y": 83.82 }, + { "time": 0.6667, "x": 393.62, "y": 30.15 }, + { "time": 1, "x": 393.62 } + ] + }, + "front-thigh": { + "translate": [ + { "x": -7.49, "y": 8.51 } + ] + }, + "front-leg-target": { + "translate": [ + { + "time": 0.3667, + "curve": [ 0.428, 10.83, 0.567, 12.78, 0.414, 7.29, 0.567, 8.79 ] + }, + { + "time": 0.6, + "x": 12.78, + "y": 8.79, + "curve": [ 0.692, 12.78, 0.772, 11.27, 0.692, 8.79, 0.766, 8.62 ] + }, + { "time": 0.8667 } + ] + }, + "rear-leg-target": { + "translate": [ + { + "time": 0.4667, + "curve": [ 0.492, 0, 0.534, 4.47, 0.492, 0, 0.542, 1.63 ] + }, + { + "time": 0.5667, + "x": 4.53, + "y": 1.77, + "curve": [ 0.622, 4.64, 0.717, 3.31, 0.615, 2.06, 0.71, 2.1 ] + }, + { "time": 0.8 } + ] + }, + "exhaust3": { + "scale": [ + { + "x": 1.882, + "y": 0.81, + "curve": [ 0.017, 1.882, 0.167, 1.3, 0.017, 0.81, 0.167, 0.649 ] + }, + { + "time": 0.2, + "x": 1.3, + "y": 0.649, + "curve": [ 0.225, 1.3, 0.275, 2.051, 0.225, 0.649, 0.275, 0.984 ] + }, + { + "time": 0.3, + "x": 2.051, + "y": 0.984, + "curve": [ 0.325, 2.051, 0.375, 1.311, 0.325, 0.984, 0.384, 0.715 ] + }, + { + "time": 0.4, + "x": 1.311, + "y": 0.686, + "curve": [ 0.433, 1.311, 0.5, 1.86, 0.426, 0.638, 0.5, 0.537 ] + }, + { + "time": 0.5333, + "x": 1.86, + "y": 0.537, + "curve": [ 0.567, 1.86, 0.633, 1.187, 0.567, 0.537, 0.604, 0.854 ] + }, + { + "time": 0.6667, + "x": 1.187, + "y": 0.854, + "curve": [ 0.7, 1.187, 0.767, 1.549, 0.707, 0.854, 0.774, 0.775 ] + }, + { + "time": 0.8, + "x": 1.549, + "y": 0.746, + "curve": [ 0.817, 1.549, 0.85, 1.181, 0.815, 0.729, 0.85, 0.713 ] + }, + { + "time": 0.8667, + "x": 1.181, + "y": 0.713, + "curve": [ 0.9, 1.181, 0.967, 1.882, 0.9, 0.713, 0.967, 0.81 ] + }, + { "time": 1, "x": 1.882, "y": 0.81 } + ] + }, + "side-glow1": { + "rotate": [ + { "value": 51.12, "curve": "stepped" }, + { "time": 0.0667, "value": 43.82, "curve": "stepped" }, + { "time": 0.1, "value": 40.95, "curve": "stepped" }, + { "time": 0.1667, "value": 27.78, "curve": "stepped" }, + { "time": 0.2, "value": 10.24, "curve": "stepped" }, + { "time": 0.2667, "curve": "stepped" }, + { "time": 0.8, "value": -25.81 } + ], + "translate": [ + { "x": 338.28, "y": 40.22, "curve": "stepped" }, + { "time": 0.0667, "x": 331.2, "y": 30.39, "curve": "stepped" }, + { "time": 0.1, "x": 318.63, "y": 20.59, "curve": "stepped" }, + { "time": 0.1667, "x": 302.45, "y": 9.64, "curve": "stepped" }, + { "time": 0.2, "x": 276.87, "y": 1.13, "curve": "stepped" }, + { "time": 0.2667, "x": 248.16, "curve": "stepped" }, + { "time": 0.3, "x": 221.36, "curve": "stepped" }, + { "time": 0.3667, "x": 195.69, "curve": "stepped" }, + { "time": 0.4, "x": 171.08, "curve": "stepped" }, + { "time": 0.4667, "x": 144.84, "curve": "stepped" }, + { "time": 0.5, "x": 121.22, "curve": "stepped" }, + { "time": 0.5667, "x": 91.98, "curve": "stepped" }, + { "time": 0.6, "x": 62.63, "curve": "stepped" }, + { "time": 0.6667, "x": 30.78, "curve": "stepped" }, + { "time": 0.7, "curve": "stepped" }, + { "time": 0.7667, "x": -28.45, "curve": "stepped" }, + { "time": 0.8, "x": -67.49, "y": 16.82, "curve": "stepped" }, + { "time": 0.8667, "x": -83.07, "y": 24.36, "curve": "stepped" }, + { "time": 0.9, "x": -93.81, "y": 29.55 } + ], + "scale": [ + { "x": 0.535, "curve": "stepped" }, + { "time": 0.0667, "x": 0.594, "curve": "stepped" }, + { "time": 0.1, "x": 0.844, "curve": "stepped" }, + { "time": 0.1667, "curve": "stepped" }, + { "time": 0.8, "x": 0.534, "curve": "stepped" }, + { "time": 0.8667, "x": 0.428, "y": 0.801, "curve": "stepped" }, + { "time": 0.9, "x": 0.349, "y": 0.654 } + ] + }, + "side-glow2": { + "rotate": [ + { "time": 0.0667, "value": 51.12, "curve": "stepped" }, + { "time": 0.1, "value": 43.82, "curve": "stepped" }, + { "time": 0.1667, "value": 40.95, "curve": "stepped" }, + { "time": 0.2, "value": 27.78, "curve": "stepped" }, + { "time": 0.2667, "value": 10.24, "curve": "stepped" }, + { "time": 0.3, "curve": "stepped" }, + { "time": 0.8667, "value": -25.81 } + ], + "translate": [ + { "time": 0.0667, "x": 338.28, "y": 40.22, "curve": "stepped" }, + { "time": 0.1, "x": 331.2, "y": 30.39, "curve": "stepped" }, + { "time": 0.1667, "x": 318.63, "y": 20.59, "curve": "stepped" }, + { "time": 0.2, "x": 302.45, "y": 9.64, "curve": "stepped" }, + { "time": 0.2667, "x": 276.87, "y": 1.13, "curve": "stepped" }, + { "time": 0.3, "x": 248.16, "curve": "stepped" }, + { "time": 0.3667, "x": 221.36, "curve": "stepped" }, + { "time": 0.4, "x": 195.69, "curve": "stepped" }, + { "time": 0.4667, "x": 171.08, "curve": "stepped" }, + { "time": 0.5, "x": 144.84, "curve": "stepped" }, + { "time": 0.5667, "x": 121.22, "curve": "stepped" }, + { "time": 0.6, "x": 91.98, "curve": "stepped" }, + { "time": 0.6667, "x": 62.63, "curve": "stepped" }, + { "time": 0.7, "x": 30.78, "curve": "stepped" }, + { "time": 0.7667, "curve": "stepped" }, + { "time": 0.8, "x": -28.45, "curve": "stepped" }, + { "time": 0.8667, "x": -67.49, "y": 16.82, "curve": "stepped" }, + { "time": 0.9, "x": -83.07, "y": 24.36, "curve": "stepped" }, + { "time": 0.9667, "x": -93.81, "y": 29.55 } + ], + "scale": [ + { "time": 0.0667, "x": 0.535, "curve": "stepped" }, + { "time": 0.1, "x": 0.594, "curve": "stepped" }, + { "time": 0.1667, "x": 0.844, "curve": "stepped" }, + { "time": 0.2, "curve": "stepped" }, + { "time": 0.8667, "x": 0.534, "curve": "stepped" }, + { "time": 0.9, "x": 0.428, "y": 0.801, "curve": "stepped" }, + { "time": 0.9667, "x": 0.349, "y": 0.654 } + ] + }, + "torso": { + "rotate": [ + { + "value": -34.73, + "curve": [ 0.034, -36.31, 0.162, -39.33 ] + }, + { + "time": 0.2667, + "value": -39.37, + "curve": [ 0.384, -39.37, 0.491, -29.52 ] + }, + { + "time": 0.5, + "value": -28.86, + "curve": [ 0.525, -26.95, 0.571, -21.01 ] + }, + { + "time": 0.6333, + "value": -21.01, + "curve": [ 0.725, -21.01, 0.969, -33.35 ] + }, + { "time": 1, "value": -34.73 } + ] + }, + "neck": { + "rotate": [ + { + "value": 10.2, + "curve": [ 0.07, 12.09, 0.189, 16.03 ] + }, + { + "time": 0.2667, + "value": 16.14, + "curve": [ 0.333, 16.14, 0.449, 8.03 ] + }, + { + "time": 0.5, + "value": 5.83, + "curve": [ 0.542, 4.02, 0.6, 2.68 ] + }, + { + "time": 0.6333, + "value": 2.68, + "curve": [ 0.725, 2.68, 0.943, 8.57 ] + }, + { "time": 1, "value": 10.2 } + ] + }, + "head": { + "rotate": [ + { + "value": 10.2, + "curve": [ 0.044, 11.52, 0.2, 16.12 ] + }, + { + "time": 0.2667, + "value": 16.14, + "curve": [ 0.375, 16.17, 0.492, 2.65 ] + }, + { + "time": 0.6333, + "value": 2.68, + "curve": [ 0.725, 2.7, 0.963, 9.26 ] + }, + { "time": 1, "value": 10.2 } + ], + "translate": [ + { + "curve": [ 0.03, -0.24, 0.2, -4.22, 0.051, -1.06, 0.2, -3.62 ] + }, + { + "time": 0.2667, + "x": -4.22, + "y": -3.62, + "curve": [ 0.358, -4.22, 0.542, 0.84, 0.358, -3.62, 0.542, 6.01 ] + }, + { + "time": 0.6333, + "x": 0.84, + "y": 6.01, + "curve": [ 0.725, 0.84, 0.939, 0.32, 0.725, 6.01, 0.945, 1.14 ] + }, + { "time": 1 } + ] + }, + "front-bracer": { + "rotate": [ + { + "value": -11.18, + "curve": [ 0.064, -14.82, 0.25, -20.01 ] + }, + { + "time": 0.3333, + "value": -20.01, + "curve": [ 0.429, -20.12, 0.58, 5.12 ] + }, + { + "time": 0.6, + "value": 8.67, + "curve": [ 0.617, 11.72, 0.687, 20.52 ] + }, + { + "time": 0.7667, + "value": 20.55, + "curve": [ 0.848, 20.7, 0.963, -9.43 ] + }, + { "time": 1, "value": -11.18 } + ] + }, + "hair3": { + "rotate": [ + { + "value": 9.61, + "curve": [ 0.014, 8.51, 0.075, 2.63 ] + }, + { + "time": 0.1, + "value": 2.63, + "curve": [ 0.15, 2.63, 0.25, 13.52 ] + }, + { + "time": 0.3, + "value": 13.52, + "curve": [ 0.35, 13.52, 0.45, 11.28 ] + }, + { + "time": 0.5, + "value": 11.28, + "curve": [ 0.575, 11.28, 0.725, 18.13 ] + }, + { + "time": 0.8, + "value": 18.13, + "curve": [ 0.85, 18.13, 0.978, 11.07 ] + }, + { "time": 1, "value": 9.61 } + ] + }, + "hair4": { + "rotate": [ + { + "value": -17.7, + "curve": [ 0.008, -17.7, 0.025, -23.73 ] + }, + { + "time": 0.0333, + "value": -23.73, + "curve": [ 0.067, -23.73, 0.154, -4.4 ] + }, + { + "time": 0.1667, + "value": -1.92, + "curve": [ 0.197, 4.09, 0.236, 12.91 ] + }, + { + "time": 0.2667, + "value": 17.56, + "curve": [ 0.301, 22.68, 0.342, 27.97 ] + }, + { + "time": 0.3667, + "value": 27.97, + "curve": [ 0.4, 27.97, 0.467, -1.45 ] + }, + { + "time": 0.5, + "value": -1.45, + "curve": [ 0.517, -1.45, 0.55, 3.16 ] + }, + { + "time": 0.5667, + "value": 3.16, + "curve": [ 0.583, 3.16, 0.617, -8.9 ] + }, + { + "time": 0.6333, + "value": -8.9, + "curve": [ 0.642, -8.9, 0.658, -5.4 ] + }, + { + "time": 0.6667, + "value": -5.4, + "curve": [ 0.683, -5.4, 0.717, -15.32 ] + }, + { + "time": 0.7333, + "value": -15.32, + "curve": [ 0.75, -15.32, 0.783, -9.19 ] + }, + { + "time": 0.8, + "value": -9.19, + "curve": [ 0.817, -9.19, 0.85, -23.6 ] + }, + { + "time": 0.8667, + "value": -23.6, + "curve": [ 0.883, -23.6, 0.917, -17.38 ] + }, + { + "time": 0.9333, + "value": -17.38, + "curve": [ 0.942, -17.38, 0.958, -20.46 ] + }, + { + "time": 0.9667, + "value": -20.46, + "curve": [ 0.975, -20.46, 0.992, -17.7 ] + }, + { "time": 1, "value": -17.7 } + ] + }, + "hair1": { + "rotate": [ + { + "value": 9.61, + "curve": [ 0.06, 9.04, 0.25, 8.9 ] + }, + { + "time": 0.3333, + "value": 8.9, + "curve": [ 0.392, 8.9, 0.508, 14.58 ] + }, + { + "time": 0.5667, + "value": 14.58, + "curve": [ 0.675, 14.58, 0.956, 10.28 ] + }, + { "time": 1, "value": 9.61 } + ] + }, + "hair2": { + "rotate": [ + { + "value": -3.82, + "curve": [ 0.017, -3.82, 0.064, -9.16 ] + }, + { + "time": 0.1333, + "value": -9.09, + "curve": [ 0.178, -9.04, 0.234, 1.29 ] + }, + { + "time": 0.2667, + "value": 5.98, + "curve": [ 0.276, 7.27, 0.336, 17.1 ] + }, + { + "time": 0.3667, + "value": 17.1, + "curve": [ 0.413, 17.1, 0.467, 1.59 ] + }, + { + "time": 0.5, + "value": 1.59, + "curve": [ 0.533, 1.59, 0.567, 13.63 ] + }, + { + "time": 0.6, + "value": 13.63, + "curve": [ 0.617, 13.63, 0.683, 0.78 ] + }, + { + "time": 0.7, + "value": 0.78, + "curve": [ 0.717, 0.78, 0.75, 12.01 ] + }, + { + "time": 0.7667, + "value": 11.9, + "curve": [ 0.792, 11.73, 0.817, -0.85 ] + }, + { + "time": 0.8333, + "value": -0.85, + "curve": [ 0.85, -0.85, 0.88, 1.99 ] + }, + { + "time": 0.9, + "value": 1.82, + "curve": [ 0.916, 1.68, 0.95, -6.9 ] + }, + { + "time": 0.9667, + "value": -6.9, + "curve": [ 0.975, -6.9, 0.992, -3.82 ] + }, + { "time": 1, "value": -3.82 } + ] + }, + "rear-upper-arm": { + "rotate": [ + { + "value": 31.65, + "curve": [ 0.108, 31.65, 0.325, 13.01 ] + }, + { + "time": 0.4333, + "value": 13.01, + "curve": [ 0.71, 13.01, 0.917, 31.65 ] + }, + { "time": 1, "value": 31.65 } + ] + }, + "rear-bracer": { + "rotate": [ + { + "value": 31, + "curve": [ 0.108, 31, 0.325, 12.76 ] + }, + { + "time": 0.4333, + "value": 12.79, + "curve": [ 0.587, 12.82, 0.917, 31 ] + }, + { "time": 1, "value": 31 } + ] + }, + "gun": { + "rotate": [ + { + "value": 1.95, + "curve": [ 0.083, 1.95, 0.245, 36.73 ] + }, + { + "time": 0.3333, + "value": 36.71, + "curve": [ 0.439, 36.69, 0.589, 10.68 ] + }, + { + "time": 0.6333, + "value": 8.75, + "curve": [ 0.701, 5.81, 0.917, 1.95 ] + }, + { "time": 1, "value": 1.95 } + ] + }, + "torso2": { + "rotate": [ + { + "curve": [ 0.033, 0, 0.1, 2.35 ] + }, + { + "time": 0.1333, + "value": 2.35, + "curve": [ 0.225, 2.35, 0.408, -2.4 ] + }, + { + "time": 0.5, + "value": -2.4, + "curve": [ 0.567, -2.4, 0.7, 1.44 ] + }, + { + "time": 0.7667, + "value": 1.44, + "curve": [ 0.825, 1.44, 0.942, 0 ] + }, + { "time": 1 } + ] + }, + "torso3": { + "rotate": [ + { + "curve": [ 0.063, 0.77, 0.106, 1.42 ] + }, + { + "time": 0.1667, + "value": 1.42, + "curve": [ 0.259, 1.42, 0.344, -1.25 ] + }, + { + "time": 0.4667, + "value": -1.26, + "curve": [ 0.656, -1.26, 0.917, -0.78 ] + }, + { "time": 1 } + ] + }, + "head-control": { + "translate": [ + { + "x": 0.37, + "y": -11.17, + "curve": [ 0.133, 0.37, 0.335, -10.23, 0.133, -11.17, 0.335, 3.15 ] + }, + { + "time": 0.5333, + "x": -10.23, + "y": 3.15, + "curve": [ 0.71, -10.23, 0.883, 0.37, 0.71, 3.15, 0.883, -11.17 ] + }, + { "time": 1, "x": 0.37, "y": -11.17 } + ] + }, + "front-shoulder": { + "translate": [ + { + "x": 1.46, + "y": 10.15, + "curve": [ 0.103, 1.46, 0.249, 1.36, 0.103, 10.15, 0.249, -4.39 ] + }, + { + "time": 0.4, + "x": 1.36, + "y": -4.39, + "curve": [ 0.621, 1.36, 0.85, 1.46, 0.621, -4.39, 0.85, 10.15 ] + }, + { "time": 1, "x": 1.46, "y": 10.15 } + ] + }, + "back-shoulder": { + "translate": [ + { + "x": 1.4, + "y": 0.44, + "curve": [ 0.088, 1.4, 0.208, -2.47, 0.088, 0.44, 0.208, 8.61 ] + }, + { + "time": 0.3333, + "x": -2.47, + "y": 8.61, + "curve": [ 0.572, -2.47, 0.833, 1.4, 0.572, 8.61, 0.833, 0.44 ] + }, + { "time": 1, "x": 1.4, "y": 0.44 } + ] + } + }, + "transform": { + "front-foot-board-transform": [ + { "mixRotate": 0.997 } + ], + "rear-foot-board-transform": [ + {} + ], + "toes-board": [ + { "mixX": 0, "mixScaleX": 0, "mixShearY": 0 } + ] + }, + "attachments": { + "default": { + "front-foot": { + "front-foot": { + "deform": [ + { + "offset": 26, + "vertices": [ -0.02832, -5.37024, -0.02832, -5.37024, 3.8188, -3.7757, -0.02832, -5.37024, -3.82159, 3.77847 ] + } + ] + } + }, + "front-shin": { + "front-shin": { + "deform": [ + { + "offset": 14, + "vertices": [ 0.5298, -1.12677, -0.85507, -4.20587, -11.35158, -10.19225, -10.79865, -8.43765, -6.06447, -6.89757, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.54892, -3.06021, 1.48463, -2.29663, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4.80437, -7.01817 ] + }, + { + "time": 0.3667, + "offset": 14, + "vertices": [ 0.5298, -1.12677, -11.66571, -9.07211, -25.65866, -17.53735, -25.53217, -16.50978, -11.78232, -11.26097, 0, 0, 0.60487, -1.63589, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.60487, -1.63589, 0, 0, -2.64522, -7.35739, 1.48463, -2.29663, 0, 0, 0, 0, 0, 0, 0.60487, -1.63589, 0.60487, -1.63589, 0.60487, -1.63589, 0.60487, -1.63589, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.60487, -1.63589, 0, 0, -10.06873, -12.0999 ] + }, + { + "time": 0.5333, + "offset": 14, + "vertices": [ 0.5298, -1.12677, -0.85507, -4.20587, -7.00775, -8.24771, -6.45482, -6.49312, -6.06447, -6.89757, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.54892, -3.06021, 1.48463, -2.29663, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4.80437, -7.01817 ] + }, + { + "time": 1, + "offset": 14, + "vertices": [ 0.5298, -1.12677, -0.85507, -4.20587, -11.35158, -10.19225, -10.79865, -8.43765, -6.06447, -6.89757, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.54892, -3.06021, 1.48463, -2.29663, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4.80437, -7.01817 ] + } + ] + } + }, + "hoverboard-board": { + "hoverboard-board": { + "deform": [ + { + "curve": [ 0.067, 0, 0.2, 1 ] + }, + { + "time": 0.2667, + "offset": 1, + "vertices": [ 2.45856, 0, 0, 0, 0, 0, 0, 0, 0, 3.55673, -3.0E-4, 3.55673, -3.0E-4, 0, 0, 0, 0, 0, 0, -7.6E-4, -9.84158, -7.6E-4, -9.84158, -7.6E-4, -9.84158, -7.6E-4, -9.84158, -7.6E-4, -9.84158, -7.6E-4, -9.84158, -7.6E-4, -9.84158, -7.6E-4, -9.84158, -7.6E-4, -9.84158, -7.6E-4, -9.84158, -7.6E-4, -9.84158, -7.6E-4, -9.84158, 0, 0, 0, 0, 0, 0, 0, 0, -4.90558, 0.11214, -9.40706, 6.2E-4, -6.34871, 4.3E-4, -6.34925, -6.57018, -6.34925, -6.57018, -6.34871, 4.3E-4, -2.3308, 1.7E-4, -2.33133, -6.57045, -2.33133, -6.57045, -2.3308, 1.7E-4, 0, 0, 1.2E-4, 2.45856, 1.2E-4, 2.45856, 1.2E-4, 2.45856, 1.2E-4, 2.45856, 3.3297, 4.44005, 3.3297, 4.44005, 3.3297, 4.44005, 1.2E-4, 2.45856, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2.46227, 1.7E-4, -2.46227, 1.7E-4, -2.52316, 1.1313, -2.52316, 1.1313, -2.52316, 1.1313, 1.2E-4, 2.45856, 1.2E-4, 2.45856, -9.40694, 2.45918, 1.88063, 0.44197, -2.9E-4, -3.54808, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2.52316, 1.1313, -2.52316, 1.1313, -2.52316, 1.1313, -2.46227, 1.7E-4, -2.46227, 1.7E-4, -2.46227, 1.7E-4, 0, 0, 0, 0, 1.2E-4, 2.45856 ], + "curve": [ 0.45, 0, 0.817, 1 ] + }, + { "time": 1 } + ] + } + }, + "rear-foot": { + "rear-foot": { + "deform": [ + { + "offset": 28, + "vertices": [ -1.93078, 1.34782, -0.31417, 2.33363, 3.05122, 0.33946, 2.31472, -2.01678, 2.17583, -2.05795, -0.04277, -2.99459, 1.15429, 0.26328, 0.97501, -0.67169 ] + } + ] + } + } + } + } + }, + "idle": { + "slots": { + "front-fist": { + "attachment": [ + { "name": "front-fist-open" } + ] + } + }, + "bones": { + "front-foot-target": { + "translate": [ + { "x": -69.06 } + ] + }, + "hip": { + "rotate": [ + { + "curve": [ 0.073, 0.35, 0.303, 1.27 ] + }, + { + "time": 0.4, + "value": 1.28, + "curve": [ 0.615, 1.3, 0.847, -1.41 ] + }, + { + "time": 1.2, + "value": -1.38, + "curve": [ 1.344, -1.37, 1.602, -0.28 ] + }, + { "time": 1.6667 } + ], + "translate": [ + { + "x": -11.97, + "y": -23.15, + "curve": [ 0.059, -12.96, 0.258, -15.19, 0.142, -23.15, 0.341, -24.89 ] + }, + { + "time": 0.4667, + "x": -15.14, + "y": -26.74, + "curve": [ 0.62, -15.1, 0.788, -13.28, 0.597, -28.66, 0.75, -30.01 ] + }, + { + "time": 0.9, + "x": -12.02, + "y": -30.01, + "curve": [ 0.978, -11.13, 1.175, -9.05, 1.036, -29.94, 1.234, -28.08 ] + }, + { + "time": 1.3333, + "x": -9.06, + "y": -26.64, + "curve": [ 1.501, -9.06, 1.614, -10.95, 1.454, -24.89, 1.609, -23.15 ] + }, + { "time": 1.6667, "x": -11.97, "y": -23.15 } + ] + }, + "rear-foot-target": { + "translate": [ + { "x": 48.87 } + ] + }, + "front-upper-arm": { + "rotate": [ + { + "value": -60.87, + "curve": [ 0.154, -60.85, 0.452, -68.65 ] + }, + { + "time": 0.8333, + "value": -68.65, + "curve": [ 1.221, -68.65, 1.542, -60.87 ] + }, + { "time": 1.6667, "value": -60.87 } + ] + }, + "front-bracer": { + "rotate": [ + { + "value": 42.46, + "curve": [ 0.029, 42.97, 0.134, 45.28 ] + }, + { + "time": 0.3333, + "value": 45.27, + "curve": [ 0.578, 45.26, 0.798, 40.07 ] + }, + { + "time": 0.8333, + "value": 39.74, + "curve": [ 0.878, 39.32, 1.019, 38.23 ] + }, + { + "time": 1.2, + "value": 38.22, + "curve": [ 1.377, 38.22, 1.619, 41.68 ] + }, + { "time": 1.6667, "value": 42.46 } + ] + }, + "rear-upper-arm": { + "rotate": [ + { + "value": 39.2, + "curve": [ 0.185, 39.22, 0.5, 29.37 ] + }, + { + "time": 0.6667, + "value": 29.37, + "curve": [ 0.917, 29.37, 1.417, 39.2 ] + }, + { "time": 1.6667, "value": 39.2 } + ] + }, + "head": { + "rotate": [ + { + "value": -6.75, + "curve": [ 0.176, -7.88, 0.349, -8.95 ] + }, + { + "time": 0.4667, + "value": -8.95, + "curve": [ 0.55, -8.95, 0.697, -6.77 ] + }, + { + "time": 0.8333, + "value": -5.44, + "curve": [ 0.88, -4.98, 1.05, -4.12 ] + }, + { + "time": 1.1333, + "value": -4.12, + "curve": [ 1.266, -4.12, 1.469, -5.48 ] + }, + { "time": 1.6667, "value": -6.75 } + ] + }, + "front-fist": { + "rotate": [ + { + "curve": [ 0.086, 0, 0.233, 2.48 ] + }, + { + "time": 0.3333, + "value": 4.13, + "curve": [ 0.429, 5.7, 0.711, 10.06 ] + }, + { + "time": 0.8333, + "value": 10.06, + "curve": [ 0.926, 10.06, 1.092, 4.21 ] + }, + { + "time": 1.2, + "value": 2.78, + "curve": [ 1.349, 0.8, 1.551, 0 ] + }, + { "time": 1.6667 } + ] + }, + "rear-bracer": { + "rotate": [ + { + "curve": [ 0.063, 0.54, 0.367, 3.39 ] + }, + { + "time": 0.5333, + "value": 3.39, + "curve": [ 0.696, 3.39, 0.939, -1.63 ] + }, + { + "time": 1.2, + "value": -1.61, + "curve": [ 1.42, -1.59, 1.574, -0.67 ] + }, + { "time": 1.6667 } + ] + }, + "gun": { + "rotate": [ + { + "curve": [ 0.099, 0.27, 0.367, 1.23 ] + }, + { + "time": 0.5333, + "value": 1.23, + "curve": [ 0.665, 1.23, 0.937, -0.56 ] + }, + { + "time": 1.1333, + "value": -0.55, + "curve": [ 1.316, -0.55, 1.582, -0.21 ] + }, + { "time": 1.6667 } + ] + }, + "torso": { + "rotate": [ + { + "value": -22.88, + "curve": [ 0.099, -23.45, 0.363, -24.74 ] + }, + { + "time": 0.5333, + "value": -24.74, + "curve": [ 0.706, -24.74, 0.961, -20.97 ] + }, + { + "time": 1.1333, + "value": -20.97, + "curve": [ 1.355, -20.97, 1.567, -22.28 ] + }, + { "time": 1.6667, "value": -22.88 } + ] + }, + "neck": { + "rotate": [ + { + "value": 3.78, + "curve": [ 0.167, 3.78, 0.5, 5.45 ] + }, + { + "time": 0.6667, + "value": 5.45, + "curve": [ 0.917, 5.45, 1.417, 3.78 ] + }, + { "time": 1.6667, "value": 3.78 } + ] + }, + "torso2": { + "rotate": [ + { + "curve": [ 0.067, 0.33, 0.341, 2.54 ] + }, + { + "time": 0.5333, + "value": 2.54, + "curve": [ 0.734, 2.55, 0.982, -0.94 ] + }, + { + "time": 1.1333, + "value": -0.93, + "curve": [ 1.365, -0.91, 1.549, -0.56 ] + }, + { "time": 1.6667 } + ] + }, + "torso3": { + "rotate": [ + { + "value": -2.15, + "curve": [ 0.052, -1.9, 0.384, -0.15 ] + }, + { + "time": 0.5333, + "value": -0.14, + "curve": [ 0.762, -0.13, 0.895, -3.1 ] + }, + { + "time": 1.1333, + "value": -3.1, + "curve": [ 1.348, -3.1, 1.592, -2.46 ] + }, + { "time": 1.6667, "value": -2.15 } + ] + }, + "hair1": { + "rotate": [ + { + "curve": [ 0.067, 0, 0.213, 2.86 ] + }, + { + "time": 0.2667, + "value": 3.65, + "curve": [ 0.358, 4.99, 0.535, 7.92 ] + }, + { + "time": 0.6667, + "value": 7.92, + "curve": [ 0.809, 7.92, 1.067, 5.49 ] + }, + { + "time": 1.1333, + "value": 4.7, + "curve": [ 1.245, 3.34, 1.525, 0 ] + }, + { "time": 1.6667 } + ] + }, + "hair2": { + "rotate": [ + { + "curve": [ 0.067, 0, 0.225, -7.97 ] + }, + { + "time": 0.2667, + "value": -9.75, + "curve": [ 0.316, -11.84, 0.519, -16.66 ] + }, + { + "time": 0.6667, + "value": -16.66, + "curve": [ 0.817, -16.66, 1.029, -11.43 ] + }, + { + "time": 1.1333, + "value": -9.14, + "curve": [ 1.25, -6.56, 1.525, 0 ] + }, + { "time": 1.6667 } + ] + }, + "hair3": { + "rotate": [ + { + "curve": [ 0.1, 0, 0.3, 1.32 ] + }, + { + "time": 0.4, + "value": 1.32, + "curve": [ 0.55, 1.32, 0.866, 0.93 ] + }, + { + "time": 1, + "value": 0.73, + "curve": [ 1.189, 0.46, 1.5, 0 ] + }, + { "time": 1.6667 } + ] + }, + "hair4": { + "rotate": [ + { + "curve": [ 0.118, -0.44, 0.3, -8.52 ] + }, + { + "time": 0.4, + "value": -8.52, + "curve": [ 0.55, -8.52, 0.85, 1.96 ] + }, + { + "time": 1, + "value": 1.96, + "curve": [ 1.167, 1.96, 1.577, 0.38 ] + }, + { "time": 1.6667 } + ] + }, + "head-control": { + "translate": [ + { + "curve": [ 0.098, 1.46, 0.3, 4.49, 0.17, 0.13, 0.316, -3.28 ] + }, + { + "time": 0.4, + "x": 4.55, + "y": -5.95, + "curve": [ 0.53, 4.64, 0.776, 2.59, 0.492, -8.89, 0.668, -14.21 ] + }, + { + "time": 0.8667, + "x": 1.42, + "y": -14.26, + "curve": [ 0.966, 0.15, 1.109, -2.91, 0.994, -14.26, 1.144, -10.58 ] + }, + { + "time": 1.2333, + "x": -3.02, + "y": -8.26, + "curve": [ 1.342, -3.02, 1.568, -1.48, 1.317, -6.1, 1.558, 0 ] + }, + { "time": 1.6667 } + ] + }, + "front-shoulder": { + "translate": [ + { + "curve": [ 0.21, 0, 0.525, -1.72, 0.21, 0, 0.525, 4.08 ] + }, + { + "time": 0.8333, + "x": -1.72, + "y": 4.08, + "curve": [ 1.15, -1.72, 1.46, 0, 1.15, 4.08, 1.46, 0 ] + }, + { "time": 1.6667 } + ] + } + } + }, + "idle-turn": { + "slots": { + "front-fist": { + "attachment": [ + { "name": "front-fist-open" } + ] + } + }, + "bones": { + "front-upper-arm": { + "rotate": [ + { + "value": -302.77, + "curve": [ 0, -406.9, 0.125, -420.87 ] + }, + { "time": 0.2667, "value": -420.87 } + ], + "translate": [ + { + "x": 2.24, + "y": -4.98, + "curve": [ 0.067, 2.24, 0.111, 0, 0.067, -4.98, 0.111, 0 ] + }, + { "time": 0.2667 } + ] + }, + "rear-upper-arm": { + "rotate": [ + { + "value": 248.56, + "curve": [ 0, 371.28, 0.062, 399.2 ] + }, + { "time": 0.1333, "value": 399.2 } + ], + "translate": [ + { + "x": -2.84, + "y": 37.28, + "curve": [ 0.033, -2.84, 0.069, 0, 0.033, 37.28, 0.069, 0 ] + }, + { "time": 0.1333 } + ] + }, + "gun": { + "rotate": [ + { + "value": -3.95, + "curve": [ 0, -10.4, 0.019, -20.43 ] + }, + { + "time": 0.0333, + "value": -20.45, + "curve": [ 0.044, -20.47, 0.125, 0 ] + }, + { "time": 0.2 } + ] + }, + "neck": { + "rotate": [ + { + "value": 17.2, + "curve": [ 0, 6.27, 0.125, 3.78 ] + }, + { "time": 0.2667, "value": 3.78 } + ] + }, + "hip": { + "translate": [ + { + "x": -2.69, + "y": -6.79, + "curve": [ 0.067, -2.69, 0.2, -11.97, 0.067, -6.79, 0.2, -23.15 ] + }, + { "time": 0.2667, "x": -11.97, "y": -23.15 } + ] + }, + "front-fist": { + "rotate": [ + { + "value": -15.54, + "curve": [ 0, -3.08, 0.034, 18.44 ] + }, + { + "time": 0.0667, + "value": 19.02, + "curve": [ 0.108, 19.75, 0.169, 0 ] + }, + { "time": 0.2667 } + ], + "scale": [ + { + "x": 0.94, + "curve": [ 0, 0.962, 0.024, 1.237, 0, 1, 0.026, 0.947 ] + }, + { + "time": 0.0667, + "x": 1.236, + "y": 0.947, + "curve": [ 0.117, 1.235, 0.189, 1, 0.117, 0.947, 0.189, 1 ] + }, + { "time": 0.2667 } + ] + }, + "rear-bracer": { + "rotate": [ + { + "value": 11.75, + "curve": [ 0, -7.97, 0.017, -33.4 ] + }, + { + "time": 0.0333, + "value": -33.39, + "curve": [ 0.049, -33.37, 0.131, 0 ] + }, + { "time": 0.2 } + ] + }, + "torso": { + "rotate": [ + { + "value": -18.25, + "curve": [ 0, -10.59, 0.125, -22.88 ] + }, + { "time": 0.2667, "value": -22.88 } + ], + "scale": [ + { + "y": 1.03, + "curve": [ 0.067, 1, 0.132, 1, 0.067, 1.03, 0.132, 1 ] + }, + { "time": 0.2667 } + ] + }, + "head": { + "rotate": [ + { + "value": 5.12, + "curve": [ 0, -6.34, 0.125, -6.75 ] + }, + { "time": 0.2667, "value": -6.75 } + ], + "scale": [ + { + "y": 1.03, + "curve": [ 0.067, 1, 0.107, 1, 0.067, 1.03, 0.107, 1 ] + }, + { "time": 0.2667 } + ] + }, + "rear-foot-target": { + "translate": [ + { + "x": -58.39, + "y": 30.48, + "curve": [ 0, -7.15, 0.047, 16.62, 0, 12.71, 0.039, 0.22 ] + }, + { + "time": 0.1, + "x": 34.14, + "y": -0.19, + "curve": [ 0.136, 45.79, 0.163, 48.87, 0.133, -0.41, 0.163, 0 ] + }, + { "time": 0.2, "x": 48.87 } + ] + }, + "front-bracer": { + "rotate": [ + { + "value": 6.69, + "curve": [ 0, 19.76, 0.039, 56.53 ] + }, + { + "time": 0.0667, + "value": 56.63, + "curve": [ 0.114, 56.79, 0.189, 42.46 ] + }, + { "time": 0.2667, "value": 42.46 } + ] + }, + "front-foot-target": { + "rotate": [ + { + "value": -1.85, + "curve": [ 0.014, -8.91, 0.047, -28.4 ] + }, + { + "time": 0.1, + "value": -28.89, + "curve": [ 0.144, -29.29, 0.262, -21.77 ] + }, + { "time": 0.2667 } + ], + "translate": [ + { + "x": 9.97, + "y": 0.82, + "curve": [ 0, -54.41, 0.078, -69.06, 0, 0.15, 0.078, 0 ] + }, + { "time": 0.1667, "x": -69.06 } + ] + }, + "hair3": { + "rotate": [ + { + "value": -9.01, + "curve": [ 0.044, -9.01, 0.072, 7.41 ] + }, + { + "time": 0.1333, + "value": 10.08, + "curve": [ 0.166, 11.47, 0.208, 0 ] + }, + { "time": 0.2667 } + ] + }, + "hair4": { + "rotate": [ + { + "value": -16.49, + "curve": [ 0.044, -16.49, 0.101, -5.98 ] + }, + { + "time": 0.1333, + "value": -2.95, + "curve": [ 0.162, -0.34, 0.208, 0 ] + }, + { "time": 0.2667 } + ] + }, + "hair1": { + "rotate": [ + { + "value": -3.85, + "curve": [ 0.044, -3.85, 0.072, 6.91 ] + }, + { + "time": 0.1333, + "value": 8.05, + "curve": [ 0.166, 8.65, 0.208, 0 ] + }, + { "time": 0.2667 } + ] + }, + "hair2": { + "rotate": [ + { + "value": 1.25, + "curve": [ 0.044, 1.25, 0.072, 8.97 ] + }, + { + "time": 0.1333, + "value": 8.6, + "curve": [ 0.166, 8.4, 0.208, 0 ] + }, + { "time": 0.2667 } + ] + }, + "front-thigh": { + "translate": [ + { + "x": 12.21, + "y": 1.89, + "curve": [ 0.033, 12.21, 0.1, 0, 0.033, 1.89, 0.1, 0 ] + }, + { "time": 0.1333 } + ] + }, + "rear-thigh": { + "translate": [ + { + "x": -16.11, + "y": -1.38, + "curve": [ 0.033, -16.11, 0.1, 0, 0.033, -1.38, 0.1, 0 ] + }, + { "time": 0.1333 } + ] + }, + "torso3": { + "rotate": [ + { "time": 0.2667, "value": -2.15 } + ] + }, + "head-control": { + "translate": [ + { + "x": -13.72, + "y": -34.7, + "curve": [ 0.067, -13.72, 0.2, 0, 0.067, -34.7, 0.2, 0 ] + }, + { "time": 0.2667 } + ] + }, + "front-shoulder": { + "translate": [ + { + "x": 1.13, + "y": -14.31, + "curve": [ 0.067, 1.13, 0.2, 0, 0.067, -14.31, 0.2, 0 ] + }, + { "time": 0.2667 } + ] + } + } + }, + "jump": { + "slots": { + "front-fist": { + "attachment": [ + { "name": "front-fist-open" }, + { "time": 0.1, "name": "front-fist-closed" }, + { "time": 0.8333, "name": "front-fist-open" } + ] + } + }, + "bones": { + "front-thigh": { + "rotate": [ + { + "value": 55.08, + "curve": [ 0.007, 46.66, 0.043, 26.3 ] + }, + { + "time": 0.0667, + "value": 22.84, + "curve": [ 0.1, 17.99, 0.165, 15.78 ] + }, + { + "time": 0.2333, + "value": 15.71, + "curve": [ 0.309, 15.63, 0.408, 46.67 ] + }, + { + "time": 0.5, + "value": 63.6, + "curve": [ 0.56, 74.72, 0.762, 91.48 ] + }, + { + "time": 0.9667, + "value": 91.81, + "curve": [ 1.068, 92.01, 1.096, 22.05 ] + }, + { + "time": 1.1667, + "value": 22.25, + "curve": [ 1.18, 22.29, 1.176, 56.17 ] + }, + { + "time": 1.2, + "value": 56.16, + "curve": [ 1.246, 56.15, 1.263, 54.94 ] + }, + { "time": 1.3333, "value": 55.08 } + ], + "translate": [ + { "x": -5.13, "y": 11.55 } + ] + }, + "torso": { + "rotate": [ + { + "value": -45.57, + "curve": [ 0.022, -44.61, 0.03, -39.06 ] + }, + { + "time": 0.0667, + "value": -35.29, + "curve": [ 0.12, -29.77, 0.28, -19.95 ] + }, + { + "time": 0.4333, + "value": -19.95, + "curve": [ 0.673, -19.95, 0.871, -22.38 ] + }, + { + "time": 0.9667, + "value": -27.08, + "curve": [ 1.094, -33.33, 1.176, -44.93 ] + }, + { "time": 1.3333, "value": -45.57 } + ], + "translate": [ + { "x": -3.79, "y": -0.77 } + ] + }, + "rear-thigh": { + "rotate": [ + { + "value": 12.81, + "curve": [ 0.067, 12.81, 0.242, 67.88 ] + }, + { + "time": 0.2667, + "value": 74.11, + "curve": [ 0.314, 86.02, 0.454, 92.23 ] + }, + { + "time": 0.5667, + "value": 92.24, + "curve": [ 0.753, 92.26, 0.966, 67.94 ] + }, + { + "time": 1, + "value": 61.32, + "curve": [ 1.039, 53.75, 1.218, 12.68 ] + }, + { "time": 1.3333, "value": 12.81 } + ] + }, + "rear-shin": { + "rotate": [ + { + "value": -115.64, + "curve": [ 0.067, -117.17, 0.125, -117.15 ] + }, + { + "time": 0.1667, + "value": -117.15, + "curve": [ 0.225, -117.15, 0.332, -108.76 ] + }, + { + "time": 0.4, + "value": -107.15, + "curve": [ 0.48, -105.26, 0.685, -103.49 ] + }, + { + "time": 0.7667, + "value": -101.97, + "curve": [ 0.826, -100.87, 0.919, -92.3 ] + }, + { + "time": 1, + "value": -92.28, + "curve": [ 1.113, -92.26, 1.297, -114.22 ] + }, + { "time": 1.3333, "value": -115.64 } + ] + }, + "front-upper-arm": { + "rotate": [ + { + "value": -40.21, + "curve": [ 0.054, -35.46, 0.15, -31.12 ] + }, + { + "time": 0.2, + "value": -31.12, + "curve": [ 0.308, -31.12, 0.547, -80.12 ] + }, + { + "time": 0.6333, + "value": -96.56, + "curve": [ 0.697, -108.56, 0.797, -112.54 ] + }, + { + "time": 0.8667, + "value": -112.6, + "curve": [ 1.137, -112.84, 1.274, -49.19 ] + }, + { "time": 1.3333, "value": -40.21 } + ] + }, + "front-bracer": { + "rotate": [ + { + "value": 20.54, + "curve": [ 0.054, 32.23, 0.192, 55.84 ] + }, + { + "time": 0.2333, + "value": 62.58, + "curve": [ 0.29, 71.87, 0.375, 79.28 ] + }, + { + "time": 0.4333, + "value": 79.18, + "curve": [ 0.555, 78.98, 0.684, 27.54 ] + }, + { + "time": 0.7333, + "value": 13.28, + "curve": [ 0.786, -1.85, 0.874, -24.76 ] + }, + { + "time": 1, + "value": -25.45, + "curve": [ 1.165, -26.36, 1.303, 9.1 ] + }, + { "time": 1.3333, "value": 20.54 } + ] + }, + "front-fist": { + "rotate": [ + { + "value": -36.16, + "curve": [ 0.114, -39.59, 0.3, -45.61 ] + }, + { + "time": 0.4, + "value": -45.61, + "curve": [ 0.442, -45.61, 0.537, -21.54 ] + }, + { + "time": 0.5667, + "value": -15.4, + "curve": [ 0.592, -10.23, 0.692, 11.89 ] + }, + { + "time": 0.7333, + "value": 11.73, + "curve": [ 0.783, 11.54, 0.831, 1.8 ] + }, + { + "time": 0.8667, + "value": -5.78, + "curve": [ 0.897, -12.22, 0.901, -14.22 ] + }, + { + "time": 0.9333, + "value": -14.51, + "curve": [ 0.974, -14.89, 0.976, 10.38 ] + }, + { + "time": 1, + "value": 10.55, + "curve": [ 1.027, 10.74, 1.023, -8.44 ] + }, + { + "time": 1.0333, + "value": -8.42, + "curve": [ 1.059, -8.36, 1.074, 10.12 ] + }, + { + "time": 1.1, + "value": 10.22, + "curve": [ 1.168, 10.48, 1.27, -36.07 ] + }, + { "time": 1.3333, "value": -36.16 } + ] + }, + "rear-upper-arm": { + "rotate": [ + { + "value": 40.5, + "curve": [ 0.048, 36.1, 0.168, 20.45 ] + }, + { + "time": 0.3, + "value": 20.45, + "curve": [ 0.476, 20.45, 0.571, 33.76 ] + }, + { + "time": 0.6, + "value": 38.67, + "curve": [ 0.642, 45.8, 0.681, 57.44 ] + }, + { + "time": 0.7333, + "value": 62.91, + "curve": [ 0.829, 72.8, 0.996, 77.61 ] + }, + { + "time": 1.0333, + "value": 80.37, + "curve": [ 1.082, 83.94, 1.148, 90.6 ] + }, + { + "time": 1.2, + "value": 90.6, + "curve": [ 1.248, 90.46, 1.317, 53.07 ] + }, + { "time": 1.3333, "value": 49.06 } + ] + }, + "rear-bracer": { + "rotate": [ + { + "value": 28.28, + "curve": [ 0.022, 25.12, 0.187, -0.89 ] + }, + { + "time": 0.2, + "value": -2.52, + "curve": [ 0.257, -9.92, 0.372, -17.38 ] + }, + { + "time": 0.4333, + "value": -17.41, + "curve": [ 0.54, -17.47, 0.659, -16.91 ] + }, + { + "time": 0.7667, + "value": -12.1, + "curve": [ 0.907, -5.79, 1.025, 14.58 ] + }, + { + "time": 1.1, + "value": 20.58, + "curve": [ 1.191, 27.85, 1.283, 29.67 ] + }, + { "time": 1.3333, "value": 29.67 } + ] + }, + "neck": { + "rotate": [ + { + "value": 11.88, + "curve": [ 0.104, 11.82, 0.179, 11.15 ] + }, + { + "time": 0.2, + "value": 10.08, + "curve": [ 0.255, 7.29, 0.405, -8.15 ] + }, + { + "time": 0.4333, + "value": -9.35, + "curve": [ 0.508, -12.48, 0.595, -13.14 ] + }, + { + "time": 0.6667, + "value": -12.61, + "curve": [ 0.714, -12.26, 0.815, -5.57 ] + }, + { + "time": 0.8333, + "value": -4.08, + "curve": [ 0.883, -0.07, 1.045, 12.77 ] + }, + { + "time": 1.1, + "value": 15.06, + "curve": [ 1.208, 19.6, 1.279, 20.64 ] + }, + { "time": 1.3333, "value": 20.73 } + ] + }, + "head": { + "rotate": [ + { + "value": 13.14, + "curve": [ 0.008, 12.19, 0.197, -23.53 ] + }, + { + "time": 0.3333, + "value": -23.95, + "curve": [ 0.509, -23.95, 0.667, -2.66 ] + }, + { + "time": 0.7333, + "value": -2.66, + "curve": [ 0.792, -2.66, 0.908, -13.32 ] + }, + { + "time": 0.9667, + "value": -13.32, + "curve": [ 1.158, -13.11, 1.241, -1.58 ] + }, + { "time": 1.3333, "value": -1.58 } + ], + "scale": [ + { + "curve": [ 0.041, 1, 0.052, 0.962, 0.041, 1, 0.052, 1.137 ] + }, + { + "time": 0.1, + "x": 0.954, + "y": 1.137, + "curve": [ 0.202, 0.962, 0.318, 1, 0.202, 1.137, 0.252, 1.002 ] + }, + { "time": 0.4667 }, + { + "time": 1.0667, + "x": 1.002, + "curve": [ 1.092, 1.002, 1.126, 1.143, 1.092, 1, 1.128, 0.975 ] + }, + { + "time": 1.1667, + "x": 1.144, + "y": 0.973, + "curve": [ 1.204, 1.145, 1.233, 0.959, 1.206, 0.972, 1.227, 1.062 ] + }, + { + "time": 1.2667, + "x": 0.958, + "y": 1.063, + "curve": [ 1.284, 0.958, 1.292, 1.001, 1.288, 1.063, 1.288, 1.001 ] + }, + { "time": 1.3333 } + ] + }, + "hip": { + "translate": [ + { + "y": -45.46, + "curve": [ 0.042, -0.09, 0.15, 15.22, 0.031, 44.98, 0.123, 289.73 ] + }, + { + "time": 0.2, + "x": 15.22, + "y": 415.85, + "curve": [ 0.332, 15.22, 0.539, -34.52, 0.271, 532.93, 0.483, 720.5 ] + }, + { + "time": 0.7667, + "x": -34.52, + "y": 721.6, + "curve": [ 0.888, -34.52, 1.057, -21.95, 1.049, 721.17, 1.098, 379.84 ] + }, + { + "time": 1.1333, + "x": -15.67, + "y": 266.77, + "curve": [ 1.144, -14.77, 1.188, -10.53, 1.15, 213.72, 1.172, -61.32 ] + }, + { + "time": 1.2333, + "x": -6.53, + "y": -61.34, + "curve": [ 1.272, -3.22, 1.311, 0.05, 1.291, -61.36, 1.296, -44.8 ] + }, + { "time": 1.3333, "y": -45.46 } + ] + }, + "front-shin": { + "rotate": [ + { + "value": -74.19, + "curve": [ 0, -51.14, 0.042, -12.54 ] + }, + { + "time": 0.1667, + "value": -12.28, + "curve": [ 0.285, -12.32, 0.37, -74.44 ] + }, + { + "time": 0.4333, + "value": -92.92, + "curve": [ 0.498, -111.86, 0.617, -140.28 ] + }, + { + "time": 0.9, + "value": -140.84, + "curve": [ 1.004, -141.04, 1.09, -47.87 ] + }, + { + "time": 1.1, + "value": -37.44, + "curve": [ 1.108, -29.83, 1.14, -21.18 ] + }, + { + "time": 1.1667, + "value": -21.08, + "curve": [ 1.18, -21.03, 1.191, -50.65 ] + }, + { + "time": 1.2, + "value": -53.17, + "curve": [ 1.22, -58.53, 1.271, -73.38 ] + }, + { "time": 1.3333, "value": -74.19 } + ] + }, + "front-foot": { + "rotate": [ + { + "value": 7.35, + "curve": [ 0, 4.8, 0.05, -26.64 ] + }, + { + "time": 0.0667, + "value": -26.64, + "curve": [ 0.192, -26.64, 0.442, -11.77 ] + }, + { + "time": 0.5667, + "value": -11.77, + "curve": [ 0.692, -11.77, 0.942, -19.36 ] + }, + { + "time": 1.0667, + "value": -19.36, + "curve": [ 1.133, -19.36, 1.32, 3.82 ] + }, + { "time": 1.3333, "value": 7.35 } + ] + }, + "rear-foot": { + "rotate": [ + { "value": -7.14 } + ] + }, + "gun": { + "rotate": [ + { + "value": 12.36, + "curve": [ 0.022, 16.28, 0.15, 30.81 ] + }, + { + "time": 0.2, + "value": 30.81, + "curve": [ 0.258, 30.81, 0.375, 13.26 ] + }, + { + "time": 0.4333, + "value": 13.26, + "curve": [ 0.508, 13.26, 0.658, 15.05 ] + }, + { + "time": 0.7333, + "value": 14.98, + "curve": [ 0.789, 14.94, 0.828, 13.62 ] + }, + { + "time": 0.8667, + "value": 12.72, + "curve": [ 0.887, 12.25, 0.984, 9.83 ] + }, + { + "time": 1.0333, + "value": 8.6, + "curve": [ 1.045, 8.31, 1.083, 7.55 ] + }, + { + "time": 1.1333, + "value": 7.13, + "curve": [ 1.175, 6.78, 1.283, 6.18 ] + }, + { "time": 1.3333, "value": 6.18 } + ] + }, + "front-leg-target": { + "translate": [ + { "x": -13.95, "y": -30.34 } + ] + }, + "rear-leg-target": { + "rotate": [ + { "value": -38.43 } + ], + "translate": [ + { "x": 85, "y": -33.59 } + ] + }, + "front-foot-target": { + "rotate": [ + { "value": -62.54 } + ], + "translate": [ + { "x": 16.34, "y": 0.18 } + ] + }, + "rear-foot-target": { + "rotate": [ + { "value": 18.55 } + ], + "translate": [ + { "x": -176.39, "y": 134.12 } + ] + }, + "back-foot-tip": { + "rotate": [ + { + "value": -143.73, + "curve": [ 0.083, -144.24, 0.167, -74.26 ] + }, + { + "time": 0.2667, + "value": -52.76, + "curve": [ 0.342, -36.57, 0.513, -36.57 ] + }, + { + "time": 0.6333, + "value": -30.97, + "curve": [ 0.724, -26.78, 0.848, -17.06 ] + }, + { + "time": 0.9667, + "value": -16.74, + "curve": [ 1.167, -16.2, 1.272, -144.17 ] + }, + { "time": 1.3333, "value": -143.73 } + ] + }, + "front-foot-tip": { + "rotate": [ + { + "value": -1.57, + "curve": [ 0, -24.71, 0.162, -60.88 ] + }, + { + "time": 0.2667, + "value": -60.83, + "curve": [ 0.342, -60.8, 0.582, -43.5 ] + }, + { + "time": 0.7, + "value": -39.45, + "curve": [ 0.773, -36.94, 0.832, -36.78 ] + }, + { + "time": 0.9667, + "value": -36.6, + "curve": [ 1.054, -36.49, 1.092, -37.37 ] + }, + { + "time": 1.1667, + "value": -33.26, + "curve": [ 1.237, -29.37, 1.147, -1.41 ] + }, + { "time": 1.2, "value": -1.57 } + ] + }, + "hair3": { + "rotate": [ + { + "value": -6.81, + "curve": [ 0, 13.59, 0.117, 18.21 ] + }, + { + "time": 0.1333, + "value": 18.21, + "curve": [ 0.167, 18.21, 0.26, 12.95 ] + }, + { + "time": 0.3, + "value": 11.56, + "curve": [ 0.382, 8.7, 0.55, 9.43 ] + }, + { + "time": 0.6667, + "value": 9.32, + "curve": [ 0.843, 9.15, 0.918, -7.34 ] + }, + { "time": 1.3333, "value": -6.81 } + ], + "translate": [ + { + "time": 0.6667, + "curve": [ 0.781, 0, 0.972, 16.03, 0.781, 0, 0.972, 0.92 ] + }, + { + "time": 1.1333, + "x": 16.03, + "y": 0.92, + "curve": [ 1.211, 16.03, 1.281, 0, 1.211, 0.92, 1.281, 0 ] + }, + { "time": 1.3333 } + ] + }, + "hair4": { + "rotate": [ + { + "value": -6.81, + "curve": [ 0.001, -3.88, 0.063, 16.18 ] + }, + { + "time": 0.1667, + "value": 16.14, + "curve": [ 0.242, 16.1, 0.249, 16.07 ] + }, + { + "time": 0.3333, + "value": 13.46, + "curve": [ 0.442, 10.09, 0.573, -2.2 ] + }, + { + "time": 0.6, + "value": -6.04, + "curve": [ 0.614, -8.05, 0.717, -33.44 ] + }, + { + "time": 0.7667, + "value": -33.44, + "curve": [ 0.809, -33.44, 0.835, -31.32 ] + }, + { + "time": 0.8667, + "value": -27.36, + "curve": [ 0.874, -26.47, 0.903, -14.28 ] + }, + { + "time": 0.9333, + "value": -14.47, + "curve": [ 0.956, -14.62, 0.944, -25.91 ] + }, + { + "time": 1, + "value": -25.96, + "curve": [ 1.062, -26.02, 1.051, -1.87 ] + }, + { + "time": 1.0667, + "value": -1.87, + "curve": [ 1.096, -1.87, 1.096, -16.09 ] + }, + { + "time": 1.1333, + "value": -16.08, + "curve": [ 1.169, -16.08, 1.153, -3.38 ] + }, + { + "time": 1.2, + "value": -3.38, + "curve": [ 1.234, -3.38, 1.271, -6.07 ] + }, + { "time": 1.3333, "value": -6.07 } + ] + }, + "hair2": { + "rotate": [ + { + "value": -6.81, + "curve": [ 0, -3.17, 0.042, 16.33 ] + }, + { + "time": 0.0667, + "value": 16.33, + "curve": [ 0.21, 15.74, 0.208, -12.06 ] + }, + { + "time": 0.3333, + "value": -12.21, + "curve": [ 0.417, -12.3, 0.552, -3.98 ] + }, + { + "time": 0.6667, + "value": 1.52, + "curve": [ 0.726, 4.35, 0.817, 4.99 ] + }, + { + "time": 0.8667, + "value": 4.99, + "curve": [ 0.901, 4.99, 0.912, -29.05 ] + }, + { + "time": 0.9667, + "value": -27.45, + "curve": [ 0.987, -26.83, 1.018, -5.42 ] + }, + { + "time": 1.0667, + "value": -5.46, + "curve": [ 1.107, -5.22, 1.095, -33.51 ] + }, + { + "time": 1.1333, + "value": -33.28, + "curve": [ 1.162, -33.57, 1.192, 8.04 ] + }, + { + "time": 1.2667, + "value": 7.86, + "curve": [ 1.302, 7.77, 1.313, 2.7 ] + }, + { "time": 1.3333, "value": 2.7 } + ] + }, + "hair1": { + "rotate": [ + { + "value": -6.81, + "curve": [ 0.001, -3.12, 0.074, 14.66 ] + }, + { + "time": 0.1333, + "value": 14.66, + "curve": [ 0.188, 14.8, 0.293, 9.56 ] + }, + { + "time": 0.3333, + "value": 5.99, + "curve": [ 0.381, 1.72, 0.55, -11.11 ] + }, + { + "time": 0.6667, + "value": -11.11, + "curve": [ 0.833, -11.11, 0.933, 22.54 ] + }, + { + "time": 1.1, + "value": 22.54, + "curve": [ 1.158, 22.54, 1.275, -6.81 ] + }, + { "time": 1.3333, "value": -6.81 } + ] + }, + "torso2": { + "rotate": [ + { + "value": 4.52, + "curve": [ 0.013, 2.33, 0.092, -9.75 ] + }, + { + "time": 0.1333, + "value": -9.75, + "curve": [ 0.175, -9.75, 0.291, -1.26 ] + }, + { + "time": 0.3333, + "value": 0.96, + "curve": [ 0.359, 2.3, 0.543, 4.25 ] + }, + { + "time": 0.6, + "value": 4.68, + "curve": [ 0.683, 5.3, 0.771, 5.92 ] + }, + { + "time": 0.8333, + "value": 6.48, + "curve": [ 0.871, 6.82, 1.083, 11.37 ] + }, + { + "time": 1.1667, + "value": 11.37, + "curve": [ 1.208, 11.37, 1.317, 6.18 ] + }, + { "time": 1.3333, "value": 4.52 } + ], + "translate": [ + { + "curve": [ 0, 0, 0.082, -2.24, 0, 0, 0.082, -0.42 ] + }, + { + "time": 0.1667, + "x": -2.98, + "y": -0.56, + "curve": [ 0.232, -2.24, 0.298, 0, 0.232, -0.42, 0.298, 0 ] + }, + { "time": 0.3333, "curve": "stepped" }, + { + "time": 0.8667, + "curve": [ 0.889, 0, 0.912, 0.26, 0.889, 0, 0.912, 0.06 ] + }, + { + "time": 0.9333, + "x": 0.68, + "y": 0.23, + "curve": [ 1.016, 2.22, 1.095, 5.9, 1.023, 0.97, 1.095, 1.99 ] + }, + { + "time": 1.1667, + "x": 6.47, + "y": 2.18, + "curve": [ 1.23, 5.75, 1.286, 0, 1.23, 1.94, 1.286, 0 ] + }, + { "time": 1.3333 } + ] + }, + "torso3": { + "rotate": [ + { + "value": 4.52, + "curve": [ 0.025, 4.52, 0.075, -6.17 ] + }, + { + "time": 0.1, + "value": -6.17, + "curve": [ 0.175, -6.17, 0.381, -0.71 ] + }, + { + "time": 0.4, + "value": -0.25, + "curve": [ 0.447, 0.87, 0.775, 4.84 ] + }, + { + "time": 0.9, + "value": 4.84, + "curve": [ 1.008, 4.84, 1.225, 4.52 ] + }, + { "time": 1.3333, "value": 4.52 } + ] + }, + "head-control": { + "translate": [ + { + "curve": [ 0.138, -2.4, 0.227, -10.44, 0.123, 1.05, 0.227, 2.7 ] + }, + { + "time": 0.3667, + "x": -10.44, + "y": 2.7, + "curve": [ 0.484, -10.44, 0.585, -5.63, 0.484, 2.7, 0.629, -23.62 ] + }, + { + "time": 0.7333, + "x": -2.29, + "y": -26.61, + "curve": [ 0.818, -0.39, 0.962, 1.21, 0.858, -30.17, 0.972, -28.75 ] + }, + { + "time": 1.1, + "x": 1.25, + "y": -28.75, + "curve": [ 1.192, 1.28, 1.234, 0.98, 1.224, -28.75, 1.235, -2.15 ] + }, + { "time": 1.3333 } + ] + }, + "front-shoulder": { + "translate": [ + { + "curve": [ 0.031, -2.22, 0.065, -3.73, 0.02, -3.25, 0.065, -14.74 ] + }, + { + "time": 0.1, + "x": -3.73, + "y": -14.74, + "curve": [ 0.216, -3.73, 0.384, -0.17, 0.216, -14.74, 0.402, -12.51 ] + }, + { + "time": 0.5, + "x": 1.63, + "y": -9.51, + "curve": [ 0.632, 3.69, 0.935, 7.41, 0.585, -6.91, 0.909, 10.86 ] + }, + { + "time": 1.1, + "x": 7.45, + "y": 10.99, + "curve": [ 1.18, 7.46, 1.265, 2.86, 1.193, 11.05, 1.294, 3.38 ] + }, + { "time": 1.3333 } + ] + } + }, + "ik": { + "front-foot-ik": [ + { + "mix": 0, + "curve": [ 0.3, 0, 0.9, 1, 0.3, 0, 0.9, 0 ] + }, + { "time": 1.2 } + ], + "front-leg-ik": [ + { + "mix": 0, + "bendPositive": false, + "curve": [ 0.3, 0, 0.9, 1, 0.3, 0, 0.9, 0 ] + }, + { "time": 1.2, "bendPositive": false } + ], + "rear-foot-ik": [ + { "mix": 0 } + ], + "rear-leg-ik": [ + { "mix": 0, "bendPositive": false } + ] + }, + "events": [ + { "time": 1.2, "name": "footstep" } + ] + }, + "portal": { + "slots": { + "clipping": { + "attachment": [ + { "name": "clipping" } + ] + }, + "front-fist": { + "attachment": [ + { "name": "front-fist-open" } + ] + }, + "mouth": { + "attachment": [ + { "time": 0.9, "name": "mouth-grind" }, + { "time": 2.2667, "name": "mouth-smile" } + ] + }, + "portal-bg": { + "attachment": [ + { "name": "portal-bg" }, + { "time": 3 } + ] + }, + "portal-flare1": { + "attachment": [ + { "time": 1.1, "name": "portal-flare1" }, + { "time": 1.1333, "name": "portal-flare2" }, + { "time": 1.1667, "name": "portal-flare3" }, + { "time": 1.2, "name": "portal-flare1" }, + { "time": 1.2333, "name": "portal-flare2" }, + { "time": 1.2667, "name": "portal-flare1" }, + { "time": 1.3333 } + ] + }, + "portal-flare2": { + "attachment": [ + { "time": 1.1, "name": "portal-flare2" }, + { "time": 1.1333, "name": "portal-flare3" }, + { "time": 1.1667, "name": "portal-flare1" }, + { "time": 1.2, "name": "portal-flare2" }, + { "time": 1.2333, "name": "portal-flare3" }, + { "time": 1.2667 } + ] + }, + "portal-flare3": { + "attachment": [ + { "time": 1.2, "name": "portal-flare3" }, + { "time": 1.2333, "name": "portal-flare2" }, + { "time": 1.2667 } + ] + }, + "portal-flare4": { + "attachment": [ + { "time": 1.2, "name": "portal-flare2" }, + { "time": 1.2333, "name": "portal-flare1" }, + { "time": 1.2667, "name": "portal-flare2" }, + { "time": 1.3333 } + ] + }, + "portal-flare5": { + "attachment": [ + { "time": 1.2333, "name": "portal-flare3" }, + { "time": 1.2667, "name": "portal-flare1" }, + { "time": 1.3333 } + ] + }, + "portal-flare6": { + "attachment": [ + { "time": 1.2667, "name": "portal-flare3" }, + { "time": 1.3333 } + ] + }, + "portal-flare7": { + "attachment": [ + { "time": 1.1333, "name": "portal-flare2" }, + { "time": 1.1667 } + ] + }, + "portal-flare8": { + "attachment": [ + { "time": 1.2, "name": "portal-flare3" }, + { "time": 1.2333, "name": "portal-flare2" }, + { "time": 1.2667 } + ] + }, + "portal-flare9": { + "attachment": [ + { "time": 1.2, "name": "portal-flare2" }, + { "time": 1.2333, "name": "portal-flare3" }, + { "time": 1.2667, "name": "portal-flare1" }, + { "time": 1.3 } + ] + }, + "portal-flare10": { + "attachment": [ + { "time": 1.2, "name": "portal-flare2" }, + { "time": 1.2333, "name": "portal-flare1" }, + { "time": 1.2667, "name": "portal-flare3" }, + { "time": 1.3 } + ] + }, + "portal-shade": { + "attachment": [ + { "name": "portal-shade" }, + { "time": 3 } + ] + }, + "portal-streaks1": { + "attachment": [ + { "name": "portal-streaks1" }, + { "time": 3 } + ] + }, + "portal-streaks2": { + "attachment": [ + { "name": "portal-streaks2" }, + { "time": 3 } + ] + } + }, + "bones": { + "portal-root": { + "translate": [ + { + "x": -458.35, + "y": 105.19, + "curve": [ 0.333, -458.22, 0.669, -457.86, 0.934, 105.19, 0.671, 105.19 ] + }, + { + "time": 1, + "x": -456.02, + "y": 105.19, + "curve": [ 1.339, -454.14, 2.208, -447.28, 1.35, 105.19, 2.05, 105.19 ] + }, + { + "time": 2.4, + "x": -439.12, + "y": 105.19, + "curve": [ 2.463, -436.44, 2.502, -432.92, 2.487, 105.19, 2.512, 105.09 ] + }, + { + "time": 2.6, + "x": -432.58, + "y": 105.09, + "curve": [ 2.784, -431.94, 2.978, -446.6, 2.772, 105.09, 2.933, 105.19 ] + }, + { "time": 3.0333, "x": -457.42, "y": 105.19 } + ], + "scale": [ + { + "x": 0.003, + "y": 0.006, + "curve": [ 0.329, 0.044, 0.347, 0.117, 0.329, 0.097, 0.37, 0.249 ] + }, + { + "time": 0.4, + "x": 0.175, + "y": 0.387, + "curve": [ 0.63, 0.619, 0.663, 0.723, 0.609, 1.338, 0.645, 1.524 ] + }, + { + "time": 0.7333, + "x": 0.724, + "y": 1.52, + "curve": [ 0.798, 0.725, 0.907, 0.647, 0.797, 1.517, 0.895, 1.424 ] + }, + { + "time": 1, + "x": 0.645, + "y": 1.426, + "curve": [ 1.095, 0.643, 1.139, 0.688, 1.089, 1.428, 1.115, 1.513 ] + }, + { + "time": 1.2333, + "x": 0.685, + "y": 1.516, + "curve": [ 1.325, 0.683, 1.508, 0.636, 1.343, 1.518, 1.467, 1.4 ] + }, + { + "time": 1.6, + "x": 0.634, + "y": 1.401, + "curve": [ 1.728, 0.631, 1.946, 0.687, 1.722, 1.402, 1.924, 1.522 ] + }, + { + "time": 2.0667, + "x": 0.688, + "y": 1.522, + "curve": [ 2.189, 0.69, 2.289, 0.649, 2.142, 1.522, 2.265, 1.417 ] + }, + { + "time": 2.4, + "x": 0.65, + "y": 1.426, + "curve": [ 2.494, 0.651, 2.504, 0.766, 2.508, 1.434, 2.543, 1.566 ] + }, + { + "time": 2.6, + "x": 0.766, + "y": 1.568, + "curve": [ 2.73, 0.765, 3.006, 0.098, 2.767, 1.564, 2.997, 0.1 ] + }, + { "time": 3.0333, "x": 0.007, "y": 0.015 } + ] + }, + "portal-streaks1": { + "rotate": [ + {}, + { "time": 3.1667, "value": 1200 } + ], + "translate": [ + { + "x": 15.15, + "curve": [ 0.162, 15.15, 0.432, 12.6, 0.162, 0, 0.432, -3.86 ] + }, + { + "time": 0.6667, + "x": 10.9, + "y": -6.44, + "curve": [ 0.794, 9.93, 0.912, 9.21, 0.794, -7.71, 0.912, -8.66 ] + }, + { + "time": 1, + "x": 9.21, + "y": -8.66, + "curve": [ 1.083, 9.21, 1.25, 21.53, 1.083, -8.66, 1.265, -4.9 ] + }, + { + "time": 1.3333, + "x": 21.53, + "y": -3.19, + "curve": [ 1.5, 21.53, 1.939, 12.3, 1.446, -0.37, 1.9, 6.26 ] + }, + { + "time": 2.0667, + "x": 11.26, + "y": 6.26, + "curve": [ 2.239, 9.85, 2.389, 9.68, 2.208, 6.26, 2.523, 0.51 ] + }, + { + "time": 2.5667, + "x": 9.39, + "y": -0.8, + "curve": [ 2.657, 9.24, 2.842, 9.21, 2.646, -3.2, 2.842, -8.91 ] + }, + { "time": 2.9333, "x": 9.21, "y": -8.91 } + ], + "scale": [ + { + "curve": [ 0.167, 1, 0.5, 1.053, 0.167, 1, 0.5, 1.053 ] + }, + { + "time": 0.6667, + "x": 1.053, + "y": 1.053, + "curve": [ 0.833, 1.053, 1.167, 0.986, 0.833, 1.053, 1.167, 0.986 ] + }, + { + "time": 1.3333, + "x": 0.986, + "y": 0.986, + "curve": [ 1.5, 0.986, 1.833, 1.053, 1.5, 0.986, 1.833, 1.053 ] + }, + { "time": 2, "x": 1.053, "y": 1.053 } + ] + }, + "portal-streaks2": { + "rotate": [ + {}, + { "time": 3.1667, "value": 600 } + ], + "translate": [ + { "x": -2.11 }, + { "time": 1, "x": -2.11, "y": 6.63 }, + { "time": 1.9333, "x": -2.11 } + ], + "scale": [ + { + "x": 1.014, + "y": 1.014, + "curve": [ 0.229, 0.909, 0.501, 0.755, 0.242, 0.892, 0.502, 0.768 ] + }, + { + "time": 0.8667, + "x": 0.745, + "y": 0.745, + "curve": [ 1.282, 0.733, 2.021, 0.699, 1.27, 0.719, 2.071, 0.709 ] + }, + { + "time": 2.2, + "x": 0.7, + "y": 0.704, + "curve": [ 2.315, 0.7, 2.421, 0.794, 2.311, 0.701, 2.485, 0.797 ] + }, + { + "time": 2.5667, + "x": 0.794, + "y": 0.794, + "curve": [ 2.734, 0.794, 2.99, 0.323, 2.714, 0.789, 3.019, 0.341 ] + }, + { "time": 3.1667, "x": 0, "y": 0 } + ] + }, + "portal-shade": { + "translate": [ + { "x": -29.68 } + ], + "scale": [ + { "x": 0.714, "y": 0.714 } + ] + }, + "portal": { + "rotate": [ + {}, + { "time": 3.1667, "value": 600 } + ] + }, + "clipping": { + "translate": [ + { "x": -476.55, "y": 2.27 } + ], + "scale": [ + { "x": 0.983, "y": 1.197 } + ] + }, + "hip": { + "rotate": [ + { + "time": 1.0667, + "value": 22.74, + "curve": [ 1.163, 18.84, 1.77, 8.77 ] + }, + { + "time": 1.9, + "value": 7.82, + "curve": [ 2.271, 5.1, 2.89, 0 ] + }, + { "time": 3.1667 } + ], + "translate": [ + { "x": -899.41, "y": 4.47, "curve": "stepped" }, + { + "time": 1.0667, + "x": -694.16, + "y": 183.28, + "curve": [ 1.091, -602.08, 1.138, -427.59, 1.115, 185.6, 1.171, 133.18 ] + }, + { + "time": 1.2333, + "x": -316.97, + "y": 55.29, + "curve": [ 1.317, -220.27, 1.512, -123.21, 1.271, 8.68, 1.461, -83.18 ] + }, + { + "time": 1.6, + "x": -95.53, + "y": -112.23, + "curve": [ 1.718, -58.25, 2.037, -22.54, 1.858, -166.17, 2.109, -31.4 ] + }, + { + "time": 2.1667, + "x": -14.82, + "y": -31.12, + "curve": [ 2.294, -7.28, 2.442, -7.2, 2.274, -30.6, 2.393, -36.76 ] + }, + { + "time": 2.6, + "x": -7.2, + "y": -36.96, + "curve": [ 2.854, -7.2, 3.071, -11.87, 2.786, -36.27, 3.082, -22.98 ] + }, + { "time": 3.1667, "x": -11.97, "y": -23.15 } + ] + }, + "rear-foot-target": { + "rotate": [ + { "time": 1.0667, "value": 41.6, "curve": "stepped" }, + { + "time": 1.2333, + "value": 41.6, + "curve": [ 1.258, 41.6, 1.379, 35.46 ] + }, + { + "time": 1.4, + "value": 30.09, + "curve": [ 1.412, 27.04, 1.433, 10.65 ] + }, + { "time": 1.4333, "value": -0.28 }, + { "time": 1.6, "value": 2.44 } + ], + "translate": [ + { "x": -899.41, "y": 4.47, "curve": "stepped" }, + { + "time": 1.0667, + "x": -591.13, + "y": 438.46, + "curve": [ 1.076, -539.77, 1.206, -268.1, 1.117, 418.44, 1.21, 333.18 ] + }, + { + "time": 1.2333, + "x": -225.28, + "y": 304.53, + "curve": [ 1.265, -175.22, 1.393, -74.21, 1.296, 226.52, 1.401, 49.61 ] + }, + { + "time": 1.4333, + "x": -52.32, + "y": 0.2, + "curve": [ 1.454, -40.85, 1.616, 40.87, 1.466, 0.17, 1.614, 0.04 ] + }, + { "time": 1.6667, "x": 45.87, "y": 0.01 }, + { "time": 1.9333, "x": 48.87 } + ] + }, + "front-foot-target": { + "rotate": [ + { + "time": 1.0667, + "value": 32.08, + "curve": [ 1.108, 32.08, 1.192, 35.16 ] + }, + { + "time": 1.2333, + "value": 35.16, + "curve": [ 1.258, 35.16, 1.317, 2.23 ] + }, + { + "time": 1.3333, + "value": -4.74, + "curve": [ 1.351, -12.14, 1.429, -34.96 ] + }, + { + "time": 1.6, + "value": -34.77, + "curve": [ 1.765, -34.58, 1.897, -17.25 ] + }, + { "time": 1.9333 } + ], + "translate": [ + { "x": -899.41, "y": 4.47, "curve": "stepped" }, + { + "time": 1.0667, + "x": -533.93, + "y": 363.75, + "curve": [ 1.074, -480.85, 1.18, -261.31, 1.094, 362.3, 1.195, 267.77 ] + }, + { + "time": 1.2333, + "x": -201.23, + "y": 199.93, + "curve": [ 1.269, -161.38, 1.294, -140.32, 1.274, 126.67, 1.308, 77.12 ] + }, + { + "time": 1.3333, + "x": -124.08, + "y": 0.2, + "curve": [ 1.426, -85.6, 1.633, -69.06, 1.45, 0.48, 1.633, 0 ] + }, + { "time": 1.7333, "x": -69.06 } + ] + }, + "torso": { + "rotate": [ + { + "time": 1.0667, + "value": 27.02, + "curve": [ 1.187, 26.86, 1.291, 7.81 ] + }, + { + "time": 1.3333, + "value": -2.62, + "curve": [ 1.402, -19.72, 1.429, -48.64 ] + }, + { + "time": 1.4667, + "value": -56.31, + "curve": [ 1.509, -64.87, 1.62, -77.14 ] + }, + { + "time": 1.7333, + "value": -77.34, + "curve": [ 1.837, -76.89, 1.895, -71.32 ] + }, + { + "time": 2, + "value": -57.52, + "curve": [ 2.104, -43.83, 2.189, -28.59 ] + }, + { + "time": 2.3, + "value": -29.03, + "curve": [ 2.413, -29.48, 2.513, -36.79 ] + }, + { + "time": 2.6667, + "value": -36.79, + "curve": [ 2.814, -36.95, 2.947, -22.88 ] + }, + { "time": 3.1667, "value": -22.88 } + ] + }, + "neck": { + "rotate": [ + { + "time": 1.0667, + "value": -3.57, + "curve": [ 1.146, -3.66, 1.15, -13.5 ] + }, + { + "time": 1.2333, + "value": -13.5, + "curve": [ 1.428, -13.5, 1.443, 11.58 ] + }, + { + "time": 1.5667, + "value": 11.42, + "curve": [ 1.658, 11.3, 1.775, 3.78 ] + }, + { + "time": 1.8667, + "value": 3.78, + "curve": [ 1.92, 3.78, 2.036, 8.01 ] + }, + { + "time": 2.1, + "value": 7.93, + "curve": [ 2.266, 7.72, 2.42, 3.86 ] + }, + { + "time": 2.5333, + "value": 3.86, + "curve": [ 2.783, 3.86, 3.004, 3.78 ] + }, + { "time": 3.1667, "value": 3.78 } + ] + }, + "head": { + "rotate": [ + { + "time": 1.0667, + "value": 16.4, + "curve": [ 1.133, 9.9, 1.207, 1.87 ] + }, + { + "time": 1.3333, + "value": 1.67, + "curve": [ 1.46, 1.56, 1.547, 47.54 ] + }, + { + "time": 1.7333, + "value": 47.55, + "curve": [ 1.897, 47.56, 2.042, 5.68 ] + }, + { + "time": 2.0667, + "value": 0.86, + "curve": [ 2.074, -0.61, 2.086, -2.81 ] + }, + { + "time": 2.1, + "value": -5.31, + "curve": [ 2.145, -13.07, 2.216, -23.65 ] + }, + { + "time": 2.2667, + "value": -23.71, + "curve": [ 2.334, -23.79, 2.426, -13.43 ] + }, + { + "time": 2.4667, + "value": -9.18, + "curve": [ 2.498, -5.91, 2.604, 2.53 ] + }, + { + "time": 2.6667, + "value": 2.52, + "curve": [ 2.738, 2.24, 2.85, -8.76 ] + }, + { + "time": 2.9333, + "value": -8.67, + "curve": [ 3.036, -8.55, 3.09, -7.09 ] + }, + { "time": 3.1667, "value": -6.75 } + ], + "scale": [ + { + "time": 1.3333, + "curve": [ 1.392, 1, 1.526, 1, 1.392, 1, 1.508, 1.043 ] + }, + { + "time": 1.5667, + "x": 0.992, + "y": 1.043, + "curve": [ 1.598, 0.985, 1.676, 0.955, 1.584, 1.043, 1.672, 1.04 ] + }, + { + "time": 1.7333, + "x": 0.954, + "y": 1.029, + "curve": [ 1.843, 0.954, 1.933, 1, 1.825, 1.013, 1.933, 1 ] + }, + { "time": 2 } + ] + }, + "rear-upper-arm": { + "rotate": [ + { + "time": 0.9, + "value": 39.24, + "curve": [ 0.968, 39.93, 1.267, 85.31 ] + }, + { + "time": 1.4667, + "value": 112.27, + "curve": [ 1.555, 124.24, 1.576, 126.44 ] + }, + { + "time": 1.6333, + "value": 126.44, + "curve": [ 1.782, 126.44, 1.992, 94.55 ] + }, + { + "time": 2.1, + "value": 79.96, + "curve": [ 2.216, 64.26, 2.407, 34.36 ] + }, + { + "time": 2.5667, + "value": 33.38, + "curve": [ 2.815, 31.87, 3.1, 39.2 ] + }, + { "time": 3.1667, "value": 39.2 } + ] + }, + "back-foot-tip": { + "rotate": [ + { + "time": 1.0667, + "value": 56.07, + "curve": [ 1.138, 59.21, 1.192, 59.65 ] + }, + { + "time": 1.2333, + "value": 59.46, + "curve": [ 1.295, 59.17, 1.45, 22.54 ] + }, + { "time": 1.4667, "value": -0.84 } + ] + }, + "front-upper-arm": { + "rotate": [ + { + "time": 1.0667, + "value": 118.03, + "curve": [ 1.075, 93.64, 1.358, -34.03 ] + }, + { + "time": 1.6667, + "value": -33.94, + "curve": [ 1.808, -33.89, 1.879, -25 ] + }, + { + "time": 1.9667, + "value": -25.19, + "curve": [ 2.09, -25.46, 2.312, -34.58 ] + }, + { + "time": 2.3667, + "value": -38.36, + "curve": [ 2.465, -45.18, 2.557, -60.1 ] + }, + { + "time": 2.8333, + "value": -61.1, + "curve": [ 2.843, -61.06, 3.16, -60.87 ] + }, + { "time": 3.1667, "value": -60.87 } + ] + }, + "front-bracer": { + "rotate": [ + { + "time": 1.0667, + "value": 0.66, + "curve": [ 1.108, 0.66, 1.221, 44.95 ] + }, + { + "time": 1.2333, + "value": 49.25, + "curve": [ 1.263, 59.42, 1.342, 68.06 ] + }, + { + "time": 1.3667, + "value": 68.34, + "curve": [ 1.409, 68.8, 1.476, 4.9 ] + }, + { + "time": 1.5, + "value": -2.05, + "curve": [ 1.529, -10.3, 1.695, -15.95 ] + }, + { + "time": 1.7333, + "value": -17.38, + "curve": [ 1.807, -20.1, 1.878, -21.19 ] + }, + { + "time": 1.9333, + "value": -21.08, + "curve": [ 2.073, -20.8, 2.146, -7.63 ] + }, + { + "time": 2.1667, + "value": -3.64, + "curve": [ 2.186, 0.12, 2.275, 15.28 ] + }, + { + "time": 2.3333, + "value": 21.78, + "curve": [ 2.392, 28.31, 2.575, 37.66 ] + }, + { + "time": 2.7, + "value": 39.43, + "curve": [ 2.947, 42.93, 3.02, 42.46 ] + }, + { "time": 3.1667, "value": 42.46 } + ] + }, + "front-thigh": { + "translate": [ + { "time": 1.1, "x": -6.41, "y": 18.23, "curve": "stepped" }, + { "time": 1.1333, "x": -6.41, "y": 18.23 }, + { "time": 1.2, "x": 1.61, "y": 3.66 }, + { "time": 1.2333, "x": 4.5, "y": -3.15 }, + { "time": 1.3667, "x": -3.79, "y": 2.94 }, + { "time": 1.4, "x": -8.37, "y": 8.72 }, + { "time": 1.4333, "x": -11.26, "y": 16.99 }, + { "time": 1.4667, "x": -9.89, "y": 24.73, "curve": "stepped" }, + { "time": 1.8667, "x": -9.89, "y": 24.73 }, + { "time": 2.1 } + ] + }, + "front-foot-tip": { + "rotate": [ + { "time": 1.0667, "value": 42.55, "curve": "stepped" }, + { "time": 1.1333, "value": 42.55 }, + { "time": 1.2333, "value": 17.71 }, + { "time": 1.3667, "value": 3.63 }, + { "time": 1.4333 } + ] + }, + "rear-bracer": { + "rotate": [ + { + "time": 1.0667, + "value": 108.71, + "curve": [ 1.082, 108.29, 1.437, 50.73 ] + }, + { + "time": 1.5667, + "value": 24.87, + "curve": [ 1.62, 14.2, 1.66, -11.74 ] + }, + { + "time": 1.7333, + "value": -11.74, + "curve": [ 1.961, -11.73, 2.172, 1.66 ] + }, + { + "time": 2.2667, + "value": 7.88, + "curve": [ 2.331, 12.13, 2.439, 18.65 ] + }, + { + "time": 2.5333, + "value": 18.72, + "curve": [ 2.788, 18.91, 3.145, -0.3 ] + }, + { "time": 3.1667 } + ] + }, + "front-fist": { + "rotate": [ + { + "time": 1.1, + "value": 6.32, + "curve": [ 1.11, 3.31, 1.153, -5.07 ] + }, + { + "time": 1.2333, + "value": -5.13, + "curve": [ 1.311, -5.19, 1.364, 34.65 ] + }, + { + "time": 1.4667, + "value": 34.53, + "curve": [ 1.574, 34.41, 1.547, -55.78 ] + }, + { + "time": 1.8667, + "value": -54.7, + "curve": [ 1.947, -54.7, 2.03, -53.94 ] + }, + { + "time": 2.1333, + "value": -42.44, + "curve": [ 2.215, -33.42, 2.358, -4.43 ] + }, + { + "time": 2.4, + "value": 0.03, + "curve": [ 2.444, 4.66, 2.536, 8.2 ] + }, + { + "time": 2.6333, + "value": 8.2, + "curve": [ 2.733, 8.19, 2.804, -0.67 ] + }, + { + "time": 2.9, + "value": -0.82, + "curve": [ 3.127, -1.16, 3.093, 0 ] + }, + { "time": 3.1667 } + ] + }, + "gun": { + "rotate": [ + { + "time": 1.2667, + "curve": [ 1.35, 0, 1.549, 7.49 ] + }, + { + "time": 1.6, + "value": 9.5, + "curve": [ 1.663, 12.02, 1.846, 19.58 ] + }, + { + "time": 1.9333, + "value": 19.43, + "curve": [ 1.985, 19.4, 2.057, 2.98 ] + }, + { + "time": 2.2, + "value": 2.95, + "curve": [ 2.304, 3.55, 2.458, 10.8 ] + }, + { + "time": 2.5, + "value": 10.8, + "curve": [ 2.642, 10.8, 2.873, -2.54 ] + }, + { + "time": 2.9333, + "value": -2.55, + "curve": [ 3.09, -2.57, 3.08, 0 ] + }, + { "time": 3.1667 } + ] + }, + "hair2": { + "rotate": [ + { + "time": 1.0667, + "value": 26.19, + "curve": [ 1.158, 26.19, 1.368, 26 ] + }, + { + "time": 1.4333, + "value": 24.43, + "curve": [ 1.534, 22.03, 2, -29.14 ] + }, + { + "time": 2.2, + "value": -29.14, + "curve": [ 2.292, -29.14, 2.475, 6.71 ] + }, + { + "time": 2.5667, + "value": 6.71, + "curve": [ 2.675, 6.71, 2.814, -5.06 ] + }, + { + "time": 2.9, + "value": -5.06, + "curve": [ 2.973, -5.06, 3.123, 0 ] + }, + { "time": 3.1667 } + ] + }, + "hair4": { + "rotate": [ + { + "time": 1.0667, + "value": 5.21, + "curve": [ 1.108, 5.21, 1.192, 26.19 ] + }, + { + "time": 1.2333, + "value": 26.19, + "curve": [ 1.317, 26.19, 1.483, 10.63 ] + }, + { + "time": 1.5667, + "value": 10.63, + "curve": [ 1.627, 10.63, 1.642, 17.91 ] + }, + { + "time": 1.7, + "value": 17.94, + "curve": [ 1.761, 17.97, 1.774, 8.22 ] + }, + { + "time": 1.8, + "value": 3.33, + "curve": [ 1.839, -4.21, 1.95, -22.67 ] + }, + { + "time": 2, + "value": -22.67, + "curve": [ 2.025, -22.67, 2.123, -21.86 ] + }, + { + "time": 2.1667, + "value": -18.71, + "curve": [ 2.228, -14.31, 2.294, -0.3 ] + }, + { + "time": 2.3667, + "value": 6.36, + "curve": [ 2.433, 12.45, 2.494, 19.21 ] + }, + { + "time": 2.6, + "value": 19.21, + "curve": [ 2.729, 19.21, 2.854, 6.75 ] + }, + { + "time": 2.9333, + "value": 4.62, + "curve": [ 3.09, 0.45, 3.062, 0 ] + }, + { "time": 3.1667 } + ] + }, + "hair3": { + "rotate": [ + { + "time": 1.4333, + "curve": [ 1.45, 0, 1.452, 11.29 ] + }, + { + "time": 1.5, + "value": 11.21, + "curve": [ 1.596, 11.06, 1.573, -14.17 ] + }, + { + "time": 1.7333, + "value": -20.4, + "curve": [ 1.851, -24.98, 1.943, -28.45 ] + }, + { + "time": 2.2, + "value": -28.75, + "curve": [ 2.317, -28.75, 2.55, 7.04 ] + }, + { + "time": 2.6667, + "value": 7.04, + "curve": [ 2.792, 7.04, 2.885, -5.19 ] + }, + { + "time": 2.9667, + "value": -5.19, + "curve": [ 3.037, -5.19, 3.096, 0 ] + }, + { "time": 3.1667 } + ] + }, + "hair1": { + "rotate": [ + { + "time": 1.2333, + "curve": [ 1.283, 0, 1.349, 3.99 ] + }, + { + "time": 1.4333, + "value": 6.58, + "curve": [ 1.497, 8.54, 1.683, 9.35 ] + }, + { + "time": 1.7667, + "value": 9.35, + "curve": [ 1.825, 9.35, 1.945, -8.71 ] + }, + { + "time": 2, + "value": -11.15, + "curve": [ 2.058, -13.71, 2.2, -14.97 ] + }, + { + "time": 2.2667, + "value": -14.97, + "curve": [ 2.367, -14.97, 2.567, 18.77 ] + }, + { + "time": 2.6667, + "value": 18.77, + "curve": [ 2.733, 18.77, 2.817, 8.29 ] + }, + { + "time": 2.8667, + "value": 6.51, + "curve": [ 2.988, 2.17, 3.058, 0 ] + }, + { "time": 3.1667 } + ] + }, + "flare1": { + "rotate": [ + { "time": 1.1, "value": 8.2 } + ], + "translate": [ + { "time": 1.1, "x": -19.97, "y": 149.68 }, + { "time": 1.2, "x": 3.85, "y": 152.43 }, + { "time": 1.2333, "x": -15.42, "y": 152.29 } + ], + "scale": [ + { + "time": 1.1, + "x": 0.805, + "y": 0.805, + "curve": [ 1.119, 0.763, 1.16, 1.162, 1.117, 0.805, 1.15, 0.605 ] + }, + { + "time": 1.1667, + "x": 1.279, + "y": 0.605, + "curve": [ 1.177, 1.47, 1.192, 2.151, 1.175, 0.605, 1.192, 0.911 ] + }, + { + "time": 1.2, + "x": 2.151, + "y": 0.911, + "curve": [ 1.208, 2.151, 1.231, 1.668, 1.208, 0.911, 1.227, 0.844 ] + }, + { + "time": 1.2333, + "x": 1.608, + "y": 0.805, + "curve": [ 1.249, 1.205, 1.283, 0.547, 1.254, 0.685, 1.283, 0.416 ] + }, + { "time": 1.3, "x": 0.547, "y": 0.416 } + ], + "shear": [ + { "time": 1.1, "y": 4.63 }, + { "time": 1.2333, "x": -5.74, "y": 4.63 } + ] + }, + "flare2": { + "rotate": [ + { "time": 1.1, "value": 12.29 } + ], + "translate": [ + { "time": 1.1, "x": -8.63, "y": 132.96 }, + { "time": 1.2, "x": 4.35, "y": 132.93 } + ], + "scale": [ + { "time": 1.1, "x": 0.864, "y": 0.864 }, + { "time": 1.1667, "x": 0.945, "y": 0.945 }, + { "time": 1.2, "x": 1.511, "y": 1.081 } + ], + "shear": [ + { "time": 1.1, "y": 24.03 } + ] + }, + "flare3": { + "rotate": [ + { "time": 1.1667, "value": 2.88 } + ], + "translate": [ + { "time": 1.1667, "x": 3.24, "y": 114.81 } + ], + "scale": [ + { "time": 1.1667, "x": 0.668, "y": 0.668 } + ], + "shear": [ + { "time": 1.1667, "y": 38.59 } + ] + }, + "flare4": { + "rotate": [ + { "time": 1.1667, "value": -8.64 } + ], + "translate": [ + { "time": 1.1667, "x": -3.82, "y": 194.06 }, + { "time": 1.2667, "x": -1.82, "y": 198.47, "curve": "stepped" }, + { "time": 1.3, "x": -1.94, "y": 187.81 } + ], + "scale": [ + { "time": 1.1667, "x": 0.545, "y": 0.545 }, + { "time": 1.2667, "x": 0.757, "y": 0.757 } + ], + "shear": [ + { "time": 1.1667, "x": 7.42, "y": -22.04 } + ] + }, + "flare5": { + "translate": [ + { "time": 1.2, "x": -11.17, "y": 176.42 }, + { "time": 1.2333, "x": -8.56, "y": 179.04, "curve": "stepped" }, + { "time": 1.3, "x": -14.57, "y": 168.69 } + ], + "scale": [ + { "time": 1.2333, "x": 1.146 }, + { "time": 1.3, "x": 0.703, "y": 0.61 } + ], + "shear": [ + { "time": 1.2, "x": 6.9 } + ] + }, + "flare6": { + "rotate": [ + { "time": 1.2333, "value": -5.36 }, + { "time": 1.2667, "value": -0.54 } + ], + "translate": [ + { "time": 1.2333, "x": 14.52, "y": 204.67 }, + { "time": 1.2667, "x": 19.16, "y": 212.9, "curve": "stepped" }, + { "time": 1.3, "x": 9.23, "y": 202.85 } + ], + "scale": [ + { "time": 1.2333, "x": 0.777, "y": 0.49 }, + { "time": 1.2667, "x": 0.777, "y": 0.657 }, + { "time": 1.3, "x": 0.475, "y": 0.401 } + ] + }, + "flare7": { + "rotate": [ + { "time": 1.1, "value": 5.98 }, + { "time": 1.1333, "value": 32.82 } + ], + "translate": [ + { "time": 1.1, "x": -6.34, "y": 112.98 }, + { "time": 1.1333, "x": 2.66, "y": 111.6 } + ], + "scale": [ + { "time": 1.1, "x": 0.588, "y": 0.588 } + ], + "shear": [ + { "time": 1.1333, "x": -19.93 } + ] + }, + "flare8": { + "rotate": [ + { "time": 1.2333, "value": -6.85 } + ], + "translate": [ + { "time": 1.1667, "x": 66.67, "y": 125.52, "curve": "stepped" }, + { "time": 1.2, "x": 58.24, "y": 113.53, "curve": "stepped" }, + { "time": 1.2333, "x": 40.15, "y": 114.69 } + ], + "scale": [ + { "time": 1.1667, "x": 1.313, "y": 1.203 }, + { "time": 1.2333, "x": 1.038, "y": 0.95 } + ], + "shear": [ + { "time": 1.2, "y": -13.01 } + ] + }, + "flare9": { + "rotate": [ + { "time": 1.1667, "value": 2.9 } + ], + "translate": [ + { "time": 1.1667, "x": 28.45, "y": 151.35, "curve": "stepped" }, + { "time": 1.2, "x": 48.8, "y": 191.09, "curve": "stepped" }, + { "time": 1.2333, "x": 52, "y": 182.52, "curve": "stepped" }, + { "time": 1.2667, "x": 77.01, "y": 195.96 } + ], + "scale": [ + { "time": 1.1667, "x": 0.871, "y": 1.073 }, + { "time": 1.2, "x": 0.927, "y": 0.944 }, + { "time": 1.2333, "x": 1.165, "y": 1.336 } + ], + "shear": [ + { "time": 1.1667, "x": 7.95, "y": 25.48 } + ] + }, + "flare10": { + "rotate": [ + { "time": 1.1667, "value": 2.18 } + ], + "translate": [ + { "time": 1.1667, "x": 55.64, "y": 137.64, "curve": "stepped" }, + { "time": 1.2, "x": 90.49, "y": 151.07, "curve": "stepped" }, + { "time": 1.2333, "x": 114.06, "y": 153.05, "curve": "stepped" }, + { "time": 1.2667, "x": 90.44, "y": 164.61 } + ], + "scale": [ + { "time": 1.1667, "x": 2.657, "y": 0.891 }, + { "time": 1.2, "x": 3.314, "y": 1.425 }, + { "time": 1.2333, "x": 2.871, "y": 0.924 }, + { "time": 1.2667, "x": 2.317, "y": 0.775 } + ], + "shear": [ + { "time": 1.1667, "x": -1.35 } + ] + }, + "torso2": { + "rotate": [ + { + "time": 1, + "curve": [ 1.117, 0, 1.255, 24.94 ] + }, + { + "time": 1.4, + "value": 24.94, + "curve": [ 1.477, 24.94, 1.59, -17.62 ] + }, + { + "time": 1.6333, + "value": -19.48, + "curve": [ 1.717, -23.1, 1.784, -26.12 ] + }, + { + "time": 1.9333, + "value": -26.14, + "curve": [ 2.067, -26.15, 2.158, 4.3 ] + }, + { + "time": 2.3, + "value": 4.22, + "curve": [ 2.45, 4.13, 2.579, -1.76 ] + }, + { + "time": 2.7333, + "value": -1.8, + "curve": [ 2.816, -1.82, 2.857, -2.94 ] + }, + { + "time": 2.9333, + "value": -2.99, + "curve": [ 3.056, -3.08, 3.09, 0 ] + }, + { "time": 3.1667 } + ] + }, + "torso3": { + "rotate": [ + { + "time": 1.3, + "curve": [ 1.352, 0, 1.408, 6.47 ] + }, + { + "time": 1.4667, + "value": 6.43, + "curve": [ 1.55, 6.39, 1.723, -5.05 ] + }, + { + "time": 1.7333, + "value": -5.53, + "curve": [ 1.782, -7.72, 1.843, -16.94 ] + }, + { + "time": 1.9667, + "value": -16.86, + "curve": [ 2.111, -16.78, 2.259, -3.97 ] + }, + { + "time": 2.4, + "value": -2.43, + "curve": [ 2.525, -1.12, 2.639, -0.5 ] + }, + { + "time": 2.7333, + "value": -0.49, + "curve": [ 2.931, -0.47, 2.999, -2.15 ] + }, + { "time": 3.1667, "value": -2.15 } + ] + }, + "head-control": { + "translate": [ + { + "time": 1.2333, + "curve": [ 1.25, 0, 1.474, 6.89, 1.25, 0, 1.496, 0.98 ] + }, + { + "time": 1.6667, + "x": 11.99, + "y": -6.42, + "curve": [ 1.743, 14.01, 1.86, 14.33, 1.785, -11.55, 1.86, -27.1 ] + }, + { + "time": 1.9667, + "x": 13.91, + "y": -26.88, + "curve": [ 2.074, 13.49, 2.244, 8.13, 2.074, -26.65, 2.215, -21.78 ] + }, + { + "time": 2.3, + "x": 6.07, + "y": -16.64, + "curve": [ 2.416, 1.84, 2.497, -1.41, 2.417, -9.57, 2.526, -1.72 ] + }, + { + "time": 2.5667, + "x": -3.78, + "y": -1.71, + "curve": [ 2.661, -6.98, 2.76, -8.76, 2.692, -1.68, 2.821, -15.75 ] + }, + { + "time": 2.9, + "x": -8.32, + "y": -16.7, + "curve": [ 2.962, -8.12, 3.082, -0.04, 2.958, -17.39, 3.089, 0 ] + }, + { "time": 3.1667 } + ] + }, + "front-shoulder": { + "translate": [ + { + "time": 1.3333, + "curve": [ 1.488, 0, 1.717, 0.21, 1.488, 0, 1.688, -30.29 ] + }, + { + "time": 1.9, + "x": 0.83, + "y": -30.29, + "curve": [ 2.078, 1.43, 2.274, 2.88, 2.071, -30.29, 2.289, 4.48 ] + }, + { + "time": 2.4333, + "x": 2.89, + "y": 4.59, + "curve": [ 2.604, 2.89, 2.677, -0.68, 2.57, 4.7, 2.694, -2.43 ] + }, + { + "time": 2.7667, + "x": -0.67, + "y": -2.47, + "curve": [ 2.866, -0.67, 2.986, -0.07, 2.882, -2.47, 3.036, -0.06 ] + }, + { "time": 3.1667 } + ] + } + }, + "ik": { + "rear-leg-ik": [ + { "time": 3.1667, "softness": 10, "bendPositive": false } + ] + } + }, + "run": { + "slots": { + "mouth": { + "attachment": [ + { "name": "mouth-grind" } + ] + } + }, + "bones": { + "front-thigh": { + "translate": [ + { + "x": -5.14, + "y": 11.13, + "curve": [ 0.033, -7.77, 0.112, -9.03, 0.034, 11.13, 0.108, 9.74 ] + }, + { + "time": 0.1667, + "x": -9.03, + "y": 7.99, + "curve": [ 0.23, -9.05, 0.314, -1.34, 0.236, 5.93, 0.28, 3.22 ] + }, + { + "time": 0.3333, + "x": 0.41, + "y": 3.19, + "curve": [ 0.352, 2.09, 0.449, 11.16, 0.384, 3.16, 0.449, 4.98 ] + }, + { + "time": 0.5, + "x": 11.17, + "y": 6.76, + "curve": [ 0.571, 10.79, 0.621, -1.83, 0.542, 8.21, 0.625, 11.13 ] + }, + { "time": 0.6667, "x": -5.14, "y": 11.13 } + ] + }, + "torso": { + "rotate": [ + { + "value": -37.66, + "curve": [ 0.034, -37.14, 0.107, -36.21 ] + }, + { + "time": 0.1333, + "value": -36.21, + "curve": [ 0.158, -36.21, 0.209, -38.8 ] + }, + { + "time": 0.2333, + "value": -38.79, + "curve": [ 0.259, -38.78, 0.313, -38.03 ] + }, + { + "time": 0.3333, + "value": -37.66, + "curve": [ 0.357, -37.21, 0.4, -36.21 ] + }, + { + "time": 0.4333, + "value": -36.21, + "curve": [ 0.458, -36.21, 0.539, -38.8 ] + }, + { + "time": 0.5667, + "value": -38.8, + "curve": [ 0.592, -38.8, 0.645, -38 ] + }, + { "time": 0.6667, "value": -37.66 } + ] + }, + "rear-thigh": { + "translate": [ + { + "x": -16.41, + "y": 1.55, + "curve": [ 0.013, -15.67, 0.183, -8.55, 0.03, 2.39, 0.183, 6.17 ] + }, + { + "time": 0.2333, + "x": -8.55, + "y": 6.17, + "curve": [ 0.308, -8.55, 0.492, -19.75, 0.308, 6.17, 0.492, 0.61 ] + }, + { + "time": 0.5667, + "x": -19.75, + "y": 0.61, + "curve": [ 0.592, -19.75, 0.641, -18.06, 0.592, 0.61, 0.632, 0.78 ] + }, + { "time": 0.6667, "x": -16.41, "y": 1.55 } + ] + }, + "front-upper-arm": { + "rotate": [ + { + "value": -39.03, + "curve": [ 0.051, -0.1, 0.145, 88.36 ] + }, + { + "time": 0.2333, + "value": 88.36, + "curve": [ 0.28, 88.76, 0.324, 59.52 ] + }, + { + "time": 0.3333, + "value": 51.13, + "curve": [ 0.358, 30.2, 0.445, -74.91 ] + }, + { + "time": 0.5667, + "value": -75.82, + "curve": [ 0.599, -76.06, 0.642, -55.72 ] + }, + { "time": 0.6667, "value": -39.03 } + ] + }, + "front-bracer": { + "rotate": [ + { + "value": 20.54, + "curve": [ 0.052, 11.42, 0.089, 0.13 ] + }, + { + "time": 0.1333, + "value": 0.15, + "curve": [ 0.186, 0.17, 0.221, 26.29 ] + }, + { + "time": 0.2333, + "value": 32.37, + "curve": [ 0.247, 39.19, 0.286, 61.45 ] + }, + { + "time": 0.3333, + "value": 61.58, + "curve": [ 0.371, 61.69, 0.42, 55.79 ] + }, + { "time": 0.4667, "value": 49.68 }, + { "time": 0.6667, "value": 20.54 } + ] + }, + "front-fist": { + "rotate": [ + { + "value": -36.16, + "curve": [ 0.014, -38.8, 0.036, -43.27 ] + }, + { + "time": 0.0667, + "value": -43.37, + "curve": [ 0.102, -43.49, 0.182, -28.46 ] + }, + { + "time": 0.2, + "value": -23.04, + "curve": [ 0.23, -13.87, 0.264, 3.86 ] + }, + { + "time": 0.3333, + "value": 3.7, + "curve": [ 0.38, 3.64, 0.535, -16.22 ] + }, + { "time": 0.5667, "value": -21.29 }, + { "time": 0.6667, "value": -36.16 } + ] + }, + "rear-upper-arm": { + "rotate": [ + { + "value": 40.5, + "curve": [ 0.028, 23.74, 0.128, -79.86 ] + }, + { + "time": 0.2333, + "value": -79.87, + "curve": [ 0.38, -79.88, 0.403, 63.25 ] + }, + { + "time": 0.5667, + "value": 64.13, + "curve": [ 0.607, 64.35, 0.644, 53.1 ] + }, + { "time": 0.6667, "value": 40.5 } + ], + "translate": [ + { + "x": -3.79, + "y": -0.77, + "curve": [ 0.044, -4.58, 0.169, -5.48, 0.044, 0.93, 0.169, 2.85 ] + }, + { + "time": 0.2333, + "x": -5.48, + "y": 2.85, + "curve": [ 0.346, -5.48, 0.475, -2.68, 0.346, 2.85, 0.475, -3.13 ] + }, + { + "time": 0.5667, + "x": -2.68, + "y": -3.13, + "curve": [ 0.611, -2.68, 0.642, -3.34, 0.611, -3.13, 0.642, -1.73 ] + }, + { "time": 0.6667, "x": -3.79, "y": -0.77 } + ] + }, + "rear-bracer": { + "rotate": [ + { "value": 28.28 }, + { + "time": 0.2333, + "value": -11.12, + "curve": [ 0.252, -14.12, 0.297, -19.37 ] + }, + { + "time": 0.3333, + "value": -19.38, + "curve": [ 0.435, -19.41, 0.522, 38.96 ] + }, + { + "time": 0.5667, + "value": 38.87, + "curve": [ 0.619, 38.76, 0.644, 32.01 ] + }, + { "time": 0.6667, "value": 28.28 } + ] + }, + "neck": { + "rotate": [ + { + "value": 11.88, + "curve": [ 0.024, 11.4, 0.075, 9.74 ] + }, + { + "time": 0.1, + "value": 9.74, + "curve": [ 0.125, 9.74, 0.208, 13.36 ] + }, + { + "time": 0.2333, + "value": 13.36, + "curve": [ 0.258, 13.36, 0.321, 12.2 ] + }, + { + "time": 0.3333, + "value": 11.88, + "curve": [ 0.365, 11.06, 0.408, 9.72 ] + }, + { + "time": 0.4333, + "value": 9.72, + "curve": [ 0.458, 9.72, 0.542, 13.36 ] + }, + { + "time": 0.5667, + "value": 13.36, + "curve": [ 0.592, 13.36, 0.636, 12.48 ] + }, + { "time": 0.6667, "value": 11.88 } + ] + }, + "head": { + "rotate": [ + { + "value": 13.14, + "curve": [ 0.02, 11.99, 0.039, 8.94 ] + }, + { + "time": 0.0667, + "value": 8.93, + "curve": [ 0.122, 8.9, 0.232, 15.8 ] + }, + { + "time": 0.2667, + "value": 15.81, + "curve": [ 0.325, 15.82, 0.357, 8.95 ] + }, + { + "time": 0.4, + "value": 8.93, + "curve": [ 0.444, 8.91, 0.568, 15.8 ] + }, + { + "time": 0.6, + "value": 15.77, + "curve": [ 0.632, 15.74, 0.649, 14.05 ] + }, + { "time": 0.6667, "value": 13.14 } + ], + "scale": [ + { + "curve": [ 0.014, 0.996, 0.068, 0.991, 0.027, 1.005, 0.083, 1.012 ] + }, + { + "time": 0.1, + "x": 0.991, + "y": 1.012, + "curve": [ 0.128, 0.991, 0.205, 1.018, 0.128, 1.012, 0.197, 0.988 ] + }, + { + "time": 0.2333, + "x": 1.018, + "y": 0.988, + "curve": [ 0.272, 1.018, 0.305, 1.008, 0.262, 0.988, 0.311, 0.995 ] + }, + { + "time": 0.3333, + "curve": [ 0.351, 0.995, 0.417, 0.987, 0.359, 1.006, 0.417, 1.013 ] + }, + { + "time": 0.4333, + "x": 0.987, + "y": 1.013, + "curve": [ 0.467, 0.987, 0.533, 1.02, 0.467, 1.013, 0.533, 0.989 ] + }, + { + "time": 0.5667, + "x": 1.02, + "y": 0.989, + "curve": [ 0.592, 1.02, 0.652, 1.004, 0.592, 0.989, 0.644, 0.996 ] + }, + { "time": 0.6667 } + ] + }, + "gun": { + "rotate": [ + { + "value": 12.36, + "curve": [ 0.022, 16.28, 0.087, 20.25 ] + }, + { + "time": 0.1333, + "value": 20.19, + "curve": [ 0.168, 20.32, 0.254, -8.82 ] + }, + { + "time": 0.2667, + "value": -11.88, + "curve": [ 0.291, -17.91, 0.344, -24.11 ] + }, + { + "time": 0.4, + "value": -23.88, + "curve": [ 0.448, -23.69, 0.533, -15.47 ] + }, + { "time": 0.5667, "value": -8.69 }, + { "time": 0.6667, "value": 12.36 } + ] + }, + "hip": { + "rotate": [ + { "value": -8.24 } + ], + "translate": [ + { + "x": -3.6, + "y": -34.1, + "curve": [ 0.042, -3.84, 0.118, 7.62, 0.042, -33.74, 0.112, 20.55 ] + }, + { + "time": 0.1667, + "x": 7.61, + "y": 20.36, + "curve": [ 0.194, 7.6, 0.21, 5.06, 0.204, 20.65, 0.217, -8.69 ] + }, + { + "time": 0.2333, + "x": 1.68, + "y": -18.48, + "curve": [ 0.279, -4.99, 0.297, -5.64, 0.254, -31.08, 0.292, -34.55 ] + }, + { + "time": 0.3333, + "x": -5.76, + "y": -35, + "curve": [ 0.379, -5.9, 0.451, 6.8, 0.384, -35.56, 0.428, 17.6 ] + }, + { + "time": 0.5, + "x": 6.61, + "y": 17.01, + "curve": [ 0.536, 6.47, 0.545, 3.56, 0.533, 16.75, 0.548, -8.71 ] + }, + { + "time": 0.5667, + "x": 0.35, + "y": -18.81, + "curve": [ 0.597, -4.07, 0.642, -3.45, 0.584, -28.58, 0.642, -34.32 ] + }, + { "time": 0.6667, "x": -3.6, "y": -34.1 } + ] + }, + "front-foot-target": { + "rotate": [ + { + "value": -62.54, + "curve": [ 0.015, -74.19, 0.056, -103.19 ] + }, + { + "time": 0.0667, + "value": -111.08, + "curve": [ 0.092, -129.44, 0.189, -146.55 ] + }, + { + "time": 0.2333, + "value": -146.32, + "curve": [ 0.285, -146.06, 0.32, -125.1 ] + }, + { "time": 0.3333, "value": -117.24 }, + { + "time": 0.5, + "value": -35.07, + "curve": [ 0.522, -28.64, 0.546, -24.84 ] + }, + { + "time": 0.5667, + "value": -24.9, + "curve": [ 0.595, -25, 0.623, -40.82 ] + }, + { "time": 0.6667, "value": -62.54 } + ], + "translate": [ + { "x": 16.34, "y": 0.18 }, + { + "time": 0.0667, + "x": -101.43, + "y": 8.04, + "curve": [ 0.085, -131.35, 0.129, -207.69, 0.08, 14.9, 0.124, 113.28 ] + }, + { + "time": 0.1667, + "x": -207.92, + "y": 145.81, + "curve": [ 0.196, -208.13, 0.21, -202.91, 0.186, 160.26, 0.206, 163.48 ] + }, + { + "time": 0.2333, + "x": -189.94, + "y": 163.85, + "curve": [ 0.27, -169.94, 0.31, -126.19, 0.269, 164.35, 0.316, 85.97 ] + }, + { + "time": 0.3333, + "x": -90.56, + "y": 78.57, + "curve": [ 0.355, -57.99, 0.376, -29.14, 0.35, 71.55, 0.376, 66.4 ] + }, + { + "time": 0.4, + "x": 2.87, + "y": 66.38, + "curve": [ 0.412, 19.24, 0.469, 90.73, 0.429, 66.37, 0.469, 70.66 ] + }, + { + "time": 0.5, + "x": 117.18, + "y": 70.46, + "curve": [ 0.522, 136.24, 0.542, 151.33, 0.539, 70.2, 0.555, 38.25 ] + }, + { + "time": 0.5667, + "x": 151.49, + "y": 25.29, + "curve": [ 0.578, 146.76, 0.586, 133.13, 0.572, 19.7, 0.582, 12.23 ] + }, + { "time": 0.6, "x": 115.02, "y": 0.1 }, + { "time": 0.6667, "x": 16.34, "y": 0.18 } + ] + }, + "front-leg-target": { + "translate": [ + { "x": -13.95, "y": -30.34 } + ] + }, + "rear-foot-target": { + "rotate": [ + { "value": 18.55 }, + { + "time": 0.2333, + "value": 167.84, + "curve": [ 0.246, 153.66, 0.256, 129.74 ] + }, + { + "time": 0.2667, + "value": 124.32, + "curve": [ 0.296, 124.43, 0.313, 129.93 ] + }, + { + "time": 0.3667, + "value": 129.87, + "curve": [ 0.421, 128.32, 0.519, 0.98 ] + }, + { + "time": 0.5667, + "curve": [ 0.6, 0.27, 0.642, 4.73 ] + }, + { "time": 0.6667, "value": 18.55 } + ], + "translate": [ + { + "x": -176.39, + "y": 134.12, + "curve": [ 0.018, -142.26, 0.054, -94.41, 0.01, 120.96, 0.044, 84.08 ] + }, + { + "time": 0.0667, + "x": -73.56, + "y": 76.68, + "curve": [ 0.086, -42.82, 0.194, 101.2, 0.098, 66.73, 0.198, 60.88 ] + }, + { "time": 0.2333, "x": 98.32, "y": 32.17 }, + { "time": 0.2667, "x": 49.13, "y": -0.63 }, + { + "time": 0.4, + "x": -147.9, + "y": 0.32, + "curve": [ 0.414, -168.78, 0.478, -284.76, 0.43, 30.09, 0.478, 129.14 ] + }, + { + "time": 0.5, + "x": -283.37, + "y": 167.12, + "curve": [ 0.526, -285.66, 0.548, -280.54, 0.516, 194.84, 0.55, 216.53 ] + }, + { + "time": 0.5667, + "x": -266.98, + "y": 216.12, + "curve": [ 0.581, -256.27, 0.643, -206.54, 0.61, 214.82, 0.65, 145.33 ] + }, + { "time": 0.6667, "x": -176.39, "y": 134.12 } + ] + }, + "rear-leg-target": { + "translate": [ + { "x": 85, "y": -33.59 } + ] + }, + "back-foot-tip": { + "rotate": [ + { + "value": -147.04, + "curve": [ 0.033, -113.4, 0.161, 44.34 ] + }, + { + "time": 0.2333, + "value": 43.48, + "curve": [ 0.24, 43.41, 0.282, 35.72 ] + }, + { + "time": 0.3, + "value": 0.29, + "curve": [ 0.347, 0.28, 0.396, 4.27 ] + }, + { + "time": 0.4, + "curve": [ 0.424, -23.8, 0.525, -181.39 ] + }, + { + "time": 0.5667, + "value": -181.39, + "curve": [ 0.592, -181.39, 0.642, -169.09 ] + }, + { "time": 0.6667, "value": -147.04 } + ] + }, + "front-foot-tip": { + "rotate": [ + { + "value": -0.25, + "curve": [ 0.008, -0.25, 0.056, 1.73 ] + }, + { + "time": 0.0667, + "value": -7.68, + "curve": [ 0.075, -43.13, 0.15, -130.44 ] + }, + { + "time": 0.2, + "value": -130.08, + "curve": [ 0.239, -129.79, 0.272, -126.8 ] + }, + { + "time": 0.3, + "value": -116.24, + "curve": [ 0.333, -103.91, 0.348, -86.1 ] + }, + { + "time": 0.3667, + "value": -71.08, + "curve": [ 0.386, -55.25, 0.415, -32.44 ] + }, + { + "time": 0.4333, + "value": -21.63, + "curve": [ 0.47, -0.01, 0.542, 33.42 ] + }, + { + "time": 0.5667, + "value": 33.2, + "curve": [ 0.622, 32.7, 0.569, 0.64 ] + }, + { "time": 0.6667, "value": -0.25 } + ] + }, + "hair1": { + "rotate": [ + { + "value": -6.81, + "curve": [ 0.087, -6.81, 0.143, -5.75 ] + }, + { + "time": 0.1667, + "value": -4.3, + "curve": [ 0.183, -3.28, 0.209, 2.79 ] + }, + { + "time": 0.2333, + "value": 2.78, + "curve": [ 0.262, 2.77, 0.305, -6.63 ] + }, + { + "time": 0.3333, + "value": -6.64, + "curve": [ 0.419, -6.68, 0.49, -4.84 ] + }, + { + "time": 0.5, + "value": -4.38, + "curve": [ 0.518, -3.56, 0.574, 2.32 ] + }, + { + "time": 0.6, + "value": 2.33, + "curve": [ 0.643, 2.35, 0.633, -6.81 ] + }, + { "time": 0.6667, "value": -6.81 } + ] + }, + "hair2": { + "rotate": [ + { + "value": -6.81, + "curve": [ 0.014, -3.17, 0.109, 43.93 ] + }, + { + "time": 0.1333, + "value": 43.95, + "curve": [ 0.177, 43.97, 0.192, -13.76 ] + }, + { + "time": 0.2667, + "value": -13.83, + "curve": [ 0.302, -13.72, 0.322, -8.86 ] + }, + { + "time": 0.3333, + "value": -6.6, + "curve": [ 0.349, -3.5, 0.436, 41.1 ] + }, + { + "time": 0.4667, + "value": 41.05, + "curve": [ 0.51, 40.99, 0.549, -14.06 ] + }, + { + "time": 0.6, + "value": -14.18, + "curve": [ 0.63, -14.26, 0.656, -9.04 ] + }, + { "time": 0.6667, "value": -6.81 } + ] + }, + "hair3": { + "rotate": [ + { + "value": -6.81, + "curve": [ 0.079, -6.83, 0.108, 0.3 ] + }, + { + "time": 0.1333, + "value": 1.96, + "curve": [ 0.177, 4.89, 0.208, 6.28 ] + }, + { + "time": 0.2333, + "value": 6.29, + "curve": [ 0.313, 6.31, 0.383, 3.49 ] + }, + { + "time": 0.4, + "value": 2.58, + "curve": [ 0.442, 0.28, 0.523, -6.81 ] + }, + { "time": 0.6, "value": -6.81 } + ] + }, + "hair4": { + "rotate": [ + { + "value": -6.81, + "curve": [ 0.011, -4.06, 0.108, 24.92 ] + }, + { + "time": 0.1333, + "value": 24.92, + "curve": [ 0.158, 24.92, 0.208, -10.62 ] + }, + { + "time": 0.2333, + "value": -10.62, + "curve": [ 0.254, -10.62, 0.312, -9.73 ] + }, + { + "time": 0.3333, + "value": -6.4, + "curve": [ 0.356, -2.95, 0.438, 24.93 ] + }, + { + "time": 0.4667, + "value": 24.93, + "curve": [ 0.492, 24.93, 0.575, -9.78 ] + }, + { + "time": 0.6, + "value": -9.78, + "curve": [ 0.617, -9.78, 0.655, -8.63 ] + }, + { "time": 0.6667, "value": -6.81 } + ] + }, + "torso2": { + "rotate": [ + { + "value": 3.5, + "curve": [ 0.07, 3.51, 0.075, 8.69 ] + }, + { + "time": 0.1, + "value": 8.69, + "curve": [ 0.139, 8.69, 0.214, 6.9 ] + }, + { + "time": 0.2333, + "value": 6.33, + "curve": [ 0.266, 5.34, 0.285, 3.48 ] + }, + { + "time": 0.3333, + "value": 3.48, + "curve": [ 0.398, 3.48, 0.408, 8.68 ] + }, + { + "time": 0.4333, + "value": 8.68, + "curve": [ 0.458, 8.68, 0.551, 6.8 ] + }, + { + "time": 0.5667, + "value": 6.26, + "curve": [ 0.598, 5.17, 0.642, 3.49 ] + }, + { "time": 0.6667, "value": 3.5 } + ] + }, + "torso3": { + "rotate": [ + { + "value": 4.52, + "curve": [ 0.067, 4.54, 0.075, -7.27 ] + }, + { + "time": 0.1, + "value": -7.27, + "curve": [ 0.125, -7.27, 0.227, 0.84 ] + }, + { + "time": 0.2333, + "value": 1.24, + "curve": [ 0.254, 2.5, 0.301, 4.51 ] + }, + { + "time": 0.3333, + "value": 4.52, + "curve": [ 0.386, 4.54, 0.408, -7.35 ] + }, + { + "time": 0.4333, + "value": -7.35, + "curve": [ 0.458, -7.35, 0.549, -0.14 ] + }, + { + "time": 0.5667, + "value": 0.95, + "curve": [ 0.586, 2.18, 0.632, 4.54 ] + }, + { "time": 0.6667, "value": 4.52 } + ] + }, + "aim-constraint-target": { + "rotate": [ + { "value": 30.57 } + ] + }, + "rear-foot": { + "rotate": [ + { "value": -6.5 } + ] + }, + "front-foot": { + "rotate": [ + { "value": 4.5 } + ] + }, + "head-control": { + "translate": [ + { + "y": -9.94, + "curve": [ 0.058, 0, 0.175, -15.32, 0.044, -4.19, 0.175, 5 ] + }, + { + "time": 0.2333, + "x": -15.32, + "y": 5, + "curve": [ 0.317, -15.32, 0.429, -9.74, 0.317, 5, 0.382, -31.71 ] + }, + { + "time": 0.4667, + "x": -7.81, + "y": -31.59, + "curve": [ 0.507, -5.76, 0.617, 0, 0.549, -31.47, 0.628, -13.33 ] + }, + { "time": 0.6667, "y": -9.94 } + ] + }, + "front-shoulder": { + "translate": [ + { + "x": -0.74, + "y": 11.22, + "curve": [ 0.061, -0.74, 0.144, 1.17, 0.061, 11.22, 0.143, -17.93 ] + }, + { + "time": 0.2333, + "x": 1.19, + "y": -17.9, + "curve": [ 0.54, 1.25, 0.558, -0.74, 0.545, -17.8, 0.558, 11.22 ] + }, + { "time": 0.6667, "x": -0.74, "y": 11.22 } + ] + }, + "back-shoulder": { + "translate": [ + { + "curve": [ 0.083, 0, 0.25, 0, 0.083, 0, 0.25, 8.93 ] + }, + { + "time": 0.3333, + "y": 8.93, + "curve": [ 0.417, 0, 0.583, 0, 0.417, 8.93, 0.583, 0 ] + }, + { "time": 0.6667 } + ] + } + }, + "ik": { + "front-leg-ik": [ + { "softness": 10, "bendPositive": false }, + { "time": 0.5667, "softness": 14.8, "bendPositive": false }, + { "time": 0.6, "softness": 48.2, "bendPositive": false }, + { "time": 0.6667, "softness": 10, "bendPositive": false } + ], + "rear-leg-ik": [ + { "bendPositive": false }, + { "time": 0.1667, "softness": 22.5, "bendPositive": false }, + { "time": 0.3, "softness": 61.4, "bendPositive": false }, + { "time": 0.6667, "bendPositive": false } + ] + }, + "events": [ + { "time": 0.2333, "name": "footstep" }, + { "time": 0.5667, "name": "footstep" } + ] + }, + "run-to-idle": { + "slots": { + "front-fist": { + "attachment": [ + { "name": "front-fist-open" } + ] + } + }, + "bones": { + "front-foot-target": { + "translate": [ + { + "x": -16.5, + "y": 3.41, + "curve": [ 0.033, -16.5, 0.1, -69.06, 0.033, 3.41, 0.1, 0 ] + }, + { "time": 0.1333, "x": -69.06 } + ] + }, + "hip": { + "translate": [ + { + "x": -28.78, + "y": -72.96, + "curve": [ 0.036, -28.63, 0.2, -10.85, 0.135, -62.35, 0.2, -23.15 ] + }, + { "time": 0.2667, "x": -11.97, "y": -23.15 } + ] + }, + "rear-foot-target": { + "translate": [ + { + "x": 33.15, + "y": 31.61, + "curve": [ 0.017, 33.15, 0.05, 24.41, 0.017, 31.61, 0.041, 20.73 ] + }, + { + "time": 0.0667, + "x": 24.41, + "y": 0.19, + "curve": [ 0.117, 24.41, 0.217, 48.87, 0.117, 0.19, 0.217, 0 ] + }, + { "time": 0.2667, "x": 48.87 } + ] + }, + "front-upper-arm": { + "rotate": [ + { + "value": -80.61, + "curve": [ 0.067, -80.61, 0.2, -60.87 ] + }, + { "time": 0.2667, "value": -60.87 } + ] + }, + "front-bracer": { + "rotate": [ + { + "value": 8.79, + "curve": [ 0.041, 8.79, 0.115, 6.3 ] + }, + { + "time": 0.1667, + "value": 6.41, + "curve": [ 0.201, 6.48, 0.241, 42.46 ] + }, + { "time": 0.2667, "value": 42.46 } + ] + }, + "rear-upper-arm": { + "rotate": [ + { + "value": 55.3, + "curve": [ 0.067, 55.3, 0.2, 39.2 ] + }, + { "time": 0.2667, "value": 39.2 } + ] + }, + "head": { + "rotate": [ + { + "curve": [ 0.05, 0, 0.083, 2.67 ] + }, + { + "time": 0.1333, + "value": 2.67, + "curve": [ 0.15, 2.67, 0.25, -6.75 ] + }, + { "time": 0.2667, "value": -6.75 } + ] + }, + "front-fist": { + "rotate": [ + { + "value": 38.26, + "curve": [ 0.041, 38.26, 0.127, -2.19 ] + }, + { + "time": 0.1667, + "value": -3, + "curve": [ 0.209, -3.84, 0.241, 0 ] + }, + { "time": 0.2667 } + ], + "scale": [ + { + "x": 0.844, + "curve": [ 0.067, 0.844, 0.2, 1, 0.067, 1, 0.2, 1 ] + }, + { "time": 0.2667 } + ] + }, + "rear-bracer": { + "rotate": [ + { + "value": 57.24, + "curve": [ 0.067, 57.24, 0.2, 0 ] + }, + { "time": 0.2667 } + ] + }, + "gun": { + "rotate": [ + { + "value": 2.28, + "curve": [ 0.041, 2.28, 0.105, 15.34 ] + }, + { + "time": 0.1667, + "value": 15.32, + "curve": [ 0.205, 15.31, 0.241, 0 ] + }, + { "time": 0.2667 } + ] + }, + "torso": { + "rotate": [ + { + "value": -12.98, + "curve": [ 0.033, -12.98, 0.103, -14.81 ] + }, + { + "time": 0.1333, + "value": -16.63, + "curve": [ 0.168, -18.69, 0.233, -22.88 ] + }, + { "time": 0.2667, "value": -22.88 } + ], + "scale": [ + { + "x": 0.963, + "y": 1.074, + "curve": [ 0.067, 0.963, 0.132, 1, 0.067, 1.074, 0.132, 1 ] + }, + { "time": 0.2667 } + ] + }, + "neck": { + "rotate": [ + {}, + { "time": 0.2667, "value": 3.78 } + ] + }, + "hair3": { + "rotate": [ + { + "curve": [ 0.033, 0, 0.1, 0.88 ] + }, + { + "time": 0.1333, + "value": 0.88, + "curve": [ 0.167, 0.88, 0.233, 0 ] + }, + { "time": 0.2667 } + ] + }, + "hair4": { + "rotate": [ + { + "curve": [ 0.033, 0, 0.1, 15.97 ] + }, + { + "time": 0.1333, + "value": 15.97, + "curve": [ 0.167, 15.97, 0.233, 0 ] + }, + { "time": 0.2667 } + ] + }, + "hair1": { + "rotate": [ + { + "curve": [ 0.033, 0, 0.1, 10.76 ] + }, + { + "time": 0.1333, + "value": 10.76, + "curve": [ 0.167, 10.76, 0.233, 0 ] + }, + { "time": 0.2667 } + ] + }, + "hair2": { + "rotate": [ + { + "curve": [ 0.014, -2.28, 0.042, -7.84 ] + }, + { + "time": 0.0667, + "value": -7.82, + "curve": [ 0.108, -7.79, 0.166, 6.57 ] + }, + { + "time": 0.2, + "value": 6.67, + "curve": [ 0.222, 6.73, 0.255, 1.98 ] + }, + { "time": 0.2667 } + ] + }, + "torso2": { + "rotate": [ + { + "curve": [ 0.041, 0, 0.107, 3.03 ] + }, + { + "time": 0.1667, + "value": 3.03, + "curve": [ 0.205, 3.03, 0.241, 0 ] + }, + { "time": 0.2667 } + ] + }, + "torso3": { + "rotate": [ + { + "curve": [ 0.049, 0, 0.166, 0.66 ] + }, + { + "time": 0.2, + "value": 0.66, + "curve": [ 0.232, 0.65, 0.249, -2.15 ] + }, + { "time": 0.2667, "value": -2.15 } + ] + }, + "head-control": { + "translate": [ + { "x": -10.12, "y": 8.71 }, + { "time": 0.2667 } + ] + }, + "front-shoulder": { + "translate": [ + { "x": 4.91, "y": 11.54 }, + { "time": 0.2667 } + ] + } + } + }, + "shoot": { + "slots": { + "muzzle": { + "rgba": [ + { "time": 0.1333, "color": "ffffffff" }, + { "time": 0.2, "color": "ffffff62" } + ], + "attachment": [ + { "time": 0.0333, "name": "muzzle01" }, + { "time": 0.0667, "name": "muzzle02" }, + { "time": 0.1, "name": "muzzle03" }, + { "time": 0.1333, "name": "muzzle04" }, + { "time": 0.1667, "name": "muzzle05" }, + { "time": 0.2 } + ] + }, + "muzzle-glow": { + "rgba": [ + { "color": "ff0c0c00" }, + { + "time": 0.0333, + "color": "ffc9adff", + "curve": [ 0.255, 1, 0.273, 1, 0.255, 0.76, 0.273, 0.4, 0.255, 0.65, 0.273, 0.22, 0.255, 1, 0.273, 1 ] + }, + { "time": 0.3, "color": "ff400cff" }, + { "time": 0.6333, "color": "ff0c0c00" } + ], + "attachment": [ + { "name": "muzzle-glow" } + ] + }, + "muzzle-ring": { + "rgba": [ + { + "time": 0.0333, + "color": "d8baffff", + "curve": [ 0.202, 0.85, 0.214, 0.84, 0.202, 0.73, 0.214, 0.73, 0.202, 1, 0.214, 1, 0.202, 1, 0.214, 0.21 ] + }, + { "time": 0.2333, "color": "d7baff00" } + ], + "attachment": [ + { "time": 0.0333, "name": "muzzle-ring" }, + { "time": 0.2333 } + ] + }, + "muzzle-ring2": { + "rgba": [ + { + "time": 0.0333, + "color": "d8baffff", + "curve": [ 0.174, 0.85, 0.184, 0.84, 0.174, 0.73, 0.184, 0.73, 0.174, 1, 0.184, 1, 0.174, 1, 0.184, 0.21 ] + }, + { "time": 0.2, "color": "d7baff00" } + ], + "attachment": [ + { "time": 0.0333, "name": "muzzle-ring" }, + { "time": 0.2 } + ] + }, + "muzzle-ring3": { + "rgba": [ + { + "time": 0.0333, + "color": "d8baffff", + "curve": [ 0.174, 0.85, 0.184, 0.84, 0.174, 0.73, 0.184, 0.73, 0.174, 1, 0.184, 1, 0.174, 1, 0.184, 0.21 ] + }, + { "time": 0.2, "color": "d7baff00" } + ], + "attachment": [ + { "time": 0.0333, "name": "muzzle-ring" }, + { "time": 0.2 } + ] + }, + "muzzle-ring4": { + "rgba": [ + { + "time": 0.0333, + "color": "d8baffff", + "curve": [ 0.174, 0.85, 0.184, 0.84, 0.174, 0.73, 0.184, 0.73, 0.174, 1, 0.184, 1, 0.174, 1, 0.184, 0.21 ] + }, + { "time": 0.2, "color": "d7baff00" } + ], + "attachment": [ + { "time": 0.0333, "name": "muzzle-ring" }, + { "time": 0.2 } + ] + } + }, + "bones": { + "gun": { + "rotate": [ + { + "time": 0.0667, + "curve": [ 0.094, 25.89, 0.112, 45.27 ] + }, + { + "time": 0.1333, + "value": 45.35, + "curve": [ 0.192, 45.28, 0.18, -0.09 ] + }, + { "time": 0.6333 } + ] + }, + "muzzle": { + "translate": [ + { "x": -11.02, "y": 25.16 } + ] + }, + "rear-upper-arm": { + "translate": [ + { + "time": 0.0333, + "curve": [ 0.045, 0.91, 0.083, 3.46, 0.044, 0.86, 0.083, 3.32 ] + }, + { + "time": 0.1, + "x": 3.46, + "y": 3.32, + "curve": [ 0.133, 3.46, 0.176, -0.1, 0.133, 3.32, 0.169, 0 ] + }, + { "time": 0.2333 } + ] + }, + "rear-bracer": { + "translate": [ + { + "time": 0.0333, + "curve": [ 0.075, -3.78, 0.083, -4.36, 0.08, -2.7, 0.083, -2.88 ] + }, + { + "time": 0.1, + "x": -4.36, + "y": -2.88, + "curve": [ 0.133, -4.36, 0.168, 0.18, 0.133, -2.88, 0.167, 0 ] + }, + { "time": 0.2333 } + ] + }, + "gun-tip": { + "translate": [ + {}, + { "time": 0.3, "x": 3.15, "y": 0.39 } + ], + "scale": [ + { "x": 0.366, "y": 0.366 }, + { "time": 0.0333, "x": 1.453, "y": 1.453 }, + { "time": 0.3, "x": 0.366, "y": 0.366 } + ] + }, + "muzzle-ring": { + "translate": [ + { "time": 0.0333 }, + { "time": 0.2333, "x": 64.47 } + ], + "scale": [ + { "time": 0.0333 }, + { "time": 0.2333, "x": 5.951, "y": 5.951 } + ] + }, + "muzzle-ring2": { + "translate": [ + { "time": 0.0333 }, + { "time": 0.2, "x": 172.57 } + ], + "scale": [ + { "time": 0.0333 }, + { "time": 0.2, "x": 4, "y": 4 } + ] + }, + "muzzle-ring3": { + "translate": [ + { "time": 0.0333 }, + { "time": 0.2, "x": 277.17 } + ], + "scale": [ + { "time": 0.0333 }, + { "time": 0.2, "x": 2, "y": 2 } + ] + }, + "muzzle-ring4": { + "translate": [ + { "time": 0.0333 }, + { "time": 0.2, "x": 392.06 } + ] + } + } + }, + "walk": { + "bones": { + "rear-foot-target": { + "rotate": [ + { + "value": -32.82, + "curve": [ 0.035, -42.69, 0.057, -70.49 ] + }, + { + "time": 0.1, + "value": -70.59, + "curve": [ 0.236, -70.78, 0.335, -9.87 ] + }, + { + "time": 0.3667, + "value": -1.56, + "curve": [ 0.393, 5.5, 0.477, 13.96 ] + }, + { + "time": 0.5, + "value": 13.96, + "curve": [ 0.519, 13.96, 0.508, 0.13 ] + }, + { "time": 0.5667, "value": -0.28 }, + { + "time": 0.7333, + "value": -0.28, + "curve": [ 0.827, -0.06, 0.958, -21.07 ] + }, + { "time": 1, "value": -32.82 } + ], + "translate": [ + { + "x": -167.32, + "y": 0.58, + "curve": [ 0.022, -180.55, 0.075, -235.51, 0.045, 0.58, 0.075, 30.12 ] + }, + { + "time": 0.1, + "x": -235.51, + "y": 39.92, + "curve": [ 0.142, -235.51, 0.208, -201.73, 0.138, 54.94, 0.18, 60.78 ] + }, + { + "time": 0.2333, + "x": -176.33, + "y": 61.48, + "curve": [ 0.272, -136.61, 0.321, -45.18, 0.275, 62.02, 0.321, 56.6 ] + }, + { + "time": 0.3667, + "x": 8.44, + "y": 49.67, + "curve": [ 0.403, 51.03, 0.486, 66.86, 0.401, 44.37, 0.48, 23.11 ] + }, + { "time": 0.5, "x": 66.57, "y": 14.22 }, + { "time": 0.5333, "x": 52.58, "y": 0.6 }, + { "time": 1, "x": -167.32, "y": 0.58 } + ] + }, + "front-foot-target": { + "rotate": [ + { + "value": 18.19, + "curve": [ 0.01, 11.17, 0.043, 1.37 ] + }, + { "time": 0.1, "value": 0.47 }, + { + "time": 0.2333, + "value": 0.55, + "curve": [ 0.364, 0.3, 0.515, -80.48 ] + }, + { + "time": 0.7333, + "value": -80.78, + "curve": [ 0.788, -80.38, 0.921, 17.42 ] + }, + { "time": 1, "value": 18.19 } + ], + "translate": [ + { + "x": 139.21, + "y": 22.94, + "curve": [ 0.025, 139.21, 0.069, 111.46, 0.031, 3.25, 0.075, 0.06 ] + }, + { "time": 0.1, "x": 96.69, "y": 0.06 }, + { + "time": 0.5, + "x": -94.87, + "y": -0.03, + "curve": [ 0.518, -106.82, 0.575, -152.56, 0.534, 5.42, 0.557, 38.46 ] + }, + { + "time": 0.6, + "x": -152.56, + "y": 57.05, + "curve": [ 0.633, -152.56, 0.688, -128.05, 0.643, 75.61, 0.7, 84.14 ] + }, + { + "time": 0.7333, + "x": -109.42, + "y": 84.14, + "curve": [ 0.771, -93.91, 0.832, -30.64, 0.787, 84.14, 0.799, 89.65 ] + }, + { + "time": 0.8667, + "x": 17, + "y": 75.25, + "curve": [ 0.903, 66.18, 0.967, 139.21, 0.932, 61.53, 0.967, 44.02 ] + }, + { "time": 1, "x": 139.21, "y": 22.94 } + ] + }, + "hip": { + "rotate": [ + { "value": -4.35 } + ], + "translate": [ + { + "x": -2.86, + "y": -13.86, + "curve": [ 0.025, -2.84, 0.067, -2.82, 0.028, -19.14, 0.054, -24.02 ] + }, + { + "time": 0.1, + "x": -2.61, + "y": -24.19, + "curve": [ 0.143, -2.34, 0.202, -1.79, 0.152, -23.98, 0.213, -14.81 ] + }, + { + "time": 0.2667, + "x": -1.21, + "y": -7.12, + "curve": [ 0.308, -0.86, 0.345, -0.51, 0.306, -1.63, 0.341, 3.15 ] + }, + { + "time": 0.3667, + "x": -0.33, + "y": 3.15, + "curve": [ 0.41, 0.02, 0.458, 0.26, 0.427, 3.3, 0.481, -6.75 ] + }, + { + "time": 0.5, + "x": 0.26, + "y": -10.59, + "curve": [ 0.553, 0.26, 0.559, 0.2, 0.519, -14.41, 0.548, -23.88 ] + }, + { + "time": 0.6, + "x": -0.17, + "y": -23.71, + "curve": [ 0.663, -0.72, 0.798, -2.09, 0.702, -23.36, 0.802, 3.53 ] + }, + { + "time": 0.8667, + "x": -2.46, + "y": 3.48, + "curve": [ 0.901, -2.63, 0.967, -2.87, 0.913, 3.45, 0.967, -7.64 ] + }, + { "time": 1, "x": -2.86, "y": -13.86 } + ] + }, + "front-foot-tip": { + "rotate": [ + { + "value": 28.96, + "curve": [ 0.056, 28.74, 0.049, 19.6 ] + }, + { "time": 0.0667, "value": 1.68 }, + { + "time": 0.5, + "value": -10, + "curve": [ 0.525, -10, 0.592, -54.69 ] + }, + { + "time": 0.6, + "value": -59.66, + "curve": [ 0.623, -74.54, 0.674, -101.78 ] + }, + { + "time": 0.7333, + "value": -101.78, + "curve": [ 0.812, -101.78, 0.855, -84.67 ] + }, + { + "time": 0.8667, + "value": -63.53, + "curve": [ 0.869, -58.38, 0.975, 28.96 ] + }, + { "time": 1, "value": 28.96 } + ] + }, + "torso": { + "rotate": [ + { + "value": -20.72, + "curve": [ 0.025, -20.57, 0.071, -20.04 ] + }, + { + "time": 0.1333, + "value": -20.04, + "curve": [ 0.187, -20.04, 0.285, -21.16 ] + }, + { + "time": 0.3667, + "value": -21.16, + "curve": [ 0.405, -21.16, 0.47, -20.9 ] + }, + { + "time": 0.5, + "value": -20.71, + "curve": [ 0.518, -20.6, 0.582, -20.03 ] + }, + { + "time": 0.6333, + "value": -20.04, + "curve": [ 0.709, -20.05, 0.815, -21.18 ] + }, + { + "time": 0.8667, + "value": -21.18, + "curve": [ 0.908, -21.18, 0.971, -20.93 ] + }, + { "time": 1, "value": -20.72 } + ] + }, + "neck": { + "rotate": [ + { + "value": 17.78, + "curve": [ 0.025, 17.93, 0.071, 18.46 ] + }, + { + "time": 0.1333, + "value": 18.46, + "curve": [ 0.187, 18.46, 0.285, 17.34 ] + }, + { + "time": 0.3667, + "value": 17.34, + "curve": [ 0.405, 17.34, 0.47, 17.6 ] + }, + { + "time": 0.5, + "value": 17.79, + "curve": [ 0.518, 17.9, 0.582, 18.47 ] + }, + { + "time": 0.6333, + "value": 18.46, + "curve": [ 0.709, 18.45, 0.815, 17.32 ] + }, + { + "time": 0.8667, + "value": 17.32, + "curve": [ 0.908, 17.32, 0.971, 17.57 ] + }, + { "time": 1, "value": 17.78 } + ] + }, + "head": { + "rotate": [ + { + "value": -12.23, + "curve": [ 0.061, -12.23, 0.191, -7.45 ] + }, + { + "time": 0.2667, + "value": -7.43, + "curve": [ 0.341, -7.42, 0.421, -12.23 ] + }, + { + "time": 0.5, + "value": -12.23, + "curve": [ 0.567, -12.26, 0.694, -7.46 ] + }, + { + "time": 0.7667, + "value": -7.47, + "curve": [ 0.853, -7.49, 0.943, -12.23 ] + }, + { "time": 1, "value": -12.23 } + ], + "scale": [ + { + "curve": [ 0.039, 1, 0.084, 0.991, 0.039, 1, 0.084, 1.019 ] + }, + { + "time": 0.1333, + "x": 0.991, + "y": 1.019, + "curve": [ 0.205, 0.991, 0.318, 1.019, 0.205, 1.019, 0.337, 0.992 ] + }, + { + "time": 0.4, + "x": 1.019, + "y": 0.992, + "curve": [ 0.456, 1.019, 0.494, 1.001, 0.483, 0.991, 0.493, 0.999 ] + }, + { + "time": 0.5, + "curve": [ 0.508, 0.998, 0.584, 0.991, 0.51, 1.002, 0.584, 1.019 ] + }, + { + "time": 0.6333, + "x": 0.991, + "y": 1.019, + "curve": [ 0.705, 0.991, 0.818, 1.019, 0.705, 1.019, 0.837, 0.992 ] + }, + { + "time": 0.9, + "x": 1.019, + "y": 0.992, + "curve": [ 0.956, 1.019, 0.955, 1, 0.983, 0.991, 0.955, 1 ] + }, + { "time": 1 } + ] + }, + "back-foot-tip": { + "rotate": [ + { "value": 4.09 }, + { "time": 0.0333, "value": 3.05 }, + { + "time": 0.1, + "value": -59.01, + "curve": [ 0.124, -72.97, 0.169, -100.05 ] + }, + { + "time": 0.2333, + "value": -99.71, + "curve": [ 0.326, -99.21, 0.349, -37.4 ] + }, + { + "time": 0.3667, + "value": -17.85, + "curve": [ 0.388, 4.74, 0.451, 32.35 ] + }, + { + "time": 0.5, + "value": 32.4, + "curve": [ 0.537, 32.44, 0.566, 6.43 ] + }, + { "time": 0.5667, "value": 2 }, + { "time": 1, "value": 4.09 } + ] + }, + "front-thigh": { + "translate": [ + { + "x": 17.15, + "y": -0.09, + "curve": [ 0.178, 17.14, 0.295, -4.26, 0.009, -0.09, 0.475, 0.02 ] + }, + { + "time": 0.5, + "x": -4.26, + "y": 0.02, + "curve": [ 0.705, -4.27, 0.848, 17.15, 0.525, 0.02, 0.975, -0.09 ] + }, + { "time": 1, "x": 17.15, "y": -0.09 } + ] + }, + "rear-thigh": { + "translate": [ + { + "x": -17.71, + "y": -4.63, + "curve": [ 0.036, -19.81, 0.043, -20.86, 0.036, -4.63, 0.05, -7.03 ] + }, + { + "time": 0.1, + "x": -20.95, + "y": -7.06, + "curve": [ 0.162, -21.05, 0.4, 7.79, 0.2, -7.13, 0.4, -1.9 ] + }, + { + "time": 0.5, + "x": 7.79, + "y": -1.94, + "curve": [ 0.612, 7.69, 0.875, -10.49, 0.592, -1.97, 0.917, -3.25 ] + }, + { "time": 1, "x": -17.71, "y": -4.63 } + ] + }, + "torso2": { + "rotate": [ + { + "value": 1, + "curve": [ 0.006, 1.2, 0.084, 2.88 ] + }, + { + "time": 0.1333, + "value": 2.88, + "curve": [ 0.205, 2.88, 0.284, -1.17 ] + }, + { + "time": 0.3667, + "value": -1.17, + "curve": [ 0.411, -1.17, 0.481, 0.57 ] + }, + { + "time": 0.5, + "value": 1, + "curve": [ 0.515, 1.33, 0.59, 2.83 ] + }, + { + "time": 0.6333, + "value": 2.85, + "curve": [ 0.683, 2.86, 0.796, -1.2 ] + }, + { + "time": 0.8667, + "value": -1.2, + "curve": [ 0.916, -1.2, 0.984, 0.62 ] + }, + { "time": 1, "value": 1 } + ] + }, + "torso3": { + "rotate": [ + { "value": -1.81 } + ] + }, + "front-upper-arm": { + "rotate": [ + { + "value": -9.51, + "curve": [ 0.021, -13.32, 0.058, -19.4 ] + }, + { + "time": 0.1, + "value": -19.4, + "curve": [ 0.238, -19.69, 0.337, 7.78 ] + }, + { + "time": 0.3667, + "value": 16.2, + "curve": [ 0.399, 25.42, 0.497, 60.19 ] + }, + { + "time": 0.6, + "value": 60.26, + "curve": [ 0.719, 60.13, 0.845, 27.61 ] + }, + { + "time": 0.8667, + "value": 22.45, + "curve": [ 0.892, 16.38, 0.979, -3.27 ] + }, + { "time": 1, "value": -9.51 } + ] + }, + "front-bracer": { + "rotate": [ + { + "value": 13.57, + "curve": [ 0.022, 9.71, 0.147, -3.78 ] + }, + { + "time": 0.3667, + "value": -3.69, + "curve": [ 0.457, -3.66, 0.479, 0.83 ] + }, + { + "time": 0.5, + "value": 4.05, + "curve": [ 0.513, 6.08, 0.635, 30.8 ] + }, + { + "time": 0.8, + "value": 30.92, + "curve": [ 0.974, 31, 0.98, 18.35 ] + }, + { "time": 1, "value": 13.57 } + ] + }, + "front-fist": { + "rotate": [ + { + "value": -28.72, + "curve": [ 0.024, -31.74, 0.176, -43.4 ] + }, + { + "time": 0.3667, + "value": -43.6, + "curve": [ 0.403, -43.65, 0.47, -40.15 ] + }, + { + "time": 0.5, + "value": -35.63, + "curve": [ 0.547, -28.59, 0.624, -4.57 ] + }, + { + "time": 0.7333, + "value": -4.59, + "curve": [ 0.891, -4.62, 0.954, -24.28 ] + }, + { "time": 1, "value": -28.48 } + ] + }, + "rear-upper-arm": { + "rotate": [ + { + "value": 28.28, + "curve": [ 0.034, 30.94, 0.068, 32.05 ] + }, + { + "time": 0.1, + "value": 31.88, + "curve": [ 0.194, 31.01, 0.336, -0.11 ] + }, + { + "time": 0.3667, + "value": -7.11, + "curve": [ 0.421, -19.73, 0.53, -46.21 ] + }, + { + "time": 0.6, + "value": -45.75, + "curve": [ 0.708, -45.03, 0.844, -13.56 ] + }, + { + "time": 0.8667, + "value": -6.48, + "curve": [ 0.909, 6.59, 0.958, 24.21 ] + }, + { "time": 1, "value": 28.28 } + ] + }, + "hair2": { + "rotate": [ + { + "value": -2.79, + "curve": [ 0.074, -2.84, 0.121, 25.08 ] + }, + { + "time": 0.2333, + "value": 24.99, + "curve": [ 0.35, 24.89, 0.427, -2.86 ] + }, + { + "time": 0.5, + "value": -2.8, + "curve": [ 0.575, -2.73, 0.652, 24.5 ] + }, + { + "time": 0.7333, + "value": 24.55, + "curve": [ 0.828, 24.6, 0.932, -2.69 ] + }, + { "time": 1, "value": -2.79 } + ] + }, + "hair4": { + "rotate": [ + { + "value": -6.01, + "curve": [ 0.106, -5.97, 0.151, 18.62 ] + }, + { + "time": 0.2333, + "value": 18.72, + "curve": [ 0.336, 18.7, 0.405, -11.37 ] + }, + { + "time": 0.5, + "value": -11.45, + "curve": [ 0.626, -11.46, 0.629, 18.94 ] + }, + { + "time": 0.7333, + "value": 18.92, + "curve": [ 0.833, 18.92, 0.913, -6.06 ] + }, + { "time": 1, "value": -6.01 } + ], + "translate": [ + { "x": 0.03, "y": 1.35 } + ] + }, + "rear-bracer": { + "rotate": [ + { + "value": 10.06, + "curve": [ 0.044, 11.16, 0.063, 11.49 ] + }, + { + "time": 0.1, + "value": 11.49, + "curve": [ 0.215, 11.49, 0.336, 2.92 ] + }, + { + "time": 0.3667, + "value": 0.84, + "curve": [ 0.416, -2.52, 0.498, -10.84 ] + }, + { + "time": 0.6, + "value": -10.83, + "curve": [ 0.762, -10.71, 0.845, -3.05 ] + }, + { + "time": 0.8667, + "value": -1.34, + "curve": [ 0.917, 2.54, 0.977, 8.81 ] + }, + { "time": 1, "value": 10.06 } + ] + }, + "gun": { + "rotate": [ + { + "value": -14.67, + "curve": [ 0.086, -14.67, 0.202, 8.31 ] + }, + { + "time": 0.2333, + "value": 12.14, + "curve": [ 0.279, 17.71, 0.391, 25.79 ] + }, + { + "time": 0.5, + "value": 25.77, + "curve": [ 0.631, 25.74, 0.694, 4.53 ] + }, + { + "time": 0.7333, + "value": -0.65, + "curve": [ 0.768, -5.21, 0.902, -14.4 ] + }, + { "time": 1, "value": -14.67 } + ] + }, + "front-leg-target": { + "translate": [ + { + "x": -2.83, + "y": -8.48, + "curve": [ 0.008, -2.83, 0.058, 0.09, 0.001, 4.97, 0.058, 6.68 ] + }, + { + "time": 0.0667, + "x": 0.09, + "y": 6.68, + "curve": [ 0.3, 0.09, 0.767, -2.83, 0.3, 6.68, 0.767, -8.48 ] + }, + { "time": 1, "x": -2.83, "y": -8.48 } + ] + }, + "hair1": { + "rotate": [ + { + "curve": [ 0.028, 1.24, 0.016, 3.46 ] + }, + { + "time": 0.1, + "value": 3.45, + "curve": [ 0.159, 3.45, 0.189, 0.23 ] + }, + { + "time": 0.2333, + "value": -2.29, + "curve": [ 0.265, -4.32, 0.305, -5.92 ] + }, + { + "time": 0.3667, + "value": -5.94, + "curve": [ 0.446, -5.96, 0.52, 3.41 ] + }, + { + "time": 0.6, + "value": 3.42, + "curve": [ 0.717, 3.42, 0.772, -5.93 ] + }, + { + "time": 0.8667, + "value": -5.97, + "curve": [ 0.933, -5.99, 0.982, -0.94 ] + }, + { "time": 1 } + ] + }, + "hair3": { + "rotate": [ + { + "curve": [ 0.067, 0, 0.159, -10.48 ] + }, + { + "time": 0.2333, + "value": -10.49, + "curve": [ 0.334, -10.5, 0.439, -0.09 ] + }, + { + "time": 0.5, + "value": -0.09, + "curve": [ 0.569, -0.09, 0.658, -10.75 ] + }, + { + "time": 0.7333, + "value": -10.7, + "curve": [ 0.833, -10.63, 0.947, 0 ] + }, + { "time": 1 } + ] + }, + "gun-tip": { + "rotate": [ + { "time": 0.2333, "value": 0.11 } + ] + }, + "muzzle-ring": { + "rotate": [ + { "time": 0.2333, "value": 0.11 } + ] + }, + "muzzle-ring2": { + "rotate": [ + { "time": 0.2667, "value": 0.11 } + ] + }, + "muzzle-ring3": { + "rotate": [ + { "time": 0.2667, "value": 0.11 } + ] + }, + "muzzle-ring4": { + "rotate": [ + { "time": 0.2667, "value": 0.11 } + ] + }, + "back-shoulder": { + "translate": [ + { + "x": -0.18, + "y": -4.49, + "curve": [ 0.133, -0.18, 0.333, 7.69, 0.133, -4.49, 0.333, 2.77 ] + }, + { + "time": 0.4667, + "x": 7.69, + "y": 2.77, + "curve": [ 0.6, 7.69, 0.858, -0.18, 0.6, 2.77, 0.858, -4.49 ] + }, + { "time": 1, "x": -0.18, "y": -4.49 } + ] + }, + "front-shoulder": { + "translate": [ + { + "x": 1.46, + "y": 9.37, + "curve": [ 0.162, 1.41, 0.333, -1.66, 0.162, 9.37, 0.301, -7.23 ] + }, + { + "time": 0.5, + "x": -1.6, + "y": -7.27, + "curve": [ 0.735, -1.5, 0.847, 1.46, 0.723, -7.31, 0.838, 9.32 ] + }, + { "time": 1, "x": 1.46, "y": 9.37 } + ] + }, + "head-control": { + "translate": [ + { + "x": -6.46, + "y": -8.4, + "curve": [ 0.053, -5.31, 0.167, -3.64, 0.093, -8.4, 0.196, -3.81 ] + }, + { + "time": 0.2333, + "x": -3.64, + "y": -1.32, + "curve": [ 0.309, -3.64, 0.436, -5.84, 0.275, 1.43, 0.38, 10.3 ] + }, + { + "time": 0.5, + "x": -7.03, + "y": 10.29, + "curve": [ 0.538, -7.75, 0.66, -10.54, 0.598, 10.27, 0.694, 1.56 ] + }, + { + "time": 0.7333, + "x": -10.54, + "y": -1.26, + "curve": [ 0.797, -10.54, 0.933, -7.91, 0.768, -3.79, 0.875, -8.4 ] + }, + { "time": 1, "x": -6.46, "y": -8.4 } + ] + } + }, + "ik": { + "front-leg-ik": [ + { + "softness": 25.7, + "bendPositive": false, + "curve": [ 0.008, 1, 0.025, 1, 0.008, 25.7, 0.025, 9.9 ] + }, + { + "time": 0.0333, + "softness": 9.9, + "bendPositive": false, + "curve": [ 0.15, 1, 0.383, 1, 0.15, 9.9, 0.383, 43.2 ] + }, + { + "time": 0.5, + "softness": 43.2, + "bendPositive": false, + "curve": [ 0.625, 1, 0.875, 1, 0.625, 43.2, 0.846, 45.57 ] + }, + { "time": 1, "softness": 25.7, "bendPositive": false } + ], + "rear-leg-ik": [ + { "softness": 5, "bendPositive": false }, + { "time": 0.4333, "softness": 4.9, "bendPositive": false }, + { "time": 0.5, "softness": 28.81, "bendPositive": false }, + { "time": 0.6, "softness": 43.8, "bendPositive": false }, + { "time": 1, "softness": 5, "bendPositive": false } + ] + }, + "events": [ + { "name": "footstep" }, + { "time": 0.5, "name": "footstep" } + ] + } + } + } \ No newline at end of file diff --git a/e2e/.dev/public/spineboy.png b/e2e/.dev/public/spineboy.png new file mode 100644 index 0000000000..0ea9737f30 Binary files /dev/null and b/e2e/.dev/public/spineboy.png differ diff --git a/e2e/.dev/public/tank-pro.atlas b/e2e/.dev/public/tank-pro.atlas new file mode 100644 index 0000000000..0c1a46580e --- /dev/null +++ b/e2e/.dev/public/tank-pro.atlas @@ -0,0 +1,65 @@ +tank-pro.png +size:870,638 +filter:Linear,Linear +scale:0.5 +antenna +bounds:647,376,11,152 +cannon +bounds:2,359,466,29 +cannon-connector +bounds:812,448,56,68 +ground +bounds:2,16,512,177 +guntower +bounds:641,4,365,145 +rotate:90 +machinegun +bounds:470,359,166,29 +machinegun-mount +bounds:823,518,36,48 +rock +bounds:690,371,265,61 +offsets:7,1,290,64 +rotate:90 +smoke-glow +bounds:817,371,50,50 +smoke-puff01-bg +bounds:753,371,92,62 +rotate:90 +smoke-puff01-fg +bounds:753,465,86,57 +offsets:1,1,88,59 +rotate:90 +smoke-puff02-fg +bounds:788,127,85,53 +offsets:4,6,92,62 +rotate:90 +smoke-puff03-fg +bounds:788,214,85,53 +offsets:4,6,92,62 +rotate:90 +smoke-puff04-fg +bounds:788,47,78,48 +rotate:90 +tank-bottom +bounds:2,390,643,138 +tank-bottom-shadow +bounds:2,195,638,162 +offsets:1,8,646,171 +tank-top +bounds:2,530,687,106 +offsets:13,5,704,111 +tread +bounds:817,423,48,15 +tread-inside +bounds:611,81,13,14 +wheel-big +bounds:516,97,96,96 +wheel-big-overlay +bounds:516,2,93,93 +wheel-mid +bounds:753,553,68,68 +wheel-mid-overlay +bounds:788,301,68,68 +wheel-small +bounds:788,9,36,36 diff --git a/e2e/.dev/public/tank-pro.json b/e2e/.dev/public/tank-pro.json new file mode 100644 index 0000000000..324d313149 --- /dev/null +++ b/e2e/.dev/public/tank-pro.json @@ -0,0 +1 @@ +{"skeleton":{"hash":"nW5DL2uoEfA","spine":"4.2.40","x":-5852.65,"y":-348.5,"width":7202.61,"height":1298.88,"images":"./images/","audio":""},"bones":[{"name":"root"},{"name":"tank-root","parent":"root","y":146.79},{"name":"tank-treads","parent":"tank-root","y":48.35},{"name":"tank-body","parent":"tank-treads","y":10},{"name":"guntower","parent":"tank-body","x":-21.72,"y":245.48},{"name":"antenna-root","parent":"guntower","x":164.61,"y":202.53},{"name":"antenna1","parent":"antenna-root","length":40,"rotation":90,"y":40,"color":"ffee00ff"},{"name":"antenna2","parent":"antenna1","length":42,"x":42,"color":"ffee00ff"},{"name":"antenna3","parent":"antenna2","length":42,"x":42,"color":"ffee00ff"},{"name":"antenna4","parent":"antenna3","length":42,"x":42,"color":"ffee00ff"},{"name":"antenna5","parent":"antenna4","length":42,"x":42,"color":"ffee00ff"},{"name":"antenna6","parent":"antenna5","length":42,"x":42,"color":"ffee00ff"},{"name":"cannon-connector","parent":"guntower","x":-235.05,"y":96.07},{"name":"cannon-target","parent":"tank-root","x":-2276.67,"y":400.17,"color":"0096ffff","icon":"arrows"},{"name":"cannon","parent":"cannon-connector","length":946.68,"rotation":180,"color":"ff4000ff"},{"name":"machinegun-mount","parent":"guntower","length":90.98,"rotation":90,"x":-123.73,"y":218.33,"color":"15ff00ff"},{"name":"machinegun-target","parent":"tank-root","x":-2272.76,"y":607.77,"color":"0096ffff","icon":"ik"},{"name":"machinegun","parent":"machinegun-mount","length":208.95,"rotation":90,"x":91.52,"y":-1.03,"color":"15ff00ff"},{"name":"machinegun-tip","parent":"machinegun","x":210.43,"y":-12.21},{"name":"rock","parent":"root","x":-1925.2,"y":33.17},{"name":"smoke-root","parent":"tank-root","x":-1200.38,"y":405.76,"scaleX":-6.5,"scaleY":6.5,"color":"ff4000ff","icon":"particles"},{"name":"smoke-glow","parent":"smoke-root","x":62.92,"y":-0.71,"color":"ff4000ff","icon":"particles"},{"name":"smoke1","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke10","parent":"smoke-root","rotation":-103.52,"x":25.45,"y":2.48,"scaleX":3.9011,"scaleY":2.8523,"color":"ff4000ff","icon":"particles"},{"name":"smoke11","parent":"smoke-root","rotation":-103.52,"x":25.45,"y":2.48,"scaleX":3.9011,"scaleY":2.8523,"color":"ff4000ff","icon":"particles"},{"name":"smoke12","parent":"smoke-root","rotation":-103.52,"x":25.45,"y":2.48,"scaleX":3.9011,"scaleY":2.8523,"color":"ff4000ff","icon":"particles"},{"name":"smoke13","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke14","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke15","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke16","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke17","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke18","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke2","parent":"smoke-root","rotation":-84.14,"x":45.06,"y":29.7,"scaleX":3.3345,"scaleY":3.3345,"color":"ff4000ff","icon":"particles"},{"name":"smoke20","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke21","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke22","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke23","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke24","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke25","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke26","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke27","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke3","parent":"smoke-root","rotation":-87.91,"x":55.15,"y":-17.5,"scaleX":3.0415,"scaleY":4.157,"color":"ff4000ff","icon":"particles"},{"name":"smoke4","parent":"smoke-root","rotation":-87.91,"x":69.25,"y":8.01,"scaleX":2.1808,"scaleY":2.9807,"color":"ff4000ff","icon":"particles"},{"name":"smoke5","parent":"smoke-root","rotation":-87.91,"x":80.63,"y":59.88,"scaleX":4.5119,"scaleY":2.9725,"color":"ff4000ff","icon":"particles"},{"name":"smoke6","parent":"smoke-root","rotation":-87.91,"x":96.19,"y":25.65,"scaleX":3.7912,"scaleY":3.0552,"color":"ff4000ff","icon":"particles"},{"name":"smoke7","parent":"smoke-root","rotation":153.68,"x":85.65,"y":-50.47,"scaleX":4.8523,"scaleY":3.6528,"color":"ff4000ff","icon":"particles"},{"name":"smoke8","parent":"smoke-root","rotation":67.58,"x":47.85,"y":-42.55,"scaleX":4.0006,"scaleY":3.4796,"color":"ff4000ff","icon":"particles"},{"name":"smoke9","parent":"smoke-root","rotation":150.05,"x":104.02,"y":-8.73,"scaleX":4.2074,"scaleY":3.0762,"color":"ff4000ff","icon":"particles"},{"name":"tank-glow","parent":"tank-root","x":-247.72,"y":404.37,"scaleX":1.0582,"scaleY":0.6785},{"name":"tread","parent":"tank-root","length":82,"rotation":180,"x":-22.9,"y":213.86,"scaleX":0.9933,"color":"e64344ff"},{"name":"wheel-mid-center","parent":"tank-root","y":-66.21},{"name":"tread-collider1","parent":"wheel-mid-center","x":-329.58,"y":-85.44,"color":"ff00fbff"},{"name":"tread-collider2","parent":"wheel-mid-center","x":-165.95,"y":-85.44,"color":"ff00fbff"},{"name":"tread-collider3","parent":"wheel-mid-center","y":-85.44,"color":"ff00fbff"},{"name":"tread-collider4","parent":"wheel-mid-center","x":163.56,"y":-85.44,"color":"ff00fbff"},{"name":"tread-collider5","parent":"wheel-mid-center","x":329.12,"y":-85.44,"color":"ff00fbff"},{"name":"tread-gravity1","parent":"tank-root","rotation":180,"x":-175.35,"y":149.31,"color":"ff00fbff"},{"name":"tread-gravity2","parent":"tank-root","rotation":180,"x":177.89,"y":144.78,"color":"ff00fbff"},{"name":"tread10","parent":"tread","length":82,"rotation":48.85,"x":662.9,"y":-120.35,"color":"e64344ff"},{"name":"tread11","parent":"tread","length":82,"rotation":97.99,"x":651.5,"y":-39.69,"color":"e64344ff"},{"name":"tread12","parent":"tread","length":82,"rotation":113.79,"x":618.43,"y":34.83,"color":"e64344ff"},{"name":"tread13","parent":"tread","length":82,"rotation":122.96,"x":573.82,"y":103.18,"color":"e64344ff"},{"name":"tread14","parent":"tread","length":82,"rotation":142.01,"x":509.19,"y":153.3,"color":"e64344ff"},{"name":"tread15","parent":"tread","length":82,"rotation":157.84,"x":433.25,"y":184.02,"color":"e64344ff"},{"name":"tread16","parent":"tread","length":82,"rotation":157.37,"x":357.56,"y":215.37,"color":"e64344ff"},{"name":"tread17","parent":"tread","length":82,"rotation":157.29,"x":281.92,"y":246.8,"color":"e64344ff"},{"name":"tread18","parent":"tread","length":82,"rotation":157.19,"x":206.33,"y":278.38,"color":"e64344ff"},{"name":"tread19","parent":"tread","length":82,"rotation":157.14,"x":130.77,"y":310.02,"color":"e64344ff"},{"name":"tread2","parent":"tread","length":82,"x":82,"color":"e64344ff"},{"name":"tread20","parent":"tread","length":82,"rotation":157.34,"x":55.1,"y":341.41,"color":"e64344ff"},{"name":"tread21","parent":"tread","length":82,"rotation":158.11,"x":-20.99,"y":371.77,"color":"e64344ff"},{"name":"tread22","parent":"tread","length":82,"rotation":157.99,"x":-97.02,"y":402.28,"color":"e64344ff"},{"name":"tread23","parent":"tread","length":82,"rotation":157.59,"x":-172.83,"y":433.33,"color":"e64344ff"},{"name":"tread24","parent":"tread","length":82,"rotation":156.86,"x":-248.23,"y":465.34,"color":"e64344ff"},{"name":"tread25","parent":"tread","length":82,"rotation":177.94,"x":-330.17,"y":468.27,"color":"e64344ff"},{"name":"tread26","parent":"tread","length":82,"rotation":-169.55,"x":-410.81,"y":453.5,"color":"e64344ff"},{"name":"tread27","parent":"tread","length":82,"rotation":-163.86,"x":-489.58,"y":430.86,"color":"e64344ff"},{"name":"tread28","parent":"tread","length":82,"rotation":-139.13,"x":-551.59,"y":377.57,"color":"e64344ff"},{"name":"tread29","parent":"tread","length":82,"rotation":-89.04,"x":-550.21,"y":296.14,"color":"e64344ff"},{"name":"tread3","parent":"tread","length":82,"rotation":-8.91,"x":163.01,"y":-12.61,"color":"e64344ff"},{"name":"tread30","parent":"tread","length":82,"rotation":-38.99,"x":-486.48,"y":244.89,"color":"e64344ff"},{"name":"tread31","parent":"tread","length":82,"rotation":-20.04,"x":-409.45,"y":216.98,"color":"e64344ff"},{"name":"tread32","parent":"tread","length":82,"rotation":-46.24,"x":-352.74,"y":158.15,"color":"e64344ff"},{"name":"tread33","parent":"tread","length":82,"rotation":-27.95,"x":-280.3,"y":119.98,"color":"e64344ff"},{"name":"tread34","parent":"tread","length":82,"rotation":10.46,"x":-199.66,"y":134.77,"color":"e64344ff"},{"name":"tread35","parent":"tread","length":82,"rotation":-17.9,"x":-121.63,"y":109.73,"color":"e64344ff"},{"name":"tread36","parent":"tread","length":82,"rotation":-36.82,"x":-55.99,"y":60.92,"color":"fbff00ff"},{"name":"tread4","parent":"tread","length":82,"rotation":-29.27,"x":234.55,"y":-52.43,"color":"e64344ff"},{"name":"tread5","parent":"tread","length":82,"rotation":-45.26,"x":292.26,"y":-110.28,"color":"e64344ff"},{"name":"tread6","parent":"tread","length":82,"rotation":-15.29,"x":371.36,"y":-131.76,"color":"e64344ff"},{"name":"tread7","parent":"tread","length":82,"rotation":-5.49,"x":452.98,"y":-139.55,"color":"e64344ff"},{"name":"tread8","parent":"tread","length":82,"rotation":-24.99,"x":527.31,"y":-173.95,"color":"e64344ff"},{"name":"tread9","parent":"tread","length":82,"rotation":-5.44,"x":608.94,"y":-181.68,"color":"e64344ff"},{"name":"wheel-big-root1","parent":"tank-treads","x":-549.6,"y":14.4,"color":"abe323ff"},{"name":"wheel-big-root2","parent":"tank-treads","x":547.34,"y":14.4},{"name":"wheel-big1","parent":"wheel-big-root1","x":-0.02,"color":"abe323ff"},{"name":"wheel-big2","parent":"wheel-big-root2"},{"name":"wheel-mid-root1","parent":"wheel-mid-center","x":-410.57,"color":"abe323ff"},{"name":"wheel-mid-root2","parent":"wheel-mid-center","x":-246.95},{"name":"wheel-mid-root3","parent":"wheel-mid-center","x":-82.73},{"name":"wheel-mid-root4","parent":"wheel-mid-center","x":80.89},{"name":"wheel-mid-root5","parent":"wheel-mid-center","x":244.51},{"name":"wheel-mid-root6","parent":"wheel-mid-center","x":408.74},{"name":"wheel-mid1","parent":"wheel-mid-root1","color":"abe323ff"},{"name":"wheel-mid2","parent":"wheel-mid-root2"},{"name":"wheel-mid3","parent":"wheel-mid-root3"},{"name":"wheel-mid4","parent":"wheel-mid-root4"},{"name":"wheel-mid5","parent":"wheel-mid-root5"},{"name":"wheel-mid6","parent":"wheel-mid-root6"},{"name":"wheel-small-root1","parent":"tank-treads","x":-337.39,"y":109.43},{"name":"wheel-small-root2","parent":"tank-treads","x":0.09,"y":109.43},{"name":"wheel-small-root3","parent":"tank-treads","x":334.69,"y":109.43},{"name":"wheel-small1","parent":"wheel-small-root1","color":"abe323ff"},{"name":"wheel-small2","parent":"wheel-small-root2"},{"name":"wheel-small3","parent":"wheel-small-root3"}],"slots":[{"name":"rock","bone":"rock","attachment":"rock"},{"name":"ground","bone":"root","attachment":"ground"},{"name":"ground2","bone":"root","attachment":"ground"},{"name":"ground3","bone":"root","attachment":"ground"},{"name":"ground4","bone":"root","attachment":"ground"},{"name":"ground5","bone":"root","attachment":"ground"},{"name":"ground6","bone":"root","attachment":"ground"},{"name":"ground7","bone":"root","attachment":"ground"},{"name":"tank-body-shadow","bone":"tank-body","color":"ffffffb9","attachment":"tank-bottom-shadow"},{"name":"bottom","bone":"tank-body","attachment":"tank-bottom"},{"name":"tread-inside1","bone":"tread","attachment":"tread-inside"},{"name":"tread-inside53","bone":"tread27","attachment":"tread-inside"},{"name":"tread-inside27","bone":"tread14","attachment":"tread-inside"},{"name":"tread-inside3","bone":"tread2","attachment":"tread-inside"},{"name":"tread-inside55","bone":"tread28","attachment":"tread-inside"},{"name":"tread-inside29","bone":"tread15","attachment":"tread-inside"},{"name":"tread-inside5","bone":"tread3","attachment":"tread-inside"},{"name":"tread-inside57","bone":"tread29","attachment":"tread-inside"},{"name":"tread-inside31","bone":"tread16","attachment":"tread-inside"},{"name":"tread-inside7","bone":"tread4","attachment":"tread-inside"},{"name":"tread-inside59","bone":"tread30","attachment":"tread-inside"},{"name":"tread-inside33","bone":"tread17","attachment":"tread-inside"},{"name":"tread-inside9","bone":"tread5","attachment":"tread-inside"},{"name":"tread-inside61","bone":"tread31","attachment":"tread-inside"},{"name":"tread-inside35","bone":"tread18","attachment":"tread-inside"},{"name":"tread-inside11","bone":"tread6","attachment":"tread-inside"},{"name":"tread-inside63","bone":"tread32","attachment":"tread-inside"},{"name":"tread-inside37","bone":"tread19","attachment":"tread-inside"},{"name":"tread-inside13","bone":"tread7","attachment":"tread-inside"},{"name":"tread-inside65","bone":"tread33","attachment":"tread-inside"},{"name":"tread-inside39","bone":"tread20","attachment":"tread-inside"},{"name":"tread-inside15","bone":"tread8","attachment":"tread-inside"},{"name":"tread-inside67","bone":"tread34","attachment":"tread-inside"},{"name":"tread-inside69","bone":"tread35","attachment":"tread-inside"},{"name":"tread-inside71","bone":"tread36","attachment":"tread-inside"},{"name":"tread-inside41","bone":"tread21","attachment":"tread-inside"},{"name":"tread-inside17","bone":"tread9","attachment":"tread-inside"},{"name":"tread-inside43","bone":"tread22","attachment":"tread-inside"},{"name":"tread-inside19","bone":"tread10","attachment":"tread-inside"},{"name":"tread-inside45","bone":"tread23","attachment":"tread-inside"},{"name":"tread-inside21","bone":"tread11","attachment":"tread-inside"},{"name":"tread-inside47","bone":"tread24","attachment":"tread-inside"},{"name":"tread-inside23","bone":"tread12","attachment":"tread-inside"},{"name":"tread-inside49","bone":"tread25","attachment":"tread-inside"},{"name":"tread-inside25","bone":"tread13","attachment":"tread-inside"},{"name":"tread-inside51","bone":"tread26","attachment":"tread-inside"},{"name":"tread-inside2","bone":"tread","attachment":"tread-inside"},{"name":"tread-inside54","bone":"tread27","attachment":"tread-inside"},{"name":"tread-inside28","bone":"tread14","attachment":"tread-inside"},{"name":"tread-inside4","bone":"tread2","attachment":"tread-inside"},{"name":"tread-inside56","bone":"tread28","attachment":"tread-inside"},{"name":"tread-inside30","bone":"tread15","attachment":"tread-inside"},{"name":"tread-inside6","bone":"tread3","attachment":"tread-inside"},{"name":"tread-inside58","bone":"tread29","attachment":"tread-inside"},{"name":"tread-inside32","bone":"tread16","attachment":"tread-inside"},{"name":"tread-inside8","bone":"tread4","attachment":"tread-inside"},{"name":"tread-inside60","bone":"tread30","attachment":"tread-inside"},{"name":"tread-inside34","bone":"tread17","attachment":"tread-inside"},{"name":"tread-inside10","bone":"tread5","attachment":"tread-inside"},{"name":"tread-inside62","bone":"tread31","attachment":"tread-inside"},{"name":"tread-inside36","bone":"tread18","attachment":"tread-inside"},{"name":"tread-inside12","bone":"tread6","attachment":"tread-inside"},{"name":"tread-inside64","bone":"tread32","attachment":"tread-inside"},{"name":"tread-inside38","bone":"tread19","attachment":"tread-inside"},{"name":"tread-inside14","bone":"tread7","attachment":"tread-inside"},{"name":"tread-inside66","bone":"tread33","attachment":"tread-inside"},{"name":"tread-inside40","bone":"tread20","attachment":"tread-inside"},{"name":"tread-inside16","bone":"tread8","attachment":"tread-inside"},{"name":"tread-inside68","bone":"tread34","attachment":"tread-inside"},{"name":"tread-inside70","bone":"tread35","attachment":"tread-inside"},{"name":"tread-inside72","bone":"tread36","attachment":"tread-inside"},{"name":"tread-inside42","bone":"tread21","attachment":"tread-inside"},{"name":"tread-inside18","bone":"tread9","attachment":"tread-inside"},{"name":"tread-inside44","bone":"tread22","attachment":"tread-inside"},{"name":"tread-inside20","bone":"tread10","attachment":"tread-inside"},{"name":"tread-inside46","bone":"tread23","attachment":"tread-inside"},{"name":"tread-inside22","bone":"tread11","attachment":"tread-inside"},{"name":"tread-inside48","bone":"tread24","attachment":"tread-inside"},{"name":"tread-inside24","bone":"tread12","attachment":"tread-inside"},{"name":"tread-inside50","bone":"tread25","attachment":"tread-inside"},{"name":"tread-inside26","bone":"tread13","attachment":"tread-inside"},{"name":"tread-inside52","bone":"tread26","attachment":"tread-inside"},{"name":"wheel-big","bone":"wheel-big1","color":"dbdbdbff","attachment":"wheel-big"},{"name":"wheel-big2","bone":"wheel-big2","color":"dbdbdbff","attachment":"wheel-big"},{"name":"wheel-mid","bone":"wheel-mid1","attachment":"wheel-mid"},{"name":"wheel-mid2","bone":"wheel-mid2","attachment":"wheel-mid"},{"name":"wheel-mid3","bone":"wheel-mid3","attachment":"wheel-mid"},{"name":"wheel-mid4","bone":"wheel-mid4","attachment":"wheel-mid"},{"name":"wheel-mid5","bone":"wheel-mid5","attachment":"wheel-mid"},{"name":"wheel-mid6","bone":"wheel-mid6","attachment":"wheel-mid"},{"name":"wheel-small","bone":"wheel-small1","attachment":"wheel-small"},{"name":"wheel-small2","bone":"wheel-small2","attachment":"wheel-small"},{"name":"wheel-small3","bone":"wheel-small3","attachment":"wheel-small"},{"name":"wheel-mid-overlay","bone":"wheel-mid-root1","color":"ffffffec","attachment":"wheel-mid-overlay","blend":"multiply"},{"name":"wheel-mid-overlay2","bone":"wheel-mid-root2","color":"ffffffec","attachment":"wheel-mid-overlay","blend":"multiply"},{"name":"wheel-mid-overlay3","bone":"wheel-mid-root3","color":"ffffffec","attachment":"wheel-mid-overlay","blend":"multiply"},{"name":"wheel-mid-overlay4","bone":"wheel-mid-root4","color":"ffffffec","attachment":"wheel-mid-overlay","blend":"multiply"},{"name":"wheel-mid-overlay5","bone":"wheel-mid-root5","color":"ffffffec","attachment":"wheel-mid-overlay","blend":"multiply"},{"name":"wheel-mid-overlay6","bone":"wheel-mid-root6","color":"ffffffec","attachment":"wheel-mid-overlay","blend":"multiply"},{"name":"wheel-big-overlay1","bone":"wheel-big-root1","color":"ffffffe9","attachment":"wheel-big-overlay","blend":"multiply"},{"name":"wheel-big-overlay2","bone":"wheel-big-root2","color":"ffffffe9","attachment":"wheel-big-overlay","blend":"multiply"},{"name":"treads-path","bone":"tank-root","attachment":"treads-path"},{"name":"tread","bone":"tread","attachment":"tread"},{"name":"tread27","bone":"tread27","color":"adc9b8ff","attachment":"tread"},{"name":"tread14","bone":"tread14","attachment":"tread"},{"name":"tread2","bone":"tread2","attachment":"tread"},{"name":"tread28","bone":"tread28","attachment":"tread"},{"name":"tread15","bone":"tread15","color":"adc9b8ff","attachment":"tread"},{"name":"tread3","bone":"tread3","color":"adc9b8ff","attachment":"tread"},{"name":"tread29","bone":"tread29","color":"adc9b8ff","attachment":"tread"},{"name":"tread16","bone":"tread16","attachment":"tread"},{"name":"tread4","bone":"tread4","attachment":"tread"},{"name":"tread30","bone":"tread30","attachment":"tread"},{"name":"tread17","bone":"tread17","color":"adc9b8ff","attachment":"tread"},{"name":"tread5","bone":"tread5","color":"adc9b8ff","attachment":"tread"},{"name":"tread31","bone":"tread31","color":"adc9b8ff","attachment":"tread"},{"name":"tread18","bone":"tread18","attachment":"tread"},{"name":"tread6","bone":"tread6","attachment":"tread"},{"name":"tread32","bone":"tread32","attachment":"tread"},{"name":"tread19","bone":"tread19","color":"adc9b8ff","attachment":"tread"},{"name":"tread7","bone":"tread7","color":"adc9b8ff","attachment":"tread"},{"name":"tread33","bone":"tread33","color":"adc9b8ff","attachment":"tread"},{"name":"tread20","bone":"tread20","attachment":"tread"},{"name":"tread8","bone":"tread8","attachment":"tread"},{"name":"tread34","bone":"tread34","attachment":"tread"},{"name":"tread35","bone":"tread35","color":"adc9b8ff","attachment":"tread"},{"name":"tread36","bone":"tread36","color":"adc9b8ff","attachment":"tread"},{"name":"tread21","bone":"tread21","color":"adc9b8ff","attachment":"tread"},{"name":"tread9","bone":"tread9","color":"adc9b8ff","attachment":"tread"},{"name":"tread22","bone":"tread22","attachment":"tread"},{"name":"tread10","bone":"tread10","attachment":"tread"},{"name":"tread23","bone":"tread23","color":"adc9b8ff","attachment":"tread"},{"name":"tread11","bone":"tread11","color":"adc9b8ff","attachment":"tread"},{"name":"tread24","bone":"tread24","attachment":"tread"},{"name":"tread12","bone":"tread12","attachment":"tread"},{"name":"tread25","bone":"tread25","color":"adc9b8ff","attachment":"tread"},{"name":"tread13","bone":"tread13","color":"adc9b8ff","attachment":"tread"},{"name":"tread26","bone":"tread26","attachment":"tread"},{"name":"machinegun","bone":"machinegun","attachment":"machinegun"},{"name":"machinegun-mount","bone":"machinegun-mount","attachment":"machinegun-mount"},{"name":"tank-top","bone":"tank-body","attachment":"tank-top"},{"name":"guntower","bone":"guntower","attachment":"guntower"},{"name":"cannon","bone":"cannon","attachment":"cannon"},{"name":"cannon-connector","bone":"cannon-connector","attachment":"cannon-connector"},{"name":"antenna","bone":"antenna-root","attachment":"antenna"},{"name":"smoke-puff1-bg","bone":"smoke1","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg13","bone":"smoke13","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg14","bone":"smoke14","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg15","bone":"smoke15","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg16","bone":"smoke16","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg17","bone":"smoke17","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg18","bone":"smoke18","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg20","bone":"smoke20","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg21","bone":"smoke21","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg22","bone":"smoke22","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg23","bone":"smoke23","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg24","bone":"smoke24","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg25","bone":"smoke25","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg26","bone":"smoke26","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg27","bone":"smoke27","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg2","bone":"smoke2","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg5","bone":"smoke5","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg6","bone":"smoke6","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg7","bone":"smoke7","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg10","bone":"smoke10","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg11","bone":"smoke11","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg12","bone":"smoke12","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg8","bone":"smoke8","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg9","bone":"smoke9","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg4","bone":"smoke4","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg3","bone":"smoke3","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg","bone":"smoke1","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg13","bone":"smoke13","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg14","bone":"smoke14","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg15","bone":"smoke15","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg16","bone":"smoke16","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg17","bone":"smoke17","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg18","bone":"smoke18","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg20","bone":"smoke20","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg21","bone":"smoke21","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg22","bone":"smoke22","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg23","bone":"smoke23","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg24","bone":"smoke24","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg25","bone":"smoke25","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg26","bone":"smoke26","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg27","bone":"smoke27","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg2","bone":"smoke2","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg5","bone":"smoke5","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg6","bone":"smoke6","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg7","bone":"smoke7","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg10","bone":"smoke10","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg11","bone":"smoke11","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg12","bone":"smoke12","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg8","bone":"smoke8","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg9","bone":"smoke9","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg4","bone":"smoke4","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg3","bone":"smoke3","color":"ecececff","dark":"000000"},{"name":"smoke-glow","bone":"smoke-glow","blend":"additive"},{"name":"clipping","bone":"tank-body","attachment":"clipping"},{"name":"tank-glow","bone":"tank-glow","color":"fcdc6da7","blend":"additive"}],"ik":[{"name":"cannon-ik","bones":["cannon"],"target":"cannon-target"},{"name":"machinegun-ik","order":1,"bones":["machinegun"],"target":"machinegun-target","mix":0}],"transform":[{"name":"wheel-big-transform","order":8,"bones":["wheel-big2"],"target":"wheel-big1","rotation":65.6,"mixX":0,"mixScaleX":0,"mixShearY":0},{"name":"wheel-mid1-transform","order":3,"bones":["wheel-mid2","wheel-mid4"],"target":"wheel-mid1","rotation":93,"mixX":0,"mixScaleX":0,"mixShearY":0},{"name":"wheel-mid2-transform","order":4,"bones":["wheel-mid3","wheel-mid5"],"target":"wheel-mid1","rotation":-89,"mixX":0,"mixScaleX":0,"mixShearY":0},{"name":"wheel-mid3-transform","order":5,"bones":["wheel-mid6"],"target":"wheel-mid1","rotation":-152.6,"mixX":0,"mixScaleX":0,"mixShearY":0},{"name":"wheel-small1-transform","order":6,"bones":["wheel-small2"],"target":"wheel-small1","rotation":87,"mixX":0,"mixScaleX":0,"mixShearY":0},{"name":"wheel-small2-transform","order":7,"bones":["wheel-small3"],"target":"wheel-small1","rotation":54.9,"mixX":0,"mixScaleX":0,"mixShearY":0}],"path":[{"name":"treads-path","order":2,"bones":["tread","tread2","tread3","tread4","tread5","tread6","tread7","tread8","tread9","tread10","tread11","tread12","tread13","tread14","tread15","tread16","tread17","tread18","tread19","tread20","tread21","tread22","tread23","tread24","tread25","tread26","tread27","tread28","tread29","tread30","tread31","tread32","tread33","tread34","tread35","tread36"],"target":"treads-path","rotateMode":"chain"}],"skins":[{"name":"default","attachments":{"antenna":{"antenna":{"type":"mesh","uvs":[0.64286,0.07876,0.65354,0.1535,0.66325,0.22138,0.67367,0.29433,0.68383,0.36543,0.6936,0.43374,0.70311,0.5003,0.71311,0.57031,0.72327,0.64139,0.73406,0.71689,0.74441,0.7893,0.75614,0.87141,0.76905,0.94311,1,0.94311,1,1,0,1,0,0.94311,0.20106,0.94311,0.20106,0.87094,0.21461,0.78847,0.22651,0.71607,0.23886,0.64099,0.25036,0.57105,0.26206,0.49983,0.27306,0.43291,0.2843,0.36454,0.29593,0.29382,0.308,0.22038,0.319,0.15345,0.33142,0.07796,0.34423,0,0.63161,0],"triangles":[30,31,0,29,30,0,29,0,1,28,29,1,28,1,2,27,28,2,27,2,3,26,3,4,25,26,4,25,4,5,26,27,3,24,5,6,23,24,6,7,23,6,24,25,5,22,7,8,21,22,8,21,8,9,7,22,23,20,9,10,19,20,10,20,21,9,19,10,11,18,19,11,17,18,11,17,11,12,15,16,17,12,13,14,15,17,12,14,15,12],"vertices":[2,10,65.38,-3.14,0.3125,11,23.38,-3.14,0.6875,2,10,42.73,-3.38,0.66667,11,0.73,-3.38,0.33333,2,9,64.17,-3.59,0.33333,10,22.17,-3.59,0.66667,2,9,42.06,-3.82,0.66667,10,0.06,-3.82,0.33333,2,8,62.52,-4.04,0.33333,9,20.52,-4.04,0.66667,2,8,41.82,-4.26,0.66667,9,-0.18,-4.26,0.33333,2,7,63.65,-4.47,0.33333,8,21.65,-4.47,0.66667,2,7,42.44,-4.69,0.66667,8,0.44,-4.69,0.33333,2,6,62.9,-4.91,0.33333,7,20.9,-4.91,0.66667,2,6,40.03,-5.15,0.66667,7,-1.97,-5.15,0.33333,2,5,5.38,58.09,0.4,6,18.09,-5.38,0.6,1,5,5.64,33.21,1,1,5,5.92,11.48,1,1,5,11,11.48,1,1,5,11,-5.76,1,1,5,-11,-5.76,1,1,5,-11,11.48,1,1,5,-6.58,11.48,1,1,5,-6.58,33.35,1,2,5,-6.28,58.34,0.4,6,18.34,6.28,0.6,2,6,40.27,6.02,0.66667,7,-1.73,6.02,0.33333,2,6,63.03,5.75,0.33333,7,21.03,5.75,0.66667,2,7,42.22,5.49,0.66667,8,0.22,5.49,0.33333,2,7,63.8,5.23,0.33333,8,21.8,5.23,0.66667,2,8,42.07,4.99,0.66667,9,0.07,4.99,0.33333,2,8,62.79,4.75,0.33333,9,20.79,4.75,0.66667,2,9,42.22,4.49,0.66667,10,0.22,4.49,0.33333,2,9,64.47,4.22,0.33333,10,22.47,4.22,0.66667,2,10,42.75,3.98,0.66667,11,0.75,3.98,0.33333,2,10,65.62,3.71,0.3125,11,23.62,3.71,0.6875,1,11,47.24,3.43,1,1,11,47.24,-2.9,1],"hull":32,"edges":[28,30,28,26,30,32,26,24,24,22,32,34,34,24,34,36,36,22,60,62,38,36,20,22,38,20,40,38,18,20,40,18,42,40,16,18,42,16,44,42,14,16,44,14,46,44,12,14,46,12,48,46,10,12,48,10,50,48,8,10,50,8,52,50,6,8,52,6,54,52,4,6,54,4,56,54,2,4,56,2,60,58,58,56,62,0,0,2,58,0],"width":22,"height":303}},"bottom":{"tank-bottom":{"x":-16.67,"y":9.89,"width":1285,"height":276}},"cannon":{"cannon":{"x":481.95,"y":-0.03,"rotation":180,"width":931,"height":58}},"cannon-connector":{"cannon-connector":{"type":"mesh","uvs":[1,0.03237,1,0.10603,0.90988,0.32859,0.81975,0.55116,0.72963,0.77373,0.6395,0.9963,0.42157,0.9963,0.20364,0.9963,0,0.85434,0,0.69902,0.02268,0.52884,0,0.31444,0.21602,0.12998,0.43368,0,0.63547,0.0037,0.48408,0.77059,0.31496,0.52497,0.64133,0.19648,0.21516,0.76766,0.58346,0.56471,0.68444,0.40146,0.46758,0.36649,0.28935,0.34604],"triangles":[7,18,6,6,18,15,7,8,18,8,9,18,18,16,15,15,16,19,9,10,18,18,10,16,16,21,19,19,21,20,10,22,16,10,11,22,16,22,21,21,17,20,21,12,13,17,13,14,17,21,13,11,12,22,21,22,12,6,15,5,5,15,4,15,19,4,4,19,3,19,20,3,3,20,2,20,17,2,2,17,1,17,14,1,14,0,1],"vertices":[1,12,35.91,69.08,1,1,12,35.91,59.14,1,1,12,25.82,29.09,1,1,12,15.72,-0.95,1,1,12,5.63,-31,1,1,12,-4.46,-61.05,1,2,12,-28.87,-61.05,0.33333,14,28.87,61.03,0.66667,1,14,53.28,61.02,1,1,14,76.09,41.84,1,1,14,71.17,21.63,1,1,14,72.83,-1.62,1,1,14,70.38,-29.12,1,1,14,50.67,-56.14,1,2,12,-28.43,74.38,0.41,14,28.43,-74.4,0.59,2,12,-4.92,72.95,0.52,14,4.92,-72.95,0.48,2,12,-21.87,-30.58,0.49,14,21.87,30.57,0.51,1,14,40.81,-2.6,1,2,12,-4.26,46.93,0.49,14,4.26,-46.93,0.51,1,14,51.99,30.15,1,2,12,-10.74,-2.78,0.49,14,10.74,2.78,0.51,2,12,0.57,19.25,0.49,14,-0.57,-19.25,0.51,1,14,23.72,-23.99,1,1,14,43.68,-26.76,1],"hull":15,"edges":[0,2,2,4,4,6,6,8,8,10,10,12,12,14,14,16,16,18,18,20,20,22,22,24,24,26,26,28,28,0],"width":112,"height":135}},"clipping":{"clipping":{"type":"clipping","end":"tank-glow","vertexCount":32,"vertices":[1,3,165.84,455.67,1,1,3,114.21,493.01,1,1,3,-38.53,492.23,1,1,3,-193.4,464.18,1,2,3,-280.85,415.48,0.752,14,24.09,-73.93,0.248,1,14,70.34,-27.32,1,1,14,412.56,-22.02,1,1,14,412.82,-29.21,1,1,14,539.26,-29.34,1,1,14,539.52,-17.09,1,1,14,894.02,-16.8,1,1,14,902.99,-28.89,1,1,14,942.06,-28.58,1,1,14,948.14,-16.64,1,1,14,947.9,14.29,1,1,14,539.3,14.55,1,1,14,539,29.22,1,1,14,412.51,29.88,1,1,14,412.51,21.73,1,1,14,74.24,27.28,1,1,3,-296.64,281.2,1,1,3,-316.06,225.71,1,1,3,-521.69,190.74,1,1,3,-610.03,141.02,1,1,3,-671.84,87.13,1,1,3,-652.23,-11.24,1,1,3,-618.53,-71.36,1,1,3,-478.77,-114.21,1,1,3,-274.11,-116.26,1,1,3,1.38,-45.75,1,1,3,189.67,148.78,1,1,3,215.75,276.59,1],"color":"ce3a3aff"}},"ground":{"ground":{"x":837.96,"y":-172,"width":1024,"height":353}},"ground2":{"ground":{"x":-179.89,"y":-172,"width":1024,"height":353}},"ground3":{"ground":{"x":-1213.48,"y":-172,"scaleX":1.035,"width":1024,"height":353}},"ground4":{"ground":{"x":-2268.51,"y":-172,"scaleX":1.04,"width":1024,"height":353}},"ground5":{"ground":{"x":-3306.54,"y":-172,"width":1024,"height":353}},"ground6":{"ground":{"x":-4322.71,"y":-172,"width":1024,"height":353}},"ground7":{"ground":{"x":-5340.65,"y":-172,"width":1024,"height":353}},"guntower":{"guntower":{"x":77.22,"y":122.59,"width":730,"height":289}},"machinegun":{"machinegun":{"x":44.85,"y":-5.72,"rotation":-180,"width":331,"height":57}},"machinegun-mount":{"machinegun-mount":{"x":47.42,"y":-1.54,"rotation":-90,"width":72,"height":96}},"rock":{"rock":{"x":25.24,"y":27.35,"width":580,"height":127}},"smoke-glow":{"smoke-glow":{"type":"mesh","uvs":[1,0.24906,1,0.51991,1,0.73165,0.70776,1,0.49012,1,0.24373,1,0,0.71158,0,0.50308,0,0.26235,0.28107,0,0.47435,0,0.73345,0,0.48858,0.51759],"triangles":[12,7,8,12,10,11,12,11,0,9,10,12,12,8,9,12,0,1,6,7,12,12,1,2,5,6,12,3,4,12,5,12,4,2,3,12],"vertices":[49.99,25.1,50,-1.98,50.01,-23.15,20.79,-50,-0.98,-50,-25.62,-50.01,-50,-21.17,-50,-0.32,-50.01,23.75,-21.9,50,-2.58,50,23.33,50.01,-1.14,-1.76],"hull":12,"edges":[2,24,24,14,20,24,24,8,2,0,20,22,0,22,18,20,14,16,18,16,12,14,8,10,12,10,6,8,2,4,6,4],"width":100,"height":100}},"smoke-puff1-bg":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg2":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg3":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg4":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg5":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg6":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg7":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg8":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg9":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg10":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg11":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg12":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg13":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg14":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg15":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg16":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg17":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg18":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg20":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg21":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg22":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg23":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg24":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg25":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg26":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg27":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-fg":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg2":{"smoke-puff01-fg":{"x":-1.01,"y":-0.07,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.82,"y":-0.39,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.82,"y":-0.39,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.82,"y":-0.39,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg3":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.02,"y":-0.25,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1145,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.03,"y":-0.43,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg4":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.63,"y":-0.09,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.63,"y":-0.09,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.63,"y":-0.09,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg5":{"smoke-puff01-fg":{"x":-1.21,"y":-0.08,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.89,"y":-0.04,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.89,"y":-0.04,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.89,"y":-0.04,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg6":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.48,"y":-0.07,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.48,"y":-0.07,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.48,"y":-0.07,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg7":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-0.7,"y":-0.36,"scaleX":0.1216,"scaleY":0.1214,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-0.56,"y":-0.15,"scaleX":0.1224,"scaleY":0.1224,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-0.56,"y":-0.15,"scaleX":0.1224,"scaleY":0.1224,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg8":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-0.65,"y":0.01,"scaleX":0.1226,"scaleY":0.1226,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-0.75,"y":-0.15,"scaleX":0.1211,"scaleY":0.1211,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-0.75,"y":-0.15,"scaleX":0.1211,"scaleY":0.1211,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg9":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.99,"y":-0.09,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.99,"y":-0.09,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-0.95,"y":-0.48,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg10":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.27,"y":-0.37,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.27,"y":-0.37,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.27,"y":-0.37,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg11":{"smoke-puff04-fg":{"x":-1.27,"y":-0.37,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg12":{"smoke-puff04-fg":{"x":-1.27,"y":-0.37,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg13":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg14":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg15":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg16":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg17":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg18":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg20":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg21":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg22":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg23":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg24":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg25":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg26":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg27":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"tank-body-shadow":{"tank-bottom-shadow":{"x":-11.44,"y":-42.89,"width":1291,"height":341}},"tank-glow":{"smoke-glow":{"type":"mesh","uvs":[1,1,0,1,1,0],"triangles":[1,2,0],"vertices":[469.64,-738.08,-1660.32,-738.08,469.64,1391.88],"hull":3,"edges":[0,2,0,4,2,4],"width":100,"height":100}},"tank-top":{"tank-top":{"x":6.8,"y":168.71,"width":1407,"height":222}},"tread":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread-inside1":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside2":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside3":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside4":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside5":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside6":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside7":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside8":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside9":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside10":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside11":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside12":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside13":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside14":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside15":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside16":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside17":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside18":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside19":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside20":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside21":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside22":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside23":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside24":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside25":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside26":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside27":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside28":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside29":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside30":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside31":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside32":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside33":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside34":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside35":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside36":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside37":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside38":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside39":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside40":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside41":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside42":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside43":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside44":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside45":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside46":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside47":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside48":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside49":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside50":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside51":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside52":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside53":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside54":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside55":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside56":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside57":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside58":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside59":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside60":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside61":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside62":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside63":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside64":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside65":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside66":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside67":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside68":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside69":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside70":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside71":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside72":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread2":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread3":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread4":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread5":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread6":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread7":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread8":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread9":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread10":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread11":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread12":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread13":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread14":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread15":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread16":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread17":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread18":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread19":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread20":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread21":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread22":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread23":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread24":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread25":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread26":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread27":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread28":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread29":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread30":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread31":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread32":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread33":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread34":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread35":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread36":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"treads-path":{"treads-path":{"type":"path","closed":true,"lengths":[185.21,354.53,478.3,608.52,786,1058.49,1138.97,1223.96,1303.87,1388.23,1471.11,1551.64,1633.55,1713.27,1799.89,1882.28,2164.2,2326.85,2444.07,2584.91,2754.15,2940.62],"vertexCount":66,"vertices":[1,110,11.23,41.87,1,1,110,0.79,41.95,1,1,110,-34.72,42.24,1,1,56,-104.22,0.41,1,1,56,0.07,0.55,1,1,56,68.8,0.65,1,1,109,20.5,43.47,1,1,109,1.14,40.82,1,1,109,-27.38,36.85,1,1,93,147.07,105.01,1,1,93,96.21,96.63,1,1,93,43.87,87.72,1,1,93,16.18,103.35,1,1,93,-33.67,94.21,1,1,93,-99.36,81.25,1,1,93,-122.05,-1.7,1,1,93,-83.58,-47.93,1,1,93,-33.53,-109.37,1,1,97,-83.57,-66.1,1,1,97,-2.17,-67.9,1,2,97,56.68,-41.49,0.68,51,-24.31,-41.49,0.32,1,51,-26.59,16.7,1,1,51,-2.69,16.7,1,1,51,13.52,16.7,1,2,98,-52.42,-46.51,0.744,51,30.21,-46.52,0.256,1,98,-0.32,-68.92,1,2,98,52.09,-44.73,0.712,52,-28.91,-44.73,0.288,1,52,-22.81,16.24,1,1,52,-1.42,16.24,1,1,52,20.48,16.24,1,2,99,-47.21,-47.46,0.744,52,36.01,-47.46,0.256,1,99,-0.29,-69.66,1,2,99,45.24,-47.26,0.736,53,-37.49,-47.26,0.264,1,53,-23.76,15.28,1,1,53,-0.14,15.28,1,1,53,24.45,15.28,1,2,100,-47.37,-48.7,0.744,53,33.53,-48.7,0.256,1,100,-0.5,-70.4,1,2,100,49.09,-48.34,0.744,54,-33.58,-48.34,0.256,1,54,-20.89,15.84,1,1,54,-1.26,15.84,1,1,54,15.78,15.84,1,2,101,-52.5,-48.21,0.76,54,28.45,-48.22,0.24,1,101,-2.5,-68.92,1,2,101,55.72,-47.82,0.752,55,-28.88,-47.83,0.248,1,55,-21.64,16.7,1,1,55,-0.48,16.7,1,1,55,20.74,16.7,1,2,102,-53.65,-48.9,0.76,55,25.97,-48.9,0.24,1,102,2.28,-69.66,1,1,102,44.95,-69.74,1,1,94,76.03,-85.61,1,1,94,93.58,-42.24,1,1,94,118.67,19.75,1,1,94,78.59,76.62,1,1,94,37.27,95.07,1,1,94,31.45,97.67,1,1,94,-15.16,87.48,1,1,94,-79.8,92.52,1,1,94,-119.06,95.58,1,1,111,47.07,42.29,1,1,111,0.25,42.75,1,1,111,-29.64,43.29,1,1,57,-86.65,1.35,1,1,57,0.49,0.26,1,1,57,92.42,-0.9,1],"color":"ff8819ff"}},"wheel-big":{"wheel-big":{"width":191,"height":191}},"wheel-big-overlay1":{"wheel-big-overlay":{"width":186,"height":186}},"wheel-big-overlay2":{"wheel-big-overlay":{"width":186,"height":186}},"wheel-big2":{"wheel-big":{"width":191,"height":191}},"wheel-mid":{"wheel-mid":{"width":136,"height":136}},"wheel-mid-overlay":{"wheel-mid-overlay":{"width":136,"height":136}},"wheel-mid-overlay2":{"wheel-mid-overlay":{"width":136,"height":136}},"wheel-mid-overlay3":{"wheel-mid-overlay":{"width":136,"height":136}},"wheel-mid-overlay4":{"wheel-mid-overlay":{"width":136,"height":136}},"wheel-mid-overlay5":{"wheel-mid-overlay":{"width":136,"height":136}},"wheel-mid-overlay6":{"wheel-mid-overlay":{"width":136,"height":136}},"wheel-mid2":{"wheel-mid":{"width":136,"height":136}},"wheel-mid3":{"wheel-mid":{"width":136,"height":136}},"wheel-mid4":{"wheel-mid":{"width":136,"height":136}},"wheel-mid5":{"wheel-mid":{"width":136,"height":136}},"wheel-mid6":{"wheel-mid":{"width":136,"height":136}},"wheel-small":{"wheel-small":{"width":71,"height":71}},"wheel-small2":{"wheel-small":{"width":71,"height":71}},"wheel-small3":{"wheel-small":{"width":71,"height":71}}}}],"animations":{"drive":{"bones":{"tank-root":{"rotate":[{"time":2},{"time":2.0667,"value":1.99},{"time":2.5,"value":-15.63},{"time":2.6667,"value":-10.37,"curve":[2.718,-10.37,2.78,-8.34]},{"time":2.8333,"value":-6.13,"curve":[2.909,-2.8,2.974,0.8]},{"time":3,"value":1.84},{"time":3.0667,"value":5.32},{"time":3.1667,"value":10.99},{"time":3.2333,"value":9.73},{"time":3.4333,"value":-4.52,"curve":[3.474,-3.99,3.608,0.01]},{"time":3.6667,"value":0.01}],"translate":[{"curve":[1.019,0,1.608,-582.83,1.019,0,1.608,0]},{"time":2,"x":-1209.75},{"time":2.3333,"x":-1652.84,"y":26.05},{"time":2.5,"x":-1877.69,"y":71.5},{"time":2.6667,"x":-2053.37,"y":100.44},{"time":2.8333,"x":-2183.86,"y":97.42},{"time":3,"x":-2312.32,"y":74.12},{"time":3.0667,"x":-2340.68,"y":45.94},{"time":3.1333,"x":-2403.04,"y":17.04},{"time":3.1667,"x":-2439.84,"y":5.45},{"time":3.2333,"x":-2523.34,"y":-3.31},{"time":3.4333,"x":-2728.27,"y":-12.73},{"time":3.5,"x":-2795.65,"y":-6.14,"curve":[3.538,-2829.89,3.583,-2878.59,3.538,-4.93,3.583,-3.21]},{"time":3.6333,"x":-2938.53,"y":-1.09,"curve":[3.89,-3218.84,4.404,-3972.02,3.89,-0.79,4.404,0]},{"time":4.8333,"x":-3972.02},{"time":5,"x":-3991.31},{"time":5.3667,"x":-3973.94}]},"tread-collider1":{"translate":[{"time":2},{"time":2.0667,"y":9.99},{"time":2.1667,"y":37.69},{"time":2.3333,"y":53.45},{"time":2.5,"y":30.97},{"time":2.6667,"y":-2.89},{"time":2.8333,"y":-0.71},{"time":3.0667,"y":-13.64},{"time":3.1667,"y":59.3},{"time":3.2333,"y":48.2},{"time":3.4333,"y":-11.27},{"time":3.6333,"y":4.15}]},"tread-collider2":{"translate":[{"time":2},{"time":2.0667,"y":-2.83},{"time":2.1667,"y":-17.44},{"time":2.3333,"y":46.07},{"time":2.5,"y":19.45},{"time":2.6667,"y":13.46},{"time":2.8333,"y":-1.92,"curve":"stepped"},{"time":2.9667,"y":-1.92},{"time":3,"y":-13.17},{"time":3.0667,"y":-23.25},{"time":3.1667,"y":28.13},{"time":3.2333,"y":25.63},{"time":3.4333,"y":-1.52},{"time":3.6333,"y":1.15}]},"tread-collider3":{"translate":[{"time":2},{"time":2.0667,"y":-7.76},{"time":2.1667,"y":-16.61},{"time":2.5,"y":29.05},{"time":2.6667,"y":30.12},{"time":2.8333,"y":5.3},{"time":3,"y":-0.38},{"time":3.1667,"y":2.6},{"time":3.4333,"y":15.41},{"time":3.6333,"y":1.44}]},"tread-collider4":{"translate":[{"time":2},{"time":2.1667,"y":-6.72},{"time":2.3333,"y":-0.92},{"time":2.5,"y":18.37},{"time":2.6667,"y":38.77},{"time":2.8333,"y":30.6},{"time":3.1667,"y":12.61},{"time":3.2333,"y":-16},{"time":3.4333,"y":25.62},{"time":3.6333,"y":-0.68}]},"tread-collider5":{"translate":[{"time":2},{"time":2.1667,"y":3.35},{"time":2.3333,"y":22.17},{"time":2.6667,"y":13.35},{"time":2.8333,"y":39},{"time":3,"y":39.88},{"time":3.1667,"y":26.57},{"time":3.2333,"y":-10.15},{"time":3.4333,"y":35.98},{"time":3.6333,"y":-1.36}]},"wheel-mid-root6":{"translate":[{"time":2},{"time":2.1667,"y":5.61},{"time":2.3333,"y":27.21},{"time":2.5,"y":30.28},{"time":2.6667,"y":-2.81},{"time":2.8333,"y":19.59},{"time":3,"y":29.11},{"time":3.1667,"y":32.55},{"time":3.2333,"y":3.55},{"time":3.4333,"y":40.54},{"time":3.6333}]},"wheel-mid-root5":{"translate":[{"time":2},{"time":2.1667,"y":-7.46},{"time":2.3333,"y":9.53},{"time":2.6667,"y":36.78},{"time":2.8333,"y":46.11},{"time":3.1667,"y":7.55},{"time":3.2333,"y":-16.28},{"time":3.4333,"y":26.21},{"time":3.6333}]},"wheel-mid-root4":{"translate":[{"time":2},{"time":2.1667,"y":-13.98},{"time":2.3333,"y":-8.26},{"time":2.5,"y":24.27},{"time":2.6667,"y":34.42},{"time":2.8333,"y":8.88},{"time":3.1667,"y":10.32},{"time":3.2333,"y":-7.63},{"time":3.4333,"y":19.69},{"time":3.6333}]},"wheel-mid-root3":{"translate":[{"time":2},{"time":2.1667,"y":-21.14},{"time":2.3333,"y":22.83},{"time":2.5,"y":23.34},{"time":2.6667,"y":18.07},{"time":2.8333,"y":1.2},{"time":3.0667,"y":-13.36},{"time":3.1667,"y":15.48},{"time":3.2333,"y":13.34},{"time":3.4333,"y":6.4},{"time":3.6333}]},"wheel-mid-root2":{"translate":[{"time":2},{"time":2.0667,"y":-4.39},{"time":2.1667,"y":3.13},{"time":2.3333,"y":53.56},{"time":2.5,"y":16.65},{"time":2.6667,"y":8.39},{"time":3.0667,"y":-19.16},{"time":3.1667,"y":43.25},{"time":3.2333,"y":39.04},{"time":3.4333,"y":-8.61},{"time":3.6333}]},"wheel-mid-root1":{"translate":[{"time":2},{"time":2.0333,"y":22.64},{"time":2.0667,"y":53.65},{"time":2.1667,"y":71.18},{"time":2.5,"y":46.83},{"time":2.6667,"y":8.38},{"time":3.0667,"y":-10.03},{"time":3.1667,"y":72.71},{"time":3.2333,"y":64.74},{"time":3.4333,"y":-17.65},{"time":3.6333}]},"tank-body":{"rotate":[{"curve":[0.208,0,0.625,-4.39]},{"time":0.8333,"value":-4.39},{"time":2},{"time":2.1667,"value":-1.34},{"time":2.3333,"value":-6.23},{"time":2.5,"value":-5.45},{"time":2.9667,"value":-5.07},{"time":3.0667,"value":-2.39},{"time":3.1667,"value":-0.98},{"time":3.2333,"value":-1.1},{"time":3.4,"value":0.43,"curve":[3.433,0.43,3.483,-1.56]},{"time":3.5333,"value":-3.55,"curve":[3.675,-3.47,3.754,1.49]},{"time":3.8333,"value":1.93},{"time":4,"value":0.48},{"time":4.3333,"curve":[4.476,0.59,4.833,3.8]},{"time":5,"value":3.8,"curve":[5.286,3.8,5.35,-2.17]},{"time":5.4667,"value":-2.17},{"time":5.6,"value":-0.61}]},"wheel-big-root1":{"translate":[{"time":2},{"time":2.0667,"y":20.07},{"time":2.3333,"y":67.24},{"time":2.6667,"y":21.04},{"time":3,"y":10.28},{"time":3.1,"y":11.28},{"time":3.1667,"y":29.43},{"time":3.2333,"y":35.31},{"time":3.4333,"y":18.38},{"time":3.5}]},"tank-treads":{"rotate":[{},{"time":0.8333,"value":-2.4},{"time":2},{"time":2.0667,"value":1.72},{"time":2.4333,"value":-0.37},{"time":2.8},{"time":3,"value":-1.41},{"time":3.1667,"value":0.54},{"time":3.2667,"value":2.22,"curve":[3.348,2.22,3.392,-1.31]},{"time":3.4333,"value":-1.31},{"time":3.7333,"value":-1.14},{"time":4.3333,"curve":[4.476,0.35,4.833,2.24]},{"time":5,"value":2.24,"curve":[5.286,2.24,5.35,0]},{"time":5.4667}]},"cannon-target":{"translate":[{},{"time":0.8333,"y":121.95},{"time":2,"y":45.73}]},"wheel-big-root2":{"translate":[{"time":3.4333,"y":13.01}]},"wheel-big1":{"rotate":[{"curve":[0.51,0,0.804,57.81]},{"time":1,"value":120},{"time":1.2667,"value":240},{"time":1.5333,"value":360},{"time":1.7667,"value":480},{"time":2.0333,"value":600},{"time":2.2,"value":720},{"time":2.4,"value":840},{"time":2.5667,"value":960},{"time":2.7333,"value":1080},{"time":2.9333,"value":1200},{"time":3.1333,"value":1320},{"time":3.3333,"value":1440},{"time":3.5,"value":1560},{"time":3.6667,"value":1680},{"time":3.8667,"value":1800},{"time":4.0667,"value":1920},{"time":4.2667,"value":2040},{"time":4.5,"value":2160,"curve":[4.563,2194.34,4.695,2225.3]},{"time":4.8333,"value":2247.67}]},"wheel-mid1":{"rotate":[{"curve":[0.459,0,0.724,57.81]},{"time":0.9,"value":120},{"time":1.1667,"value":240},{"time":1.4333,"value":360},{"time":1.6333,"value":480},{"time":1.8333,"value":600},{"time":2,"value":720},{"time":2.1333,"value":840},{"time":2.2667,"value":960},{"time":2.4,"value":1080},{"time":2.5333,"value":1200},{"time":2.6667,"value":1320},{"time":2.8333,"value":1440},{"time":2.9667,"value":1560},{"time":3.1,"value":1680},{"time":3.2333,"value":1800},{"time":3.3667,"value":1920},{"time":3.5,"value":2040},{"time":3.6333,"value":2160},{"time":3.7667,"value":2280},{"time":3.9,"value":2400},{"time":4.0333,"value":2520},{"time":4.1667,"value":2640},{"time":4.3,"value":2760},{"time":4.4667,"value":2880,"curve":[4.538,2949.2,4.742,3000]},{"time":4.8333,"value":3000}]},"wheel-small1":{"rotate":[{"curve":[0.34,0,0.536,57.81]},{"time":0.6667,"value":120},{"time":0.8667,"value":240},{"time":1.0333,"value":360},{"time":1.1667,"value":480},{"time":1.3,"value":600},{"time":1.4333,"value":720},{"time":1.5333,"value":840},{"time":1.6333,"value":960},{"time":1.7333,"value":1080},{"time":1.8333,"value":1200},{"time":1.9333,"value":1320},{"time":2.0333,"value":1440},{"time":2.1333,"value":1560},{"time":2.2333,"value":1680},{"time":2.3333,"value":1800},{"time":2.4333,"value":1920},{"time":2.5333,"value":2040},{"time":2.6333,"value":2160},{"time":2.7333,"value":2280},{"time":2.8333,"value":2400},{"time":2.9333,"value":2520},{"time":3.0333,"value":2640},{"time":3.1333,"value":2760},{"time":3.2333,"value":2880},{"time":3.3333,"value":3000},{"time":3.4333,"value":3120},{"time":3.5333,"value":3240},{"time":3.6333,"value":3360},{"time":3.7333,"value":3480},{"time":3.8333,"value":3600},{"time":3.9333,"value":3720},{"time":4.0333,"value":3840},{"time":4.1333,"value":3960},{"time":4.2333,"value":4080},{"time":4.3333,"value":4200},{"time":4.4333,"value":4320},{"time":4.6667,"value":4440},{"time":4.9,"value":4490}]},"wheel-small-root1":{"translate":[{"time":2},{"time":2.1333,"y":12.37},{"time":2.4667,"y":32.37},{"time":2.7333,"y":-5.27},{"time":2.9667,"y":14.31},{"time":3.1667,"y":19.54},{"time":3.4667,"y":7.5},{"time":4.3667,"y":-2.4}]},"wheel-small-root2":{"translate":[{"time":2},{"time":2.9,"y":5.26},{"time":3.1667,"y":10.67},{"time":3.4667,"y":-4.71}]},"wheel-small-root3":{"translate":[{"time":2},{"time":2.4667,"y":-10.56},{"time":2.9,"y":-16.08},{"time":3.1667,"y":10.12},{"time":3.4667,"y":4.1},{"time":4.3667,"y":-0.03}]},"antenna1":{"rotate":[{},{"time":0.3333,"value":1.05,"curve":[0.731,1.05,1.008,-4.55]},{"time":1.2333,"value":-4.55,"curve":[1.35,-4.55,1.583,0.53]},{"time":1.7,"value":0.53},{"time":2,"value":-2.13},{"time":2.2,"value":4.71},{"time":2.3667,"value":-7.26},{"time":2.5667,"value":2.26},{"time":2.7333,"value":-3.39},{"time":3.0667,"value":-5.53},{"time":3.2333,"value":7.78},{"time":3.4667,"value":-5.99},{"time":3.7,"value":3.11},{"time":3.9,"value":-3.05},{"time":4.1,"value":0.31},{"time":4.3,"value":-3.06},{"time":4.5333,"value":0.36},{"time":4.8667,"value":4.94,"curve":[4.925,4.94,5.042,-2.38]},{"time":5.1,"value":-2.38},{"time":5.2667,"value":3.65},{"time":5.4,"value":-3.04},{"time":5.5,"value":1.49},{"time":5.6,"value":-1.86},{"time":5.7,"value":0.42}]},"antenna2":{"rotate":[{},{"time":0.3333,"value":1.05,"curve":[0.731,1.05,1.008,-4.55]},{"time":1.2333,"value":-4.55,"curve":[1.35,-4.55,1.583,0.53]},{"time":1.7,"value":0.53},{"time":2,"value":-2.13},{"time":2.2,"value":4.71},{"time":2.3667,"value":-7.26},{"time":2.5667,"value":2.26},{"time":2.7333,"value":-3.39},{"time":3.0667,"value":-5.53},{"time":3.2333,"value":7.78},{"time":3.4667,"value":-5.99},{"time":3.7,"value":3.11},{"time":3.9,"value":-3.05},{"time":4.1,"value":0.31},{"time":4.3,"value":-3.06},{"time":4.5333,"value":0.36},{"time":4.8667,"value":4.94,"curve":[4.925,4.94,5.042,-2.38]},{"time":5.1,"value":-2.38},{"time":5.2667,"value":3.65},{"time":5.4,"value":-3.04},{"time":5.5,"value":1.49},{"time":5.6,"value":-1.86},{"time":5.7,"value":0.42}]},"antenna3":{"rotate":[{},{"time":0.3333,"value":1.05,"curve":[0.731,1.05,1.008,-4.55]},{"time":1.2333,"value":-4.55,"curve":[1.35,-4.55,1.583,0.53]},{"time":1.7,"value":0.53},{"time":2,"value":-2.13},{"time":2.2,"value":4.71},{"time":2.3667,"value":-7.26},{"time":2.5667,"value":2.26},{"time":2.7333,"value":-3.39},{"time":3.0667,"value":-5.53},{"time":3.2333,"value":7.78},{"time":3.4667,"value":-5.99},{"time":3.7,"value":3.11},{"time":3.9,"value":-3.05},{"time":4.1,"value":0.31},{"time":4.3,"value":-3.06},{"time":4.5333,"value":0.36},{"time":4.8667,"value":4.94,"curve":[4.925,4.94,5.042,-2.38]},{"time":5.1,"value":-2.38},{"time":5.2667,"value":3.65},{"time":5.4,"value":-3.04},{"time":5.5,"value":1.49},{"time":5.6,"value":-1.86},{"time":5.7,"value":0.42}]},"antenna4":{"rotate":[{},{"time":0.3333,"value":1.05,"curve":[0.731,1.05,1.008,-4.55]},{"time":1.2333,"value":-4.55,"curve":[1.35,-4.55,1.583,0.53]},{"time":1.7,"value":0.53},{"time":2,"value":-2.13},{"time":2.2,"value":4.71},{"time":2.3667,"value":-7.26},{"time":2.5667,"value":2.26},{"time":2.7333,"value":-3.39},{"time":3.0667,"value":-5.53},{"time":3.2333,"value":7.78},{"time":3.4667,"value":-5.99},{"time":3.7,"value":3.11},{"time":3.9,"value":-3.05},{"time":4.1,"value":0.31},{"time":4.3,"value":-3.06},{"time":4.5333,"value":0.36},{"time":4.8667,"value":4.94,"curve":[4.925,4.94,5.042,-2.38]},{"time":5.1,"value":-2.38},{"time":5.2667,"value":3.65},{"time":5.4,"value":-3.04},{"time":5.5,"value":1.49},{"time":5.6,"value":-1.86},{"time":5.7,"value":0.42}]},"antenna5":{"rotate":[{},{"time":0.3333,"value":1.05,"curve":[0.731,1.05,1.008,-4.55]},{"time":1.2333,"value":-4.55,"curve":[1.35,-4.55,1.583,0.53]},{"time":1.7,"value":0.53},{"time":2,"value":-2.13},{"time":2.2,"value":4.71},{"time":2.3667,"value":-7.26},{"time":2.5667,"value":2.26},{"time":2.7333,"value":-3.39},{"time":3.0667,"value":-5.53},{"time":3.2333,"value":7.78},{"time":3.4667,"value":-5.99},{"time":3.7,"value":3.11},{"time":3.9,"value":-3.05},{"time":4.1,"value":0.31},{"time":4.3,"value":-3.06},{"time":4.5333,"value":0.36},{"time":4.8667,"value":4.94,"curve":[4.925,4.94,5.042,-2.38]},{"time":5.1,"value":-2.38},{"time":5.2667,"value":3.65},{"time":5.4,"value":-3.04},{"time":5.5,"value":1.49},{"time":5.6,"value":-1.86},{"time":5.7,"value":0.42}]},"antenna6":{"rotate":[{},{"time":0.3333,"value":1.05,"curve":[0.731,1.05,1.008,-4.55]},{"time":1.2333,"value":-4.55,"curve":[1.35,-4.55,1.583,0.53]},{"time":1.7,"value":0.53},{"time":2,"value":-2.13},{"time":2.2,"value":4.71},{"time":2.3667,"value":-7.26},{"time":2.5667,"value":2.26},{"time":2.7333,"value":-3.39},{"time":3.0667,"value":-5.53},{"time":3.2333,"value":7.78},{"time":3.4667,"value":-5.99},{"time":3.7,"value":3.11},{"time":3.9,"value":-3.05},{"time":4.1,"value":0.31},{"time":4.3,"value":-3.06},{"time":4.5333,"value":0.36},{"time":4.8667,"value":4.94,"curve":[4.925,4.94,5.042,-2.38]},{"time":5.1,"value":-2.38},{"time":5.2667,"value":3.65},{"time":5.4,"value":-3.04},{"time":5.5,"value":1.49},{"time":5.6,"value":-1.86},{"time":5.7,"value":0.42}]},"machinegun":{"rotate":[{"value":8.07,"curve":"stepped"},{"time":2.0667,"value":8.07},{"time":2.1667,"value":3.11},{"time":2.5667,"value":-10.99,"curve":"stepped"},{"time":3.1333,"value":-10.99},{"time":3.2667,"value":18.18},{"time":3.4333,"value":2.75,"curve":"stepped"},{"time":4.7,"value":2.75},{"time":4.9,"value":8.07}]}},"path":{"treads-path":{"position":[{"curve":[0.984,0,1.588,0.1788]},{"time":2,"value":0.385,"curve":[2.023,0.3916,2.045,0.3983]},{"time":2.0667,"value":0.405},{"time":2.3333,"value":0.555},{"time":2.5,"value":0.605},{"time":2.6667,"value":0.685},{"time":2.8333,"value":0.745},{"time":3,"value":0.785},{"time":3.0667,"value":0.8},{"time":3.1333,"value":0.825},{"time":3.1667,"value":0.835},{"time":3.2333,"value":0.87},{"time":3.5,"value":0.98,"curve":[3.726,1.0474,4.335,1.4]},{"time":4.8333,"value":1.4}]}}},"shoot":{"slots":{"rock":{"attachment":[{}]},"smoke-glow":{"rgba":[{"time":0.1333,"color":"ffffffff"},{"time":0.1667,"color":"ffbc8af4"},{"time":0.2,"color":"fc8e8e90"},{"time":0.2667,"color":"fa3e3e1e"}],"attachment":[{"time":0.0667,"name":"smoke-glow"},{"time":0.3}]},"smoke-puff1-bg":{"rgba2":[{"time":0.0667,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.6,"light":"ffd50cff","dark":"604b3f"},{"time":1.0333,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.0667,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg2":{"rgba2":[{"time":0.0667,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":0.8333,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg3":{"rgba2":[{"time":0.0667,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":0.8333,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg4":{"rgba2":[{"time":0.0667,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.6,"light":"ffd50cff","dark":"604b3f"},{"time":0.9,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg5":{"rgba2":[{"time":0.0667,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.6,"light":"ffd50cff","dark":"604b3f"},{"time":0.9,"light":"ffd50c00","dark":"604a3f"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg6":{"rgba2":[{"time":0.0667,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":0.7,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg7":{"rgba2":[{"time":0.0667,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3333,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":0.8333,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg8":{"rgba2":[{"time":0.0667,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4333,"light":"ffd50cff","dark":"604b3f"},{"time":0.9333,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg9":{"rgba2":[{"time":0.1333,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.5333,"light":"ffd50cff","dark":"604b3f"},{"time":0.8333,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg10":{"rgba2":[{"time":0.1333,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.5333,"light":"ffd50cff","dark":"604b3f"},{"time":0.8333,"light":"ffd50c00","dark":"604a3f"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg11":{"rgba2":[{"time":0.1333,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":0.7667,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.3333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg12":{"rgba2":[{"time":0.3333,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.6,"light":"ffd50cff","dark":"604b3f"},{"time":0.8667,"light":"ffd50c00","dark":"604a3f"}],"attachment":[{"time":0.3667,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg13":{"rgba2":[{"time":0.3667,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":1,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.3667,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg14":{"rgba2":[{"time":0.4333,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.6,"light":"ffd50cff","dark":"604b3f"},{"time":1.0667,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.4333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg15":{"rgba2":[{"time":0.4,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":0.8333,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.4,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg16":{"rgba2":[{"time":0.4,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":0.7,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.4,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg17":{"rgba2":[{"time":0.2333,"light":"ffd50cff","dark":"534035"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4,"light":"ffd50cff","dark":"604b3f"},{"time":0.6667,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.2333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg18":{"rgba2":[{"time":0.0667,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.5,"light":"ffd50cff","dark":"604b3f"},{"time":0.8,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.2333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg20":{"rgba2":[{"time":0.0667,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":0.8,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.3333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg21":{"rgba2":[{"time":0.0667,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":0.7,"light":"ffd50c00","dark":"604b3f"}]},"smoke-puff1-bg22":{"rgba2":[{"time":0.0667,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":0.7,"light":"ffd50c00","dark":"604b3f"}]},"smoke-puff1-bg23":{"rgba2":[{"time":0.0667,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.5,"light":"ffd50cff","dark":"604b3f"},{"time":0.7667,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.3,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg24":{"rgba2":[{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":0.7,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.3,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg25":{"rgba2":[{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":1,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.3667,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg26":{"rgba2":[{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.6,"light":"ffd50cff","dark":"604b3f"},{"time":0.9333,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.3667,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg27":{"rgba2":[{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":0.7333,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.3667,"name":"smoke-puff01-bg"}]},"smoke-puff1-fg":{"rgba2":[{"time":0.0667,"light":"ffdf31ff","dark":"ff0000"},{"time":0.1333,"light":"fde252ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.6,"light":"ab764cff","dark":"ac8d75"},{"time":1.0333,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.0667,"name":"smoke-puff01-fg"},{"time":0.1667,"name":"smoke-puff02-fg"},{"time":0.2,"name":"smoke-puff03-fg"},{"time":0.2333,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg2":{"rgba2":[{"time":0.1333,"light":"ffdf31ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":0.8333,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-fg"},{"time":0.1667,"name":"smoke-puff02-fg"},{"time":0.2,"name":"smoke-puff03-fg"},{"time":0.2333,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg3":{"rgba2":[{"time":0.1333,"light":"ffe457ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":0.8333,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-fg"},{"time":0.1667,"name":"smoke-puff02-fg"},{"time":0.2,"name":"smoke-puff03-fg"},{"time":0.2667,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg4":{"rgba2":[{"time":0.1333,"light":"fae781ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.6,"light":"ab764cff","dark":"ac8d75"},{"time":0.9,"light":"ac8c7500","dark":"604a3f"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-fg"},{"time":0.1667,"name":"smoke-puff02-fg"},{"time":0.2,"name":"smoke-puff03-fg"},{"time":0.2667,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg5":{"rgba2":[{"time":0.1333,"light":"ffdf31ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.6,"light":"ab764cff","dark":"ac8d75"},{"time":0.9,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-fg"},{"time":0.1667,"name":"smoke-puff02-fg"},{"time":0.2,"name":"smoke-puff03-fg"},{"time":0.2667,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg6":{"rgba2":[{"time":0.1333,"light":"ffdf31ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":0.7,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-fg"},{"time":0.1667,"name":"smoke-puff02-fg"},{"time":0.2,"name":"smoke-puff03-fg"},{"time":0.2667,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg7":{"rgba2":[{"time":0.1333,"light":"ffdf31ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3333,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":0.8333,"light":"ac8c7500","dark":"604a3f"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-fg"},{"time":0.1667,"name":"smoke-puff02-fg"},{"time":0.2,"name":"smoke-puff03-fg"},{"time":0.2333,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg8":{"rgba2":[{"time":0.1333,"light":"ffdf31ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4333,"light":"ab764cff","dark":"ac8d75"},{"time":0.9333,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-fg"},{"time":0.1667,"name":"smoke-puff02-fg"},{"time":0.2,"name":"smoke-puff03-fg"},{"time":0.2333,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg9":{"rgba2":[{"time":0.1333,"light":"ffdf31ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.5333,"light":"ab764cff","dark":"ac8d75"},{"time":0.8333,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-fg"},{"time":0.1667,"name":"smoke-puff02-fg"},{"time":0.2,"name":"smoke-puff03-fg"},{"time":0.2333,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg10":{"rgba2":[{"time":0.1333,"light":"fce35dff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.5333,"light":"ab764cff","dark":"ac8d75"},{"time":0.8333,"light":"ac8c7500","dark":"604a3f"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-fg"},{"time":0.1667,"name":"smoke-puff02-fg"},{"time":0.2,"name":"smoke-puff03-fg"},{"time":0.2667,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg11":{"rgba2":[{"time":0.3333,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":0.7667,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.3333,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg12":{"rgba2":[{"time":0.3667,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.6,"light":"ab764cff","dark":"ac8d75"},{"time":0.8667,"light":"ac8c7500","dark":"604a3f"}],"attachment":[{"time":0.3667,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg13":{"rgba2":[{"time":0.3667,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":1,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.3667,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg14":{"rgba2":[{"time":0.4333,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.6,"light":"ab764cff","dark":"ac8d75"},{"time":1.0667,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.4333,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg15":{"rgba2":[{"time":0.4,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":0.8333,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.4,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg16":{"rgba2":[{"time":0.4,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":0.7,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.4,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg17":{"rgba2":[{"time":0.2333,"light":"e3c05eff","dark":"ab7e59"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4,"light":"ab764cff","dark":"ac8d75"},{"time":0.6667,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.2333,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg18":{"rgba2":[{"time":0.1333,"light":"ffdf31ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.5,"light":"ab764cff","dark":"ac8d75"},{"time":0.7667,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.2333,"name":"smoke-puff03-fg"},{"time":0.2667,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg20":{"rgba2":[{"time":0.1333,"light":"ffdf31ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":0.8,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.3333,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg21":{"rgba2":[{"time":0.1333,"light":"ffdf31ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":0.7,"light":"ab764c00","dark":"ac8d75"}]},"smoke-puff1-fg22":{"rgba2":[{"time":0.1333,"light":"ffdf31ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":0.7,"light":"ab764c00","dark":"ac8d75"}]},"smoke-puff1-fg23":{"rgba2":[{"time":0.1333,"light":"ffdf31ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.5,"light":"ab764cff","dark":"ac8d75"},{"time":0.7667,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.3,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg24":{"rgba2":[{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":0.7,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.3,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg25":{"rgba2":[{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":1,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.3667,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg26":{"rgba2":[{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.6,"light":"ab764cff","dark":"ac8d75"},{"time":0.9333,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.3667,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg27":{"rgba2":[{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":0.7333,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.3667,"name":"smoke-puff04-fg"}]},"tank-glow":{"rgba":[{"time":0.0667,"color":"fc994d84"},{"time":0.1333,"color":"f5b16bc8","curve":[0.221,0.96,0.252,0.98,0.221,0.69,0.252,0.62,0.221,0.42,0.252,0.33,0.221,0.78,0.252,0.32]},{"time":0.2667,"color":"fc994c30"}],"attachment":[{"time":0.0667,"name":"smoke-glow"},{"time":0.2667}]}},"bones":{"cannon":{"translate":[{"time":0.0667},{"time":0.1667,"x":34.77,"y":0.9},{"time":0.2667,"x":1.3}]},"tank-body":{"rotate":[{"time":0.0667},{"time":0.1667,"value":-4.29,"curve":[0.2,-4.29,0.267,2.37]},{"time":0.3,"value":2.37,"curve":[0.333,2.37,0.4,0]},{"time":0.4333}],"translate":[{"time":0.0667},{"time":0.1667,"x":31.04,"y":1.67,"curve":[0.2,31.04,0.267,-12.05,0.2,1.67,0.267,-0.23]},{"time":0.3,"x":-12.05,"y":-0.23},{"time":0.3667}]},"tank-treads":{"rotate":[{"time":0.0667},{"time":0.1667,"value":-3.08},{"time":0.3,"value":-0.42}]},"smoke1":{"rotate":[{"time":0.0667},{"time":0.1333,"value":2.88},{"time":0.1667,"value":2.34},{"time":0.2,"value":124.36},{"time":0.2667,"value":142.26},{"time":0.3333,"value":86.78},{"time":0.4667,"value":128.79},{"time":0.6333,"value":146.22},{"time":1.0333,"value":210.7}],"translate":[{"time":0.0667,"x":-9.69,"y":1.05},{"time":0.1333,"x":7.53,"y":1.21},{"time":0.1667,"x":3.26,"y":4.07},{"time":0.2,"x":29.64,"y":-17.46},{"time":0.2667,"x":86.97,"y":17.83},{"time":0.3333,"x":193.74,"y":-38.98},{"time":0.4,"x":341.67,"y":-39.52},{"time":0.6333,"x":393.24,"y":-4.01},{"time":1.0333,"x":410.76,"y":6.35}],"scale":[{"time":0.0667},{"time":0.1333,"x":3.171,"y":0.756},{"time":0.1667,"x":3.488,"y":1.279},{"time":0.2,"x":5.151,"y":2.369},{"time":0.2667,"x":4.735,"y":3.622},{"time":0.3,"x":4.735,"y":4.019},{"time":0.3333,"x":4.613,"y":3.339},{"time":0.3667,"x":4.918,"y":3.561},{"time":0.4,"x":4.6,"y":4.263},{"time":0.6333,"x":4.449,"y":2.62},{"time":1.0333,"x":3.09,"y":1.447}]},"smoke2":{"rotate":[{"time":0.1667,"value":31.55},{"time":0.3,"value":-22.63},{"time":0.4667,"value":142.89},{"time":0.6,"value":253.78},{"time":0.8333,"value":299.28}],"translate":[{"time":0.1667,"x":17.26,"y":4.86},{"time":0.2333,"x":141.22,"y":27.27},{"time":0.3,"x":178.86,"y":56.63},{"time":0.3667,"x":200.46,"y":71.05},{"time":0.4333,"x":213.12,"y":78.39},{"time":0.6333,"x":221.44,"y":73.1},{"time":0.8333,"x":223.32,"y":73.74}],"scale":[{"time":0.1667,"x":1.34,"y":1.34},{"time":0.2333,"x":2.81,"y":1.317},{"time":0.3,"x":2.932,"y":1.374},{"time":0.4667,"x":1.247,"y":0.639},{"time":0.8333,"x":0.778,"y":0.515}]},"smoke3":{"rotate":[{"time":0.1667,"value":-5.54},{"time":0.2333,"value":0.2},{"time":0.3333,"value":20.27},{"time":0.4,"value":31.36},{"time":0.4667,"value":68.52},{"time":0.5333,"value":99.74},{"time":0.6333,"value":145.8},{"time":0.8333,"value":193.28}],"translate":[{"time":0.1333,"x":1.17,"y":8.53},{"time":0.1667,"x":37.53,"y":4.84},{"time":0.2,"x":67.99,"y":9.85},{"time":0.2333,"x":134.14,"y":-13.5},{"time":0.2667,"x":181.31,"y":-19.93},{"time":0.3,"x":238.28,"y":-8.82},{"time":0.3333,"x":268.51,"y":-25.75},{"time":0.3667,"x":359.06,"y":-28.49},{"time":0.4,"x":432.96,"y":-24.11},{"time":0.4667,"x":452.16,"y":-16.73},{"time":0.6333,"x":456.28,"y":-0.41},{"time":0.8333,"x":454.14,"y":16.41}],"scale":[{"time":0.1333,"x":2.258,"y":1.366},{"time":0.1667,"x":2.656,"y":1.47},{"time":0.2,"x":3.202,"y":1.772},{"time":0.2333,"x":3.202,"y":1.93},{"time":0.2667,"x":3.124,"y":1.896},{"time":0.3,"x":3.593,"y":1.896},{"time":0.3333,"x":2.363,"y":1.247},{"time":0.3667,"x":1.845,"y":0.973},{"time":0.4,"x":1.754,"y":0.926},{"time":0.4333,"x":1.448,"y":0.695},{"time":0.4667,"x":1.441,"y":0.688},{"time":0.5333,"x":0.865,"y":0.456},{"time":0.7,"x":0.86,"y":0.454},{"time":0.8333,"x":0.211,"y":0.111}]},"smoke4":{"rotate":[{"time":0.1667,"value":-20.35},{"time":0.2333,"value":18.5},{"time":0.3,"value":57.77},{"time":0.4,"value":105.85},{"time":0.6,"value":161.28},{"time":0.9,"value":208.43}],"translate":[{"time":0.1667,"x":35.95,"y":25.54},{"time":0.2333,"x":34.17,"y":1.87},{"time":0.3,"x":136.7,"y":21.5},{"time":0.4,"x":138.61,"y":34.8},{"time":0.6,"x":160.38,"y":37.13},{"time":0.9,"x":196.41,"y":30.36}],"scale":[{"time":0.1667,"x":2.751,"y":1.754},{"time":0.2333,"x":3.486,"y":2.224},{"time":0.2667,"x":3.486,"y":2.586},{"time":0.3,"x":3.847,"y":2.109},{"time":0.4,"x":1.96,"y":1.074},{"time":0.9,"x":0.825,"y":0.452}]},"smoke5":{"rotate":[{"time":0.2,"value":23.09},{"time":0.2667,"value":12.24},{"time":0.3333,"value":36.92},{"time":0.4333,"value":-37.33},{"time":0.5333,"value":-0.66},{"time":0.9,"value":64.02}],"translate":[{"time":0.1333},{"time":0.2333,"x":123.76,"y":19.44},{"time":0.3,"x":239.08,"y":-49.72},{"time":0.3667,"x":280.23,"y":-51.46},{"time":0.7,"x":340.62,"y":-20.09},{"time":0.9,"x":349.18,"y":-5.25}],"scale":[{"time":0.1333},{"time":0.1667,"x":1.718,"y":1.718},{"time":0.2,"x":2.109,"y":2.109},{"time":0.2333,"x":1.781,"y":2.183},{"time":0.2667,"x":2.148,"y":2.633},{"time":0.3333,"x":2.234,"y":2.738},{"time":0.3667,"x":1.366,"y":2.148},{"time":0.4,"x":0.97,"y":1.524},{"time":0.4333,"x":1.078,"y":1.157},{"time":0.4667,"x":1.126,"y":1.005},{"time":0.7,"x":1.241,"y":1.301},{"time":0.9,"x":0.709,"y":0.893}]},"smoke6":{"rotate":[{"time":0.1667,"value":-37.43},{"time":0.2333,"value":-18.36},{"time":0.3333,"value":28.58},{"time":0.4,"value":150.54},{"time":0.7,"value":301.59}],"translate":[{"time":0.1333},{"time":0.2,"x":68.04,"y":16.15},{"time":0.2667,"x":214.52,"y":13.25},{"time":0.3333,"x":285.4,"y":17.95},{"time":0.4,"x":202.91,"y":101.43},{"time":0.4667,"x":189.25,"y":116.39},{"time":0.7,"x":182.77,"y":137.4}],"scale":[{"time":0.1333},{"time":0.1667,"x":1.152,"y":1.288},{"time":0.2,"x":1.939,"y":2.168},{"time":0.2333,"x":2.278,"y":2.223},{"time":0.2667,"x":2.023,"y":1.974},{"time":0.3,"x":2.644,"y":1.974},{"time":0.4,"x":1.539,"y":1.425},{"time":0.4667,"x":1.14,"y":0.939},{"time":0.7,"x":0.215,"y":0.161}]},"smoke7":{"rotate":[{"time":0.1667,"value":-243.11},{"time":0.4,"value":-182.02},{"time":0.8333,"value":-83.02}],"translate":[{"time":0.1333,"x":3.19,"y":-6.53},{"time":0.1667,"x":44.54,"y":1.12},{"time":0.2,"x":65.84,"y":6.02},{"time":0.2333,"x":173.84,"y":97.51},{"time":0.4,"x":167.39,"y":74.58},{"time":0.8333,"x":227.77,"y":84.64}],"scale":[{"time":0.1333,"x":0.878,"y":0.878},{"time":0.1667,"x":1.235,"y":1.235},{"time":0.2,"x":1.461,"y":1.461},{"time":0.2333,"x":1.114,"y":1.114},{"time":0.3333,"x":1.067,"y":1.067},{"time":0.4667,"x":0.81,"y":0.753},{"time":0.8333,"x":0.52,"y":0.484}]},"smoke8":{"rotate":[{"time":0.1667,"value":-156.52},{"time":0.2667,"value":-154.05},{"time":0.3333,"value":-108.35},{"time":0.6,"value":-93.14},{"time":0.9333,"value":-70.89}],"translate":[{"time":0.1667,"x":20.72,"y":0.25},{"time":0.2333,"x":46.1,"y":-10.06},{"time":0.3,"x":149.77,"y":0.92},{"time":0.3667,"x":241.21,"y":49.01},{"time":0.5333,"x":276,"y":58.76},{"time":0.7,"x":292.02,"y":65.91},{"time":0.9333,"x":308.7,"y":69.51}],"scale":[{"time":0.1333,"y":1.174},{"time":0.1667,"x":1.813,"y":1.438},{"time":0.2,"x":1.813,"y":1.878},{"time":0.2333,"x":1.211,"y":1.878},{"time":0.2667,"x":1.584,"y":1.596},{"time":0.3,"x":1.958,"y":1.878},{"time":0.4667,"x":1.139,"y":0.958},{"time":0.9333,"x":0.839,"y":0.591}]},"smoke9":{"rotate":[{"time":0.1333,"value":-44.34},{"time":0.1667,"value":14.73},{"time":0.2333,"value":116.07},{"time":0.2667,"value":118.29},{"time":0.3333,"value":148.13},{"time":0.3667,"value":172.74},{"time":0.4,"value":235.69},{"time":0.4333,"value":283.36},{"time":0.7667,"value":358.76}],"translate":[{"time":0.1333,"x":-3.49,"y":0.04},{"time":0.2,"x":87.4,"y":-7.97},{"time":0.2667,"x":233.69,"y":-33.86},{"time":0.3333,"x":296.44,"y":-30.87},{"time":0.4,"x":390.8,"y":4},{"time":0.4667,"x":391.42,"y":13.17},{"time":0.6333,"x":413.3,"y":36.13},{"time":0.7667,"x":408.59,"y":40.75}],"scale":[{"time":0.1333,"x":1.289,"y":1.501},{"time":0.2,"x":1.751,"y":2.039},{"time":0.2667,"x":2.064,"y":2.347},{"time":0.3333,"x":1.822,"y":2.072},{"time":0.4,"x":1.296,"y":1.045},{"time":0.4667,"x":1.872,"y":1.526},{"time":0.6333,"x":1.181,"y":1.037},{"time":0.7667,"x":0.716,"y":0.615}]},"smoke10":{"rotate":[{"time":0.1333,"value":12.16},{"time":0.2,"value":49.19},{"time":0.2667,"value":33.17},{"time":0.3333,"value":42.23},{"time":0.4,"value":11.69},{"time":0.4667,"value":41.83},{"time":0.5333,"value":54.86},{"time":0.6333,"value":75.25},{"time":0.8333,"value":126.4}],"translate":[{"time":0.1333,"x":7.74,"y":10.25},{"time":0.2,"x":42.9,"y":72.89},{"time":0.2667,"x":221.58,"y":82.27},{"time":0.3333,"x":297.31,"y":85.39},{"time":0.4,"x":322.91,"y":81.04},{"time":0.4667,"x":346.62,"y":76.68},{"time":0.6667,"x":377.46,"y":81.85},{"time":0.8333,"x":402.18,"y":101.03}],"scale":[{"time":0.1333,"x":0.537,"y":1.062},{"time":0.1667,"x":1.042,"y":0.841},{"time":0.2,"x":1.937,"y":1.563},{"time":0.2333,"x":1.937,"y":2.176},{"time":0.2667,"x":2.254,"y":2.532},{"time":0.3,"x":2.24,"y":2.516},{"time":0.5333,"x":1.731,"y":1.882},{"time":0.8333,"x":0.855,"y":0.867}]},"smoke-glow":{"translate":[{"time":0.0667,"x":-57.08,"y":0.01},{"time":0.1,"x":-49.68,"y":-1.46},{"time":0.1333,"x":6.3,"y":-2.92},{"time":0.1667,"x":31.57,"y":0.44},{"time":0.2,"x":34.04,"y":0.27},{"time":0.2333,"x":109.29,"y":1.02},{"time":0.4,"x":119.89,"y":1.01},{"time":0.4333,"x":135.2,"y":1.03},{"time":0.4667,"x":152.86,"y":1.06},{"time":0.5333,"x":164.64,"y":1.07},{"time":0.6,"x":179.94,"y":1.09},{"time":0.6333,"x":190.54,"y":1.1}],"scale":[{"time":0.0667,"x":0.233,"y":0.233},{"time":0.1,"x":0.42,"y":0.288},{"time":0.1333,"x":1.669,"y":1.072},{"time":0.1667,"x":1.669,"y":1.785,"curve":"stepped"},{"time":0.2,"x":1.669,"y":1.785},{"time":0.2333,"x":2.544,"y":1.785},{"time":0.4333,"x":3.48,"y":2.22},{"time":0.4667,"x":4.337,"y":2.655}]},"smoke11":{"rotate":[{"time":0.4,"value":47.07},{"time":0.4333,"value":109.71},{"time":0.4667,"value":164.62},{"time":0.8333,"value":276.93}],"translate":[{"time":0.3333,"x":280.31,"y":126.85},{"time":0.4,"x":296.27,"y":125.62},{"time":0.4667,"x":312.45,"y":131.57},{"time":0.6667,"x":310.5,"y":149.67},{"time":0.8333,"x":307.08,"y":153.94}],"scale":[{"time":0.3333,"x":1.491,"y":1.491},{"time":0.4667,"x":1.144,"y":0.948},{"time":0.5667,"x":0.491,"y":0.491},{"time":0.8333,"x":0.985,"y":0.91}]},"smoke12":{"rotate":[{"time":0.3667,"value":-37.96},{"time":0.4333,"value":28.55},{"time":0.5333,"value":108.53},{"time":0.8667,"value":191.85}],"translate":[{"time":0.3667,"x":390.22,"y":-1.06},{"time":0.4333,"x":411.78,"y":26.39},{"time":0.5333,"x":428.12,"y":56.28},{"time":0.8667,"x":444.34,"y":68.06}],"scale":[{"time":0.3667,"x":2.006,"y":1.821},{"time":0.5333,"x":1.719,"y":1.293},{"time":0.7333,"x":1.562,"y":1.304},{"time":0.8667,"x":0.727,"y":0.637}]},"smoke13":{"rotate":[{"time":0.3667,"value":305.8},{"time":0.4,"value":478.49},{"time":0.4333,"value":537.45},{"time":0.4667,"value":573.84},{"time":0.5333,"value":596.4},{"time":0.7,"value":622.3},{"time":1,"value":657.95}],"translate":[{"time":0.3667,"x":331.84,"y":-25.82},{"time":0.4,"x":417.88,"y":-42.62},{"time":0.4667,"x":451.61,"y":-42.21},{"time":0.5333,"x":453.81,"y":-37.03},{"time":0.6,"x":451.86,"y":-31.89},{"time":0.7,"x":453.37,"y":-27.28},{"time":1,"x":454.04,"y":-17.89}],"scale":[{"time":0.3667,"x":4.509,"y":3.114},{"time":0.4,"x":3.673,"y":2.537},{"time":0.4333,"x":4.201,"y":2.638},{"time":0.4667,"x":4.27,"y":2.399},{"time":0.6,"x":2.798,"y":1.932},{"time":0.8333,"x":2.316,"y":1.599},{"time":1,"x":1.081,"y":0.746}]},"smoke14":{"rotate":[{"time":0.4333,"value":271.03},{"time":0.7,"value":299.97},{"time":1.0667,"value":331.16}],"translate":[{"time":0.4333,"x":371.68,"y":-29.8},{"time":0.7667,"x":400.59,"y":-44.36},{"time":1.0667,"x":432.26,"y":-44.79}],"scale":[{"time":0.4333,"x":4.011,"y":3.366},{"time":0.7667,"x":2.071,"y":1.624},{"time":1.0667,"x":1.798,"y":1.111}]},"smoke15":{"rotate":[{"time":0.4,"value":111.75},{"time":0.4667,"value":171.93},{"time":0.6,"value":256.95},{"time":0.8333,"value":299.15}],"translate":[{"time":0.4,"x":266.71,"y":-53.04},{"time":0.4333,"x":290.84,"y":-51.43},{"time":0.5333,"x":305.65,"y":-44.32},{"time":0.6667,"x":318.96,"y":-38.95},{"time":0.8333,"x":342.65,"y":-27.33}],"scale":[{"time":0.4,"x":2.749,"y":2.095},{"time":0.4333,"x":3.302,"y":2.289},{"time":0.4667,"x":2.591,"y":1.895},{"time":0.5333,"x":1.777,"y":1.354},{"time":0.7,"x":1.932,"y":1.267},{"time":0.8333,"x":1.002,"y":1.546}]},"smoke16":{"rotate":[{"time":0.4,"value":89.78},{"time":0.4667,"value":137.83},{"time":0.5333,"value":193.49},{"time":0.6,"value":235.26},{"time":0.6333,"value":286.8}],"translate":[{"time":0.4,"x":217.23,"y":-21.45},{"time":0.4667,"x":249.95,"y":-13.73},{"time":0.5333,"x":264.96,"y":-9.87},{"time":0.6,"x":278.95,"y":6.37},{"time":0.6333,"x":245.65,"y":11.77}],"scale":[{"time":0.4,"x":2.265,"y":1.859},{"time":0.4333,"x":2.621,"y":1.955},{"time":0.4667,"x":1.953,"y":1.538},{"time":0.6,"x":1.005,"y":0.825},{"time":0.6333,"x":0.387,"y":0.318}]},"smoke17":{"rotate":[{"time":0.2333,"value":99.02},{"time":0.3,"value":58.06},{"time":0.3333,"value":34.05},{"time":0.3667,"value":-17.34},{"time":0.6667,"value":-62.36}],"translate":[{"time":0.2333,"x":18.91,"y":-62.91},{"time":0.3,"x":2.43,"y":-61.54},{"time":0.3333,"x":1.89,"y":-36.55},{"time":0.3667,"x":6.97,"y":-29.52},{"time":0.4333,"x":10.78,"y":-20.55},{"time":0.6667,"x":18.65,"y":-13.19}],"scale":[{"time":0.2333,"x":1.915,"y":1.915},{"time":0.3,"x":1.509,"y":1.509},{"time":0.3333,"x":1.01,"y":1.01},{"time":0.3667,"x":0.715,"y":0.715},{"time":0.4333,"x":0.949,"y":0.721},{"time":0.5667,"x":0.785,"y":0.74}]},"smoke18":{"rotate":[{"time":0.2333,"value":141.75},{"time":0.2667,"value":134.51},{"time":0.3333,"value":249.12},{"time":0.5,"value":363.82},{"time":0.7333,"value":450.54}],"translate":[{"time":0.2333,"x":60.81,"y":56.17},{"time":0.2667,"x":68.74,"y":69.4},{"time":0.3333,"x":76.85,"y":69.07},{"time":0.5,"x":101.49,"y":89.87},{"time":0.7333,"x":118.58,"y":101.16}],"scale":[{"time":0.2333,"x":2.288,"y":2.288},{"time":0.2667,"x":2.288,"y":1.628},{"time":0.3,"x":1.524,"y":1.308},{"time":0.5,"x":1.757,"y":1.385},{"time":0.5333,"x":2.08,"y":1.51},{"time":0.7333,"x":1.405,"y":0.896}]},"smoke20":{"rotate":[{"time":0.3333,"value":95.16},{"time":0.3667,"value":130.42},{"time":0.4,"value":170.7},{"time":0.4333,"value":266.75},{"time":0.4667,"value":299.82},{"time":0.5333,"value":326.88},{"time":0.6,"value":350.8},{"time":0.9,"value":403.14}],"translate":[{"time":0.3333,"x":124.61,"y":-46.55},{"time":0.5333,"x":173.8,"y":-36.62},{"time":0.7,"x":186.5,"y":-35.41},{"time":0.9,"x":188.56,"y":-37.75}],"scale":[{"time":0.3333,"x":3.346,"y":2.654},{"time":0.3667,"x":2.661,"y":2.111},{"time":0.4333,"x":2.751,"y":1.984},{"time":0.4667,"x":3.059,"y":2.21},{"time":0.5333,"x":2.159,"y":1.712},{"time":0.7,"x":1.601,"y":1.27},{"time":0.9,"x":1.679,"y":0.856}]},"smoke23":{"rotate":[{"time":0.3,"value":115.12},{"time":0.3667,"value":79.01},{"time":0.7667,"value":6.96}],"translate":[{"time":0.3,"x":75.15,"y":-50.92},{"time":0.3667,"x":59.33,"y":-53.52},{"time":0.7667,"x":39.68,"y":-48.64}],"scale":[{"time":0.3,"x":3.331,"y":2.096},{"time":0.4333,"x":2.4,"y":2.006},{"time":0.5,"x":2.555,"y":2.094},{"time":0.7667,"x":1.35,"y":1.241}]},"antenna1":{"rotate":[{"time":0.0667},{"time":0.2,"value":11.78},{"time":0.3,"value":-9.52},{"time":0.4,"value":8.07},{"time":0.5,"value":-4.45},{"time":0.6,"value":1.54},{"time":0.7,"value":-0.34}]},"antenna2":{"rotate":[{"time":0.0667},{"time":0.2,"value":11.78},{"time":0.3,"value":-9.52},{"time":0.4,"value":8.07},{"time":0.5,"value":-4.45},{"time":0.6,"value":1.54},{"time":0.7,"value":-0.34}]},"antenna3":{"rotate":[{"time":0.0667},{"time":0.2,"value":11.78},{"time":0.3,"value":-9.52},{"time":0.4,"value":8.07},{"time":0.5,"value":-4.45},{"time":0.6,"value":1.54},{"time":0.7,"value":-0.34}]},"antenna4":{"rotate":[{"time":0.0667},{"time":0.2,"value":11.78},{"time":0.3,"value":-9.52},{"time":0.4,"value":8.07},{"time":0.5,"value":-4.45},{"time":0.6,"value":1.54},{"time":0.7,"value":-0.34}]},"antenna5":{"rotate":[{"time":0.0667},{"time":0.2,"value":11.78},{"time":0.3,"value":-9.52},{"time":0.4,"value":8.07},{"time":0.5,"value":-4.45},{"time":0.6,"value":1.54},{"time":0.7,"value":-0.34}]},"antenna6":{"rotate":[{"time":0.0667},{"time":0.2,"value":11.78},{"time":0.3,"value":-9.52},{"time":0.4,"value":8.07},{"time":0.5,"value":-4.45},{"time":0.6,"value":1.54},{"time":0.7,"value":-0.34}]},"smoke24":{"rotate":[{"time":0.3,"value":71.32},{"time":0.3667,"value":112.39},{"time":0.4667,"value":159.56},{"time":0.7,"value":224.21}],"translate":[{"time":0.3,"x":90.72,"y":-18.79},{"time":0.3667,"x":149.69,"y":-7.78},{"time":0.4667,"x":176.26,"y":12.31},{"time":0.7,"x":184.07,"y":31.75}],"scale":[{"time":0.3,"x":2.906,"y":2.311},{"time":0.4333,"x":3.567,"y":2.58},{"time":0.4667,"x":3.157,"y":2.41},{"time":0.7,"x":1.705,"y":1.356}]},"smoke25":{"rotate":[{"time":0.3667,"value":91.25},{"time":0.4333,"value":117.56},{"time":0.6333,"value":150.9},{"time":1,"value":189.47}],"translate":[{"time":0.3667,"x":187.21,"y":-51.18},{"time":0.5333,"x":245.48,"y":-46.28},{"time":0.6667,"x":277.36,"y":-43.12},{"time":1,"x":313.27,"y":-38.14}],"scale":[{"time":0.3667,"x":3.606,"y":2.657},{"time":0.4333,"x":4.166,"y":2.792},{"time":0.5333,"x":3.09,"y":2.091},{"time":1,"x":3.062,"y":1.801}]},"smoke26":{"rotate":[{"time":0.3667,"value":10.64},{"time":0.4,"value":60.85},{"time":0.4667,"value":89.45},{"time":0.7,"value":125.01},{"time":0.9333,"value":155.24}],"translate":[{"time":0.3667,"x":442.07,"y":-13.19},{"time":0.4,"x":453.7,"y":0.81},{"time":0.4667,"x":443.57,"y":-6.95},{"time":0.7,"x":460.97,"y":15.79},{"time":0.9333,"x":465.22,"y":20.92}],"scale":[{"time":0.3667,"x":2.726,"y":2.726},{"time":0.4333,"x":3.729,"y":2.822},{"time":0.4667,"x":3.398,"y":2.441},{"time":0.7,"x":4.324,"y":3.159},{"time":0.9,"x":1.977,"y":1.48}]},"smoke27":{"rotate":[{"time":0.3667,"value":24.75},{"time":0.4333,"value":-5.43},{"time":0.5333,"value":-39.76},{"time":0.8333,"value":-56.25}],"translate":[{"time":0.3667,"x":92.98,"y":-49.06},{"time":0.5333,"x":129.81,"y":-33.09},{"time":0.8333,"x":143.68,"y":-25.27}],"scale":[{"time":0.3667,"x":3.633,"y":2.223},{"time":0.4333,"x":2.745,"y":2.283},{"time":0.4667,"x":2.962,"y":2.122},{"time":0.5333,"x":2.007,"y":1.266}]},"cannon-target":{"translate":[{"time":0.1333},{"time":0.2,"y":128.38,"curve":[0.4,0,0.8,0,0.4,128.38,0.8,0]},{"time":1}],"scale":[{"time":0.4333,"x":0.632,"y":1.244},{"time":0.4667,"x":0.477,"y":1.487}]},"machinegun-target":{"scale":[{"time":0.4333,"x":0.632,"y":1.244},{"time":0.4667,"x":0.477,"y":1.487}]},"machinegun":{"rotate":[{"value":8.07,"curve":"stepped"},{"time":0.0667,"value":8.07},{"time":0.2333,"value":-18.67,"curve":[0.894,-18.44,0.832,7.5]},{"time":0.9,"value":8.07}]},"tank-root":{"translate":[{"time":0.0667},{"time":0.1667,"x":46.59,"curve":[0.192,46.59,0.242,0,0.192,0,0.242,0]},{"time":0.2667}]},"tank-glow":{"translate":[{"time":0.1333,"x":198.14,"curve":[0.199,190.76,0.222,-255.89,0.199,0,0.222,0]},{"time":0.2333,"x":-390}],"scale":[{"time":0.0667},{"time":0.1333,"x":1.185,"y":0.945,"curve":[0.199,1.182,0.222,1.048,0.199,0.939,0.222,0.579]},{"time":0.2333,"x":1.008,"y":0.471}]}},"attachments":{"default":{"clipping":{"clipping":{"deform":[{"time":0.0667,"offset":54,"vertices":[4.59198,-4.59192]},{"time":0.1333,"offset":8,"vertices":[-8.97369,-1.88211,9.11177,1.02258,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-14.73321,-45.16878,-30.31448,-84.4631,-32.24969,-108.78421,70.26825,-36.90201]},{"time":0.1667,"offset":8,"vertices":[-11.32373,-1.65065,11.42179,0.53259,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-15.36503,-69.18713,-4.45626,-121.90839,5.46554,-115.23274,71.78526,-33.85687]},{"time":0.2,"offset":8,"vertices":[-8.70522,1.02196,8.65102,-1.4101,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4.59198,-4.59192]},{"time":0.2333,"offset":8,"vertices":[-5.23146,0.85796,5.23882,-0.81519]},{"time":0.2667,"offset":54,"vertices":[4.59198,-4.59192]}]}},"smoke-glow":{"smoke-glow":{"deform":[{"time":0.1333,"vertices":[-14.17073,19.14352,0,0,-10.97961,-15.09065,-5.79558,-24.82121,0.68117,-17.78759,-1.1179,-5.4463,0,0,0,0,17.52957,6.89397,-0.33841,-2.21582,5.51004,18.88118,-6.80153,20.91101]},{"time":0.1667,"vertices":[-4.34264,39.78125,5.6649,-2.42686,-8.39346,-22.52338,-2.66431,5.08595,-19.28093,3.98568,-11.21397,10.2879,4.56749,4.1329,-19.50706,-2.28786,11.35747,4.55941,9.04341,-11.72194,2.15381,5.14344,-12.82158,16.08209,-23.19814,1.81836]},{"time":0.2,"vertices":[-3.95581,36.12203,37.20779,-0.87419,21.29579,-15.76854,-2.02438,6.16526,-5.92201,4.19709,-1.39027,9.92793,7.70584,-0.7169,-6.69733,-2.62048,17.91826,7.77333,-12.2858,3.25454,-12.75876,3.71516,9.67891,15.48546]},{"time":0.2333,"vertices":[-11.9371,26.01078,2.91821,-0.27533,7.69899,-17.45375,-2.02438,6.16526,-5.92201,4.19709,-1.39027,9.92793,7.70584,-0.7169,-6.69733,-2.62048,17.91826,7.77333,-4.30551,-6.01406,-12.75876,3.71516,-5.10017,17.59191]},{"time":0.2667,"vertices":[0.5959,23.58176,20.74303,0.93943,7.69899,-17.45375,-2.02438,6.16526,-5.92201,4.19709,-1.39027,9.92793,20.51733,2.52203,13.35544,2.64274,24.32408,-1.94308,8.50604,-20.99353,13.14276,5.73959,6.31876,19.2114,16.98909,0.80981]}]}}}},"drawOrder":[{"time":0.3,"offsets":[{"slot":"smoke-puff1-bg2","offset":24},{"slot":"smoke-puff1-bg8","offset":19},{"slot":"smoke-puff1-bg9","offset":22},{"slot":"smoke-puff1-bg3","offset":17},{"slot":"smoke-puff1-fg17","offset":13},{"slot":"smoke-puff1-fg2","offset":2},{"slot":"smoke-puff1-fg5","offset":8},{"slot":"smoke-puff1-fg6","offset":4},{"slot":"smoke-puff1-fg7","offset":-4},{"slot":"smoke-puff1-fg4","offset":-4}]},{"time":0.3333,"offsets":[{"slot":"smoke-puff1-bg2","offset":8},{"slot":"smoke-puff1-bg8","offset":5},{"slot":"smoke-puff1-bg9","offset":3},{"slot":"smoke-puff1-fg","offset":24},{"slot":"smoke-puff1-fg5","offset":-14},{"slot":"smoke-puff1-fg6","offset":-21},{"slot":"smoke-puff1-fg7","offset":-19},{"slot":"smoke-puff1-fg10","offset":-21}]},{"time":0.3667,"offsets":[{"slot":"smoke-puff1-bg2","offset":7},{"slot":"smoke-puff1-bg9","offset":4},{"slot":"smoke-puff1-fg","offset":24},{"slot":"smoke-puff1-fg2","offset":5},{"slot":"smoke-puff1-fg6","offset":-22},{"slot":"smoke-puff1-fg7","offset":-18},{"slot":"smoke-puff1-fg10","offset":-20}]},{"time":0.4,"offsets":[{"slot":"smoke-puff1-bg2","offset":5},{"slot":"smoke-puff1-bg4","offset":0},{"slot":"smoke-puff1-fg","offset":24},{"slot":"smoke-puff1-fg2","offset":5},{"slot":"smoke-puff1-fg6","offset":-21},{"slot":"smoke-puff1-fg7","offset":-18},{"slot":"smoke-puff1-fg10","offset":-22}]},{"time":0.4333,"offsets":[{"slot":"smoke-puff1-bg2","offset":4},{"slot":"smoke-puff1-bg9","offset":4},{"slot":"smoke-puff1-fg","offset":24},{"slot":"smoke-puff1-fg2","offset":5},{"slot":"smoke-puff1-fg6","offset":-17},{"slot":"smoke-puff1-fg7","offset":-19},{"slot":"smoke-puff1-fg10","offset":-23}]},{"time":0.5333,"offsets":[{"slot":"smoke-puff1-bg2","offset":9},{"slot":"smoke-puff1-bg12","offset":0},{"slot":"smoke-puff1-fg","offset":24},{"slot":"smoke-puff1-fg2","offset":6},{"slot":"smoke-puff1-fg6","offset":-20},{"slot":"smoke-puff1-fg7","offset":-19},{"slot":"smoke-puff1-fg10","offset":-23},{"slot":"smoke-puff1-fg4","offset":-5}]}]}}} \ No newline at end of file diff --git a/e2e/.dev/public/tank-pro.png b/e2e/.dev/public/tank-pro.png new file mode 100644 index 0000000000..badc613ca2 Binary files /dev/null and b/e2e/.dev/public/tank-pro.png differ diff --git a/e2e/.dev/vite.config.js b/e2e/.dev/vite.config.js index 2cfc11d824..498a9a0244 100644 --- a/e2e/.dev/vite.config.js +++ b/e2e/.dev/vite.config.js @@ -42,6 +42,7 @@ demoList.forEach(({ file }) => { fs.outputJSONSync(path.join(__dirname, OUT_PATH, ".demoList.json"), demoSorted); module.exports = { + publicDir: path.resolve(__dirname, "public"), server: { open: true, host: "0.0.0.0", diff --git a/e2e/case/animator-blendShape.ts b/e2e/case/animator-blendShape.ts index 28641ca91f..27307ec95d 100644 --- a/e2e/case/animator-blendShape.ts +++ b/e2e/case/animator-blendShape.ts @@ -38,7 +38,7 @@ WebGLEngine.create({ canvas: "canvas" }).then((engine) => { const { defaultSceneRoot } = asset; rootEntity.addChild(defaultSceneRoot); const animator = defaultSceneRoot.getComponent(Animator); - const skinMeshRenderer = defaultSceneRoot.getComponent(SkinnedMeshRenderer); + const skinMeshRenderer = defaultSceneRoot.getComponentsIncludeChildren(SkinnedMeshRenderer, [])[0]; skinMeshRenderer.blendShapeWeights[0] = 1.0; animator.play("TheWave"); diff --git a/e2e/case/animator-multiSubMeshBlendShape.ts b/e2e/case/animator-multiSubMeshBlendShape.ts index 546d9b8f3d..5c1365a758 100644 --- a/e2e/case/animator-multiSubMeshBlendShape.ts +++ b/e2e/case/animator-multiSubMeshBlendShape.ts @@ -45,8 +45,8 @@ WebGLEngine.create({ canvas: "canvas" }).then((engine) => { .then((asset) => { const { defaultSceneRoot } = asset; rootEntity.addChild(defaultSceneRoot); - const entity = defaultSceneRoot; - defaultSceneRoot.transform.rotation = new Vector3(-90, -0, 0); + const entity = defaultSceneRoot.children[0]; + entity.transform.rotation = new Vector3(-90, -0, 0); const animator = entity.addComponent(Animator); animator.animatorController = new AnimatorController(engine); diff --git a/e2e/case/gpu-instancing-auto-batch.ts b/e2e/case/gpu-instancing-auto-batch.ts new file mode 100644 index 0000000000..e9f80e35ab --- /dev/null +++ b/e2e/case/gpu-instancing-auto-batch.ts @@ -0,0 +1,68 @@ +/** + * @title GPU Instancing Auto Batch + * @category Mesh + */ +import { + AmbientLight, + AssetType, + Camera, + Color, + DirectLight, + GLTFResource, + Logger, + Vector3, + WebGLEngine +} from "@galacean/engine"; +import { initScreenshot, updateForE2E } from "./.mockForE2E"; + +Logger.enable(); +WebGLEngine.create({ canvas: "canvas" }).then(async (engine) => { + engine.canvas.resizeByClientSize(2); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity("Root"); + + // Camera + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 10, 80); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + const camera = cameraEntity.addComponent(Camera); + camera.farClipPlane = 300; + + // Light + const lightEntity = rootEntity.createChild("Light"); + lightEntity.transform.setRotation(-45, -45, 0); + lightEntity.addComponent(DirectLight).color = new Color(1, 1, 1, 1); + + // Load Duck model and ambient light + const [glTF, ambientLight] = await Promise.all([ + engine.resourceManager.load({ + url: "https://gw.alipayobjects.com/os/bmw-prod/6cb8f543-285c-491a-8cfd-57a1160dc9ab.glb", + type: AssetType.GLTF + }), + engine.resourceManager.load({ + url: "https://mdn.alipayobjects.com/oasis_be/afts/file/A*eRJ8QKzf5zAAAAAAgBAAAAgAekp5AQ/ambient.ambLight", + type: AssetType.AmbientLight + }) + ]); + scene.ambientLight = ambientLight; + + // Clone ducks with fixed seed positions for deterministic output + const count = 500; + const spread = 50; + for (let i = 0; i < count; i++) { + const duck = glTF.instantiateSceneRoot(); + // Use deterministic positions based on index + const t = i / count; + duck.transform.setPosition( + Math.sin(t * 137.5) * spread * 0.5, + Math.cos(t * 97.3) * spread * 0.5, + Math.sin(t * 59.1) * spread * 0.5 + ); + duck.transform.setRotation(t * 360, t * 720, t * 1080); + rootEntity.addChild(duck); + } + + updateForE2E(engine); + initScreenshot(engine, camera); +}); diff --git a/e2e/case/gpu-instancing-custom-data.ts b/e2e/case/gpu-instancing-custom-data.ts new file mode 100644 index 0000000000..f64b01a8f7 --- /dev/null +++ b/e2e/case/gpu-instancing-custom-data.ts @@ -0,0 +1,100 @@ +/** + * @title GPU Instancing Custom Data + * @category Mesh + */ +import { + Camera, + Color, + DirectLight, + Logger, + Material, + MeshRenderer, + PrimitiveMesh, + Shader, + ShaderProperty, + Vector3, + Vector4, + WebGLEngine +} from "@galacean/engine"; +import { initScreenshot, updateForE2E } from "./.mockForE2E"; + +Logger.enable(); + +// Custom shader: uses renderer_CustomColor (per-instance) for fragment output +Shader.create( + "CustomInstanceShader", + ` + #include + attribute vec3 POSITION; + attribute vec3 NORMAL; + + varying vec3 v_normal; + + void main() { + gl_Position = renderer_MVPMat * vec4(POSITION, 1.0); + v_normal = normalize((renderer_NormalMat * vec4(NORMAL, 0.0)).xyz); + } + `, + ` + uniform vec4 renderer_CustomColor; + + varying vec3 v_normal; + + void main() { + vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0)); + float NdotL = max(dot(v_normal, lightDir), 0.2); + gl_FragColor = vec4(renderer_CustomColor.rgb * NdotL, 1.0); + } + ` +); + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(2); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity("Root"); + + // Camera + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 10, 80); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + const camera = cameraEntity.addComponent(Camera); + camera.farClipPlane = 300; + + // Light + const lightEntity = rootEntity.createChild("Light"); + lightEntity.transform.setRotation(-45, -45, 0); + lightEntity.addComponent(DirectLight).color = new Color(1, 1, 1, 1); + + // Shared mesh and material + const mesh = PrimitiveMesh.createCuboid(engine, 1, 1, 1); + const material = new Material(engine, Shader.find("CustomInstanceShader")); + const customColorProperty = ShaderProperty.getByName("renderer_CustomColor"); + + // Create cubes with deterministic positions and colors + const count = 500; + const spread = 50; + for (let i = 0; i < count; i++) { + const entity = rootEntity.createChild("Cube" + i); + const t = i / count; + entity.transform.setPosition( + Math.sin(t * 137.5) * spread * 0.5, + Math.cos(t * 97.3) * spread * 0.5, + Math.sin(t * 59.1) * spread * 0.5 + ); + entity.transform.setRotation(t * 360, t * 720, t * 1080); + + const renderer = entity.addComponent(MeshRenderer); + renderer.mesh = mesh; + renderer.setMaterial(material); + + // Deterministic colors based on index + renderer.shaderData.setVector4( + customColorProperty, + new Vector4(Math.sin(t * 6.28) * 0.5 + 0.5, Math.cos(t * 4.71) * 0.5 + 0.5, Math.sin(t * 3.14) * 0.5 + 0.5, 1.0) + ); + } + + updateForE2E(engine); + initScreenshot(engine, camera); +}); diff --git a/e2e/case/particleRenderer-dream.ts b/e2e/case/particleRenderer-dream.ts index e515bd4fbf..6eb51637ed 100644 --- a/e2e/case/particleRenderer-dream.ts +++ b/e2e/case/particleRenderer-dream.ts @@ -68,8 +68,8 @@ WebGLEngine.create({ cameraEntity.addChild(fireEntity); - updateForE2E(engine, 500); - initScreenshot(engine, camera); + // updateForE2E(engine, 500); + // initScreenshot(engine, camera); }); }); diff --git a/e2e/case/shaderLab-mrt.ts b/e2e/case/shaderLab-mrt.ts index 54afaf6494..a8f3a2c7cd 100644 --- a/e2e/case/shaderLab-mrt.ts +++ b/e2e/case/shaderLab-mrt.ts @@ -9,7 +9,7 @@ import { initScreenshot, updateForE2E } from "./.mockForE2E"; const shaderLab = new ShaderLab(); -const shaderSource = `Shader "/custom.gs" { +const shaderSource = `Shader "/custom.shader" { SubShader "Default" { UsePass "pbr/Default/ShadowCaster" diff --git a/e2e/case/spine-spineboy.ts b/e2e/case/spine-spineboy.ts new file mode 100644 index 0000000000..4ccfdc21bf --- /dev/null +++ b/e2e/case/spine-spineboy.ts @@ -0,0 +1,36 @@ +/** + * @title Spine Spineboy + * @category Spine + */ +import { Camera, Logger, Vector3, WebGLEngine } from "@galacean/engine"; +import { SpineAnimationRenderer } from "@galacean/engine-spine"; +import "@galacean/engine-spine-core-4.2"; +import { initScreenshot, updateForE2E } from "./.mockForE2E"; + +Logger.enable(); + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const root = scene.createRootEntity(); + + const cameraEntity = root.createChild("camera"); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(0, 0, 20); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + + engine.resourceManager + .load({ + urls: ["/spineboy.json", "/spineboy.atlas", "/spineboy.png"], + type: "Spine" + }) + .then((resource: any) => { + const spineEntity = resource.instantiate(); + spineEntity.transform.setPosition(-0.5, -3.2, 0); + root.addChild(spineEntity); + spineEntity.getComponent(SpineAnimationRenderer).state.setAnimation(0, "idle", true); + + updateForE2E(engine); + initScreenshot(engine, camera); + }); +}); diff --git a/e2e/case/spine-tint-black.ts b/e2e/case/spine-tint-black.ts new file mode 100644 index 0000000000..b2c6c91504 --- /dev/null +++ b/e2e/case/spine-tint-black.ts @@ -0,0 +1,41 @@ +/** + * @title Spine TintBlack + * @category Spine + */ +import { Camera, Logger, Vector3, WebGLEngine } from "@galacean/engine"; +import { SpineAnimationRenderer } from "@galacean/engine-spine"; +import "@galacean/engine-spine-core-4.2"; +import { initScreenshot, updateForE2E } from "./.mockForE2E"; + +Logger.enable(); + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const root = scene.createRootEntity(); + + const cameraEntity = root.createChild("camera"); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(0, 0, 60); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + + engine.resourceManager + .load({ + urls: ["/tank-pro.json", "/tank-pro.atlas", "/tank-pro.png"], + type: "Spine" + }) + .then((resource: any) => { + const spineEntity = resource.instantiate(); + const spine = spineEntity.getComponent(SpineAnimationRenderer); + // tank-pro ships with per-slot darkColor; tintBlack renders the two-color tint. + spine.tintBlack = true; + spineEntity.transform.setPosition(3, -5, 0); + root.addChild(spineEntity); + spine.state.setAnimation(0, "shoot", true); + + // Sample ~0.4s into "shoot": the muzzle-smoke darkColor makes the two-color tint + // clearly visible (tintBlack on/off differs ~7% here, vs ~0.01% on a static frame). + updateForE2E(engine, 40, 10); + initScreenshot(engine, camera); + }); +}); diff --git a/e2e/config.ts b/e2e/config.ts index e5a9b621cf..d0142dcc29 100644 --- a/e2e/config.ts +++ b/e2e/config.ts @@ -1,4 +1,18 @@ export const E2E_CONFIG = { + Spine: { + spineboy: { + category: "Spine", + caseFileName: "spine-spineboy", + threshold: 0, + diffPercentage: 0.05 + }, + tintBlack: { + category: "Spine", + caseFileName: "spine-tint-black", + threshold: 0, + diffPercentage: 0.05 + } + }, Animator: { additive: { category: "Animator", diff --git a/e2e/fixtures/originImage/Animator_animator-crossfade.jpg b/e2e/fixtures/originImage/Animator_animator-crossfade.jpg index ab638de414..c71cd431c8 100644 --- a/e2e/fixtures/originImage/Animator_animator-crossfade.jpg +++ b/e2e/fixtures/originImage/Animator_animator-crossfade.jpg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e18021c1e0a3b682259046e812eba199c907ffd9a634dda12379f1f0c3b8b43a -size 45710 +oid sha256:a1dfa2435f26222617b08ce2830ca28d8d84e083678dde1a9d945479c7b6d019 +size 45716 diff --git a/e2e/fixtures/originImage/Animator_animator-play.jpg b/e2e/fixtures/originImage/Animator_animator-play.jpg index 79170b806b..ddfb02c4e7 100644 --- a/e2e/fixtures/originImage/Animator_animator-play.jpg +++ b/e2e/fixtures/originImage/Animator_animator-play.jpg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4910985a371b9022e73d2a7efbd71ca6754bcc7bf4c25edc2a41391f1bb1533b -size 46499 +oid sha256:82d8ba4776b6b27b923d0f1f0094d48d0d4c59e61e8666275690fec70c39e854 +size 46527 diff --git a/e2e/fixtures/originImage/Camera_camera-opaque-texture.jpg b/e2e/fixtures/originImage/Camera_camera-opaque-texture.jpg index bcc2d5f331..1d409ae544 100644 --- a/e2e/fixtures/originImage/Camera_camera-opaque-texture.jpg +++ b/e2e/fixtures/originImage/Camera_camera-opaque-texture.jpg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fdde03beb8ead802b61a14e8649102c18ccfe6b9e1e176d3b9f8bc0dfe7f9c76 -size 47500 +oid sha256:a2068be8d0b35c94ec1ddaed9da63494f1a93294ea6128e2036563df1947e854 +size 47482 diff --git a/e2e/fixtures/originImage/GPUInstancing_gpu-instancing-auto-batch.jpg b/e2e/fixtures/originImage/GPUInstancing_gpu-instancing-auto-batch.jpg new file mode 100644 index 0000000000..ecec018d8c --- /dev/null +++ b/e2e/fixtures/originImage/GPUInstancing_gpu-instancing-auto-batch.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc2a26d0ad06c2dfa722484b2e914b43c4c6671c76ec31d18715dc4a89ec1d88 +size 590047 diff --git a/e2e/fixtures/originImage/GPUInstancing_gpu-instancing-custom-data.jpg b/e2e/fixtures/originImage/GPUInstancing_gpu-instancing-custom-data.jpg new file mode 100644 index 0000000000..79399c097c --- /dev/null +++ b/e2e/fixtures/originImage/GPUInstancing_gpu-instancing-custom-data.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:baece2c4437803e1474699eb4e99214b38caa05393f747fe0f7e47bd6469a693 +size 427005 diff --git a/e2e/fixtures/originImage/Physics_physx-collision.jpg b/e2e/fixtures/originImage/Physics_physx-collision.jpg deleted file mode 100644 index b1e9becba9..0000000000 --- a/e2e/fixtures/originImage/Physics_physx-collision.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:55cd5d3035ef575743d7332317b571ea2403f9330c918a3c45e96dbbe853e164 -size 39366 diff --git a/e2e/fixtures/originImage/Physics_physx-customUrl.jpg b/e2e/fixtures/originImage/Physics_physx-customUrl.jpg deleted file mode 100644 index b1e9becba9..0000000000 --- a/e2e/fixtures/originImage/Physics_physx-customUrl.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:55cd5d3035ef575743d7332317b571ea2403f9330c918a3c45e96dbbe853e164 -size 39366 diff --git a/e2e/fixtures/originImage/Physics_physx-mesh-collider-data.jpg b/e2e/fixtures/originImage/Physics_physx-mesh-collider-data.jpg deleted file mode 100644 index 6bdbf7360d..0000000000 --- a/e2e/fixtures/originImage/Physics_physx-mesh-collider-data.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e62ca32193b97c4a47ce59666a4f7754f0f20486e055ac3998de2e07aa4d1272 -size 128943 diff --git a/e2e/fixtures/originImage/Spine_spine-spineboy.jpg b/e2e/fixtures/originImage/Spine_spine-spineboy.jpg new file mode 100644 index 0000000000..76065431eb --- /dev/null +++ b/e2e/fixtures/originImage/Spine_spine-spineboy.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58b5f1a156b82707ead037eb64d0b130534834c146d907b45be9b0cb16dbed49 +size 74591 diff --git a/e2e/fixtures/originImage/Spine_spine-tint-black.jpg b/e2e/fixtures/originImage/Spine_spine-tint-black.jpg new file mode 100644 index 0000000000..410a970ccf --- /dev/null +++ b/e2e/fixtures/originImage/Spine_spine-tint-black.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:51fc0948e2ce8b819093526b5665b10e9975a810d4c1975fa661c091f2842e81 +size 122845 diff --git a/e2e/package.json b/e2e/package.json index 34a2f56922..fb743e744a 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,7 +1,7 @@ { "name": "@galacean/engine-e2e", "private": true, - "version": "2.0.0-alpha.24", + "version": "0.0.0-experimental-2.0-game.19", "license": "MIT", "scripts": { "case": "vite serve .dev --config .dev/vite.config.js", @@ -20,6 +20,8 @@ "@galacean/engine-rhi-webgl": "workspace:*", "@galacean/engine-physics-lite": "workspace:*", "@galacean/engine-physics-physx": "workspace:*", + "@galacean/engine-spine": "workspace:*", + "@galacean/engine-spine-core-4.2": "workspace:*", "dat.gui": "^0.7.9", "vite": "3.1.6", "sass": "^1.55.0" diff --git a/examples/package.json b/examples/package.json index a57e6c4187..d0847d14a4 100644 --- a/examples/package.json +++ b/examples/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-examples", - "version": "2.0.0-alpha.24", + "version": "0.0.0-experimental-2.0-game.19", "private": true, "license": "MIT", "main": "dist/main.js", @@ -21,11 +21,15 @@ "@galacean/engine-rhi-webgl": "workspace:*", "@galacean/engine-shader": "workspace:*", "@galacean/engine-shaderlab": "workspace:*", + "@galacean/engine-spine": "workspace:*", + "@galacean/engine-spine-core-3.8": "workspace:*", + "@galacean/engine-spine-core-4.2": "workspace:*", "@galacean/engine-toolkit": "latest", + "@galacean/engine-toolkit-stats": "latest", "@galacean/engine-ui": "workspace:*" }, "devDependencies": { "dat.gui": "^0.7.9", "vite": "^4.4.4" } -} \ No newline at end of file +} diff --git a/examples/src/gpu-instancing-auto-batch.ts b/examples/src/gpu-instancing-auto-batch.ts new file mode 100644 index 0000000000..d7aa1b9f80 --- /dev/null +++ b/examples/src/gpu-instancing-auto-batch.ts @@ -0,0 +1,194 @@ +/** + * @title GPU Instancing Auto Batch + * @category Mesh + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*jjZMTrp-vU8AAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl, Stats } from "@galacean/engine-toolkit"; +import { + AmbientLight, + AssetType, + Camera, + Color, + DirectLight, + GLTFResource, + Logger, + Material, + MeshRenderer, + PrimitiveMesh, + Script, + Shader, + ShaderProperty, + Vector3, + Vector4, + WebGLEngine, + WebGLMode +} from "@galacean/engine"; + +const _customColorProperty = ShaderProperty.getByName("renderer_CustomColor"); + +class SpiralAnimate extends Script { + radius: number = 0; + radiusSpeed: number = 0; + theta: number = 0; + thetaSpeed: number = 0; + phi: number = 0; + phiSpeed: number = 0; + rotateSpeed: Vector3 = new Vector3(); + scaleBase: number = 1; + scaleFreq: number = 0; + colorPhase: number = 0; + colorSpeed: number = 0; + private _time: number = 0; + private _color: Vector4 = new Vector4(); + private _hasColor: boolean = false; + + onUpdate(deltaTime: number): void { + this._time += deltaTime; + const t = this._time; + const transform = this.entity.transform; + + // Spiral breathing motion + const r = this.radius * (0.6 + 0.4 * Math.sin(t * this.radiusSpeed)); + const theta = this.theta + t * this.thetaSpeed; + const phi = this.phi + t * this.phiSpeed; + + const sinTheta = Math.sin(theta); + transform.setPosition(r * sinTheta * Math.cos(phi), r * Math.cos(theta), r * sinTheta * Math.sin(phi)); + + // Rotation + const { rotateSpeed } = this; + transform.rotate(rotateSpeed.x * deltaTime, rotateSpeed.y * deltaTime, rotateSpeed.z * deltaTime); + + // Scale pulse + const s = this.scaleBase * (0.7 + 0.3 * Math.sin(t * this.scaleFreq)); + transform.setScale(s, s, s); + + // Color animation (cubes only) + if (this._hasColor) { + const ct = t * this.colorSpeed + this.colorPhase; + this._color.set( + 0.5 + 0.5 * Math.sin(ct), + 0.5 + 0.5 * Math.sin(ct + 2.094), + 0.5 + 0.5 * Math.sin(ct + 4.189), + 1.0 + ); + this.entity.getComponent(MeshRenderer).shaderData.setVector4(_customColorProperty, this._color); + } + } + + enableColor(): void { + this._hasColor = true; + } +} + +// Custom shader for cubes +Shader.create( + "CustomInstanceShader", + ` + #include + attribute vec3 POSITION; + attribute vec3 NORMAL; + + varying vec3 v_normal; + + void main() { + gl_Position = renderer_MVPMat * vec4(POSITION, 1.0); + v_normal = normalize((renderer_NormalMat * vec4(NORMAL, 0.0)).xyz); + } + `, + ` + uniform vec4 renderer_CustomColor; + + varying vec3 v_normal; + + void main() { + vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0)); + float NdotL = max(dot(v_normal, lightDir), 0.2); + gl_FragColor = vec4(renderer_CustomColor.rgb * NdotL, 1.0); + } + ` +); + +WebGLEngine.create({ canvas: "canvas", graphicDeviceOptions: { webGLMode: WebGLMode.WebGL2 } }).then(async (engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity("Root"); + + // Camera + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 100); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + const camera = cameraEntity.addComponent(Camera); + camera.farClipPlane = 500; + cameraEntity.addComponent(OrbitControl); + + // Stats + cameraEntity.addComponent(Stats); + + // Light + const lightEntity = rootEntity.createChild("Light"); + lightEntity.transform.setRotation(-45, -45, 0); + lightEntity.addComponent(DirectLight).color = new Color(1, 1, 1, 1); + + // Load Duck model and ambient light + const [glTF, ambientLight] = await Promise.all([ + engine.resourceManager.load({ + url: "https://mdn.alipayobjects.com/rms/afts/file/A*9R-_TY9K_6oAAAAAgIAAAAgAehQnAQ/Avocado.glb", + type: AssetType.GLTF + }), + engine.resourceManager.load({ + url: "https://mdn.alipayobjects.com/oasis_be/afts/file/A*eRJ8QKzf5zAAAAAAgBAAAAgAekp5AQ/ambient.ambLight", + type: AssetType.AmbientLight + }) + ]); + scene.ambientLight = ambientLight; + + // Cube resources + const cubeMesh = PrimitiveMesh.createCuboid(engine, 1, 1, 1); + const cubeMaterial = new Material(engine, Shader.find("CustomInstanceShader")); + const customColorProperty = ShaderProperty.getByName("renderer_CustomColor"); + + // Interleave ducks and cubes to break batching — instancing shines here + const duckCount = 2500; + const cubeCount = 2500; + const totalCount = duckCount + cubeCount; + + for (let i = 0; i < totalCount; i++) { + const ti = i / totalCount; + const isDuck = i % 2 === 0 && i / 2 < duckCount; + const isCube = !isDuck; + + let entity; + if (isDuck) { + entity = glTF.instantiateSceneRoot(); + rootEntity.addChild(entity); + } else { + entity = rootEntity.createChild("Cube" + i); + const renderer = entity.addComponent(MeshRenderer); + renderer.mesh = cubeMesh; + renderer.setMaterial(cubeMaterial); + const initColor = new Vector4(Math.random(), Math.random(), Math.random(), 1.0); + renderer.shaderData.setVector4(customColorProperty, initColor); + } + + const anim = entity.addComponent(SpiralAnimate); + anim.radius = 10 + Math.random() * 40; + anim.radiusSpeed = 0.3 + Math.random() * 0.6; + anim.theta = ti * Math.PI * 2 * 13.7; + anim.phi = ti * Math.PI * 2 * 7.3; + anim.thetaSpeed = (0.2 + Math.random() * 0.4) * (Math.random() > 0.5 ? 1 : -1); + anim.phiSpeed = (0.3 + Math.random() * 0.5) * (Math.random() > 0.5 ? 1 : -1); + anim.rotateSpeed = new Vector3((Math.random() - 0.5) * 60, (Math.random() - 0.5) * 60, (Math.random() - 0.5) * 60); + anim.scaleBase = (isDuck ? 20 : 1) * (0.6 + Math.random() * 0.8); + anim.scaleFreq = 0.5 + Math.random() * 2; + + if (isCube) { + anim.colorPhase = Math.random() * Math.PI * 2; + anim.colorSpeed = 0.5 + Math.random() * 2; + anim.enableColor(); + } + } + + engine.run(); +}); diff --git a/examples/src/gpu-instancing-custom-data.ts b/examples/src/gpu-instancing-custom-data.ts new file mode 100644 index 0000000000..27857d5730 --- /dev/null +++ b/examples/src/gpu-instancing-custom-data.ts @@ -0,0 +1,151 @@ +/** + * @title GPU Instancing Custom Data + * @category Mesh + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*jjZMTrp-vU8AAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl, Stats } from "@galacean/engine-toolkit"; +import { + Camera, + Color, + DirectLight, + Logger, + Material, + MeshRenderer, + PrimitiveMesh, + Script, + Shader, + ShaderProperty, + Vector3, + Vector4, + WebGLEngine, + WebGLMode +} from "@galacean/engine"; + +const _customColorProperty = ShaderProperty.getByName("renderer_CustomColor"); + +class SpiralFlash extends Script { + radius: number = 0; + radiusSpeed: number = 0; + theta: number = 0; + thetaSpeed: number = 0; + phi: number = 0; + phiSpeed: number = 0; + rotateSpeed: Vector3 = new Vector3(); + scaleBase: number = 1; + scaleFreq: number = 0; + colorPhase: number = 0; + colorSpeed: number = 1; + private _time: number = 0; + private _color: Vector4 = new Vector4(); + + onUpdate(deltaTime: number): void { + this._time += deltaTime; + const t = this._time; + const transform = this.entity.transform; + + // Spiral breathing motion + const r = this.radius * (0.6 + 0.4 * Math.sin(t * this.radiusSpeed)); + const theta = this.theta + t * this.thetaSpeed; + const phi = this.phi + t * this.phiSpeed; + + const sinTheta = Math.sin(theta); + transform.setPosition(r * sinTheta * Math.cos(phi), r * Math.cos(theta), r * sinTheta * Math.sin(phi)); + + // Rotation + const { rotateSpeed } = this; + transform.rotate(rotateSpeed.x * deltaTime, rotateSpeed.y * deltaTime, rotateSpeed.z * deltaTime); + + // Scale pulse + const s = this.scaleBase * (0.7 + 0.3 * Math.sin(t * this.scaleFreq)); + transform.setScale(s, s, s); + + // Color cycles through hue based on time + unique phase + const ct = t * this.colorSpeed + this.colorPhase; + this._color.set(0.5 + 0.5 * Math.sin(ct), 0.5 + 0.5 * Math.sin(ct + 2.094), 0.5 + 0.5 * Math.sin(ct + 4.189), 1.0); + this.entity.getComponent(MeshRenderer).shaderData.setVector4(_customColorProperty, this._color); + } +} + +Logger.enable(); + +Shader.create( + "CustomInstanceShader", + ` + #include + attribute vec3 POSITION; + attribute vec3 NORMAL; + + varying vec3 v_normal; + + void main() { + gl_Position = renderer_MVPMat * vec4(POSITION, 1.0); + v_normal = normalize((renderer_NormalMat * vec4(NORMAL, 0.0)).xyz); + } + `, + ` + uniform vec4 renderer_CustomColor; + + varying vec3 v_normal; + + void main() { + vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0)); + float NdotL = max(dot(v_normal, lightDir), 0.2); + gl_FragColor = vec4(renderer_CustomColor.rgb * NdotL, 1.0); + } + ` +); + +WebGLEngine.create({ canvas: "canvas", graphicDeviceOptions: { webGLMode: WebGLMode.WebGL2 } }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity("Root"); + + // Camera + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 100); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + const camera = cameraEntity.addComponent(Camera); + camera.farClipPlane = 500; + cameraEntity.addComponent(OrbitControl); + + // Stats + cameraEntity.addComponent(Stats); + + // Light + const lightEntity = rootEntity.createChild("Light"); + lightEntity.transform.setRotation(-45, -45, 0); + lightEntity.addComponent(DirectLight).color = new Color(1, 1, 1, 1); + + const mesh = PrimitiveMesh.createCuboid(engine, 1, 1, 1); + const material = new Material(engine, Shader.find("CustomInstanceShader")); + const customColorProperty = ShaderProperty.getByName("renderer_CustomColor"); + + const count = 5000; + for (let i = 0; i < count; i++) { + const entity = rootEntity.createChild("Cube" + i); + const ti = i / count; + + const renderer = entity.addComponent(MeshRenderer); + renderer.mesh = mesh; + renderer.setMaterial(material); + + const initColor = new Vector4(Math.random(), Math.random(), Math.random(), 1.0); + renderer.shaderData.setVector4(customColorProperty, initColor); + + const anim = entity.addComponent(SpiralFlash); + anim.radius = 10 + Math.random() * 40; + anim.radiusSpeed = 0.3 + Math.random() * 0.6; + anim.theta = ti * Math.PI * 2 * 13.7; + anim.phi = ti * Math.PI * 2 * 7.3; + anim.thetaSpeed = (0.2 + Math.random() * 0.4) * (Math.random() > 0.5 ? 1 : -1); + anim.phiSpeed = (0.3 + Math.random() * 0.5) * (Math.random() > 0.5 ? 1 : -1); + anim.rotateSpeed = new Vector3((Math.random() - 0.5) * 60, (Math.random() - 0.5) * 60, (Math.random() - 0.5) * 60); + anim.scaleBase = 0.6 + Math.random() * 0.8; + anim.scaleFreq = 0.5 + Math.random() * 2; + anim.colorPhase = Math.random() * Math.PI * 2; + anim.colorSpeed = 0.5 + Math.random() * 2; + } + + engine.run(); +}); diff --git a/examples/src/spine-keli-4.2.ts b/examples/src/spine-keli-4.2.ts new file mode 100644 index 0000000000..8ad862a4a0 --- /dev/null +++ b/examples/src/spine-keli-4.2.ts @@ -0,0 +1,60 @@ +/** + * @title Spine Keli (4.2) + * @category Spine + */ +import { Camera, Entity, Logger, Vector3, WebGLEngine } from "@galacean/engine"; +import { SpineAnimationRenderer, SpineResource } from "@galacean/engine-spine"; +import "@galacean/engine-spine-core-4.2"; +import * as dat from "dat.gui"; + +Logger.enable(); + +const gui = new dat.GUI(); +const state = { animation: "" }; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const root = scene.createRootEntity(); + + const cameraEntity = root.createChild("camera"); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(0, 2, 10); + cameraEntity.transform.lookAt(new Vector3(0, 2, 0)); + + engine.run(); + + engine.resourceManager + .load({ + urls: [ + "/spine/keli/keli.json", + "/spine/keli/keli.atlas", + "/spine/keli/images/arm_l.png", + "/spine/keli/images/arm_r.png", + "/spine/keli/images/head.png", + "/spine/keli/images/legs.png", + "/spine/keli/images/torso.png" + ], + type: "Spine" + }) + .then((resource) => { + const spineEntity: Entity = resource.instantiate(); + root.addChild(spineEntity); + + const animator = spineEntity.getComponent(SpineAnimationRenderer); + const animationNames = resource.skeletonData.animations.map((animation) => animation.name); + + state.animation = animationNames[0]; + animator.state.setAnimation(0, state.animation, true); + + gui + .add(state, "animation", animationNames) + .name("Animation") + .onChange((name: string) => { + animator.state.setAnimation(0, name, true); + }); + }) + .catch((error) => { + console.error("Failed to load keli (4.2):", error); + }); +}); diff --git a/examples/src/spine-otakugirl-3.8.ts b/examples/src/spine-otakugirl-3.8.ts new file mode 100644 index 0000000000..410db286dd --- /dev/null +++ b/examples/src/spine-otakugirl-3.8.ts @@ -0,0 +1,55 @@ +/** + * @title Spine Otakugirl (3.8) + * @category Spine + * @remarks + * A genuine spine 3.8.99 export (binary .skel), unlike the keli asset used in the 4.2 example — + * this verifies the 3.8 backend against data it's actually meant to parse. + */ +import { Camera, Entity, Logger, Vector3, WebGLEngine } from "@galacean/engine"; +import { SpineAnimationRenderer, SpineResource } from "@galacean/engine-spine"; +import "@galacean/engine-spine-core-3.8"; +import * as dat from "dat.gui"; + +Logger.enable(); + +const gui = new dat.GUI(); +const state = { animation: "" }; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const root = scene.createRootEntity(); + + const cameraEntity = root.createChild("camera"); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(0, 2, 10); + cameraEntity.transform.lookAt(new Vector3(0, 2, 0)); + + engine.run(); + + engine.resourceManager + .load({ + urls: ["/spine/otakugirl/otakugirl.skel", "/spine/otakugirl/otakugirl.atlas", "/spine/otakugirl/otakugirl.png"], + type: "Spine" + }) + .then((resource) => { + const spineEntity: Entity = resource.instantiate(); + root.addChild(spineEntity); + + const animator = spineEntity.getComponent(SpineAnimationRenderer); + const animationNames = resource.skeletonData.animations.map((animation) => animation.name); + + state.animation = animationNames[0]; + animator.state.setAnimation(0, state.animation, true); + + gui + .add(state, "animation", animationNames) + .name("Animation") + .onChange((name: string) => { + animator.state.setAnimation(0, name, true); + }); + }) + .catch((error) => { + console.error("Failed to load otakugirl (3.8):", error); + }); +}); diff --git a/examples/src/sprite-mask.ts b/examples/src/sprite-mask.ts new file mode 100644 index 0000000000..f8dc719105 --- /dev/null +++ b/examples/src/sprite-mask.ts @@ -0,0 +1,87 @@ +/** + * @title Sprite Mask + * @category 2D + */ +import { + AssetType, + Camera, + Sprite, + SpriteMask, + SpriteMaskInteraction, + SpriteMaskLayer, + SpriteRenderer, + Texture2D, + WebGLEngine +} from "@galacean/engine"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + scene.background.solidColor.set(0.05, 0.05, 0.07, 1); + const root = scene.createRootEntity("Root"); + + const cameraEntity = root.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 50); + cameraEntity.addComponent(Camera); + + engine.resourceManager + .load({ + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*ApFPTZSqcMkAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D + }) + .then((texture) => { + const sprite = new Sprite(engine, texture); + const maskSprite = new Sprite(engine, createSolidTexture(engine)); + + const spriteWidth = sprite.width; + const spriteHeight = sprite.height; + // Mask covers ~half of the sprite so the cut is obvious. + const maskWidth = spriteWidth * 0.6; + const maskHeight = spriteHeight * 0.6; + // Lay the two characters out side by side based on sprite size. + const groupOffsetX = spriteWidth * 0.6; + + // --- Left: VisibleInsideMask -> only the part covered by the square mask is visible --- + const leftGroup = root.createChild("LeftGroup"); + leftGroup.transform.setPosition(-groupOffsetX, 0, 0); + + const leftMaskEntity = leftGroup.createChild("Mask"); + const leftMask = leftMaskEntity.addComponent(SpriteMask); + leftMask.sprite = maskSprite; + leftMask.width = maskWidth; + leftMask.height = maskHeight; + leftMask.influenceLayers = SpriteMaskLayer.Layer0; + + const leftSpriteEntity = leftGroup.createChild("Sprite"); + const leftSprite = leftSpriteEntity.addComponent(SpriteRenderer); + leftSprite.sprite = sprite; + leftSprite.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + leftSprite.maskLayer = SpriteMaskLayer.Layer0; + + // --- Right: VisibleOutsideMask -> character with a square hole punched out --- + const rightGroup = root.createChild("RightGroup"); + rightGroup.transform.setPosition(groupOffsetX, 0, 0); + + const rightMaskEntity = rightGroup.createChild("Mask"); + const rightMask = rightMaskEntity.addComponent(SpriteMask); + rightMask.sprite = maskSprite; + rightMask.width = maskWidth; + rightMask.height = maskHeight; + rightMask.influenceLayers = SpriteMaskLayer.Layer1; + + const rightSpriteEntity = rightGroup.createChild("Sprite"); + const rightSprite = rightSpriteEntity.addComponent(SpriteRenderer); + rightSprite.sprite = sprite; + rightSprite.maskInteraction = SpriteMaskInteraction.VisibleOutsideMask; + rightSprite.maskLayer = SpriteMaskLayer.Layer1; + }); + + engine.run(); +}); + +function createSolidTexture(engine: WebGLEngine): Texture2D { + const texture = new Texture2D(engine, 1, 1); + texture.setPixelBuffer(new Uint8Array([255, 255, 255, 255])); + return texture; +} diff --git a/examples/src/sprite-renderer-filled.ts b/examples/src/sprite-renderer-filled.ts new file mode 100644 index 0000000000..e31504875f --- /dev/null +++ b/examples/src/sprite-renderer-filled.ts @@ -0,0 +1,146 @@ +/** + * @title Sprite Filled + * @category 2D + */ + +import * as dat from "dat.gui"; +import { + AssetType, + Camera, + Sprite, + SpriteDrawMode, + SpriteFilledMode, + SpriteFilledOrigin, + SpriteRenderer, + Texture2D, + Vector3, + WebGLEngine +} from "@galacean/engine"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + scene.background.solidColor.set(0.15, 0.15, 0.18, 1); + const rootEntity = scene.createRootEntity(); + + // Create camera + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.transform.setPosition(0, 0, 50); + cameraEntity.addComponent(Camera); + + // Load texture and create sprite + engine.resourceManager + .load({ + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*ApFPTZSqcMkAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D + }) + .then((texture) => { + const spriteEntity = rootEntity.createChild("sprite"); + spriteEntity.transform.position = new Vector3(0, 0, 0); + spriteEntity.transform.setScale(2, 2, 2); + const renderer = spriteEntity.addComponent(SpriteRenderer); + renderer.sprite = new Sprite(engine, texture); + + // Set filled mode + renderer.drawMode = SpriteDrawMode.Filled; + renderer.filledMode = SpriteFilledMode.Radial360; + renderer.filledOrigin = SpriteFilledOrigin.Bottom; + renderer.filledAmount = 0.75; + renderer.filledClockWise = true; + + addGUI(renderer); + }); + + engine.run(); + + function addGUI(renderer: SpriteRenderer) { + const gui = new dat.GUI(); + + const filledModeMap: Record = { + Horizontal: SpriteFilledMode.Horizontal, + Vertical: SpriteFilledMode.Vertical, + Radial90: SpriteFilledMode.Radial90, + Radial180: SpriteFilledMode.Radial180, + Radial360: SpriteFilledMode.Radial360 + }; + + const originForRadial360: string[] = ["Right", "Top", "Left", "Bottom"]; + const originForRadial180: string[] = ["Right", "Top", "Left", "Bottom"]; + const originForRadial90: string[] = ["BottomLeft", "BottomRight", "TopRight", "TopLeft"]; + const originForHorizontal: string[] = ["Left", "Right"]; + const originForVertical: string[] = ["Bottom", "Top"]; + + const originMap: Record = { + Right: SpriteFilledOrigin.Right, + TopRight: SpriteFilledOrigin.TopRight, + Top: SpriteFilledOrigin.Top, + TopLeft: SpriteFilledOrigin.TopLeft, + Left: SpriteFilledOrigin.Left, + BottomLeft: SpriteFilledOrigin.BottomLeft, + Bottom: SpriteFilledOrigin.Bottom, + BottomRight: SpriteFilledOrigin.BottomRight + }; + + const state = { + filledMode: "Radial360", + origin: "Bottom", + amount: 0.75, + clockWise: true + }; + + const folder = gui.addFolder("Filled Sprite"); + folder.open(); + + // Filled mode + folder.add(state, "filledMode", Object.keys(filledModeMap)).onChange((value: string) => { + renderer.filledMode = filledModeMap[value]; + updateOriginOptions(value); + }); + + // Origin + let originCtrl = folder.add(state, "origin", originForRadial360).onChange((value: string) => { + renderer.filledOrigin = originMap[value]; + }); + + // Amount + folder.add(state, "amount", 0.0, 1.0, 0.01).onChange((value: number) => { + renderer.filledAmount = value; + }); + + // ClockWise + folder.add(state, "clockWise").onChange((value: boolean) => { + renderer.filledClockWise = value; + }); + + function updateOriginOptions(mode: string) { + folder.remove(originCtrl); + + let options: string[]; + switch (mode) { + case "Horizontal": + options = originForHorizontal; + break; + case "Vertical": + options = originForVertical; + break; + case "Radial90": + options = originForRadial90; + break; + case "Radial180": + options = originForRadial180; + break; + default: + options = originForRadial360; + break; + } + + state.origin = options[0]; + renderer.filledOrigin = originMap[state.origin]; + + originCtrl = folder.add(state, "origin", options).onChange((value: string) => { + renderer.filledOrigin = originMap[value]; + }); + } + } +}); diff --git a/examples/src/ui-mask-alpha.ts b/examples/src/ui-mask-alpha.ts new file mode 100644 index 0000000000..9b5f340fd0 --- /dev/null +++ b/examples/src/ui-mask-alpha.ts @@ -0,0 +1,97 @@ +/** + * @title UI Mask Alpha Cutoff + * @category UI + */ +import * as dat from "dat.gui"; +import { Camera, Color, Sprite, SpriteMaskInteraction, Texture2D, TextureFormat, WebGLEngine } from "@galacean/engine"; +import { CanvasRenderMode, Image, Mask, Text, UICanvas, UITransform } from "@galacean/engine-ui"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + scene.background.solidColor = new Color(0.03, 0.04, 0.07, 1); + const root = scene.createRootEntity("Root"); + + const cameraEntity = root.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 10); + const camera = cameraEntity.addComponent(Camera); + + const canvasEntity = root.createChild("UICanvas"); + const uiCanvas = canvasEntity.addComponent(UICanvas); + uiCanvas.renderMode = CanvasRenderMode.ScreenSpaceCamera; + uiCanvas.camera = camera; + uiCanvas.referenceResolutionPerUnit = 100; + uiCanvas.referenceResolution.set(1200, 800); + + const solidSprite = createSolidSprite(engine); + const circleSprite = createCircleSprite(engine, 256); + + const groupEntity = canvasEntity.createChild("Group"); + (groupEntity.transform).setPosition(0, 40, 0); + + // Circular mask + const maskEntity = groupEntity.createChild("Mask"); + (maskEntity.transform).size.set(300, 300); + const mask = maskEntity.addComponent(Mask); + mask.sprite = circleSprite; + mask.alphaCutoff = 0.5; + + // Background visible inside circle + const insideEntity = groupEntity.createChild("Inside"); + (insideEntity.transform).size.set(500, 500); + const inside = insideEntity.addComponent(Image); + inside.sprite = solidSprite; + inside.color.set(0.95, 0.61, 0.07, 1); + inside.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + + const labelEntity = canvasEntity.createChild("Label"); + (labelEntity.transform).size.set(800, 80); + (labelEntity.transform).setPosition(0, -260, 0); + const label = labelEntity.addComponent(Text); + label.text = "Drag the slider to change alphaCutoff"; + label.fontSize = 28; + label.color.set(0.85, 0.9, 1, 1); + + const gui = new dat.GUI(); + const state = { alphaCutoff: 0.5 }; + gui + .add(state, "alphaCutoff", 0.0, 1.0, 0.01) + .name("Mask Alpha Cutoff") + .onChange((value: number) => { + mask.alphaCutoff = value; + }); + + engine.run(); +}); + +function createSolidSprite(engine: WebGLEngine): Sprite { + const texture = new Texture2D(engine, 1, 1); + texture.setPixelBuffer(new Uint8Array([255, 255, 255, 255])); + return new Sprite(engine, texture); +} + +/** Soft circle: alpha falls off radially so alphaCutoff has visible effect. */ +function createCircleSprite(engine: WebGLEngine, size: number): Sprite { + const buffer = new Uint8Array(size * size * 4); + const cx = size * 0.5; + const cy = size * 0.5; + const radius = size * 0.5; + for (let y = 0; y < size; y++) { + for (let x = 0; x < size; x++) { + const dx = x - cx; + const dy = y - cy; + const dist = Math.sqrt(dx * dx + dy * dy); + const t = Math.max(0, 1 - dist / radius); + const alpha = Math.min(255, Math.floor(t * 255)); + const i = (y * size + x) * 4; + buffer[i] = 255; + buffer[i + 1] = 255; + buffer[i + 2] = 255; + buffer[i + 3] = alpha; + } + } + const texture = new Texture2D(engine, size, size, TextureFormat.R8G8B8A8, false); + texture.setPixelBuffer(buffer); + return new Sprite(engine, texture); +} diff --git a/examples/src/ui-mask-overlay.ts b/examples/src/ui-mask-overlay.ts new file mode 100644 index 0000000000..e7fa6b5096 --- /dev/null +++ b/examples/src/ui-mask-overlay.ts @@ -0,0 +1,120 @@ +/** + * @title UI Mask Overlay + * @category UI + */ +import { Camera, Color, Sprite, SpriteMaskInteraction, Texture2D, WebGLEngine } from "@galacean/engine"; +import { CanvasRenderMode, Image, Mask, RectMask2D, Text, UICanvas, UITransform } from "@galacean/engine-ui"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + scene.background.solidColor = new Color(0.03, 0.04, 0.07, 1); + const root = scene.createRootEntity("Root"); + + // Camera is required for default scene rendering even though overlay UI doesn't use it for projection. + const cameraEntity = root.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 10); + cameraEntity.addComponent(Camera); + + const canvasEntity = root.createChild("UICanvas"); + const uiCanvas = canvasEntity.addComponent(UICanvas); + uiCanvas.renderMode = CanvasRenderMode.ScreenSpaceOverlay; + uiCanvas.referenceResolutionPerUnit = 100; + uiCanvas.referenceResolution.set(1200, 800); + + const solidSprite = createSolidSprite(engine); + + // ===== Left half: SpriteMask (Mask component) ===== + const maskGroup = canvasEntity.createChild("MaskGroup"); + (maskGroup.transform).setPosition(-300, 60, 0); + + const maskEntity = maskGroup.createChild("Mask"); + (maskEntity.transform).size.set(280, 280); + const mask = maskEntity.addComponent(Mask); + mask.sprite = solidSprite; + + const insideEntity = maskGroup.createChild("InsideImage"); + (insideEntity.transform).size.set(440, 440); + const inside = insideEntity.addComponent(Image); + inside.sprite = solidSprite; + inside.color.set(0.91, 0.3, 0.24, 1); + inside.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + + const maskLabelEntity = maskGroup.createChild("Label"); + (maskLabelEntity.transform).size.set(360, 60); + (maskLabelEntity.transform).setPosition(0, -260, 0); + const maskLabel = maskLabelEntity.addComponent(Text); + maskLabel.text = "Mask (Overlay)"; + maskLabel.fontSize = 32; + maskLabel.color.set(1, 1, 1, 1); + + // ===== Right half: RectMask2D ===== + const rectGroup = canvasEntity.createChild("RectGroup"); + (rectGroup.transform).setPosition(300, 60, 0); + + const viewportEntity = rectGroup.createChild("Viewport"); + (viewportEntity.transform).size.set(360, 280); + const viewport = viewportEntity.addComponent(Image); + viewport.sprite = solidSprite; + viewport.color.set(0.17, 0.18, 0.2, 1); + viewportEntity.addComponent(RectMask2D); + + const contentEntity = viewportEntity.createChild("Content"); + (contentEntity.transform).size.set(560, 480); + (contentEntity.transform).setPosition(60, -50, 0); + + const tileColors = [ + new Color(0.91, 0.3, 0.24, 1), + new Color(0.16, 0.5, 0.73, 1), + new Color(0.18, 0.8, 0.44, 1), + new Color(0.95, 0.61, 0.07, 1) + ]; + const tileSize = 220; + const gap = 20; + for (let row = 0; row < 2; row++) { + for (let col = 0; col < 2; col++) { + const i = row * 2 + col; + const tileEntity = contentEntity.createChild(`Tile_${i}`); + const t = tileEntity.transform; + t.size.set(tileSize, tileSize); + t.setPosition(col * (tileSize + gap) - (tileSize + gap) / 2, (tileSize + gap) / 2 - row * (tileSize + gap), 0); + + const tile = tileEntity.addComponent(Image); + tile.sprite = solidSprite; + tile.color = tileColors[i]; + + const labelEntity = tileEntity.createChild("Label"); + (labelEntity.transform).size.set(tileSize, tileSize); + const label = labelEntity.addComponent(Text); + label.text = `${i + 1}`; + label.fontSize = 64; + label.color.set(1, 1, 1, 1); + } + } + + const rectLabelEntity = rectGroup.createChild("Label"); + (rectLabelEntity.transform).size.set(360, 60); + (rectLabelEntity.transform).setPosition(0, -260, 0); + const rectLabel = rectLabelEntity.addComponent(Text); + rectLabel.text = "RectMask2D (Overlay)"; + rectLabel.fontSize = 32; + rectLabel.color.set(1, 1, 1, 1); + + // Top header + const headerEntity = canvasEntity.createChild("Header"); + (headerEntity.transform).size.set(900, 80); + (headerEntity.transform).setPosition(0, 320, 0); + const header = headerEntity.addComponent(Text); + header.text = "ScreenSpaceOverlay · Mask & RectMask2D"; + header.fontSize = 36; + header.color.set(0.85, 0.92, 1, 1); + + engine.run(); +}); + +function createSolidSprite(engine: WebGLEngine): Sprite { + const texture = new Texture2D(engine, 1, 1); + texture.setPixelBuffer(new Uint8Array([255, 255, 255, 255])); + return new Sprite(engine, texture); +} diff --git a/examples/src/ui-mask.ts b/examples/src/ui-mask.ts new file mode 100644 index 0000000000..260392e6e1 --- /dev/null +++ b/examples/src/ui-mask.ts @@ -0,0 +1,85 @@ +/** + * @title UI Mask + * @category UI + */ +import { Camera, Color, Sprite, SpriteMaskInteraction, Texture2D, WebGLEngine } from "@galacean/engine"; +import { CanvasRenderMode, Image, Mask, Text, UICanvas, UITransform } from "@galacean/engine-ui"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + scene.background.solidColor = new Color(0.03, 0.04, 0.07, 1); + const root = scene.createRootEntity("Root"); + + const cameraEntity = root.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 10); + const camera = cameraEntity.addComponent(Camera); + + const canvasEntity = root.createChild("UICanvas"); + const uiCanvas = canvasEntity.addComponent(UICanvas); + uiCanvas.renderMode = CanvasRenderMode.ScreenSpaceCamera; + uiCanvas.camera = camera; + uiCanvas.referenceResolutionPerUnit = 100; + uiCanvas.referenceResolution.set(1200, 800); + + const solidSprite = createSolidSprite(engine); + + // --- Left group: VisibleInsideMask --- + const leftGroupEntity = canvasEntity.createChild("LeftGroup"); + (leftGroupEntity.transform).setPosition(-300, 0, 0); + + // Square mask + const leftMaskEntity = leftGroupEntity.createChild("Mask"); + (leftMaskEntity.transform).size.set(300, 300); + const leftMask = leftMaskEntity.addComponent(Mask); + leftMask.sprite = solidSprite; + + // Image clipped to inside the mask + const insideImageEntity = leftGroupEntity.createChild("InsideImage"); + (insideImageEntity.transform).size.set(500, 500); + const insideImage = insideImageEntity.addComponent(Image); + insideImage.sprite = solidSprite; + insideImage.color.set(0.91, 0.3, 0.24, 1); + insideImage.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + + const leftLabelEntity = leftGroupEntity.createChild("Label"); + (leftLabelEntity.transform).size.set(300, 60); + (leftLabelEntity.transform).setPosition(0, -210, 0); + const leftLabel = leftLabelEntity.addComponent(Text); + leftLabel.text = "VisibleInsideMask"; + leftLabel.fontSize = 30; + leftLabel.color.set(1, 1, 1, 1); + + // --- Right group: VisibleOutsideMask --- + const rightGroupEntity = canvasEntity.createChild("RightGroup"); + (rightGroupEntity.transform).setPosition(300, 0, 0); + + const rightMaskEntity = rightGroupEntity.createChild("Mask"); + (rightMaskEntity.transform).size.set(300, 300); + const rightMask = rightMaskEntity.addComponent(Mask); + rightMask.sprite = solidSprite; + + const outsideImageEntity = rightGroupEntity.createChild("OutsideImage"); + (outsideImageEntity.transform).size.set(500, 500); + const outsideImage = outsideImageEntity.addComponent(Image); + outsideImage.sprite = solidSprite; + outsideImage.color.set(0.16, 0.5, 0.73, 1); + outsideImage.maskInteraction = SpriteMaskInteraction.VisibleOutsideMask; + + const rightLabelEntity = rightGroupEntity.createChild("Label"); + (rightLabelEntity.transform).size.set(300, 60); + (rightLabelEntity.transform).setPosition(0, -210, 0); + const rightLabel = rightLabelEntity.addComponent(Text); + rightLabel.text = "VisibleOutsideMask"; + rightLabel.fontSize = 30; + rightLabel.color.set(1, 1, 1, 1); + + engine.run(); +}); + +function createSolidSprite(engine: WebGLEngine): Sprite { + const texture = new Texture2D(engine, 1, 1); + texture.setPixelBuffer(new Uint8Array([255, 255, 255, 255])); + return new Sprite(engine, texture); +} diff --git a/examples/src/ui-rect-mask-nested.ts b/examples/src/ui-rect-mask-nested.ts new file mode 100644 index 0000000000..0eae971bfb --- /dev/null +++ b/examples/src/ui-rect-mask-nested.ts @@ -0,0 +1,76 @@ +/** + * @title UI RectMask2D Nested + * @category UI + */ +import { Camera, Color, Sprite, Texture2D, WebGLEngine } from "@galacean/engine"; +import { CanvasRenderMode, Image, RectMask2D, Text, UICanvas, UITransform } from "@galacean/engine-ui"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + scene.background.solidColor = new Color(0.03, 0.04, 0.07, 1); + const root = scene.createRootEntity("Root"); + + const cameraEntity = root.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 10); + const camera = cameraEntity.addComponent(Camera); + + const canvasEntity = root.createChild("UICanvas"); + const uiCanvas = canvasEntity.addComponent(UICanvas); + uiCanvas.renderMode = CanvasRenderMode.ScreenSpaceCamera; + uiCanvas.camera = camera; + uiCanvas.referenceResolutionPerUnit = 100; + uiCanvas.referenceResolution.set(1200, 800); + + const solidSprite = createSolidSprite(engine); + + // Outer mask (wide, short) + const outerEntity = canvasEntity.createChild("OuterMask"); + (outerEntity.transform).size.set(560, 240); + (outerEntity.transform).setPosition(0, 60, 0); + const outerImage = outerEntity.addComponent(Image); + outerImage.sprite = solidSprite; + outerImage.color.set(0.13, 0.18, 0.28, 1); + outerEntity.addComponent(RectMask2D); + + // Inner mask (tall, narrow), child of outer + const innerEntity = outerEntity.createChild("InnerMask"); + (innerEntity.transform).size.set(240, 480); + (innerEntity.transform).setPosition(0, 0, 0); + const innerImage = innerEntity.addComponent(Image); + innerImage.sprite = solidSprite; + innerImage.color.set(0.2, 0.3, 0.5, 1); + innerEntity.addComponent(RectMask2D); + + // Big colored content under both masks — only the intersection of outer ∩ inner remains visible + const contentEntity = innerEntity.createChild("Content"); + (contentEntity.transform).size.set(800, 800); + const content = contentEntity.addComponent(Image); + content.sprite = solidSprite; + content.color.set(0.95, 0.61, 0.07, 1); + + const labelTopEntity = canvasEntity.createChild("LabelTop"); + (labelTopEntity.transform).size.set(900, 60); + (labelTopEntity.transform).setPosition(0, 220, 0); + const labelTop = labelTopEntity.addComponent(Text); + labelTop.text = "Outer 560x240 ∩ Inner 240x480 → visible: 240x240"; + labelTop.fontSize = 28; + labelTop.color.set(1, 1, 1, 1); + + const labelBottomEntity = canvasEntity.createChild("LabelBottom"); + (labelBottomEntity.transform).size.set(900, 60); + (labelBottomEntity.transform).setPosition(0, -260, 0); + const labelBottom = labelBottomEntity.addComponent(Text); + labelBottom.text = "Nested RectMask2D takes the rect intersection of all ancestor masks."; + labelBottom.fontSize = 24; + labelBottom.color.set(0.77, 0.82, 0.89, 1); + + engine.run(); +}); + +function createSolidSprite(engine: WebGLEngine): Sprite { + const texture = new Texture2D(engine, 1, 1); + texture.setPixelBuffer(new Uint8Array([255, 255, 255, 255])); + return new Sprite(engine, texture); +} diff --git a/examples/src/ui-rect-mask.ts b/examples/src/ui-rect-mask.ts new file mode 100644 index 0000000000..92078d6c6a --- /dev/null +++ b/examples/src/ui-rect-mask.ts @@ -0,0 +1,135 @@ +/** + * @title UI RectMask2D + * @category UI + */ +import * as dat from "dat.gui"; +import { Camera, Color, Sprite, Texture2D, Vector2, WebGLEngine } from "@galacean/engine"; +import { CanvasRenderMode, Image, RectMask2D, Text, UICanvas, UITransform } from "@galacean/engine-ui"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + scene.background.solidColor = new Color(0.03, 0.04, 0.07, 1); + const root = scene.createRootEntity("Root"); + + const cameraEntity = root.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 10); + const camera = cameraEntity.addComponent(Camera); + + const canvasEntity = root.createChild("UICanvas"); + const uiCanvas = canvasEntity.addComponent(UICanvas); + uiCanvas.renderMode = CanvasRenderMode.ScreenSpaceCamera; + uiCanvas.camera = camera; + uiCanvas.referenceResolutionPerUnit = 100; + uiCanvas.referenceResolution.set(1200, 800); + + const solidSprite = createSolidSprite(engine); + + // Frame card + const frameEntity = canvasEntity.createChild("Frame"); + (frameEntity.transform).size.set(560, 460); + (frameEntity.transform).setPosition(-180, 20, 0); + const frame = frameEntity.addComponent(Image); + frame.sprite = solidSprite; + frame.color.set(0.09, 0.11, 0.15, 1); + + // Viewport with RectMask2D + const viewportEntity = frameEntity.createChild("Viewport"); + (viewportEntity.transform).size.set(440, 320); + (viewportEntity.transform).setPosition(30, -10, 0); + const viewport = viewportEntity.addComponent(Image); + viewport.sprite = solidSprite; + viewport.color.set(0.17, 0.18, 0.2, 1); + const rectMask = viewportEntity.addComponent(RectMask2D); + + // 3x3 colored tiles overflow the viewport + const contentEntity = viewportEntity.createChild("Content"); + (contentEntity.transform).size.set(740, 560); + (contentEntity.transform).setPosition(80, -60, 0); + + const colors = [ + new Color(0.91, 0.3, 0.24, 1), + new Color(0.16, 0.5, 0.73, 1), + new Color(0.18, 0.8, 0.44, 1), + new Color(0.95, 0.61, 0.07, 1), + new Color(0.56, 0.27, 0.68, 1), + new Color(0.2, 0.6, 0.86, 1), + new Color(0.83, 0.33, 0.33, 1), + new Color(0.1, 0.74, 0.61, 1), + new Color(0.93, 0.78, 0.0, 1) + ]; + + const tileWidth = 180; + const tileHeight = 180; + const gap = 10; + for (let row = 0; row < 3; row++) { + for (let col = 0; col < 3; col++) { + const index = row * 3 + col; + const tileEntity = contentEntity.createChild(`Tile_${index}`); + const t = tileEntity.transform; + t.size.set(tileWidth, tileHeight); + t.setPosition(col * (tileWidth + gap) - 170, 170 - row * (tileHeight + gap), 0); + + const tile = tileEntity.addComponent(Image); + tile.sprite = solidSprite; + tile.color = colors[index]; + + const labelEntity = tileEntity.createChild("Label"); + (labelEntity.transform).size.set(tileWidth, tileHeight); + const label = labelEntity.addComponent(Text); + label.text = `${index + 1}`; + label.fontSize = 56; + label.color.set(1, 1, 1, 1); + } + } + + // Right info card + const noteEntity = canvasEntity.createChild("Note"); + (noteEntity.transform).size.set(360, 220); + (noteEntity.transform).setPosition(290, 20, 0); + const note = noteEntity.addComponent(Image); + note.sprite = solidSprite; + note.color.set(0.08, 0.09, 0.12, 1); + + const noteTextEntity = noteEntity.createChild("Copy"); + (noteTextEntity.transform).size.set(320, 180); + const noteText = noteTextEntity.addComponent(Text); + noteText.text = + "RectMask2D clips Image\nand Text by an axis-\naligned rectangle.\n\nUse the GUI to tweak\nsoftness / alphaClip."; + noteText.fontSize = 26; + noteText.color.set(0.77, 0.82, 0.89, 1); + + const gui = new dat.GUI(); + const state = { + softnessX: 0, + softnessY: 0, + alphaClip: false + }; + gui + .add(state, "softnessX", 0, 80, 1) + .name("softness.x") + .onChange((v: number) => { + rectMask.softness = new Vector2(v, state.softnessY); + }); + gui + .add(state, "softnessY", 0, 80, 1) + .name("softness.y") + .onChange((v: number) => { + rectMask.softness = new Vector2(state.softnessX, v); + }); + gui + .add(state, "alphaClip") + .name("alphaClip (discard)") + .onChange((v: boolean) => { + rectMask.alphaClip = v; + }); + + engine.run(); +}); + +function createSolidSprite(engine: WebGLEngine): Sprite { + const texture = new Texture2D(engine, 1, 1); + texture.setPixelBuffer(new Uint8Array([255, 255, 255, 255])); + return new Sprite(engine, texture); +} diff --git a/examples/src/ui-text-outline.ts b/examples/src/ui-text-outline.ts new file mode 100644 index 0000000000..fae8865a9f --- /dev/null +++ b/examples/src/ui-text-outline.ts @@ -0,0 +1,121 @@ +/** + * @title UI Text Outline + * @category UI + */ +import * as dat from "dat.gui"; +import { Camera, Color, WebGLEngine } from "@galacean/engine"; +import { CanvasRenderMode, Text, UICanvas, UITransform } from "@galacean/engine-ui"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + scene.background.solidColor = new Color(0.05, 0.07, 0.1, 1); + const root = scene.createRootEntity("Root"); + + const cameraEntity = root.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 10); + const camera = cameraEntity.addComponent(Camera); + + const canvasEntity = root.createChild("UICanvas"); + const uiCanvas = canvasEntity.addComponent(UICanvas); + uiCanvas.renderMode = CanvasRenderMode.ScreenSpaceCamera; + uiCanvas.camera = camera; + uiCanvas.referenceResolutionPerUnit = 100; + uiCanvas.referenceResolution.set(1280, 800); + + // ---------- Matrix grid ---------- + // Rows = outlineWidth (0 / 1 / 2 / 4 / 8 px) + // Cols = (text color, outline color) presets + const widthCases = [0, 1, 2, 4, 8]; + const colorCases: { name: string; fill: Color; outline: Color }[] = [ + { name: "white / black", fill: new Color(1, 1, 1, 1), outline: new Color(0, 0, 0, 1) }, + { name: "black / white", fill: new Color(0, 0, 0, 1), outline: new Color(1, 1, 1, 1) }, + { name: "yellow / red", fill: new Color(1, 0.85, 0.1, 1), outline: new Color(0.85, 0.1, 0.1, 1) } + ]; + + const cellW = 380; + const cellH = 110; + const startX = -cellW * (colorCases.length - 1) * 0.5; + const startY = 220; + + // Column headers + for (let c = 0; c < colorCases.length; c++) { + const headerEntity = canvasEntity.createChild(`header-${c}`); + const ht = headerEntity.transform; + ht.size.set(cellW, 32); + ht.setPosition(startX + c * cellW, startY + cellH * 0.5 + 20, 0); + const header = headerEntity.addComponent(Text); + header.text = `text/outline = ${colorCases[c].name}`; + header.fontSize = 18; + header.color.set(0.7, 0.78, 0.9, 1); + } + + for (let r = 0; r < widthCases.length; r++) { + const w = widthCases[r]; + + // Row label + const labelEntity = canvasEntity.createChild(`row-label-${r}`); + const lt = labelEntity.transform; + lt.size.set(120, cellH); + lt.setPosition(startX - cellW * 0.5 - 60, startY - r * cellH, 0); + const label = labelEntity.addComponent(Text); + label.text = `width = ${w}px`; + label.fontSize = 18; + label.color.set(0.7, 0.78, 0.9, 1); + + for (let c = 0; c < colorCases.length; c++) { + const { fill, outline } = colorCases[c]; + + const cell = canvasEntity.createChild(`cell-${r}-${c}`); + const ct = cell.transform; + ct.size.set(cellW, cellH); + ct.setPosition(startX + c * cellW, startY - r * cellH, 0); + const text = cell.addComponent(Text); + text.text = "Hello 描边 Outline"; + text.fontSize = 36; + text.color = fill; + text.outlineWidth = w; + text.outlineColor = outline; + } + } + + // ---------- Live preview controlled by dat.gui ---------- + const previewEntity = canvasEntity.createChild("preview"); + const previewTransform = previewEntity.transform; + previewTransform.size.set(900, 160); + previewTransform.setPosition(0, -300, 0); + const preview = previewEntity.addComponent(Text); + preview.text = "实时 Live 预览 Preview"; + preview.fontSize = 72; + preview.color = new Color(1, 1, 1, 1); + preview.outlineWidth = 3; + preview.outlineColor = new Color(0, 0, 0, 1); + + const state = { + fontSize: preview.fontSize, + outlineWidth: preview.outlineWidth, + fillColor: [255, 255, 255], + outlineColor: [0, 0, 0], + text: preview.text + }; + + const gui = new dat.GUI(); + gui.add(state, "text").onChange((v: string) => { + preview.text = v; + }); + gui.add(state, "fontSize", 12, 120, 1).onChange((v: number) => { + preview.fontSize = v; + }); + gui.add(state, "outlineWidth", 0, 8, 0.1).onChange((v: number) => { + preview.outlineWidth = v; + }); + gui.addColor(state, "fillColor").onChange((rgb: number[]) => { + preview.color.set(rgb[0] / 255, rgb[1] / 255, rgb[2] / 255, 1); + }); + gui.addColor(state, "outlineColor").onChange((rgb: number[]) => { + preview.outlineColor.set(rgb[0] / 255, rgb[1] / 255, rgb[2] / 255, 1); + }); + + engine.run(); +}); diff --git a/package.json b/package.json index 6be276f782..feef7fc6c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-root", - "version": "2.0.0-alpha.24", + "version": "0.0.0-experimental-2.0-game.19", "packageManager": "pnpm@9.3.0", "private": true, "scripts": { @@ -41,18 +41,18 @@ "@types/dom-webcodecs": "^0.1.13", "@types/node": "^18.7.16", "@types/webxr": "latest", - "@typescript-eslint/eslint-plugin": "^6.1.0", - "@typescript-eslint/parser": "^6.1.0", + "@typescript-eslint/eslint-plugin": "^8.58.1", + "@typescript-eslint/parser": "^8.58.1", "@vitest/coverage-v8": "2.1.3", "bumpp": "^9.5.2", "cross-env": "^5.2.0", "electron": "^13", - "eslint": "^8.44.0", + "eslint": "^8.57.1", "eslint-config-prettier": "^8.8.0", "eslint-plugin-prettier": "^5.0.0", "fs-extra": "^10.1.0", "husky": "^8.0.0", - "lint-staged": "^10.5.3", + "lint-staged": "^16.4.0", "nyc": "^15.1.0", "odiff-bin": "^2.5.0", "prettier": "^3.0.0", @@ -65,9 +65,8 @@ "vitest": "2.1.3" }, "lint-staged": { - "*.{ts}": [ - "eslint --fix", - "git add" + "**/*.ts": [ + "eslint --fix" ] }, "repository": "git@github.com:galacean/runtime.git" diff --git a/packages/core/package.json b/packages/core/package.json index be735da868..b6d08f767b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-core", - "version": "2.0.0-alpha.24", + "version": "0.0.0-experimental-2.0-game.19", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/core/src/2d/assembler/FilledSpriteAssembler.ts b/packages/core/src/2d/assembler/FilledSpriteAssembler.ts new file mode 100644 index 0000000000..b5e9797119 --- /dev/null +++ b/packages/core/src/2d/assembler/FilledSpriteAssembler.ts @@ -0,0 +1,613 @@ +import { BoundingBox, Matrix, Vector2, Vector3 } from "@galacean/engine-math"; +import { SubPrimitiveChunk } from "../../RenderPipeline/SubPrimitiveChunk"; +import { StaticInterfaceImplement } from "../../base/StaticInterfaceImplement"; +import { SpriteFilledMode } from "../enums/SpriteFilledMode"; +import { SpriteFilledOrigin } from "../enums/SpriteFilledOrigin"; +import { ISpriteAssembler } from "./ISpriteAssembler"; +import { ISpriteRenderer } from "./ISpriteRenderer"; + +/** + * Assemble vertex data for the sprite renderer in filled mode. + */ +@StaticInterfaceImplement() +export class FilledSpriteAssembler { + private static _matrix = new Matrix(); + private static _worldPositions = [ + new Vector3(), + new Vector3(), + new Vector3(), + new Vector3(), + new Vector3(), + new Vector3(), + new Vector3(), + new Vector3(), + new Vector3() + ]; + private static _uvs = [ + new Vector2(), + new Vector2(), + new Vector2(), + new Vector2(), + new Vector2(), + new Vector2(), + new Vector2(), + new Vector2(), + new Vector2() + ]; + private static _inPositions: Vector3[] = []; + private static _inUVs: Vector2[] = []; + private static _outPositions: Vector3[] = [new Vector3(), new Vector3(), new Vector3(), new Vector3()]; + private static _outUVs: Vector2[] = [new Vector2(), new Vector2(), new Vector2(), new Vector2()]; + private static _vertexOffset = 0; + private static _indicesOffset = 0; + + static resetData(renderer: ISpriteRenderer): void { + const manager = renderer._getChunkManager(); + const lastSubChunk = renderer._subChunk; + lastSubChunk && manager.freeSubChunk(lastSubChunk); + const subChunk = manager.allocateSubChunk(16); + subChunk.indices = []; + renderer._subChunk = subChunk; + } + + static updatePositions( + renderer: ISpriteRenderer, + worldMatrix: Matrix, + width: number, + height: number, + pivot: Vector2, + flipX: boolean, + flipY: boolean + ): void { + const { x: pivotX, y: pivotY } = pivot; + const modelMatrix = FilledSpriteAssembler._matrix; + const { elements: wE } = modelMatrix; + const { elements: pWE } = worldMatrix; + const sx = flipX ? -width : width; + const sy = flipY ? -height : height; + (wE[0] = pWE[0] * sx), (wE[1] = pWE[1] * sx), (wE[2] = pWE[2] * sx); + (wE[4] = pWE[4] * sy), (wE[5] = pWE[5] * sy), (wE[6] = pWE[6] * sy); + (wE[8] = pWE[8]), (wE[9] = pWE[9]), (wE[10] = pWE[10]); + wE[12] = pWE[12] - pivotX * wE[0] - pivotY * wE[4]; + wE[13] = pWE[13] - pivotX * wE[1] - pivotY * wE[5]; + wE[14] = pWE[14] - pivotX * wE[2] - pivotY * wE[6]; + + switch (renderer.filledMode) { + case SpriteFilledMode.Horizontal: + this._filledLinear(renderer, modelMatrix, true); + break; + case SpriteFilledMode.Vertical: + this._filledLinear(renderer, modelMatrix, false); + break; + case SpriteFilledMode.Radial90: + this._filledRadial90( + renderer, + modelMatrix, + renderer.filledOrigin, + renderer.filledAmount, + renderer.filledClockWise + ); + break; + case SpriteFilledMode.Radial180: + this._filledRadial180( + renderer, + modelMatrix, + renderer.filledOrigin, + renderer.filledAmount, + renderer.filledClockWise + ); + break; + case SpriteFilledMode.Radial360: + this._filledRadial360( + renderer, + modelMatrix, + renderer.filledOrigin, + renderer.filledAmount, + renderer.filledClockWise + ); + break; + default: + break; + } + + // @ts-ignore + BoundingBox.transform(renderer.sprite._getBounds(), modelMatrix, renderer._bounds); + } + + static updateUVs(renderer: ISpriteRenderer): void { + // UVs are computed in updatePositions. + } + + static updateColor(renderer: ISpriteRenderer, alpha: number): void { + const subChunk = renderer._subChunk; + const { r, g, b, a } = renderer.color; + const finalAlpha = a * alpha; + const vertices = subChunk.chunk.vertices; + const vertexArea = subChunk.vertexArea; + for (let i = 0, o = vertexArea.start + 5, n = vertexArea.size / 9; i < n; ++i, o += 9) { + vertices[o] = r; + vertices[o + 1] = g; + vertices[o + 2] = b; + vertices[o + 3] = finalAlpha; + } + } + + private static _filledLinear(renderer: ISpriteRenderer, matrix: Matrix, isHorizontal: boolean): void { + const amount = renderer.filledAmount; + if (amount <= 0.001) { + renderer._subChunk.indices.length = 0; + return; + } + + const sprite = renderer.sprite; + const [lPosLB, lPosRB, lPosLT, lPosRT] = sprite._getPositions(); + const spriteUVs = sprite._getUVs(); + const { x: left, y: bottom } = spriteUVs[0]; + const { x: right, y: top } = spriteUVs[15]; + + const subChunk = renderer._subChunk; + const vertices = subChunk.chunk.vertices; + + let x0: number, y0: number, u0: number, v0: number; + let x1: number, y1: number, u1: number, v1: number; + let x2: number, y2: number, u2: number, v2: number; + let x3: number, y3: number, u3: number, v3: number; + + if (isHorizontal) { + const originIsStart = renderer.filledOrigin === SpriteFilledOrigin.Left; + const startX = originIsStart ? lPosLB.x : lPosRB.x - (lPosRB.x - lPosLB.x) * amount; + const endX = originIsStart ? lPosLB.x + (lPosRB.x - lPosLB.x) * amount : lPosRB.x; + const startU = originIsStart ? left : right - (right - left) * amount; + const endU = originIsStart ? left + (right - left) * amount : right; + (x0 = startX), (y0 = lPosLB.y), (u0 = startU), (v0 = bottom); + (x1 = endX), (y1 = lPosRB.y), (u1 = endU), (v1 = bottom); + (x2 = startX), (y2 = lPosLT.y), (u2 = startU), (v2 = top); + (x3 = endX), (y3 = lPosRT.y), (u3 = endU), (v3 = top); + } else { + const originIsStart = renderer.filledOrigin === SpriteFilledOrigin.Bottom; + const startY = originIsStart ? lPosLB.y : lPosLT.y - (lPosLT.y - lPosLB.y) * amount; + const endY = originIsStart ? lPosLB.y + (lPosLT.y - lPosLB.y) * amount : lPosLT.y; + const startV = originIsStart ? bottom : top - (top - bottom) * amount; + const endV = originIsStart ? bottom + (top - bottom) * amount : top; + (x0 = lPosLB.x), (y0 = startY), (u0 = left), (v0 = startV); + (x1 = lPosRB.x), (y1 = startY), (u1 = right), (v1 = startV); + (x2 = lPosLT.x), (y2 = endY), (u2 = left), (v2 = endV); + (x3 = lPosRT.x), (y3 = endY), (u3 = right), (v3 = endV); + } + + const { elements: wE } = matrix; + const start = subChunk.vertexArea.start; + // LB + vertices[start] = wE[0] * x0 + wE[4] * y0 + wE[12]; + vertices[start + 1] = wE[1] * x0 + wE[5] * y0 + wE[13]; + vertices[start + 2] = wE[2] * x0 + wE[6] * y0 + wE[14]; + vertices[start + 3] = u0; + vertices[start + 4] = v0; + // RB + vertices[start + 9] = wE[0] * x1 + wE[4] * y1 + wE[12]; + vertices[start + 10] = wE[1] * x1 + wE[5] * y1 + wE[13]; + vertices[start + 11] = wE[2] * x1 + wE[6] * y1 + wE[14]; + vertices[start + 12] = u1; + vertices[start + 13] = v1; + // LT + vertices[start + 18] = wE[0] * x2 + wE[4] * y2 + wE[12]; + vertices[start + 19] = wE[1] * x2 + wE[5] * y2 + wE[13]; + vertices[start + 20] = wE[2] * x2 + wE[6] * y2 + wE[14]; + vertices[start + 21] = u2; + vertices[start + 22] = v2; + // RT + vertices[start + 27] = wE[0] * x3 + wE[4] * y3 + wE[12]; + vertices[start + 28] = wE[1] * x3 + wE[5] * y3 + wE[13]; + vertices[start + 29] = wE[2] * x3 + wE[6] * y3 + wE[14]; + vertices[start + 30] = u3; + vertices[start + 31] = v3; + + const indices = subChunk.indices; + indices[0] = 0; + indices[1] = 1; + indices[2] = 2; + indices[3] = 2; + indices[4] = 1; + indices[5] = 3; + indices.length = 6; + } + + private static _filledRadial90( + renderer: ISpriteRenderer, + matrix: Matrix, + origin: SpriteFilledOrigin, + amount: number, + cw: boolean + ): void { + if (amount <= 0.001) { + renderer._subChunk.indices.length = 0; + return; + } + + const sprite = renderer.sprite; + const [lPosLB, lPosRB, lPosLT, lPosRT] = sprite._getPositions(); + const spriteUVs = sprite._getUVs(); + const { x: left, y: bottom } = spriteUVs[0]; + const { x: right, y: top } = spriteUVs[15]; + + // Transform 4 corners to world space + const [wLB, wRB, wLT, wRT] = this._worldPositions; + const [uvLB, uvRB, uvLT, uvRT] = this._uvs; + wLB.set(lPosLB.x, lPosLB.y, 0).transformToVec3(matrix), uvLB.set(left, bottom); + wRB.set(lPosRB.x, lPosRB.y, 0).transformToVec3(matrix), uvRB.set(right, bottom); + wLT.set(lPosLT.x, lPosLT.y, 0).transformToVec3(matrix), uvLT.set(left, top); + wRT.set(lPosRT.x, lPosRT.y, 0).transformToVec3(matrix), uvRT.set(right, top); + + // Map vertices based on origin corner: + // [center, CW-adjacent, CCW-adjacent, opposite] + const { _inPositions: inPositions, _inUVs: inUVs, _outPositions: outPositions, _outUVs: outUVs } = this; + switch (origin) { + case SpriteFilledOrigin.BottomLeft: + (inPositions[0] = wLB), (inUVs[0] = uvLB); + (inPositions[1] = wRB), (inUVs[1] = uvRB); + (inPositions[2] = wLT), (inUVs[2] = uvLT); + (inPositions[3] = wRT), (inUVs[3] = uvRT); + break; + case SpriteFilledOrigin.BottomRight: + (inPositions[0] = wRB), (inUVs[0] = uvRB); + (inPositions[1] = wRT), (inUVs[1] = uvRT); + (inPositions[2] = wLB), (inUVs[2] = uvLB); + (inPositions[3] = wLT), (inUVs[3] = uvLT); + break; + case SpriteFilledOrigin.TopRight: + (inPositions[0] = wRT), (inUVs[0] = uvRT); + (inPositions[1] = wLT), (inUVs[1] = uvLT); + (inPositions[2] = wRB), (inUVs[2] = uvRB); + (inPositions[3] = wLB), (inUVs[3] = uvLB); + break; + case SpriteFilledOrigin.TopLeft: + (inPositions[0] = wLT), (inUVs[0] = uvLT); + (inPositions[1] = wLB), (inUVs[1] = uvLB); + (inPositions[2] = wRT), (inUVs[2] = uvRT); + (inPositions[3] = wRB), (inUVs[3] = uvRB); + break; + default: + break; + } + + const startAngle = cw ? 90 - amount * 90 : 0; + const endAngle = cw ? 90 : amount * 90; + + this._vertexOffset = this._indicesOffset = 0; + this._radialCut(renderer._subChunk, inPositions, inUVs, startAngle, endAngle, outPositions, outUVs); + renderer._subChunk.indices.length = this._indicesOffset; + } + + private static _filledRadial180( + renderer: ISpriteRenderer, + matrix: Matrix, + origin: SpriteFilledOrigin, + amount: number, + cw: boolean + ): void { + if (amount <= 0.001) { + renderer._subChunk.indices.length = 0; + return; + } + + const sprite = renderer.sprite; + const [lPosLB, lPosRB, lPosLT, lPosRT] = sprite._getPositions(); + const spriteUVs = sprite._getUVs(); + const { x: left, y: bottom } = spriteUVs[0]; + const { x: right, y: top } = spriteUVs[15]; + + // Transform corners and compute edge midpoints + const [wLB, wMB, wRB, wLM, , wRM, wLT, wMT, wRT] = this._worldPositions; + const [uvLB, uvMB, uvRB, uvLM, , uvRM, uvLT, uvMT, uvRT] = this._uvs; + wLB.set(lPosLB.x, lPosLB.y, 0).transformToVec3(matrix), uvLB.set(left, bottom); + wRB.set(lPosRB.x, lPosRB.y, 0).transformToVec3(matrix), uvRB.set(right, bottom); + wLT.set(lPosLT.x, lPosLT.y, 0).transformToVec3(matrix), uvLT.set(left, top); + wRT.set(lPosRT.x, lPosRT.y, 0).transformToVec3(matrix), uvRT.set(right, top); + Vector3.lerp(wLB, wRB, 0.5, wMB), Vector2.lerp(uvLB, uvRB, 0.5, uvMB); + Vector3.lerp(wLB, wLT, 0.5, wLM), Vector2.lerp(uvLB, uvLT, 0.5, uvLM); + Vector3.lerp(wLT, wRT, 0.5, wMT), Vector2.lerp(uvLT, uvRT, 0.5, uvMT); + Vector3.lerp(wRB, wRT, 0.5, wRM), Vector2.lerp(uvRB, uvRT, 0.5, uvRM); + + const startAngle = cw ? 180 - amount * 180 : 0; + const endAngle = cw ? 180 : amount * 180; + + this._vertexOffset = this._indicesOffset = 0; + const { _inPositions: inPositions, _inUVs: inUVs, _outPositions: outPositions, _outUVs: outUVs } = this; + const { _subChunk: subChunk } = renderer; + + // Center is at the origin edge midpoint; two quadrants cover the full sprite. + // Quadrant A (0°-90°), Quadrant B (90°-180°) + switch (origin) { + case SpriteFilledOrigin.Bottom: + // Center=MB, A: [MB,RB,MT,RT], B: [MB,MT,LB,LT] + (inPositions[0] = wMB), (inUVs[0] = uvMB); + (inPositions[1] = wRB), (inUVs[1] = uvRB); + (inPositions[2] = wMT), (inUVs[2] = uvMT); + (inPositions[3] = wRT), (inUVs[3] = uvRT); + this._radialCut(subChunk, inPositions, inUVs, startAngle, endAngle, outPositions, outUVs); + (inPositions[1] = wMT), (inUVs[1] = uvMT); + (inPositions[2] = wLB), (inUVs[2] = uvLB); + (inPositions[3] = wLT), (inUVs[3] = uvLT); + this._radialCut(subChunk, inPositions, inUVs, startAngle - 90, endAngle - 90, outPositions, outUVs); + break; + case SpriteFilledOrigin.Top: + // Center=MT, A: [MT,LT,MB,LB], B: [MT,MB,RT,RB] + (inPositions[0] = wMT), (inUVs[0] = uvMT); + (inPositions[1] = wLT), (inUVs[1] = uvLT); + (inPositions[2] = wMB), (inUVs[2] = uvMB); + (inPositions[3] = wLB), (inUVs[3] = uvLB); + this._radialCut(subChunk, inPositions, inUVs, startAngle, endAngle, outPositions, outUVs); + (inPositions[1] = wMB), (inUVs[1] = uvMB); + (inPositions[2] = wRT), (inUVs[2] = uvRT); + (inPositions[3] = wRB), (inUVs[3] = uvRB); + this._radialCut(subChunk, inPositions, inUVs, startAngle - 90, endAngle - 90, outPositions, outUVs); + break; + case SpriteFilledOrigin.Left: + // Center=LM, A: [LM,LB,RM,RB], B: [LM,RM,LT,RT] + (inPositions[0] = wLM), (inUVs[0] = uvLM); + (inPositions[1] = wLB), (inUVs[1] = uvLB); + (inPositions[2] = wRM), (inUVs[2] = uvRM); + (inPositions[3] = wRB), (inUVs[3] = uvRB); + this._radialCut(subChunk, inPositions, inUVs, startAngle, endAngle, outPositions, outUVs); + (inPositions[1] = wRM), (inUVs[1] = uvRM); + (inPositions[2] = wLT), (inUVs[2] = uvLT); + (inPositions[3] = wRT), (inUVs[3] = uvRT); + this._radialCut(subChunk, inPositions, inUVs, startAngle - 90, endAngle - 90, outPositions, outUVs); + break; + case SpriteFilledOrigin.Right: + // Center=RM, A: [RM,RT,LM,LT], B: [RM,LM,RB,LB] + (inPositions[0] = wRM), (inUVs[0] = uvRM); + (inPositions[1] = wRT), (inUVs[1] = uvRT); + (inPositions[2] = wLM), (inUVs[2] = uvLM); + (inPositions[3] = wLT), (inUVs[3] = uvLT); + this._radialCut(subChunk, inPositions, inUVs, startAngle, endAngle, outPositions, outUVs); + (inPositions[1] = wLM), (inUVs[1] = uvLM); + (inPositions[2] = wRB), (inUVs[2] = uvRB); + (inPositions[3] = wLB), (inUVs[3] = uvLB); + this._radialCut(subChunk, inPositions, inUVs, startAngle - 90, endAngle - 90, outPositions, outUVs); + break; + default: + break; + } + + subChunk.indices.length = this._indicesOffset; + } + + private static _filledRadial360( + renderer: ISpriteRenderer, + matrix: Matrix, + origin: SpriteFilledOrigin, + amount: number, + cw: boolean + ): void { + if (amount <= 0.001) { + renderer._subChunk.indices.length = 0; + return; + } + + let startAngle = 0; + switch (origin) { + case SpriteFilledOrigin.Right: + startAngle = cw ? 360 - amount * 360 : 0; + break; + case SpriteFilledOrigin.Top: + startAngle = cw ? 450 - amount * 360 : 90; + break; + case SpriteFilledOrigin.Left: + startAngle = cw ? 540 - amount * 360 : 180; + break; + case SpriteFilledOrigin.Bottom: + startAngle = cw ? 630 - amount * 360 : 270; + break; + default: + break; + } + const endAngle = startAngle + amount * 360; + + this._processRadialGrid(renderer, matrix, startAngle, endAngle); + } + + /** + * Prepare the 3x3 grid and process 4 quadrants for radial fill. + */ + private static _processRadialGrid( + renderer: ISpriteRenderer, + matrix: Matrix, + startAngle: number, + endAngle: number + ): void { + const sprite = renderer.sprite; + const [lPosLB, lPosRB, lPosLT, lPosRT] = sprite._getPositions(); + const spriteUVs = sprite._getUVs(); + const { x: left, y: bottom } = spriteUVs[0]; + const { x: right, y: top } = spriteUVs[15]; + + // --------------- + // LT - MT - RT + // | | | + // LM - C - RM + // | | | + // LB - MB - RB + // --------------- + const [wPosLB, wPosMB, wPosRB, wPosLM, wPosC, wPosRM, wPosLT, wPosMT, wPosRT] = this._worldPositions; + const [uvLB, uvMB, uvRB, uvLM, uvC, uvRM, uvLT, uvMT, uvRT] = this._uvs; + + wPosLB.set(lPosLB.x, lPosLB.y, 0).transformToVec3(matrix), uvLB.set(left, bottom); + wPosRB.set(lPosRB.x, lPosRB.y, 0).transformToVec3(matrix), uvRB.set(right, bottom); + wPosLT.set(lPosLT.x, lPosLT.y, 0).transformToVec3(matrix), uvLT.set(left, top); + wPosRT.set(lPosRT.x, lPosRT.y, 0).transformToVec3(matrix), uvRT.set(right, top); + Vector3.lerp(wPosLB, wPosRB, 0.5, wPosMB), Vector2.lerp(uvLB, uvRB, 0.5, uvMB); + Vector3.lerp(wPosLB, wPosLT, 0.5, wPosLM), Vector2.lerp(uvLB, uvLT, 0.5, uvLM); + Vector3.lerp(wPosLT, wPosRT, 0.5, wPosMT), Vector2.lerp(uvLT, uvRT, 0.5, uvMT); + Vector3.lerp(wPosRB, wPosRT, 0.5, wPosRM), Vector2.lerp(uvRB, uvRT, 0.5, uvRM); + Vector3.lerp(wPosLB, wPosRT, 0.5, wPosC), Vector2.lerp(uvLB, uvRT, 0.5, uvC); + + this._vertexOffset = this._indicesOffset = 0; + const { _inPositions: inPositions, _inUVs: inUVs, _outPositions: outPositions, _outUVs: outUVs } = this; + const { _subChunk: subChunk } = renderer; + let quadrantStart = 0; + let quadrantEnd = 0; + (inPositions[0] = wPosC), (inUVs[0] = uvC); + + { + // First quadrant (0°-90°) + if (startAngle >= 90) { + quadrantStart = startAngle - 360; + quadrantEnd = endAngle - 360; + } else { + quadrantStart = startAngle; + quadrantEnd = endAngle; + } + (inPositions[1] = wPosRM), (inUVs[1] = uvRM); + (inPositions[2] = wPosMT), (inUVs[2] = uvMT); + (inPositions[3] = wPosRT), (inUVs[3] = uvRT); + this._radialCut(subChunk, inPositions, inUVs, quadrantStart, quadrantEnd, outPositions, outUVs); + } + + { + // Second quadrant (90°-180°) + if (startAngle >= 180) { + quadrantStart = startAngle - 360 - 90; + quadrantEnd = endAngle - 360 - 90; + } else { + quadrantStart = startAngle - 90; + quadrantEnd = endAngle - 90; + } + (inPositions[1] = wPosMT), (inUVs[1] = uvMT); + (inPositions[2] = wPosLM), (inUVs[2] = uvLM); + (inPositions[3] = wPosLT), (inUVs[3] = uvLT); + this._radialCut(subChunk, inPositions, inUVs, quadrantStart, quadrantEnd, outPositions, outUVs); + } + + { + // Third quadrant (180°-270°) + if (startAngle >= 270) { + quadrantStart = startAngle - 360 - 180; + quadrantEnd = endAngle - 360 - 180; + } else { + quadrantStart = startAngle - 180; + quadrantEnd = endAngle - 180; + } + (inPositions[1] = wPosLM), (inUVs[1] = uvLM); + (inPositions[2] = wPosMB), (inUVs[2] = uvMB); + (inPositions[3] = wPosLB), (inUVs[3] = uvLB); + this._radialCut(subChunk, inPositions, inUVs, quadrantStart, quadrantEnd, outPositions, outUVs); + } + + { + // Fourth quadrant (270°-360°) + if (startAngle >= 360) { + quadrantStart = startAngle - 360 - 270; + quadrantEnd = endAngle - 360 - 270; + } else { + quadrantStart = startAngle - 270; + quadrantEnd = endAngle - 270; + } + (inPositions[1] = wPosMB), (inUVs[1] = uvMB); + (inPositions[2] = wPosRM), (inUVs[2] = uvRM); + (inPositions[3] = wPosRB), (inUVs[3] = uvRB); + this._radialCut(subChunk, inPositions, inUVs, quadrantStart, quadrantEnd, outPositions, outUVs); + } + + subChunk.indices.length = this._indicesOffset; + } + + private static _radialCut( + subChunk: SubPrimitiveChunk, + positions: Vector3[], + uvs: Vector2[], + start: number, + end: number, + outPositions: Vector3[], + outUVs: Vector2[] + ): void { + if (start >= 90 || end <= 0) return; + outPositions[0].copyFrom(positions[0]); + outUVs[0].copyFrom(uvs[0]); + + if (start <= 0) { + outPositions[1].copyFrom(positions[1]); + outUVs[1].copyFrom(uvs[1]); + } else { + const startTan = Math.tan((start * Math.PI) / 180); + if (startTan < 1) { + Vector3.lerp(positions[1], positions[3], startTan, outPositions[1]); + Vector2.lerp(uvs[1], uvs[3], startTan, outUVs[1]); + } else { + Vector3.lerp(positions[2], positions[3], 1 / startTan, outPositions[1]); + Vector2.lerp(uvs[2], uvs[3], 1 / startTan, outUVs[1]); + } + } + + if (end >= 90) { + outPositions[2].copyFrom(positions[2]); + outUVs[2].copyFrom(uvs[2]); + } else { + const endTan = Math.tan((end * Math.PI) / 180); + if (endTan < 1) { + Vector3.lerp(positions[1], positions[3], endTan, outPositions[2]); + Vector2.lerp(uvs[1], uvs[3], endTan, outUVs[2]); + } else { + Vector3.lerp(positions[2], positions[3], 1 / endTan, outPositions[2]); + Vector2.lerp(uvs[2], uvs[3], 1 / endTan, outUVs[2]); + } + } + + if (start < 45 && end > 45) { + outPositions[3].copyFrom(positions[3]); + outUVs[3].copyFrom(uvs[3]); + this._addQuad(subChunk, outPositions, outUVs); + } else { + this._addTriangle(subChunk, outPositions, outUVs); + } + } + + private static _addTriangle(subChunk: SubPrimitiveChunk, positions: Vector3[], uvs: Vector2[]): void { + const vertices = subChunk.chunk.vertices; + const indices = subChunk.indices; + const vertexOffset = this._vertexOffset; + const vertexCount = vertexOffset / 9; + const start = subChunk.vertexArea.start + vertexOffset; + for (let i = 0, o = start; i < 3; ++i, o += 9) { + const position = positions[i]; + const uv = uvs[i]; + vertices[o] = position.x; + vertices[o + 1] = position.y; + vertices[o + 2] = position.z; + vertices[o + 3] = uv.x; + vertices[o + 4] = uv.y; + } + const indicesOffset = this._indicesOffset; + indices[indicesOffset] = vertexCount; + indices[indicesOffset + 1] = vertexCount + 1; + indices[indicesOffset + 2] = vertexCount + 2; + this._vertexOffset += 3 * 9; + this._indicesOffset += 3; + } + + private static _addQuad(subChunk: SubPrimitiveChunk, positions: Vector3[], uvs: Vector2[]): void { + const vertices = subChunk.chunk.vertices; + const indices = subChunk.indices; + const vertexOffset = this._vertexOffset; + const vertexCount = vertexOffset / 9; + const start = subChunk.vertexArea.start + vertexOffset; + for (let i = 0, o = start; i < 4; ++i, o += 9) { + const position = positions[i]; + const uv = uvs[i]; + vertices[o] = position.x; + vertices[o + 1] = position.y; + vertices[o + 2] = position.z; + vertices[o + 3] = uv.x; + vertices[o + 4] = uv.y; + } + const indicesOffset = this._indicesOffset; + indices[indicesOffset] = vertexCount; + indices[indicesOffset + 1] = vertexCount + 1; + indices[indicesOffset + 2] = vertexCount + 2; + indices[indicesOffset + 3] = vertexCount + 2; + indices[indicesOffset + 4] = vertexCount + 1; + indices[indicesOffset + 5] = vertexCount + 3; + this._vertexOffset += 4 * 9; + this._indicesOffset += 6; + } +} diff --git a/packages/core/src/2d/assembler/ISpriteRenderer.ts b/packages/core/src/2d/assembler/ISpriteRenderer.ts index a72f4e9436..04b50b3fc9 100644 --- a/packages/core/src/2d/assembler/ISpriteRenderer.ts +++ b/packages/core/src/2d/assembler/ISpriteRenderer.ts @@ -1,6 +1,8 @@ import { Color } from "@galacean/engine-math"; import { PrimitiveChunkManager } from "../../RenderPipeline/PrimitiveChunkManager"; import { SubPrimitiveChunk } from "../../RenderPipeline/SubPrimitiveChunk"; +import { SpriteFilledMode } from "../enums/SpriteFilledMode"; +import { SpriteFilledOrigin } from "../enums/SpriteFilledOrigin"; import { SpriteTileMode } from "../enums/SpriteTileMode"; import { Sprite } from "../sprite"; @@ -12,6 +14,10 @@ export interface ISpriteRenderer { color?: Color; tileMode?: SpriteTileMode; tiledAdaptiveThreshold?: number; + filledMode?: SpriteFilledMode; + filledAmount?: number; + filledOrigin?: SpriteFilledOrigin; + filledClockWise?: boolean; _subChunk: SubPrimitiveChunk; _getChunkManager(): PrimitiveChunkManager; } diff --git a/packages/core/src/2d/assembler/SimpleSpriteAssembler.ts b/packages/core/src/2d/assembler/SimpleSpriteAssembler.ts index 563f812106..6e0e07c368 100644 --- a/packages/core/src/2d/assembler/SimpleSpriteAssembler.ts +++ b/packages/core/src/2d/assembler/SimpleSpriteAssembler.ts @@ -66,20 +66,25 @@ export class SimpleSpriteAssembler { } static updateUVs(renderer: ISpriteRenderer): void { + // sprite._uvs 16 个网格点 (column-major, index=column*4+row),4 corner: [0]=LB / [3]=LT / [12]=RB / [15]=RT + // atlasRotated 由 sprite._updateUVs 内部处理,此处直接用结果,无需关心朝向 const spriteUVs = renderer.sprite._getUVs(); - const { x: left, y: bottom } = spriteUVs[0]; - const { x: right, y: top } = spriteUVs[3]; const subChunk = renderer._subChunk; const vertices = subChunk.chunk.vertices; const offset = subChunk.vertexArea.start + 3; - vertices[offset] = left; - vertices[offset + 1] = bottom; - vertices[offset + 9] = right; - vertices[offset + 10] = bottom; - vertices[offset + 18] = left; - vertices[offset + 19] = top; - vertices[offset + 27] = right; - vertices[offset + 28] = top; + const uvLB = spriteUVs[0]; + const uvLT = spriteUVs[3]; + const uvRB = spriteUVs[12]; + const uvRT = spriteUVs[15]; + // SimpleAssembler 的 vertex 顺序:0=LB, 1=RB, 2=LT, 3=RT (按 _rectangleTriangles 的拓扑) + vertices[offset] = uvLB.x; + vertices[offset + 1] = uvLB.y; + vertices[offset + 9] = uvRB.x; + vertices[offset + 10] = uvRB.y; + vertices[offset + 18] = uvLT.x; + vertices[offset + 19] = uvLT.y; + vertices[offset + 27] = uvRT.x; + vertices[offset + 28] = uvRT.y; } static updateColor(renderer: ISpriteRenderer, alpha: number): void { diff --git a/packages/core/src/2d/assembler/SlicedSpriteAssembler.ts b/packages/core/src/2d/assembler/SlicedSpriteAssembler.ts index 31d19d149c..09abce5be3 100644 --- a/packages/core/src/2d/assembler/SlicedSpriteAssembler.ts +++ b/packages/core/src/2d/assembler/SlicedSpriteAssembler.ts @@ -126,14 +126,15 @@ export class SlicedSpriteAssembler { } static updateUVs(renderer: ISpriteRenderer): void { + // 16 UV 网格 (column-major: index = i*4+j, i=column, j=row),与 vertex 索引一一对应 const subChunk = renderer._subChunk; const vertices = subChunk.chunk.vertices; const spriteUVs = renderer.sprite._getUVs(); for (let i = 0, o = subChunk.vertexArea.start + 3; i < 4; i++) { - const rowU = spriteUVs[i].x; for (let j = 0; j < 4; j++, o += 9) { - vertices[o] = rowU; - vertices[o + 1] = spriteUVs[j].y; + const uv = spriteUVs[i * 4 + j]; + vertices[o] = uv.x; + vertices[o + 1] = uv.y; } } } diff --git a/packages/core/src/2d/assembler/TiledSpriteAssembler.ts b/packages/core/src/2d/assembler/TiledSpriteAssembler.ts index bc765c45f9..448d0543a4 100644 --- a/packages/core/src/2d/assembler/TiledSpriteAssembler.ts +++ b/packages/core/src/2d/assembler/TiledSpriteAssembler.ts @@ -188,7 +188,12 @@ export class TiledSpriteAssembler { const spritePositions = sprite._getPositions(); const { x: left, y: bottom } = spritePositions[0]; const { x: right, y: top } = spritePositions[3]; - const [spriteUV0, spriteUV1, spriteUV2, spriteUV3] = sprite._getUVs(); + // 16 UV column-major: [0]=LB(left,bottom), [5]=border-LB(bLeft,bBottom), [10]=border-RT(bRight,bTop), [15]=RT(right,top) + const allUVs = sprite._getUVs(); + const spriteUV0 = allUVs[0]; + const spriteUV1 = allUVs[5]; + const spriteUV2 = allUVs[10]; + const spriteUV3 = allUVs[15]; const expectWidth = sprite.width * referenceResolutionPerUnit; const expectHeight = sprite.height * referenceResolutionPerUnit; const fixedL = expectWidth * border.x; diff --git a/packages/core/src/2d/atlas/FontAtlas.ts b/packages/core/src/2d/atlas/FontAtlas.ts index 36170f2f49..102c9620cc 100644 --- a/packages/core/src/2d/atlas/FontAtlas.ts +++ b/packages/core/src/2d/atlas/FontAtlas.ts @@ -12,10 +12,10 @@ export class FontAtlas extends ReferResource { texture: Texture2D; _charInfoMap: Record = {}; - private _space: number = 1; - private _curX: number = 1; - private _curY: number = 1; - private _nextY: number = 1; + private _space: number = 4; + private _curX: number = 4; + private _curY: number = 4; + private _nextY: number = 4; constructor(engine: Engine) { super(engine); diff --git a/packages/core/src/2d/atlas/SpriteAtlas.ts b/packages/core/src/2d/atlas/SpriteAtlas.ts index 44adadbfdd..93828efe2e 100644 --- a/packages/core/src/2d/atlas/SpriteAtlas.ts +++ b/packages/core/src/2d/atlas/SpriteAtlas.ts @@ -45,7 +45,7 @@ export class SpriteAtlas extends ReferResource { sprite.name === name && outSprites.push(sprite); } } else { - console.warn("The name of the sprite you want to find is not exit in SpriteAtlas."); + console.warn("There is no sprite named " + name + " in the atlas."); } return outSprites; } diff --git a/packages/core/src/2d/enums/SpriteDrawMode.ts b/packages/core/src/2d/enums/SpriteDrawMode.ts index 46bcfd3783..6f35d8c1ef 100644 --- a/packages/core/src/2d/enums/SpriteDrawMode.ts +++ b/packages/core/src/2d/enums/SpriteDrawMode.ts @@ -7,5 +7,7 @@ export enum SpriteDrawMode { /** When modifying the size of the renderer, it scales to fill the range according to the sprite border settings. */ Sliced, /** When modifying the size of the renderer, it will tile to fill the range according to the sprite border settings. */ - Tiled + Tiled, + /** Fill the sprite partially, controlled by fill amount, mode and origin. */ + Filled } diff --git a/packages/core/src/2d/enums/SpriteFilledMode.ts b/packages/core/src/2d/enums/SpriteFilledMode.ts new file mode 100644 index 0000000000..a01cda9e53 --- /dev/null +++ b/packages/core/src/2d/enums/SpriteFilledMode.ts @@ -0,0 +1,15 @@ +/** + * Sprite's filled mode enumeration. + */ +export enum SpriteFilledMode { + /** Fill horizontally. */ + Horizontal, + /** Fill vertically. */ + Vertical, + /** Fill radially over 90 degrees. */ + Radial90, + /** Fill radially over 180 degrees. */ + Radial180, + /** Fill radially over 360 degrees. */ + Radial360 +} diff --git a/packages/core/src/2d/enums/SpriteFilledOrigin.ts b/packages/core/src/2d/enums/SpriteFilledOrigin.ts new file mode 100644 index 0000000000..e9b3193970 --- /dev/null +++ b/packages/core/src/2d/enums/SpriteFilledOrigin.ts @@ -0,0 +1,21 @@ +/** + * Sprite's filled origin enumeration. + */ +export enum SpriteFilledOrigin { + /** Origin at the right. */ + Right, + /** Origin at the top-right. */ + TopRight, + /** Origin at the top. */ + Top, + /** Origin at the top-left. */ + TopLeft, + /** Origin at the left. */ + Left, + /** Origin at the bottom-left. */ + BottomLeft, + /** Origin at the bottom. */ + Bottom, + /** Origin at the bottom-right. */ + BottomRight +} diff --git a/packages/core/src/2d/enums/TextOverflow.ts b/packages/core/src/2d/enums/TextOverflow.ts index adef863f1f..bf8fdf8aa4 100644 --- a/packages/core/src/2d/enums/TextOverflow.ts +++ b/packages/core/src/2d/enums/TextOverflow.ts @@ -1,9 +1,11 @@ /** - * The way to handle the situation where wrapped text is too tall to fit in the height. + * The way to handle the situation where the text is too large to fit in the bounds. */ export enum OverflowMode { /** Overflow when the text is too tall */ Overflow = 0, /** Truncate with height when the text is too tall */ - Truncate = 1 + Truncate = 1, + /** Shrink the font size until the text fits within the bounds (both width and height) */ + Shrink = 2 } diff --git a/packages/core/src/2d/index.ts b/packages/core/src/2d/index.ts index 47be64ccfc..5481f42b28 100644 --- a/packages/core/src/2d/index.ts +++ b/packages/core/src/2d/index.ts @@ -1,11 +1,14 @@ export type { ISpriteAssembler } from "./assembler/ISpriteAssembler"; export type { ISpriteRenderer } from "./assembler/ISpriteRenderer"; +export { FilledSpriteAssembler } from "./assembler/FilledSpriteAssembler"; export { SimpleSpriteAssembler } from "./assembler/SimpleSpriteAssembler"; export { SlicedSpriteAssembler } from "./assembler/SlicedSpriteAssembler"; export { TiledSpriteAssembler } from "./assembler/TiledSpriteAssembler"; export { SpriteAtlas } from "./atlas/SpriteAtlas"; export { FontStyle } from "./enums/FontStyle"; export { SpriteDrawMode } from "./enums/SpriteDrawMode"; +export { SpriteFilledMode } from "./enums/SpriteFilledMode"; +export { SpriteFilledOrigin } from "./enums/SpriteFilledOrigin"; export { SpriteMaskInteraction } from "./enums/SpriteMaskInteraction"; export { SpriteModifyFlags } from "./enums/SpriteModifyFlags"; export { SpriteTileMode } from "./enums/SpriteTileMode"; diff --git a/packages/core/src/2d/sprite/MaskRenderable.ts b/packages/core/src/2d/sprite/MaskRenderable.ts new file mode 100644 index 0000000000..441492ba85 --- /dev/null +++ b/packages/core/src/2d/sprite/MaskRenderable.ts @@ -0,0 +1,398 @@ +import { BoundingBox, Vector2, Vector3 } from "@galacean/engine-math"; +import { RenderElement } from "../../RenderPipeline/RenderElement"; +import { VertexMergeBatcher } from "../../RenderPipeline/VertexMergeBatcher"; +import { Renderer, RendererUpdateFlags } from "../../Renderer"; +import { assignmentClone, ignoreClone } from "../../clone/CloneManager"; +import { SpriteMaskLayer } from "../../enums/SpriteMaskLayer"; +import { ShaderProperty } from "../../shader/ShaderProperty"; +import type { ISpriteRenderer } from "../assembler/ISpriteRenderer"; +import { SimpleSpriteAssembler } from "../assembler/SimpleSpriteAssembler"; +import { SpriteModifyFlags } from "../enums/SpriteModifyFlags"; +import { Sprite } from "./Sprite"; +import { SpriteMaskUtils } from "./SpriteMaskUtils"; + +/** + * Public contract of the MaskRenderable mixin, used for declaration file generation. + */ +export interface IMaskRenderable { + influenceLayers: SpriteMaskLayer; + flipX: boolean; + flipY: boolean; + sprite: Sprite; + alphaCutoff: number; + _renderElement: RenderElement; + _maskIndex: number; + _containsWorldPoint(worldPoint: Vector3): boolean; + _initMask(): void; + _cloneMaskData(target: IMaskRenderable): void; + _destroyMaskResources(): void; + _updateMaskBounds(worldBounds: BoundingBox): void; + _renderMask(distanceForSort: number): void; + _onSpriteChange(type: SpriteModifyFlags): void; + _onSpriteChangeExtra(type: SpriteModifyFlags): void; + _getSpriteWidth(): number; + _getSpriteHeight(): number; + _getSpritePivot(): Vector2; +} + +type RendererConstructor = abstract new (...args: any[]) => Renderer; + +/** + * Mixin that provides shared mask rendering logic for both 2D SpriteMask and UI Mask. + */ +export function MaskRenderable( + Base: T +): (abstract new (...args: any[]) => IMaskRenderable) & T { + abstract class MaskRenderableBase extends Base implements IMaskRenderable { + private static _maskTextureProperty = ShaderProperty.getByName("renderer_MaskTexture"); + private static _alphaCutoffProperty = ShaderProperty.getByName("renderer_MaskAlphaCutoff"); + + @assignmentClone + private _influenceLayers: SpriteMaskLayer = SpriteMaskLayer.Everything; + /** @internal */ + @ignoreClone + _renderElement: RenderElement; + /** @internal */ + @ignoreClone + _maskIndex: number = -1; + @ignoreClone + private _sprite: Sprite = null; + @assignmentClone + private _flipX: boolean = false; + @assignmentClone + private _flipY: boolean = false; + @assignmentClone + private _alphaCutoff: number = 0.5; + + /** + * The mask layers the sprite mask influence to. + */ + get influenceLayers(): SpriteMaskLayer { + return this._influenceLayers; + } + + set influenceLayers(value: SpriteMaskLayer) { + if (this._influenceLayers !== value) { + this._influenceLayers = value; + // @ts-ignore + if (this._phasedActiveInScene) { + // @ts-ignore + this.scene._maskManager.onMaskInfluenceLayersChange(); + } + } + } + + /** + * Flips the sprite on the X axis. + */ + get flipX(): boolean { + return this._flipX; + } + + set flipX(value: boolean) { + if (this._flipX !== value) { + this._flipX = value; + // @ts-ignore + this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; + } + } + + /** + * Flips the sprite on the Y axis. + */ + get flipY(): boolean { + return this._flipY; + } + + set flipY(value: boolean) { + if (this._flipY !== value) { + this._flipY = value; + // @ts-ignore + this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; + } + } + + /** + * The Sprite to render. + */ + get sprite(): Sprite { + return this._sprite; + } + + set sprite(value: Sprite | null) { + const lastSprite = this._sprite; + if (lastSprite !== value) { + if (lastSprite) { + // @ts-ignore + this._addResourceReferCount(lastSprite, -1); + lastSprite._updateFlagManager.removeListener(this._onSpriteChange); + } + // @ts-ignore + this._dirtyUpdateFlag |= MaskDirtyFlags.All; + if (value) { + // @ts-ignore + this._addResourceReferCount(value, 1); + value._updateFlagManager.addListener(this._onSpriteChange); + // @ts-ignore + this.shaderData.setTexture(MaskRenderableBase._maskTextureProperty, value.texture); + } else { + // @ts-ignore + this.shaderData.setTexture(MaskRenderableBase._maskTextureProperty, null); + } + this._sprite = value; + } + } + + /** + * The minimum alpha value used by the mask to select the area of influence defined over the mask's sprite. Value between 0 and 1. + */ + get alphaCutoff(): number { + return this._alphaCutoff; + } + + set alphaCutoff(value: number) { + if (this._alphaCutoff !== value) { + this._alphaCutoff = value; + // @ts-ignore + this.shaderData.setFloat(MaskRenderableBase._alphaCutoffProperty, value); + } + } + + /** + * @internal + */ + // @ts-ignore + override _canBatch(preElement: RenderElement, curElement: RenderElement): boolean { + return VertexMergeBatcher.canBatchSpriteMask(preElement, curElement); + } + + /** + * @internal + */ + // @ts-ignore + override _batch(preElement: RenderElement | null, curElement: RenderElement): void { + VertexMergeBatcher.batch(preElement, curElement); + } + + /** + * @internal + */ + // @ts-ignore + override _onEnableInScene(): void { + // @ts-ignore + super._onEnableInScene(); + // @ts-ignore + this.scene._maskManager.addSpriteMask(this); + } + + /** + * @internal + */ + // @ts-ignore + override _onDisableInScene(): void { + // @ts-ignore + super._onDisableInScene(); + // @ts-ignore + this.scene._maskManager.removeSpriteMask(this); + } + + /** + * @internal + */ + _containsWorldPoint(worldPoint: Vector3): boolean { + return SpriteMaskUtils.containsWorldPoint( + worldPoint, + this._sprite, + // @ts-ignore + this._transformEntity.transform.worldMatrix, + this._getSpriteWidth(), + this._getSpriteHeight(), + this._getSpritePivot(), + this._flipX, + this._flipY, + this._alphaCutoff + ); + } + + /** + * @internal + * Initialize shared mask resources. Must be called from subclass constructor. + */ + _initMask(): void { + SimpleSpriteAssembler.resetData(this as unknown as ISpriteRenderer); + // @ts-ignore + this.setMaterial(this._engine._basicResources.spriteMaskDefaultMaterial); + // @ts-ignore + this.shaderData.setFloat(MaskRenderableBase._alphaCutoffProperty, this._alphaCutoff); + this._renderElement = new RenderElement(); + this._onSpriteChange = this._onSpriteChange.bind(this); + } + + /** + * @internal + * Clone mask data to target. Called from subclass _cloneTo. + */ + _cloneMaskData(target: MaskRenderableBase): void { + target.sprite = this._sprite; + } + + /** + * @internal + * Release mask sprite resources. Called from subclass _onDestroy. + */ + _destroyMaskResources(): void { + const sprite = this._sprite; + if (sprite) { + // @ts-ignore + this._addResourceReferCount(sprite, -1); + sprite._updateFlagManager.removeListener(this._onSpriteChange); + } + this._sprite = null; + this._renderElement = null; + } + + /** + * @internal + * Update bounds using SimpleSpriteAssembler directly. + */ + _updateMaskBounds(worldBounds: BoundingBox): void { + const sprite = this._sprite; + if (sprite) { + SimpleSpriteAssembler.updatePositions( + this as unknown as ISpriteRenderer, + // @ts-ignore + this._transformEntity.transform.worldMatrix, + this._getSpriteWidth(), + this._getSpriteHeight(), + this._getSpritePivot(), + this._flipX, + this._flipY + ); + } else { + // @ts-ignore + const { worldPosition } = this._transformEntity.transform; + worldBounds.min.copyFrom(worldPosition); + worldBounds.max.copyFrom(worldPosition); + } + } + + /** + * @internal + * Shared render logic for mask geometry. + */ + _renderMask(distanceForSort: number): void { + const { _sprite: sprite } = this; + const width = this._getSpriteWidth(); + const height = this._getSpriteHeight(); + if (!sprite?.texture || !width || !height) { + return; + } + + // @ts-ignore + let material = this.getMaterial(); + if (!material) { + return; + } + if (material.destroyed) { + // @ts-ignore + material = this._engine._basicResources.spriteMaskDefaultMaterial; + } + + // Update position + // @ts-ignore + if (this._dirtyUpdateFlag & RendererUpdateFlags.WorldVolume) { + SimpleSpriteAssembler.updatePositions( + this as unknown as ISpriteRenderer, + // @ts-ignore + this._transformEntity.transform.worldMatrix, + width, + height, + this._getSpritePivot(), + this._flipX, + this._flipY + ); + // @ts-ignore + this._dirtyUpdateFlag &= ~RendererUpdateFlags.WorldVolume; + } + + // Update uv + // @ts-ignore + if (this._dirtyUpdateFlag & MaskDirtyFlags.UV) { + SimpleSpriteAssembler.updateUVs(this as unknown as ISpriteRenderer); + // @ts-ignore + this._dirtyUpdateFlag &= ~MaskDirtyFlags.UV; + } + + const renderElement = this._renderElement; + const subChunk = (this as any)._subChunk; + // @ts-ignore + renderElement.set(this, material, subChunk.chunk.primitive, subChunk.subMesh, sprite.texture, subChunk); + // @ts-ignore + renderElement.priority = this.priority; + renderElement.distanceForSort = distanceForSort; + renderElement.subShader = material.shader.subShaders[0]; + } + + /** @internal */ + @ignoreClone + _onSpriteChange(type: SpriteModifyFlags): void { + switch (type) { + case SpriteModifyFlags.texture: + // @ts-ignore + this.shaderData.setTexture(MaskRenderableBase._maskTextureProperty, this.sprite.texture); + break; + case SpriteModifyFlags.region: + case SpriteModifyFlags.atlasRegionOffset: + // @ts-ignore + this._dirtyUpdateFlag |= MaskDirtyFlags.WorldVolumeAndUV; + break; + case SpriteModifyFlags.atlasRegion: + // @ts-ignore + this._dirtyUpdateFlag |= MaskDirtyFlags.UV; + break; + case SpriteModifyFlags.destroy: + this.sprite = null; + break; + default: + this._onSpriteChangeExtra(type); + break; + } + } + + /** + * @internal + * Hook for subclass-specific sprite change handling. + * SpriteMask overrides this to handle size/pivot changes. + */ + _onSpriteChangeExtra(type: SpriteModifyFlags): void {} + + /** @internal */ + _getSpriteWidth(): number { + return 0; + } + /** @internal */ + _getSpriteHeight(): number { + return 0; + } + /** @internal */ + _getSpritePivot(): Vector2 { + return null; + } + } + + return MaskRenderableBase as unknown as (abstract new (...args: any[]) => IMaskRenderable) & T; +} + +/** + * @remarks Extends `RendererUpdateFlags`. + */ +export enum MaskDirtyFlags { + /** UV. */ + UV = 0x2, + /** Automatic Size. */ + AutomaticSize = 0x8, + /** WorldVolume and UV. */ + WorldVolumeAndUV = 0x3, + /** All. */ + All = 0xb +} diff --git a/packages/core/src/2d/sprite/Sprite.ts b/packages/core/src/2d/sprite/Sprite.ts index 5232001c9a..b7f01f342e 100644 --- a/packages/core/src/2d/sprite/Sprite.ts +++ b/packages/core/src/2d/sprite/Sprite.ts @@ -20,7 +20,16 @@ export class Sprite extends ReferResource { private _customHeight: number = undefined; private _positions: Vector2[] = [new Vector2(), new Vector2(), new Vector2(), new Vector2()]; - private _uvs: Vector2[] = [new Vector2(), new Vector2(), new Vector2(), new Vector2()]; + // 16 UV 顶点构成 4×4 网格(含 9-slice 内边界)。column-major:index = i*4 + j,i=column(0=left..3=right),j=row(0=bottom..3=top)。 + // 与 SlicedAssembler/TiledAssembler 的 vertex 索引图一致:[0]=LB, [3]=LT, [12]=RB, [15]=RT。 + // SimpleAssembler 取 [0]/[3]/[12]/[15] 4 个 corner;Sliced/Tiled 用全 16。 + // prettier-ignore + private _uvs: Vector2[] = [ + new Vector2(), new Vector2(), new Vector2(), new Vector2(), + new Vector2(), new Vector2(), new Vector2(), new Vector2(), + new Vector2(), new Vector2(), new Vector2(), new Vector2(), + new Vector2(), new Vector2(), new Vector2(), new Vector2(), + ]; private _bounds: BoundingBox = new BoundingBox(); private _texture: Texture2D = null; @@ -287,16 +296,18 @@ export class Sprite extends ReferResource { private _calDefaultSize(): void { if (this._texture) { - const { _texture, _atlasRegion, _atlasRegionOffset, _region } = this; - const pixelsPerUnitReciprocal = 1.0 / Engine._pixelsPerUnit; + const { _texture, _atlasRegion, _atlasRegionOffset, _region, _atlasRotated } = this; + const ppuReciprocal = 1.0 / Engine._pixelsPerUnit; + // 先算 atlas 中绝对像素(texture 不一定是方形,必须各自乘对应维度) + const atlasPxW = _texture.width * _atlasRegion.width; + const atlasPxH = _texture.height * _atlasRegion.height; + // atlas 顺时针 pack 90°:原图 W×H 在 atlas 中占 H×W 区域,仅交换 atlasPx 的 W/H + const originWidth = _atlasRotated ? atlasPxH : atlasPxW; + const originHeight = _atlasRotated ? atlasPxW : atlasPxH; this._automaticWidth = - ((_texture.width * _atlasRegion.width) / (1 - _atlasRegionOffset.x - _atlasRegionOffset.z)) * - _region.width * - pixelsPerUnitReciprocal; + (originWidth / (1 - _atlasRegionOffset.x - _atlasRegionOffset.z)) * _region.width * ppuReciprocal; this._automaticHeight = - ((_texture.height * _atlasRegion.height) / (1 - _atlasRegionOffset.y - _atlasRegionOffset.w)) * - _region.height * - pixelsPerUnitReciprocal; + (originHeight / (1 - _atlasRegionOffset.y - _atlasRegionOffset.w)) * _region.height * ppuReciprocal; } else { this._automaticWidth = this._automaticHeight = 0; } @@ -332,34 +343,52 @@ export class Sprite extends ReferResource { } private _updateUVs(): void { - const { _uvs: uv, _atlasRegionOffset: atlasRegionOffset } = this; - const { x: regionX, y: regionY, width: regionW, height: regionH } = this._region; - const regionRight = 1 - regionX - regionW; - const regionBottom = 1 - regionY - regionH; + const { _uvs: uvs, _atlasRotated: atlasRotated, _border: border } = this; + const { x: regionLeft, y: regionTop, width: regionW, height: regionH } = this._region; + const regionRight = 1 - regionLeft - regionW; + const regionBottom = 1 - regionTop - regionH; const { x: atlasRegionX, y: atlasRegionY, width: atlasRegionW, height: atlasRegionH } = this._atlasRegion; - const { x: offsetLeft, y: offsetTop, z: offsetRight, w: offsetBottom } = atlasRegionOffset; + const { x: offsetLeft, y: offsetTop, z: offsetRight, w: offsetBottom } = this._atlasRegionOffset; const realWidth = atlasRegionW / (1 - offsetLeft - offsetRight); const realHeight = atlasRegionH / (1 - offsetTop - offsetBottom); - // Coordinates of the four boundaries. - const left = Math.max(regionX - offsetLeft, 0) * realWidth + atlasRegionX; - const top = Math.max(regionBottom - offsetTop, 0) * realHeight + atlasRegionY; - const right = atlasRegionW + atlasRegionX - Math.max(regionRight - offsetRight, 0) * realWidth; - const bottom = atlasRegionH + atlasRegionY - Math.max(regionY - offsetBottom, 0) * realHeight; - const { x: borderLeft, y: borderBottom, z: borderRight, w: borderTop } = this._border; - // Left-Bottom - uv[0].set(left, bottom); - // Border ( Left-Bottom ) - uv[1].set( - (regionX - offsetLeft + borderLeft * regionW) * realWidth + atlasRegionX, - atlasRegionH + atlasRegionY - (regionY - offsetBottom + borderBottom * regionH) * realHeight - ); - // Border ( Right-Top ) - uv[2].set( - atlasRegionW + atlasRegionX - (regionRight - offsetRight + borderRight * regionW) * realWidth, - (regionBottom - offsetTop + borderTop * regionH) * realHeight + atlasRegionY - ); - // Right-Top - uv[3].set(right, top); + // 4 个外边界 + 4 个 9-slice 内边界 + let left: number, top: number, right: number, bottom: number; + let bLeft: number, bTop: number, bRight: number, bBottom: number; + if (atlasRotated) { + // 原图 region/offset (left/top/right/bottom) 在 atlas 中映射为 (bottom/left/top/right) + left = Math.max(regionBottom - offsetLeft, 0) * realWidth + atlasRegionX; + top = Math.max(regionLeft - offsetTop, 0) * realHeight + atlasRegionY; + right = atlasRegionW + atlasRegionX - Math.max(regionTop - offsetRight, 0) * realWidth; + bottom = atlasRegionH + atlasRegionY - Math.max(regionRight - offsetBottom, 0) * realHeight; + bLeft = (regionBottom - offsetLeft + border.y * regionH) * realWidth + atlasRegionX; + bTop = (regionLeft - offsetTop + border.x * regionW) * realHeight + atlasRegionY; + bRight = atlasRegionW + atlasRegionX - (regionTop - offsetRight + border.w * regionH) * realWidth; + bBottom = atlasRegionH + atlasRegionY - (regionRight - offsetBottom + border.z * regionW) * realHeight; + } else { + left = Math.max(regionLeft - offsetLeft, 0) * realWidth + atlasRegionX; + top = Math.max(regionBottom - offsetTop, 0) * realHeight + atlasRegionY; + right = atlasRegionW + atlasRegionX - Math.max(regionRight - offsetRight, 0) * realWidth; + bottom = atlasRegionH + atlasRegionY - Math.max(regionTop - offsetBottom, 0) * realHeight; + bLeft = (regionLeft - offsetLeft + border.x * regionW) * realWidth + atlasRegionX; + bTop = (regionBottom - offsetTop + border.w * regionH) * realHeight + atlasRegionY; + bRight = atlasRegionW + atlasRegionX - (regionRight - offsetRight + border.z * regionW) * realWidth; + bBottom = atlasRegionH + atlasRegionY - (regionTop - offsetBottom + border.y * regionH) * realHeight; + } + + // 16 UV 网格填充(column-major:index=i*4+j,i=column[0=left..3=right],j=row[0=bottom..3=top]) + // - 非 rotated:column 决定 atlas X (left/bLeft/bRight/right),row 决定 atlas Y (bottom/bBottom/bTop/top) + // - rotated 90° 顺时针 packed:display column 对应 atlas Y 的反向(顶→底),display row 对应 atlas X + if (atlasRotated) { + uvs[0].set(left, top), uvs[1].set(bLeft, top), uvs[2].set(bRight, top), uvs[3].set(right, top); + uvs[4].set(left, bTop), uvs[5].set(bLeft, bTop), uvs[6].set(bRight, bTop), uvs[7].set(right, bTop); + uvs[8].set(left, bBottom), uvs[9].set(bLeft, bBottom), uvs[10].set(bRight, bBottom), uvs[11].set(right, bBottom); + uvs[12].set(left, bottom), uvs[13].set(bLeft, bottom), uvs[14].set(bRight, bottom), uvs[15].set(right, bottom); + } else { + uvs[0].set(left, bottom), uvs[1].set(left, bBottom), uvs[2].set(left, bTop), uvs[3].set(left, top); + uvs[4].set(bLeft, bottom), uvs[5].set(bLeft, bBottom), uvs[6].set(bLeft, bTop), uvs[7].set(bLeft, top); + uvs[8].set(bRight, bottom), uvs[9].set(bRight, bBottom), uvs[10].set(bRight, bTop), uvs[11].set(bRight, top); + uvs[12].set(right, bottom), uvs[13].set(right, bBottom), uvs[14].set(right, bTop), uvs[15].set(right, top); + } this._dirtyUpdateFlag &= ~SpriteUpdateFlags.uvs; } diff --git a/packages/core/src/2d/sprite/SpriteMask.ts b/packages/core/src/2d/sprite/SpriteMask.ts index 141f352d35..771faead34 100644 --- a/packages/core/src/2d/sprite/SpriteMask.ts +++ b/packages/core/src/2d/sprite/SpriteMask.ts @@ -1,46 +1,25 @@ import { BoundingBox } from "@galacean/engine-math"; import { Entity } from "../../Entity"; -import { RenderQueueFlags } from "../../RenderPipeline/BasicRenderPipeline"; -import { BatchUtils } from "../../RenderPipeline/BatchUtils"; import { PrimitiveChunkManager } from "../../RenderPipeline/PrimitiveChunkManager"; import { RenderContext } from "../../RenderPipeline/RenderContext"; -import { RenderElement } from "../../RenderPipeline/RenderElement"; import { SubPrimitiveChunk } from "../../RenderPipeline/SubPrimitiveChunk"; -import { SubRenderElement } from "../../RenderPipeline/SubRenderElement"; import { Renderer, RendererUpdateFlags } from "../../Renderer"; import { assignmentClone, ignoreClone } from "../../clone/CloneManager"; -import { SpriteMaskLayer } from "../../enums/SpriteMaskLayer"; import { ShaderProperty } from "../../shader/ShaderProperty"; -import { ISpriteRenderer } from "../assembler/ISpriteRenderer"; -import { SimpleSpriteAssembler } from "../assembler/SimpleSpriteAssembler"; import { SpriteModifyFlags } from "../enums/SpriteModifyFlags"; -import { Sprite } from "./Sprite"; +import { MaskDirtyFlags, MaskRenderable } from "./MaskRenderable"; /** * A component for masking Sprites. */ -export class SpriteMask extends Renderer implements ISpriteRenderer { - /** @internal */ - static _textureProperty: ShaderProperty = ShaderProperty.getByName("renderer_MaskTexture"); +export class SpriteMask extends MaskRenderable(Renderer) { /** @internal */ static _alphaCutoffProperty: ShaderProperty = ShaderProperty.getByName("renderer_MaskAlphaCutoff"); - - /** The mask layers the sprite mask influence to. */ - @assignmentClone - influenceLayers: SpriteMaskLayer = SpriteMaskLayer.Everything; /** @internal */ - @ignoreClone - _renderElement: RenderElement; - + static _textureProperty: ShaderProperty = ShaderProperty.getByName("renderer_MaskTexture"); /** @internal */ @ignoreClone _subChunk: SubPrimitiveChunk; - /** @internal */ - @ignoreClone - _maskIndex: number = -1; - - @ignoreClone - private _sprite: Sprite = null; @ignoreClone private _automaticWidth: number = 0; @@ -50,13 +29,6 @@ export class SpriteMask extends Renderer implements ISpriteRenderer { private _customWidth: number = undefined; @assignmentClone private _customHeight: number = undefined; - @assignmentClone - private _flipX: boolean = false; - @assignmentClone - private _flipY: boolean = false; - - @assignmentClone - private _alphaCutoff: number = 0.5; /** * Render width (in world coordinates). @@ -69,7 +41,7 @@ export class SpriteMask extends Renderer implements ISpriteRenderer { if (this._customWidth !== undefined) { return this._customWidth; } else { - this._dirtyUpdateFlag & SpriteMaskUpdateFlags.AutomaticSize && this._calDefaultSize(); + this._dirtyUpdateFlag & MaskDirtyFlags.AutomaticSize && this._calDefaultSize(); return this._automaticWidth; } } @@ -92,7 +64,7 @@ export class SpriteMask extends Renderer implements ISpriteRenderer { if (this._customHeight !== undefined) { return this._customHeight; } else { - this._dirtyUpdateFlag & SpriteMaskUpdateFlags.AutomaticSize && this._calDefaultSize(); + this._dirtyUpdateFlag & MaskDirtyFlags.AutomaticSize && this._calDefaultSize(); return this._automaticHeight; } } @@ -104,93 +76,20 @@ export class SpriteMask extends Renderer implements ISpriteRenderer { } } - /** - * Flips the sprite on the X axis. - */ - get flipX(): boolean { - return this._flipX; - } - - set flipX(value: boolean) { - if (this._flipX !== value) { - this._flipX = value; - this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; - } - } - - /** - * Flips the sprite on the Y axis. - */ - get flipY(): boolean { - return this._flipY; - } - - set flipY(value: boolean) { - if (this._flipY !== value) { - this._flipY = value; - this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; - } - } - - /** - * The Sprite to render. - */ - get sprite(): Sprite { - return this._sprite; - } - - set sprite(value: Sprite | null) { - const lastSprite = this._sprite; - if (lastSprite !== value) { - if (lastSprite) { - this._addResourceReferCount(lastSprite, -1); - lastSprite._updateFlagManager.removeListener(this._onSpriteChange); - } - this._dirtyUpdateFlag |= SpriteMaskUpdateFlags.All; - if (value) { - this._addResourceReferCount(value, 1); - value._updateFlagManager.addListener(this._onSpriteChange); - this.shaderData.setTexture(SpriteMask._textureProperty, value.texture); - } else { - this.shaderData.setTexture(SpriteMask._textureProperty, null); - } - this._sprite = value; - } - } - - /** - * The minimum alpha value used by the mask to select the area of influence defined over the mask's sprite. Value between 0 and 1. - */ - get alphaCutoff(): number { - return this._alphaCutoff; - } - - set alphaCutoff(value: number) { - if (this._alphaCutoff !== value) { - this._alphaCutoff = value; - this.shaderData.setFloat(SpriteMask._alphaCutoffProperty, value); - } - } - /** * @internal */ constructor(entity: Entity) { super(entity); - SimpleSpriteAssembler.resetData(this); - this.setMaterial(this._engine._basicResources.spriteMaskDefaultMaterial); - this.shaderData.setFloat(SpriteMask._alphaCutoffProperty, this._alphaCutoff); - this._renderElement = new RenderElement(); - this._renderElement.addSubRenderElement(new SubRenderElement()); - this._onSpriteChange = this._onSpriteChange.bind(this); + this._initMask(); } /** * @internal */ - override _updateTransformShaderData(context: RenderContext, onlyMVP: boolean, batched: boolean): void { + override _updateTransformShaderData(context: RenderContext, onlyMVP: boolean): void { //@todo: Always update world positions to buffer, should opt - super._updateTransformShaderData(context, onlyMVP, true); + this._updateWorldSpaceTransformShaderData(context, onlyMVP); } /** @@ -198,37 +97,7 @@ export class SpriteMask extends Renderer implements ISpriteRenderer { */ override _cloneTo(target: SpriteMask): void { super._cloneTo(target); - target.sprite = this._sprite; - } - - /** - * @internal - */ - override _canBatch(elementA: SubRenderElement, elementB: SubRenderElement): boolean { - return BatchUtils.canBatchSpriteMask(elementA, elementB); - } - - /** - * @internal - */ - override _batch(elementA: SubRenderElement, elementB?: SubRenderElement): void { - BatchUtils.batchFor2D(elementA, elementB); - } - - /** - * @internal - */ - override _onEnableInScene(): void { - super._onEnableInScene(); - this.scene._maskManager.addSpriteMask(this); - } - - /** - * @internal - */ - override _onDisableInScene(): void { - super._onDisableInScene(); - this.scene._maskManager.removeSpriteMask(this); + this._cloneMaskData(target); } /** @@ -239,147 +108,64 @@ export class SpriteMask extends Renderer implements ISpriteRenderer { } protected override _updateBounds(worldBounds: BoundingBox): void { - const sprite = this._sprite; - if (sprite) { - SimpleSpriteAssembler.updatePositions( - this, - this._transformEntity.transform.worldMatrix, - this.width, - this.height, - sprite.pivot, - this._flipX, - this._flipY - ); - } else { - const { worldPosition } = this._transformEntity.transform; - worldBounds.min.copyFrom(worldPosition); - worldBounds.max.copyFrom(worldPosition); - } + this._updateMaskBounds(worldBounds); } /** * @inheritdoc */ protected override _render(context: RenderContext): void { - const { _sprite: sprite } = this; - if (!sprite?.texture || !this.width || !this.height) { - return; - } - - let material = this.getMaterial(); - if (!material) { - return; - } - const { _engine: engine } = this; - // @todo: This question needs to be raised rather than hidden. - if (material.destroyed) { - material = engine._basicResources.spriteMaskDefaultMaterial; - } - - // Update position - if (this._dirtyUpdateFlag & RendererUpdateFlags.WorldVolume) { - SimpleSpriteAssembler.updatePositions( - this, - this._transformEntity.transform.worldMatrix, - this.width, - this.height, - sprite.pivot, - this._flipX, - this._flipY - ); - this._dirtyUpdateFlag &= ~RendererUpdateFlags.WorldVolume; - } - - // Update uv - if (this._dirtyUpdateFlag & SpriteMaskUpdateFlags.UV) { - SimpleSpriteAssembler.updateUVs(this); - this._dirtyUpdateFlag &= ~SpriteMaskUpdateFlags.UV; - } - - const renderElement = this._renderElement; - const subRenderElement = renderElement.subRenderElements[0]; - renderElement.set(this.priority, this._distanceForSort); - - const subChunk = this._subChunk; - subRenderElement.set(this, material, subChunk.chunk.primitive, subChunk.subMesh, this.sprite.texture, subChunk); - subRenderElement.shaderPasses = material.shader.subShaders[0].passes; - subRenderElement.renderQueueFlags = RenderQueueFlags.All; - renderElement.addSubRenderElement(subRenderElement); + this._renderMask(this._distanceForSort); } /** * @inheritdoc */ protected override _onDestroy(): void { - const sprite = this._sprite; - if (sprite) { - this._addResourceReferCount(sprite, -1); - sprite._updateFlagManager.removeListener(this._onSpriteChange); - } + this._destroyMaskResources(); super._onDestroy(); - this._sprite = null; if (this._subChunk) { this._getChunkManager().freeSubChunk(this._subChunk); this._subChunk = null; } + } - this._renderElement = null; + override _getSpriteWidth(): number { + return this.width; } - private _calDefaultSize(): void { - const sprite = this._sprite; - if (sprite) { - this._automaticWidth = sprite.width; - this._automaticHeight = sprite.height; - } else { - this._automaticWidth = this._automaticHeight = 0; - } - this._dirtyUpdateFlag &= ~SpriteMaskUpdateFlags.AutomaticSize; + override _getSpriteHeight(): number { + return this.height; } - @ignoreClone - private _onSpriteChange(type: SpriteModifyFlags): void { + override _getSpritePivot() { + return this.sprite?.pivot; + } + + override _onSpriteChangeExtra(type: SpriteModifyFlags): void { switch (type) { - case SpriteModifyFlags.texture: - this.shaderData.setTexture(SpriteMask._textureProperty, this.sprite.texture); - break; case SpriteModifyFlags.size: - this._dirtyUpdateFlag |= SpriteMaskUpdateFlags.AutomaticSize; + this._dirtyUpdateFlag |= MaskDirtyFlags.AutomaticSize; if (this._customWidth === undefined || this._customHeight === undefined) { this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; } break; - case SpriteModifyFlags.region: - case SpriteModifyFlags.atlasRegionOffset: - this._dirtyUpdateFlag |= SpriteMaskUpdateFlags.WorldVolumeAndUV; - break; - case SpriteModifyFlags.atlasRegion: - this._dirtyUpdateFlag |= SpriteMaskUpdateFlags.UV; - break; case SpriteModifyFlags.pivot: this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; break; - case SpriteModifyFlags.destroy: - this.sprite = null; - break; - default: - break; } } -} -/** - * @remarks Extends `RendererUpdateFlags`. - */ -enum SpriteMaskUpdateFlags { - /** UV. */ - UV = 0x2, - /** Automatic Size. */ - AutomaticSize = 0x4, - /** WorldVolume and UV. */ - WorldVolumeAndUV = 0x3, - /** All. */ - All = 0x7 + private _calDefaultSize(): void { + const sprite = this.sprite; + if (sprite) { + this._automaticWidth = sprite.width; + this._automaticHeight = sprite.height; + } else { + this._automaticWidth = this._automaticHeight = 0; + } + this._dirtyUpdateFlag &= ~MaskDirtyFlags.AutomaticSize; + } } diff --git a/packages/core/src/2d/sprite/SpriteMaskUtils.ts b/packages/core/src/2d/sprite/SpriteMaskUtils.ts new file mode 100644 index 0000000000..b7033ee800 --- /dev/null +++ b/packages/core/src/2d/sprite/SpriteMaskUtils.ts @@ -0,0 +1,136 @@ +import { Matrix, Vector2, Vector3 } from "@galacean/engine-math"; +import { Texture2D, TextureFormat } from "../../texture"; +import { Sprite } from "./Sprite"; + +/** + * Internal helpers for sprite mask hit testing. + * @internal + */ +export class SpriteMaskUtils { + private static _tempMat: Matrix = new Matrix(); + private static _tempVec3: Vector3 = new Vector3(); + private static _u8Buffer1 = new Uint8Array(1); + private static _u8Buffer2 = new Uint8Array(2); + private static _u8Buffer4 = new Uint8Array(4); + private static _u16Buffer1 = new Uint16Array(1); + private static _u16Buffer4 = new Uint16Array(4); + private static _f32Buffer4 = new Float32Array(4); + private static _u32Buffer4 = new Uint32Array(4); + + static containsWorldPoint( + worldPoint: Vector3, + sprite: Sprite | null, + worldMatrix: Matrix, + width: number, + height: number, + pivot: Vector2, + flipX: boolean, + flipY: boolean, + alphaCutoff: number = 0 + ): boolean { + if (!sprite || !width || !height) { + return false; + } + + const worldMatrixInv = SpriteMaskUtils._tempMat; + Matrix.invert(worldMatrix, worldMatrixInv); + const localPosition = SpriteMaskUtils._tempVec3; + Vector3.transformCoordinate(worldPoint, worldMatrixInv, localPosition); + + const sx = flipX ? -width : width; + const sy = flipY ? -height : height; + if (!sx || !sy) { + return false; + } + + const spriteX = localPosition.x / sx + pivot.x; + const spriteY = localPosition.y / sy + pivot.y; + const spritePositions = sprite._getPositions(); + const { x: left, y: bottom } = spritePositions[0]; + const { x: right, y: top } = spritePositions[3]; + if (!(spriteX >= left && spriteX <= right && spriteY >= bottom && spriteY <= top)) { + return false; + } + + if (alphaCutoff <= 0) { + return true; + } + + const texture = sprite.texture; + if (!texture) { + return false; + } + + const spriteUVs = sprite._getUVs(); + const leftU = spriteUVs[0].x; + const bottomV = spriteUVs[0].y; + const rightU = spriteUVs[3].x; + const topV = spriteUVs[3].y; + const positionWidth = right - left; + const positionHeight = top - bottom; + if (!positionWidth || !positionHeight) { + return false; + } + + const tx = (spriteX - left) / positionWidth; + const ty = (spriteY - bottom) / positionHeight; + const u = leftU + (rightU - leftU) * tx; + const v = bottomV + (topV - bottomV) * ty; + const x = Math.min(Math.max(Math.floor(u * texture.width), 0), texture.width - 1); + const y = Math.min(Math.max(Math.floor(v * texture.height), 0), texture.height - 1); + return SpriteMaskUtils._sampleTextureAlpha(texture, x, y) >= alphaCutoff; + } + + private static _sampleTextureAlpha(texture: Texture2D, x: number, y: number): number { + try { + switch (texture.format) { + case TextureFormat.R8G8B8A8: { + const buffer = SpriteMaskUtils._u8Buffer4; + texture.getPixelBuffer(x, y, 1, 1, buffer); + return buffer[3] / 255; + } + case TextureFormat.R4G4B4A4: { + const buffer = SpriteMaskUtils._u16Buffer1; + texture.getPixelBuffer(x, y, 1, 1, buffer); + return (buffer[0] & 0xf) / 15; + } + case TextureFormat.R5G5B5A1: { + const buffer = SpriteMaskUtils._u16Buffer1; + texture.getPixelBuffer(x, y, 1, 1, buffer); + return buffer[0] & 0x1; + } + case TextureFormat.Alpha8: + case TextureFormat.R8: { + const buffer = SpriteMaskUtils._u8Buffer1; + texture.getPixelBuffer(x, y, 1, 1, buffer); + return buffer[0] / 255; + } + case TextureFormat.LuminanceAlpha: + case TextureFormat.R8G8: { + const buffer = SpriteMaskUtils._u8Buffer2; + texture.getPixelBuffer(x, y, 1, 1, buffer); + return buffer[1] / 255; + } + case TextureFormat.R16G16B16A16: { + const buffer = SpriteMaskUtils._u16Buffer4; + texture.getPixelBuffer(x, y, 1, 1, buffer); + return buffer[3] / 65535; + } + case TextureFormat.R32G32B32A32: { + const buffer = SpriteMaskUtils._f32Buffer4; + texture.getPixelBuffer(x, y, 1, 1, buffer); + return buffer[3]; + } + case TextureFormat.R32G32B32A32_UInt: { + const buffer = SpriteMaskUtils._u32Buffer4; + texture.getPixelBuffer(x, y, 1, 1, buffer); + return buffer[3] / 4294967295; + } + default: + return 1; + } + } catch { + return 1; + } + } +} diff --git a/packages/core/src/2d/sprite/SpriteRenderer.ts b/packages/core/src/2d/sprite/SpriteRenderer.ts index c1b183ee7a..06bd3d3c6c 100644 --- a/packages/core/src/2d/sprite/SpriteRenderer.ts +++ b/packages/core/src/2d/sprite/SpriteRenderer.ts @@ -1,19 +1,22 @@ import { BoundingBox, Color, MathUtil } from "@galacean/engine-math"; import { Entity } from "../../Entity"; -import { BatchUtils } from "../../RenderPipeline/BatchUtils"; +import { VertexMergeBatcher } from "../../RenderPipeline/VertexMergeBatcher"; import { PrimitiveChunkManager } from "../../RenderPipeline/PrimitiveChunkManager"; import { RenderContext } from "../../RenderPipeline/RenderContext"; import { SubPrimitiveChunk } from "../../RenderPipeline/SubPrimitiveChunk"; -import { SubRenderElement } from "../../RenderPipeline/SubRenderElement"; +import { RenderElement } from "../../RenderPipeline/RenderElement"; import { Renderer, RendererUpdateFlags } from "../../Renderer"; import { assignmentClone, deepClone, ignoreClone } from "../../clone/CloneManager"; import { ShaderProperty } from "../../shader/ShaderProperty"; import { ISpriteAssembler } from "../assembler/ISpriteAssembler"; import { ISpriteRenderer } from "../assembler/ISpriteRenderer"; +import { FilledSpriteAssembler } from "../assembler/FilledSpriteAssembler"; import { SimpleSpriteAssembler } from "../assembler/SimpleSpriteAssembler"; import { SlicedSpriteAssembler } from "../assembler/SlicedSpriteAssembler"; import { TiledSpriteAssembler } from "../assembler/TiledSpriteAssembler"; import { SpriteDrawMode } from "../enums/SpriteDrawMode"; +import { SpriteFilledMode } from "../enums/SpriteFilledMode"; +import { SpriteFilledOrigin } from "../enums/SpriteFilledOrigin"; import { SpriteMaskInteraction } from "../enums/SpriteMaskInteraction"; import { SpriteModifyFlags } from "../enums/SpriteModifyFlags"; import { SpriteTileMode } from "../enums/SpriteTileMode"; @@ -39,6 +42,14 @@ export class SpriteRenderer extends Renderer implements ISpriteRenderer { private _tileMode: SpriteTileMode = SpriteTileMode.Continuous; @assignmentClone private _tiledAdaptiveThreshold: number = 0.5; + @assignmentClone + private _filledMode: SpriteFilledMode = SpriteFilledMode.Radial360; + @assignmentClone + private _filledAmount: number = 1; + @assignmentClone + private _filledOrigin: SpriteFilledOrigin = SpriteFilledOrigin.Bottom; + @assignmentClone + private _filledClockWise: boolean = true; @deepClone private _color: Color = new Color(1, 1, 1, 1); @@ -78,6 +89,9 @@ export class SpriteRenderer extends Renderer implements ISpriteRenderer { case SpriteDrawMode.Tiled: this._assembler = TiledSpriteAssembler; break; + case SpriteDrawMode.Filled: + this._assembler = FilledSpriteAssembler; + break; default: break; } @@ -119,6 +133,74 @@ export class SpriteRenderer extends Renderer implements ISpriteRenderer { } } + /** + * The fill amount of the sprite renderer, range from 0 to 1. (Only works in filled mode.) + */ + get filledAmount(): number { + return this._filledAmount; + } + + set filledAmount(value: number) { + value = MathUtil.clamp(value, 0, 1); + if (this._filledAmount !== value) { + this._filledAmount = value; + if (this._drawMode === SpriteDrawMode.Filled) { + this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeAndUV; + } + } + } + + /** + * The fill mode of the sprite renderer. (Only works in filled mode.) + */ + get filledMode(): SpriteFilledMode { + return this._filledMode; + } + + set filledMode(value: SpriteFilledMode) { + if (this._filledMode !== value) { + this._filledMode = value; + // Reset origin to a valid default for the new mode + this._filledOrigin = + value === SpriteFilledMode.Radial90 ? SpriteFilledOrigin.BottomLeft : SpriteFilledOrigin.Bottom; + if (this._drawMode === SpriteDrawMode.Filled) { + this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeAndUV; + } + } + } + + /** + * The fill origin of the sprite renderer. (Only works in filled mode.) + */ + get filledOrigin(): SpriteFilledOrigin { + return this._filledOrigin; + } + + set filledOrigin(value: SpriteFilledOrigin) { + if (this._filledOrigin !== value) { + this._filledOrigin = value; + if (this._drawMode === SpriteDrawMode.Filled) { + this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeAndUV; + } + } + } + + /** + * Whether the fill is clockwise. (Only works in filled radial mode.) + */ + get filledClockWise(): boolean { + return this._filledClockWise; + } + + set filledClockWise(value: boolean) { + if (this._filledClockWise !== value) { + this._filledClockWise = value; + if (this._drawMode === SpriteDrawMode.Filled) { + this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeAndUV; + } + } + } + /** * The Sprite to render. */ @@ -278,9 +360,9 @@ export class SpriteRenderer extends Renderer implements ISpriteRenderer { /** * @internal */ - override _updateTransformShaderData(context: RenderContext, onlyMVP: boolean, batched: boolean): void { + override _updateTransformShaderData(context: RenderContext, onlyMVP: boolean): void { //@todo: Always update world positions to buffer, should opt - super._updateTransformShaderData(context, onlyMVP, true); + this._updateWorldSpaceTransformShaderData(context, onlyMVP); } /** @@ -295,15 +377,15 @@ export class SpriteRenderer extends Renderer implements ISpriteRenderer { /** * @internal */ - override _canBatch(elementA: SubRenderElement, elementB: SubRenderElement): boolean { - return BatchUtils.canBatchSprite(elementA, elementB); + override _canBatch(preElement: RenderElement, curElement: RenderElement): boolean { + return VertexMergeBatcher.canBatchSprite(preElement, curElement); } /** * @internal */ - override _batch(elementA: SubRenderElement, elementB?: SubRenderElement): void { - BatchUtils.batchFor2D(elementA, elementB); + override _batch(preElement: RenderElement | null, curElement: RenderElement): void { + VertexMergeBatcher.batch(preElement, curElement); } /** @@ -377,11 +459,10 @@ export class SpriteRenderer extends Renderer implements ISpriteRenderer { const camera = context.camera; const engine = camera.engine; const renderElement = engine._renderElementPool.get(); - renderElement.set(this.priority, this._distanceForSort); - const subRenderElement = engine._subRenderElementPool.get(); const subChunk = this._subChunk; - subRenderElement.set(this, material, subChunk.chunk.primitive, subChunk.subMesh, this.sprite.texture, subChunk); - renderElement.addSubRenderElement(subRenderElement); + renderElement.set(this, material, subChunk.chunk.primitive, subChunk.subMesh, this.sprite.texture, subChunk); + renderElement.priority = this.priority; + renderElement.distanceForSort = this._distanceForSort; camera._renderPipeline.pushRenderElement(context, renderElement); } @@ -438,6 +519,9 @@ export class SpriteRenderer extends Renderer implements ISpriteRenderer { case SpriteDrawMode.Tiled: this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeUVAndColor; break; + case SpriteDrawMode.Filled: + this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeUVAndColor; + break; } break; case SpriteModifyFlags.border: diff --git a/packages/core/src/2d/sprite/index.ts b/packages/core/src/2d/sprite/index.ts index 162d016472..c48fb9261b 100644 --- a/packages/core/src/2d/sprite/index.ts +++ b/packages/core/src/2d/sprite/index.ts @@ -1,3 +1,6 @@ +export type { IMaskRenderable } from "./MaskRenderable"; +export { MaskDirtyFlags, MaskRenderable } from "./MaskRenderable"; export { Sprite } from "./Sprite"; export { SpriteMask } from "./SpriteMask"; +export { SpriteMaskUtils } from "./SpriteMaskUtils"; export { SpriteRenderer } from "./SpriteRenderer"; diff --git a/packages/core/src/2d/text/TextRenderer.ts b/packages/core/src/2d/text/TextRenderer.ts index 143e46a7da..7d2c26b2e0 100644 --- a/packages/core/src/2d/text/TextRenderer.ts +++ b/packages/core/src/2d/text/TextRenderer.ts @@ -1,14 +1,15 @@ -import { BoundingBox, Color, Vector3 } from "@galacean/engine-math"; +import { BoundingBox, Color, Vector2, Vector3 } from "@galacean/engine-math"; import { Engine } from "../../Engine"; import { Entity } from "../../Entity"; -import { BatchUtils } from "../../RenderPipeline/BatchUtils"; import { PrimitiveChunkManager } from "../../RenderPipeline/PrimitiveChunkManager"; import { RenderContext } from "../../RenderPipeline/RenderContext"; +import { RenderElement } from "../../RenderPipeline/RenderElement"; import { SubPrimitiveChunk } from "../../RenderPipeline/SubPrimitiveChunk"; -import { SubRenderElement } from "../../RenderPipeline/SubRenderElement"; +import { VertexMergeBatcher } from "../../RenderPipeline/VertexMergeBatcher"; import { Renderer } from "../../Renderer"; import { TransformModifyFlags } from "../../Transform"; import { assignmentClone, deepClone, ignoreClone } from "../../clone/CloneManager"; +import { SpriteMaskLayer } from "../../enums/SpriteMaskLayer"; import { ShaderData, ShaderProperty } from "../../shader"; import { ShaderDataGroup } from "../../shader/enums/ShaderDataGroup"; import { Texture2D } from "../../texture"; @@ -21,15 +22,18 @@ import { Font } from "./Font"; import { ITextRenderer } from "./ITextRenderer"; import { SubFont } from "./SubFont"; import { TextUtils } from "./TextUtils"; -import { SpriteMaskLayer } from "../../enums/SpriteMaskLayer"; /** * Renders a text for 2D graphics. */ export class TextRenderer extends Renderer implements ITextRenderer { - private static _textureProperty = ShaderProperty.getByName("renderElement_TextTexture"); + private static _tempVec20 = new Vector2(); private static _tempVec30 = new Vector3(); private static _tempVec31 = new Vector3(); + private static _textureProperty = ShaderProperty.getByName("renderElement_TextTexture"); + private static _textTextureSizeProperty = ShaderProperty.getByName("renderElement_TextTextureSize"); + private static _outlineColorProperty = ShaderProperty.getByName("renderer_OutlineColor"); + private static _outlineWidthProperty = ShaderProperty.getByName("renderer_OutlineWidth"); private static _worldPositions = [new Vector3(), new Vector3(), new Vector3(), new Vector3()]; private static _charRenderInfos: CharRenderInfo[] = []; @@ -69,6 +73,10 @@ export class TextRenderer extends Renderer implements ITextRenderer { private _enableWrapping = false; @assignmentClone private _overflowMode = OverflowMode.Overflow; + @deepClone + private _outlineColor = new Color(0, 0, 0, 1); + @ignoreClone + private _outlineWidth = 0; /** * Rendering color for the Text. @@ -255,6 +263,35 @@ export class TextRenderer extends Renderer implements ITextRenderer { } } + /** + * The outline width in pixels. 0 means outline is disabled. Clamped to [0, 8]. + */ + get outlineWidth(): number { + return this._outlineWidth; + } + + set outlineWidth(value: number) { + value = Math.max(0, Math.min(value, 3)); + if (this._outlineWidth !== value) { + this._outlineWidth = value; + this.shaderData.setFloat(TextRenderer._outlineWidthProperty, value); + this._setDirtyFlagTrue(DirtyFlag.Position); + } + } + + /** + * The outline color. Only effective when outlineWidth > 0. + */ + get outlineColor(): Color { + return this._outlineColor; + } + + set outlineColor(value: Color) { + if (this._outlineColor !== value) { + this._outlineColor.copyFrom(value); + } + } + /** * Interacts with the masks. */ @@ -309,8 +346,13 @@ export class TextRenderer extends Renderer implements ITextRenderer { this._font = engine._textDefaultFont; this._addResourceReferCount(this._font, 1); this.setMaterial(engine._basicResources.textDefaultMaterial); + const shaderData = this.shaderData; + shaderData.setFloat(TextRenderer._outlineWidthProperty, this._outlineWidth); + shaderData.setColor(TextRenderer._outlineColorProperty, this._outlineColor); //@ts-ignore this._color._onValueChanged = this._onColorChanged.bind(this); + // @ts-ignore + this._outlineColor._onValueChanged = this._onOutlineColorChanged.bind(this); } /** @@ -337,6 +379,7 @@ export class TextRenderer extends Renderer implements ITextRenderer { super._cloneTo(target); target.font = this._font; target._subFont = this._subFont; + target.outlineWidth = this._outlineWidth; } /** @@ -373,23 +416,23 @@ export class TextRenderer extends Renderer implements ITextRenderer { /** * @internal */ - override _updateTransformShaderData(context: RenderContext, onlyMVP: boolean, batched: boolean): void { + override _updateTransformShaderData(context: RenderContext, onlyMVP: boolean): void { //@todo: Always update world positions to buffer, should opt - super._updateTransformShaderData(context, onlyMVP, true); + this._updateWorldSpaceTransformShaderData(context, onlyMVP); } /** * @internal */ - override _canBatch(elementA: SubRenderElement, elementB: SubRenderElement): boolean { - return BatchUtils.canBatchSprite(elementA, elementB); + override _canBatch(preElement: RenderElement, curElement: RenderElement): boolean { + return VertexMergeBatcher.canBatchText(preElement, curElement); } /** * @internal */ - override _batch(elementA: SubRenderElement, elementB?: SubRenderElement): void { - BatchUtils.batchFor2D(elementA, elementB); + override _batch(preElement: RenderElement | null, curElement: RenderElement): void { + VertexMergeBatcher.batch(preElement, curElement); } /** @@ -430,20 +473,27 @@ export class TextRenderer extends Renderer implements ITextRenderer { const camera = context.camera; const engine = camera.engine; - const textSubRenderElementPool = engine._textSubRenderElementPool; + const textRenderElementPool = engine._textRenderElementPool; const material = this.getMaterial(); - const renderElement = engine._renderElementPool.get(); - renderElement.set(this.priority, this._distanceForSort); + const priority = this.priority; + const distanceForSort = this._distanceForSort; + const renderPipeline = camera._renderPipeline; const textChunks = this._textChunks; + const textTextureSize = TextRenderer._tempVec20; for (let i = 0, n = textChunks.length; i < n; ++i) { const { subChunk, texture } = textChunks[i]; - const subRenderElement = textSubRenderElementPool.get(); - subRenderElement.set(this, material, subChunk.chunk.primitive, subChunk.subMesh, texture, subChunk); - subRenderElement.shaderData ||= new ShaderData(ShaderDataGroup.RenderElement); - subRenderElement.shaderData.setTexture(TextRenderer._textureProperty, texture); - renderElement.addSubRenderElement(subRenderElement); + const renderElement = textRenderElementPool.get(); + renderElement.set(this, material, subChunk.chunk.primitive, subChunk.subMesh, texture, subChunk); + renderElement.shaderData ||= new ShaderData(ShaderDataGroup.RenderElement); + renderElement.shaderData.setTexture(TextRenderer._textureProperty, texture); + renderElement.shaderData.setVector2( + TextRenderer._textTextureSizeProperty, + textTextureSize.set(texture.width, texture.height) + ); + renderElement.priority = priority; + renderElement.distanceForSort = distanceForSort; + renderPipeline.pushRenderElement(context, renderElement); } - camera._renderPipeline.pushRenderElement(context, renderElement); } private _resetSubFont(): void { @@ -452,6 +502,16 @@ export class TextRenderer extends Renderer implements ITextRenderer { this._subFont.nativeFontString = TextUtils.getNativeFontString(font.name, this.fontSize, this.fontStyle); } + /** + * Switch the sub font to a specific font size, used by the SHRINK overflow measurement. + */ + private _applyFontSizeForShrink(fontSize: number): void { + const font = this._font; + const subFont = font._getSubFont(fontSize, this._fontStyle); + subFont.nativeFontString = TextUtils.getNativeFontString(font.name, fontSize, this._fontStyle); + this._subFont = subFont; + } + private _updatePosition(): void { const { transform } = this.entity; const e = transform.worldMatrix.elements; @@ -525,22 +585,37 @@ export class TextRenderer extends Renderer implements ITextRenderer { const { _pixelsPerUnit } = Engine; const { min, max } = this._localBounds; const charRenderInfos = TextRenderer._charRenderInfos; + const rendererWidth = this.width * _pixelsPerUnit; + const rendererHeight = this.height * _pixelsPerUnit; + let fontSize = this._fontSize; + let textMetrics: ReturnType; + if (this._overflowMode === OverflowMode.Shrink) { + const result = TextUtils.measureTextWithShrink( + this, + rendererWidth, + rendererHeight, + this._fontSize, + this._lineSpacing, + this._characterSpacing, + this.enableWrapping, + (size) => this._applyFontSizeForShrink(size) + ); + fontSize = result.fontSize; + textMetrics = result.metrics; + } else { + const characterSpacing = this._characterSpacing * fontSize; + textMetrics = this.enableWrapping + ? TextUtils.measureTextWithWrap( + this, + rendererWidth, + rendererHeight, + this._lineSpacing * fontSize, + characterSpacing + ) + : TextUtils.measureTextWithoutWrap(this, rendererHeight, this._lineSpacing * fontSize, characterSpacing); + } const charFont = this._getSubFont(); - const characterSpacing = this._characterSpacing * this._fontSize; - const textMetrics = this.enableWrapping - ? TextUtils.measureTextWithWrap( - this, - this.width * _pixelsPerUnit, - this.height * _pixelsPerUnit, - this._lineSpacing * this._fontSize, - characterSpacing - ) - : TextUtils.measureTextWithoutWrap( - this, - this.height * _pixelsPerUnit, - this._lineSpacing * this._fontSize, - characterSpacing - ); + const characterSpacing = this._characterSpacing * fontSize; const { height, lines, lineWidths, lineHeight, lineMaxSizes } = textMetrics; const charRenderInfoPool = this.engine._charRenderInfoPool; const linesLen = lines.length; @@ -549,9 +624,7 @@ export class TextRenderer extends Renderer implements ITextRenderer { if (linesLen > 0) { const { horizontalAlignment } = this; const pixelsPerUnitReciprocal = 1.0 / _pixelsPerUnit; - const rendererWidth = this._width * _pixelsPerUnit; const halfRendererWidth = rendererWidth * 0.5; - const rendererHeight = this._height * _pixelsPerUnit; const halfLineHeight = lineHeight * 0.5; let startY = 0; @@ -562,7 +635,10 @@ export class TextRenderer extends Renderer implements ITextRenderer { startY = rendererHeight * 0.5 - halfLineHeight + topDiff; break; case TextVerticalAlignment.Center: - startY = height * 0.5 - halfLineHeight - (bottomDiff - topDiff) * 0.5; + // Center the text block (lineHeight * lineCount) within the renderer, independent of + // `height` — which equals the renderer height for Truncate/Shrink and would otherwise + // push the text upward by (rendererHeight - blockHeight) / 2 when the box is taller. + startY = lineHeight * linesLen * 0.5 - halfLineHeight - (bottomDiff - topDiff) * 0.5; break; case TextVerticalAlignment.Bottom: startY = height - rendererHeight * 0.5 - halfLineHeight - bottomDiff; @@ -604,10 +680,11 @@ export class TextRenderer extends Renderer implements ITextRenderer { charRenderInfo.texture = charFont._getTextureByIndex(charInfo.index); charRenderInfo.uvs = charInfo.uvs; const { w, ascent, descent } = charInfo; - const left = startX * pixelsPerUnitReciprocal; - const right = (startX + w) * pixelsPerUnitReciprocal; - const top = (startY + ascent) * pixelsPerUnitReciprocal; - const bottom = (startY - descent) * pixelsPerUnitReciprocal; + const ow = this._outlineWidth * pixelsPerUnitReciprocal; + const left = startX * pixelsPerUnitReciprocal - ow; + const right = (startX + w) * pixelsPerUnitReciprocal + ow; + const top = (startY + ascent) * pixelsPerUnitReciprocal + ow; + const bottom = (startY - descent) * pixelsPerUnitReciprocal - ow; localPositions.set(left, top, right, bottom); i === firstLine && (maxY = Math.max(maxY, top)); minY = Math.min(minY, bottom); @@ -686,7 +763,7 @@ export class TextRenderer extends Renderer implements ITextRenderer { this._text === "" || this._fontSize === 0 || (this.enableWrapping && this.width <= 0) || - (this.overflowMode === OverflowMode.Truncate && this.height <= 0) + ((this.overflowMode === OverflowMode.Truncate || this.overflowMode === OverflowMode.Shrink) && this.height <= 0) ); } @@ -698,6 +775,10 @@ export class TextRenderer extends Renderer implements ITextRenderer { const vertices = subChunk.chunk.vertices; const indices = (subChunk.indices = []); const charRenderInfos = textChunk.charRenderInfos; + const ow = this._outlineWidth; + const texture = textChunk.texture; + const owU = ow > 0 ? ow / texture.width : 0; + const owV = ow > 0 ? ow / texture.height : 0; for (let i = 0, ii = 0, io = 0, vo = subChunk.vertexArea.start + 3; i < count; ++i, io += 4) { const charRenderInfo = charRenderInfos[i]; charRenderInfo.indexInChunk = i; @@ -707,10 +788,13 @@ export class TextRenderer extends Renderer implements ITextRenderer { indices[ii++] = tempIndices[j] + io; } - // Set uv and color for vertices + // Set uv and color for vertices, expand uv outward by outline width for (let j = 0; j < 4; ++j, vo += 9) { const uv = charRenderInfo.uvs[j]; - uv.copyToArray(vertices, vo); + const su = j === 1 || j === 2 ? 1 : -1; + const sv = j >= 2 ? 1 : -1; + vertices[vo] = uv.x + owU * su; + vertices[vo + 1] = uv.y + owV * sv; vertices[vo + 2] = r; vertices[vo + 3] = g; vertices[vo + 4] = b; @@ -743,6 +827,11 @@ export class TextRenderer extends Renderer implements ITextRenderer { private _onColorChanged(): void { this._setDirtyFlagTrue(DirtyFlag.Color); } + + @ignoreClone + private _onOutlineColorChanged(): void { + this.shaderData.setColor(TextRenderer._outlineColorProperty, this._outlineColor); + } } class TextChunk { diff --git a/packages/core/src/2d/text/TextUtils.ts b/packages/core/src/2d/text/TextUtils.ts index ce2440573f..5305049fa9 100644 --- a/packages/core/src/2d/text/TextUtils.ts +++ b/packages/core/src/2d/text/TextUtils.ts @@ -100,7 +100,8 @@ export class TextUtils { rendererWidth: number, rendererHeight: number, lineSpacing: number, - characterSpacing: number + characterSpacing: number, + uploadCharTexture: boolean = true ): TextMetrics { const subFont = renderer._getSubFont(); const fontString = subFont.nativeFontString; @@ -137,7 +138,7 @@ export class TextUtils { for (let j = 0, m = subText.length; j < m; ++j) { const char = subText[j]; - const charInfo = TextUtils._getCharInfo(char, fontString, subFont); + const charInfo = TextUtils._getCharInfo(char, fontString, subFont, uploadCharTexture); const charCode = char.charCodeAt(0); const isSpace = charCode === 32; @@ -284,7 +285,8 @@ export class TextUtils { renderer: ITextRenderer, rendererHeight: number, lineSpacing: number, - characterSpacing: number + characterSpacing: number, + uploadCharTexture: boolean = true ): TextMetrics { const subFont = renderer._getSubFont(); const fontString = subFont.nativeFontString; @@ -306,7 +308,7 @@ export class TextUtils { let maxDescent = 0; for (let j = 0; j < lineLength; ++j) { - const charInfo = TextUtils._getCharInfo(line[j], fontString, subFont); + const charInfo = TextUtils._getCharInfo(line[j], fontString, subFont, uploadCharTexture); curWidth += charInfo.xAdvance; const { offsetY } = charInfo; const halfH = charInfo.h * 0.5; @@ -337,6 +339,85 @@ export class TextUtils { }; } + /** + * Measure text in SHRINK overflow mode: keep shrinking the font size until the text fits + * within the bounds (both width and height). Mirrors Cocos Creator's `Overflow.SHRINK`. + * + * @param renderer - The text renderer + * @param rendererWidth - The width of the bounds in pixels + * @param rendererHeight - The height of the bounds in pixels + * @param originalFontSize - The font size set on the renderer (the upper bound, never enlarged) + * @param lineSpacing - The line spacing ratio (relative to font size) + * @param characterSpacing - The character spacing ratio (relative to font size) + * @param enableWrapping - Whether wrapping is enabled + * @param applyFontSize - Callback that switches the renderer's sub font to the given font size + * @returns The fitted text metrics and the actual font size used for layout + */ + static measureTextWithShrink( + renderer: ITextRenderer, + rendererWidth: number, + rendererHeight: number, + originalFontSize: number, + lineSpacing: number, + characterSpacing: number, + enableWrapping: boolean, + applyFontSize: (fontSize: number) => void + ): { metrics: TextMetrics; fontSize: number } { + // During the binary search we only need the text dimensions, so pass `uploadCharTexture=false` + // to avoid building GPU font atlases for the intermediate font sizes that won't be used. + // The fitted size is then re-measured once with upload=true to populate its atlas. This trades + // one extra full measure pass (CPU) for skipping ~log2(size) throwaway atlas textures (GPU); + // we can't reuse the search's last measure because its char bitmaps were never cached. + const measureAt = (fontSize: number, uploadCharTexture: boolean): TextMetrics => { + applyFontSize(fontSize); + return enableWrapping + ? TextUtils.measureTextWithWrap( + renderer, + rendererWidth, + rendererHeight, + lineSpacing * fontSize, + characterSpacing * fontSize, + uploadCharTexture + ) + : TextUtils.measureTextWithoutWrap( + renderer, + rendererHeight, + lineSpacing * fontSize, + characterSpacing * fontSize, + uploadCharTexture + ); + }; + // The content height is `lineHeight * lineCount`; `metrics.height` is clamped to the bounds + // unless overflowMode is Overflow, so it can't be used to detect vertical overflow here. + const isFit = (metrics: TextMetrics): boolean => + metrics.width <= rendererWidth && metrics.lineHeight * metrics.lines.length <= rendererHeight; + + // If the text already fits at the original size, keep it (SHRINK only shrinks, never enlarges). + let metrics = measureAt(originalFontSize, false); + if (isFit(metrics)) { + metrics = measureAt(originalFontSize, true); + return { metrics, fontSize: originalFontSize }; + } + + // Binary search for the largest integer font size that fits (measure only, no atlas upload). + let low = 1; + let high = Math.floor(originalFontSize); + let fitFontSize = low; + while (low <= high) { + const mid = (low + high) >> 1; + metrics = measureAt(mid, false); + if (isFit(metrics)) { + fitFontSize = mid; + low = mid + 1; + } else { + high = mid - 1; + } + } + // Re-measure the fitted size with upload=true so the sub font / atlas correspond to it. + metrics = measureAt(fitFontSize, true); + return { metrics, fontSize: fitFontSize }; + } + /** * Get native font hash. * @param fontName - The font name @@ -462,12 +543,16 @@ export class TextUtils { /** * @internal */ - static _getCharInfo(char: string, fontString: string, font: SubFont): CharInfo { + static _getCharInfo(char: string, fontString: string, font: SubFont, uploadCharTexture: boolean = true): CharInfo { let charInfo = font._getCharInfo(char); if (!charInfo) { charInfo = TextUtils.measureChar(char, fontString); - font._uploadCharTexture(charInfo); - font._addCharInfo(char, charInfo); + // SHRINK 的二分阶段只需字符尺寸(measureChar 已给出),传 uploadCharTexture=false 跳过 GPU + // 字形图集上传与缓存,避免给用不到的中间字号建字体 atlas 纹理。 + if (uploadCharTexture) { + font._uploadCharTexture(charInfo); + font._addCharInfo(char, charInfo); + } } return charInfo; diff --git a/packages/core/src/Camera.ts b/packages/core/src/Camera.ts index 53b689dd84..3faf19a2ef 100644 --- a/packages/core/src/Camera.ts +++ b/packages/core/src/Camera.ts @@ -9,7 +9,7 @@ import { Transform } from "./Transform"; import { UpdateFlagManager } from "./UpdateFlagManager"; import { VirtualCamera } from "./VirtualCamera"; import { GLCapabilityType, Logger } from "./base"; -import { deepClone, ignoreClone } from "./clone/CloneManager"; +import { assignmentClone, deepClone, ignoreClone } from "./clone/CloneManager"; import { AntiAliasing } from "./enums/AntiAliasing"; import { CameraClearFlags } from "./enums/CameraClearFlags"; import { CameraModifyFlags } from "./enums/CameraModifyFlags"; @@ -125,8 +125,10 @@ export class Camera extends Component { @deepClone _virtualCamera: VirtualCamera = new VirtualCamera(); /** @internal */ + @assignmentClone _replacementShader: Shader = null; /** @internal */ + @assignmentClone _replacementSubShaderTag: ShaderTagKey = null; /** @internal */ _replacementFailureStrategy: ReplacementFailureStrategy = null; @@ -934,7 +936,15 @@ export class Camera extends Component { private _getInvViewProjMat(): Matrix { if (this._isInvViewProjDirty.flag) { this._isInvViewProjDirty.flag = false; - Matrix.multiply(this._entity.transform.worldMatrix, this._getInverseProjectionMatrix(), this._invViewProjMat); + const matrix = this._invViewProjMat; + if (this._isCustomViewMatrix) { + Matrix.invert(this.viewMatrix, matrix); + } else { + // Ignore scale, consistent with viewMatrix getter + const transform = this._entity.transform; + Matrix.rotationTranslation(transform.worldRotationQuaternion, transform.worldPosition, matrix); + } + matrix.multiply(this._getInverseProjectionMatrix()); } return this._invViewProjMat; } diff --git a/packages/core/src/ComponentsManager.ts b/packages/core/src/ComponentsManager.ts index e9b9a1cc0d..7df837a336 100644 --- a/packages/core/src/ComponentsManager.ts +++ b/packages/core/src/ComponentsManager.ts @@ -36,9 +36,6 @@ export class ComponentsManager { // Render private _onUpdateRenderers = new DisorderedArray(); - // Delay dispose active/inActive Pool - private _componentsContainerPool: Component[][] = []; - addCamera(camera: Camera) { camera._cameraIndex = this._activeCameras.length; this._activeCameras.add(camera); @@ -270,15 +267,6 @@ export class ComponentsManager { ); } - getActiveChangedTempList(): Component[] { - return this._componentsContainerPool.length ? this._componentsContainerPool.pop() : []; - } - - putActiveChangedTempList(componentContainer: Component[]): void { - componentContainer.length = 0; - this._componentsContainerPool.push(componentContainer); - } - /** * @internal */ diff --git a/packages/core/src/Engine.ts b/packages/core/src/Engine.ts index 55b0ea0b4c..69bceed1f3 100644 --- a/packages/core/src/Engine.ts +++ b/packages/core/src/Engine.ts @@ -15,9 +15,8 @@ import { EngineSettings } from "./EngineSettings"; import { Entity } from "./Entity"; import { BatcherManager } from "./RenderPipeline/BatcherManager"; import { RenderContext } from "./RenderPipeline/RenderContext"; -import { RenderElement } from "./RenderPipeline/RenderElement"; import { RenderTargetPool } from "./RenderPipeline/RenderTargetPool"; -import { SubRenderElement } from "./RenderPipeline/SubRenderElement"; +import { RenderElement } from "./RenderPipeline/RenderElement"; import { Scene } from "./Scene"; import { SceneManager } from "./SceneManager"; import { RenderingStatistics } from "./asset/RenderingStatistics"; @@ -33,7 +32,8 @@ import { Shader } from "./shader/Shader"; import { ShaderMacro } from "./shader/ShaderMacro"; import { ShaderMacroCollection } from "./shader/ShaderMacroCollection"; import { ShaderPool } from "./shader/ShaderPool"; -import { ShaderProgramPool } from "./shader/ShaderProgramPool"; +import { ShaderProgramMap } from "./shader/ShaderProgramMap"; +import { ShaderProgram } from "./shader/ShaderProgram"; import { RenderState } from "./shader/state/RenderState"; import { Texture2D, TextureFormat } from "./texture"; import { UIUtils } from "./ui/UIUtils"; @@ -93,9 +93,7 @@ export class Engine extends EventDispatcher { /* @internal */ _renderElementPool = new ClearableObjectPool(RenderElement); /* @internal */ - _subRenderElementPool = new ClearableObjectPool(SubRenderElement); - /* @internal */ - _textSubRenderElementPool = new ClearableObjectPool(SubRenderElement); + _textRenderElementPool = new ClearableObjectPool(RenderElement); /* @internal */ _charRenderInfoPool = new ReturnableObjectPool(CharRenderInfo, 50); @@ -112,7 +110,7 @@ export class Engine extends EventDispatcher { /* @internal */ _renderCount: number = 0; /* @internal */ - _shaderProgramPools: ShaderProgramPool[] = []; + _shaderProgramMaps: ShaderProgramMap[] = []; /** @internal */ _fontMap: Record = {}; /** @internal */ @@ -140,6 +138,22 @@ export class Engine extends EventDispatcher { private _waitingGC: boolean = false; private _postProcessPasses = new Array(); private _activePostProcessPasses = new Array(); + private _lastCanvasWidth: number = -1; + private _lastCanvasHeight: number = -1; + + /** Evict pool entries sized to the previous canvas dimensions. */ + private _onCanvasResize = (): void => { + const canvas = this._canvas; + const newWidth = canvas.width; + const newHeight = canvas.height; + if (this._lastCanvasWidth !== newWidth || this._lastCanvasHeight !== newHeight) { + if (this._lastCanvasWidth >= 0) { + this._renderTargetPool.evictBySize(this._lastCanvasWidth, this._lastCanvasHeight); + } + this._lastCanvasWidth = newWidth; + this._lastCanvasHeight = newHeight; + } + }; private _animate = () => { if (this._vSyncCount) { @@ -258,6 +272,9 @@ export class Engine extends EventDispatcher { this._batcherManager = new BatcherManager(this); this._renderTargetPool = new RenderTargetPool(this); + this._lastCanvasWidth = canvas.width; + this._lastCanvasHeight = canvas.height; + canvas._sizeUpdateFlagManager.addListener(this._onCanvasResize); this.inputManager = new InputManager(this, configuration.input); const { xrDevice } = configuration; @@ -326,12 +343,13 @@ export class Engine extends EventDispatcher { const time = this._time; time._update(); + this._renderTargetPool.tick(time.frameCount); + const deltaTime = time.deltaTime; this._frameInProcess = true; - this._subRenderElementPool.clear(); - this._textSubRenderElementPool.clear(); this._renderElementPool.clear(); + this._textRenderElementPool.clear(); this.xrManager?._update(); const { inputManager, _physicsInitialized: physicsInitialized } = this; @@ -504,6 +522,8 @@ export class Engine extends EventDispatcher { this._destroyed = true; this._waitingDestroy = false; + this._canvas._sizeUpdateFlagManager.removeListener(this._onCanvasResize); + this._sceneManager._destroyAllScene(); this._resourceManager._destroy(); @@ -541,18 +561,18 @@ export class Engine extends EventDispatcher { /** * @internal */ - _getShaderProgramPool(index: number, trackPools?: ShaderProgramPool[]): ShaderProgramPool { - const shaderProgramPools = this._shaderProgramPools; - let pool = shaderProgramPools[index]; - if (!pool) { + _getShaderProgramMap(index: number, trackMaps?: ShaderProgramMap[]): ShaderProgramMap { + const shaderProgramMaps = this._shaderProgramMaps; + let map = shaderProgramMaps[index]; + if (!map) { const length = index + 1; - if (length > shaderProgramPools.length) { - shaderProgramPools.length = length; + if (length > shaderProgramMaps.length) { + shaderProgramMaps.length = length; } - shaderProgramPools[index] = pool = new ShaderProgramPool(this); - trackPools?.push(pool); + shaderProgramMaps[index] = map = new ShaderProgramMap(this); + trackMaps?.push(map); } - return pool; + return map; } /** @@ -674,9 +694,9 @@ export class Engine extends EventDispatcher { private _onDeviceRestored(): void { this._hardwareRenderer.resetState(); this._lastRenderState = new RenderState(); - // Clear shader pools + // Clear shader program maps Shader._clear(this); - this._shaderProgramPools.length = 0; + this._shaderProgramMaps.length = 0; const { resourceManager } = this; // Restore graphic resources @@ -697,9 +717,8 @@ export class Engine extends EventDispatcher { } private _gc(): void { - this._subRenderElementPool.garbageCollection(); - this._textSubRenderElementPool.garbageCollection(); this._renderElementPool.garbageCollection(); + this._textRenderElementPool.garbageCollection(); this._renderContext.garbageCollection(); const scenes = this._sceneManager._scenes.getLoopArray(); for (let i = 0, n = scenes.length; i < n; i++) { diff --git a/packages/core/src/Entity.ts b/packages/core/src/Entity.ts index 08503f5498..5b0c929ac1 100644 --- a/packages/core/src/Entity.ts +++ b/packages/core/src/Entity.ts @@ -9,7 +9,7 @@ import { Script } from "./Script"; import { Transform } from "./Transform"; import { UpdateFlagManager } from "./UpdateFlagManager"; import { ReferResource } from "./asset/ReferResource"; -import { EngineObject } from "./base"; +import { EngineObject, Logger } from "./base"; import { CloneUtils } from "./clone/CloneUtils"; import { ComponentCloner } from "./clone/ComponentCloner"; import { ActiveChangeFlag } from "./enums/ActiveChangeFlag"; @@ -122,7 +122,7 @@ export class Entity extends EngineObject { private _transform: Transform; private _templateResource: ReferResource; private _parent: Entity = null; - private _activeChangedComponents: Component[]; + private _isActiveChanging: boolean = false; private _modifyFlagManager: UpdateFlagManager; /** @@ -212,16 +212,14 @@ export class Entity extends EngineObject { } set siblingIndex(value: number) { - if (this._siblingIndex === -1) { - throw `The entity ${this.name} is not in the hierarchy`; - } - if (this._isRoot) { this._setSiblingIndex(this._scene._rootEntities, value); - } else { + } else if (this._parent) { const parent = this._parent; this._setSiblingIndex(parent._children, value); parent._dispatchModify(EntityModifyFlags.Child, parent); + } else { + Logger.warn(`The entity ${this.name} is not in the hierarchy`); } } @@ -403,6 +401,11 @@ export class Entity extends EngineObject { for (let i = children.length - 1; i >= 0; i--) { const child = children[i]; child._parent = null; + child._siblingIndex = -1; + // Dispatch `Child` to the old parent before `_processInActive` (which unregisters + // UI listeners via `cleanRootCanvas`), so subscribers such as UICanvas can react + // to the hierarchy change while still attached. + this._dispatchModify(EntityModifyFlags.Child, this); let activeChangeFlag = ActiveChangeFlag.None; child._isActiveInHierarchy && (activeChangeFlag |= ActiveChangeFlag.Hierarchy); @@ -410,6 +413,8 @@ export class Entity extends EngineObject { activeChangeFlag && child._processInActive(activeChangeFlag); Entity._traverseSetOwnerScene(child, null); // Must after child._processInActive(). + + child._setParentChange(); } children.length = 0; } @@ -565,24 +570,24 @@ export class Entity extends EngineObject { * @internal */ _processActive(activeChangeFlag: ActiveChangeFlag): void { - if (this._activeChangedComponents) { + if (this._isActiveChanging) { throw "Note: can't set the 'main inActive entity' active in hierarchy, if the operation is in main inActive entity or it's children script's onDisable Event."; } - this._activeChangedComponents = this._scene._componentsManager.getActiveChangedTempList(); - this._setActiveInHierarchy(this._activeChangedComponents, activeChangeFlag); - this._setActiveComponents(true, activeChangeFlag); + this._isActiveChanging = true; + this._setActiveInHierarchy(activeChangeFlag); + this._isActiveChanging = false; } /** * @internal */ _processInActive(activeChangeFlag: ActiveChangeFlag): void { - if (this._activeChangedComponents) { + if (this._isActiveChanging) { throw "Note: can't set the 'main active entity' inActive in hierarchy, if the operation is in main active entity or it's children script's onEnable Event."; } - this._activeChangedComponents = this._scene._componentsManager.getActiveChangedTempList(); - this._setInActiveInHierarchy(this._activeChangedComponents, activeChangeFlag); - this._setActiveComponents(false, activeChangeFlag); + this._isActiveChanging = true; + this._setInActiveInHierarchy(activeChangeFlag); + this._isActiveChanging = false; } /** @@ -679,53 +684,46 @@ export class Entity extends EngineObject { } private _getComponentsInChildren(type: ComponentConstructor, results: T[]): void { - for (let i = this._components.length - 1; i >= 0; i--) { - const component = this._components[i]; + const components = this._components; + for (let i = 0, n = components.length; i < n; i++) { + const component = components[i]; if (component instanceof type) { results.push(component); } } - for (let i = this._children.length - 1; i >= 0; i--) { - this._children[i]._getComponentsInChildren(type, results); - } - } - - private _setActiveComponents(isActive: boolean, activeChangeFlag: ActiveChangeFlag): void { - const activeChangedComponents = this._activeChangedComponents; - for (let i = 0, length = activeChangedComponents.length; i < length; ++i) { - activeChangedComponents[i]._setActive(isActive, activeChangeFlag); + const children = this._children; + for (let i = 0, n = children.length; i < n; i++) { + children[i]._getComponentsInChildren(type, results); } - this._scene._componentsManager.putActiveChangedTempList(activeChangedComponents); - this._activeChangedComponents = null; } - private _setActiveInHierarchy(activeChangedComponents: Component[], activeChangeFlag: ActiveChangeFlag): void { + private _setActiveInHierarchy(activeChangeFlag: ActiveChangeFlag): void { activeChangeFlag & ActiveChangeFlag.Hierarchy && (this._isActiveInHierarchy = true); activeChangeFlag & ActiveChangeFlag.Scene && (this._isActiveInScene = true); const components = this._components; for (let i = 0, n = components.length; i < n; i++) { const component = components[i]; - (component.enabled || !component._awoken) && activeChangedComponents.push(component); + (component.enabled || !component._awoken) && component._setActive(true, activeChangeFlag); } const children = this._children; - for (let i = 0, n = children.length; i < n; i++) { + for (let i = children.length - 1; i >= 0; i--) { const child = children[i]; - child.isActive && child._setActiveInHierarchy(activeChangedComponents, activeChangeFlag); + child.isActive && child._setActiveInHierarchy(activeChangeFlag); } } - private _setInActiveInHierarchy(activeChangedComponents: Component[], activeChangeFlag: ActiveChangeFlag): void { + private _setInActiveInHierarchy(activeChangeFlag: ActiveChangeFlag): void { activeChangeFlag & ActiveChangeFlag.Hierarchy && (this._isActiveInHierarchy = false); activeChangeFlag & ActiveChangeFlag.Scene && (this._isActiveInScene = false); const components = this._components; for (let i = 0, n = components.length; i < n; i++) { const component = components[i]; - component.enabled && activeChangedComponents.push(component); + component.enabled && component._setActive(false, activeChangeFlag); } const children = this._children; - for (let i = 0, n = children.length; i < n; i++) { + for (let i = children.length - 1; i >= 0; i--) { const child = children[i]; - child.isActive && child._setInActiveInHierarchy(activeChangedComponents, activeChangeFlag); + child.isActive && child._setInActiveInHierarchy(activeChangeFlag); } } diff --git a/packages/core/src/RenderPipeline/BasicRenderPipeline.ts b/packages/core/src/RenderPipeline/BasicRenderPipeline.ts index dee2231a0a..98522ef90e 100644 --- a/packages/core/src/RenderPipeline/BasicRenderPipeline.ts +++ b/packages/core/src/RenderPipeline/BasicRenderPipeline.ts @@ -10,7 +10,7 @@ import { ScalableAmbientObscurancePass } from "../lighting/ambientOcclusion/Scal import { FinalPass } from "../postProcess"; import { Shader } from "../shader/Shader"; import { ShaderMacroCollection } from "../shader/ShaderMacroCollection"; -import { ShaderPass } from "../shader/ShaderPass"; +import { SubShader } from "../shader/SubShader"; import { RenderQueueType } from "../shader/enums/RenderQueueType"; import { RenderState } from "../shader/state/RenderState"; import { CascadedShadowCasterPass } from "../shadow/CascadedShadowCasterPass"; @@ -30,7 +30,6 @@ import { OpaqueTexturePass } from "./OpaqueTexturePass"; import { PipelineUtils } from "./PipelineUtils"; import { ContextRendererUpdateFlag, RenderContext } from "./RenderContext"; import { RenderElement } from "./RenderElement"; -import { SubRenderElement } from "./SubRenderElement"; import { PipelineStage } from "./enums/PipelineStage"; /** * Basic render pipeline. @@ -132,15 +131,17 @@ export class BasicRenderPipeline { this._cascadedShadowCasterPass.onRender(context); } - const batcherManager = engine._batcherManager; cullingResults.reset(); // Depth use camera's view and projection matrix this._cullingResults.setRenderUpdateFlagTrue(ContextRendererUpdateFlag.viewProjectionMatrix); context.applyVirtualCamera(camera._virtualCamera, depthPassEnabled); - this._prepareRender(context); + this._prepareWorldRender(context); - cullingResults.sortBatch(batcherManager); + const batcherManager = engine._batcherManager; + cullingResults.sort(); + this._prepareUIRender(context); + cullingResults.batch(batcherManager); batcherManager.uploadBuffer(); if (depthPassEnabled) { @@ -198,19 +199,8 @@ export class BasicRenderPipeline { } this._internalColorTarget = internalColorTarget; - } else { - const internalColorTarget = this._internalColorTarget; - const copyBackgroundTexture = this._copyBackgroundTexture; - const pool = engine._renderTargetPool; - if (internalColorTarget) { - pool.freeRenderTarget(internalColorTarget); - this._internalColorTarget = null; - } - if (copyBackgroundTexture) { - pool.freeTexture(copyBackgroundTexture); - this._copyBackgroundTexture = null; - } } + // Both fields are released at the end of `_drawRenderPass`, so they're null on every entry here. // Scalable ambient obscurance pass // Before opaque pass so materials can sample ambient occlusion in BRDF @@ -361,6 +351,17 @@ export class BasicRenderPipeline { cameraRenderTarget?._blitRenderTarget(); cameraRenderTarget?.generateMipmaps(); + + // Release per-frame leases so the next camera with matching shape can reuse them. + const pool = engine._renderTargetPool; + if (this._internalColorTarget) { + pool.freeRenderTarget(this._internalColorTarget); + this._internalColorTarget = null; + } + if (this._copyBackgroundTexture) { + pool.freeTexture(this._copyBackgroundTexture); + this._copyBackgroundTexture = null; + } } /** @@ -369,84 +370,87 @@ export class BasicRenderPipeline { * @param renderElement - Render element */ pushRenderElement(context: RenderContext, renderElement: RenderElement): void { - renderElement.renderQueueFlags = RenderQueueFlags.None; - const subRenderElements = renderElement.subRenderElements; - for (let i = 0, n = subRenderElements.length; i < n; ++i) { - const subRenderElement = subRenderElements[i]; - const { material } = subRenderElement; - const { renderStates } = material; - const materialSubShader = material.shader.subShaders[0]; - const replacementShader = context.replacementShader; - if (replacementShader) { - const replacementSubShaders = replacementShader.subShaders; - const { replacementTag } = context; - if (replacementTag) { - let replacementSuccess = false; - for (let j = 0, m = replacementSubShaders.length; j < m; j++) { - const subShader = replacementSubShaders[j]; - if (subShader.getTagValue(replacementTag) === materialSubShader.getTagValue(replacementTag)) { - this.pushRenderElementByType(renderElement, subRenderElement, subShader.passes, renderStates); - replacementSuccess = true; - } + const { material } = renderElement; + const { renderStates } = material; + const materialSubShader = material.shader.subShaders[0]; + const replacementShader = context.replacementShader; + if (replacementShader) { + const replacementSubShaders = replacementShader.subShaders; + const { replacementTag } = context; + if (replacementTag) { + let replacementSuccess = false; + for (let j = 0, m = replacementSubShaders.length; j < m; j++) { + const subShader = replacementSubShaders[j]; + if (subShader.getTagValue(replacementTag) === materialSubShader.getTagValue(replacementTag)) { + this._pushRenderElementByType(renderElement, subShader, renderStates); + replacementSuccess = true; } + } - if ( - !replacementSuccess && - context.replacementFailureStrategy === ReplacementFailureStrategy.KeepOriginalShader - ) { - this.pushRenderElementByType(renderElement, subRenderElement, materialSubShader.passes, renderStates); - } - } else { - this.pushRenderElementByType(renderElement, subRenderElement, replacementSubShaders[0].passes, renderStates); + if ( + !replacementSuccess && + context.replacementFailureStrategy === ReplacementFailureStrategy.KeepOriginalShader + ) { + this._pushRenderElementByType(renderElement, materialSubShader, renderStates); } } else { - this.pushRenderElementByType(renderElement, subRenderElement, materialSubShader.passes, renderStates); + this._pushRenderElementByType(renderElement, replacementSubShaders[0], renderStates); } + } else { + this._pushRenderElementByType(renderElement, materialSubShader, renderStates); } } - private pushRenderElementByType( + private _pushRenderElementByType( renderElement: RenderElement, - subRenderElement: SubRenderElement, - shaderPasses: ReadonlyArray, + subShader: SubShader, renderStates: ReadonlyArray ): void { + const shaderPasses = subShader.passes; const cullingResults = this._cullingResults; + renderElement.subShader = subShader; + let pushedQueueFlags = RenderQueueFlags.None; for (let i = 0, n = shaderPasses.length; i < n; i++) { - // Get render queue type let renderQueueType: RenderQueueType; const shaderPass = shaderPasses[i]; const renderState = shaderPass._renderState; if (renderState) { renderQueueType = renderState._getRenderQueueByShaderData( shaderPass._renderStateDataMap, - subRenderElement.material.shaderData + renderElement.material.shaderData ); } else { renderQueueType = renderStates[i].renderQueueType; } const flag = 1 << renderQueueType; - - subRenderElement.shaderPasses = shaderPasses; - subRenderElement.renderQueueFlags |= flag; - - if (renderElement.renderQueueFlags & flag) { + if (pushedQueueFlags & flag) { continue; } + // First queue keeps the original element; subsequent queues each get an isolated + // clone so per-queue batch state (`_isBatched`, `instancedRenderers`) doesn't + // leak across queues for multi-pass materials (e.g. Opaque + Transparent) + let elementForQueue: RenderElement; + if (pushedQueueFlags === RenderQueueFlags.None) { + elementForQueue = renderElement; + } else { + elementForQueue = renderElement.component.engine._renderElementPool.get(); + elementForQueue._cloneFrom(renderElement); + } + switch (renderQueueType) { case RenderQueueType.Opaque: - cullingResults.opaqueQueue.pushRenderElement(renderElement); + cullingResults.opaqueQueue.pushRenderElement(elementForQueue); break; case RenderQueueType.AlphaTest: - cullingResults.alphaTestQueue.pushRenderElement(renderElement); + cullingResults.alphaTestQueue.pushRenderElement(elementForQueue); break; case RenderQueueType.Transparent: - cullingResults.transparentQueue.pushRenderElement(renderElement); + cullingResults.transparentQueue.pushRenderElement(elementForQueue); break; } - renderElement.renderQueueFlags |= flag; + pushedQueueFlags |= flag; } } @@ -482,10 +486,10 @@ export class BasicRenderPipeline { rhi.drawPrimitive(mesh._primitive, mesh.subMesh, program); } - private _prepareRender(context: RenderContext): void { + private _prepareWorldRender(context: RenderContext): void { const camera = context.camera; const { engine, enableFrustumCulling, cullingMask, _frustum: frustum } = camera; - const { _renderers: renderers, _canvases: canvases } = camera.scene._componentsManager; + const renderers = camera.scene._componentsManager._renderers; const rendererElements = renderers._elements; for (let i = renderers.length - 1; i >= 0; --i) { @@ -504,7 +508,12 @@ export class BasicRenderPipeline { renderer._prepareRender(context); renderer._renderFrameCount = engine.time.frameCount; } + } + private _prepareUIRender(context: RenderContext): void { + const camera = context.camera; + const { cullingMask } = camera; + const canvases = camera.scene._componentsManager._canvases; const canvasesElements = canvases._elements; for (let i = canvases.length - 1; i >= 0; i--) { const canvas = canvasesElements[i]; @@ -516,7 +525,10 @@ export class BasicRenderPipeline { continue; } canvas._prepareRender(context); - this.pushRenderElement(context, canvas._renderElement); + const canvasElements = canvas._renderElements; + for (let j = 0, m = canvasElements.length; j < m; j++) { + this.pushRenderElement(context, canvasElements[j]); + } } } } diff --git a/packages/core/src/RenderPipeline/BatchUtils.ts b/packages/core/src/RenderPipeline/BatchUtils.ts deleted file mode 100644 index 387c951f93..0000000000 --- a/packages/core/src/RenderPipeline/BatchUtils.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { SpriteMask, SpriteMaskInteraction, SpriteRenderer } from "../2d"; -import { ShaderTagKey } from "../shader"; -import { SubRenderElement } from "./SubRenderElement"; - -/** - * @internal - */ -export class BatchUtils { - protected static _disableBatchTag: ShaderTagKey = ShaderTagKey.getByName("spriteDisableBatching"); - - static canBatchSprite(elementA: SubRenderElement, elementB: SubRenderElement): boolean { - if (elementB.shaderPasses[0].getTagValue(BatchUtils._disableBatchTag) === true) { - return false; - } - if (elementA.subChunk.chunk !== elementB.subChunk.chunk) { - return false; - } - - const rendererA = elementA.component; - const rendererB = elementB.component; - const maskInteractionA = rendererA.maskInteraction; - - // Compare mask, texture and material - return ( - maskInteractionA === rendererB.maskInteraction && - (maskInteractionA === SpriteMaskInteraction.None || rendererA.maskLayer === rendererB.maskLayer) && - elementA.texture === elementB.texture && - elementA.material === elementB.material - ); - } - - static canBatchSpriteMask(elementA: SubRenderElement, elementB: SubRenderElement): boolean { - if (elementA.subChunk.chunk !== elementB.subChunk.chunk) { - return false; - } - - const alphaCutoffProperty = SpriteMask._alphaCutoffProperty; - - // Compare renderer property - return ( - elementA.texture === elementB.texture && - (elementA.component).shaderData.getFloat(alphaCutoffProperty) === - (elementB.component).shaderData.getFloat(alphaCutoffProperty) - ); - } - - static batchFor2D(elementA: SubRenderElement, elementB?: SubRenderElement): void { - const subChunk = elementB ? elementB.subChunk : elementA.subChunk; - const { chunk, indices: subChunkIndices } = subChunk; - - const length = subChunkIndices.length; - let startIndex = chunk.updateIndexLength; - if (elementB) { - elementA.subChunk.subMesh.count += length; - } else { - // Reset subMesh - const subMesh = subChunk.subMesh; - subMesh.start = startIndex; - subMesh.count = length; - } - - const { start, size } = subChunk.vertexArea; - const vertexOffset = start / 9; - const indices = chunk.indices; - for (let i = 0; i < length; ++i) { - indices[startIndex++] = vertexOffset + subChunkIndices[i]; - } - chunk.updateIndexLength += length; - chunk.updateVertexStart = Math.min(chunk.updateVertexStart, start); - chunk.updateVertexEnd = Math.max(chunk.updateVertexEnd, start + size); - } -} diff --git a/packages/core/src/RenderPipeline/BatcherManager.ts b/packages/core/src/RenderPipeline/BatcherManager.ts index 702a4a01e8..05ec1b7e89 100644 --- a/packages/core/src/RenderPipeline/BatcherManager.ts +++ b/packages/core/src/RenderPipeline/BatcherManager.ts @@ -1,8 +1,9 @@ import { Engine } from "../Engine"; import { Renderer } from "../Renderer"; +import { InstanceBuffer } from "./InstanceBuffer"; import { PrimitiveChunkManager } from "./PrimitiveChunkManager"; import { RenderQueue } from "./RenderQueue"; -import { SubRenderElement } from "./SubRenderElement"; +import { RenderElement } from "./RenderElement"; /** * @internal @@ -11,9 +12,14 @@ export class BatcherManager { private _primitiveChunkManager2D: PrimitiveChunkManager; private _primitiveChunkManagerMask: PrimitiveChunkManager; private _primitiveChunkManagerUI: PrimitiveChunkManager; + private _instanceBuffer: InstanceBuffer; constructor(public engine: Engine) {} + get instanceBuffer(): InstanceBuffer { + return (this._instanceBuffer ||= new InstanceBuffer(this.engine)); + } + get primitiveChunkManager2D(): PrimitiveChunkManager { return (this._primitiveChunkManager2D ||= new PrimitiveChunkManager(this.engine)); } @@ -39,47 +45,40 @@ export class BatcherManager { this._primitiveChunkManagerUI.destroy(); this._primitiveChunkManagerUI = null; } + if (this._instanceBuffer) { + this._instanceBuffer.destroy(); + this._instanceBuffer = null; + } } batch(renderQueue: RenderQueue): void { - const { elements, batchedSubElements, renderQueueType } = renderQueue; - let preSubElement: SubRenderElement; + const { elements, batchedElements } = renderQueue; + + let preElement: RenderElement; let preRenderer: Renderer; let preConstructor: Function; for (let i = 0, n = elements.length; i < n; ++i) { - const subElements = elements[i].subRenderElements; - for (let j = 0, m = subElements.length; j < m; ++j) { - const subElement = subElements[j]; - - // Some sub render elements may not belong to the current render queue - if (!(subElement.renderQueueFlags & (1 << renderQueueType))) { - continue; - } - - const renderer = subElement.component; - const constructor = renderer.constructor; - if (preSubElement) { - if (preConstructor === constructor && preRenderer._canBatch(preSubElement, subElement)) { - preRenderer._batch(preSubElement, subElement); - preSubElement.batched = true; - } else { - batchedSubElements.push(preSubElement); - preSubElement = subElement; - preRenderer = renderer; - preConstructor = constructor; - renderer._batch(subElement); - subElement.batched = false; - } + const curElement = elements[i]; + const renderer = curElement.component; + const constructor = renderer.constructor; + if (preElement) { + if (preConstructor === constructor && preRenderer._canBatch(preElement, curElement)) { + preRenderer._batch(preElement, curElement); } else { - preSubElement = subElement; + batchedElements.push(preElement); + preElement = curElement; preRenderer = renderer; preConstructor = constructor; - renderer._batch(subElement); - subElement.batched = false; + renderer._batch(null, curElement); } + } else { + preElement = curElement; + preRenderer = renderer; + preConstructor = constructor; + renderer._batch(null, curElement); } } - preSubElement && batchedSubElements.push(preSubElement); + preElement && batchedElements.push(preElement); } uploadBuffer() { diff --git a/packages/core/src/RenderPipeline/CullingResults.ts b/packages/core/src/RenderPipeline/CullingResults.ts index 673e0694d3..6ff5c298a7 100644 --- a/packages/core/src/RenderPipeline/CullingResults.ts +++ b/packages/core/src/RenderPipeline/CullingResults.ts @@ -30,6 +30,18 @@ export class CullingResults { this.transparentQueue.sortBatch(RenderQueue.compareForTransparent, batcherManager); } + sort() { + this.opaqueQueue.sort(RenderQueue.compareForOpaque); + this.alphaTestQueue.sort(RenderQueue.compareForOpaque); + this.transparentQueue.sort(RenderQueue.compareForTransparent); + } + + batch(batcherManager: BatcherManager): void { + this.opaqueQueue.batch(batcherManager); + this.alphaTestQueue.batch(batcherManager); + this.transparentQueue.batch(batcherManager); + } + setRenderUpdateFlagTrue(rendererUpdateFlag: ContextRendererUpdateFlag): void { this.opaqueQueue.rendererUpdateFlag |= rendererUpdateFlag; this.transparentQueue.rendererUpdateFlag |= rendererUpdateFlag; diff --git a/packages/core/src/RenderPipeline/InstanceBuffer.ts b/packages/core/src/RenderPipeline/InstanceBuffer.ts new file mode 100644 index 0000000000..f62253f78c --- /dev/null +++ b/packages/core/src/RenderPipeline/InstanceBuffer.ts @@ -0,0 +1,86 @@ +import { Engine } from "../Engine"; +import { Buffer } from "../graphic/Buffer"; +import { BufferBindFlag } from "../graphic/enums/BufferBindFlag"; +import { BufferUsage } from "../graphic/enums/BufferUsage"; +import { SetDataOptions } from "../graphic/enums/SetDataOptions"; +import { Renderer } from "../Renderer"; +import { ShaderMacro } from "../shader/ShaderMacro"; +import { InstanceBufferLayout } from "../shaderlib/ShaderFactory"; + +/** + * @internal + * Manages a UBO for GPU instancing, packing per-instance renderer data (ModelMat, Layer, etc.). + */ +export class InstanceBuffer { + static gpuInstanceMacro = ShaderMacro.getByName("RENDERER_GPU_INSTANCE"); + + buffer: Buffer; + + private _engine: Engine; + private _layout: InstanceBufferLayout; + private _data: ArrayBuffer; + private _floatView: Float32Array; + private _intView: Int32Array; + + constructor(engine: Engine) { + this._engine = engine; + } + + /** + * Set UBO layout and allocate buffer if needed. + */ + setLayout(layout: InstanceBufferLayout): void { + this._layout = layout; + const totalBytes = layout.instanceMaxCount * layout.structSize; + // Only reallocate when buffer is too small + if (!this.buffer || totalBytes > this.buffer.byteLength) { + this._data = new ArrayBuffer(totalBytes); + this._floatView = new Float32Array(this._data); + this._intView = new Int32Array(this._data); + this.buffer?.destroy(); + this.buffer = new Buffer(this._engine, BufferBindFlag.ConstantBuffer, totalBytes, BufferUsage.Dynamic); + } + } + + /** + * Pack renderer data into UBO and upload to GPU. + */ + upload(renderers: Renderer[], start: number, count: number): void { + const { instanceFields, structSize } = this._layout; + const elementsPerInstance = structSize / 4; + const { _floatView: floatView, _intView: intView } = this; + const modelMatId = Renderer._worldMatrixProperty._uniqueId; + + for (let i = 0; i < count; i++) { + const renderer = renderers[start + i]; + const propertyValueMap = renderer.shaderData._propertyValueMap; + const baseOffset = i * elementsPerInstance; + + for (let j = 0, n = instanceFields.length; j < n; j++) { + const field = instanceFields[j]; + const fieldOffset = baseOffset + field.offsetInElements; + const propertyId = field.property._uniqueId; + + if (propertyId === modelMatId) { + // Instancing skips _updateTransformShaderData, so worldMatrix is not in propertyValueMap + // Must read from transform getter to trigger lazy update + field.pack(floatView, fieldOffset, renderer.entity.transform.worldMatrix); + } else { + const value = propertyValueMap[propertyId]; + if (value != null) { + field.pack(field.useIntView ? intView : floatView, fieldOffset, value); + } + } + } + } + + this.buffer.setData(floatView, 0, 0, count * elementsPerInstance, SetDataOptions.Discard); + } + + destroy(): void { + this.buffer?.destroy(); + this._data = null; + this._floatView = null; + this._intView = null; + } +} diff --git a/packages/core/src/RenderPipeline/MaskManager.ts b/packages/core/src/RenderPipeline/MaskManager.ts index 8454e1d584..12284ca611 100644 --- a/packages/core/src/RenderPipeline/MaskManager.ts +++ b/packages/core/src/RenderPipeline/MaskManager.ts @@ -1,4 +1,6 @@ -import { SpriteMask } from "../2d"; +import { Vector3 } from "@galacean/engine-math"; +import { SpriteMaskInteraction } from "../2d/enums/SpriteMaskInteraction"; +import { IMaskRenderable } from "../2d/sprite/MaskRenderable"; import { CameraClearFlags } from "../enums/CameraClearFlags"; import { SpriteMaskLayer } from "../enums/SpriteMaskLayer"; import { Material } from "../material"; @@ -28,17 +30,49 @@ export class MaskManager { hasStencilWritten = false; private _preMaskLayer = SpriteMaskLayer.Nothing; - private _allSpriteMasks = new DisorderedArray(); + private _allSpriteMasks = new DisorderedArray(); + private _filteredMasksByLayer = new Map(); + private _isFilteredMasksDirty = true; - addSpriteMask(mask: SpriteMask): void { + addSpriteMask(mask: IMaskRenderable): void { mask._maskIndex = this._allSpriteMasks.length; this._allSpriteMasks.add(mask); + this._setFilteredMasksDirty(); } - removeSpriteMask(mask: SpriteMask): void { + removeSpriteMask(mask: IMaskRenderable): void { const replaced = this._allSpriteMasks.deleteByIndex(mask._maskIndex); replaced && (replaced._maskIndex = mask._maskIndex); mask._maskIndex = -1; + this._setFilteredMasksDirty(); + } + + onMaskInfluenceLayersChange(): void { + this._setFilteredMasksDirty(); + } + + isVisibleByMask(maskInteraction: SpriteMaskInteraction, maskLayer: SpriteMaskLayer, worldPoint: Vector3): boolean { + if (maskInteraction === SpriteMaskInteraction.None) { + return true; + } + + const masks = this._getMasksByLayer(maskLayer); + let insideMask = false; + for (let i = 0, n = masks.length; i < n; i++) { + if (masks[i]._containsWorldPoint(worldPoint)) { + insideMask = true; + break; + } + } + + switch (maskInteraction) { + case SpriteMaskInteraction.VisibleInsideMask: + return insideMask; + case SpriteMaskInteraction.VisibleOutsideMask: + return !insideMask; + default: + return true; + } } drawMask(context: RenderContext, pipelineStageTagValue: string, maskLayer: SpriteMaskLayer): void { @@ -118,6 +152,38 @@ export class MaskManager { const allSpriteMasks = this._allSpriteMasks; allSpriteMasks.length = 0; allSpriteMasks.garbageCollection(); + this._filteredMasksByLayer.clear(); + this._isFilteredMasksDirty = true; + } + + private _setFilteredMasksDirty(): void { + this._isFilteredMasksDirty = true; + } + + private _getMasksByLayer(maskLayer: SpriteMaskLayer): IMaskRenderable[] { + if (maskLayer === SpriteMaskLayer.Nothing) { + return []; + } + + if (this._isFilteredMasksDirty) { + this._filteredMasksByLayer.clear(); + this._isFilteredMasksDirty = false; + } + + let filteredMasks = this._filteredMasksByLayer.get(maskLayer); + if (!filteredMasks) { + filteredMasks = []; + const allMasks = this._allSpriteMasks; + const maskElements = allMasks._elements; + for (let i = 0, n = allMasks.length; i < n; i++) { + const mask = maskElements[i]; + if (mask.influenceLayers & maskLayer) { + filteredMasks.push(mask); + } + } + this._filteredMasksByLayer.set(maskLayer, filteredMasks); + } + return filteredMasks; } private _buildMaskRenderElement( diff --git a/packages/core/src/RenderPipeline/RenderElement.ts b/packages/core/src/RenderPipeline/RenderElement.ts index 582e72f47d..50cbf8ffe9 100644 --- a/packages/core/src/RenderPipeline/RenderElement.ts +++ b/packages/core/src/RenderPipeline/RenderElement.ts @@ -1,24 +1,67 @@ +import { Renderer } from "../Renderer"; +import { Primitive, SubMesh } from "../graphic"; +import { Material } from "../material"; +import { ShaderData, SubShader } from "../shader"; +import { Texture2D } from "../texture"; import { IPoolElement } from "../utils/ObjectPool"; -import { RenderQueueFlags } from "./BasicRenderPipeline"; -import { SubRenderElement } from "./SubRenderElement"; +import { SubPrimitiveChunk } from "./SubPrimitiveChunk"; export class RenderElement implements IPoolElement { priority: number; distanceForSort: number; - subRenderElements = Array(); - renderQueueFlags: RenderQueueFlags; + component: Renderer; + primitive: Primitive; + material: Material; + subPrimitive: SubMesh; + subShader: SubShader; + shaderData?: ShaderData; + instancedRenderers: Renderer[] = []; - set(priority: number, distanceForSort: number): void { - this.priority = priority; - this.distanceForSort = distanceForSort; - this.subRenderElements.length = 0; + // @todo: maybe should remove later + texture?: Texture2D; + subChunk?: SubPrimitiveChunk; + + set( + component: Renderer, + material: Material, + primitive: Primitive, + subPrimitive: SubMesh, + texture?: Texture2D, + subChunk?: SubPrimitiveChunk + ): void { + this.component = component; + this.material = material; + this.primitive = primitive; + this.subPrimitive = subPrimitive; + this.texture = texture; + this.subChunk = subChunk; + this.instancedRenderers.length = 0; } - addSubRenderElement(element: SubRenderElement): void { - this.subRenderElements.push(element); + /** + * @internal + * Copy identity fields from `source` and reset per-queue batch state, so the same + * logical element can live in multiple queues (e.g. an Opaque + Transparent multi-pass + * material) without one queue's batching corrupting another's `instancedRenderers`. + */ + _cloneFrom(source: RenderElement): void { + this.set(source.component, source.material, source.primitive, source.subPrimitive, source.texture, source.subChunk); + this.priority = source.priority; + this.distanceForSort = source.distanceForSort; + this.subShader = source.subShader; + this.shaderData = source.shaderData; } dispose(): void { - this.subRenderElements.length = 0; + this.component = null; + this.material = null; + this.primitive = null; + this.subPrimitive = null; + this.subShader = null; + this.shaderData && (this.shaderData = null); + this.instancedRenderers = null; + + this.texture && (this.texture = null); + this.subChunk && (this.subChunk = null); } } diff --git a/packages/core/src/RenderPipeline/RenderQueue.ts b/packages/core/src/RenderPipeline/RenderQueue.ts index 3558679996..5f4a4d89af 100644 --- a/packages/core/src/RenderPipeline/RenderQueue.ts +++ b/packages/core/src/RenderPipeline/RenderQueue.ts @@ -3,10 +3,11 @@ import { BasicResources, RenderStateElementMap } from "../BasicResources"; import { Utils } from "../Utils"; import { RenderQueueType, Shader } from "../shader"; import { ShaderMacroCollection } from "../shader/ShaderMacroCollection"; +import { ConstantBufferBindingPoint } from "../shader/enums/ConstantBufferBindingPoint"; import { BatcherManager } from "./BatcherManager"; +import { InstanceBuffer } from "./InstanceBuffer"; import { ContextRendererUpdateFlag, RenderContext } from "./RenderContext"; import { RenderElement } from "./RenderElement"; -import { SubRenderElement } from "./SubRenderElement"; import { RenderQueueMaskType } from "./enums/RenderQueueMaskType"; /** @@ -14,7 +15,11 @@ import { RenderQueueMaskType } from "./enums/RenderQueueMaskType"; */ export class RenderQueue { static compareForOpaque(a: RenderElement, b: RenderElement): number { - return a.priority - b.priority || a.distanceForSort - b.distanceForSort; + return ( + a.priority - b.priority || + a.material.instanceId - b.material.instanceId || + a.primitive.instanceId - b.primitive.instanceId + ); } static compareForTransparent(a: RenderElement, b: RenderElement): number { @@ -22,7 +27,7 @@ export class RenderQueue { } readonly elements = new Array(); - readonly batchedSubElements = new Array(); + readonly batchedElements = new Array(); rendererUpdateFlag = ContextRendererUpdateFlag.None; constructor(public renderQueueType: RenderQueueType) {} @@ -36,6 +41,10 @@ export class RenderQueue { this.batch(batcherManager); } + sort(compareFunc: Function): void { + Utils._quickSort(this.elements, 0, this.elements.length, compareFunc); + } + batch(batcherManager: BatcherManager): void { batcherManager.batch(this); } @@ -45,8 +54,8 @@ export class RenderQueue { pipelineStageTagValue: string, maskType: RenderQueueMaskType = RenderQueueMaskType.No ): void { - const batchedSubElements = this.batchedSubElements; - const length = batchedSubElements.length; + const batchedElements = this.batchedElements; + const length = batchedElements.length; if (length === 0) { return; } @@ -57,34 +66,33 @@ export class RenderQueue { const rhi = engine._hardwareRenderer; const pipelineStageKey = RenderContext.pipelineStageKey; const renderQueueType = this.renderQueueType; + const needMaskType = maskType !== RenderQueueMaskType.No; for (let i = 0; i < length; i++) { - const subElement = batchedSubElements[i]; - const { component: renderer, batched, material } = subElement; - - // @todo: Can optimize update view projection matrix updated - if ( - this.rendererUpdateFlag & ContextRendererUpdateFlag.WorldViewMatrix || - renderer._batchedTransformShaderData != batched - ) { - // Update world matrix and view matrix and model matrix - renderer._updateTransformShaderData(context, false, batched); - renderer._batchedTransformShaderData = batched; - } else if (this.rendererUpdateFlag & ContextRendererUpdateFlag.ProjectionMatrix) { - // Only projection matrix need updated - renderer._updateTransformShaderData(context, true, batched); + const curElement = batchedElements[i]; + const { component, material } = curElement; + const isInstanced = curElement.instancedRenderers.length > 0; + + // Update transform shader data + // Instancing packs per-renderer transforms into the instance UBO at draw time, so skip here + if (!isInstanced) { + if (this.rendererUpdateFlag & ContextRendererUpdateFlag.WorldViewMatrix) { + component._updateTransformShaderData(context, false); + } else if (this.rendererUpdateFlag & ContextRendererUpdateFlag.ProjectionMatrix) { + component._updateTransformShaderData(context, true); + } } - const maskInteraction = renderer._maskInteraction; + // Resolve mask render states + const maskInteraction = component._maskInteraction; const needMaskInteraction = maskInteraction !== SpriteMaskInteraction.None; - const needMaskType = maskType !== RenderQueueMaskType.No; let customStates: RenderStateElementMap = null; if (needMaskType) { customStates = BasicResources.getMaskTypeRenderStates(maskType); } else { if (needMaskInteraction) { - maskManager.drawMask(context, pipelineStageTagValue, subElement.component._maskLayer); + maskManager.drawMask(context, pipelineStageTagValue, component._maskLayer); customStates = BasicResources.getMaskInteractionRenderStates(maskInteraction); } else { maskManager.isReadStencil(material) && maskManager.clearMask(context, pipelineStageTagValue); @@ -92,21 +100,27 @@ export class RenderQueue { maskManager.isStencilWritten(material) && (maskManager.hasStencilWritten = true); } - const compileMacros = Shader._compileMacros; - const { primitive, shaderPasses, shaderData: renderElementShaderData } = subElement; - const { shaderData: rendererData, instanceId: rendererId } = renderer; + const { shaderData: renderElementShaderData } = curElement; + const shaderPasses = curElement.subShader.passes; + const { shaderData: rendererData, instanceId: rendererId } = component; const { shaderData: materialData, instanceId: materialId, renderStates } = material; - // Union render global macro and material self macro - ShaderMacroCollection.unionCollection(renderer._globalShaderMacro, materialData._macroCollection, compileMacros); + // Build compile macros + const compileMacros = Shader._compileMacros; + ShaderMacroCollection.unionCollection(component._globalShaderMacro, materialData._macroCollection, compileMacros); ShaderMacroCollection.unionCollection(compileMacros, engine._macroCollection, compileMacros); + if (isInstanced) { + compileMacros.enable(InstanceBuffer.gpuInstanceMacro); + } + for (let j = 0, m = shaderPasses.length; j < m; j++) { const shaderPass = shaderPasses[j]; if (shaderPass.getTagValue(pipelineStageKey) !== pipelineStageTagValue) { continue; } + // Pick render state and filter by queue type let renderState = shaderPass._renderState; if (needMaskType) { // Mask don't care render queue type @@ -134,18 +148,21 @@ export class RenderQueue { const switchProgram = program.bind(); const switchRenderCount = renderCount !== program._uploadRenderCount; + // Upload uniforms (cache-aware per block). Renderer block carries plain samplers/arrays + // even on the instanced path (GLSL forbids them in UBOs); `_canBatch` ensures the whole + // batch agrees on those, so the leader's values are correct for the draw call if (switchRenderCount) { program.groupingOtherUniformBlock(); program.uploadAll(program.sceneUniformBlock, sceneData); program.uploadAll(program.cameraUniformBlock, cameraData); program.uploadAll(program.rendererUniformBlock, rendererData); + program._uploadRendererId = isInstanced ? -1 : rendererId; program.uploadAll(program.materialUniformBlock, materialData); renderElementShaderData && program.uploadAll(program.renderElementUniformBlock, renderElementShaderData); - // UnGroup textures should upload default value, texture uint maybe change by logic of texture bind. + // UnGroup textures should upload default value, texture uint maybe change by logic of texture bind program.uploadUnGroupTextures(); program._uploadSceneId = sceneId; program._uploadCameraId = cameraId; - program._uploadRendererId = rendererId; program._uploadMaterialId = materialId; program._uploadRenderCount = renderCount; } else { @@ -163,7 +180,11 @@ export class RenderQueue { program.uploadTextures(program.cameraUniformBlock, cameraData); } - if (program._uploadRendererId !== rendererId) { + if (isInstanced) { + // Different batches may have different leaders, re-upload every time + program.uploadAll(program.rendererUniformBlock, rendererData); + program._uploadRendererId = -1; + } else if (program._uploadRendererId !== rendererId) { program.uploadAll(program.rendererUniformBlock, rendererData); program._uploadRendererId = rendererId; } else if (switchProgram) { @@ -179,20 +200,41 @@ export class RenderQueue { renderElementShaderData && program.uploadAll(program.renderElementUniformBlock, renderElementShaderData); - // We only consider switchProgram case, because UnGroup texture's value is always default. + // We only consider switchProgram case, because UnGroup texture's value is always default if (switchProgram) { program.uploadUnGroupTextures(); } } + // Apply render state renderState._applyStates( engine, - renderer._isFrontFaceInvert(), + component._isFrontFaceInvert(), shaderPass._renderStateDataMap, material.shaderData, customStates ); - rhi.drawPrimitive(primitive, subElement.subPrimitive, program); + + // Draw + const layout = program._instanceLayout; + if (isInstanced && layout) { + const { primitive, subPrimitive, instancedRenderers } = curElement; + const totalCount = instancedRenderers.length; + const maxCount = layout.instanceMaxCount; + const instanceBuffer = engine._batcherManager.instanceBuffer; + + instanceBuffer.setLayout(layout); + rhi.bindUniformBufferBase(ConstantBufferBindingPoint.RendererInstance, instanceBuffer.buffer._platformBuffer); + for (let start = 0; start < totalCount; start += maxCount) { + const count = Math.min(maxCount, totalCount - start); + instanceBuffer.upload(instancedRenderers, start, count); + primitive.instanceCount = count; + rhi.drawPrimitive(primitive, subPrimitive, program); + } + primitive.instanceCount = 0; + } else { + rhi.drawPrimitive(curElement.primitive, curElement.subPrimitive, program); + } } } @@ -201,7 +243,7 @@ export class RenderQueue { clear(): void { this.elements.length = 0; - this.batchedSubElements.length = 0; + this.batchedElements.length = 0; } destroy(): void {} diff --git a/packages/core/src/RenderPipeline/RenderTargetPool.ts b/packages/core/src/RenderPipeline/RenderTargetPool.ts index 3eae130d62..b5d6ecb382 100644 --- a/packages/core/src/RenderPipeline/RenderTargetPool.ts +++ b/packages/core/src/RenderPipeline/RenderTargetPool.ts @@ -5,9 +5,73 @@ import { RenderTarget, Texture2D, TextureFilterMode, TextureFormat, TextureWrapM * @internal */ export class RenderTargetPool { + private static _destroyRenderTargetResource(rt: RenderTarget): void { + const colorTexture = rt.getColorTexture(0); + const depthTexture = rt.depthTexture; + rt.destroy(true); + colorTexture?.destroy(true); + if (depthTexture && depthTexture !== colorTexture) { + depthTexture.destroy(true); + } + } + + private static _matchRenderTarget( + renderTarget: RenderTarget, + width: number, + height: number, + colorFormat: TextureFormat | null, + depthFormat: TextureFormat | null, + needDepthTexture: boolean, + mipmap: boolean, + isSRGBColorSpace: boolean, + antiAliasing: number + ): boolean { + if (renderTarget.width !== width || renderTarget.height !== height || renderTarget.antiAliasing !== antiAliasing) { + return false; + } + + const colorTexture = renderTarget.getColorTexture(0) as Texture2D; + if (colorFormat != null) { + if ( + !colorTexture || + colorTexture.format !== colorFormat || + colorTexture.mipmapCount > 1 !== mipmap || + colorTexture.isSRGBColorSpace !== isSRGBColorSpace + ) { + return false; + } + } else if (colorTexture) { + return false; + } + + const depthTexture = renderTarget.depthTexture; + if (needDepthTexture) { + if (depthFormat) { + if (!depthTexture || (depthTexture as Texture2D).format !== depthFormat) { + return false; + } + } else if (depthTexture) { + return false; + } + } else { + if (renderTarget._depthFormat !== depthFormat) { + return false; + } + } + + return true; + } + + /** Frames an entry may sit idle before `tick()` destroys it. */ + maxFreeAgeFrames: number = 60; + + private _engine: Engine; + private _freeRenderTargets: RenderTarget[] = []; + private _freeRenderTargetFrames: number[] = []; + private _freeTextures: Texture2D[] = []; - private _engine: Engine; + private _freeTextureFrames: number[] = []; constructor(engine: Engine) { this._engine = engine; @@ -41,8 +105,7 @@ export class RenderTargetPool { antiAliasing ) ) { - freeRenderTargets[i] = freeRenderTargets[freeRenderTargets.length - 1]; - freeRenderTargets.length--; + this._removeFreeRenderTargetAt(i); const colorTexture = renderTarget.getColorTexture(0) as Texture2D; if (colorTexture) { colorTexture.wrapModeU = colorTexture.wrapModeV = wrapMode; @@ -103,8 +166,7 @@ export class RenderTargetPool { texture.mipmapCount > 1 === mipmap && texture.isSRGBColorSpace === isSRGBColorSpace ) { - freeTextures[i] = freeTextures[freeTextures.length - 1]; - freeTextures.length--; + this._removeFreeTextureAt(i); texture.wrapModeU = texture.wrapModeV = wrapMode; texture.filterMode = filterMode; return texture; @@ -122,78 +184,97 @@ export class RenderTargetPool { freeRenderTarget(renderTarget: RenderTarget): void { if (!renderTarget || renderTarget.destroyed) return; this._freeRenderTargets.push(renderTarget); + this._freeRenderTargetFrames.push(this._engine.time.frameCount); } freeTexture(texture: Texture2D): void { if (!texture || texture.destroyed) return; this._freeTextures.push(texture); + this._freeTextureFrames.push(this._engine.time.frameCount); + } + + tick(currentFrame: number): void { + const maxAge = this.maxFreeAgeFrames; + const rtFrames = this._freeRenderTargetFrames; + for (let i = rtFrames.length - 1; i >= 0; i--) { + if (currentFrame - rtFrames[i] > maxAge) { + this._destroyFreeRenderTargetAt(i); + } + } + const texFrames = this._freeTextureFrames; + for (let i = texFrames.length - 1; i >= 0; i--) { + if (currentFrame - texFrames[i] > maxAge) { + this._destroyFreeTextureAt(i); + } + } + } + + evictBySize(width: number, height: number): void { + const freeRenderTargets = this._freeRenderTargets; + for (let i = freeRenderTargets.length - 1; i >= 0; i--) { + const rt = freeRenderTargets[i]; + if (rt.width === width && rt.height === height) { + this._destroyFreeRenderTargetAt(i); + } + } + const freeTextures = this._freeTextures; + for (let i = freeTextures.length - 1; i >= 0; i--) { + const tex = freeTextures[i]; + if (tex.width === width && tex.height === height) { + this._destroyFreeTextureAt(i); + } + } } gc(): void { const freeRenderTargets = this._freeRenderTargets; for (let i = 0, n = freeRenderTargets.length; i < n; i++) { - const renderTarget = freeRenderTargets[i]; - const colorTexture = renderTarget.getColorTexture(0); - const depthTexture = renderTarget.depthTexture; - renderTarget.destroy(true); - colorTexture?.destroy(true); - if (depthTexture && depthTexture !== colorTexture) { - depthTexture.destroy(true); - } + RenderTargetPool._destroyRenderTargetResource(freeRenderTargets[i]); } freeRenderTargets.length = 0; + this._freeRenderTargetFrames.length = 0; const freeTextures = this._freeTextures; for (let i = 0, n = freeTextures.length; i < n; i++) { freeTextures[i].destroy(true); } freeTextures.length = 0; + this._freeTextureFrames.length = 0; } - private static _matchRenderTarget( - renderTarget: RenderTarget, - width: number, - height: number, - colorFormat: TextureFormat | null, - depthFormat: TextureFormat | null, - needDepthTexture: boolean, - mipmap: boolean, - isSRGBColorSpace: boolean, - antiAliasing: number - ): boolean { - if (renderTarget.width !== width || renderTarget.height !== height || renderTarget.antiAliasing !== antiAliasing) { - return false; + private _removeFreeRenderTargetAt(index: number): void { + const rts = this._freeRenderTargets; + const frames = this._freeRenderTargetFrames; + const last = rts.length - 1; + if (index !== last) { + rts[index] = rts[last]; + frames[index] = frames[last]; } + rts.length = last; + frames.length = last; + } - const colorTexture = renderTarget.getColorTexture(0) as Texture2D; - if (colorFormat != null) { - if ( - !colorTexture || - colorTexture.format !== colorFormat || - colorTexture.mipmapCount > 1 !== mipmap || - colorTexture.isSRGBColorSpace !== isSRGBColorSpace - ) { - return false; - } - } else if (colorTexture) { - return false; + private _removeFreeTextureAt(index: number): void { + const texs = this._freeTextures; + const frames = this._freeTextureFrames; + const last = texs.length - 1; + if (index !== last) { + texs[index] = texs[last]; + frames[index] = frames[last]; } + texs.length = last; + frames.length = last; + } - const depthTexture = renderTarget.depthTexture; - if (needDepthTexture) { - if (depthFormat) { - if (!depthTexture || (depthTexture as Texture2D).format !== depthFormat) { - return false; - } - } else if (depthTexture) { - return false; - } - } else { - if (renderTarget._depthFormat !== depthFormat) { - return false; - } - } + private _destroyFreeRenderTargetAt(index: number): void { + const rt = this._freeRenderTargets[index]; + this._removeFreeRenderTargetAt(index); + RenderTargetPool._destroyRenderTargetResource(rt); + } - return true; + private _destroyFreeTextureAt(index: number): void { + const tex = this._freeTextures[index]; + this._removeFreeTextureAt(index); + tex.destroy(true); } } diff --git a/packages/core/src/RenderPipeline/SubRenderElement.ts b/packages/core/src/RenderPipeline/SubRenderElement.ts deleted file mode 100644 index f8c86c114d..0000000000 --- a/packages/core/src/RenderPipeline/SubRenderElement.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Renderer } from "../Renderer"; -import { Primitive, SubMesh } from "../graphic"; -import { Material } from "../material"; -import { ShaderData, ShaderPass } from "../shader"; -import { Texture2D } from "../texture"; -import { IPoolElement } from "../utils/ObjectPool"; -import { RenderQueueFlags } from "./BasicRenderPipeline"; -import { SubPrimitiveChunk } from "./SubPrimitiveChunk"; - -export class SubRenderElement implements IPoolElement { - component: Renderer; - primitive: Primitive; - material: Material; - subPrimitive: SubMesh; - shaderPasses: ReadonlyArray; - shaderData?: ShaderData; - batched: boolean; - renderQueueFlags: RenderQueueFlags; - - // @todo: maybe should remove later - texture?: Texture2D; - subChunk?: SubPrimitiveChunk; - - set( - component: Renderer, - material: Material, - primitive: Primitive, - subPrimitive: SubMesh, - texture?: Texture2D, - subChunk?: SubPrimitiveChunk - ): void { - this.component = component; - this.material = material; - this.primitive = primitive; - this.subPrimitive = subPrimitive; - this.texture = texture; - this.subChunk = subChunk; - } - - dispose(): void { - this.component = null; - this.material = null; - this.primitive = null; - this.subPrimitive = null; - this.shaderPasses = null; - this.shaderData && (this.shaderData = null); - - this.texture && (this.texture = null); - this.subChunk && (this.subChunk = null); - } -} diff --git a/packages/core/src/RenderPipeline/VertexMergeBatcher.ts b/packages/core/src/RenderPipeline/VertexMergeBatcher.ts new file mode 100644 index 0000000000..67565f8071 --- /dev/null +++ b/packages/core/src/RenderPipeline/VertexMergeBatcher.ts @@ -0,0 +1,121 @@ +import { SpriteMask, SpriteMaskInteraction, SpriteRenderer } from "../2d"; +import { ShaderTagKey } from "../shader"; +import { RenderElement } from "./RenderElement"; + +/** + * @internal + */ +export class VertexMergeBatcher { + protected static _disableBatchTag: ShaderTagKey = ShaderTagKey.getByName("spriteDisableBatching"); + + static canBatchSprite(preElement: RenderElement, curElement: RenderElement): boolean { + const preRenderer = preElement.component; + const renderer = curElement.component; + const maskInteraction = preRenderer.maskInteraction; + + const preRendererAny = preRenderer as any; + const curRendererAny = renderer as any; + const rectMaskEnabledA = preRendererAny._rectMaskEnabled; + if (rectMaskEnabledA !== curRendererAny._rectMaskEnabled) { + return false; + } + if (rectMaskEnabledA) { + const rectMaskRectA = preRendererAny._rectMaskRect; + const rectMaskRectB = curRendererAny._rectMaskRect; + const rectMaskSoftnessA = preRendererAny._rectMaskSoftness; + const rectMaskSoftnessB = curRendererAny._rectMaskSoftness; + if ( + !rectMaskRectA || + !rectMaskRectB || + !rectMaskSoftnessA || + !rectMaskSoftnessB || + rectMaskRectA.x !== rectMaskRectB.x || + rectMaskRectA.y !== rectMaskRectB.y || + rectMaskRectA.z !== rectMaskRectB.z || + rectMaskRectA.w !== rectMaskRectB.w || + rectMaskSoftnessA.x !== rectMaskSoftnessB.x || + rectMaskSoftnessA.y !== rectMaskSoftnessB.y || + rectMaskSoftnessA.z !== rectMaskSoftnessB.z || + rectMaskSoftnessA.w !== rectMaskSoftnessB.w || + preRendererAny._rectMaskHardClip !== curRendererAny._rectMaskHardClip + ) { + return false; + } + } + + // Order: cheap reference checks → mask state → tag lookup (rare opt-out) + return ( + preElement.subChunk.chunk === curElement.subChunk.chunk && + preElement.texture === curElement.texture && + preElement.material === curElement.material && + maskInteraction === renderer.maskInteraction && + (maskInteraction === SpriteMaskInteraction.None || preRenderer.maskLayer === renderer.maskLayer) && + curElement.subShader.passes[0].getTagValue(VertexMergeBatcher._disableBatchTag) !== true + ); + } + + /** + * Text-specific batch check: extends sprite check with outline parity. + * Different outlineWidth or outlineColor must split into separate draw calls, + * because outline uniforms are shared per draw call. + */ + static canBatchText(preElement: RenderElement, curElement: RenderElement): boolean { + if (!VertexMergeBatcher.canBatchSprite(preElement, curElement)) { + return false; + } + const preRendererAny = preElement.component as any; + const curRendererAny = curElement.component as any; + if (preRendererAny._outlineWidth !== curRendererAny._outlineWidth) { + return false; + } + if (preRendererAny._outlineWidth > 0) { + const a = preRendererAny._outlineColor; + const b = curRendererAny._outlineColor; + if (a.r !== b.r || a.g !== b.g || a.b !== b.b || a.a !== b.a) { + return false; + } + } + return true; + } + + static canBatchSpriteMask(preElement: RenderElement, curElement: RenderElement): boolean { + if (preElement.subChunk.chunk !== curElement.subChunk.chunk) { + return false; + } + + const alphaCutoffProperty = SpriteMask._alphaCutoffProperty; + + // Compare renderer property + return ( + preElement.texture === curElement.texture && + (preElement.component).shaderData.getFloat(alphaCutoffProperty) === + (curElement.component).shaderData.getFloat(alphaCutoffProperty) + ); + } + + static batch(preElement: RenderElement | null, curElement: RenderElement): void { + const subChunk = curElement.subChunk; + const { chunk, indices: subChunkIndices } = subChunk; + + const length = subChunkIndices.length; + let startIndex = chunk.updateIndexLength; + if (preElement) { + preElement.subChunk.subMesh.count += length; + } else { + // Reset subMesh + const subMesh = subChunk.subMesh; + subMesh.start = startIndex; + subMesh.count = length; + } + + const { start, size } = subChunk.vertexArea; + const vertexOffset = start / 9; + const indices = chunk.indices; + for (let i = 0; i < length; ++i) { + indices[startIndex++] = vertexOffset + subChunkIndices[i]; + } + chunk.updateIndexLength += length; + chunk.updateVertexStart = Math.min(chunk.updateVertexStart, start); + chunk.updateVertexEnd = Math.max(chunk.updateVertexEnd, start + size); + } +} diff --git a/packages/core/src/RenderPipeline/index.ts b/packages/core/src/RenderPipeline/index.ts index 7161b57757..a95c0f8f46 100644 --- a/packages/core/src/RenderPipeline/index.ts +++ b/packages/core/src/RenderPipeline/index.ts @@ -1,5 +1,6 @@ export { BasicRenderPipeline, RenderQueueFlags } from "./BasicRenderPipeline"; -export { BatchUtils } from "./BatchUtils"; +export { VertexMergeBatcher } from "./VertexMergeBatcher"; export { Blitter } from "./Blitter"; -export { RenderQueue } from "./RenderQueue"; export { PipelineStage } from "./enums/PipelineStage"; +export { RenderElement } from "./RenderElement"; +export { RenderQueue } from "./RenderQueue"; diff --git a/packages/core/src/Renderer.ts b/packages/core/src/Renderer.ts index ab9f743c0c..c6444d5354 100644 --- a/packages/core/src/Renderer.ts +++ b/packages/core/src/Renderer.ts @@ -5,7 +5,7 @@ import { Component } from "./Component"; import { DependentMode, dependentComponents } from "./ComponentsDependencies"; import { Entity } from "./Entity"; import { RenderContext } from "./RenderPipeline/RenderContext"; -import { SubRenderElement } from "./RenderPipeline/SubRenderElement"; +import { RenderElement } from "./RenderPipeline/RenderElement"; import { Transform, TransformModifyFlags } from "./Transform"; import { assignmentClone, deepClone, ignoreClone } from "./clone/CloneManager"; import { SpriteMaskLayer } from "./enums/SpriteMaskLayer"; @@ -23,14 +23,15 @@ import { ShaderDataGroup } from "./shader/enums/ShaderDataGroup"; export class Renderer extends Component { private static _tempVector0 = new Vector3(); + /** @internal */ + static _worldMatrixProperty = ShaderProperty.getByName("renderer_ModelMat"); + /** @internal */ + static _rendererLayerProperty = ShaderProperty.getByName("renderer_Layer"); + private static _receiveShadowMacro = ShaderMacro.getByName("RENDERER_IS_RECEIVE_SHADOWS"); - private static _localMatrixProperty = ShaderProperty.getByName("renderer_LocalMat"); - private static _worldMatrixProperty = ShaderProperty.getByName("renderer_ModelMat"); private static _mvMatrixProperty = ShaderProperty.getByName("renderer_MVMat"); private static _mvpMatrixProperty = ShaderProperty.getByName("renderer_MVPMat"); - private static _mvInvMatrixProperty = ShaderProperty.getByName("renderer_MVInvMat"); private static _normalMatrixProperty = ShaderProperty.getByName("renderer_NormalMat"); - private static _rendererLayerProperty = ShaderProperty.getByName("renderer_Layer"); /** @internal */ @ignoreClone @@ -46,12 +47,8 @@ export class Renderer extends Component { _globalShaderMacro: ShaderMacroCollection = new ShaderMacroCollection(); @ignoreClone _renderFrameCount: number; - /** @internal */ @assignmentClone _maskInteraction: SpriteMaskInteraction = SpriteMaskInteraction.None; - /** @internal */ - @ignoreClone - _batchedTransformShaderData: boolean = false; @assignmentClone _maskLayer: SpriteMaskLayer = SpriteMaskLayer.Layer0; @@ -74,8 +71,6 @@ export class Renderer extends Component { @ignoreClone private _mvpMatrix: Matrix = new Matrix(); @ignoreClone - private _mvInvMatrix: Matrix = new Matrix(); - @ignoreClone private _normalMatrix: Matrix = new Matrix(); @ignoreClone private _materialsInstanced: boolean[] = []; @@ -384,7 +379,6 @@ export class Renderer extends Component { this._shaderData = null; this._mvMatrix = null; this._mvpMatrix = null; - this._mvInvMatrix = null; this._normalMatrix = null; this._materialsInstanced = null; this._rendererLayer = null; @@ -393,74 +387,66 @@ export class Renderer extends Component { /** * @internal */ - _updateTransformShaderData(context: RenderContext, onlyMVP: boolean, batched: boolean): void { + _updateTransformShaderData(context: RenderContext, onlyMVP: boolean): void { const worldMatrix = this._transformEntity.transform.worldMatrix; + const { shaderData } = this; if (onlyMVP) { - this._updateProjectionRelatedShaderData(context, worldMatrix, batched); + const mvpMatrix = this._mvpMatrix; + Matrix.multiply(context.viewProjectionMatrix, worldMatrix, mvpMatrix); + shaderData.setMatrix(Renderer._mvpMatrixProperty, mvpMatrix); } else { - this._updateWorldViewRelatedShaderData(context, worldMatrix, batched); + const mvMatrix = this._mvMatrix; + const normalMatrix = this._normalMatrix; + + Matrix.multiply(context.viewMatrix, worldMatrix, mvMatrix); + Matrix.invert(worldMatrix, normalMatrix); + normalMatrix.transpose(); + + shaderData.setMatrix(Renderer._worldMatrixProperty, worldMatrix); + shaderData.setMatrix(Renderer._mvMatrixProperty, mvMatrix); + shaderData.setMatrix(Renderer._normalMatrixProperty, normalMatrix); + + const mvpMatrix = this._mvpMatrix; + Matrix.multiply(context.viewProjectionMatrix, worldMatrix, mvpMatrix); + shaderData.setMatrix(Renderer._mvpMatrixProperty, mvpMatrix); } } /** * @internal */ - _canBatch(elementA: SubRenderElement, elementB: SubRenderElement): boolean { + _canBatch(preElement: RenderElement, curElement: RenderElement): boolean { return false; } /** * @internal */ - _batch(elementA: SubRenderElement, elementB?: SubRenderElement): void {} + _batch(preElement: RenderElement | null, curElement: RenderElement): void {} /** - * Update once per frame per renderer, not influenced by batched. + * Update once per frame per renderer. */ protected _update(context: RenderContext): void { const { layer } = this.entity; this._rendererLayer.set(layer & 65535, (layer >>> 16) & 65535, 0, 0); } - protected _updateWorldViewRelatedShaderData(context: RenderContext, worldMatrix: Matrix, batched: boolean): void { - const { shaderData, _mvInvMatrix: mvInvMatrix } = this; - if (batched) { + /** + * Update transform shader data for world-space vertices (2D renderers). + * Vertices are already in world space, so model matrix is identity. + */ + protected _updateWorldSpaceTransformShaderData(context: RenderContext, onlyMVP: boolean): void { + const { shaderData } = this; + if (onlyMVP) { + shaderData.setMatrix(Renderer._mvpMatrixProperty, context.viewProjectionMatrix); + } else { // @ts-ignore const identityMatrix = Matrix._identity; - - Matrix.invert(context.viewMatrix, mvInvMatrix); - - shaderData.setMatrix(Renderer._localMatrixProperty, identityMatrix); shaderData.setMatrix(Renderer._worldMatrixProperty, identityMatrix); shaderData.setMatrix(Renderer._mvMatrixProperty, context.viewMatrix); - shaderData.setMatrix(Renderer._mvInvMatrixProperty, mvInvMatrix); shaderData.setMatrix(Renderer._normalMatrixProperty, identityMatrix); - } else { - const mvMatrix = this._mvMatrix; - const normalMatrix = this._normalMatrix; - - Matrix.multiply(context.viewMatrix, worldMatrix, mvMatrix); - Matrix.invert(mvMatrix, mvInvMatrix); - Matrix.invert(worldMatrix, normalMatrix); - normalMatrix.transpose(); - - shaderData.setMatrix(Renderer._localMatrixProperty, this._transformEntity.transform.localMatrix); - shaderData.setMatrix(Renderer._worldMatrixProperty, worldMatrix); - shaderData.setMatrix(Renderer._mvMatrixProperty, mvMatrix); - shaderData.setMatrix(Renderer._mvInvMatrixProperty, mvInvMatrix); - shaderData.setMatrix(Renderer._normalMatrixProperty, normalMatrix); - } - - this._updateProjectionRelatedShaderData(context, worldMatrix, batched); - } - - protected _updateProjectionRelatedShaderData(context: RenderContext, worldMatrix: Matrix, batched: boolean): void { - if (batched) { - this.shaderData.setMatrix(Renderer._mvpMatrixProperty, context.viewProjectionMatrix); - } else { - const mvpMatrix = this._mvpMatrix; - Matrix.multiply(context.viewProjectionMatrix, worldMatrix, mvpMatrix); - this.shaderData.setMatrix(Renderer._mvpMatrixProperty, mvpMatrix); + shaderData.setMatrix(Renderer._mvpMatrixProperty, context.viewProjectionMatrix); } } diff --git a/packages/core/src/SceneManager.ts b/packages/core/src/SceneManager.ts index 92de60d66b..1fa51ba265 100644 --- a/packages/core/src/SceneManager.ts +++ b/packages/core/src/SceneManager.ts @@ -91,10 +91,21 @@ export class SceneManager { * @returns scene promise */ loadScene(url: string, destroyOldScene: boolean = true): AssetPromise { - const scenePromise = this.engine.resourceManager.load({ url, type: AssetType.Scene }); + const resourceManager = this.engine.resourceManager; + // Evict the Scene asset cache for managed scenes about to be destroyed, so a fresh Scene + // instance is created by the loader instead of returning the same instance we're about to + // destroy (self-destroy would leave the active scene in a zombie state). + if (destroyOldScene) { + const realPath = resourceManager._virtualPathResourceMap[url]?.path ?? url; + const cached = resourceManager.getFromCache(realPath); + if (cached && this._scenes.indexOf(cached) !== -1) { + resourceManager._deleteAsset(cached); + } + } + const scenePromise = resourceManager.load({ url, type: AssetType.Scene }); scenePromise.then((scene: Scene) => { if (destroyOldScene) { - const scenes = this._scenes.getArray(); + const scenes = this._scenes.getLoopArray(); for (let i = 0, n = scenes.length; i < n; i++) { scenes[i].destroy(); } diff --git a/packages/core/src/Signal.ts b/packages/core/src/Signal.ts index a18b86cf5a..e337e03d9d 100644 --- a/packages/core/src/Signal.ts +++ b/packages/core/src/Signal.ts @@ -20,9 +20,11 @@ export class Signal { on(fn: (...args: T) => void, target?: any): void; /** * Add a structured binding listener. Structured bindings support clone remapping. + * The target method will be invoked as `method(...signalArgs, ...args)` — + * runtime signal arguments come first, bound arguments are appended. * @param target - The target component * @param methodName - The method name to invoke on the target - * @param args - Pre-resolved arguments + * @param args - Pre-resolved arguments appended after the runtime signal arguments */ on(target: Component, methodName: string, ...args: any[]): void; on(fnOrTarget: ((...args: T) => void) | Component, targetOrMethodName?: any, ...args: any[]): void { @@ -37,9 +39,11 @@ export class Signal { once(fn: (...args: T) => void, target?: any): void; /** * Add a one-time structured binding listener. + * The target method will be invoked as `method(...signalArgs, ...args)` — + * runtime signal arguments come first, bound arguments are appended. * @param target - The target component * @param methodName - The method name to invoke on the target - * @param args - Pre-resolved arguments + * @param args - Pre-resolved arguments appended after the runtime signal arguments */ once(target: Component, methodName: string, ...args: any[]): void; once(fnOrTarget: ((...args: T) => void) | Component, targetOrMethodName?: any, ...args: any[]): void { @@ -171,7 +175,7 @@ export class Signal { const methodName = targetOrMethodName as string; const fn = args.length > 0 - ? (...signalArgs: any[]) => (target as any)[methodName](...args, ...signalArgs) + ? (...signalArgs: any[]) => (target as any)[methodName](...signalArgs, ...args) : (...signalArgs: any[]) => (target as any)[methodName](...signalArgs); this._listeners.push({ fn: fn as (...args: T) => void, diff --git a/packages/core/src/Transform.ts b/packages/core/src/Transform.ts index f822474dcc..f408814c93 100644 --- a/packages/core/src/Transform.ts +++ b/packages/core/src/Transform.ts @@ -48,6 +48,8 @@ export class Transform extends Component { private _worldRight: Vector3 = null; @ignoreClone private _worldUp: Vector3 = null; + @ignoreClone + private _frontFaceInvert: boolean = false; @ignoreClone protected _isParentDirty: boolean = true; @@ -563,18 +565,42 @@ export class Transform extends Component { */ _parentChange(): void { this._isParentDirty = true; - this._updateAllWorldFlag(TransformModifyFlags.WmWpWeWqWsWus); + // Reparent invalidates the world state of the entire subtree: + // 1) `_updateAllWorldFlag` has an early-exit that skips propagation when + // self's world dirty flags are already set — invalid after reparent. + // 2) Descendants may have cached a stale `_parentTransformCache` (e.g. if + // `_getParentTransform` was ever called while their ancestor chain was + // partially constructed during clone/instantiate). Force them to + // re-resolve the parent transform on next access. + this._propagateReparentDirty(TransformModifyFlags.WmWpWeWqWsWus); + } + + private _propagateReparentDirty(flags: TransformModifyFlags): void { + this._worldAssociatedChange(flags); + const children = this._entity._children; + for (let i = 0, n = children.length; i < n; i++) { + const transform = children[i].transform; + if (transform) { + transform._isParentDirty = true; + transform._propagateReparentDirty(flags); + } + } } /** * @internal */ _isFrontFaceInvert(): boolean { - const scale = this.lossyWorldScale; - let isInvert = scale.x < 0; - scale.y < 0 && (isInvert = !isInvert); - scale.z < 0 && (isInvert = !isInvert); - return isInvert; + if (this._isContainDirtyFlag(TransformModifyFlags.WorldFrontFaceInvert)) { + const s = this._scale; + let invert = s.x < 0; + s.y < 0 && (invert = !invert); + s.z < 0 && (invert = !invert); + const parent = this._getParentTransform(); + this._frontFaceInvert = parent ? invert !== parent._isFrontFaceInvert() : invert; + this._setDirtyFlagFalse(TransformModifyFlags.WorldFrontFaceInvert); + } + return this._frontFaceInvert; } /** @@ -597,8 +623,13 @@ export class Transform extends Component { // @ts-ignore scale._onValueChanged = target._onScaleChanged; - // When cloning, other components may obtain properties such as `rotationQuaternion` in the constructor, local related dirty flags need to be corrected + // When cloning, other components may obtain properties such as `rotationQuaternion` in the constructor, local related dirty flags need to be corrected. + // Earlier in this Entity's construction other components (e.g. DynamicCollider) may have queried + // `worldPosition`, which clears the WorldPosition / WorldMatrix dirty flags as a side effect of caching + // the computed value. After this _cloneTo writes new local values, those world-derived caches are stale, + // so re-dirty them and notify listeners (Collider._updateFlag etc.) so subsequent reads recompute correctly. target._setDirtyFlagTrue(TransformModifyFlags.LocalQuat | TransformModifyFlags.LocalMatrix); + target._worldAssociatedChange(TransformModifyFlags.WmWpWeWqWsWus); } protected _onLocalMatrixChanging(): void { @@ -909,24 +940,27 @@ export enum TransformModifyFlags { */ IsWorldUniformScaling = 0x100, + // Note: 0x200 (UITransformModifyFlags.Size) and 0x400 (Pivot) are reserved by UITransform — do not reuse. + WorldFrontFaceInvert = 0x800, + /** WorldMatrix | WorldPosition */ WmWp = 0x84, /** WorldMatrix | WorldEuler | WorldQuat */ WmWeWq = 0x98, - /** WorldMatrix | WorldEuler | WorldQuat | WorldScale*/ - WmWeWqWs = 0xb8, + /** WorldMatrix | WorldEuler | WorldQuat | WorldScale | WorldFrontFaceInvert */ + WmWeWqWs = 0x8b8, /** WorldMatrix | WorldPosition | WorldEuler | WorldQuat */ WmWpWeWq = 0x9c, - /** WorldMatrix | WorldScale */ - WmWs = 0xa0, - /** WorldMatrix | WorldScale | WorldUniformScaling */ - WmWsWus = 0x1a0, - /** WorldMatrix | WorldPosition | WorldScale */ - WmWpWs = 0xa4, - /** WorldMatrix | WorldPosition | WorldEuler | WorldQuat | WorldScale */ - WmWpWeWqWs = 0xbc, - /** WorldMatrix | WorldPosition | WorldEuler | WorldQuat | WorldScale | WorldUniformScaling */ - WmWpWeWqWsWus = 0x1bc, - /** LocalQuat | LocalMatrix | WorldMatrix | WorldPosition | WorldEuler | WorldQuat | WorldScale | WorldUniformScaling */ - LqLmWmWpWeWqWsWus = 0x1fe + /** WorldMatrix | WorldScale | WorldFrontFaceInvert */ + WmWs = 0x8a0, + /** WorldMatrix | WorldScale | WorldUniformScaling | WorldFrontFaceInvert */ + WmWsWus = 0x9a0, + /** WorldMatrix | WorldPosition | WorldScale | WorldFrontFaceInvert */ + WmWpWs = 0x8a4, + /** WorldMatrix | WorldPosition | WorldEuler | WorldQuat | WorldScale | WorldFrontFaceInvert */ + WmWpWeWqWs = 0x8bc, + /** WorldMatrix | WorldPosition | WorldEuler | WorldQuat | WorldScale | WorldUniformScaling | WorldFrontFaceInvert */ + WmWpWeWqWsWus = 0x9bc, + /** LocalQuat | LocalMatrix | WorldMatrix | WorldPosition | WorldEuler | WorldQuat | WorldScale | WorldUniformScaling | WorldFrontFaceInvert */ + LqLmWmWpWeWqWsWus = 0x9fe } diff --git a/packages/core/src/animation/AnimationClip.ts b/packages/core/src/animation/AnimationClip.ts index adb0c07c91..9f7b6779fc 100644 --- a/packages/core/src/animation/AnimationClip.ts +++ b/packages/core/src/animation/AnimationClip.ts @@ -204,6 +204,15 @@ export class AnimationClip extends EngineObject { this._length = 0; } + /** + * Samples an animation at a given time. + * @param entity - The animated entity + * @param time - The time to sample an animation + */ + sampleAnimation(entity: Entity, time: number): void { + this._sampleAnimation(entity, time); + } + /** * @internal * Samples an animation at a given time. diff --git a/packages/core/src/animation/AnimationClipCurveBinding.ts b/packages/core/src/animation/AnimationClipCurveBinding.ts index 906c154087..e6e4066181 100644 --- a/packages/core/src/animation/AnimationClipCurveBinding.ts +++ b/packages/core/src/animation/AnimationClipCurveBinding.ts @@ -33,7 +33,7 @@ export class AnimationClipCurveBinding { /** The animation curve. */ curve: AnimationCurve; - private _tempCurveOwner: Record> = {}; + private _tempCurveOwner = new WeakMap>(); /** * @internal @@ -63,10 +63,11 @@ export class AnimationClipCurveBinding { * @internal */ _getTempCurveOwner(entity: Entity, component: Component): AnimationCurveOwner { - const { instanceId } = entity; - if (!this._tempCurveOwner[instanceId]) { - this._tempCurveOwner[instanceId] = this._createCurveOwner(entity, component); + let owner = this._tempCurveOwner.get(entity); + if (!owner) { + owner = this._createCurveOwner(entity, component); + this._tempCurveOwner.set(entity, owner); } - return this._tempCurveOwner[instanceId]; + return owner; } } diff --git a/packages/core/src/animation/Animator.ts b/packages/core/src/animation/Animator.ts index 180c2d1356..0bb5fea0c0 100644 --- a/packages/core/src/animation/Animator.ts +++ b/packages/core/src/animation/Animator.ts @@ -19,6 +19,7 @@ import { AnimatorCullingMode } from "./enums/AnimatorCullingMode"; import { AnimatorLayerBlendingMode } from "./enums/AnimatorLayerBlendingMode"; import { AnimatorStatePlayState } from "./enums/AnimatorStatePlayState"; import { LayerState } from "./enums/LayerState"; +import { WrapMode } from "./enums/WrapMode"; import { AnimationCurveLayerOwner } from "./internal/AnimationCurveLayerOwner"; import { AnimationEventHandler } from "./internal/AnimationEventHandler"; import { AnimatorLayerData } from "./internal/AnimatorLayerData"; @@ -37,6 +38,9 @@ export class Animator extends Component { /** The playback speed of the Animator, 1.0 is normal playback speed. */ @assignmentClone speed = 1.0; + /** Whether the Animator sends AnimationEvent callbacks. */ + @assignmentClone + fireEvents = true; /** @internal */ _playFrameCount = -1; @@ -53,7 +57,7 @@ export class Animator extends Component { @ignoreClone private _animatorLayersData = new Array(); @ignoreClone - private _curveOwnerPool: Record>> = Object.create(null); + private _curveOwnerPool: WeakMap>> = new WeakMap(); @ignoreClone private _animationEventHandlerPool = new ClearableObjectPool(AnimationEventHandler); @ignoreClone @@ -218,13 +222,31 @@ export class Animator extends Component { * @param stateName - The state name * @param layerIndex - The layer index(default -1). If layer is -1, find the first state with the given state name */ - findAnimatorState(stateName: string, layerIndex: number = -1): AnimatorState { - return this._getAnimatorStateInfo(stateName, layerIndex).state; + /** + * Find the per-instance play data for a state by name. + * The returned object's `speed` and `wrapMode` are per-instance and safe to modify without affecting other Animator instances. + * @param stateName - The state name + * @param layerIndex - The layer index (default -1, searches all layers) + * @returns Per-instance AnimatorStatePlayData, or null if not found + */ + findAnimatorState(stateName: string, layerIndex: number = -1): AnimatorStatePlayData { + const { state, layerIndex: foundLayer } = this._getAnimatorStateInfo(stateName, layerIndex); + if (!state || foundLayer < 0) return null; + const layerData = this._animatorLayersData[foundLayer]; + if (!layerData) return null; + // Check srcPlayData and destPlayData for the matching state + if (layerData.srcPlayData.state === state) return layerData.srcPlayData; + if (layerData.destPlayData.state === state) return layerData.destPlayData; + // State exists in controller but not currently playing — return srcPlayData initialized with the state + return layerData.srcPlayData; } /** * Get the layer by name. * @param name - The layer's name. + * @todo Return per-instance layer data (like AnimatorStatePlayData for states) instead of shared asset. + * Currently returns the shared AnimatorControllerLayer — modifying `weight` affects all instances. + * Should follow Unity's pattern: Animator.SetLayerWeight/GetLayerWeight (per-instance). */ findLayerByName(name: string): AnimatorControllerLayer { return this._animatorController?._layersMap[name]; @@ -312,17 +334,32 @@ export class Animator extends Component { * @internal */ _reset(): void { - const { _curveOwnerPool: animationCurveOwners } = this; - for (let instanceId in animationCurveOwners) { - const propertyOwners = animationCurveOwners[instanceId]; - for (let property in propertyOwners) { - const owner = propertyOwners[property]; - owner.revertDefaultValue(); + // revertDefaultValue 可能在死 target 上抛;listener 清理必须始终发生,否则 + // polyfill 的 clipChangedListener 会留在共享的 AnimatorState 上,捕获 this(Animator)→Entity 整树。 + // 用 try/catch 隔离 revert,listener 块紧跟其后无条件执行。 + const layersData = this._animatorLayersData; + for (let i = 0, n = layersData.length; i < n; i++) { + const stateDataMap = layersData[i].animatorStateDataMap; + for (const stateName in stateDataMap) { + const stateData = stateDataMap[stateName]; + const layerOwners = stateData.curveLayerOwner; + for (let j = 0, m = layerOwners.length; j < m; j++) { + try { + layerOwners[j]?.curveOwner?.revertDefaultValue(); + } catch (e) { + Logger.warn("Animator._reset: revertDefaultValue threw", e); + } + } + if (stateData.clipChangedListener && stateData.state) { + stateData.state._updateFlagManager.removeListener(stateData.clipChangedListener); + stateData.clipChangedListener = null; + stateData.state = null; + } } } this._animatorLayersData.length = 0; - this._curveOwnerPool = Object.create(null); + this._curveOwnerPool = new WeakMap(); this._parametersValueMap = Object.create(null); this._animationEventHandlerPool.clear(); @@ -344,6 +381,7 @@ export class Animator extends Component { protected override _onDestroy(): void { super._onDestroy(); + this._reset(); const controller = this._animatorController; if (controller) { this._addResourceReferCount(controller, -1); @@ -442,17 +480,20 @@ export class Animator extends Component { } const { property } = curve; - const { instanceId } = component; - // Get owner - const propertyOwners = (curveOwnerPool[instanceId] ||= >>( - Object.create(null) - )); + // Get owner — WeakMap keyed by Component so dead components let entries GC. + let propertyOwners = curveOwnerPool.get(component); + if (!propertyOwners) { + propertyOwners = >>Object.create(null); + curveOwnerPool.set(component, propertyOwners); + } const owner = (propertyOwners[property] ||= curve._createCurveOwner(targetEntity, component)); - // Get layer owner - const layerPropertyOwners = (layerCurveOwnerPool[instanceId] ||= >( - Object.create(null) - )); + // Get layer owner — same WeakMap-by-Component pattern (was Record which kept dead components pinned for the Animator's lifetime). + let layerPropertyOwners = layerCurveOwnerPool.get(component); + if (!layerPropertyOwners) { + layerPropertyOwners = >Object.create(null); + layerCurveOwnerPool.set(component, layerPropertyOwners); + } const layerOwner = (layerPropertyOwners[property] ||= curve._createCurveLayerOwner(owner)); if (mask && mask.pathMasks.length) { @@ -495,6 +536,8 @@ export class Animator extends Component { }; clipChangedListener(); state._updateFlagManager.addListener(clipChangedListener); + animatorStateData.state = state; + animatorStateData.clipChangedListener = clipChangedListener; } private _clearCrossData(animatorLayerData: AnimatorLayerData): void { @@ -616,7 +659,7 @@ export class Animator extends Component { const { srcPlayData } = layerData; const { state } = srcPlayData; - const playSpeed = state.speed * this.speed; + const playSpeed = srcPlayData.speed * this.speed; const playDeltaTime = playSpeed * deltaTime; srcPlayData.updateOrientation(playDeltaTime); @@ -753,8 +796,8 @@ export class Animator extends Component { return; } - const srcPlaySpeed = srcState.speed * speed; - const dstPlaySpeed = destState.speed * speed; + const srcPlaySpeed = srcPlayData.speed * speed; + const dstPlaySpeed = destPlayData.speed * speed; const dstPlayDeltaTime = dstPlaySpeed * deltaTime; srcPlayData && srcPlayData.updateOrientation(srcPlaySpeed * deltaTime); @@ -883,7 +926,7 @@ export class Animator extends Component { return; } - const playSpeed = state.speed * this.speed; + const playSpeed = destPlayData.speed * this.speed; const playDeltaTime = playSpeed * deltaTime; destPlayData.updateOrientation(playDeltaTime); @@ -989,7 +1032,7 @@ export class Animator extends Component { ): void { const playData = layerData.srcPlayData; const { state } = playData; - const actualSpeed = state.speed * this.speed; + const actualSpeed = playData.speed * this.speed; const actualDeltaTime = actualSpeed * deltaTime; playData.updateOrientation(actualDeltaTime); @@ -1452,23 +1495,28 @@ export class Animator extends Component { lastClipTime: number, deltaTime: number ): void { - const { state, isForward, clipTime } = playData; + const { state, isForward, clipTime, wrapMode } = playData; const startTime = state._getClipActualStartTime(); const endTime = state._getClipActualEndTime(); + const canWrap = wrapMode === WrapMode.Loop; if (isForward) { if (lastClipTime + deltaTime >= endTime) { this._fireSubAnimationEvents(playData, eventHandlers, lastClipTime, endTime); - playData.currentEventIndex = 0; - this._fireSubAnimationEvents(playData, eventHandlers, startTime, clipTime); + if (canWrap) { + playData.currentEventIndex = 0; + this._fireSubAnimationEvents(playData, eventHandlers, startTime, clipTime); + } } else { this._fireSubAnimationEvents(playData, eventHandlers, lastClipTime, clipTime); } } else { if (lastClipTime + deltaTime <= startTime) { this._fireBackwardSubAnimationEvents(playData, eventHandlers, lastClipTime, startTime); - playData.currentEventIndex = eventHandlers.length - 1; - this._fireBackwardSubAnimationEvents(playData, eventHandlers, endTime, clipTime); + if (canWrap) { + playData.currentEventIndex = eventHandlers.length - 1; + this._fireBackwardSubAnimationEvents(playData, eventHandlers, endTime, clipTime); + } } else { this._fireBackwardSubAnimationEvents(playData, eventHandlers, lastClipTime, clipTime); } @@ -1565,7 +1613,9 @@ export class Animator extends Component { deltaTime: number ) { const { eventHandlers } = playData.stateData; - eventHandlers.length && this._fireAnimationEvents(playData, eventHandlers, lastClipTime, deltaTime); + this.fireEvents && + eventHandlers.length && + this._fireAnimationEvents(playData, eventHandlers, lastClipTime, deltaTime); if (lastPlayState === AnimatorStatePlayState.UnStarted) { state._callOnEnter(this, layerIndex); diff --git a/packages/core/src/animation/index.ts b/packages/core/src/animation/index.ts index b829ffde54..bd201a871f 100644 --- a/packages/core/src/animation/index.ts +++ b/packages/core/src/animation/index.ts @@ -11,6 +11,7 @@ export { Animator } from "./Animator"; export { AnimatorController } from "./AnimatorController"; export { AnimatorControllerLayer } from "./AnimatorControllerLayer"; export { AnimatorState } from "./AnimatorState"; +export { AnimatorStatePlayData } from "./internal/AnimatorStatePlayData"; export { AnimatorStateMachine } from "./AnimatorStateMachine"; export { AnimatorStateTransition } from "./AnimatorStateTransition"; export { AnimatorConditionMode } from "./enums/AnimatorConditionMode"; diff --git a/packages/core/src/animation/internal/AnimatorLayerData.ts b/packages/core/src/animation/internal/AnimatorLayerData.ts index a7bd04f1a5..959aadf208 100644 --- a/packages/core/src/animation/internal/AnimatorLayerData.ts +++ b/packages/core/src/animation/internal/AnimatorLayerData.ts @@ -1,3 +1,4 @@ +import { Component } from "../../Component"; import { AnimatorControllerLayer } from "../AnimatorControllerLayer"; import { AnimatorStateTransition } from "../AnimatorStateTransition"; import { LayerState } from "../enums/LayerState"; @@ -11,7 +12,7 @@ import { AnimatorStatePlayData } from "./AnimatorStatePlayData"; export class AnimatorLayerData { layerIndex: number; layer: AnimatorControllerLayer; - curveOwnerPool: Record> = Object.create(null); + curveOwnerPool: WeakMap> = new WeakMap(); animatorStateDataMap: Record = {}; srcPlayData: AnimatorStatePlayData = new AnimatorStatePlayData(); destPlayData: AnimatorStatePlayData = new AnimatorStatePlayData(); diff --git a/packages/core/src/animation/internal/AnimatorStateData.ts b/packages/core/src/animation/internal/AnimatorStateData.ts index 146c0bb52f..42b50edd87 100644 --- a/packages/core/src/animation/internal/AnimatorStateData.ts +++ b/packages/core/src/animation/internal/AnimatorStateData.ts @@ -1,3 +1,4 @@ +import { AnimatorState } from "../AnimatorState"; import { AnimationCurveLayerOwner } from "./AnimationCurveLayerOwner"; import { AnimationEventHandler } from "./AnimationEventHandler"; @@ -7,4 +8,6 @@ import { AnimationEventHandler } from "./AnimationEventHandler"; export class AnimatorStateData { curveLayerOwner: AnimationCurveLayerOwner[] = []; eventHandlers: AnimationEventHandler[] = []; + state: AnimatorState = null; + clipChangedListener: () => void = null; } diff --git a/packages/core/src/animation/internal/AnimatorStatePlayData.ts b/packages/core/src/animation/internal/AnimatorStatePlayData.ts index 7d10fc2324..7adc90c2f4 100644 --- a/packages/core/src/animation/internal/AnimatorStatePlayData.ts +++ b/packages/core/src/animation/internal/AnimatorStatePlayData.ts @@ -1,20 +1,60 @@ +import { AnimationClip } from "../AnimationClip"; import { AnimatorState } from "../AnimatorState"; +import { AnimatorStateTransition } from "../AnimatorStateTransition"; import { AnimatorStatePlayState } from "../enums/AnimatorStatePlayState"; import { WrapMode } from "../enums/WrapMode"; +import { StateMachineScript } from "../StateMachineScript"; import { AnimatorStateData } from "./AnimatorStateData"; /** - * @internal + * Per-instance runtime data for an AnimatorState. + * Proxies read-only properties from the shared AnimatorState asset, + * while providing per-instance mutable properties (e.g. speed, wrapMode). */ export class AnimatorStatePlayData { + /** @internal */ state: AnimatorState; + /** @internal */ stateData: AnimatorStateData; + /** @internal */ playedTime: number; playState: AnimatorStatePlayState; + /** @internal */ clipTime: number; + /** @internal */ currentEventIndex: number; + /** @internal */ isForward = true; + /** @internal */ offsetFrameTime: number; + /** Per-instance speed. Initialized from AnimatorState.speed, safe to modify without affecting other instances. */ + speed: number = 1.0; + /** Per-instance wrap mode. Initialized from AnimatorState.wrapMode, safe to modify without affecting other instances. */ + wrapMode: WrapMode = WrapMode.Loop; + + // ── Proxy properties from AnimatorState (read-only) ── + + /** The name of the state. */ + get name(): string { + return this.state.name; + } + + /** The clip played by this state. */ + get clip(): AnimationClip { + return this.state.clip; + } + + /** The transitions going out of this state. */ + get transitions(): Readonly { + return this.state.transitions; + } + + /** + * Add a state machine script to the underlying AnimatorState. + */ + addStateMachineScript(scriptType: new () => T): T { + return this.state.addStateMachineScript(scriptType); + } private _changedOrientation = false; @@ -27,6 +67,8 @@ export class AnimatorStatePlayData { this.clipTime = state.clipStartTime * state.clip.length; this.currentEventIndex = 0; this.isForward = true; + this.speed = state.speed; + this.wrapMode = state.wrapMode; this.state._transitionCollection.needResetCurrentCheckIndex = true; } @@ -47,7 +89,7 @@ export class AnimatorStatePlayData { let time = this.playedTime + this.offsetFrameTime; const duration = state._getDuration(); this.playState = AnimatorStatePlayState.Playing; - if (state.wrapMode === WrapMode.Loop) { + if (this.wrapMode === WrapMode.Loop) { time = duration ? time % duration : 0; } else { if (Math.abs(time) >= duration) { diff --git a/packages/core/src/asset/AssetPromise.ts b/packages/core/src/asset/AssetPromise.ts index 99f00e86f2..9e9dc261c0 100644 --- a/packages/core/src/asset/AssetPromise.ts +++ b/packages/core/src/asset/AssetPromise.ts @@ -164,16 +164,16 @@ export class AssetPromise implements PromiseLike { * @returns AssetPromise */ onProgress( - onTaskComplete: (loaded: number, total: number) => void, + onTaskComplete?: (loaded: number, total: number) => void, onTaskDetail?: (identifier: string, loaded: number, total: number) => void ): AssetPromise { const completeProgress = this._taskCompleteProgress; const detailProgress = this._taskDetailProgress; - if (completeProgress) { + if (completeProgress && onTaskComplete) { onTaskComplete(completeProgress.loaded, completeProgress.total); } - if (detailProgress) { + if (detailProgress && onTaskDetail) { for (let url in detailProgress) { const { loaded, total } = detailProgress[url]; onTaskDetail(url, loaded, total); diff --git a/packages/core/src/asset/AssetType.ts b/packages/core/src/asset/AssetType.ts index 17ee100043..fad0e75043 100644 --- a/packages/core/src/asset/AssetType.ts +++ b/packages/core/src/asset/AssetType.ts @@ -46,7 +46,7 @@ export enum AssetType { Font = "Font", /** Source Font, include ttf, otf and woff. */ SourceFont = "SourceFont", - /** AudioClip, include ogg, wav and mp3. */ + /** AudioClip, include ogg, wav, mp3, m4a, aac and flac. */ Audio = "Audio", /** Project asset. */ Project = "project", diff --git a/packages/core/src/asset/ResourceManager.ts b/packages/core/src/asset/ResourceManager.ts index 0395b2b29b..94cb7ee946 100644 --- a/packages/core/src/asset/ResourceManager.ts +++ b/packages/core/src/asset/ResourceManager.ts @@ -340,7 +340,12 @@ export class ResourceManager { } private _assignDefaultOptions(assetInfo: LoadItem): LoadItem { - assetInfo.type = assetInfo.type ?? ResourceManager._getTypeByUrl(assetInfo.url); + const remoteConfig = this._virtualPathResourceMap[assetInfo.url]; + if (remoteConfig) { + assetInfo.type = remoteConfig.type; + } else { + assetInfo.type = assetInfo.type ?? ResourceManager._getTypeByUrl(assetInfo.url); + } if (assetInfo.type === undefined) { throw `asset type should be specified: ${assetInfo.url}`; } diff --git a/packages/core/src/audio/AudioManager.ts b/packages/core/src/audio/AudioManager.ts index ad93f5ed59..b1c4c5651b 100644 --- a/packages/core/src/audio/AudioManager.ts +++ b/packages/core/src/audio/AudioManager.ts @@ -1,3 +1,9 @@ +type ResumableAudioSource = { + _resumePendingPlayback(): void; + _suspendPlaybackForInterruption(): boolean; + _resumeInterruptedPlayback(): void; +}; + /** * Audio Manager for managing global audio context and settings. */ @@ -7,15 +13,22 @@ export class AudioManager { private static _context: AudioContext; private static _gainNode: GainNode; - private static _resumePromise: Promise = null; private static _needsUserGestureResume = false; + private static _pendingSources = new Set(); + private static _playingSources = new Set(); + private static _interruptedSources = new Set(); + private static _foregroundRestoreDelay = 300; + private static _foregroundRestoreTimer: number | undefined; + private static _hidden = false; + private static _eventsBound = false; /** * Suspend the audio context. * @returns A promise that resolves when the audio context is suspended */ static suspend(): Promise { - return AudioManager._context.suspend(); + AudioManager._suspendActiveSourcesForInterruption(); + return AudioManager._context?.suspend() ?? Promise.resolve(); } /** @@ -24,14 +37,48 @@ export class AudioManager { * @returns A promise that resolves when the audio context is resumed */ static resume(): Promise { - return (AudioManager._resumePromise ??= AudioManager._context - .resume() - .then(() => { - AudioManager._needsUserGestureResume = false; - }) - .finally(() => { - AudioManager._resumePromise = null; - })); + const context = AudioManager._context; + if (!context) { + return Promise.resolve(); + } + if (context.state === "running") { + AudioManager._clearForegroundRestore(); + AudioManager._needsUserGestureResume = false; + AudioManager._resumePendingSources(); + AudioManager._resumeInterruptedSources(); + return Promise.resolve(); + } + return context.resume().then(() => { + AudioManager._clearForegroundRestore(); + AudioManager._needsUserGestureResume = false; + AudioManager._resumePendingSources(); + AudioManager._resumeInterruptedSources(); + }); + } + + /** @internal */ + static _registerPendingSource(source: ResumableAudioSource): void { + AudioManager._pendingSources.add(source); + } + + /** @internal */ + static _unregisterPendingSource(source: ResumableAudioSource): void { + AudioManager._pendingSources.delete(source); + } + + /** @internal */ + static _registerPlayingSource(source: ResumableAudioSource): void { + AudioManager._playingSources.add(source); + } + + /** @internal */ + static _unregisterPlayingSource(source: ResumableAudioSource): void { + AudioManager._playingSources.delete(source); + } + + /** @internal */ + static _unregisterInterruptedSource(source: ResumableAudioSource): void { + AudioManager._interruptedSources.delete(source); } /** @@ -41,11 +88,12 @@ export class AudioManager { let context = AudioManager._context; if (!context) { AudioManager._context = context = new window.AudioContext(); - document.addEventListener("visibilitychange", AudioManager._onVisibilityChange); - // iOS Safari requires user gesture to resume AudioContext - document.addEventListener("touchstart", AudioManager._resumeAfterInterruption, { passive: true }); - document.addEventListener("touchend", AudioManager._resumeAfterInterruption, { passive: true }); - document.addEventListener("click", AudioManager._resumeAfterInterruption); + context.onstatechange = AudioManager._onContextStateChange; + if (!AudioManager._eventsBound) { + AudioManager._eventsBound = true; + AudioManager._bindLifecycleEvents(); + AudioManager._bindGestureEvents(); + } } return context; } @@ -70,22 +118,153 @@ export class AudioManager { return AudioManager.getContext().state === "running"; } - private static _onVisibilityChange(): void { - if (!document.hidden && AudioManager._playingCount > 0 && !AudioManager.isAudioContextRunning()) { - // iOS WKWebView WebKit bug(Triggered in LingGuang App): AudioContext may be in a "zombie" state where - // state reports "suspended" but resume() alone won't restart audio rendering. - // Calling suspend() first forces a clean internal state reset before user gesture triggers resume. - // Related: https://bugs.webkit.org/show_bug.cgi?id=263627 - AudioManager.suspend(); - AudioManager._needsUserGestureResume = true; + private static _onContextStateChange(): void { + if (AudioManager._context?.state === "running") { + if (AudioManager._hidden || AudioManager._needsUserGestureResume) { + return; + } + AudioManager._needsUserGestureResume = false; + AudioManager._resumePendingSources(); + AudioManager._resumeInterruptedSources(); + } + } + + private static _resumePendingSources(): void { + if (!AudioManager._pendingSources.size || !AudioManager.isAudioContextRunning()) { + return; + } + + const pendingSources = Array.from(AudioManager._pendingSources); + AudioManager._pendingSources.clear(); + + for (let i = 0, n = pendingSources.length; i < n; i++) { + pendingSources[i]._resumePendingPlayback(); + } + } + + private static _suspendActiveSourcesForInterruption(): void { + if (!AudioManager._playingSources.size) { + return; + } + + const playingSources = Array.from(AudioManager._playingSources); + for (let i = 0, n = playingSources.length; i < n; i++) { + const source = playingSources[i]; + if (source._suspendPlaybackForInterruption()) { + AudioManager._interruptedSources.add(source); + } + } + } + + private static _resumeInterruptedSources(): void { + if (!AudioManager._interruptedSources.size || !AudioManager.isAudioContextRunning()) { + return; + } + + const interruptedSources = Array.from(AudioManager._interruptedSources); + AudioManager._interruptedSources.clear(); + + for (let i = 0, n = interruptedSources.length; i < n; i++) { + interruptedSources[i]._resumeInterruptedPlayback(); + } + } + + private static _bindLifecycleEvents(): void { + const hiddenProp = AudioManager._getHiddenProp(); + const visibilityEvents = [ + "visibilitychange", + "mozvisibilitychange", + "msvisibilitychange", + "webkitvisibilitychange", + "qbrowserVisibilityChange" + ]; + + for (let i = 0, n = visibilityEvents.length; i < n; i++) { + document.addEventListener(visibilityEvents[i], (event) => { + const hidden = hiddenProp ? Boolean((document as any)[hiddenProp] || (event as any)?.hidden) : document.hidden; + hidden ? AudioManager._onHidden() : AudioManager._onShown(); + }); + } + + window.addEventListener("pagehide", AudioManager._onHidden); + window.addEventListener("pageshow", AudioManager._onShown); + document.addEventListener("pagehide", AudioManager._onHidden); + document.addEventListener("pageshow", AudioManager._onShown); + } + + private static _bindGestureEvents(): void { + const gestureEvents = ["pointerdown", "pointerup", "touchstart", "touchend", "mouseup", "click"]; + for (let i = 0, n = gestureEvents.length; i < n; i++) { + document.addEventListener(gestureEvents[i], AudioManager._resumeAfterInterruption, { passive: true }); + } + } + + private static _getHiddenProp(): string { + const doc = document as any; + if (typeof doc.hidden !== "undefined") return "hidden"; + if (typeof doc.mozHidden !== "undefined") return "mozHidden"; + if (typeof doc.msHidden !== "undefined") return "msHidden"; + if (typeof doc.webkitHidden !== "undefined") return "webkitHidden"; + return ""; + } + + private static _hasResumeWork(): boolean { + return ( + AudioManager._needsUserGestureResume || + AudioManager._pendingSources.size > 0 || + AudioManager._interruptedSources.size > 0 + ); + } + + private static _onHidden(): void { + if (AudioManager._hidden) { + return; + } + AudioManager._hidden = true; + AudioManager._clearForegroundRestore(); + AudioManager.suspend().catch(() => {}); + } + + private static _onShown(): void { + if (!AudioManager._hidden) { + return; + } + AudioManager._hidden = false; + + if (AudioManager._hasResumeWork()) { + AudioManager._prepareGestureResume(); + AudioManager._scheduleForegroundRestore(); } } private static _resumeAfterInterruption(): void { - if (AudioManager._needsUserGestureResume) { + if (AudioManager._hasResumeWork()) { AudioManager.resume().catch((e) => { console.warn("Failed to resume AudioContext:", e); }); } } + + private static _scheduleForegroundRestore(): void { + AudioManager._clearForegroundRestore(); + AudioManager._foregroundRestoreTimer = window.setTimeout(() => { + AudioManager._foregroundRestoreTimer = undefined; + AudioManager.resume().catch(() => AudioManager._prepareGestureResume()); + }, AudioManager._foregroundRestoreDelay); + } + + private static _clearForegroundRestore(): void { + if (AudioManager._foregroundRestoreTimer === undefined) { + return; + } + window.clearTimeout(AudioManager._foregroundRestoreTimer); + AudioManager._foregroundRestoreTimer = undefined; + } + + private static _prepareGestureResume(): Promise { + // iOS WKWebView may report a resumable state while rendering is still frozen. + // Force a clean context edge, then let a gesture or foreground retry restore sources. + AudioManager._needsUserGestureResume = true; + return AudioManager.suspend().catch(() => {}); + } } diff --git a/packages/core/src/audio/AudioSource.ts b/packages/core/src/audio/AudioSource.ts index 29a3ed8bc1..3dacf53ec3 100644 --- a/packages/core/src/audio/AudioSource.ts +++ b/packages/core/src/audio/AudioSource.ts @@ -159,27 +159,11 @@ export class AudioSource extends Component { if (AudioManager.isAudioContextRunning()) { this._startPlayback(); } else { - // iOS Safari requires resume() to be called within the same user gesture callback that triggers playback. - // Document-level events won't work - must call resume() directly here in play(). this._pendingPlay = true; - AudioManager.resume().then( - () => { - // Check if cancelled by stop()/pause() - if (!this._pendingPlay) { - return; - } - this._pendingPlay = false; - // Check if still valid to play after async resume - if (this._destroyed || !this.enabled || !this._clip) { - return; - } - this._startPlayback(); - }, - (e) => { - this._pendingPlay = false; - console.warn("Failed to resume AudioContext:", e); - } - ); + AudioManager._registerPendingSource(this); + AudioManager.resume().catch((e) => { + console.warn("Failed to resume AudioContext:", e); + }); } } @@ -187,23 +171,27 @@ export class AudioSource extends Component { * Stops playing the clip. */ stop(): void { - this._pendingPlay = false; + this._cancelPendingPlayback(); + AudioManager._unregisterInterruptedSource(this); if (this._isPlaying) { this._clearSourceNode(); this._isPlaying = false; - this._pausedTime = -1; - this._playTime = -1; AudioManager._playingCount--; + AudioManager._unregisterPlayingSource(this); } + + this._pausedTime = -1; + this._playTime = -1; } /** * Pauses playing the clip. */ pause(): void { - this._pendingPlay = false; + this._cancelPendingPlayback(); + AudioManager._unregisterInterruptedSource(this); if (this._isPlaying) { this._clearSourceNode(); @@ -211,6 +199,7 @@ export class AudioSource extends Component { this._pausedTime = AudioManager.getContext().currentTime; this._isPlaying = false; AudioManager._playingCount--; + AudioManager._unregisterPlayingSource(this); } } @@ -250,34 +239,120 @@ export class AudioSource extends Component { this.stop(); } + /** @internal */ + _resumePendingPlayback(): void { + if (!this._pendingPlay) { + return; + } + + this._pendingPlay = false; + + if (this._destroyed || !this.enabled || !this._clip?._getAudioSource()) { + return; + } + + this._startPlayback(); + } + + /** @internal */ + _suspendPlaybackForInterruption(): boolean { + if (!this._isPlaying) { + return false; + } + + const pausedTime = AudioManager.getContext().currentTime; + this._clearSourceNode(); + + this._pausedTime = pausedTime; + this._isPlaying = false; + AudioManager._playingCount--; + AudioManager._unregisterPlayingSource(this); + + return true; + } + + /** @internal */ + _resumeInterruptedPlayback(): void { + if ( + this._destroyed || + !this.enabled || + this._isPlaying || + this._pendingPlay || + !this._clip?._getAudioSource() || + this._playTime < 0 + ) { + return; + } + + if (AudioManager.isAudioContextRunning()) { + this._startPlayback(); + } else { + this._pendingPlay = true; + AudioManager._registerPendingSource(this); + } + } + private _startPlayback(): void { const startTime = this._pausedTime > 0 ? this._pausedTime - this._playTime : 0; - this._initSourceNode(startTime); + if (!this._initSourceNode(startTime)) { + this._pausedTime = -1; + this._playTime = -1; + return; + } this._playTime = AudioManager.getContext().currentTime - startTime; this._pausedTime = -1; this._isPlaying = true; AudioManager._playingCount++; + AudioManager._registerPlayingSource(this); } - private _initSourceNode(startTime: number): void { + private _initSourceNode(startTime: number): boolean { const context = AudioManager.getContext(); const sourceNode = context.createBufferSource(); + const audioBuffer = this._clip._getAudioSource(); + const duration = audioBuffer.duration; + let offset = Math.max(0, startTime); + + if (duration > 0) { + if (this._loop) { + offset %= duration; + } else if (offset >= duration) { + return false; + } + } - sourceNode.buffer = this._clip._getAudioSource(); + sourceNode.buffer = audioBuffer; sourceNode.playbackRate.value = this._playbackRate; sourceNode.loop = this._loop; sourceNode.onended = this._onPlayEnd; this._sourceNode = sourceNode; sourceNode.connect(this._gainNode); - sourceNode.start(0, startTime); + sourceNode.start(0, offset); + return true; } private _clearSourceNode(): void { - this._sourceNode.stop(); - this._sourceNode.disconnect(); - this._sourceNode.onended = null; + const sourceNode = this._sourceNode; + if (!sourceNode) { + return; + } + + sourceNode.onended = null; + try { + sourceNode.stop(); + } catch {} + sourceNode.disconnect(); this._sourceNode = null; } + + private _cancelPendingPlayback(): void { + if (!this._pendingPlay) { + return; + } + + this._pendingPlay = false; + AudioManager._unregisterPendingSource(this); + } } diff --git a/packages/core/src/clone/CloneManager.ts b/packages/core/src/clone/CloneManager.ts index be46462982..d827140224 100644 --- a/packages/core/src/clone/CloneManager.ts +++ b/packages/core/src/clone/CloneManager.ts @@ -1,5 +1,6 @@ import { Entity } from "../Entity"; import { TypedArray } from "../base/Constant"; +import { ReferResource } from "../asset/ReferResource"; import { ICustomClone } from "./ComponentCloner"; import { CloneMode } from "./enums/CloneMode"; @@ -105,21 +106,46 @@ export class CloneManager { ): void { const sourceProperty = source[k]; - // Remappable references (Entity/Component) are always remapped, regardless of clone decorator + // 1. Remappable references (Entity/Component) are always remapped, highest priority if (sourceProperty instanceof Object && (sourceProperty)._remap) { target[k] = (sourceProperty)._remap(srcRoot, targetRoot); return; } + // 2. Explicit ignore if (cloneMode === CloneMode.Ignore) return; - // Primitives, undecorated, or @assignmentClone: direct assign - if (!(sourceProperty instanceof Object) || cloneMode === undefined || cloneMode === CloneMode.Assignment) { + // 3. Primitives / null / undefined - direct assign + if (!(sourceProperty instanceof Object)) { target[k] = sourceProperty; return; } - // @shallowClone / @deepClone: deep copy complex objects + // 4. Determine effective clone mode + let effectiveCloneMode: CloneMode = cloneMode; + if (effectiveCloneMode === undefined) { + // Undecorated: infer from runtime type + effectiveCloneMode = CloneManager._inferCloneMode(sourceProperty, target[k]); + } else if (effectiveCloneMode !== CloneMode.Assignment) { + // Decorated Shallow/Deep: upgrade to Deep if target already has independent same-type instance. + // Assignment is never upgraded — it means the user explicitly wants a reference copy. + const targetProperty = target[k]; + if ( + targetProperty && + targetProperty !== sourceProperty && + targetProperty.constructor === sourceProperty.constructor + ) { + effectiveCloneMode = CloneMode.Deep; + } + } + + // 5. Assignment - direct reference copy + if (effectiveCloneMode === CloneMode.Assignment) { + target[k] = sourceProperty; + return; + } + + // 6. Shallow/Deep clone for complex types const type = sourceProperty.constructor; switch (type) { case Uint8Array: @@ -137,6 +163,37 @@ export class CloneManager { targetPropertyT.set(sourceProperty); } break; + case Map: + let targetPropertyM = >target[k]; + if (targetPropertyM == null) { + target[k] = targetPropertyM = new Map(); + } else { + targetPropertyM.clear(); + } + (>sourceProperty).forEach((value, key) => { + if (key instanceof Object && (key)._remap) { + key = (key)._remap(srcRoot, targetRoot); + } + if (value instanceof Object && (value)._remap) { + value = (value)._remap(srcRoot, targetRoot); + } + targetPropertyM.set(key, value); + }); + break; + case Set: + let targetPropertyS = >target[k]; + if (targetPropertyS == null) { + target[k] = targetPropertyS = new Set(); + } else { + targetPropertyS.clear(); + } + (>sourceProperty).forEach((value) => { + if (value instanceof Object && (value)._remap) { + value = (value)._remap(srcRoot, targetRoot); + } + targetPropertyS.add(value); + }); + break; case Array: let targetPropertyA = >target[k]; const length = (>sourceProperty).length; @@ -150,7 +207,7 @@ export class CloneManager { >sourceProperty, targetPropertyA, i, - cloneMode, + cloneMode, // Pass original mode: decorated → children inherit, undecorated → children infer independently srcRoot, targetRoot, deepInstanceMap @@ -158,25 +215,27 @@ export class CloneManager { } break; default: - let targetProperty = target[k]; - // If the target property is undefined, create new instance and keep reference sharing like the source - if (!targetProperty) { - targetProperty = deepInstanceMap.get(sourceProperty); - if (!targetProperty) { - targetProperty = new sourceProperty.constructor(); - deepInstanceMap.set(sourceProperty, targetProperty); - } - target[k] = targetProperty; + // Check if we've already visited this source object (cycle detection) + if (deepInstanceMap.has(sourceProperty)) { + target[k] = deepInstanceMap.get(sourceProperty); + return; + } + + let targetPropertyD = target[k]; + if (!targetPropertyD) { + targetPropertyD = new sourceProperty.constructor(); + target[k] = targetPropertyD; } + deepInstanceMap.set(sourceProperty, targetPropertyD); if ((sourceProperty).copyFrom) { - (targetProperty).copyFrom(sourceProperty); + (targetPropertyD).copyFrom(sourceProperty); } else { const cloneModes = CloneManager.getCloneMode(sourceProperty.constructor); for (let k in sourceProperty) { CloneManager.cloneProperty( sourceProperty, - targetProperty, + targetPropertyD, k, cloneModes[k], srcRoot, @@ -184,12 +243,50 @@ export class CloneManager { deepInstanceMap ); } - (sourceProperty)._cloneTo?.(targetProperty, srcRoot, targetRoot); + (sourceProperty)._cloneTo?.(targetPropertyD, srcRoot, targetRoot); } break; } } + /** + * Infer the appropriate clone mode for an undecorated property based on its runtime type. + * This enables user custom scripts to get correct clone behavior without decorators. + */ + private static _inferCloneMode(sourceProperty: Object, targetProperty: any): CloneMode { + // If target already has an independent instance of the same type, + // deep clone to preserve isolation (e.g., constructor-created objects) + if ( + targetProperty && + targetProperty !== sourceProperty && + targetProperty.constructor === sourceProperty.constructor + ) { + return CloneMode.Deep; + } + + // Arrays need recursive processing (may contain Entity/Component refs) + if (Array.isArray(sourceProperty)) return CloneMode.Deep; + + // TypedArrays - copy data + if (ArrayBuffer.isView(sourceProperty)) return CloneMode.Deep; + + // Maps and Sets - create independent copies + if (sourceProperty instanceof Map || sourceProperty instanceof Set) return CloneMode.Deep; + + // Value types with copyFrom (math types like Vector3, Color, etc.) + if ((sourceProperty).copyFrom) return CloneMode.Deep; + + // Engine resources (Material, Texture, Mesh, Shader, ...) — refCount-managed, + // shared by reference. Anything outside this contract is treated as a value-like + // user class and gets a deep clone (so nested Entity/Component refs are remapped). + if (sourceProperty instanceof ReferResource) return CloneMode.Assignment; + + // Default: deep clone. Covers plain objects {...} and user-defined value classes + // (EventHandler, business data containers). Internal Entity/Component refs reach + // line 109's _remap branch and are correctly rebound to the target tree. + return CloneMode.Deep; + } + static deepCloneObject(source: Object, target: Object, deepInstanceMap: Map): void { for (let k in source) { CloneManager.cloneProperty(source, target, k, CloneMode.Deep, null, null, deepInstanceMap); diff --git a/packages/core/src/graphic/TransformFeedbackShader.ts b/packages/core/src/graphic/TransformFeedbackShader.ts index c200e41597..64ffd7d114 100644 --- a/packages/core/src/graphic/TransformFeedbackShader.ts +++ b/packages/core/src/graphic/TransformFeedbackShader.ts @@ -28,16 +28,17 @@ export class TransformFeedbackShader { * Get or compile a shader program for the given engine and macro combination. */ getProgram(engine: Engine, macroCollection: ShaderMacroCollection): ShaderProgram | null { - const pool = engine._getShaderProgramPool(this._id); + const map = engine._getShaderProgramMap(this._id); - let program = pool.get(macroCollection); + let program = map.get(macroCollection); if (program) return program; const { vertexSource, fragmentSource } = ShaderFactory.compilePlatformSource( engine, macroCollection, this.vertexSource, - this.fragmentSource + this.fragmentSource, + false ); program = new ShaderProgram(engine, vertexSource, fragmentSource, this.feedbackVaryings); @@ -47,7 +48,7 @@ export class TransformFeedbackShader { return null; } - pool.cache(program); + map.cache(program); return program; } } diff --git a/packages/core/src/graphic/enums/BufferBindFlag.ts b/packages/core/src/graphic/enums/BufferBindFlag.ts index 152b9d2e54..4616d1eaff 100644 --- a/packages/core/src/graphic/enums/BufferBindFlag.ts +++ b/packages/core/src/graphic/enums/BufferBindFlag.ts @@ -5,5 +5,7 @@ export enum BufferBindFlag { /** Vertex buffer binding flag. */ VertexBuffer, /** Index buffer binding flag. */ - IndexBuffer + IndexBuffer, + /** Constant buffer binding flag (WebGL2 only). */ + ConstantBuffer } diff --git a/packages/core/src/input/pointer/Pointer.ts b/packages/core/src/input/pointer/Pointer.ts index a7412bc2ed..bb0d9fb904 100644 --- a/packages/core/src/input/pointer/Pointer.ts +++ b/packages/core/src/input/pointer/Pointer.ts @@ -23,6 +23,8 @@ export class Pointer { pressedButtons: PointerButton; /** The position of the pointer in screen space pixel coordinates. */ position: Vector2 = new Vector2(); + /** The position of the pointer when it was last pressed down (in screen space pixel coordinates). */ + pressedPosition: Vector2 = new Vector2(); /** The change of the pointer. */ deltaPosition: Vector2 = new Vector2(); /** @internal */ diff --git a/packages/core/src/input/pointer/PointerEventData.ts b/packages/core/src/input/pointer/PointerEventData.ts index 89f9ae09b6..33dc41b001 100644 --- a/packages/core/src/input/pointer/PointerEventData.ts +++ b/packages/core/src/input/pointer/PointerEventData.ts @@ -1,4 +1,5 @@ import { Vector3 } from "@galacean/engine-math"; +import { Entity } from "../../Entity"; import { IPoolElement } from "../../utils/ObjectPool"; import { Pointer } from "./Pointer"; @@ -10,11 +11,17 @@ export class PointerEventData implements IPoolElement { pointer: Pointer; /** The position of the event trigger (in world space). */ worldPosition: Vector3 = new Vector3(); + /** The hit-tested target entity (deepest entity on the bubble path). */ + target: Entity = null; + /** The entity currently handling the event (same as target on non-bubbling fire, varies on bubble). */ + currentTarget: Entity = null; /** * @internal */ dispose() { this.pointer = null; + this.target = null; + this.currentTarget = null; } } diff --git a/packages/core/src/input/pointer/PointerManager.ts b/packages/core/src/input/pointer/PointerManager.ts index fc6d8a2010..83b2310c83 100644 --- a/packages/core/src/input/pointer/PointerManager.ts +++ b/packages/core/src/input/pointer/PointerManager.ts @@ -245,6 +245,7 @@ export class PointerManager implements IInput { pointer._downMap[button] = frameCount; pointer._frameEvents |= PointerEventType.Down; pointer.phase = PointerPhase.Down; + pointer.pressedPosition.copyFrom(pointer.position); break; } case "pointerup": { diff --git a/packages/core/src/input/pointer/emitter/PhysicsPointerEventEmitter.ts b/packages/core/src/input/pointer/emitter/PhysicsPointerEventEmitter.ts index 5778f924a9..5cfc2ec8cd 100644 --- a/packages/core/src/input/pointer/emitter/PhysicsPointerEventEmitter.ts +++ b/packages/core/src/input/pointer/emitter/PhysicsPointerEventEmitter.ts @@ -53,13 +53,13 @@ export class PhysicsPointerEventEmitter extends PointerEventEmitter { override processDrag(pointer: Pointer): void { const entity = this._draggedEntity; - entity && this._fireDrag(entity, this._createEventData(pointer)); + entity && this._fireDrag(entity, this._createEventData(pointer, entity, entity)); } override processDown(pointer: Pointer): void { const entity = (this._pressedEntity = this._draggedEntity = this._enteredEntity); if (entity) { - const eventData = this._createEventData(pointer); + const eventData = this._createEventData(pointer, entity, entity); this._fireDown(entity, eventData); this._fireBeginDrag(entity, eventData); } @@ -69,14 +69,14 @@ export class PhysicsPointerEventEmitter extends PointerEventEmitter { const { _enteredEntity: enteredEntity, _draggedEntity: draggedEntity } = this; if (enteredEntity) { const sameTarget = this._pressedEntity === enteredEntity; - const eventData = this._createEventData(pointer); + const eventData = this._createEventData(pointer, enteredEntity, enteredEntity); this._fireUp(enteredEntity, eventData); sameTarget && this._fireClick(enteredEntity, eventData); this._fireDrop(enteredEntity, eventData); } this._pressedEntity = null; if (draggedEntity) { - this._fireEndDrag(draggedEntity, this._createEventData(pointer)); + this._fireEndDrag(draggedEntity, this._createEventData(pointer, draggedEntity, draggedEntity)); this._draggedEntity = null; } } @@ -84,13 +84,13 @@ export class PhysicsPointerEventEmitter extends PointerEventEmitter { override processLeave(pointer: Pointer): void { const enteredEntity = this._enteredEntity; if (enteredEntity) { - this._fireExit(enteredEntity, this._createEventData(pointer)); + this._fireExit(enteredEntity, this._createEventData(pointer, enteredEntity, enteredEntity)); this._enteredEntity = null; } const draggedEntity = this._draggedEntity; if (draggedEntity) { - this._fireEndDrag(draggedEntity, this._createEventData(pointer)); + this._fireEndDrag(draggedEntity, this._createEventData(pointer, draggedEntity, draggedEntity)); this._draggedEntity = null; } this._pressedEntity = null; @@ -108,10 +108,10 @@ export class PhysicsPointerEventEmitter extends PointerEventEmitter { const enteredEntity = this._enteredEntity; if (entity !== enteredEntity) { if (enteredEntity) { - this._fireExit(enteredEntity, this._createEventData(pointer)); + this._fireExit(enteredEntity, this._createEventData(pointer, enteredEntity, enteredEntity)); } if (entity) { - this._fireEnter(entity, this._createEventData(pointer)); + this._fireEnter(entity, this._createEventData(pointer, entity, entity)); } this._enteredEntity = entity; } diff --git a/packages/core/src/input/pointer/emitter/PointerEventEmitter.ts b/packages/core/src/input/pointer/emitter/PointerEventEmitter.ts index 6f6c62cdfb..98188891e6 100644 --- a/packages/core/src/input/pointer/emitter/PointerEventEmitter.ts +++ b/packages/core/src/input/pointer/emitter/PointerEventEmitter.ts @@ -32,10 +32,12 @@ export abstract class PointerEventEmitter { protected abstract _init(): void; - protected _createEventData(pointer: Pointer): PointerEventData { + protected _createEventData(pointer: Pointer, target: Entity, currentTarget: Entity = target): PointerEventData { const data = this._pool.get(); data.pointer = pointer; data.worldPosition.copyFrom(this._hitResult.point); + data.target = target; + data.currentTarget = currentTarget; return data; } diff --git a/packages/core/src/mesh/MeshRenderer.ts b/packages/core/src/mesh/MeshRenderer.ts index e08e51ac4c..3cac98586a 100644 --- a/packages/core/src/mesh/MeshRenderer.ts +++ b/packages/core/src/mesh/MeshRenderer.ts @@ -1,6 +1,7 @@ import { BoundingBox } from "@galacean/engine-math"; import { Entity } from "../Entity"; import { RenderContext } from "../RenderPipeline/RenderContext"; +import { RenderElement } from "../RenderPipeline/RenderElement"; import { Renderer, RendererUpdateFlags } from "../Renderer"; import { Logger } from "../base/Logger"; import { ignoreClone } from "../clone/CloneManager"; @@ -151,9 +152,10 @@ export class MeshRenderer extends Renderer { const { _materials: materials, _engine: engine } = this; const subMeshes = mesh.subMeshes; - const renderElement = engine._renderElementPool.get(); - renderElement.set(this.priority, this._distanceForSort); - const subRenderElementPool = engine._subRenderElementPool; + const priority = this.priority; + const distanceForSort = this._distanceForSort; + const renderElementPool = engine._renderElementPool; + const renderPipeline = context.camera._renderPipeline; for (let i = 0, n = subMeshes.length; i < n; i++) { let material = materials[i]; if (!material) { @@ -163,11 +165,41 @@ export class MeshRenderer extends Renderer { material = this.engine._basicResources.meshMagentaMaterial; } - const subRenderElement = subRenderElementPool.get(); - subRenderElement.set(this, material, mesh._primitive, subMeshes[i]); - renderElement.addSubRenderElement(subRenderElement); + const renderElement = renderElementPool.get(); + renderElement.set(this, material, mesh._primitive, subMeshes[i]); + renderElement.priority = priority; + renderElement.distanceForSort = distanceForSort; + renderPipeline.pushRenderElement(context, renderElement); } - context.camera._renderPipeline.pushRenderElement(context, renderElement); + } + + /** + * @internal + */ + override _canBatch(preElement: RenderElement, curElement: RenderElement): boolean { + if (!this._engine._hardwareRenderer.isWebGL2) return false; + const curShaderData = curElement.component.shaderData; + return ( + preElement.primitive === curElement.primitive && + preElement.subPrimitive === curElement.subPrimitive && + preElement.material === curElement.material && + this._isFrontFaceInvert() === (curElement.component)._isFrontFaceInvert() && + this.shaderData._macroCollection.isEqual(curShaderData._macroCollection) && + // Renderer-group samplers/arrays are shared across the whole instanced draw call + this.shaderData._matchesRendererInstanceBatch(curShaderData) + ); + } + + /** + * @internal + */ + override _batch(preElement: RenderElement | null, curElement: RenderElement): void { + if (!preElement) return; + const renderers = preElement.instancedRenderers; + if (renderers.length === 0) { + renderers.push(preElement.component); + } + renderers.push(curElement.component); } private _setMesh(mesh: Mesh): void { diff --git a/packages/core/src/mesh/ModelMesh.ts b/packages/core/src/mesh/ModelMesh.ts index 27b92ced49..8ee92c4825 100644 --- a/packages/core/src/mesh/ModelMesh.ts +++ b/packages/core/src/mesh/ModelMesh.ts @@ -925,7 +925,7 @@ export class ModelMesh extends Mesh { return null; } if (!buffer.readable) { - throw "Not allowed to access data while vertex buffer readable is false."; + throw new Error("Not allowed to access data while vertex buffer readable is false."); } const vertexCount = this.vertexCount; diff --git a/packages/core/src/mesh/Skin.ts b/packages/core/src/mesh/Skin.ts index 7a9b3444b0..18489805c5 100644 --- a/packages/core/src/mesh/Skin.ts +++ b/packages/core/src/mesh/Skin.ts @@ -15,7 +15,7 @@ export class Skin extends EngineObject { inverseBindMatrices = new Array(); /** @internal */ - @ignoreClone + @deepClone _skinMatrices: Float32Array; /** @internal */ @ignoreClone diff --git a/packages/core/src/mesh/SkinnedMeshRenderer.ts b/packages/core/src/mesh/SkinnedMeshRenderer.ts index d8e83e16bb..4d7f726184 100644 --- a/packages/core/src/mesh/SkinnedMeshRenderer.ts +++ b/packages/core/src/mesh/SkinnedMeshRenderer.ts @@ -1,6 +1,7 @@ import { BoundingBox, Vector2 } from "@galacean/engine-math"; import { Entity } from "../Entity"; import { RenderContext } from "../RenderPipeline/RenderContext"; +import { RenderElement } from "../RenderPipeline/RenderElement"; import { RendererUpdateFlags } from "../Renderer"; import { Logger } from "../base/Logger"; import { deepClone, ignoreClone } from "../clone/CloneManager"; @@ -120,6 +121,13 @@ export class SkinnedMeshRenderer extends MeshRenderer { localBounds.max._onValueChanged = this._onLocalBoundsChanged; } + /** + * @internal + */ + override _canBatch(_preElement: RenderElement, _curElement: RenderElement): boolean { + return false; + } + /** * @internal */ diff --git a/packages/core/src/particle/ParticleGenerator.ts b/packages/core/src/particle/ParticleGenerator.ts index d180c7f929..ecb488d8bf 100644 --- a/packages/core/src/particle/ParticleGenerator.ts +++ b/packages/core/src/particle/ParticleGenerator.ts @@ -112,6 +112,7 @@ export class ParticleGenerator { @ignoreClone _subPrimitive = new SubMesh(0, 0, MeshTopology.Triangles); /** @internal */ + @ignoreClone readonly _renderer: ParticleRenderer; /** @internal */ @@ -235,6 +236,8 @@ export class ParticleGenerator { } } else { this._isPlaying = false; + // Invalidate the rateOverDistance baseline so emitter movement during the stop + // interval doesn't burst on resume. if (stopMode === ParticleStopMode.StopEmittingAndClear) { this._clearActiveParticles(); this._playTime = 0; @@ -242,6 +245,8 @@ export class ParticleGenerator { this._firstActiveTransformedBoundingBox = this._firstFreeTransformedBoundingBox; this.emission._reset(); + } else { + this.emission._invalidateDistanceBaseline(); } } } @@ -257,37 +262,41 @@ export class ParticleGenerator { /** * @internal */ - _emit(playTime: number, count: number): void { - const { emission } = this; - if (emission.enabled) { - const { main } = this; - // Wait the existing particles to be retired - const notRetireParticleCount = this._getNotRetiredParticleCount(); - if (notRetireParticleCount >= main.maxParticles) { - return; - } - const position = ParticleGenerator._tempVector30; - const direction = ParticleGenerator._tempVector31; - const transform = this._renderer.entity.transform; - const shape = emission.shape; - const positionScale = main._getPositionScale(); - for (let i = 0; i < count; i++) { - if (shape?.enabled) { - shape._generatePositionAndDirection(emission._shapeRand, playTime, position, direction); - position.multiply(positionScale); - direction.normalize().multiply(positionScale); - } else { - position.set(0, 0, 0); - direction.set(0, 0, -1); - // Speed is scaled by shape scale in world simulation space - // So if no shape and in world simulation space, we shouldn't scale the speed - if (main.simulationSpace === ParticleSimulationSpace.Local) { - direction.multiply(positionScale); - } + _emit(playTime: number, count: number, emitWorldPositionOverride?: Vector3): number { + const { emission, main } = this; + if (!emission.enabled) { + return 0; + } + const budget = main.maxParticles - this._getNotRetiredParticleCount(); + if (count > budget) { + count = budget; + } + if (count <= 0) { + return 0; + } + + const position = ParticleGenerator._tempVector30; + const direction = ParticleGenerator._tempVector31; + const transform = this._renderer.entity.transform; + const shape = emission.shape; + const positionScale = main._getPositionScale(); + for (let i = 0; i < count; i++) { + if (shape?.enabled) { + shape._generatePositionAndDirection(emission._shapeRand, playTime, position, direction); + position.multiply(positionScale); + direction.normalize().multiply(positionScale); + } else { + position.set(0, 0, 0); + direction.set(0, 0, -1); + // Speed is scaled by shape scale in world simulation space + // So if no shape and in world simulation space, we shouldn't scale the speed + if (main.simulationSpace === ParticleSimulationSpace.Local) { + direction.multiply(positionScale); } - this._addNewParticle(position, direction, transform, playTime); } + this._addNewParticle(position, direction, transform, playTime, emitWorldPositionOverride); } + return count; } /** @@ -831,7 +840,13 @@ export class ParticleGenerator { } } - private _addNewParticle(position: Vector3, direction: Vector3, transform: Transform, playTime: number): void { + private _addNewParticle( + position: Vector3, + direction: Vector3, + transform: Transform, + playTime: number, + emitWorldPositionOverride?: Vector3 + ): void { const firstFreeElement = this._firstFreeElement; let nextFreeElement = firstFreeElement + 1; if (nextFreeElement >= this._currentParticleCount) { @@ -863,7 +878,7 @@ export class ParticleGenerator { let pos: Vector3, rot: Quaternion; if (main.simulationSpace === ParticleSimulationSpace.World) { - pos = transform.worldPosition; + pos = emitWorldPositionOverride ?? transform.worldPosition; rot = transform.worldRotationQuaternion; } @@ -1022,7 +1037,7 @@ export class ParticleGenerator { // Initialize feedback buffer for this particle if (this._useTransformFeedback) { - this._addFeedbackParticle(firstFreeElement, position, direction, startSpeed, transform); + this._addFeedbackParticle(firstFreeElement, position, direction, startSpeed, transform, pos); } this._firstFreeElement = nextFreeElement; @@ -1033,7 +1048,8 @@ export class ParticleGenerator { shapePosition: Vector3, direction: Vector3, startSpeed: number, - transform: Transform + transform: Transform, + emitWorldPosition?: Vector3 ): void { let position: Vector3; if (this.main.simulationSpace === ParticleSimulationSpace.Local) { @@ -1041,7 +1057,7 @@ export class ParticleGenerator { } else { position = ParticleGenerator._tempVector32; Vector3.transformByQuat(shapePosition, transform.worldRotationQuaternion, position); - position.add(transform.worldPosition); + position.add(emitWorldPosition ?? transform.worldPosition); } this._feedbackSimulator.writeParticleData( diff --git a/packages/core/src/particle/ParticleRenderer.ts b/packages/core/src/particle/ParticleRenderer.ts index 633e166989..ed6e4a7275 100644 --- a/packages/core/src/particle/ParticleRenderer.ts +++ b/packages/core/src/particle/ParticleRenderer.ts @@ -5,7 +5,7 @@ import { Renderer, RendererUpdateFlags } from "../Renderer"; import { TransformModifyFlags } from "../Transform"; import { GLCapabilityType } from "../base/Constant"; import { Logger } from "../base/Logger"; -import { deepClone, ignoreClone, shallowClone } from "../clone/CloneManager"; +import { assignmentClone, deepClone, ignoreClone, shallowClone } from "../clone/CloneManager"; import { ModelMesh } from "../mesh/ModelMesh"; import { ShaderMacro } from "../shader/ShaderMacro"; import { ShaderProperty } from "../shader/ShaderProperty"; @@ -47,7 +47,9 @@ export class ParticleRenderer extends Renderer { @ignoreClone _transformedBounds = new BoundingBox(); + @assignmentClone private _renderMode: ParticleRenderMode; + @assignmentClone private _currentRenderModeMacro: ShaderMacro; private _mesh: ModelMesh; private _supportInstancedArrays: boolean; @@ -174,9 +176,9 @@ export class ParticleRenderer extends Renderer { /** * @internal */ - override _updateTransformShaderData(context: RenderContext, onlyMVP: boolean, batched: boolean): void { + override _updateTransformShaderData(context: RenderContext, onlyMVP: boolean): void { //@todo: Don't need to update transform shader data, temp solution - super._updateTransformShaderData(context, onlyMVP, true); + this._updateWorldSpaceTransformShaderData(context, onlyMVP); } protected override _updateBounds(worldBounds: BoundingBox): void { const { generator } = this; @@ -251,13 +253,29 @@ export class ParticleRenderer extends Renderer { const engine = this._engine; const renderElement = engine._renderElementPool.get(); - renderElement.set(this.priority, this._distanceForSort); - const subRenderElement = engine._subRenderElementPool.get(); - subRenderElement.set(this, material, generator._primitive, generator._subPrimitive); - renderElement.addSubRenderElement(subRenderElement); + renderElement.set(this, material, generator._primitive, generator._subPrimitive); + renderElement.priority = this.priority; + renderElement.distanceForSort = this._distanceForSort; context.camera._renderPipeline.pushRenderElement(context, renderElement); } + /** + * @internal + */ + override _cloneTo(target: ParticleRenderer): void { + super._cloneTo(target); + + // ShaderData internals (_macroCollection, _propertyValueMap) are @ignoreClone, + // so the cloned shaderData only has the constructor-set Billboard macro. + // Must re-apply the source's shaderData state. + this.shaderData.cloneTo(target.shaderData); + + // Rebuild GPU resources to match cloned renderMode/mesh/maxParticles. + const gen = target.generator; + gen._reorganizeGeometryBuffers(); + gen._resizeInstanceBuffer(true, gen.main.maxParticles); + } + protected override _onDestroy(): void { const mesh = this._mesh; if (mesh) { diff --git a/packages/core/src/particle/modules/EmissionModule.ts b/packages/core/src/particle/modules/EmissionModule.ts index 108832c38c..292205f0f1 100644 --- a/packages/core/src/particle/modules/EmissionModule.ts +++ b/packages/core/src/particle/modules/EmissionModule.ts @@ -1,7 +1,8 @@ -import { Rand } from "@galacean/engine-math"; +import { MathUtil, Rand, Vector3 } from "@galacean/engine-math"; import { deepClone, ignoreClone } from "../../clone/CloneManager"; import { ShaderMacro } from "../../shader/ShaderMacro"; import { ParticleRandomSubSeeds } from "../enums/ParticleRandomSubSeeds"; +import { ParticleSimulationSpace } from "../enums/ParticleSimulationSpace"; import { Burst } from "./Burst"; import { ParticleCompositeCurve } from "./ParticleCompositeCurve"; import { ParticleGeneratorModule } from "./ParticleGeneratorModule"; @@ -14,6 +15,8 @@ export class EmissionModule extends ParticleGeneratorModule { /** @internal */ static readonly _emissionShapeMacro = ShaderMacro.getByName("RENDERER_EMISSION_SHAPE"); + private static _tempEmitPosition = new Vector3(); + /** The rate of particle emission. */ @deepClone rateOverTime: ParticleCompositeCurve = new ParticleCompositeCurve(10); @@ -29,6 +32,13 @@ export class EmissionModule extends ParticleGeneratorModule { /** @internal */ _frameRateTime: number = 0; + @ignoreClone + private _distanceAccumulator = 0; + @ignoreClone + private _lastEmitPosition = new Vector3(); + @ignoreClone + private _hasLastEmitPosition = false; + @deepClone private _bursts: Burst[] = []; @@ -130,6 +140,7 @@ export class EmissionModule extends ParticleGeneratorModule { */ _emit(lastPlayTime: number, playTime: number): void { this._emitByRateOverTime(playTime); + this._emitByRateOverDistance(lastPlayTime, playTime); this._emitByBurst(lastPlayTime, playTime); } @@ -147,6 +158,15 @@ export class EmissionModule extends ParticleGeneratorModule { _reset(): void { this._frameRateTime = 0; this._currentBurstIndex = 0; + this._invalidateDistanceBaseline(); + } + + /** + * @internal + */ + _invalidateDistanceBaseline(): void { + this._hasLastEmitPosition = false; + this._distanceAccumulator = 0; } /** @@ -171,6 +191,65 @@ export class EmissionModule extends ParticleGeneratorModule { } } + private _emitByRateOverDistance(lastPlayTime: number, playTime: number): void { + const ratePerUnit = this.rateOverDistance.evaluate(undefined, undefined); + const generator = this._generator; + + if (ratePerUnit <= 0) { + this._invalidateDistanceBaseline(); + return; + } + if (!this._hasLastEmitPosition) { + this._lastEmitPosition.copyFrom(generator._renderer.entity.transform.worldPosition); + this._hasLastEmitPosition = true; + return; + } + + const lastPos = this._lastEmitPosition; + const currentPos = generator._renderer.entity.transform.worldPosition; + const dx = currentPos.x - lastPos.x; + const dy = currentPos.y - lastPos.y; + const dz = currentPos.z - lastPos.z; + const moveLength = Math.sqrt(dx * dx + dy * dy + dz * dz); + this._distanceAccumulator += moveLength; + + const emitInterval = 1.0 / ratePerUnit; + // `+ zeroTolerance` absorbs float divide error so an exact `N*interval` accumulator doesn't drop 1 + const count = Math.floor(this._distanceAccumulator / emitInterval + MathUtil.zeroTolerance); + + if (count > 0) { + this._distanceAccumulator -= count * emitInterval; + // `subFrameAge ∈ [0, 1]`: how far back into the frame a particle was born + // (0 = newest at currentPos/playTime, 1 = oldest at lastPos/lastPlayTime). + // The initial clamp protects two edges — moveLength ≈ 0 (collapse to frame-end + // emit) and a tiny moveLength near the emitInterval edge (would put age > 1). + // Local simulation space ignores the position override but still uses emitTime. + const isWorld = generator.main.simulationSpace === ParticleSimulationSpace.World; + const invMoveLength = moveLength > MathUtil.zeroTolerance ? 1.0 / moveLength : 0; + const ageStep = emitInterval * invMoveLength; + const dt = playTime - lastPlayTime; + let subFrameAge = Math.min(this._distanceAccumulator * invMoveLength, 1.0); + const emitPos = EmissionModule._tempEmitPosition; + for (let i = 0; i < count; i++) { + if (isWorld) { + emitPos.set( + currentPos.x - dx * subFrameAge, + currentPos.y - dy * subFrameAge, + currentPos.z - dz * subFrameAge + ); + } + if (generator._emit(playTime - dt * subFrameAge, 1, isWorld ? emitPos : undefined) === 0) { + // Buffer full: settle the frame's distance budget instead of carrying it over + this._distanceAccumulator = 0; + break; + } + subFrameAge += ageStep; + } + } + + lastPos.copyFrom(currentPos); + } + private _emitByBurst(lastPlayTime: number, playTime: number): void { const main = this._generator.main; const duration = main.duration; diff --git a/packages/core/src/physics/CharacterController.ts b/packages/core/src/physics/CharacterController.ts index f3c9e6f048..ea6961b70f 100644 --- a/packages/core/src/physics/CharacterController.ts +++ b/packages/core/src/physics/CharacterController.ts @@ -1,5 +1,5 @@ import { ICharacterController } from "@galacean/engine-design"; -import { Vector3 } from "@galacean/engine-math"; +import { Quaternion, Vector3 } from "@galacean/engine-math"; import { Engine } from "../Engine"; import { Entity } from "../Entity"; import { Collider } from "./Collider"; @@ -162,6 +162,10 @@ export class CharacterController extends Collider { (this._nativeCollider).setSlopeLimit(this._slopeLimit); } + protected override _teleportToEntityTransform(worldPosition: Vector3, _worldRotation: Quaternion): void { + (this._nativeCollider).setWorldPosition(worldPosition); + } + private _syncWorldPositionFromPhysicalSpace(): void { (this._nativeCollider).getWorldPosition(this.entity.transform.worldPosition); } diff --git a/packages/core/src/physics/Collider.ts b/packages/core/src/physics/Collider.ts index a8a591fe56..5afa164403 100644 --- a/packages/core/src/physics/Collider.ts +++ b/packages/core/src/physics/Collider.ts @@ -1,4 +1,5 @@ import { ICollider, IStaticCollider } from "@galacean/engine-design"; +import { Quaternion, Vector3 } from "@galacean/engine-math"; import { BoolUpdateFlag } from "../BoolUpdateFlag"; import { deepClone, ignoreClone } from "../clone/CloneManager"; import { ICustomClone } from "../clone/ComponentCloner"; @@ -28,6 +29,16 @@ export class Collider extends Component implements ICustomClone { protected _shapes: ColliderShape[] = []; protected _collisionLayerIndex: number = 0; + /** + * Disabling a collider only detaches its native actor from the simulation + * scene; the actor is not destroyed, so on re-enable it still holds its + * pre-disable pose. The first transform sync after (re-)enable must teleport, + * never sweep — otherwise a kinematic actor drags across the scene from that + * stale pose and fires spurious contacts along the path. + */ + @ignoreClone + private _pendingReenterTeleport: boolean = false; + /** * The shapes of this collider. */ @@ -108,20 +119,29 @@ export class Collider extends Component implements ICustomClone { * @internal */ _onUpdate(): void { - if (this._updateFlag.flag) { + const shapes = this._shapes; + if (this._pendingReenterTeleport || this._updateFlag.flag) { const { transform } = this.entity; - (this._nativeCollider).setWorldTransform( - transform.worldPosition, - transform.worldRotationQuaternion - ); + if (this._pendingReenterTeleport) { + this._teleportToEntityTransform(transform.worldPosition, transform.worldRotationQuaternion); + this._pendingReenterTeleport = false; + } else { + this._syncEntityTransformToNative(transform.worldPosition, transform.worldRotationQuaternion); + } const worldScale = transform.lossyWorldScale; - const shapes = this._shapes; for (let i = 0, n = shapes.length; i < n; i++) { shapes[i]._nativeShape?.setWorldScale(worldScale); } this._updateFlag.flag = false; } + + // Drive per-shape physics update (e.g. MeshColliderShape retries pending + // native shape creation when mesh data becomes accessible asynchronously). + // No-op for shape types that don't override `_onPhysicsUpdate`. + for (let i = 0, n = shapes.length; i < n; i++) { + shapes[i]._onPhysicsUpdate(); + } } /** @@ -134,6 +154,7 @@ export class Collider extends Component implements ICustomClone { */ override _onEnableInScene(): void { this.scene.physics._addCollider(this); + this._pendingReenterTeleport = true; } /** @@ -164,6 +185,32 @@ export class Collider extends Component implements ICustomClone { this._addNativeShape(this.shapes[i]); } this._setCollisionLayer(); + // Teleport native actor to entity's current world pose. + // The native actor was created in constructor() with the entity's then-current + // worldPosition/Rotation. On clone, the entity's transform fields are deep-cloned + // AFTER the Component (and its native actor) are constructed, so the native actor's + // pose lags behind the cloned entity transform until this sync. + const { transform } = this.entity; + this._teleportToEntityTransform(transform.worldPosition, transform.worldRotationQuaternion); + } + + /** + * Teleport native actor to a world pose (instant, no implied velocity). + * Used during initialization paths (clone) where the native actor must be re-aligned + * with the entity transform after construction-time pose was based on stale defaults. + */ + protected _teleportToEntityTransform(worldPosition: Vector3, worldRotation: Quaternion): void { + (this._nativeCollider).setWorldTransform(worldPosition, worldRotation); + } + + /** + * Sync entity world transform to native actor for per-frame updates. + * Default semantics: teleport (setGlobalPose). Subclasses override to express + * physics-aware movement (e.g. DynamicCollider routes kinematic actors through + * setKinematicTarget to generate contact events on swept motion). + */ + protected _syncEntityTransformToNative(worldPosition: Vector3, worldRotation: Quaternion): void { + (this._nativeCollider).setWorldTransform(worldPosition, worldRotation); } /** diff --git a/packages/core/src/physics/Collision.ts b/packages/core/src/physics/Collision.ts index 16edfd5bb3..440864e4fe 100644 --- a/packages/core/src/physics/Collision.ts +++ b/packages/core/src/physics/Collision.ts @@ -29,8 +29,7 @@ export class Collision { */ getContacts(outContacts: ContactPoint[]): number { const nativeCollision = this._nativeCollision; - const smallerShapeId = Math.min(nativeCollision.shape0Id, nativeCollision.shape1Id); - const factor = this.shape.id === smallerShapeId ? 1 : -1; + const factor = this.shape.id === nativeCollision.shape1Id ? 1 : -1; const nativeContactPoints = nativeCollision.getContacts(); const length = nativeContactPoints.size(); for (let i = 0; i < length; i++) { diff --git a/packages/core/src/physics/DynamicCollider.ts b/packages/core/src/physics/DynamicCollider.ts index 380a3b927f..debf078b14 100644 --- a/packages/core/src/physics/DynamicCollider.ts +++ b/packages/core/src/physics/DynamicCollider.ts @@ -13,6 +13,8 @@ import { MeshColliderShape } from "./shape/MeshColliderShape"; */ export class DynamicCollider extends Collider { private static _tempVector3 = new Vector3(); + private static _tempVector3_1 = new Vector3(); + private static _tempVector3_2 = new Vector3(); private static _tempQuat = new Quaternion(); private _linearDamping = 0; @@ -33,7 +35,9 @@ export class DynamicCollider extends Collider { private _isKinematic = false; private _constraints: DynamicColliderConstraints = 0; private _collisionDetectionMode: CollisionDetectionMode = CollisionDetectionMode.Discrete; - private _sleepThreshold = 5e-3; + private _kinematicTransformSyncMode: DynamicColliderKinematicTransformSyncMode = + DynamicColliderKinematicTransformSyncMode.Target; + private _sleepThreshold: number | undefined; private _automaticCenterOfMass = true; private _automaticInertiaTensor = true; @@ -223,7 +227,7 @@ export class DynamicCollider extends Collider { * The mass-normalized energy threshold, below which objects start going to sleep. */ get sleepThreshold(): number { - return this._sleepThreshold; + return this._sleepThreshold ?? Engine._nativePhysics?.getDefaultSleepThreshold?.() ?? 5e-3; } set sleepThreshold(value: number) { @@ -325,6 +329,22 @@ export class DynamicCollider extends Collider { } } + /** + * Controls how entity transform changes are synchronized to a kinematic native actor. + * + * @remarks + * `Target` routes transform changes through {@link move}, so PhysX treats the + * actor as moving between frames and can generate swept contacts. `Teleport` + * writes the native pose directly and does not imply velocity. + */ + get kinematicTransformSyncMode(): DynamicColliderKinematicTransformSyncMode { + return this._kinematicTransformSyncMode; + } + + set kinematicTransformSyncMode(value: DynamicColliderKinematicTransformSyncMode) { + this._kinematicTransformSyncMode = value; + } + /** * @internal */ @@ -367,6 +387,33 @@ export class DynamicCollider extends Collider { this._phasedActiveInScene && (this._nativeCollider).addTorque(torque); } + /** + * Apply a force to the DynamicCollider at a given position in world space. + * The force generates both linear acceleration through the center of mass and angular + * acceleration about it (torque = (position - centerOfMass) × force). + * @param force - The force to apply, in world space + * @param position - The position where the force is applied, in world space + */ + applyForceAtPosition(force: Vector3, position: Vector3): void { + if (!this._phasedActiveInScene) return; + const nativeCollider = this._nativeCollider; + + const localCoM = DynamicCollider._tempVector3; + nativeCollider.getCenterOfMass(localCoM); + + const transform = this.entity.transform; + const worldCoM = DynamicCollider._tempVector3_1; + Vector3.transformByQuat(localCoM, transform.worldRotationQuaternion, worldCoM); + worldCoM.add(transform.worldPosition); + + const torque = DynamicCollider._tempVector3_2; + Vector3.subtract(position, worldCoM, torque); + Vector3.cross(torque, force, torque); + + nativeCollider.addForce(force); + nativeCollider.addTorque(torque); + } + /** * Moves the kinematic collider to the specified position. * @remarks Only available when {@link isKinematic} is true. @@ -433,6 +480,30 @@ export class DynamicCollider extends Collider { super.addShape(shape); } + /** + * Route per-frame entity → native transform sync to the correct physics API based + * on kinematic state. + * + * PhysX 4.x docs (PxRigidDynamic): + * "If you intend to move a kinematic actor with [setGlobalPose] and want + * collision detection, use setKinematicTarget() instead." + * + * setGlobalPose is a teleport: PhysX skips contact detection between the old + * and new pose. setKinematicTarget tells PhysX the actor is animating to the + * target during the next simulate(), enabling swept contacts. Some compatibility + * layers need transform writes to stay teleport-like, so the sync mode is + * explicit while {@link move} always keeps target semantics. + * + * @internal + */ + protected override _syncEntityTransformToNative(worldPosition: Vector3, worldRotation: Quaternion): void { + if (this._isKinematic && this._kinematicTransformSyncMode === DynamicColliderKinematicTransformSyncMode.Target) { + (this._nativeCollider).move(worldPosition, worldRotation); + } else { + super._syncEntityTransformToNative(worldPosition, worldRotation); + } + } + /** * @internal */ @@ -460,6 +531,7 @@ export class DynamicCollider extends Collider { target._angularVelocity.copyFrom(this.angularVelocity); target._centerOfMass.copyFrom(this.centerOfMass); target._inertiaTensor.copyFrom(this.inertiaTensor); + target._kinematicTransformSyncMode = this._kinematicTransformSyncMode; super._cloneTo(target); } @@ -489,7 +561,9 @@ export class DynamicCollider extends Collider { } (this._nativeCollider).setMaxAngularVelocity(this._maxAngularVelocity); (this._nativeCollider).setMaxDepenetrationVelocity(this._maxDepenetrationVelocity); - (this._nativeCollider).setSleepThreshold(this._sleepThreshold); + if (this._sleepThreshold !== undefined) { + (this._nativeCollider).setSleepThreshold(this._sleepThreshold); + } (this._nativeCollider).setSolverIterations(this._solverIterations); (this._nativeCollider).setUseGravity(this._useGravity); (this._nativeCollider).setIsKinematic(this._isKinematic); @@ -555,6 +629,16 @@ export enum CollisionDetectionMode { ContinuousSpeculative } +/** + * Kinematic transform synchronization mode. + */ +export enum DynamicColliderKinematicTransformSyncMode { + /** Synchronize transform changes through PhysX setKinematicTarget. */ + Target, + /** Synchronize transform changes by directly teleporting the native actor. */ + Teleport +} + /** * Use these flags to constrain motion of dynamic collider. */ diff --git a/packages/core/src/physics/PhysicsMaterial.ts b/packages/core/src/physics/PhysicsMaterial.ts index 3ac16e66d8..aa22bc8c24 100644 --- a/packages/core/src/physics/PhysicsMaterial.ts +++ b/packages/core/src/physics/PhysicsMaterial.ts @@ -1,6 +1,7 @@ import { IPhysicsMaterial } from "@galacean/engine-design"; import { Engine } from "../Engine"; import { PhysicsMaterialCombineMode } from "./enums/PhysicsMaterialCombineMode"; +import { ignoreClone } from "../clone/CloneManager"; /** * Material class to represent a set of surface properties. @@ -14,6 +15,7 @@ export class PhysicsMaterial { private _destroyed: boolean; /** @internal */ + @ignoreClone _nativeMaterial: IPhysicsMaterial; constructor() { @@ -21,8 +23,8 @@ export class PhysicsMaterial { this._staticFriction, this._dynamicFriction, this._bounciness, - this._bounceCombine, - this._frictionCombine + this._frictionCombine, + this._bounceCombine ); } @@ -103,4 +105,23 @@ export class PhysicsMaterial { !this._destroyed && this._nativeMaterial.destroy(); this._destroyed = true; } + + /** + * @internal + */ + _cloneTo(target: PhysicsMaterial): void { + target._syncNative(); + } + + /** + * @internal + */ + _syncNative(): void { + const nativeMaterial = this._nativeMaterial; + nativeMaterial.setStaticFriction(this._staticFriction); + nativeMaterial.setDynamicFriction(this._dynamicFriction); + nativeMaterial.setBounciness(this._bounciness); + nativeMaterial.setFrictionCombine(this._frictionCombine); + nativeMaterial.setBounceCombine(this._bounceCombine); + } } diff --git a/packages/core/src/physics/PhysicsScene.ts b/packages/core/src/physics/PhysicsScene.ts index b1f96d77dd..5fccc08246 100644 --- a/packages/core/src/physics/PhysicsScene.ts +++ b/packages/core/src/physics/PhysicsScene.ts @@ -646,6 +646,7 @@ export class PhysicsScene { for (let i = 0; i < step; i++) { componentsManager.callScriptOnPhysicsUpdate(); this._callColliderOnUpdate(); + nativePhysicsManager.setContactEventEnabled(this._hasCollisionEventConsumers()); nativePhysicsManager.update(fixedTimeStep); this._callColliderOnLateUpdate(); this._dispatchEvents(nativePhysicsManager.updateEvents()); @@ -825,6 +826,28 @@ export class PhysicsScene { } } + private _hasCollisionEventConsumers(): boolean { + const { _elements: colliders } = this._colliders; + const { onCollisionEnter, onCollisionExit, onCollisionStay } = Script.prototype; + + for (let i = this._colliders.length - 1; i >= 0; --i) { + const scripts = colliders[i].entity._scripts; + const scriptElements = scripts._elements; + for (let j = scripts.length - 1; j >= 0; --j) { + const script = scriptElements[j]; + if ( + script.onCollisionEnter !== onCollisionEnter || + script.onCollisionExit !== onCollisionExit || + script.onCollisionStay !== onCollisionStay + ) { + return true; + } + } + } + + return false; + } + private _setGravity(): void { this._nativePhysicsScene.setGravity(this._gravity); } @@ -835,7 +858,7 @@ export class PhysicsScene { if (!shape) { return false; } - return shape.collider.entity.layer & mask && shape.isSceneQuery; + return shape.collider.collisionLayer & mask && shape.isSceneQuery; }; } diff --git a/packages/core/src/physics/index.ts b/packages/core/src/physics/index.ts index 537bd367d6..f2d4cdf576 100644 --- a/packages/core/src/physics/index.ts +++ b/packages/core/src/physics/index.ts @@ -1,6 +1,11 @@ export { CharacterController } from "./CharacterController"; export { Collider } from "./Collider"; -export { CollisionDetectionMode, DynamicCollider, DynamicColliderConstraints } from "./DynamicCollider"; +export { + CollisionDetectionMode, + DynamicCollider, + DynamicColliderConstraints, + DynamicColliderKinematicTransformSyncMode +} from "./DynamicCollider"; export { HitResult } from "./HitResult"; export { PhysicsMaterial } from "./PhysicsMaterial"; export { PhysicsScene } from "./PhysicsScene"; diff --git a/packages/core/src/physics/shape/ColliderShape.ts b/packages/core/src/physics/shape/ColliderShape.ts index 2ac82319fd..e74736ff64 100644 --- a/packages/core/src/physics/shape/ColliderShape.ts +++ b/packages/core/src/physics/shape/ColliderShape.ts @@ -27,7 +27,7 @@ export abstract class ColliderShape implements ICustomClone { private _rotation: Vector3 = new Vector3(); @deepClone private _position: Vector3 = new Vector3(); - private _contactOffset: number = 0.02; + private _contactOffset: number | undefined; /** * @internal @@ -55,7 +55,7 @@ export abstract class ColliderShape implements ICustomClone { * @defaultValue 0.02 */ get contactOffset(): number { - return this._contactOffset; + return this._contactOffset ?? Engine._nativePhysics?.getDefaultContactOffset?.() ?? 0.02; } set contactOffset(value: number) { @@ -168,6 +168,18 @@ export abstract class ColliderShape implements ICustomClone { target._syncNative(); } + /** + * @internal + * + * Called once per physics update tick by `Collider._onUpdate`. Base no-op. + * + * Subclasses can override for frame-driven maintenance. Currently used by + * `MeshColliderShape` to retry native shape creation when mesh data becomes + * accessible later or PhysX cooking previously failed due to transient state + * (async resource loading, cook pipeline warmup, etc.). + */ + _onPhysicsUpdate(): void {} + /** * @internal */ @@ -183,7 +195,9 @@ export abstract class ColliderShape implements ICustomClone { if (!this._nativeShape) return; this._nativeShape.setPosition(this._position); this._nativeShape.setRotation(this._rotation); - this._nativeShape.setContactOffset(this._contactOffset); + if (this._contactOffset !== undefined) { + this._nativeShape.setContactOffset(this._contactOffset); + } this._nativeShape.setIsTrigger(this._isTrigger); this._nativeShape.setMaterial(this._material._nativeMaterial); diff --git a/packages/core/src/physics/shape/MeshColliderShape.ts b/packages/core/src/physics/shape/MeshColliderShape.ts index 51a46ab0e0..83935fe10c 100644 --- a/packages/core/src/physics/shape/MeshColliderShape.ts +++ b/packages/core/src/physics/shape/MeshColliderShape.ts @@ -16,6 +16,12 @@ export class MeshColliderShape extends ColliderShape { private _indices: Uint8Array | Uint16Array | Uint32Array | null = null; private _cookingFlags = MeshColliderShapeCookingFlag.Cleaning | MeshColliderShapeCookingFlag.VertexWelding; private _isShapeAttached = false; + /** + * `true` if a native shape creation was attempted but failed (mesh data not yet + * accessible, PhysX cooking transient failure, etc.). The `_onPhysicsUpdate` hook + * will keep retrying every frame until creation succeeds. + */ + private _pendingNativeShapeCreation = false; /** * Cooking flags for this mesh collider shape. @@ -74,15 +80,25 @@ export class MeshColliderShape extends ColliderShape { this._mesh?._addReferCount(-1); value?._addReferCount(1); this._mesh = value; - if (value && this._extractMeshData(value)) { - if (this._nativeShape) { - this._updateNativeShapeData(); + if (value) { + if (this._extractMeshData(value)) { + if (this._nativeShape) { + this._updateNativeShapeData(); + } else { + this._createNativeShape(); + } + // _createNativeShape can fail silently (cookMesh transient failure); mark pending if so + this._pendingNativeShapeCreation = !this._nativeShape; } else { - this._createNativeShape(); + // Mesh not yet accessible — keep pending so `_onPhysicsUpdate` retries later + this._destroyNativeShape(); + this._clearMeshData(); + this._pendingNativeShapeCreation = true; } } else { this._destroyNativeShape(); this._clearMeshData(); + this._pendingNativeShapeCreation = false; } } } @@ -197,10 +213,13 @@ export class MeshColliderShape extends ColliderShape { ); if (!nativeShape) { + // Cook failed — `_onPhysicsUpdate` will retry next frame + this._pendingNativeShapeCreation = true; return; } this._nativeShape = nativeShape; + this._pendingNativeShapeCreation = false; // Sync base class properties (position, rotation, contactOffset, isTrigger, material) super._syncNative(); @@ -211,4 +230,38 @@ export class MeshColliderShape extends ColliderShape { this._attachToCollider(); } } + + /** + * @internal + * Retry hook: keep attempting `_createNativeShape` until it succeeds. + * + * Triggered every physics update tick by `Collider._onUpdate`. Handles the case + * where `_cookMesh` fails on first attempt due to PhysX cooking pipeline timing + * (the mesh data was extracted successfully at `set mesh` time, but the native + * cook call returned null). No-op once a valid native shape exists. + * + * We DO NOT re-call `_extractMeshData` here — once `set mesh` finished, either: + * - extraction succeeded → `_positions` is populated and we reuse it + * - extraction failed → `_clearMeshData` cleared `_positions`, and `mesh.accessible` + * won't recover (GPU upload is one-way), so re-extracting would fail again + */ + override _onPhysicsUpdate(): void { + if (!this._pendingNativeShapeCreation || !this._mesh || !this._positions) return; + this._createNativeShape(); + } + + /** + * @internal + * After CloneManager deep-copies `_positions` / `_indices` / `_mesh` and remaps `_collider`, + * the cloned shape still has no native PhysX shape because `_nativeShape` is `@ignoreClone`. + * Cook a fresh native shape now using the already-cloned vertex/index buffers; on transient + * cook failure `_createNativeShape` sets `_pendingNativeShapeCreation = true` and + * `_onPhysicsUpdate` will retry next tick. + */ + override _cloneTo(target: MeshColliderShape): void { + super._cloneTo(target); + if (target._positions) { + target._createNativeShape(); + } + } } diff --git a/packages/core/src/shader/Shader.ts b/packages/core/src/shader/Shader.ts index bbdbc23de4..80de735c90 100644 --- a/packages/core/src/shader/Shader.ts +++ b/packages/core/src/shader/Shader.ts @@ -210,12 +210,12 @@ export class Shader implements IReferable { const passes = subShaders[i].passes; for (let j = 0, m = passes.length; j < m; j++) { const pass = passes[j]; - const passShaderProgramPools = pass._shaderProgramPools; - for (let k = passShaderProgramPools.length - 1; k >= 0; k--) { - const shaderProgramPool = passShaderProgramPools[k]; - if (shaderProgramPool.engine !== engine) continue; - shaderProgramPool._destroy(); - passShaderProgramPools.splice(k, 1); + const passShaderProgramMaps = pass._shaderProgramMaps; + for (let k = passShaderProgramMaps.length - 1; k >= 0; k--) { + const map = passShaderProgramMaps[k]; + if (map.engine !== engine) continue; + map.destroy(); + passShaderProgramMaps.splice(k, 1); } } } diff --git a/packages/core/src/shader/ShaderBlockProperty.ts b/packages/core/src/shader/ShaderBlockProperty.ts new file mode 100644 index 0000000000..fd47b4cecc --- /dev/null +++ b/packages/core/src/shader/ShaderBlockProperty.ts @@ -0,0 +1,28 @@ +/** + * @internal + * Shader block property, used to identify uniform blocks by id. + */ +export class ShaderBlockProperty { + private static _counter = 0; + private static _nameMap: Record = Object.create(null); + + /** + * Get shader block property by name. + * @param name - Name of the uniform block + * @returns Shader block property + */ + static getByName(name: string): ShaderBlockProperty { + const nameMap = ShaderBlockProperty._nameMap; + return (nameMap[name] ??= new ShaderBlockProperty(name)); + } + + /** Uniform block name. */ + readonly name: string; + /** @internal */ + readonly _uniqueId: number; + + private constructor(name: string) { + this.name = name; + this._uniqueId = ShaderBlockProperty._counter++; + } +} diff --git a/packages/core/src/shader/ShaderData.ts b/packages/core/src/shader/ShaderData.ts index 72a2f096f0..a25f85c7ca 100644 --- a/packages/core/src/shader/ShaderData.ts +++ b/packages/core/src/shader/ShaderData.ts @@ -22,6 +22,14 @@ export class ShaderData implements IReferable, IClone { /** @internal */ @ignoreClone _macroCollection: ShaderMacroCollection = new ShaderMacroCollection(); + /** + * @internal + * Sorted (ascending) property ids of renderer-group samplers and arrays. These values + * can't differ across instances inside an instanced draw call — `_canBatch` uses this + * list to refuse batching renderers that would disagree on what to bind + */ + @ignoreClone + _instanceBatchFields: number[] = []; @ignoreClone private _macroMap: Record = Object.create(null); @@ -607,7 +615,11 @@ export class ShaderData implements IReferable, IClone { cloneTo(target: ShaderData): void { CloneManager.deepCloneObject(this._macroCollection, target._macroCollection, new Map()); - Object.assign(target._macroMap, this._macroMap); + const targetMacroMap = target._macroMap; + for (const key in targetMacroMap) { + delete targetMacroMap[key]; + } + Object.assign(targetMacroMap, this._macroMap); const referCount = target._getReferCount(); const propertyValueMap = this._propertyValueMap; const targetPropertyValueMap = target._propertyValueMap; @@ -665,7 +677,61 @@ export class ShaderData implements IReferable, IClone { } } - this._propertyValueMap[property._uniqueId] = value; + const id = property._uniqueId; + // Track renderer-group samplers/arrays so `_canBatch` can refuse instanced batches + // whose leader and follower would disagree on what to bind + if ( + this._group === ShaderDataGroup.Renderer && + (type === ShaderPropertyType.Texture || + type === ShaderPropertyType.TextureArray || + type === ShaderPropertyType.FloatArray || + type === ShaderPropertyType.IntArray) + ) { + const oldHas = this._propertyValueMap[id] != null; + const newHas = value != null; + if (oldHas !== newHas) { + const fields = this._instanceBatchFields; + if (newHas) { + // Insert keeping ascending order so exact compare is index-by-index + let lo = 0; + let hi = fields.length; + while (lo < hi) { + const mid = (lo + hi) >>> 1; + if (fields[mid] < id) { + lo = mid + 1; + } else { + hi = mid; + } + } + fields.splice(lo, 0, id); + } else { + fields.splice(fields.indexOf(id), 1); + } + } + } + + this._propertyValueMap[id] = value; + } + + /** + * @internal + */ + _matchesRendererInstanceBatch(other: ShaderData): boolean { + const selfFields = this._instanceBatchFields; + const otherFields = other._instanceBatchFields; + const fieldCount = selfFields.length; + if (fieldCount !== otherFields.length) { + return false; + } + const selfMap = this._propertyValueMap; + const otherMap = other._propertyValueMap; + for (let i = 0; i < fieldCount; i++) { + const id = selfFields[i]; + if (id !== otherFields[i] || selfMap[id] !== otherMap[id]) { + return false; + } + } + return true; } /** diff --git a/packages/core/src/shader/ShaderMacroCollection.ts b/packages/core/src/shader/ShaderMacroCollection.ts index 749abc22ee..3efb98b77c 100644 --- a/packages/core/src/shader/ShaderMacroCollection.ts +++ b/packages/core/src/shader/ShaderMacroCollection.ts @@ -158,6 +158,20 @@ export class ShaderMacroCollection { return (this._mask[index] & macro._maskValue) !== 0; } + /** + * Whether this macro collection is equal to another. + * @param other - macro collection to compare + */ + isEqual(other: ShaderMacroCollection): boolean { + if (this._length !== other._length) return false; + const mask = this._mask; + const otherMask = other._mask; + for (let i = 0, n = this._length; i < n; i++) { + if (mask[i] !== otherMask[i]) return false; + } + return true; + } + /** * Clear this macro collection. */ diff --git a/packages/core/src/shader/ShaderPass.ts b/packages/core/src/shader/ShaderPass.ts index 8c11182df7..6dc63f0220 100644 --- a/packages/core/src/shader/ShaderPass.ts +++ b/packages/core/src/shader/ShaderPass.ts @@ -1,13 +1,14 @@ import { Engine } from "../Engine"; +import { InstanceBuffer } from "../RenderPipeline/InstanceBuffer"; import { PipelineStage } from "../RenderPipeline/enums/PipelineStage"; import { GLCapabilityType } from "../base/Constant"; -import { ShaderFactory } from "../shaderlib"; +import { ShaderFactory, InstanceBufferLayout } from "../shaderlib/ShaderFactory"; import { Shader } from "./Shader"; import { ShaderMacro } from "./ShaderMacro"; import { ShaderMacroCollection } from "./ShaderMacroCollection"; import { ShaderPart } from "./ShaderPart"; +import { ShaderProgramMap } from "./ShaderProgramMap"; import { ShaderProgram } from "./ShaderProgram"; -import { ShaderProgramPool } from "./ShaderProgramPool"; import { ShaderProperty } from "./ShaderProperty"; import { ShaderLanguage } from "./enums/ShaderLanguage"; import { RenderState } from "./state/RenderState"; @@ -47,7 +48,7 @@ export class ShaderPass extends ShaderPart { /** @internal */ _renderStateDataMap: Record = {}; /** @internal */ - _shaderProgramPools: ShaderProgramPool[] = []; + _shaderProgramMaps: ShaderProgramMap[] = []; private _vertexSource: string; private _fragmentSource: string; @@ -110,15 +111,15 @@ export class ShaderPass extends ShaderPart { * @internal */ _getShaderProgram(engine: Engine, macroCollection: ShaderMacroCollection): ShaderProgram { - const shaderProgramPool = engine._getShaderProgramPool(this._shaderPassId, this._shaderProgramPools); - let shaderProgram = shaderProgramPool.get(macroCollection); + const shaderProgramMap = engine._getShaderProgramMap(this._shaderPassId, this._shaderProgramMaps); + let shaderProgram = shaderProgramMap.get(macroCollection); if (shaderProgram) { return shaderProgram; } - shaderProgram = this._getCanonicalShaderProgram(engine, macroCollection); + shaderProgram = this._compileShaderProgram(engine, macroCollection); - shaderProgramPool.cache(shaderProgram); + shaderProgramMap.cache(shaderProgram); return shaderProgram; } @@ -126,32 +127,46 @@ export class ShaderPass extends ShaderPart { * @internal */ _destroy(): void { - const shaderProgramPools = this._shaderProgramPools; - for (let i = 0, n = shaderProgramPools.length; i < n; i++) { - const shaderProgramPool = shaderProgramPools[i]; - shaderProgramPool._destroy(); - delete shaderProgramPool.engine._shaderProgramPools[this._shaderPassId]; + const shaderProgramMaps = this._shaderProgramMaps; + for (let i = 0, n = shaderProgramMaps.length; i < n; i++) { + const map = shaderProgramMaps[i]; + map.destroy(); + delete map.engine._shaderProgramMaps[this._shaderPassId]; } - // Clear array storing multiple engine shader program pools - shaderProgramPools.length = 0; + shaderProgramMaps.length = 0; } - private _getCanonicalShaderProgram(engine: Engine, macroCollection: ShaderMacroCollection): ShaderProgram { - if (this._platformTarget != undefined) { - return this._getShaderLabProgram(engine, macroCollection); - } + private _compileShaderProgram(engine: Engine, macroCollection: ShaderMacroCollection): ShaderProgram { + const isGPUInstance = macroCollection.isEnable(InstanceBuffer.gpuInstanceMacro); + const { vertexSource, fragmentSource, instanceLayout } = + this._platformTarget != undefined + ? this._compileShaderLabSource(engine, macroCollection, isGPUInstance) + : this._compilePlatformSource(engine, macroCollection, isGPUInstance); + + const program = new ShaderProgram(engine, vertexSource, fragmentSource); + program._instanceLayout = instanceLayout; + return program; + } - const { vertexSource, fragmentSource } = ShaderFactory.compilePlatformSource( + private _compilePlatformSource( + engine: Engine, + macroCollection: ShaderMacroCollection, + isGPUInstance: boolean + ): { vertexSource: string; fragmentSource: string; instanceLayout: InstanceBufferLayout | null } { + return ShaderFactory.compilePlatformSource( engine, macroCollection, this._vertexSource, - this._fragmentSource + this._fragmentSource, + isGPUInstance ); - - return new ShaderProgram(engine, vertexSource, fragmentSource); } - private _getShaderLabProgram(engine: Engine, macroCollection: ShaderMacroCollection): ShaderProgram { + private _compileShaderLabSource( + engine: Engine, + macroCollection: ShaderMacroCollection, + isGPUInstance: boolean + ): { vertexSource: string; fragmentSource: string; instanceLayout: InstanceBufferLayout | null } { const isWebGL2: boolean = engine._hardwareRenderer.isWebGL2; const shaderMacroList = new Array(); ShaderMacro._getMacrosElements(macroCollection, shaderMacroList); @@ -169,6 +184,14 @@ export class ShaderPass extends ShaderPart { noIncludeVertex = Shader._shaderLab._parseMacros(noIncludeVertex, shaderMacroList); noIncludeFrag = Shader._shaderLab._parseMacros(noIncludeFrag, shaderMacroList); + let instanceLayout: InstanceBufferLayout | null = null; + if (isGPUInstance) { + const injected = ShaderFactory.injectInstanceUBO(engine, noIncludeVertex, noIncludeFrag); + noIncludeVertex = injected.vertexSource; + noIncludeFrag = injected.fragmentSource; + instanceLayout = injected.instanceLayout; + } + if (isWebGL2 && this._platformTarget === ShaderLanguage.GLSLES100) { noIncludeVertex = ShaderFactory.convertTo300(noIncludeVertex); noIncludeFrag = ShaderFactory.convertTo300(noIncludeFrag, true); @@ -176,15 +199,16 @@ export class ShaderPass extends ShaderPart { const versionStr = isWebGL2 ? "#version 300 es" : "#version 100"; - const vertexSource = ` ${versionStr} + return { + vertexSource: ` ${versionStr} ${noIncludeVertex} - `; - const fragmentSource = ` ${versionStr} - ${isWebGL2 ? "" : ShaderFactory._shaderExtension} + `, + fragmentSource: ` ${versionStr} + ${isWebGL2 ? "" : ShaderFactory.shaderExtension} ${precisionStr} ${noIncludeFrag} - `; - - return new ShaderProgram(engine, vertexSource, fragmentSource); + `, + instanceLayout + }; } } diff --git a/packages/core/src/shader/ShaderPool.ts b/packages/core/src/shader/ShaderPool.ts index e677d9e22f..231f1121fe 100644 --- a/packages/core/src/shader/ShaderPool.ts +++ b/packages/core/src/shader/ShaderPool.ts @@ -24,6 +24,8 @@ import spriteMaskFs from "../shaderlib/extra/sprite-mask.fs.glsl"; import spriteMaskVs from "../shaderlib/extra/sprite-mask.vs.glsl"; import spriteFs from "../shaderlib/extra/sprite.fs.glsl"; import spriteVs from "../shaderlib/extra/sprite.vs.glsl"; +import spineFs from "../shaderlib/extra/spine.fs.glsl"; +import spineVs from "../shaderlib/extra/spine.vs.glsl"; import textFs from "../shaderlib/extra/text.fs.glsl"; import textVs from "../shaderlib/extra/text.vs.glsl"; import trailFs from "../shaderlib/extra/trail.fs.glsl"; @@ -33,7 +35,10 @@ import unlitVs from "../shaderlib/extra/unlit.vs.glsl"; import { TransformFeedbackShader } from "../graphic/TransformFeedbackShader"; import { Shader } from "./Shader"; import { ShaderPass } from "./ShaderPass"; +import { ShaderProperty } from "./ShaderProperty"; +import { CullMode } from "./enums/CullMode"; import { RenderStateElementKey } from "./enums/RenderStateElementKey"; +import { RenderQueueType } from "./enums/RenderQueueType"; import { RenderState } from "./state"; /** @@ -87,8 +92,32 @@ export class ShaderPool { Shader.create("SpriteMask", [new ShaderPass("Forward", spriteMaskVs, spriteMaskFs, forwardPassTags)]); Shader.create("Sprite", [new ShaderPass("Forward", spriteVs, spriteFs, forwardPassTags)]); Shader.create("Text", [new ShaderPass("Forward", textVs, textFs, forwardPassTags)]); + Shader.create("2D/Spine", [ShaderPool._createSpinePass(forwardPassTags)]); Shader.create("background-texture", [ new ShaderPass("Forward", backgroundTextureVs, backgroundTextureFs, forwardPassTags) ]); } + + private static _createSpinePass(tags: Record): ShaderPass { + const pass = new ShaderPass("Forward", spineVs, spineFs, tags); + const renderState = (pass._renderState = new RenderState()); + const blendState = renderState.blendState.targetBlendState; + + blendState.enabled = true; + renderState.depthState.writeEnabled = false; + renderState.rasterState.cullMode = CullMode.Off; + renderState.renderQueueType = RenderQueueType.Transparent; + + const renderStateDataMap = pass._renderStateDataMap; + renderStateDataMap[RenderStateElementKey.BlendStateSourceColorBlendFactor0] = + ShaderProperty.getByName("sourceColorBlendFactor"); + renderStateDataMap[RenderStateElementKey.BlendStateDestinationColorBlendFactor0] = + ShaderProperty.getByName("destinationColorBlendFactor"); + renderStateDataMap[RenderStateElementKey.BlendStateSourceAlphaBlendFactor0] = + ShaderProperty.getByName("sourceAlphaBlendFactor"); + renderStateDataMap[RenderStateElementKey.BlendStateDestinationAlphaBlendFactor0] = + ShaderProperty.getByName("destinationAlphaBlendFactor"); + + return pass; + } } diff --git a/packages/core/src/shader/ShaderProgram.ts b/packages/core/src/shader/ShaderProgram.ts index 22151e5f77..9548548db6 100644 --- a/packages/core/src/shader/ShaderProgram.ts +++ b/packages/core/src/shader/ShaderProgram.ts @@ -7,7 +7,9 @@ import { ShaderData } from "./ShaderData"; import { ShaderProperty } from "./ShaderProperty"; import { ShaderUniform } from "./ShaderUniform"; import { ShaderUniformBlock } from "./ShaderUniformBlock"; +import { ShaderBlockProperty } from "./ShaderBlockProperty"; import { ShaderDataGroup } from "./enums/ShaderDataGroup"; +import { InstanceBufferLayout, ShaderFactory } from "../shaderlib/ShaderFactory"; /** * Shader program, corresponding to the GPU shader program. @@ -52,7 +54,11 @@ export class ShaderProgram { /** @internal */ _uploadMaterialId: number = -1; + /** @internal */ + _instanceLayout: InstanceBufferLayout | null = null; + attributeLocation: Record = Object.create(null); + uniformBlockIds: number[] = []; // @todo: move to RHI. private _isValid: boolean; @@ -339,6 +345,9 @@ export class ShaderProgram { } const location = gl.getUniformLocation(program, name); + // UBO members have no individual location, skip them + if (location === null) return; + shaderUniform.name = name; shaderUniform.propertyId = ShaderProperty.getByName(name)._uniqueId; shaderUniform.location = location; @@ -412,9 +421,33 @@ export class ShaderProgram { shaderUniform.cacheValue = new Vector4(0, 0, 0); } break; + case gl.FLOAT_MAT2: + shaderUniform.applyFunc = shaderUniform.uploadMat2; + break; + case gl.FLOAT_MAT3: + shaderUniform.applyFunc = shaderUniform.uploadMat3; + break; case gl.FLOAT_MAT4: shaderUniform.applyFunc = isArray ? shaderUniform.uploadMat4v : shaderUniform.uploadMat4; break; + case (gl).FLOAT_MAT2x3: + shaderUniform.applyFunc = shaderUniform.uploadMat2x3; + break; + case (gl).FLOAT_MAT2x4: + shaderUniform.applyFunc = shaderUniform.uploadMat2x4; + break; + case (gl).FLOAT_MAT3x2: + shaderUniform.applyFunc = shaderUniform.uploadMat3x2; + break; + case (gl).FLOAT_MAT3x4: + shaderUniform.applyFunc = shaderUniform.uploadMat3x4; + break; + case (gl).FLOAT_MAT4x2: + shaderUniform.applyFunc = shaderUniform.uploadMat4x2; + break; + case (gl).FLOAT_MAT4x3: + shaderUniform.applyFunc = shaderUniform.uploadMat4x3; + break; case gl.SAMPLER_2D: case gl.SAMPLER_CUBE: case (gl).UNSIGNED_INT_SAMPLER_2D: @@ -476,6 +509,21 @@ export class ShaderProgram { attributeInfos.forEach(({ name }) => { this.attributeLocation[name] = gl.getAttribLocation(program, name); }); + + // Record uniform block indices and bind binding points (WebGL2 only) + if (this._engine._hardwareRenderer.isWebGL2) { + const gl2 = gl; + const bindingMap = ShaderFactory.uniformBlockBindingMap; + const blockCount = gl2.getProgramParameter(program, gl2.ACTIVE_UNIFORM_BLOCKS) ?? 0; + for (let i = 0; i < blockCount; i++) { + const id = ShaderBlockProperty.getByName(gl2.getActiveUniformBlockName(program, i))._uniqueId; + this.uniformBlockIds[i] = id; + const bindingPoint = bindingMap[id]; + if (bindingPoint !== undefined) { + gl2.uniformBlockBinding(program, i, bindingPoint); + } + } + } } private _getUniformInfos(): WebGLActiveInfo[] { diff --git a/packages/core/src/shader/ShaderProgramPool.ts b/packages/core/src/shader/ShaderProgramMap.ts similarity index 56% rename from packages/core/src/shader/ShaderProgramPool.ts rename to packages/core/src/shader/ShaderProgramMap.ts index 3b43a07afa..5673708863 100644 --- a/packages/core/src/shader/ShaderProgramPool.ts +++ b/packages/core/src/shader/ShaderProgramMap.ts @@ -2,23 +2,26 @@ import { Engine } from "../Engine"; import { ShaderMacroCollection } from "./ShaderMacroCollection"; import { ShaderProgram } from "./ShaderProgram"; +type Tree = { + [key: number]: Tree | ShaderProgram; +}; + /** - * Shader program pool. + * Map keyed by ShaderMacroCollection bitmask, caching ShaderProgram instances. * @internal */ -export class ShaderProgramPool { +export class ShaderProgramMap { + engine: Engine; + private _cacheHierarchyDepth: number = 1; - private _cacheMap: Tree = Object.create(null); + private _cacheMap: Tree = Object.create(null); private _lastQueryMap: Record; private _lastQueryKey: number; - constructor(public engine: Engine) {} + constructor(engine: Engine) { + this.engine = engine; + } - /** - * Get shader program by macro collection. - * @param macros - macro collection - * @returns shader program - */ get(macros: ShaderMacroCollection): ShaderProgram | null { let cacheMap = this._cacheMap; const maskLength = macros._length; @@ -33,41 +36,31 @@ export class ShaderProgramPool { const maxEndIndex = this._cacheHierarchyDepth - 1; for (let i = 0; i < maxEndIndex; i++) { const subMask = endIndex < i ? 0 : mask[i]; - let subCacheShaders = >cacheMap[subMask]; - subCacheShaders || (cacheMap[subMask] = subCacheShaders = Object.create(null)); - cacheMap = subCacheShaders; + let subCache = cacheMap[subMask]; + subCache || (cacheMap[subMask] = subCache = Object.create(null)); + cacheMap = subCache; } const cacheKey = endIndex < maxEndIndex ? 0 : mask[maxEndIndex]; - const shader = (>cacheMap)[cacheKey]; - if (!shader) { + const value = (>cacheMap)[cacheKey]; + if (!value) { this._lastQueryKey = cacheKey; this._lastQueryMap = >cacheMap; } - return shader; + return value; } - /** - * Cache the shader program. - * - * @remarks - * The method must return an empty value after calling get() to run normally. - * - * @param shaderProgram - shader program - */ - cache(shaderProgram: ShaderProgram): void { - this._lastQueryMap[this._lastQueryKey] = shaderProgram; + cache(value: ShaderProgram): void { + this._lastQueryMap[this._lastQueryKey] = value; } - /** - * @internal - */ - _destroy(): void { - this._recursiveDestroy(0, this._cacheMap); + destroy(): void { + this._recursiveForEach(0, this._cacheMap); this._cacheMap = Object.create(null); + this._cacheHierarchyDepth = 1; } - private _recursiveDestroy(hierarchy: number, cacheMap: Tree): void { + private _recursiveForEach(hierarchy: number, cacheMap: Tree): void { if (hierarchy === this._cacheHierarchyDepth - 1) { for (let k in cacheMap) { (cacheMap[k]).destroy(); @@ -76,35 +69,30 @@ export class ShaderProgramPool { } ++hierarchy; for (let k in cacheMap) { - this._recursiveDestroy(hierarchy, >cacheMap[k]); + this._recursiveForEach(hierarchy, cacheMap[k]); } } private _resizeCacheMapHierarchy( - cacheMap: Tree, + cacheMap: Tree, hierarchy: number, currentHierarchy: number, increaseHierarchy: number ): void { - // Only expand but not shrink if (hierarchy == currentHierarchy - 1) { for (let k in cacheMap) { - const shader = cacheMap[k]; + const value = cacheMap[k]; let subCacheMap = cacheMap; for (let i = 0; i < increaseHierarchy; i++) { subCacheMap[i == 0 ? k : 0] = subCacheMap = Object.create(null); } - subCacheMap[0] = shader; + subCacheMap[0] = value; } } else { hierarchy++; for (let k in cacheMap) { - this._resizeCacheMapHierarchy(>cacheMap[k], hierarchy, currentHierarchy, increaseHierarchy); + this._resizeCacheMapHierarchy(cacheMap[k], hierarchy, currentHierarchy, increaseHierarchy); } } } } - -type Tree = { - [key: number]: Tree | T; -}; diff --git a/packages/core/src/shader/ShaderUniform.ts b/packages/core/src/shader/ShaderUniform.ts index 07017a4abc..90d9233c1b 100644 --- a/packages/core/src/shader/ShaderUniform.ts +++ b/packages/core/src/shader/ShaderUniform.ts @@ -237,6 +237,14 @@ export class ShaderUniform { this._gl.uniform4iv(shaderUniform.location, value); } + uploadMat2(shaderUniform: ShaderUniform, value: Float32Array): void { + this._gl.uniformMatrix2fv(shaderUniform.location, false, value); + } + + uploadMat3(shaderUniform: ShaderUniform, value: Float32Array): void { + this._gl.uniformMatrix3fv(shaderUniform.location, false, value); + } + uploadMat4(shaderUniform: ShaderUniform, value: Matrix): void { this._gl.uniformMatrix4fv(shaderUniform.location, false, value.elements); } @@ -245,6 +253,30 @@ export class ShaderUniform { this._gl.uniformMatrix4fv(shaderUniform.location, false, value); } + uploadMat2x3(shaderUniform: ShaderUniform, value: Float32Array): void { + (this._gl).uniformMatrix2x3fv(shaderUniform.location, false, value); + } + + uploadMat2x4(shaderUniform: ShaderUniform, value: Float32Array): void { + (this._gl).uniformMatrix2x4fv(shaderUniform.location, false, value); + } + + uploadMat3x2(shaderUniform: ShaderUniform, value: Float32Array): void { + (this._gl).uniformMatrix3x2fv(shaderUniform.location, false, value); + } + + uploadMat3x4(shaderUniform: ShaderUniform, value: Float32Array): void { + (this._gl).uniformMatrix3x4fv(shaderUniform.location, false, value); + } + + uploadMat4x2(shaderUniform: ShaderUniform, value: Float32Array): void { + (this._gl).uniformMatrix4x2fv(shaderUniform.location, false, value); + } + + uploadMat4x3(shaderUniform: ShaderUniform, value: Float32Array): void { + (this._gl).uniformMatrix4x3fv(shaderUniform.location, false, value); + } + uploadTexture(shaderUniform: ShaderUniform, value: Texture): void { const rhi = this._rhi; rhi.activeTexture(shaderUniform.textureIndex as GLenum); diff --git a/packages/core/src/shader/enums/ConstantBufferBindingPoint.ts b/packages/core/src/shader/enums/ConstantBufferBindingPoint.ts new file mode 100644 index 0000000000..595d159924 --- /dev/null +++ b/packages/core/src/shader/enums/ConstantBufferBindingPoint.ts @@ -0,0 +1,7 @@ +/** + * @internal + * Constant buffer binding point allocation. + */ +export enum ConstantBufferBindingPoint { + RendererInstance = 0 +} diff --git a/packages/core/src/shaderlib/ShaderFactory.ts b/packages/core/src/shaderlib/ShaderFactory.ts index e314e4409d..9e62a3fc25 100644 --- a/packages/core/src/shaderlib/ShaderFactory.ts +++ b/packages/core/src/shaderlib/ShaderFactory.ts @@ -1,13 +1,28 @@ +import { Matrix, Vector2, Vector3, Vector4 } from "@galacean/engine-math"; import { GLCapabilityType } from "../base/Constant"; import { Logger } from "../base/Logger"; import { Engine } from "../Engine"; +import { Renderer } from "../Renderer"; +import { ShaderDataGroup } from "../shader/enums/ShaderDataGroup"; import { ShaderMacro } from "../shader/ShaderMacro"; import { ShaderMacroCollection } from "../shader/ShaderMacroCollection"; +import { ShaderProperty } from "../shader/ShaderProperty"; +import { ShaderBlockProperty } from "../shader/ShaderBlockProperty"; +import { ConstantBufferBindingPoint } from "../shader/enums/ConstantBufferBindingPoint"; import { ShaderLib } from "./ShaderLib"; +/** + * @internal + */ export class ShaderFactory { - /** @internal */ - static readonly _shaderExtension = [ + static readonly RENDERER_INSTANCE_BLOCK_NAME = "RendererInstanceData"; + + static readonly uniformBlockBindingMap: Record = { + [ShaderBlockProperty.getByName(ShaderFactory.RENDERER_INSTANCE_BLOCK_NAME)._uniqueId]: + ConstantBufferBindingPoint.RendererInstance + }; + + static readonly shaderExtension = [ "GL_EXT_shader_texture_lod", "GL_OES_standard_derivatives", "GL_EXT_draw_buffers", @@ -16,35 +31,140 @@ export class ShaderFactory { .map((e) => `#extension ${e} : enable\n`) .join(""); - private static readonly _has300OutInFragReg = /\bout\s+(?:\w+\s+)?(?:vec4)\s+(?:\w+)\s*;/; // [layout(location = 0)] out [highp] vec4 [color]; + private static readonly _std140TypeInfoMap: Record = { + float: { size: 4, align: 4 }, + int: { size: 4, align: 4 }, + uint: { size: 4, align: 4 }, + vec2: { size: 8, align: 8 }, + ivec2: { size: 8, align: 8 }, + vec3: { size: 12, align: 16 }, + ivec3: { size: 12, align: 16 }, + vec4: { size: 16, align: 16 }, + ivec4: { size: 16, align: 16 }, + mat4: { size: 64, align: 16 }, + mat3x4: { size: 48, align: 16 } + }; + + private static readonly _has300OutInFragReg = /\bout\s+(?:\w+\s+)?vec4\s+\w+\s*;/; + + private static readonly _precisionStr = ` +#ifdef GL_FRAGMENT_PRECISION_HIGH + precision highp float; + precision highp int; +#else + precision mediump float; + precision mediump int; +#endif +`; + + // Derived built-ins re-exposed on top of `renderer_ModelMat`. + // `renderer_NormalMat` uses the cofactor (cross-product) form, which algebraically equals + // `det(M) · transpose(inverse(M))`. After `normalize()` it's directionally identical to the + // classic `transpose(inverse(M))`, but stays NaN-free when `M` is singular (e.g. any scale + // axis is 0 — common in animations that pop / hide via scale). `sign(det)` (`s` below) + // keeps mirrored matrices facing the right way + private static readonly _derivedDefines = `\ +mat3 _normalMatFromModel(mat3 m) { + vec3 c0 = cross(m[1], m[2]); + vec3 c1 = cross(m[2], m[0]); + vec3 c2 = cross(m[0], m[1]); + float s = (dot(m[0], c0) < 0.0) ? -1.0 : 1.0; + return mat3(c0 * s, c1 * s, c2 * s); +} +#define renderer_MVMat (camera_ViewMat * renderer_ModelMat) +#define renderer_MVPMat (camera_VPMat * renderer_ModelMat) +#define renderer_NormalMat mat4(_normalMatFromModel(mat3(renderer_ModelMat)))`; + + // Built-in renderer uniforms. value=true means derived (remove but not added to UBO) + private static readonly _builtinRendererUniforms: Record = { + renderer_ModelMat: false, + renderer_Layer: false, + renderer_MVMat: true, + renderer_MVPMat: true, + renderer_NormalMat: true + }; + + private static readonly _uboUniformRegex = + /^[ \t]*uniform\s+(?:(?:lowp|mediump|highp)\s+)?(\w+)\s+(\w+)\s*(\[.+?\])?\s*;/gm; + + private static _packFuncMap: Record = (() => { + const packScalar = (v: Float32Array | Int32Array, o: number, val: number) => { + v[o] = val; + }; + const packVec2 = (v: Float32Array | Int32Array, o: number, val: Vector2) => { + v[o] = val.x; + v[o + 1] = val.y; + }; + const packVec3 = (v: Float32Array | Int32Array, o: number, val: Vector3) => { + v[o] = val.x; + v[o + 1] = val.y; + v[o + 2] = val.z; + }; + const packVec4 = (v: Float32Array | Int32Array, o: number, val: Vector4) => { + v[o] = val.x; + v[o + 1] = val.y; + v[o + 2] = val.z; + v[o + 3] = val.w; + }; + return { + float: packScalar, + int: packScalar, + uint: packScalar, + vec2: packVec2, + ivec2: packVec2, + vec3: packVec3, + ivec3: packVec3, + vec4: packVec4, + ivec4: packVec4, + mat4: (v: Float32Array | Int32Array, o: number, val: Matrix) => { + const e = val.elements; + for (let k = 0; k < 16; k++) v[o + k] = e[k]; + }, + // Affine mat4 stored as mat3x4: write 3 transposed rows (row3 is always 0,0,0,1) + mat3x4: (v: Float32Array | Int32Array, o: number, val: Matrix) => { + const e = val.elements; + // Row 0 + v[o] = e[0]; + v[o + 1] = e[4]; + v[o + 2] = e[8]; + v[o + 3] = e[12]; + // Row 1 + v[o + 4] = e[1]; + v[o + 5] = e[5]; + v[o + 6] = e[9]; + v[o + 7] = e[13]; + // Row 2 + v[o + 8] = e[2]; + v[o + 9] = e[6]; + v[o + 10] = e[10]; + v[o + 11] = e[14]; + } + }; + })(); static parseCustomMacros(macros: ShaderMacro[]) { return macros.map((m) => `#define ${m.value ? m.name + ` ` + m.value : m.name}\n`).join(""); } /** - * @internal * Compile vertex and fragment source with standard macros, includes, and version header. - * @param engine - Engine instance - * @param macroCollection - Current macro collection - * @param vertexSource - Raw vertex shader source (may contain #include) - * @param fragmentSource - Raw fragment shader source - * @returns Compiled { vertexSource, fragmentSource } ready for ShaderProgram */ static compilePlatformSource( engine: Engine, macroCollection: ShaderMacroCollection, vertexSource: string, - fragmentSource: string - ): { vertexSource: string; fragmentSource: string } { - const isWebGL2 = engine._hardwareRenderer.isWebGL2; + fragmentSource: string, + isGPUInstance: boolean + ): { vertexSource: string; fragmentSource: string; instanceLayout: InstanceBufferLayout | null } { + const rhi = engine._hardwareRenderer; + const isWebGL2 = rhi.isWebGL2; const shaderMacroList = new Array(); ShaderMacro._getMacrosElements(macroCollection, shaderMacroList); shaderMacroList.push(ShaderMacro.getByName(isWebGL2 ? "GRAPHICS_API_WEBGL2" : "GRAPHICS_API_WEBGL1")); - if (engine._hardwareRenderer.canIUse(GLCapabilityType.shaderTextureLod)) { + if (rhi.canIUse(GLCapabilityType.shaderTextureLod)) { shaderMacroList.push(ShaderMacro.getByName("HAS_TEX_LOD")); } - if (engine._hardwareRenderer.canIUse(GLCapabilityType.standardDerivatives)) { + if (rhi.canIUse(GLCapabilityType.standardDerivatives)) { shaderMacroList.push(ShaderMacro.getByName("HAS_DERIVATIVES")); } @@ -55,28 +175,82 @@ export class ShaderFactory { noIncludeVertex = macroStr + noIncludeVertex; noIncludeFrag = macroStr + noIncludeFrag; + let instanceLayout: InstanceBufferLayout | null = null; + if (isGPUInstance) { + const activeMacros = new Set(); + for (let i = 0, len = shaderMacroList.length; i < len; i++) activeMacros.add(shaderMacroList[i].name); + const injected = ShaderFactory.injectInstanceUBO(engine, noIncludeVertex, noIncludeFrag, activeMacros); + noIncludeVertex = injected.vertexSource; + noIncludeFrag = injected.fragmentSource; + instanceLayout = injected.instanceLayout; + } + if (isWebGL2) { noIncludeVertex = ShaderFactory.convertTo300(noIncludeVertex); noIncludeFrag = ShaderFactory.convertTo300(noIncludeFrag, true); } const versionStr = isWebGL2 ? "#version 300 es" : "#version 100"; - const precisionStr = ` -#ifdef GL_FRAGMENT_PRECISION_HIGH - precision highp float; - precision highp int; -#else - precision mediump float; - precision mediump int; -#endif -`; return { vertexSource: `${versionStr}\nprecision highp float;\n${noIncludeVertex}`, - fragmentSource: `${versionStr}\n${isWebGL2 ? "" : ShaderFactory._shaderExtension}${precisionStr}${noIncludeFrag}` + fragmentSource: `${versionStr}\n${isWebGL2 ? "" : ShaderFactory.shaderExtension}${ShaderFactory._precisionStr}${noIncludeFrag}`, + instanceLayout }; } + /** + * Scan VS/FS for renderer-group `uniform` declarations, replace them with a shared + * std140 UBO (instanced array), and emit `#define` remapping so original uniform + * names resolve to `rendererData[instanceID].field`. + */ + static injectInstanceUBO( + engine: Engine, + vertexSource: string, + fragmentSource: string, + activeMacros?: Set + ): { vertexSource: string; fragmentSource: string; instanceLayout: InstanceBufferLayout | null } { + // 1. Scan & strip renderer uniforms from both stages, collect into fieldMap + const fieldMap: Record = Object.create(null); + if (activeMacros) { + vertexSource = ShaderFactory._scanInstanceUniformsWithMacros(vertexSource, fieldMap, activeMacros); + fragmentSource = ShaderFactory._scanInstanceUniformsWithMacros(fragmentSource, fieldMap, activeMacros); + } else { + vertexSource = ShaderFactory._scanInstanceUniforms(vertexSource, fieldMap); + fragmentSource = ShaderFactory._scanInstanceUniforms(fragmentSource, fieldMap); + } + + // Fast empty check without allocating an array + let hasField = false; + for (const _ in fieldMap) { + hasField = true; + break; + } + if (!hasField) return { vertexSource, fragmentSource, instanceLayout: null }; + + // 2. Compute std140 layout (field offsets, struct size, max instance count) + const instanceLayout = ShaderFactory._buildLayout(engine, fieldMap); + + // 3. Generate GLSL UBO block and inject into both stages + const { instanceFields } = instanceLayout; + const uboDecl = ShaderFactory._buildUBODeclaration(instanceLayout); + const fieldDefinesVS = ShaderFactory._buildFieldDefines(instanceFields, "gl_InstanceID"); + const fieldDefinesFS = ShaderFactory._buildFieldDefines(instanceFields, "v_instanceID"); + const derivedDefines = ShaderFactory._derivedDefines; + + const vsBlock = `${uboDecl}flat out int v_instanceID;\n${fieldDefinesVS}\n${derivedDefines}\n`; + const fsBlock = `${uboDecl}flat in int v_instanceID;\n${fieldDefinesFS}\n${derivedDefines}\n`; + + vertexSource = vsBlock + vertexSource; + vertexSource = vertexSource.replace( + /void\s+main\s*\(\s*\)\s*\{/, + "void main() {\n v_instanceID = gl_InstanceID;" + ); + fragmentSource = fsBlock + fragmentSource; + + return { vertexSource, fragmentSource, instanceLayout }; + } + static registerInclude(includeName: string, includeSource: string) { if (ShaderLib[includeName]) { throw `The "${includeName}" shader include already exist`; @@ -93,25 +267,21 @@ export class ShaderFactory { * since `ShaderLab` use the same parsing function but different syntax for `#include` --- `/^[ \t]*#include +"([\w\d.]+)"/gm` */ static parseIncludes(src: string, regex = /^[ \t]*#include +<([\w\d.]+)>/gm) { - function replace(match, slice) { - var replace = ShaderLib[slice]; - - if (replace === undefined) { + return src.replace(regex, (match, slice) => { + const replacement = ShaderLib[slice]; + if (replacement === undefined) { Logger.error(`Shader slice "${match.trim()}" not founded.`); return ""; } - - return ShaderFactory.parseIncludes(replace, regex); - } - - return src.replace(regex, replace); + return ShaderFactory.parseIncludes(replacement, regex); + }); } /** * Convert lower GLSL version to GLSL 300 es. * @param shader - code * @param isFrag - Whether it is a fragment shader. - * */ + */ static convertTo300(shader: string, isFrag?: boolean) { shader = shader.replace(/\bvarying\b/g, isFrag ? "in" : "out"); shader = shader.replace(/\btexture(2D|Cube)\b/g, "texture"); @@ -124,7 +294,7 @@ export class ShaderFactory { if (isFrag) { shader = shader.replace(/\bgl_FragDepthEXT\b/g, "gl_FragDepth"); - if (!ShaderFactory._has300Output(shader)) { + if (!ShaderFactory._has300OutInFragReg.test(shader)) { const isMRT = /\bgl_FragData\[.+?\]/g.test(shader); if (isMRT) { shader = shader.replace(/\bgl_FragColor\b/g, "gl_FragData[0]"); @@ -142,8 +312,162 @@ export class ShaderFactory { return shader; } - private static _has300Output(fragmentShader: string): boolean { - return ShaderFactory._has300OutInFragReg.test(fragmentShader); + private static _scanInstanceUniforms(source: string, fieldMap: Record): string { + const builtinUniforms = ShaderFactory._builtinRendererUniforms; + return source.replace(ShaderFactory._uboUniformRegex, (match, type, name, arraySize) => { + if (type.includes("sampler")) return match; + const isDerived = builtinUniforms[name]; + if (isDerived === undefined && ShaderProperty._getShaderPropertyGroup(name) !== ShaderDataGroup.Renderer) + return match; + if (isDerived) return ""; + // Array uniforms not supported in instancing UBO, keep as regular uniform + if (arraySize) { + Logger.error(`GPU Instancing does not support array uniform "${name}${arraySize}"`); + return match; + } + // ModelMat is affine, store as mat3x4 (3 columns) to save 16 bytes per instance + fieldMap[ShaderProperty.getByName(name)._uniqueId] = + type === "mat4" && name === "renderer_ModelMat" ? "mat3x4" : type; + return ""; + }); + } + + // Matches preprocessor directives at line start. Only `#ifdef / #ifndef / #else / #endif` are + // supported; `#if` with expressions is not recognized (such blocks are treated as always-active). + private static readonly _ifdefRegex = /^[ \t]*#ifdef\s+(\w+)/; + private static readonly _ifndefRegex = /^[ \t]*#ifndef\s+(\w+)/; + private static readonly _elseRegex = /^[ \t]*#else\b/; + private static readonly _endifRegex = /^[ \t]*#endif\b/; + + /** + * Scan with preprocessor awareness, for raw GLSL paths where `#ifdef` blocks are not yet + * expanded. Uniforms inside inactive branches are skipped. + */ + private static _scanInstanceUniformsWithMacros( + source: string, + fieldMap: Record, + activeMacros: Set + ): string { + // Preprocessor branch stack: each frame tracks whether current branch is active + const branchStack: boolean[] = [true]; + const lines = source.split("\n"); + + for (let i = 0, n = lines.length; i < n; i++) { + const line = lines[i]; + + let m = line.match(ShaderFactory._ifdefRegex); + if (m) { + const parentActive = branchStack[branchStack.length - 1]; + branchStack.push(parentActive && activeMacros.has(m[1])); + continue; + } + m = line.match(ShaderFactory._ifndefRegex); + if (m) { + const parentActive = branchStack[branchStack.length - 1]; + branchStack.push(parentActive && !activeMacros.has(m[1])); + continue; + } + if (ShaderFactory._elseRegex.test(line)) { + const parentActive = branchStack.length >= 2 ? branchStack[branchStack.length - 2] : true; + const currentActive = branchStack[branchStack.length - 1]; + branchStack[branchStack.length - 1] = parentActive && !currentActive; + continue; + } + if (ShaderFactory._endifRegex.test(line)) { + if (branchStack.length > 1) branchStack.pop(); + continue; + } + if (!branchStack[branchStack.length - 1]) continue; + + lines[i] = ShaderFactory._scanInstanceUniforms(line, fieldMap); + } + + return lines.join("\n"); + } + + private static _buildLayout(engine: Engine, fieldMap: Record): InstanceBufferLayout { + const maxUBOSize = engine._hardwareRenderer.maxUniformBlockSize; + const std140Map = ShaderFactory._std140TypeInfoMap; + const instanceFields: InstanceFieldInfo[] = []; + let currentOffset = 0; + + const packFuncMap = ShaderFactory._packFuncMap; + const addField = (id: number): void => { + const type = fieldMap[id]; + const info = std140Map[type]; + if (!info) return; + currentOffset = Math.ceil(currentOffset / info.align) * info.align; + instanceFields.push({ + property: ShaderProperty._propertyIdMap[id], + type, + offset: currentOffset, + offsetInElements: currentOffset / 4, + useIntView: type[0] === "i" || type[0] === "u", + pack: packFuncMap[type] + }); + currentOffset += info.size; + }; + + // Priority fields first + const modelMatId = Renderer._worldMatrixProperty._uniqueId; + const layerId = Renderer._rendererLayerProperty._uniqueId; + if (modelMatId in fieldMap) { + addField(modelMatId); + delete fieldMap[modelMatId]; + } + if (layerId in fieldMap) { + addField(layerId); + delete fieldMap[layerId]; + } + + // Remaining fields sorted by id + const keys: number[] = []; + for (const k in fieldMap) keys.push(+k); + keys.sort((a, b) => a - b); + for (let i = 0; i < keys.length; i++) addField(keys[i]); + + const structSize = Math.ceil(currentOffset / 16) * 16; + const instanceMaxCount = Math.floor(maxUBOSize / structSize); + + return { instanceFields, instanceMaxCount, structSize }; + } + + private static _buildUBODeclaration(layout: InstanceBufferLayout): string { + const { instanceFields, instanceMaxCount } = layout; + const structLines: string[] = []; + for (let i = 0; i < instanceFields.length; i++) { + const { type, property } = instanceFields[i]; + structLines.push(` ${type} ${property.name};`); + } + return ( + `#define INSTANCE_MAX_COUNT ${instanceMaxCount}\n` + + `struct RendererInstanceStruct {\n${structLines.join("\n")}\n};\n` + + `layout(std140) uniform ${ShaderFactory.RENDERER_INSTANCE_BLOCK_NAME} {\n` + + ` RendererInstanceStruct rendererData[INSTANCE_MAX_COUNT];\n};\n` + ); + } + + private static _buildFieldDefines(fields: InstanceFieldInfo[], idExpr: string): string { + const accessor = `rendererData[${idExpr}]`; + const lines: string[] = []; + for (let i = 0; i < fields.length; i++) { + const { type, property } = fields[i]; + const n = property.name; + if (type === "mat3x4") { + // mat3x4 stores 3 transposed rows; reconstruct column-major mat4 with row3=(0,0,0,1) + const m = `${accessor}.${n}`; + lines.push( + `#define ${n} mat4(` + + `vec4(${m}[0].x,${m}[1].x,${m}[2].x,0.0),` + + `vec4(${m}[0].y,${m}[1].y,${m}[2].y,0.0),` + + `vec4(${m}[0].z,${m}[1].z,${m}[2].z,0.0),` + + `vec4(${m}[0].w,${m}[1].w,${m}[2].w,1.0))` + ); + } else { + lines.push(`#define ${n} ${accessor}.${n}`); + } + } + return lines.join("\n"); } private static _replaceMRTShader(shader: string, result: string[]): string { @@ -166,3 +490,24 @@ export class ShaderFactory { return shader; } } + +export interface InstanceFieldInfo { + property: ShaderProperty; + type: string; + offset: number; + /** offset / 4, precomputed to avoid repeated division in upload loop */ + offsetInElements: number; + useIntView: boolean; + pack: InstancePackFunc; +} + +/** + * @internal + */ +export interface InstanceBufferLayout { + instanceFields: InstanceFieldInfo[]; + instanceMaxCount: number; + structSize: number; +} + +type InstancePackFunc = (view: Float32Array | Int32Array, offset: number, value: any) => void; diff --git a/packages/core/src/shaderlib/extra/depthOnly.vs.glsl b/packages/core/src/shaderlib/extra/depthOnly.vs.glsl index 06ea2dc057..ecaea3e6ce 100644 --- a/packages/core/src/shaderlib/extra/depthOnly.vs.glsl +++ b/packages/core/src/shaderlib/extra/depthOnly.vs.glsl @@ -2,8 +2,6 @@ #include #include #include -uniform mat4 camera_VPMat; - void main() { diff --git a/packages/core/src/shaderlib/extra/shadow-map.vs.glsl b/packages/core/src/shaderlib/extra/shadow-map.vs.glsl index fae6a4b849..2da9dc1092 100644 --- a/packages/core/src/shaderlib/extra/shadow-map.vs.glsl +++ b/packages/core/src/shaderlib/extra/shadow-map.vs.glsl @@ -3,7 +3,6 @@ #include #include #include -uniform mat4 camera_VPMat; uniform vec2 scene_ShadowBias; // x: depth bias, y: normal bias uniform vec3 scene_LightDirection; @@ -26,7 +25,7 @@ void main() { #include #include #include - + vec4 positionWS = renderer_ModelMat * position; positionWS.xyz = applyShadowBias(positionWS.xyz); diff --git a/packages/core/src/shaderlib/extra/skybox.vs.glsl b/packages/core/src/shaderlib/extra/skybox.vs.glsl index 54c7185dd8..de9692f754 100644 --- a/packages/core/src/shaderlib/extra/skybox.vs.glsl +++ b/packages/core/src/shaderlib/extra/skybox.vs.glsl @@ -1,7 +1,5 @@ #include -uniform mat4 camera_VPMat; - varying vec3 v_cubeUV; uniform float material_Rotation; diff --git a/packages/core/src/shaderlib/extra/spine.fs.glsl b/packages/core/src/shaderlib/extra/spine.fs.glsl new file mode 100644 index 0000000000..4137749806 --- /dev/null +++ b/packages/core/src/shaderlib/extra/spine.fs.glsl @@ -0,0 +1,29 @@ +#include + +uniform sampler2D material_SpineTexture; +uniform float renderer_PremultipliedAlpha; + +varying vec2 v_uv; +varying vec4 v_lightColor; + +#ifdef RENDERER_TINT_BLACK + varying vec3 v_darkColor; +#endif + +void main() +{ + vec4 texColor = texture2D(material_SpineTexture, v_uv); + vec4 lightColor = sRGBToLinear(v_lightColor); + + #ifdef RENDERER_TINT_BLACK + vec4 darkColor = sRGBToLinear(vec4(v_darkColor, 1.0)); + vec3 dark_premult = (texColor.a - texColor.rgb) * darkColor.rgb; + vec3 dark_nonpremult = (1.0 - texColor.rgb) * darkColor.rgb; + vec3 dark = mix(dark_nonpremult, dark_premult, renderer_PremultipliedAlpha); + vec3 light = texColor.rgb * lightColor.rgb; + gl_FragColor.rgb = dark + light; + gl_FragColor.a = texColor.a * lightColor.a; + #else + gl_FragColor = texColor * lightColor; + #endif +} diff --git a/packages/core/src/shaderlib/extra/spine.vs.glsl b/packages/core/src/shaderlib/extra/spine.vs.glsl new file mode 100644 index 0000000000..ccac4ff172 --- /dev/null +++ b/packages/core/src/shaderlib/extra/spine.vs.glsl @@ -0,0 +1,28 @@ +uniform mat4 renderer_MVPMat; + +attribute vec3 POSITION; +attribute vec2 TEXCOORD_0; +attribute vec4 LIGHT_COLOR; + +#ifdef RENDERER_TINT_BLACK + attribute vec3 DARK_COLOR; +#endif + +varying vec2 v_uv; +varying vec4 v_lightColor; + +#ifdef RENDERER_TINT_BLACK + varying vec3 v_darkColor; +#endif + +void main() +{ + gl_Position = renderer_MVPMat * vec4(POSITION, 1.0); + + v_uv = TEXCOORD_0; + v_lightColor = LIGHT_COLOR; + + #ifdef RENDERER_TINT_BLACK + v_darkColor = DARK_COLOR; + #endif +} diff --git a/packages/core/src/shaderlib/extra/text.fs.glsl b/packages/core/src/shaderlib/extra/text.fs.glsl index 8fe1125d69..0ea05d15d2 100644 --- a/packages/core/src/shaderlib/extra/text.fs.glsl +++ b/packages/core/src/shaderlib/extra/text.fs.glsl @@ -1,15 +1,76 @@ uniform sampler2D renderElement_TextTexture; +uniform vec2 renderElement_TextTextureSize; +uniform vec4 renderer_OutlineColor; +uniform float renderer_OutlineWidth; + +uniform vec4 renderer_UIRectClipRect; +uniform float renderer_UIRectClipEnabled; +uniform vec4 renderer_UIRectClipSoftness; +uniform float renderer_UIRectClipHardClip; varying vec2 v_uv; varying vec4 v_color; +varying vec2 v_worldPosition; -void main() +float getUIRectClipAlpha() { - vec4 texColor = texture2D(renderElement_TextTexture, v_uv); + vec4 edgeDistance = vec4( + v_worldPosition.x - renderer_UIRectClipRect.x, + v_worldPosition.y - renderer_UIRectClipRect.y, + renderer_UIRectClipRect.z - v_worldPosition.x, + renderer_UIRectClipRect.w - v_worldPosition.y + ); + vec4 hardClipFactor = step(vec4(0.0), edgeDistance); + vec4 softness = max(renderer_UIRectClipSoftness, vec4(1e-5)); + vec4 softClipFactor = clamp(edgeDistance / softness, 0.0, 1.0); + vec4 useSoftness = step(vec4(1e-5), renderer_UIRectClipSoftness); + vec4 clipFactor = mix(hardClipFactor, softClipFactor, useSoftness); + return clipFactor.x * clipFactor.y * clipFactor.z * clipFactor.w; +} + +float sampleCoverage(vec2 uv) +{ + vec4 c = texture2D(renderElement_TextTexture, uv); #ifdef GRAPHICS_API_WEBGL2 - float coverage = texColor.r; + return c.r; #else - float coverage = texColor.a; + return c.a; #endif - gl_FragColor = vec4(v_color.rgb, v_color.a * coverage); +} + +void main() +{ + float rectClipAlpha = 1.0; + if (renderer_UIRectClipEnabled > 0.5) { + rectClipAlpha = getUIRectClipAlpha(); + } + + float coverage = sampleCoverage(v_uv); + vec4 finalColor; + + if (renderer_OutlineWidth > 0.0) { + vec2 texelSize = 1.0 / renderElement_TextTextureSize; + vec2 step = texelSize * renderer_OutlineWidth; + float outlineCoverage = coverage; + outlineCoverage = max(outlineCoverage, sampleCoverage(v_uv + vec2( step.x, 0.0))); + outlineCoverage = max(outlineCoverage, sampleCoverage(v_uv + vec2(-step.x, 0.0))); + outlineCoverage = max(outlineCoverage, sampleCoverage(v_uv + vec2( 0.0, step.y))); + outlineCoverage = max(outlineCoverage, sampleCoverage(v_uv + vec2( 0.0, -step.y))); + outlineCoverage = max(outlineCoverage, sampleCoverage(v_uv + vec2( step.x * 0.7071, step.y * 0.7071))); + outlineCoverage = max(outlineCoverage, sampleCoverage(v_uv + vec2(-step.x * 0.7071, step.y * 0.7071))); + outlineCoverage = max(outlineCoverage, sampleCoverage(v_uv + vec2( step.x * 0.7071, -step.y * 0.7071))); + outlineCoverage = max(outlineCoverage, sampleCoverage(v_uv + vec2(-step.x * 0.7071, -step.y * 0.7071))); + + vec3 rgb = mix(renderer_OutlineColor.rgb, v_color.rgb, coverage); + float alpha = max(coverage, outlineCoverage * renderer_OutlineColor.a) * v_color.a; + finalColor = vec4(rgb, alpha); + } else { + finalColor = vec4(v_color.rgb, v_color.a * coverage); + } + + finalColor.a *= rectClipAlpha; + if (renderer_UIRectClipEnabled > 0.5 && renderer_UIRectClipHardClip > 0.5 && finalColor.a < 0.001) { + discard; + } + gl_FragColor = finalColor; } diff --git a/packages/core/src/shaderlib/extra/text.vs.glsl b/packages/core/src/shaderlib/extra/text.vs.glsl index 37a6b2d333..c3971d0172 100644 --- a/packages/core/src/shaderlib/extra/text.vs.glsl +++ b/packages/core/src/shaderlib/extra/text.vs.glsl @@ -1,4 +1,5 @@ uniform mat4 renderer_MVPMat; +uniform mat4 renderer_ModelMat; attribute vec3 POSITION; attribute vec2 TEXCOORD_0; @@ -6,6 +7,7 @@ attribute vec4 COLOR_0; varying vec2 v_uv; varying vec4 v_color; +varying vec2 v_worldPosition; void main() { @@ -13,4 +15,5 @@ void main() v_uv = TEXCOORD_0; v_color = COLOR_0; + v_worldPosition = POSITION.xy; } diff --git a/packages/core/src/shaderlib/normal_vert.glsl b/packages/core/src/shaderlib/normal_vert.glsl index 096a4595d1..906be70ef7 100644 --- a/packages/core/src/shaderlib/normal_vert.glsl +++ b/packages/core/src/shaderlib/normal_vert.glsl @@ -1,9 +1,10 @@ #ifndef MATERIAL_OMIT_NORMAL #ifdef RENDERER_HAS_NORMAL - v_normal = normalize( mat3(renderer_NormalMat) * normal ); + mat3 normalMat = mat3(renderer_NormalMat); + v_normal = normalize( normalMat * normal ); #if defined(RENDERER_HAS_TANGENT) && ( defined(MATERIAL_HAS_NORMALTEXTURE) || defined(MATERIAL_HAS_CLEAR_COAT_NORMAL_TEXTURE) || defined(MATERIAL_ENABLE_ANISOTROPY) ) - vec3 tangentW = normalize( mat3(renderer_NormalMat) * tangent.xyz ); + vec3 tangentW = normalize( normalMat * tangent.xyz ); vec3 bitangentW = cross( v_normal, tangentW ) * tangent.w; v_TBN = mat3( tangentW, bitangentW, v_normal ); diff --git a/packages/core/src/shaderlib/particle/rotation_over_lifetime_module.glsl b/packages/core/src/shaderlib/particle/rotation_over_lifetime_module.glsl index 226848bba8..66e4740d54 100644 --- a/packages/core/src/shaderlib/particle/rotation_over_lifetime_module.glsl +++ b/packages/core/src/shaderlib/particle/rotation_over_lifetime_module.glsl @@ -77,7 +77,7 @@ vec3 computeParticleRotationVec3(in vec3 rotation, in float age, in float normal #else float ageRot = renderer_ROLMaxConst.z * age; #endif - rotation += ageRot; + rotation.z += ageRot; #endif #ifdef RENDERER_ROL_CURVE_MODE @@ -86,7 +86,7 @@ vec3 computeParticleRotationVec3(in vec3 rotation, in float age, in float normal #ifdef RENDERER_ROL_IS_RANDOM_TWO lifeRotation = mix(evaluateParticleCurveCumulative(renderer_ROLMinCurveZ, normalizedAge, currentValue), lifeRotation, a_Random0.w); #endif - rotation += lifeRotation * a_ShapePositionStartLifeTime.w; + rotation.z += lifeRotation * a_ShapePositionStartLifeTime.w; #endif #endif return rotation; diff --git a/packages/core/src/shaderlib/transform_declare.glsl b/packages/core/src/shaderlib/transform_declare.glsl index 00e1d5edba..86048dcbde 100644 --- a/packages/core/src/shaderlib/transform_declare.glsl +++ b/packages/core/src/shaderlib/transform_declare.glsl @@ -1,7 +1,8 @@ -uniform mat4 renderer_LocalMat; -uniform mat4 renderer_ModelMat; uniform mat4 camera_ViewMat; uniform mat4 camera_ProjMat; +uniform mat4 camera_VPMat; + +uniform mat4 renderer_ModelMat; uniform mat4 renderer_MVMat; uniform mat4 renderer_MVPMat; uniform mat4 renderer_NormalMat; \ No newline at end of file diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index b8d97f4365..6485d658ca 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -1,7 +1,6 @@ import { BoundingBox, Color, Vector2, Vector3, Vector4 } from "@galacean/engine-math"; import { Entity } from "../Entity"; import { RenderContext } from "../RenderPipeline/RenderContext"; -import { RenderElement } from "../RenderPipeline/RenderElement"; import { Renderer, RendererUpdateFlags } from "../Renderer"; import { deepClone, ignoreClone } from "../clone/CloneManager"; import { Buffer } from "../graphic/Buffer"; @@ -232,8 +231,9 @@ export class TrailRenderer extends Renderer { const { _firstActiveElement: firstActive, _firstFreeElement: firstFree } = this; - const renderElement = this._engine._renderElementPool.get(); - renderElement.set(this.priority, this._distanceForSort); + const priority = this.priority; + const distanceForSort = this._distanceForSort; + const renderPipeline = context.camera._renderPipeline; // spansBoundary: active points cross buffer end // wrapped: spansBoundary AND point 0 has been written (need bridge + second segment) @@ -241,13 +241,19 @@ export class TrailRenderer extends Renderer { const wrapped = spansBoundary && firstFree > 0; const mainCount = (spansBoundary ? this._currentPointCapacity - firstActive + (wrapped ? 1 : 0) : firstFree - firstActive) * 2; - this._addSubRenderElement(renderElement, material, this._mainSubPrimitive, firstActive * 2, mainCount); + this._addRenderElement( + context, + material, + this._mainSubPrimitive, + firstActive * 2, + mainCount, + priority, + distanceForSort + ); if (wrapped) { - this._addSubRenderElement(renderElement, material, this._wrapSubPrimitive, 0, firstFree * 2); + this._addRenderElement(context, material, this._wrapSubPrimitive, 0, firstFree * 2, priority, distanceForSort); } - - context.camera._renderPipeline.pushRenderElement(context, renderElement); } protected override _updateBounds(worldBounds: BoundingBox): void { @@ -588,18 +594,22 @@ export class TrailRenderer extends Renderer { this._bufferResized = false; } - private _addSubRenderElement( - renderElement: RenderElement, + private _addRenderElement( + context: RenderContext, material: Material, subPrimitive: SubPrimitive, start: number, - count: number + count: number, + priority: number, + distanceForSort: number ): void { subPrimitive.start = start; subPrimitive.count = count; - const subRenderElement = this._engine._subRenderElementPool.get(); - subRenderElement.set(this, material, this._primitive, subPrimitive); - renderElement.addSubRenderElement(subRenderElement); + const renderElement = this._engine._renderElementPool.get(); + renderElement.set(this, material, this._primitive, subPrimitive); + renderElement.priority = priority; + renderElement.distanceForSort = distanceForSort; + context.camera._renderPipeline.pushRenderElement(context, renderElement); } @ignoreClone diff --git a/packages/core/src/ui/IUICanvas.ts b/packages/core/src/ui/IUICanvas.ts index 3139b6dba8..e90e209aa2 100644 --- a/packages/core/src/ui/IUICanvas.ts +++ b/packages/core/src/ui/IUICanvas.ts @@ -10,7 +10,7 @@ export interface IUICanvas { entity: Entity; sortOrder: number; _canvasIndex: number; - _renderElement: RenderElement; + _renderElements: RenderElement[]; _canRender(camera: Camera): boolean; _prepareRender(renderContext: RenderContext): void; } diff --git a/packages/core/src/ui/UIUtils.ts b/packages/core/src/ui/UIUtils.ts index c57c8bdf1f..0e455a135c 100644 --- a/packages/core/src/ui/UIUtils.ts +++ b/packages/core/src/ui/UIUtils.ts @@ -1,14 +1,21 @@ -import { Matrix, Vector4 } from "@galacean/engine-math"; +import { Color, Matrix, Vector4 } from "@galacean/engine-math"; import { Camera } from "../Camera"; import { Engine } from "../Engine"; import { Layer } from "../Layer"; +import { Blitter } from "../RenderPipeline/Blitter"; import { RenderQueue } from "../RenderPipeline"; import { ContextRendererUpdateFlag } from "../RenderPipeline/RenderContext"; import { Scene } from "../Scene"; import { VirtualCamera } from "../VirtualCamera"; import { EngineObject } from "../base"; -import { RenderQueueType, ShaderData, ShaderDataGroup, ShaderMacro } from "../shader"; +import { CameraClearFlags } from "../enums/CameraClearFlags"; +import { Material } from "../material"; +import { RenderQueueType, Shader, ShaderData, ShaderDataGroup, ShaderMacro } from "../shader"; +import { BlendFactor } from "../shader/enums/BlendFactor"; import { ShaderMacroCollection } from "../shader/ShaderMacroCollection"; +import { RenderTarget } from "../texture/RenderTarget"; +import { Texture2D } from "../texture/Texture2D"; +import { TextureFormat } from "../texture/enums/TextureFormat"; import { DisorderedArray } from "../utils/DisorderedArray"; import { IUICanvas } from "./IUICanvas"; @@ -19,6 +26,11 @@ export class UIUtils { private static _virtualCamera: VirtualCamera; private static _viewport: Vector4; private static _overlayCamera: OverlayCamera; + private static _overlayRT: RenderTarget; + private static _overlayBlitMaterial: Material; + private static _clearColor = new Color(0, 0, 0, 0); + /** Flip V so that Y-up RT content maps correctly onto the default framebuffer. */ + private static _flipYScaleOffset = new Vector4(1, -1, 0, 1); static renderOverlay(engine: Engine, scene: Scene, uiCanvases: DisorderedArray): void { engine._macroCollection.enable(UIUtils._shouldSRGBCorrect); @@ -31,10 +43,19 @@ export class UIUtils { camera.engine = engine; camera.scene = scene; renderContext.camera = camera as unknown as Camera; + + const { width, height } = canvas; const { elements: projectE } = virtualCamera.projectionMatrix; const { elements: viewE } = virtualCamera.viewMatrix; - (projectE[0] = 2 / canvas.width), (projectE[5] = 2 / canvas.height), (projectE[10] = 0); - renderContext.setRenderTarget(null, viewport, 0); + (projectE[0] = 2 / width), (projectE[5] = 2 / height), (projectE[10] = 0); + + // Render to an intermediate RT with Depth24Stencil8 so that stencil-based UI Mask works. + // The default canvas framebuffer is created without a stencil buffer + // (see WebGLGraphicDevice._webGLOptions.stencil = false). + const overlayRT = UIUtils._getOverlayRT(engine, width, height); + renderContext.setRenderTarget(overlayRT, viewport, 0); + rhi.clearRenderTarget(engine, CameraClearFlags.All, UIUtils._clearColor); + for (let i = 0, n = uiCanvases.length; i < n; i++) { const uiCanvas = uiCanvases.get(i); if (uiCanvas) { @@ -44,7 +65,10 @@ export class UIUtils { renderContext.applyVirtualCamera(virtualCamera, false); uiRenderQueue.rendererUpdateFlag |= ContextRendererUpdateFlag.ProjectionMatrix; uiCanvas._prepareRender(renderContext); - uiRenderQueue.pushRenderElement(uiCanvas._renderElement); + const curElements = uiCanvas._renderElements; + for (let j = 0, m = curElements.length; j < m; j++) { + uiRenderQueue.pushRenderElement(curElements[j]); + } uiRenderQueue.batch(batcherManager); batcherManager.uploadBuffer(); uiRenderQueue.render(renderContext, "Forward"); @@ -52,9 +76,60 @@ export class UIUtils { engine._renderCount++; } } + + // Blit overlay RT to default framebuffer with premultiplied alpha blending. + // Blitter.blitTexture picks the non-flipping `blitMesh` when destination is null, + // but the RT contents are written in standard Y-up NDC, so we flip V here via + // sourceScaleOffset to match the default framebuffer orientation. + Blitter.blitTexture( + engine, + overlayRT.getColorTexture(0) as Texture2D, + null, + 0, + viewport, + UIUtils._getOverlayBlitMaterial(engine), + 0, + UIUtils._flipYScaleOffset + ); + renderContext.camera = null; engine._macroCollection.disable(UIUtils._shouldSRGBCorrect); } + + private static _getOverlayRT(engine: Engine, width: number, height: number): RenderTarget { + let rt = UIUtils._overlayRT; + if (!rt || rt.width !== width || rt.height !== height) { + if (rt) { + rt.getColorTexture(0).destroy(); + rt.destroy(); + } + const colorTexture = new Texture2D(engine, width, height, TextureFormat.R8G8B8A8, false); + colorTexture.isGCIgnored = true; + rt = new RenderTarget(engine, width, height, colorTexture, TextureFormat.Depth24Stencil8); + rt.isGCIgnored = true; + UIUtils._overlayRT = rt; + } + return rt; + } + + private static _getOverlayBlitMaterial(engine: Engine): Material { + let material = UIUtils._overlayBlitMaterial; + if (!material) { + material = new Material(engine, Shader.find("blit")); + material.isGCIgnored = true; + const renderState = material.renderState; + renderState.depthState.enabled = false; + renderState.depthState.writeEnabled = false; + const target = renderState.blendState.targetBlendState; + target.enabled = true; + target.sourceColorBlendFactor = BlendFactor.One; + target.destinationColorBlendFactor = BlendFactor.OneMinusSourceAlpha; + target.sourceAlphaBlendFactor = BlendFactor.One; + target.destinationAlphaBlendFactor = BlendFactor.OneMinusSourceAlpha; + UIUtils._overlayBlitMaterial = material; + } + return material; + } } class OverlayCamera { diff --git a/packages/design/package.json b/packages/design/package.json index 86e73a7c33..e0a42fc2c0 100644 --- a/packages/design/package.json +++ b/packages/design/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-design", - "version": "2.0.0-alpha.24", + "version": "0.0.0-experimental-2.0-game.19", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/design/src/physics/IPhysics.ts b/packages/design/src/physics/IPhysics.ts index 17c9b6b466..91a6c1c7c6 100644 --- a/packages/design/src/physics/IPhysics.ts +++ b/packages/design/src/physics/IPhysics.ts @@ -36,6 +36,16 @@ export interface IPhysics { */ createPhysicsScene(physicsManager: IPhysicsManager): IPhysicsScene; + /** + * Get the default contact offset for collider shapes. + */ + getDefaultContactOffset?(): number; + + /** + * Get the default sleep threshold for dynamic colliders. + */ + getDefaultSleepThreshold?(): number; + /** * Create dynamic collider. * @param position - The global position diff --git a/packages/design/src/physics/IPhysicsScene.ts b/packages/design/src/physics/IPhysicsScene.ts index 5520114104..e5b8d9f335 100644 --- a/packages/design/src/physics/IPhysicsScene.ts +++ b/packages/design/src/physics/IPhysicsScene.ts @@ -43,6 +43,12 @@ export interface IPhysicsScene { */ update(elapsedTime: number): void; + /** + * Enable contact event buffering. + * @param enabled - Whether collision contact events should be buffered for dispatch. + */ + setContactEventEnabled(enabled: boolean): void; + /** * Collect buffered collision and trigger events. * Must be called after update() and after syncing transforms back from physics. diff --git a/packages/design/src/renderingHardwareInterface/IHardwareRenderer.ts b/packages/design/src/renderingHardwareInterface/IHardwareRenderer.ts index bccb92bf65..193ef7a558 100644 --- a/packages/design/src/renderingHardwareInterface/IHardwareRenderer.ts +++ b/packages/design/src/renderingHardwareInterface/IHardwareRenderer.ts @@ -2,6 +2,8 @@ * Hardware graphics API renderer. */ export interface IHardwareRenderer { + readonly maxUniformBlockSize: number; + // todo: implements [key: string]: any; } diff --git a/packages/galacean/package.json b/packages/galacean/package.json index a2bf568984..9e4d0479cd 100644 --- a/packages/galacean/package.json +++ b/packages/galacean/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine", - "version": "2.0.0-alpha.24", + "version": "0.0.0-experimental-2.0-game.19", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/loader/package.json b/packages/loader/package.json index 61e5825838..3e42b89824 100644 --- a/packages/loader/package.json +++ b/packages/loader/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-loader", - "version": "2.0.0-alpha.24", + "version": "0.0.0-experimental-2.0-game.19", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/loader/src/AudioLoader.ts b/packages/loader/src/AudioLoader.ts index 24c2b788c9..29cd7d40b7 100644 --- a/packages/loader/src/AudioLoader.ts +++ b/packages/loader/src/AudioLoader.ts @@ -9,7 +9,7 @@ import { ResourceManager, resourceLoader } from "@galacean/engine-core"; -@resourceLoader(AssetType.Audio, ["mp3", "ogg", "wav"]) +@resourceLoader(AssetType.Audio, ["mp3", "ogg", "wav", "audio", "m4a", "aac", "flac"]) class AudioLoader extends Loader { load(item: LoadItem, resourceManager: ResourceManager): AssetPromise { return new AssetPromise((resolve, reject) => { diff --git a/packages/loader/src/SceneLoader.ts b/packages/loader/src/SceneLoader.ts index 069b2db6e2..b16fb2cc92 100644 --- a/packages/loader/src/SceneLoader.ts +++ b/packages/loader/src/SceneLoader.ts @@ -128,6 +128,15 @@ class SceneLoader extends Loader { if (fog.fogColor != undefined) scene.fogColor.copyFrom(fog.fogColor); } + // parse physics + const physics = data.scene.physics; + // PhysicsScene has a native backing only when the engine was created with a physics backend. + // Keep scene files loadable for render-only engines by ignoring serialized physics settings there. + if (physics && (engine as any)._physicsInitialized) { + if (physics.gravity != undefined) scene.physics.gravity.copyFrom(physics.gravity); + if (physics.fixedTimeStep != undefined) scene.physics.fixedTimeStep = physics.fixedTimeStep; + } + // Post Process const postProcessData = data.scene.postProcess; if (postProcessData) { diff --git a/packages/loader/src/ShaderLoader.ts b/packages/loader/src/ShaderLoader.ts index 3380ff297e..d7e714af50 100644 --- a/packages/loader/src/ShaderLoader.ts +++ b/packages/loader/src/ShaderLoader.ts @@ -9,7 +9,7 @@ import { } from "@galacean/engine-core"; import { ShaderChunkLoader } from "./ShaderChunkLoader"; -@resourceLoader(AssetType.Shader, ["gs", "gsl"]) +@resourceLoader(AssetType.Shader, ["shader"]) class ShaderLoader extends Loader { private static _builtinRegex = /^\s*\/\/\s*@builtin\s+(\w+)/; diff --git a/packages/loader/src/SpriteAtlasLoader.ts b/packages/loader/src/SpriteAtlasLoader.ts index 0bada4b334..c5a1d6a550 100644 --- a/packages/loader/src/SpriteAtlasLoader.ts +++ b/packages/loader/src/SpriteAtlasLoader.ts @@ -102,7 +102,7 @@ class SpriteAtlasLoader extends Loader { const { x: offsetLeft, y: offsetTop, z: offsetRight, w: offsetBottom } = atlasRegionOffset; sprite.atlasRegionOffset.set(offsetLeft * invW, offsetTop * invH, offsetRight * invW, offsetBottom * invH); } - config.atlasRotated && (sprite.atlasRotated = true); + sprite.atlasRotated = config.atlasRotated ?? false; } width === undefined || (sprite.width = width); height === undefined || (sprite.height = height); diff --git a/packages/loader/src/gltf/parser/GLTFParserContext.ts b/packages/loader/src/gltf/parser/GLTFParserContext.ts index 83f9f65124..b549da5ca2 100644 --- a/packages/loader/src/gltf/parser/GLTFParserContext.ts +++ b/packages/loader/src/gltf/parser/GLTFParserContext.ts @@ -116,13 +116,13 @@ export class GLTFParserContext { return AssetPromise.all([ this.get(GLTFParserType.Validator), + this.get(GLTFParserType.Scene), this.get(GLTFParserType.Texture), this.get(GLTFParserType.Material), this.get(GLTFParserType.Mesh), this.get(GLTFParserType.Skin), this.get(GLTFParserType.Animation), - this.get(GLTFParserType.AnimatorController), - this.get(GLTFParserType.Scene) + this.get(GLTFParserType.AnimatorController) ]).then(() => { const glTFResource = this.glTFResource; const animatorController = glTFResource.animatorController; diff --git a/packages/loader/src/gltf/parser/GLTFSceneParser.ts b/packages/loader/src/gltf/parser/GLTFSceneParser.ts index 6517356c97..553bf177f2 100644 --- a/packages/loader/src/gltf/parser/GLTFSceneParser.ts +++ b/packages/loader/src/gltf/parser/GLTFSceneParser.ts @@ -31,18 +31,14 @@ export class GLTFSceneParser extends GLTFParser { const sceneNodes = sceneInfo.nodes || []; let sceneRoot: Entity; - if (sceneNodes.length === 1) { - sceneRoot = context.get(GLTFParserType.Entity, sceneNodes[0]); - } else { - sceneRoot = new Entity(engine, "GLTF_ROOT"); - // @ts-ignore - sceneRoot._markAsTemplate(glTFResource); - for (let i = 0; i < sceneNodes.length; i++) { - const childEntity = context.get(GLTFParserType.Entity, sceneNodes[i]); - sceneRoot.addChild(childEntity); - } + sceneRoot = new Entity(engine, "GLTF_ROOT"); + // @ts-ignore + sceneRoot._markAsTemplate(glTFResource); + for (let i = 0; i < sceneNodes.length; i++) { + sceneRoot.addChild(context.get(GLTFParserType.Entity, sceneNodes[i])); } + (glTFResource._sceneRoots ||= [])[index] = sceneRoot; if (isDefaultScene) { glTFResource._defaultSceneRoot = sceneRoot; } @@ -200,49 +196,9 @@ export class GLTFSceneParser extends GLTFParser { if (rootBoneIndex !== -1) { BoundingBox.transform(mesh.bounds, inverseBindMatrices[rootBoneIndex], skinnedMeshRenderer.localBounds); } else { - // Root bone is not in joints list, we can only compute approximate inverse bind matrix - // Average all root bone's children inverse bind matrix - const approximateBindMatrix = new Matrix(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); - let subRootBoneCount = this._computeApproximateBindMatrix( - bones, - inverseBindMatrices, - rootBone, - approximateBindMatrix - ); - - if (subRootBoneCount !== 0) { - Matrix.multiplyScalar(approximateBindMatrix, 1.0 / subRootBoneCount, approximateBindMatrix); - BoundingBox.transform(mesh.bounds, approximateBindMatrix, skinnedMeshRenderer.localBounds); - } else { - skinnedMeshRenderer.localBounds.copyFrom(mesh.bounds); - } + const inverseRootBoneWorld = new Matrix(); + Matrix.invert(rootBone.transform.worldMatrix, inverseRootBoneWorld); + BoundingBox.transform(mesh.bounds, inverseRootBoneWorld, skinnedMeshRenderer.localBounds); } } - - private _computeApproximateBindMatrix( - jointEntities: ReadonlyArray, - inverseBindMatrices: Matrix[], - rootEntity: Entity, - approximateBindMatrix: Matrix - ): number { - let subRootBoneCount = 0; - const children = rootEntity.children; - for (let i = 0, n = children.length; i < n; i++) { - const rootChild = children[i]; - const index = jointEntities.indexOf(rootChild); - if (index !== -1) { - Matrix.add(approximateBindMatrix, inverseBindMatrices[index], approximateBindMatrix); - subRootBoneCount++; - } else { - subRootBoneCount += this._computeApproximateBindMatrix( - jointEntities, - inverseBindMatrices, - rootChild, - approximateBindMatrix - ); - } - } - - return subRootBoneCount; - } } diff --git a/packages/loader/src/gltf/parser/GLTFSkinParser.test.ts b/packages/loader/src/gltf/parser/GLTFSkinParser.test.ts new file mode 100644 index 0000000000..bb5714b847 --- /dev/null +++ b/packages/loader/src/gltf/parser/GLTFSkinParser.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; + +describe("GLTFSkinParser rootBone resolution", () => { + async function createParser(): Promise { + (globalThis as any).window = { AudioContext: undefined, TextMetrics: undefined }; + const { GLTFSkinParser } = await import("./GLTFSkinParser"); + return new GLTFSkinParser(); + } + + it("includes skinned mesh nodes when resolving missing skin.skeleton", async () => { + const parser = await createParser(); + const sceneRoot = { name: "GLTF_ROOT" }; + const meshRoot = { name: "Character_Man", parent: sceneRoot }; + const hips = { name: "mixamorig:Hips", parent: sceneRoot }; + const spine = { name: "mixamorig:Spine", parent: hips }; + + const rootBone = (parser as any)._findSkinRootBoneByLCA( + 0, + [1, 2], + [meshRoot, hips, spine], + [{ name: "Character_Man", skin: 0 }, { name: "mixamorig:Hips" }, { name: "mixamorig:Spine" }] + ); + + expect(rootBone).toBe(sceneRoot); + }); + + it("does not promote to the scene wrapper for unrelated top-level siblings", async () => { + const parser = await createParser(); + const sceneRoot = { name: "GLTF_ROOT" }; + const characterRoot = { name: "Character_Root", parent: sceneRoot }; + const mesh = { name: "Character_Mesh", parent: characterRoot }; + const hips = { name: "mixamorig:Hips", parent: characterRoot }; + const light = { name: "Light", parent: sceneRoot }; + + const rootBone = (parser as any)._findSkinRootBoneByLCA( + 0, + [3], + [characterRoot, mesh, light, hips], + [{ name: "Character_Root" }, { name: "Character_Mesh", skin: 0 }, { name: "Light" }, { name: "mixamorig:Hips" }] + ); + + expect(rootBone).toBe(characterRoot); + }); +}); diff --git a/packages/loader/src/gltf/parser/GLTFSkinParser.ts b/packages/loader/src/gltf/parser/GLTFSkinParser.ts index 7c1580e4ca..3f11f487e5 100644 --- a/packages/loader/src/gltf/parser/GLTFSkinParser.ts +++ b/packages/loader/src/gltf/parser/GLTFSkinParser.ts @@ -39,7 +39,7 @@ export class GLTFSkinParser extends GLTFParser { const rootBone = entities[skeleton]; skin.rootBone = rootBone; } else { - const rootBone = this._findSkeletonRootBone(joints, entities); + const rootBone = this._findSkinRootBoneByLCA(index, joints, entities, glTF.nodes); if (rootBone) { skin.rootBone = rootBone; } else { @@ -53,28 +53,50 @@ export class GLTFSkinParser extends GLTFParser { return AssetPromise.resolve(skinPromise); } - private _findSkeletonRootBone(joints: number[], entities: Entity[]): Entity { - const paths = >{}; - for (const index of joints) { + private _findSkinRootBoneByLCA( + skinIndex: number, + joints: number[], + entities: Entity[], + nodes: Array<{ skin?: number }> = [] + ): Entity | null { + const nodeIndices = joints.slice(); + for (let i = 0, n = nodes.length; i < n; i++) { + if (nodes[i]?.skin === skinIndex) { + nodeIndices.push(i); + } + } + + return this._findRootBoneByLCA(nodeIndices, entities); + } + + private _findRootBoneByLCA(nodeIndices: number[], entities: Entity[]): Entity | null { + const paths: Entity[][] = []; + for (const index of nodeIndices) { const path = new Array(); let entity = entities[index]; while (entity) { path.unshift(entity); entity = entity.parent; } - paths[index] = path; + if (path.length) { + paths.push(path); + } + } + + if (!paths.length) { + return null; } - let rootNode = null; + let rootNode: Entity | null = null; for (let i = 0; ; i++) { - let path = paths[joints[0]]; + let path = paths[0]; if (i >= path.length) { return rootNode; } const entity = path[i]; - for (let j = 1, m = joints.length; j < m; j++) { - path = paths[joints[j]]; + for (let j = 1, m = paths.length; j < m; j++) { + path = paths[j]; if (i >= path.length || entity !== path[i]) { return rootNode; } diff --git a/packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts b/packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts index e79ea06864..fe92bbcf6a 100644 --- a/packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts +++ b/packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts @@ -125,7 +125,14 @@ export class ReflectionParser { if (!entity) return Promise.resolve(null); const type = Loader.getClass(value.componentType); if (!type) return Promise.resolve(null); - return Promise.resolve(entity.getComponents(type, [])[value.componentIndex] ?? null); + // Try direct components first, fallback to children search (for GLB clone entities + // where the component lives on a child entity inside the clone) + const direct = entity.getComponents(type, []); + const result = direct[value.componentIndex]; + if (result) return Promise.resolve(result); + const includeChildren: any[] = []; + entity.getComponentsIncludeChildren(type, includeChildren); + return Promise.resolve(includeChildren[value.componentIndex] ?? null); } else if (ReflectionParser._isEntityRef(value)) { return Promise.resolve(this._resolveEntityByPath(value.entityPath)); } else if (ReflectionParser._isSignalRef(value)) { diff --git a/packages/loader/src/resource-deserialize/resources/schema/SceneSchema.ts b/packages/loader/src/resource-deserialize/resources/schema/SceneSchema.ts index 81aaff21d6..1f64dcb87c 100644 --- a/packages/loader/src/resource-deserialize/resources/schema/SceneSchema.ts +++ b/packages/loader/src/resource-deserialize/resources/schema/SceneSchema.ts @@ -54,6 +54,10 @@ export interface IScene extends IHierarchyFile { fogDensity: number; fogColor: IColor; }; + physics?: { + gravity?: IVector3; + fixedTimeStep?: number; + }; postProcess?: { isActive: boolean; bloom: { diff --git a/packages/math/package.json b/packages/math/package.json index 42a52983a1..8839a44ce2 100644 --- a/packages/math/package.json +++ b/packages/math/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-math", - "version": "2.0.0-alpha.24", + "version": "0.0.0-experimental-2.0-game.19", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/physics-lite/package.json b/packages/physics-lite/package.json index 30c4065480..5a48498a49 100644 --- a/packages/physics-lite/package.json +++ b/packages/physics-lite/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-physics-lite", - "version": "2.0.0-alpha.24", + "version": "0.0.0-experimental-2.0-game.19", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/physics-lite/src/LitePhysicsMaterial.ts b/packages/physics-lite/src/LitePhysicsMaterial.ts index 7983beb394..de1a0dee53 100644 --- a/packages/physics-lite/src/LitePhysicsMaterial.ts +++ b/packages/physics-lite/src/LitePhysicsMaterial.ts @@ -2,8 +2,16 @@ import { IPhysicsMaterial } from "@galacean/engine-design"; /** * Physics material describes how to handle colliding objects (friction, bounciness). + * + * Physics-lite does not implement material effects; setters are no-ops (matching + * the convention used by `LiteColliderShape.setMaterial/setContactOffset/setIsTrigger`). + * This lets engine-side state changes (including clone `_syncNative` re-writes) flow + * through without crashing while still leaving a one-time hint via console.log on + * the first write so users notice the limitation. */ export class LitePhysicsMaterial implements IPhysicsMaterial { + private static _warned = false; + constructor( staticFriction: number, dynamicFriction: number, @@ -16,39 +24,45 @@ export class LitePhysicsMaterial implements IPhysicsMaterial { * {@inheritDoc IPhysicsMaterial.setBounciness } */ setBounciness(value: number): void { - throw "Physics-lite don't support physics material. Use Physics-PhysX instead!"; + LitePhysicsMaterial._warnOnce(); } /** * {@inheritDoc IPhysicsMaterial.setDynamicFriction } */ setDynamicFriction(value: number): void { - throw "Physics-lite don't support physics material. Use Physics-PhysX instead!"; + LitePhysicsMaterial._warnOnce(); } /** * {@inheritDoc IPhysicsMaterial.setStaticFriction } */ setStaticFriction(value: number): void { - throw "Physics-lite don't support physics material. Use Physics-PhysX instead!"; + LitePhysicsMaterial._warnOnce(); } /** * {@inheritDoc IPhysicsMaterial.setBounceCombine } */ setBounceCombine(value: number): void { - throw "Physics-lite don't support physics material. Use Physics-PhysX instead!"; + LitePhysicsMaterial._warnOnce(); } /** * {@inheritDoc IPhysicsMaterial.setFrictionCombine } */ setFrictionCombine(value: number): void { - throw "Physics-lite don't support physics material. Use Physics-PhysX instead!"; + LitePhysicsMaterial._warnOnce(); } /** * {@inheritDoc IPhysicsMaterial.destroy } */ destroy(): void {} + + private static _warnOnce(): void { + if (LitePhysicsMaterial._warned) return; + LitePhysicsMaterial._warned = true; + console.log("Physics-lite don't support physics material. Use Physics-PhysX instead!"); + } } diff --git a/packages/physics-lite/src/LitePhysicsScene.ts b/packages/physics-lite/src/LitePhysicsScene.ts index 0b6b6eb3e8..58742b5069 100644 --- a/packages/physics-lite/src/LitePhysicsScene.ts +++ b/packages/physics-lite/src/LitePhysicsScene.ts @@ -115,6 +115,13 @@ export class LitePhysicsScene implements IPhysicsScene { } } + /** + * {@inheritDoc IPhysicsScene.setContactEventEnabled } + */ + setContactEventEnabled(_enabled: boolean): void { + // Physics-lite only produces trigger events, so there is no contact buffer to toggle. + } + /** * {@inheritDoc IPhysicsScene.updateEvents } */ diff --git a/packages/physics-physx/libs/physx.release.js b/packages/physics-physx/libs/physx.release.js index 426e4ca185..f816e9dce3 100644 --- a/packages/physics-physx/libs/physx.release.js +++ b/packages/physics-physx/libs/physx.release.js @@ -1,2 +1,2 @@ -var PHYSX=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};if(typeof __filename!="undefined"){_scriptName=__filename}else if(ENVIRONMENT_IS_WORKER){_scriptName=self.location.href}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{if(isFileURI(url)){return new Promise((resolve,reject)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){resolve(xhr.response);return}reject(xhr.status)};xhr.onerror=reject;xhr.send(null)})}var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var EXITSTATUS;var isFileURI=filename=>filename.startsWith("file://");var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["P"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){return "https://mdn.alipayobjects.com/rms/afts/file/A*RkDQSaUOhxsAAAAAgCAAAAgAehQnAQ/physx.release.wasm"}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!isFileURI(binaryFile)&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var __abort_js=()=>abort("");var createNamedFunction=(name,func)=>Object.defineProperty(func,"name",{value:name});var emval_freelist=[];var emval_handles=[0,1,,1,null,1,true,1,false,1];var BindingError=class BindingError extends Error{constructor(message){super(message);this.name="BindingError"}};var throwBindingError=message=>{throw new BindingError(message)};var Emval={toValue:handle=>{if(!handle){throwBindingError(`Cannot use deleted val. handle = ${handle}`)}return emval_handles[handle]},toHandle:value=>{switch(value){case undefined:return 2;case null:return 4;case true:return 6;case false:return 8;default:{const handle=emval_freelist.pop()||emval_handles.length;emval_handles[handle]=value;emval_handles[handle+1]=1;return handle}}}};class PureVirtualError extends Error{}var AsciiToString=ptr=>{var str="";while(1){var ch=HEAPU8[ptr++];if(!ch)return str;str+=String.fromCharCode(ch)}};var registeredInstances={};var getBasestPointer=(class_,ptr)=>{if(ptr===undefined){throwBindingError("ptr should not be undefined")}while(class_.baseClass){ptr=class_.upcast(ptr);class_=class_.baseClass}return ptr};var registerInheritedInstance=(class_,ptr,instance)=>{ptr=getBasestPointer(class_,ptr);if(registeredInstances.hasOwnProperty(ptr)){throwBindingError(`Tried to register registered instance: ${ptr}`)}else{registeredInstances[ptr]=instance}};var registeredTypes={};var getTypeName=type=>{var ptr=___getTypeName(type);var rv=AsciiToString(ptr);_free(ptr);return rv};var requireRegisteredType=(rawType,humanName)=>{var impl=registeredTypes[rawType];if(undefined===impl){throwBindingError(`${humanName} has unknown type ${getTypeName(rawType)}`)}return impl};var unregisterInheritedInstance=(class_,ptr)=>{ptr=getBasestPointer(class_,ptr);if(registeredInstances.hasOwnProperty(ptr)){delete registeredInstances[ptr]}else{throwBindingError(`Tried to unregister unregistered instance: ${ptr}`)}};var detachFinalizer=handle=>{};var finalizationRegistry=false;var runDestructor=$$=>{if($$.smartPtr){$$.smartPtrType.rawDestructor($$.smartPtr)}else{$$.ptrType.registeredClass.rawDestructor($$.ptr)}};var releaseClassHandle=$$=>{$$.count.value-=1;var toDelete=0===$$.count.value;if(toDelete){runDestructor($$)}};var attachFinalizer=handle=>{if(!globalThis.FinalizationRegistry){attachFinalizer=handle=>handle;return handle}finalizationRegistry=new FinalizationRegistry(info=>{releaseClassHandle(info.$$)});attachFinalizer=handle=>{var $$=handle.$$;var hasSmartPtr=!!$$.smartPtr;if(hasSmartPtr){var info={$$};finalizationRegistry.register(handle,info,handle)}return handle};detachFinalizer=handle=>finalizationRegistry.unregister(handle);return attachFinalizer(handle)};var __embind_create_inheriting_constructor=(constructorName,wrapperType,properties)=>{constructorName=AsciiToString(constructorName);wrapperType=requireRegisteredType(wrapperType,"wrapper");properties=Emval.toValue(properties);var registeredClass=wrapperType.registeredClass;var wrapperPrototype=registeredClass.instancePrototype;var baseClass=registeredClass.baseClass;var baseClassPrototype=baseClass.instancePrototype;var baseConstructor=registeredClass.baseClass.constructor;var ctor=createNamedFunction(constructorName,function(...args){for(var name of registeredClass.baseClass.pureVirtualFunctions){if(this[name]===baseClassPrototype[name]){throw new PureVirtualError(`Pure virtual function ${name} must be implemented in JavaScript`)}}Object.defineProperty(this,"__parent",{value:wrapperPrototype});this["__construct"](...args)});wrapperPrototype["__construct"]=function __construct(...args){if(this===wrapperPrototype){throwBindingError("Pass correct 'this' to __construct")}var inner=baseConstructor["implement"](this,...args);detachFinalizer(inner);var $$=inner.$$;inner["notifyOnDestruction"]();$$.preservePointerOnDelete=true;Object.defineProperties(this,{$$:{value:$$}});attachFinalizer(this);registerInheritedInstance(registeredClass,$$.ptr,this)};wrapperPrototype["__destruct"]=function __destruct(){if(this===wrapperPrototype){throwBindingError("Pass correct 'this' to __destruct")}detachFinalizer(this);unregisterInheritedInstance(registeredClass,this.$$.ptr)};ctor.prototype=Object.create(wrapperPrototype);Object.assign(ctor.prototype,properties);return Emval.toHandle(ctor)};var structRegistrations={};var runDestructors=destructors=>{while(destructors.length){var ptr=destructors.pop();var del=destructors.pop();del(ptr)}};function readPointer(pointer){return this.fromWireType(HEAPU32[pointer>>2])}var awaitingDependencies={};var typeDependencies={};var InternalError=class InternalError extends Error{constructor(message){super(message);this.name="InternalError"}};var throwInternalError=message=>{throw new InternalError(message)};var whenDependentTypesAreResolved=(myTypes,dependentTypes,getTypeConverters)=>{myTypes.forEach(type=>typeDependencies[type]=dependentTypes);function onComplete(typeConverters){var myTypeConverters=getTypeConverters(typeConverters);if(myTypeConverters.length!==myTypes.length){throwInternalError("Mismatched type converter count")}for(var i=0;i{typeConverters[i]=registeredTypes[dt];++registered;if(registered===unregisteredTypes.length){onComplete(typeConverters)}})}}if(0===unregisteredTypes.length){onComplete(typeConverters)}};var __embind_finalize_value_object=structType=>{var reg=structRegistrations[structType];delete structRegistrations[structType];var rawConstructor=reg.rawConstructor;var rawDestructor=reg.rawDestructor;var fieldRecords=reg.fields;var fieldTypes=fieldRecords.map(field=>field.getterReturnType).concat(fieldRecords.map(field=>field.setterArgumentType));whenDependentTypesAreResolved([structType],fieldTypes,fieldTypes=>{var fields={};for(var[i,field]of fieldRecords.entries()){const getterReturnType=fieldTypes[i];const getter=field.getter;const getterContext=field.getterContext;const setterArgumentType=fieldTypes[i+fieldRecords.length];const setter=field.setter;const setterContext=field.setterContext;fields[field.fieldName]={read:ptr=>getterReturnType.fromWireType(getter(getterContext,ptr)),write:(ptr,o)=>{var destructors=[];setter(setterContext,ptr,setterArgumentType.toWireType(destructors,o));runDestructors(destructors)},optional:getterReturnType.optional}}return[{name:reg.name,fromWireType:ptr=>{var rv={};for(var i in fields){rv[i]=fields[i].read(ptr)}rawDestructor(ptr);return rv},toWireType:(destructors,o)=>{for(var fieldName in fields){if(!(fieldName in o)&&!fields[fieldName].optional){throw new TypeError(`Missing field: "${fieldName}"`)}}var ptr=rawConstructor();for(fieldName in fields){fields[fieldName].write(ptr,o[fieldName])}if(destructors!==null){destructors.push(rawDestructor,ptr)}return ptr},readValueFromPointer:readPointer,destructorFunction:rawDestructor}]})};function sharedRegisterType(rawType,registeredInstance,options={}){var name=registeredInstance.name;if(!rawType){throwBindingError(`type "${name}" must have a positive integer typeid pointer`)}if(registeredTypes.hasOwnProperty(rawType)){if(options.ignoreDuplicateRegistrations){return}else{throwBindingError(`Cannot register type '${name}' twice`)}}registeredTypes[rawType]=registeredInstance;delete typeDependencies[rawType];if(awaitingDependencies.hasOwnProperty(rawType)){var callbacks=awaitingDependencies[rawType];delete awaitingDependencies[rawType];callbacks.forEach(cb=>cb())}}function registerType(rawType,registeredInstance,options={}){return sharedRegisterType(rawType,registeredInstance,options)}var integerReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?pointer=>HEAP8[pointer]:pointer=>HEAPU8[pointer];case 2:return signed?pointer=>HEAP16[pointer>>1]:pointer=>HEAPU16[pointer>>1];case 4:return signed?pointer=>HEAP32[pointer>>2]:pointer=>HEAPU32[pointer>>2];case 8:return signed?pointer=>HEAP64[pointer>>3]:pointer=>HEAPU64[pointer>>3];default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};var __embind_register_bigint=(primitiveType,name,size,minRange,maxRange)=>{name=AsciiToString(name);const isUnsignedType=minRange===0n;let fromWireType=value=>value;if(isUnsignedType){const bitSize=size*8;fromWireType=value=>BigInt.asUintN(bitSize,value);maxRange=fromWireType(maxRange)}registerType(primitiveType,{name,fromWireType,toWireType:(destructors,value)=>{if(typeof value=="number"){value=BigInt(value)}return value},readValueFromPointer:integerReadValueFromPointer(name,size,!isUnsignedType),destructorFunction:null})};var __embind_register_bool=(rawType,name,trueValue,falseValue)=>{name=AsciiToString(name);registerType(rawType,{name,fromWireType:function(wt){return!!wt},toWireType:function(destructors,o){return o?trueValue:falseValue},readValueFromPointer:function(pointer){return this.fromWireType(HEAPU8[pointer])},destructorFunction:null})};var shallowCopyInternalPointer=o=>({count:o.count,deleteScheduled:o.deleteScheduled,preservePointerOnDelete:o.preservePointerOnDelete,ptr:o.ptr,ptrType:o.ptrType,smartPtr:o.smartPtr,smartPtrType:o.smartPtrType});var throwInstanceAlreadyDeleted=obj=>{function getInstanceTypeName(handle){return handle.$$.ptrType.registeredClass.name}throwBindingError(getInstanceTypeName(obj)+" instance already deleted")};var deletionQueue=[];var flushPendingDeletes=()=>{while(deletionQueue.length){var obj=deletionQueue.pop();obj.$$.deleteScheduled=false;obj["delete"]()}};var delayFunction;var init_ClassHandle=()=>{let proto=ClassHandle.prototype;Object.assign(proto,{isAliasOf(other){if(!(this instanceof ClassHandle)){return false}if(!(other instanceof ClassHandle)){return false}var leftClass=this.$$.ptrType.registeredClass;var left=this.$$.ptr;other.$$=other.$$;var rightClass=other.$$.ptrType.registeredClass;var right=other.$$.ptr;while(leftClass.baseClass){left=leftClass.upcast(left);leftClass=leftClass.baseClass}while(rightClass.baseClass){right=rightClass.upcast(right);rightClass=rightClass.baseClass}return leftClass===rightClass&&left===right},clone(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.preservePointerOnDelete){this.$$.count.value+=1;return this}else{var clone=attachFinalizer(Object.create(Object.getPrototypeOf(this),{$$:{value:shallowCopyInternalPointer(this.$$)}}));clone.$$.count.value+=1;clone.$$.deleteScheduled=false;return clone}},delete(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}detachFinalizer(this);releaseClassHandle(this.$$);if(!this.$$.preservePointerOnDelete){this.$$.smartPtr=undefined;this.$$.ptr=undefined}},isDeleted(){return!this.$$.ptr},deleteLater(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}deletionQueue.push(this);if(deletionQueue.length===1&&delayFunction){delayFunction(flushPendingDeletes)}this.$$.deleteScheduled=true;return this}});const symbolDispose=Symbol.dispose;if(symbolDispose){proto[symbolDispose]=proto["delete"]}};function ClassHandle(){}var registeredPointers={};var ensureOverloadTable=(proto,methodName,humanName)=>{if(undefined===proto[methodName].overloadTable){var prevFunc=proto[methodName];proto[methodName]=function(...args){if(!proto[methodName].overloadTable.hasOwnProperty(args.length)){throwBindingError(`Function '${humanName}' called with an invalid number of arguments (${args.length}) - expects one of (${proto[methodName].overloadTable})!`)}return proto[methodName].overloadTable[args.length].apply(this,args)};proto[methodName].overloadTable=[];proto[methodName].overloadTable[prevFunc.argCount]=prevFunc}};var exposePublicSymbol=(name,value,numArguments)=>{if(Module.hasOwnProperty(name)){if(undefined===numArguments||undefined!==Module[name].overloadTable&&undefined!==Module[name].overloadTable[numArguments]){throwBindingError(`Cannot register public name '${name}' twice`)}ensureOverloadTable(Module,name,name);if(Module[name].overloadTable.hasOwnProperty(numArguments)){throwBindingError(`Cannot register multiple overloads of a function with the same number of arguments (${numArguments})!`)}Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}};var char_0=48;var char_9=57;var makeLegalFunctionName=name=>{name=name.replace(/[^a-zA-Z0-9_]/g,"$");var f=name.charCodeAt(0);if(f>=char_0&&f<=char_9){return`_${name}`}return name};function RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast){this.name=name;this.constructor=constructor;this.instancePrototype=instancePrototype;this.rawDestructor=rawDestructor;this.baseClass=baseClass;this.getActualType=getActualType;this.upcast=upcast;this.downcast=downcast;this.pureVirtualFunctions=[]}var upcastPointer=(ptr,ptrClass,desiredClass)=>{while(ptrClass!==desiredClass){if(!ptrClass.upcast){throwBindingError(`Expected null or instance of ${desiredClass.name}, got an instance of ${ptrClass.name}`)}ptr=ptrClass.upcast(ptr);ptrClass=ptrClass.baseClass}return ptr};var embindRepr=v=>{if(v===null){return"null"}var t=typeof v;if(t==="object"||t==="array"||t==="function"){return v.toString()}else{return""+v}};function constNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}function genericPointerToWireType(destructors,handle){var ptr;if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}if(this.isSmartPointer){ptr=this.rawConstructor();if(destructors!==null){destructors.push(this.rawDestructor,ptr)}return ptr}else{return 0}}if(!handle||!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(!this.isConst&&handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);if(this.isSmartPointer){if(undefined===handle.$$.smartPtr){throwBindingError("Passing raw pointer to smart pointer is illegal")}switch(this.sharingPolicy){case 0:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}break;case 1:ptr=handle.$$.smartPtr;break;case 2:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{var clonedHandle=handle["clone"]();ptr=this.rawShare(ptr,Emval.toHandle(()=>clonedHandle["delete"]()));if(destructors!==null){destructors.push(this.rawDestructor,ptr)}}break;default:throwBindingError("Unsupported sharing policy")}}return ptr}function nonConstNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}var downcastPointer=(ptr,ptrClass,desiredClass)=>{if(ptrClass===desiredClass){return ptr}if(undefined===desiredClass.baseClass){return null}var rv=downcastPointer(ptr,ptrClass,desiredClass.baseClass);if(rv===null){return null}return desiredClass.downcast(rv)};var getInheritedInstance=(class_,ptr)=>{ptr=getBasestPointer(class_,ptr);return registeredInstances[ptr]};var makeClassHandle=(prototype,record)=>{if(!record.ptrType||!record.ptr){throwInternalError("makeClassHandle requires ptr and ptrType")}var hasSmartPtrType=!!record.smartPtrType;var hasSmartPtr=!!record.smartPtr;if(hasSmartPtrType!==hasSmartPtr){throwInternalError("Both smartPtrType and smartPtr must be specified")}record.count={value:1};return attachFinalizer(Object.create(prototype,{$$:{value:record,writable:true}}))};function RegisteredPointer_fromWireType(ptr){var rawPointer=this.getPointee(ptr);if(!rawPointer){this.destructor(ptr);return null}var registeredInstance=getInheritedInstance(this.registeredClass,rawPointer);if(undefined!==registeredInstance){if(0===registeredInstance.$$.count.value){registeredInstance.$$.ptr=rawPointer;registeredInstance.$$.smartPtr=ptr;return registeredInstance["clone"]()}else{var rv=registeredInstance["clone"]();this.destructor(ptr);return rv}}function makeDefaultHandle(){if(this.isSmartPointer){return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this.pointeeType,ptr:rawPointer,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this,ptr})}}var actualType=this.registeredClass.getActualType(rawPointer);var registeredPointerRecord=registeredPointers[actualType];if(!registeredPointerRecord){return makeDefaultHandle.call(this)}var toType;if(this.isConst){toType=registeredPointerRecord.constPointerType}else{toType=registeredPointerRecord.pointerType}var dp=downcastPointer(rawPointer,this.registeredClass,toType.registeredClass);if(dp===null){return makeDefaultHandle.call(this)}if(this.isSmartPointer){return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp})}}var init_RegisteredPointer=()=>{Object.assign(RegisteredPointer.prototype,{getPointee(ptr){if(this.rawGetPointee){ptr=this.rawGetPointee(ptr)}return ptr},destructor(ptr){this.rawDestructor?.(ptr)},readValueFromPointer:readPointer,fromWireType:RegisteredPointer_fromWireType})};function RegisteredPointer(name,registeredClass,isReference,isConst,isSmartPointer,pointeeType,sharingPolicy,rawGetPointee,rawConstructor,rawShare,rawDestructor){this.name=name;this.registeredClass=registeredClass;this.isReference=isReference;this.isConst=isConst;this.isSmartPointer=isSmartPointer;this.pointeeType=pointeeType;this.sharingPolicy=sharingPolicy;this.rawGetPointee=rawGetPointee;this.rawConstructor=rawConstructor;this.rawShare=rawShare;this.rawDestructor=rawDestructor;if(!isSmartPointer&®isteredClass.baseClass===undefined){if(isConst){this.toWireType=constNoSmartPtrRawPointerToWireType;this.destructorFunction=null}else{this.toWireType=nonConstNoSmartPtrRawPointerToWireType;this.destructorFunction=null}}else{this.toWireType=genericPointerToWireType}}var replacePublicSymbol=(name,value,numArguments)=>{if(!Module.hasOwnProperty(name)){throwInternalError("Replacing nonexistent public symbol")}if(undefined!==Module[name].overloadTable&&undefined!==numArguments){Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}};var wasmTableMirror=[];var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var embind__requireFunction=(signature,rawFunction,isAsync=false)=>{signature=AsciiToString(signature);function makeDynCaller(){var rtn=getWasmTableEntry(rawFunction);return rtn}var fp=makeDynCaller();if(typeof fp!="function"){throwBindingError(`unknown function pointer with signature ${signature}: ${rawFunction}`)}return fp};class UnboundTypeError extends Error{}var throwUnboundTypeError=(message,types)=>{var unboundTypes=[];var seen={};function visit(type){if(seen[type]){return}if(registeredTypes[type]){return}if(typeDependencies[type]){typeDependencies[type].forEach(visit);return}unboundTypes.push(type);seen[type]=true}types.forEach(visit);throw new UnboundTypeError(`${message}: `+unboundTypes.map(getTypeName).join([", "]))};var __embind_register_class=(rawType,rawPointerType,rawConstPointerType,baseClassRawType,getActualTypeSignature,getActualType,upcastSignature,upcast,downcastSignature,downcast,name,destructorSignature,rawDestructor)=>{name=AsciiToString(name);getActualType=embind__requireFunction(getActualTypeSignature,getActualType);upcast&&=embind__requireFunction(upcastSignature,upcast);downcast&&=embind__requireFunction(downcastSignature,downcast);rawDestructor=embind__requireFunction(destructorSignature,rawDestructor);var legalFunctionName=makeLegalFunctionName(name);exposePublicSymbol(legalFunctionName,function(){throwUnboundTypeError(`Cannot construct ${name} due to unbound types`,[baseClassRawType])});whenDependentTypesAreResolved([rawType,rawPointerType,rawConstPointerType],baseClassRawType?[baseClassRawType]:[],base=>{base=base[0];var baseClass;var basePrototype;if(baseClassRawType){baseClass=base.registeredClass;basePrototype=baseClass.instancePrototype}else{basePrototype=ClassHandle.prototype}var constructor=createNamedFunction(name,function(...args){if(Object.getPrototypeOf(this)!==instancePrototype){throw new BindingError(`Use 'new' to construct ${name}`)}if(undefined===registeredClass.constructor_body){throw new BindingError(`${name} has no accessible constructor`)}var body=registeredClass.constructor_body[args.length];if(undefined===body){throw new BindingError(`Tried to invoke ctor of ${name} with invalid number of parameters (${args.length}) - expected (${Object.keys(registeredClass.constructor_body).toString()}) parameters instead!`)}return body.apply(this,args)});var instancePrototype=Object.create(basePrototype,{constructor:{value:constructor}});constructor.prototype=instancePrototype;var registeredClass=new RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast);if(registeredClass.baseClass){registeredClass.baseClass.__derivedClasses??=[];registeredClass.baseClass.__derivedClasses.push(registeredClass)}var referenceConverter=new RegisteredPointer(name,registeredClass,true,false,false);var pointerConverter=new RegisteredPointer(name+"*",registeredClass,false,false,false);var constPointerConverter=new RegisteredPointer(name+" const*",registeredClass,false,true,false);registeredPointers[rawType]={pointerType:pointerConverter,constPointerType:constPointerConverter};replacePublicSymbol(legalFunctionName,constructor);return[referenceConverter,pointerConverter,constPointerConverter]})};function usesDestructorStack(argTypes){for(var i=1;i{var array=[];for(var i=0;i>2])}return array};var getFunctionName=signature=>{signature=signature.trim();const argsIndex=signature.indexOf("(");if(argsIndex===-1)return signature;return signature.slice(0,argsIndex)};var __embind_register_class_class_function=(rawClassType,methodName,argCount,rawArgTypesAddr,invokerSignature,rawInvoker,fn,isAsync,isNonnullReturn)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);methodName=AsciiToString(methodName);methodName=getFunctionName(methodName);rawInvoker=embind__requireFunction(invokerSignature,rawInvoker,isAsync);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`${classType.name}.${methodName}`;function unboundTypesHandler(){throwUnboundTypeError(`Cannot call ${humanName} due to unbound types`,rawArgTypes)}if(methodName.startsWith("@@")){methodName=Symbol[methodName.substring(2)]}var proto=classType.registeredClass.constructor;if(undefined===proto[methodName]){unboundTypesHandler.argCount=argCount-1;proto[methodName]=unboundTypesHandler}else{ensureOverloadTable(proto,methodName,humanName);proto[methodName].overloadTable[argCount-1]=unboundTypesHandler}whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{var invokerArgsArray=[argTypes[0],null].concat(argTypes.slice(1));var func=craftInvokerFunction(humanName,invokerArgsArray,null,rawInvoker,fn,isAsync);if(undefined===proto[methodName].overloadTable){func.argCount=argCount-1;proto[methodName]=func}else{proto[methodName].overloadTable[argCount-1]=func}if(classType.registeredClass.__derivedClasses){for(const derivedClass of classType.registeredClass.__derivedClasses){if(!derivedClass.constructor.hasOwnProperty(methodName)){derivedClass.constructor[methodName]=func}}}return[]});return[]})};var __embind_register_class_constructor=(rawClassType,argCount,rawArgTypesAddr,invokerSignature,invoker,rawConstructor)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);invoker=embind__requireFunction(invokerSignature,invoker);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`constructor ${classType.name}`;if(undefined===classType.registeredClass.constructor_body){classType.registeredClass.constructor_body=[]}if(undefined!==classType.registeredClass.constructor_body[argCount-1]){throw new BindingError(`Cannot register multiple constructors with identical number of parameters (${argCount-1}) for class '${classType.name}'! Overload resolution is currently only performed using the parameter count, not actual type info!`)}classType.registeredClass.constructor_body[argCount-1]=()=>{throwUnboundTypeError(`Cannot construct ${classType.name} due to unbound types`,rawArgTypes)};whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{argTypes.splice(1,0,null);classType.registeredClass.constructor_body[argCount-1]=craftInvokerFunction(humanName,argTypes,null,invoker,rawConstructor);return[]});return[]})};var __embind_register_class_function=(rawClassType,methodName,argCount,rawArgTypesAddr,invokerSignature,rawInvoker,context,isPureVirtual,isAsync,isNonnullReturn)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);methodName=AsciiToString(methodName);methodName=getFunctionName(methodName);rawInvoker=embind__requireFunction(invokerSignature,rawInvoker,isAsync);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`${classType.name}.${methodName}`;if(methodName.startsWith("@@")){methodName=Symbol[methodName.substring(2)]}if(isPureVirtual){classType.registeredClass.pureVirtualFunctions.push(methodName)}function unboundTypesHandler(){throwUnboundTypeError(`Cannot call ${humanName} due to unbound types`,rawArgTypes)}var proto=classType.registeredClass.instancePrototype;var method=proto[methodName];if(undefined===method||undefined===method.overloadTable&&method.className!==classType.name&&method.argCount===argCount-2){unboundTypesHandler.argCount=argCount-2;unboundTypesHandler.className=classType.name;proto[methodName]=unboundTypesHandler}else{ensureOverloadTable(proto,methodName,humanName);proto[methodName].overloadTable[argCount-2]=unboundTypesHandler}whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{var memberFunction=craftInvokerFunction(humanName,argTypes,classType,rawInvoker,context,isAsync);if(undefined===proto[methodName].overloadTable){memberFunction.argCount=argCount-2;proto[methodName]=memberFunction}else{proto[methodName].overloadTable[argCount-2]=memberFunction}return[]});return[]})};var validateThis=(this_,classType,humanName)=>{if(!(this_ instanceof Object)){throwBindingError(`${humanName} with invalid "this": ${this_}`)}if(!(this_ instanceof classType.registeredClass.constructor)){throwBindingError(`${humanName} incompatible with "this" of type ${this_.constructor.name}`)}if(!this_.$$.ptr){throwBindingError(`cannot call emscripten binding method ${humanName} on deleted object`)}return upcastPointer(this_.$$.ptr,this_.$$.ptrType.registeredClass,classType.registeredClass)};var __embind_register_class_property=(classType,fieldName,getterReturnType,getterSignature,getter,getterContext,setterArgumentType,setterSignature,setter,setterContext)=>{fieldName=AsciiToString(fieldName);getter=embind__requireFunction(getterSignature,getter);whenDependentTypesAreResolved([],[classType],classType=>{classType=classType[0];var humanName=`${classType.name}.${fieldName}`;var desc={get(){throwUnboundTypeError(`Cannot access ${humanName} due to unbound types`,[getterReturnType,setterArgumentType])},enumerable:true,configurable:true};if(setter){desc.set=()=>throwUnboundTypeError(`Cannot access ${humanName} due to unbound types`,[getterReturnType,setterArgumentType])}else{desc.set=v=>throwBindingError(humanName+" is a read-only property")}Object.defineProperty(classType.registeredClass.instancePrototype,fieldName,desc);whenDependentTypesAreResolved([],setter?[getterReturnType,setterArgumentType]:[getterReturnType],types=>{var getterReturnType=types[0];var desc={get(){var ptr=validateThis(this,classType,humanName+" getter");return getterReturnType.fromWireType(getter(getterContext,ptr))},enumerable:true};if(setter){setter=embind__requireFunction(setterSignature,setter);var setterArgumentType=types[1];desc.set=function(v){var ptr=validateThis(this,classType,humanName+" setter");var destructors=[];setter(setterContext,ptr,setterArgumentType.toWireType(destructors,v));runDestructors(destructors)}}Object.defineProperty(classType.registeredClass.instancePrototype,fieldName,desc);return[]});return[]})};var __embind_register_constant=(name,type,value)=>{name=AsciiToString(name);whenDependentTypesAreResolved([],[type],type=>{type=type[0];Module[name]=type.fromWireType(value);return[]})};var __emval_decref=handle=>{if(handle>9&&0===--emval_handles[handle+1]){emval_handles[handle]=undefined;emval_freelist.push(handle)}};var EmValType={name:"emscripten::val",fromWireType:handle=>{var rv=Emval.toValue(handle);__emval_decref(handle);return rv},toWireType:(destructors,value)=>Emval.toHandle(value),readValueFromPointer:readPointer,destructorFunction:null};var __embind_register_emval=rawType=>registerType(rawType,EmValType);var enumReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?function(pointer){return this.fromWireType(HEAP8[pointer])}:function(pointer){return this.fromWireType(HEAPU8[pointer])};case 2:return signed?function(pointer){return this.fromWireType(HEAP16[pointer>>1])}:function(pointer){return this.fromWireType(HEAPU16[pointer>>1])};case 4:return signed?function(pointer){return this.fromWireType(HEAP32[pointer>>2])}:function(pointer){return this.fromWireType(HEAPU32[pointer>>2])};default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};function getEnumValueType(rawValueType){return rawValueType===0?"object":rawValueType===1?"number":"string"}var __embind_register_enum=(rawType,name,size,isSigned,rawValueType)=>{name=AsciiToString(name);const valueType=getEnumValueType(rawValueType);switch(valueType){case"object":{function ctor(){}ctor.values={};registerType(rawType,{name,constructor:ctor,valueType,fromWireType:function(c){return this.constructor.values[c]},toWireType:(destructors,c)=>c.value,readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,ctor);break}case"number":{var keysMap={};registerType(rawType,{name,keysMap,valueType,fromWireType:c=>c,toWireType:(destructors,c)=>c,readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,keysMap);delete Module[name].argCount;break}case"string":{var valuesMap={};var reverseMap={};var keysMap={};registerType(rawType,{name,valuesMap,reverseMap,keysMap,valueType,fromWireType:function(c){return this.reverseMap[c]},toWireType:function(destructors,c){return this.valuesMap[c]},readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,keysMap);delete Module[name].argCount;break}}};var __embind_register_enum_value=(rawEnumType,name,enumValue)=>{var enumType=requireRegisteredType(rawEnumType,"enum");name=AsciiToString(name);switch(enumType.valueType){case"object":{var Enum=enumType.constructor;var Value=Object.create(enumType.constructor.prototype,{value:{value:enumValue},constructor:{value:createNamedFunction(`${enumType.name}_${name}`,function(){})}});Enum.values[enumValue]=Value;Enum[name]=Value;break}case"number":{enumType.keysMap[name]=enumValue;break}case"string":{enumType.valuesMap[name]=enumValue;enumType.reverseMap[enumValue]=name;enumType.keysMap[name]=name;break}}};var floatReadValueFromPointer=(name,width)=>{switch(width){case 4:return function(pointer){return this.fromWireType(HEAPF32[pointer>>2])};case 8:return function(pointer){return this.fromWireType(HEAPF64[pointer>>3])};default:throw new TypeError(`invalid float width (${width}): ${name}`)}};var __embind_register_float=(rawType,name,size)=>{name=AsciiToString(name);registerType(rawType,{name,fromWireType:value=>value,toWireType:(destructors,value)=>value,readValueFromPointer:floatReadValueFromPointer(name,size),destructorFunction:null})};var __embind_register_function=(name,argCount,rawArgTypesAddr,signature,rawInvoker,fn,isAsync,isNonnullReturn)=>{var argTypes=heap32VectorToArray(argCount,rawArgTypesAddr);name=AsciiToString(name);name=getFunctionName(name);rawInvoker=embind__requireFunction(signature,rawInvoker,isAsync);exposePublicSymbol(name,function(){throwUnboundTypeError(`Cannot call ${name} due to unbound types`,argTypes)},argCount-1);whenDependentTypesAreResolved([],argTypes,argTypes=>{var invokerArgsArray=[argTypes[0],null].concat(argTypes.slice(1));replacePublicSymbol(name,craftInvokerFunction(name,invokerArgsArray,null,rawInvoker,fn,isAsync),argCount-1);return[]})};var __embind_register_integer=(primitiveType,name,size,minRange,maxRange)=>{name=AsciiToString(name);const isUnsignedType=minRange===0;let fromWireType=value=>value;if(isUnsignedType){var bitshift=32-8*size;fromWireType=value=>value<>>bitshift;maxRange=fromWireType(maxRange)}registerType(primitiveType,{name,fromWireType,toWireType:(destructors,value)=>value,readValueFromPointer:integerReadValueFromPointer(name,size,minRange!==0),destructorFunction:null})};var installIndexedIterator=(proto,sizeMethodName,getMethodName)=>{const makeIterator=(size,getValue)=>{let index=0;return{next(){if(index>=size){return{done:true}}const current=index;index++;const value=getValue(current);return{value,done:false}},[Symbol.iterator](){return this}}};if(!proto[Symbol.iterator]){proto[Symbol.iterator]=function(){const size=this[sizeMethodName]();return makeIterator(size,i=>this[getMethodName](i))}}};var __embind_register_iterable=(rawClassType,rawElementType,sizeMethodName,getMethodName)=>{sizeMethodName=AsciiToString(sizeMethodName);getMethodName=AsciiToString(getMethodName);whenDependentTypesAreResolved([],[rawClassType,rawElementType],types=>{const classType=types[0];installIndexedIterator(classType.registeredClass.instancePrototype,sizeMethodName,getMethodName);return[]})};var __embind_register_memory_view=(rawType,dataTypeIndex,name)=>{var typeMapping=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array,BigInt64Array,BigUint64Array];var TA=typeMapping[dataTypeIndex];function decodeMemoryView(handle){var size=HEAPU32[handle>>2];var data=HEAPU32[handle+4>>2];return new TA(HEAP8.buffer,data,size)}name=AsciiToString(name);registerType(rawType,{name,fromWireType:decodeMemoryView,readValueFromPointer:decodeMemoryView},{ignoreDuplicateRegistrations:true})};var EmValOptionalType=Object.assign({optional:true},EmValType);var __embind_register_optional=(rawOptionalType,rawType)=>{registerType(rawOptionalType,EmValOptionalType)};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var __embind_register_std_string=(rawType,name)=>{name=AsciiToString(name);var stdStringIsUTF8=true;registerType(rawType,{name,fromWireType(value){var length=HEAPU32[value>>2];var payload=value+4;var str;if(stdStringIsUTF8){str=UTF8ToString(payload,length,true)}else{str="";for(var i=0;i>2]=length;if(valueIsOfTypeString){if(stdStringIsUTF8){stringToUTF8(value,ptr,length+1)}else{for(var i=0;i255){_free(base);throwBindingError("String has UTF-16 code units that do not fit in 8 bits")}HEAPU8[ptr+i]=charCode}}}else{HEAPU8.set(value,ptr)}if(destructors!==null){destructors.push(_free,base)}return base},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var UTF16Decoder=globalThis.TextDecoder?new TextDecoder("utf-16le"):undefined;var UTF16ToString=(ptr,maxBytesToRead,ignoreNul)=>{var idx=ptr>>1;var endIdx=findStringEnd(HEAPU16,idx,maxBytesToRead/2,ignoreNul);if(endIdx-idx>16&&UTF16Decoder)return UTF16Decoder.decode(HEAPU16.subarray(idx,endIdx));var str="";for(var i=idx;i{maxBytesToWrite??=2147483647;if(maxBytesToWrite<2)return 0;maxBytesToWrite-=2;var startPtr=outPtr;var numCharsToWrite=maxBytesToWrite>1]=codeUnit;outPtr+=2}HEAP16[outPtr>>1]=0;return outPtr-startPtr};var lengthBytesUTF16=str=>str.length*2;var UTF32ToString=(ptr,maxBytesToRead,ignoreNul)=>{var str="";var startIdx=ptr>>2;for(var i=0;!(i>=maxBytesToRead/4);i++){var utf32=HEAPU32[startIdx+i];if(!utf32&&!ignoreNul)break;str+=String.fromCodePoint(utf32)}return str};var stringToUTF32=(str,outPtr,maxBytesToWrite)=>{maxBytesToWrite??=2147483647;if(maxBytesToWrite<4)return 0;var startPtr=outPtr;var endPtr=startPtr+maxBytesToWrite-4;for(var i=0;i65535){i++}HEAP32[outPtr>>2]=codePoint;outPtr+=4;if(outPtr+4>endPtr)break}HEAP32[outPtr>>2]=0;return outPtr-startPtr};var lengthBytesUTF32=str=>{var len=0;for(var i=0;i65535){i++}len+=4}return len};var __embind_register_std_wstring=(rawType,charSize,name)=>{name=AsciiToString(name);var decodeString,encodeString,lengthBytesUTF;if(charSize===2){decodeString=UTF16ToString;encodeString=stringToUTF16;lengthBytesUTF=lengthBytesUTF16}else{decodeString=UTF32ToString;encodeString=stringToUTF32;lengthBytesUTF=lengthBytesUTF32}registerType(rawType,{name,fromWireType:value=>{var length=HEAPU32[value>>2];var str=decodeString(value+4,length*charSize,true);_free(value);return str},toWireType:(destructors,value)=>{if(!(typeof value=="string")){throwBindingError(`Cannot pass non-string to C++ string type ${name}`)}var length=lengthBytesUTF(value);var ptr=_malloc(4+length+charSize);HEAPU32[ptr>>2]=length/charSize;encodeString(value,ptr+4,length+charSize);if(destructors!==null){destructors.push(_free,ptr)}return ptr},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var __embind_register_value_object=(rawType,name,constructorSignature,rawConstructor,destructorSignature,rawDestructor)=>{structRegistrations[rawType]={name:AsciiToString(name),rawConstructor:embind__requireFunction(constructorSignature,rawConstructor),rawDestructor:embind__requireFunction(destructorSignature,rawDestructor),fields:[]}};var __embind_register_value_object_field=(structType,fieldName,getterReturnType,getterSignature,getter,getterContext,setterArgumentType,setterSignature,setter,setterContext)=>{structRegistrations[structType].fields.push({fieldName:AsciiToString(fieldName),getterReturnType,getter:embind__requireFunction(getterSignature,getter),getterContext,setterArgumentType,setter:embind__requireFunction(setterSignature,setter),setterContext})};var __embind_register_void=(rawType,name)=>{name=AsciiToString(name);registerType(rawType,{isVoid:true,name,fromWireType:()=>undefined,toWireType:(destructors,o)=>undefined})};var runtimeKeepaliveCounter=0;var __emscripten_runtime_keepalive_clear=()=>{noExitRuntime=false;runtimeKeepaliveCounter=0};var emval_methodCallers=[];var emval_addMethodCaller=caller=>{var id=emval_methodCallers.length;emval_methodCallers.push(caller);return id};var emval_lookupTypes=(argCount,argTypes)=>{var a=new Array(argCount);for(var i=0;i>2],`parameter ${i}`)}return a};var emval_returnValue=(toReturnWire,destructorsRef,handle)=>{var destructors=[];var result=toReturnWire(destructors,handle);if(destructors.length){HEAPU32[destructorsRef>>2]=Emval.toHandle(destructors)}return result};var emval_symbols={};var getStringOrSymbol=address=>{var symbol=emval_symbols[address];if(symbol===undefined){return AsciiToString(address)}return symbol};var __emval_create_invoker=(argCount,argTypesPtr,kind)=>{var GenericWireTypeSize=8;var[retType,...argTypes]=emval_lookupTypes(argCount,argTypesPtr);var toReturnWire=retType.toWireType.bind(retType);var argFromPtr=argTypes.map(type=>type.readValueFromPointer.bind(type));argCount--;var argN=new Array(argCount);var invokerFunction=(handle,methodName,destructorsRef,args)=>{var offset=0;for(var i=0;it.name)}) => ${retType.name}>`;return emval_addMethodCaller(createNamedFunction(functionName,invokerFunction))};var __emval_invoke=(caller,handle,methodName,destructorsRef,args)=>emval_methodCallers[caller](handle,methodName,destructorsRef,args);var __emval_new_cstring=v=>Emval.toHandle(getStringOrSymbol(v));var __emval_new_object=()=>Emval.toHandle({});var __emval_run_destructors=handle=>{var destructors=Emval.toValue(handle);runDestructors(destructors);__emval_decref(handle)};var __emval_set_property=(handle,key,value)=>{handle=Emval.toValue(handle);key=Emval.toValue(key);value=Emval.toValue(value);handle[key]=value};var timers={};var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;var maybeExit=()=>{if(!keepRuntimeAlive()){try{_exit(EXITSTATUS)}catch(e){handleException(e)}}};var callUserCallback=func=>{if(ABORT){return}try{return func()}catch(e){handleException(e)}finally{maybeExit()}};var _emscripten_get_now=()=>performance.now();var __setitimer_js=(which,timeout_ms)=>{if(timers[which]){clearTimeout(timers[which].id);delete timers[which]}if(!timeout_ms)return 0;var id=setTimeout(()=>{delete timers[which];callUserCallback(()=>__emscripten_timeout(which,_emscripten_get_now()))},timeout_ms);timers[which]={id,timeout_ms};return 0};var _emscripten_date_now=()=>Date.now();var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var printCharBuffers=[null,[],[]];var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(curr===0||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer));buffer.length=0}else{buffer.push(curr)}};var _fd_write=(fd,iov,iovcnt,pnum)=>{var num=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j>2]=num;return 0};init_ClassHandle();init_RegisteredPointer();{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}var ___getTypeName,_free,_malloc,__emscripten_timeout,memory,__indirect_function_table,wasmMemory,wasmTable;function assignWasmExports(wasmExports){___getTypeName=wasmExports["Q"];_free=Module["_free"]=wasmExports["R"];_malloc=Module["_malloc"]=wasmExports["S"];__emscripten_timeout=wasmExports["U"];memory=wasmMemory=wasmExports["O"];__indirect_function_table=wasmTable=wasmExports["T"]}var wasmImports={J:__abort_js,x:__embind_create_inheriting_constructor,s:__embind_finalize_value_object,z:__embind_register_bigint,A:__embind_register_bool,b:__embind_register_class,p:__embind_register_class_class_function,e:__embind_register_class_constructor,a:__embind_register_class_function,d:__embind_register_class_property,K:__embind_register_constant,M:__embind_register_emval,l:__embind_register_enum,f:__embind_register_enum_value,y:__embind_register_float,g:__embind_register_function,o:__embind_register_integer,q:__embind_register_iterable,h:__embind_register_memory_view,r:__embind_register_optional,N:__embind_register_std_string,u:__embind_register_std_wstring,t:__embind_register_value_object,m:__embind_register_value_object_field,B:__embind_register_void,D:__emscripten_runtime_keepalive_clear,k:__emval_create_invoker,n:__emval_decref,j:__emval_invoke,v:__emval_new_cstring,L:__emval_new_object,i:__emval_run_destructors,w:__emval_set_property,E:__setitimer_js,I:_emscripten_date_now,c:_emscripten_get_now,F:_emscripten_resize_heap,G:_exit,H:_fd_write,C:_proc_exit};function run(){preRun();function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} +var PHYSX=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};if(typeof __filename!="undefined"){_scriptName=__filename}else if(ENVIRONMENT_IS_WORKER){_scriptName=self.location.href}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{if(isFileURI(url)){return new Promise((resolve,reject)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){resolve(xhr.response);return}reject(xhr.status)};xhr.onerror=reject;xhr.send(null)})}var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var EXITSTATUS;var isFileURI=filename=>filename.startsWith("file://");var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["P"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("physx.release.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!isFileURI(binaryFile)&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var __abort_js=()=>abort("");var createNamedFunction=(name,func)=>Object.defineProperty(func,"name",{value:name});var emval_freelist=[];var emval_handles=[0,1,,1,null,1,true,1,false,1];var BindingError=class BindingError extends Error{constructor(message){super(message);this.name="BindingError"}};var throwBindingError=message=>{throw new BindingError(message)};var Emval={toValue:handle=>{if(!handle){throwBindingError(`Cannot use deleted val. handle = ${handle}`)}return emval_handles[handle]},toHandle:value=>{switch(value){case undefined:return 2;case null:return 4;case true:return 6;case false:return 8;default:{const handle=emval_freelist.pop()||emval_handles.length;emval_handles[handle]=value;emval_handles[handle+1]=1;return handle}}}};class PureVirtualError extends Error{}var AsciiToString=ptr=>{var str="";while(1){var ch=HEAPU8[ptr++];if(!ch)return str;str+=String.fromCharCode(ch)}};var registeredInstances={};var getBasestPointer=(class_,ptr)=>{if(ptr===undefined){throwBindingError("ptr should not be undefined")}while(class_.baseClass){ptr=class_.upcast(ptr);class_=class_.baseClass}return ptr};var registerInheritedInstance=(class_,ptr,instance)=>{ptr=getBasestPointer(class_,ptr);if(registeredInstances.hasOwnProperty(ptr)){throwBindingError(`Tried to register registered instance: ${ptr}`)}else{registeredInstances[ptr]=instance}};var registeredTypes={};var getTypeName=type=>{var ptr=___getTypeName(type);var rv=AsciiToString(ptr);_free(ptr);return rv};var requireRegisteredType=(rawType,humanName)=>{var impl=registeredTypes[rawType];if(undefined===impl){throwBindingError(`${humanName} has unknown type ${getTypeName(rawType)}`)}return impl};var unregisterInheritedInstance=(class_,ptr)=>{ptr=getBasestPointer(class_,ptr);if(registeredInstances.hasOwnProperty(ptr)){delete registeredInstances[ptr]}else{throwBindingError(`Tried to unregister unregistered instance: ${ptr}`)}};var detachFinalizer=handle=>{};var finalizationRegistry=false;var runDestructor=$$=>{if($$.smartPtr){$$.smartPtrType.rawDestructor($$.smartPtr)}else{$$.ptrType.registeredClass.rawDestructor($$.ptr)}};var releaseClassHandle=$$=>{$$.count.value-=1;var toDelete=0===$$.count.value;if(toDelete){runDestructor($$)}};var attachFinalizer=handle=>{if(!globalThis.FinalizationRegistry){attachFinalizer=handle=>handle;return handle}finalizationRegistry=new FinalizationRegistry(info=>{releaseClassHandle(info.$$)});attachFinalizer=handle=>{var $$=handle.$$;var hasSmartPtr=!!$$.smartPtr;if(hasSmartPtr){var info={$$};finalizationRegistry.register(handle,info,handle)}return handle};detachFinalizer=handle=>finalizationRegistry.unregister(handle);return attachFinalizer(handle)};var __embind_create_inheriting_constructor=(constructorName,wrapperType,properties)=>{constructorName=AsciiToString(constructorName);wrapperType=requireRegisteredType(wrapperType,"wrapper");properties=Emval.toValue(properties);var registeredClass=wrapperType.registeredClass;var wrapperPrototype=registeredClass.instancePrototype;var baseClass=registeredClass.baseClass;var baseClassPrototype=baseClass.instancePrototype;var baseConstructor=registeredClass.baseClass.constructor;var ctor=createNamedFunction(constructorName,function(...args){for(var name of registeredClass.baseClass.pureVirtualFunctions){if(this[name]===baseClassPrototype[name]){throw new PureVirtualError(`Pure virtual function ${name} must be implemented in JavaScript`)}}Object.defineProperty(this,"__parent",{value:wrapperPrototype});this["__construct"](...args)});wrapperPrototype["__construct"]=function __construct(...args){if(this===wrapperPrototype){throwBindingError("Pass correct 'this' to __construct")}var inner=baseConstructor["implement"](this,...args);detachFinalizer(inner);var $$=inner.$$;inner["notifyOnDestruction"]();$$.preservePointerOnDelete=true;Object.defineProperties(this,{$$:{value:$$}});attachFinalizer(this);registerInheritedInstance(registeredClass,$$.ptr,this)};wrapperPrototype["__destruct"]=function __destruct(){if(this===wrapperPrototype){throwBindingError("Pass correct 'this' to __destruct")}detachFinalizer(this);unregisterInheritedInstance(registeredClass,this.$$.ptr)};ctor.prototype=Object.create(wrapperPrototype);Object.assign(ctor.prototype,properties);return Emval.toHandle(ctor)};var structRegistrations={};var runDestructors=destructors=>{while(destructors.length){var ptr=destructors.pop();var del=destructors.pop();del(ptr)}};function readPointer(pointer){return this.fromWireType(HEAPU32[pointer>>2])}var awaitingDependencies={};var typeDependencies={};var InternalError=class InternalError extends Error{constructor(message){super(message);this.name="InternalError"}};var throwInternalError=message=>{throw new InternalError(message)};var whenDependentTypesAreResolved=(myTypes,dependentTypes,getTypeConverters)=>{myTypes.forEach(type=>typeDependencies[type]=dependentTypes);function onComplete(typeConverters){var myTypeConverters=getTypeConverters(typeConverters);if(myTypeConverters.length!==myTypes.length){throwInternalError("Mismatched type converter count")}for(var i=0;i{typeConverters[i]=registeredTypes[dt];++registered;if(registered===unregisteredTypes.length){onComplete(typeConverters)}})}}if(0===unregisteredTypes.length){onComplete(typeConverters)}};var __embind_finalize_value_object=structType=>{var reg=structRegistrations[structType];delete structRegistrations[structType];var rawConstructor=reg.rawConstructor;var rawDestructor=reg.rawDestructor;var fieldRecords=reg.fields;var fieldTypes=fieldRecords.map(field=>field.getterReturnType).concat(fieldRecords.map(field=>field.setterArgumentType));whenDependentTypesAreResolved([structType],fieldTypes,fieldTypes=>{var fields={};for(var[i,field]of fieldRecords.entries()){const getterReturnType=fieldTypes[i];const getter=field.getter;const getterContext=field.getterContext;const setterArgumentType=fieldTypes[i+fieldRecords.length];const setter=field.setter;const setterContext=field.setterContext;fields[field.fieldName]={read:ptr=>getterReturnType.fromWireType(getter(getterContext,ptr)),write:(ptr,o)=>{var destructors=[];setter(setterContext,ptr,setterArgumentType.toWireType(destructors,o));runDestructors(destructors)},optional:getterReturnType.optional}}return[{name:reg.name,fromWireType:ptr=>{var rv={};for(var i in fields){rv[i]=fields[i].read(ptr)}rawDestructor(ptr);return rv},toWireType:(destructors,o)=>{for(var fieldName in fields){if(!(fieldName in o)&&!fields[fieldName].optional){throw new TypeError(`Missing field: "${fieldName}"`)}}var ptr=rawConstructor();for(fieldName in fields){fields[fieldName].write(ptr,o[fieldName])}if(destructors!==null){destructors.push(rawDestructor,ptr)}return ptr},readValueFromPointer:readPointer,destructorFunction:rawDestructor}]})};function sharedRegisterType(rawType,registeredInstance,options={}){var name=registeredInstance.name;if(!rawType){throwBindingError(`type "${name}" must have a positive integer typeid pointer`)}if(registeredTypes.hasOwnProperty(rawType)){if(options.ignoreDuplicateRegistrations){return}else{throwBindingError(`Cannot register type '${name}' twice`)}}registeredTypes[rawType]=registeredInstance;delete typeDependencies[rawType];if(awaitingDependencies.hasOwnProperty(rawType)){var callbacks=awaitingDependencies[rawType];delete awaitingDependencies[rawType];callbacks.forEach(cb=>cb())}}function registerType(rawType,registeredInstance,options={}){return sharedRegisterType(rawType,registeredInstance,options)}var integerReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?pointer=>HEAP8[pointer]:pointer=>HEAPU8[pointer];case 2:return signed?pointer=>HEAP16[pointer>>1]:pointer=>HEAPU16[pointer>>1];case 4:return signed?pointer=>HEAP32[pointer>>2]:pointer=>HEAPU32[pointer>>2];case 8:return signed?pointer=>HEAP64[pointer>>3]:pointer=>HEAPU64[pointer>>3];default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};var __embind_register_bigint=(primitiveType,name,size,minRange,maxRange)=>{name=AsciiToString(name);const isUnsignedType=minRange===0n;let fromWireType=value=>value;if(isUnsignedType){const bitSize=size*8;fromWireType=value=>BigInt.asUintN(bitSize,value);maxRange=fromWireType(maxRange)}registerType(primitiveType,{name,fromWireType,toWireType:(destructors,value)=>{if(typeof value=="number"){value=BigInt(value)}return value},readValueFromPointer:integerReadValueFromPointer(name,size,!isUnsignedType),destructorFunction:null})};var __embind_register_bool=(rawType,name,trueValue,falseValue)=>{name=AsciiToString(name);registerType(rawType,{name,fromWireType:function(wt){return!!wt},toWireType:function(destructors,o){return o?trueValue:falseValue},readValueFromPointer:function(pointer){return this.fromWireType(HEAPU8[pointer])},destructorFunction:null})};var shallowCopyInternalPointer=o=>({count:o.count,deleteScheduled:o.deleteScheduled,preservePointerOnDelete:o.preservePointerOnDelete,ptr:o.ptr,ptrType:o.ptrType,smartPtr:o.smartPtr,smartPtrType:o.smartPtrType});var throwInstanceAlreadyDeleted=obj=>{function getInstanceTypeName(handle){return handle.$$.ptrType.registeredClass.name}throwBindingError(getInstanceTypeName(obj)+" instance already deleted")};var deletionQueue=[];var flushPendingDeletes=()=>{while(deletionQueue.length){var obj=deletionQueue.pop();obj.$$.deleteScheduled=false;obj["delete"]()}};var delayFunction;var init_ClassHandle=()=>{let proto=ClassHandle.prototype;Object.assign(proto,{isAliasOf(other){if(!(this instanceof ClassHandle)){return false}if(!(other instanceof ClassHandle)){return false}var leftClass=this.$$.ptrType.registeredClass;var left=this.$$.ptr;other.$$=other.$$;var rightClass=other.$$.ptrType.registeredClass;var right=other.$$.ptr;while(leftClass.baseClass){left=leftClass.upcast(left);leftClass=leftClass.baseClass}while(rightClass.baseClass){right=rightClass.upcast(right);rightClass=rightClass.baseClass}return leftClass===rightClass&&left===right},clone(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.preservePointerOnDelete){this.$$.count.value+=1;return this}else{var clone=attachFinalizer(Object.create(Object.getPrototypeOf(this),{$$:{value:shallowCopyInternalPointer(this.$$)}}));clone.$$.count.value+=1;clone.$$.deleteScheduled=false;return clone}},delete(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}detachFinalizer(this);releaseClassHandle(this.$$);if(!this.$$.preservePointerOnDelete){this.$$.smartPtr=undefined;this.$$.ptr=undefined}},isDeleted(){return!this.$$.ptr},deleteLater(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}deletionQueue.push(this);if(deletionQueue.length===1&&delayFunction){delayFunction(flushPendingDeletes)}this.$$.deleteScheduled=true;return this}});const symbolDispose=Symbol.dispose;if(symbolDispose){proto[symbolDispose]=proto["delete"]}};function ClassHandle(){}var registeredPointers={};var ensureOverloadTable=(proto,methodName,humanName)=>{if(undefined===proto[methodName].overloadTable){var prevFunc=proto[methodName];proto[methodName]=function(...args){if(!proto[methodName].overloadTable.hasOwnProperty(args.length)){throwBindingError(`Function '${humanName}' called with an invalid number of arguments (${args.length}) - expects one of (${proto[methodName].overloadTable})!`)}return proto[methodName].overloadTable[args.length].apply(this,args)};proto[methodName].overloadTable=[];proto[methodName].overloadTable[prevFunc.argCount]=prevFunc}};var exposePublicSymbol=(name,value,numArguments)=>{if(Module.hasOwnProperty(name)){if(undefined===numArguments||undefined!==Module[name].overloadTable&&undefined!==Module[name].overloadTable[numArguments]){throwBindingError(`Cannot register public name '${name}' twice`)}ensureOverloadTable(Module,name,name);if(Module[name].overloadTable.hasOwnProperty(numArguments)){throwBindingError(`Cannot register multiple overloads of a function with the same number of arguments (${numArguments})!`)}Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}};var char_0=48;var char_9=57;var makeLegalFunctionName=name=>{name=name.replace(/[^a-zA-Z0-9_]/g,"$");var f=name.charCodeAt(0);if(f>=char_0&&f<=char_9){return`_${name}`}return name};function RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast){this.name=name;this.constructor=constructor;this.instancePrototype=instancePrototype;this.rawDestructor=rawDestructor;this.baseClass=baseClass;this.getActualType=getActualType;this.upcast=upcast;this.downcast=downcast;this.pureVirtualFunctions=[]}var upcastPointer=(ptr,ptrClass,desiredClass)=>{while(ptrClass!==desiredClass){if(!ptrClass.upcast){throwBindingError(`Expected null or instance of ${desiredClass.name}, got an instance of ${ptrClass.name}`)}ptr=ptrClass.upcast(ptr);ptrClass=ptrClass.baseClass}return ptr};var embindRepr=v=>{if(v===null){return"null"}var t=typeof v;if(t==="object"||t==="array"||t==="function"){return v.toString()}else{return""+v}};function constNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}function genericPointerToWireType(destructors,handle){var ptr;if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}if(this.isSmartPointer){ptr=this.rawConstructor();if(destructors!==null){destructors.push(this.rawDestructor,ptr)}return ptr}else{return 0}}if(!handle||!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(!this.isConst&&handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);if(this.isSmartPointer){if(undefined===handle.$$.smartPtr){throwBindingError("Passing raw pointer to smart pointer is illegal")}switch(this.sharingPolicy){case 0:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}break;case 1:ptr=handle.$$.smartPtr;break;case 2:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{var clonedHandle=handle["clone"]();ptr=this.rawShare(ptr,Emval.toHandle(()=>clonedHandle["delete"]()));if(destructors!==null){destructors.push(this.rawDestructor,ptr)}}break;default:throwBindingError("Unsupported sharing policy")}}return ptr}function nonConstNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}var downcastPointer=(ptr,ptrClass,desiredClass)=>{if(ptrClass===desiredClass){return ptr}if(undefined===desiredClass.baseClass){return null}var rv=downcastPointer(ptr,ptrClass,desiredClass.baseClass);if(rv===null){return null}return desiredClass.downcast(rv)};var getInheritedInstance=(class_,ptr)=>{ptr=getBasestPointer(class_,ptr);return registeredInstances[ptr]};var makeClassHandle=(prototype,record)=>{if(!record.ptrType||!record.ptr){throwInternalError("makeClassHandle requires ptr and ptrType")}var hasSmartPtrType=!!record.smartPtrType;var hasSmartPtr=!!record.smartPtr;if(hasSmartPtrType!==hasSmartPtr){throwInternalError("Both smartPtrType and smartPtr must be specified")}record.count={value:1};return attachFinalizer(Object.create(prototype,{$$:{value:record,writable:true}}))};function RegisteredPointer_fromWireType(ptr){var rawPointer=this.getPointee(ptr);if(!rawPointer){this.destructor(ptr);return null}var registeredInstance=getInheritedInstance(this.registeredClass,rawPointer);if(undefined!==registeredInstance){if(0===registeredInstance.$$.count.value){registeredInstance.$$.ptr=rawPointer;registeredInstance.$$.smartPtr=ptr;return registeredInstance["clone"]()}else{var rv=registeredInstance["clone"]();this.destructor(ptr);return rv}}function makeDefaultHandle(){if(this.isSmartPointer){return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this.pointeeType,ptr:rawPointer,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this,ptr})}}var actualType=this.registeredClass.getActualType(rawPointer);var registeredPointerRecord=registeredPointers[actualType];if(!registeredPointerRecord){return makeDefaultHandle.call(this)}var toType;if(this.isConst){toType=registeredPointerRecord.constPointerType}else{toType=registeredPointerRecord.pointerType}var dp=downcastPointer(rawPointer,this.registeredClass,toType.registeredClass);if(dp===null){return makeDefaultHandle.call(this)}if(this.isSmartPointer){return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp})}}var init_RegisteredPointer=()=>{Object.assign(RegisteredPointer.prototype,{getPointee(ptr){if(this.rawGetPointee){ptr=this.rawGetPointee(ptr)}return ptr},destructor(ptr){this.rawDestructor?.(ptr)},readValueFromPointer:readPointer,fromWireType:RegisteredPointer_fromWireType})};function RegisteredPointer(name,registeredClass,isReference,isConst,isSmartPointer,pointeeType,sharingPolicy,rawGetPointee,rawConstructor,rawShare,rawDestructor){this.name=name;this.registeredClass=registeredClass;this.isReference=isReference;this.isConst=isConst;this.isSmartPointer=isSmartPointer;this.pointeeType=pointeeType;this.sharingPolicy=sharingPolicy;this.rawGetPointee=rawGetPointee;this.rawConstructor=rawConstructor;this.rawShare=rawShare;this.rawDestructor=rawDestructor;if(!isSmartPointer&®isteredClass.baseClass===undefined){if(isConst){this.toWireType=constNoSmartPtrRawPointerToWireType;this.destructorFunction=null}else{this.toWireType=nonConstNoSmartPtrRawPointerToWireType;this.destructorFunction=null}}else{this.toWireType=genericPointerToWireType}}var replacePublicSymbol=(name,value,numArguments)=>{if(!Module.hasOwnProperty(name)){throwInternalError("Replacing nonexistent public symbol")}if(undefined!==Module[name].overloadTable&&undefined!==numArguments){Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}};var wasmTableMirror=[];var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var embind__requireFunction=(signature,rawFunction,isAsync=false)=>{signature=AsciiToString(signature);function makeDynCaller(){var rtn=getWasmTableEntry(rawFunction);return rtn}var fp=makeDynCaller();if(typeof fp!="function"){throwBindingError(`unknown function pointer with signature ${signature}: ${rawFunction}`)}return fp};class UnboundTypeError extends Error{}var throwUnboundTypeError=(message,types)=>{var unboundTypes=[];var seen={};function visit(type){if(seen[type]){return}if(registeredTypes[type]){return}if(typeDependencies[type]){typeDependencies[type].forEach(visit);return}unboundTypes.push(type);seen[type]=true}types.forEach(visit);throw new UnboundTypeError(`${message}: `+unboundTypes.map(getTypeName).join([", "]))};var __embind_register_class=(rawType,rawPointerType,rawConstPointerType,baseClassRawType,getActualTypeSignature,getActualType,upcastSignature,upcast,downcastSignature,downcast,name,destructorSignature,rawDestructor)=>{name=AsciiToString(name);getActualType=embind__requireFunction(getActualTypeSignature,getActualType);upcast&&=embind__requireFunction(upcastSignature,upcast);downcast&&=embind__requireFunction(downcastSignature,downcast);rawDestructor=embind__requireFunction(destructorSignature,rawDestructor);var legalFunctionName=makeLegalFunctionName(name);exposePublicSymbol(legalFunctionName,function(){throwUnboundTypeError(`Cannot construct ${name} due to unbound types`,[baseClassRawType])});whenDependentTypesAreResolved([rawType,rawPointerType,rawConstPointerType],baseClassRawType?[baseClassRawType]:[],base=>{base=base[0];var baseClass;var basePrototype;if(baseClassRawType){baseClass=base.registeredClass;basePrototype=baseClass.instancePrototype}else{basePrototype=ClassHandle.prototype}var constructor=createNamedFunction(name,function(...args){if(Object.getPrototypeOf(this)!==instancePrototype){throw new BindingError(`Use 'new' to construct ${name}`)}if(undefined===registeredClass.constructor_body){throw new BindingError(`${name} has no accessible constructor`)}var body=registeredClass.constructor_body[args.length];if(undefined===body){throw new BindingError(`Tried to invoke ctor of ${name} with invalid number of parameters (${args.length}) - expected (${Object.keys(registeredClass.constructor_body).toString()}) parameters instead!`)}return body.apply(this,args)});var instancePrototype=Object.create(basePrototype,{constructor:{value:constructor}});constructor.prototype=instancePrototype;var registeredClass=new RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast);if(registeredClass.baseClass){registeredClass.baseClass.__derivedClasses??=[];registeredClass.baseClass.__derivedClasses.push(registeredClass)}var referenceConverter=new RegisteredPointer(name,registeredClass,true,false,false);var pointerConverter=new RegisteredPointer(name+"*",registeredClass,false,false,false);var constPointerConverter=new RegisteredPointer(name+" const*",registeredClass,false,true,false);registeredPointers[rawType]={pointerType:pointerConverter,constPointerType:constPointerConverter};replacePublicSymbol(legalFunctionName,constructor);return[referenceConverter,pointerConverter,constPointerConverter]})};function usesDestructorStack(argTypes){for(var i=1;i{var array=[];for(var i=0;i>2])}return array};var getFunctionName=signature=>{signature=signature.trim();const argsIndex=signature.indexOf("(");if(argsIndex===-1)return signature;return signature.slice(0,argsIndex)};var __embind_register_class_class_function=(rawClassType,methodName,argCount,rawArgTypesAddr,invokerSignature,rawInvoker,fn,isAsync,isNonnullReturn)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);methodName=AsciiToString(methodName);methodName=getFunctionName(methodName);rawInvoker=embind__requireFunction(invokerSignature,rawInvoker,isAsync);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`${classType.name}.${methodName}`;function unboundTypesHandler(){throwUnboundTypeError(`Cannot call ${humanName} due to unbound types`,rawArgTypes)}if(methodName.startsWith("@@")){methodName=Symbol[methodName.substring(2)]}var proto=classType.registeredClass.constructor;if(undefined===proto[methodName]){unboundTypesHandler.argCount=argCount-1;proto[methodName]=unboundTypesHandler}else{ensureOverloadTable(proto,methodName,humanName);proto[methodName].overloadTable[argCount-1]=unboundTypesHandler}whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{var invokerArgsArray=[argTypes[0],null].concat(argTypes.slice(1));var func=craftInvokerFunction(humanName,invokerArgsArray,null,rawInvoker,fn,isAsync);if(undefined===proto[methodName].overloadTable){func.argCount=argCount-1;proto[methodName]=func}else{proto[methodName].overloadTable[argCount-1]=func}if(classType.registeredClass.__derivedClasses){for(const derivedClass of classType.registeredClass.__derivedClasses){if(!derivedClass.constructor.hasOwnProperty(methodName)){derivedClass.constructor[methodName]=func}}}return[]});return[]})};var __embind_register_class_constructor=(rawClassType,argCount,rawArgTypesAddr,invokerSignature,invoker,rawConstructor)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);invoker=embind__requireFunction(invokerSignature,invoker);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`constructor ${classType.name}`;if(undefined===classType.registeredClass.constructor_body){classType.registeredClass.constructor_body=[]}if(undefined!==classType.registeredClass.constructor_body[argCount-1]){throw new BindingError(`Cannot register multiple constructors with identical number of parameters (${argCount-1}) for class '${classType.name}'! Overload resolution is currently only performed using the parameter count, not actual type info!`)}classType.registeredClass.constructor_body[argCount-1]=()=>{throwUnboundTypeError(`Cannot construct ${classType.name} due to unbound types`,rawArgTypes)};whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{argTypes.splice(1,0,null);classType.registeredClass.constructor_body[argCount-1]=craftInvokerFunction(humanName,argTypes,null,invoker,rawConstructor);return[]});return[]})};var __embind_register_class_function=(rawClassType,methodName,argCount,rawArgTypesAddr,invokerSignature,rawInvoker,context,isPureVirtual,isAsync,isNonnullReturn)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);methodName=AsciiToString(methodName);methodName=getFunctionName(methodName);rawInvoker=embind__requireFunction(invokerSignature,rawInvoker,isAsync);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`${classType.name}.${methodName}`;if(methodName.startsWith("@@")){methodName=Symbol[methodName.substring(2)]}if(isPureVirtual){classType.registeredClass.pureVirtualFunctions.push(methodName)}function unboundTypesHandler(){throwUnboundTypeError(`Cannot call ${humanName} due to unbound types`,rawArgTypes)}var proto=classType.registeredClass.instancePrototype;var method=proto[methodName];if(undefined===method||undefined===method.overloadTable&&method.className!==classType.name&&method.argCount===argCount-2){unboundTypesHandler.argCount=argCount-2;unboundTypesHandler.className=classType.name;proto[methodName]=unboundTypesHandler}else{ensureOverloadTable(proto,methodName,humanName);proto[methodName].overloadTable[argCount-2]=unboundTypesHandler}whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{var memberFunction=craftInvokerFunction(humanName,argTypes,classType,rawInvoker,context,isAsync);if(undefined===proto[methodName].overloadTable){memberFunction.argCount=argCount-2;proto[methodName]=memberFunction}else{proto[methodName].overloadTable[argCount-2]=memberFunction}return[]});return[]})};var validateThis=(this_,classType,humanName)=>{if(!(this_ instanceof Object)){throwBindingError(`${humanName} with invalid "this": ${this_}`)}if(!(this_ instanceof classType.registeredClass.constructor)){throwBindingError(`${humanName} incompatible with "this" of type ${this_.constructor.name}`)}if(!this_.$$.ptr){throwBindingError(`cannot call emscripten binding method ${humanName} on deleted object`)}return upcastPointer(this_.$$.ptr,this_.$$.ptrType.registeredClass,classType.registeredClass)};var __embind_register_class_property=(classType,fieldName,getterReturnType,getterSignature,getter,getterContext,setterArgumentType,setterSignature,setter,setterContext)=>{fieldName=AsciiToString(fieldName);getter=embind__requireFunction(getterSignature,getter);whenDependentTypesAreResolved([],[classType],classType=>{classType=classType[0];var humanName=`${classType.name}.${fieldName}`;var desc={get(){throwUnboundTypeError(`Cannot access ${humanName} due to unbound types`,[getterReturnType,setterArgumentType])},enumerable:true,configurable:true};if(setter){desc.set=()=>throwUnboundTypeError(`Cannot access ${humanName} due to unbound types`,[getterReturnType,setterArgumentType])}else{desc.set=v=>throwBindingError(humanName+" is a read-only property")}Object.defineProperty(classType.registeredClass.instancePrototype,fieldName,desc);whenDependentTypesAreResolved([],setter?[getterReturnType,setterArgumentType]:[getterReturnType],types=>{var getterReturnType=types[0];var desc={get(){var ptr=validateThis(this,classType,humanName+" getter");return getterReturnType.fromWireType(getter(getterContext,ptr))},enumerable:true};if(setter){setter=embind__requireFunction(setterSignature,setter);var setterArgumentType=types[1];desc.set=function(v){var ptr=validateThis(this,classType,humanName+" setter");var destructors=[];setter(setterContext,ptr,setterArgumentType.toWireType(destructors,v));runDestructors(destructors)}}Object.defineProperty(classType.registeredClass.instancePrototype,fieldName,desc);return[]});return[]})};var __embind_register_constant=(name,type,value)=>{name=AsciiToString(name);whenDependentTypesAreResolved([],[type],type=>{type=type[0];Module[name]=type.fromWireType(value);return[]})};var __emval_decref=handle=>{if(handle>9&&0===--emval_handles[handle+1]){emval_handles[handle]=undefined;emval_freelist.push(handle)}};var EmValType={name:"emscripten::val",fromWireType:handle=>{var rv=Emval.toValue(handle);__emval_decref(handle);return rv},toWireType:(destructors,value)=>Emval.toHandle(value),readValueFromPointer:readPointer,destructorFunction:null};var __embind_register_emval=rawType=>registerType(rawType,EmValType);var enumReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?function(pointer){return this.fromWireType(HEAP8[pointer])}:function(pointer){return this.fromWireType(HEAPU8[pointer])};case 2:return signed?function(pointer){return this.fromWireType(HEAP16[pointer>>1])}:function(pointer){return this.fromWireType(HEAPU16[pointer>>1])};case 4:return signed?function(pointer){return this.fromWireType(HEAP32[pointer>>2])}:function(pointer){return this.fromWireType(HEAPU32[pointer>>2])};default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};function getEnumValueType(rawValueType){return rawValueType===0?"object":rawValueType===1?"number":"string"}var __embind_register_enum=(rawType,name,size,isSigned,rawValueType)=>{name=AsciiToString(name);const valueType=getEnumValueType(rawValueType);switch(valueType){case"object":{function ctor(){}ctor.values={};registerType(rawType,{name,constructor:ctor,valueType,fromWireType:function(c){return this.constructor.values[c]},toWireType:(destructors,c)=>c.value,readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,ctor);break}case"number":{var keysMap={};registerType(rawType,{name,keysMap,valueType,fromWireType:c=>c,toWireType:(destructors,c)=>c,readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,keysMap);delete Module[name].argCount;break}case"string":{var valuesMap={};var reverseMap={};var keysMap={};registerType(rawType,{name,valuesMap,reverseMap,keysMap,valueType,fromWireType:function(c){return this.reverseMap[c]},toWireType:function(destructors,c){return this.valuesMap[c]},readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,keysMap);delete Module[name].argCount;break}}};var __embind_register_enum_value=(rawEnumType,name,enumValue)=>{var enumType=requireRegisteredType(rawEnumType,"enum");name=AsciiToString(name);switch(enumType.valueType){case"object":{var Enum=enumType.constructor;var Value=Object.create(enumType.constructor.prototype,{value:{value:enumValue},constructor:{value:createNamedFunction(`${enumType.name}_${name}`,function(){})}});Enum.values[enumValue]=Value;Enum[name]=Value;break}case"number":{enumType.keysMap[name]=enumValue;break}case"string":{enumType.valuesMap[name]=enumValue;enumType.reverseMap[enumValue]=name;enumType.keysMap[name]=name;break}}};var floatReadValueFromPointer=(name,width)=>{switch(width){case 4:return function(pointer){return this.fromWireType(HEAPF32[pointer>>2])};case 8:return function(pointer){return this.fromWireType(HEAPF64[pointer>>3])};default:throw new TypeError(`invalid float width (${width}): ${name}`)}};var __embind_register_float=(rawType,name,size)=>{name=AsciiToString(name);registerType(rawType,{name,fromWireType:value=>value,toWireType:(destructors,value)=>value,readValueFromPointer:floatReadValueFromPointer(name,size),destructorFunction:null})};var __embind_register_function=(name,argCount,rawArgTypesAddr,signature,rawInvoker,fn,isAsync,isNonnullReturn)=>{var argTypes=heap32VectorToArray(argCount,rawArgTypesAddr);name=AsciiToString(name);name=getFunctionName(name);rawInvoker=embind__requireFunction(signature,rawInvoker,isAsync);exposePublicSymbol(name,function(){throwUnboundTypeError(`Cannot call ${name} due to unbound types`,argTypes)},argCount-1);whenDependentTypesAreResolved([],argTypes,argTypes=>{var invokerArgsArray=[argTypes[0],null].concat(argTypes.slice(1));replacePublicSymbol(name,craftInvokerFunction(name,invokerArgsArray,null,rawInvoker,fn,isAsync),argCount-1);return[]})};var __embind_register_integer=(primitiveType,name,size,minRange,maxRange)=>{name=AsciiToString(name);const isUnsignedType=minRange===0;let fromWireType=value=>value;if(isUnsignedType){var bitshift=32-8*size;fromWireType=value=>value<>>bitshift;maxRange=fromWireType(maxRange)}registerType(primitiveType,{name,fromWireType,toWireType:(destructors,value)=>value,readValueFromPointer:integerReadValueFromPointer(name,size,minRange!==0),destructorFunction:null})};var installIndexedIterator=(proto,sizeMethodName,getMethodName)=>{const makeIterator=(size,getValue)=>{let index=0;return{next(){if(index>=size){return{done:true}}const current=index;index++;const value=getValue(current);return{value,done:false}},[Symbol.iterator](){return this}}};if(!proto[Symbol.iterator]){proto[Symbol.iterator]=function(){const size=this[sizeMethodName]();return makeIterator(size,i=>this[getMethodName](i))}}};var __embind_register_iterable=(rawClassType,rawElementType,sizeMethodName,getMethodName)=>{sizeMethodName=AsciiToString(sizeMethodName);getMethodName=AsciiToString(getMethodName);whenDependentTypesAreResolved([],[rawClassType,rawElementType],types=>{const classType=types[0];installIndexedIterator(classType.registeredClass.instancePrototype,sizeMethodName,getMethodName);return[]})};var __embind_register_memory_view=(rawType,dataTypeIndex,name)=>{var typeMapping=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array,BigInt64Array,BigUint64Array];var TA=typeMapping[dataTypeIndex];function decodeMemoryView(handle){var size=HEAPU32[handle>>2];var data=HEAPU32[handle+4>>2];return new TA(HEAP8.buffer,data,size)}name=AsciiToString(name);registerType(rawType,{name,fromWireType:decodeMemoryView,readValueFromPointer:decodeMemoryView},{ignoreDuplicateRegistrations:true})};var EmValOptionalType=Object.assign({optional:true},EmValType);var __embind_register_optional=(rawOptionalType,rawType)=>{registerType(rawOptionalType,EmValOptionalType)};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var __embind_register_std_string=(rawType,name)=>{name=AsciiToString(name);var stdStringIsUTF8=true;registerType(rawType,{name,fromWireType(value){var length=HEAPU32[value>>2];var payload=value+4;var str;if(stdStringIsUTF8){str=UTF8ToString(payload,length,true)}else{str="";for(var i=0;i>2]=length;if(valueIsOfTypeString){if(stdStringIsUTF8){stringToUTF8(value,ptr,length+1)}else{for(var i=0;i255){_free(base);throwBindingError("String has UTF-16 code units that do not fit in 8 bits")}HEAPU8[ptr+i]=charCode}}}else{HEAPU8.set(value,ptr)}if(destructors!==null){destructors.push(_free,base)}return base},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var UTF16Decoder=globalThis.TextDecoder?new TextDecoder("utf-16le"):undefined;var UTF16ToString=(ptr,maxBytesToRead,ignoreNul)=>{var idx=ptr>>1;var endIdx=findStringEnd(HEAPU16,idx,maxBytesToRead/2,ignoreNul);if(endIdx-idx>16&&UTF16Decoder)return UTF16Decoder.decode(HEAPU16.subarray(idx,endIdx));var str="";for(var i=idx;i{maxBytesToWrite??=2147483647;if(maxBytesToWrite<2)return 0;maxBytesToWrite-=2;var startPtr=outPtr;var numCharsToWrite=maxBytesToWrite>1]=codeUnit;outPtr+=2}HEAP16[outPtr>>1]=0;return outPtr-startPtr};var lengthBytesUTF16=str=>str.length*2;var UTF32ToString=(ptr,maxBytesToRead,ignoreNul)=>{var str="";var startIdx=ptr>>2;for(var i=0;!(i>=maxBytesToRead/4);i++){var utf32=HEAPU32[startIdx+i];if(!utf32&&!ignoreNul)break;str+=String.fromCodePoint(utf32)}return str};var stringToUTF32=(str,outPtr,maxBytesToWrite)=>{maxBytesToWrite??=2147483647;if(maxBytesToWrite<4)return 0;var startPtr=outPtr;var endPtr=startPtr+maxBytesToWrite-4;for(var i=0;i65535){i++}HEAP32[outPtr>>2]=codePoint;outPtr+=4;if(outPtr+4>endPtr)break}HEAP32[outPtr>>2]=0;return outPtr-startPtr};var lengthBytesUTF32=str=>{var len=0;for(var i=0;i65535){i++}len+=4}return len};var __embind_register_std_wstring=(rawType,charSize,name)=>{name=AsciiToString(name);var decodeString,encodeString,lengthBytesUTF;if(charSize===2){decodeString=UTF16ToString;encodeString=stringToUTF16;lengthBytesUTF=lengthBytesUTF16}else{decodeString=UTF32ToString;encodeString=stringToUTF32;lengthBytesUTF=lengthBytesUTF32}registerType(rawType,{name,fromWireType:value=>{var length=HEAPU32[value>>2];var str=decodeString(value+4,length*charSize,true);_free(value);return str},toWireType:(destructors,value)=>{if(!(typeof value=="string")){throwBindingError(`Cannot pass non-string to C++ string type ${name}`)}var length=lengthBytesUTF(value);var ptr=_malloc(4+length+charSize);HEAPU32[ptr>>2]=length/charSize;encodeString(value,ptr+4,length+charSize);if(destructors!==null){destructors.push(_free,ptr)}return ptr},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var __embind_register_value_object=(rawType,name,constructorSignature,rawConstructor,destructorSignature,rawDestructor)=>{structRegistrations[rawType]={name:AsciiToString(name),rawConstructor:embind__requireFunction(constructorSignature,rawConstructor),rawDestructor:embind__requireFunction(destructorSignature,rawDestructor),fields:[]}};var __embind_register_value_object_field=(structType,fieldName,getterReturnType,getterSignature,getter,getterContext,setterArgumentType,setterSignature,setter,setterContext)=>{structRegistrations[structType].fields.push({fieldName:AsciiToString(fieldName),getterReturnType,getter:embind__requireFunction(getterSignature,getter),getterContext,setterArgumentType,setter:embind__requireFunction(setterSignature,setter),setterContext})};var __embind_register_void=(rawType,name)=>{name=AsciiToString(name);registerType(rawType,{isVoid:true,name,fromWireType:()=>undefined,toWireType:(destructors,o)=>undefined})};var runtimeKeepaliveCounter=0;var __emscripten_runtime_keepalive_clear=()=>{noExitRuntime=false;runtimeKeepaliveCounter=0};var emval_methodCallers=[];var emval_addMethodCaller=caller=>{var id=emval_methodCallers.length;emval_methodCallers.push(caller);return id};var emval_lookupTypes=(argCount,argTypes)=>{var a=new Array(argCount);for(var i=0;i>2],`parameter ${i}`)}return a};var emval_returnValue=(toReturnWire,destructorsRef,handle)=>{var destructors=[];var result=toReturnWire(destructors,handle);if(destructors.length){HEAPU32[destructorsRef>>2]=Emval.toHandle(destructors)}return result};var emval_symbols={};var getStringOrSymbol=address=>{var symbol=emval_symbols[address];if(symbol===undefined){return AsciiToString(address)}return symbol};var __emval_create_invoker=(argCount,argTypesPtr,kind)=>{var GenericWireTypeSize=8;var[retType,...argTypes]=emval_lookupTypes(argCount,argTypesPtr);var toReturnWire=retType.toWireType.bind(retType);var argFromPtr=argTypes.map(type=>type.readValueFromPointer.bind(type));argCount--;var argN=new Array(argCount);var invokerFunction=(handle,methodName,destructorsRef,args)=>{var offset=0;for(var i=0;it.name)}) => ${retType.name}>`;return emval_addMethodCaller(createNamedFunction(functionName,invokerFunction))};var __emval_invoke=(caller,handle,methodName,destructorsRef,args)=>emval_methodCallers[caller](handle,methodName,destructorsRef,args);var __emval_new_cstring=v=>Emval.toHandle(getStringOrSymbol(v));var __emval_new_object=()=>Emval.toHandle({});var __emval_run_destructors=handle=>{var destructors=Emval.toValue(handle);runDestructors(destructors);__emval_decref(handle)};var __emval_set_property=(handle,key,value)=>{handle=Emval.toValue(handle);key=Emval.toValue(key);value=Emval.toValue(value);handle[key]=value};var timers={};var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;var maybeExit=()=>{if(!keepRuntimeAlive()){try{_exit(EXITSTATUS)}catch(e){handleException(e)}}};var callUserCallback=func=>{if(ABORT){return}try{return func()}catch(e){handleException(e)}finally{maybeExit()}};var _emscripten_get_now=()=>performance.now();var __setitimer_js=(which,timeout_ms)=>{if(timers[which]){clearTimeout(timers[which].id);delete timers[which]}if(!timeout_ms)return 0;var id=setTimeout(()=>{delete timers[which];callUserCallback(()=>__emscripten_timeout(which,_emscripten_get_now()))},timeout_ms);timers[which]={id,timeout_ms};return 0};var _emscripten_date_now=()=>Date.now();var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var printCharBuffers=[null,[],[]];var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(curr===0||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer));buffer.length=0}else{buffer.push(curr)}};var _fd_write=(fd,iov,iovcnt,pnum)=>{var num=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j>2]=num;return 0};init_ClassHandle();init_RegisteredPointer();{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}var ___getTypeName,_free,_malloc,__emscripten_timeout,memory,__indirect_function_table,wasmMemory,wasmTable;function assignWasmExports(wasmExports){___getTypeName=wasmExports["Q"];_free=Module["_free"]=wasmExports["R"];_malloc=Module["_malloc"]=wasmExports["S"];__emscripten_timeout=wasmExports["U"];memory=wasmMemory=wasmExports["O"];__indirect_function_table=wasmTable=wasmExports["T"]}var wasmImports={J:__abort_js,x:__embind_create_inheriting_constructor,s:__embind_finalize_value_object,z:__embind_register_bigint,A:__embind_register_bool,b:__embind_register_class,p:__embind_register_class_class_function,e:__embind_register_class_constructor,a:__embind_register_class_function,d:__embind_register_class_property,K:__embind_register_constant,M:__embind_register_emval,l:__embind_register_enum,f:__embind_register_enum_value,y:__embind_register_float,g:__embind_register_function,o:__embind_register_integer,q:__embind_register_iterable,h:__embind_register_memory_view,r:__embind_register_optional,N:__embind_register_std_string,u:__embind_register_std_wstring,t:__embind_register_value_object,m:__embind_register_value_object_field,B:__embind_register_void,D:__emscripten_runtime_keepalive_clear,k:__emval_create_invoker,n:__emval_decref,j:__emval_invoke,v:__emval_new_cstring,L:__emval_new_object,i:__emval_run_destructors,w:__emval_set_property,E:__setitimer_js,I:_emscripten_date_now,c:_emscripten_get_now,F:_emscripten_resize_heap,G:_exit,H:_fd_write,C:_proc_exit};function run(){preRun();function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} ;return moduleRtn}})();if(typeof exports==="object"&&typeof module==="object"){module.exports=PHYSX;module.exports.default=PHYSX}else if(typeof define==="function"&&define["amd"])define([],()=>PHYSX); diff --git a/packages/physics-physx/libs/physx.release.simd.js b/packages/physics-physx/libs/physx.release.simd.js index bdb1d70990..45843d369b 100644 --- a/packages/physics-physx/libs/physx.release.simd.js +++ b/packages/physics-physx/libs/physx.release.simd.js @@ -1,2 +1,2 @@ -var PHYSX=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};if(typeof __filename!="undefined"){_scriptName=__filename}else if(ENVIRONMENT_IS_WORKER){_scriptName=self.location.href}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{if(isFileURI(url)){return new Promise((resolve,reject)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){resolve(xhr.response);return}reject(xhr.status)};xhr.onerror=reject;xhr.send(null)})}var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var EXITSTATUS;var isFileURI=filename=>filename.startsWith("file://");var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["P"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){return "https://mdn.alipayobjects.com/rms/afts/file/A*6vi1SJaSJ6IAAAAAgDAAAAgAehQnAQ/physx.release.simd.wasm"}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!isFileURI(binaryFile)&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var __abort_js=()=>abort("");var createNamedFunction=(name,func)=>Object.defineProperty(func,"name",{value:name});var emval_freelist=[];var emval_handles=[0,1,,1,null,1,true,1,false,1];var BindingError=class BindingError extends Error{constructor(message){super(message);this.name="BindingError"}};var throwBindingError=message=>{throw new BindingError(message)};var Emval={toValue:handle=>{if(!handle){throwBindingError(`Cannot use deleted val. handle = ${handle}`)}return emval_handles[handle]},toHandle:value=>{switch(value){case undefined:return 2;case null:return 4;case true:return 6;case false:return 8;default:{const handle=emval_freelist.pop()||emval_handles.length;emval_handles[handle]=value;emval_handles[handle+1]=1;return handle}}}};class PureVirtualError extends Error{}var AsciiToString=ptr=>{var str="";while(1){var ch=HEAPU8[ptr++];if(!ch)return str;str+=String.fromCharCode(ch)}};var registeredInstances={};var getBasestPointer=(class_,ptr)=>{if(ptr===undefined){throwBindingError("ptr should not be undefined")}while(class_.baseClass){ptr=class_.upcast(ptr);class_=class_.baseClass}return ptr};var registerInheritedInstance=(class_,ptr,instance)=>{ptr=getBasestPointer(class_,ptr);if(registeredInstances.hasOwnProperty(ptr)){throwBindingError(`Tried to register registered instance: ${ptr}`)}else{registeredInstances[ptr]=instance}};var registeredTypes={};var getTypeName=type=>{var ptr=___getTypeName(type);var rv=AsciiToString(ptr);_free(ptr);return rv};var requireRegisteredType=(rawType,humanName)=>{var impl=registeredTypes[rawType];if(undefined===impl){throwBindingError(`${humanName} has unknown type ${getTypeName(rawType)}`)}return impl};var unregisterInheritedInstance=(class_,ptr)=>{ptr=getBasestPointer(class_,ptr);if(registeredInstances.hasOwnProperty(ptr)){delete registeredInstances[ptr]}else{throwBindingError(`Tried to unregister unregistered instance: ${ptr}`)}};var detachFinalizer=handle=>{};var finalizationRegistry=false;var runDestructor=$$=>{if($$.smartPtr){$$.smartPtrType.rawDestructor($$.smartPtr)}else{$$.ptrType.registeredClass.rawDestructor($$.ptr)}};var releaseClassHandle=$$=>{$$.count.value-=1;var toDelete=0===$$.count.value;if(toDelete){runDestructor($$)}};var attachFinalizer=handle=>{if(!globalThis.FinalizationRegistry){attachFinalizer=handle=>handle;return handle}finalizationRegistry=new FinalizationRegistry(info=>{releaseClassHandle(info.$$)});attachFinalizer=handle=>{var $$=handle.$$;var hasSmartPtr=!!$$.smartPtr;if(hasSmartPtr){var info={$$};finalizationRegistry.register(handle,info,handle)}return handle};detachFinalizer=handle=>finalizationRegistry.unregister(handle);return attachFinalizer(handle)};var __embind_create_inheriting_constructor=(constructorName,wrapperType,properties)=>{constructorName=AsciiToString(constructorName);wrapperType=requireRegisteredType(wrapperType,"wrapper");properties=Emval.toValue(properties);var registeredClass=wrapperType.registeredClass;var wrapperPrototype=registeredClass.instancePrototype;var baseClass=registeredClass.baseClass;var baseClassPrototype=baseClass.instancePrototype;var baseConstructor=registeredClass.baseClass.constructor;var ctor=createNamedFunction(constructorName,function(...args){for(var name of registeredClass.baseClass.pureVirtualFunctions){if(this[name]===baseClassPrototype[name]){throw new PureVirtualError(`Pure virtual function ${name} must be implemented in JavaScript`)}}Object.defineProperty(this,"__parent",{value:wrapperPrototype});this["__construct"](...args)});wrapperPrototype["__construct"]=function __construct(...args){if(this===wrapperPrototype){throwBindingError("Pass correct 'this' to __construct")}var inner=baseConstructor["implement"](this,...args);detachFinalizer(inner);var $$=inner.$$;inner["notifyOnDestruction"]();$$.preservePointerOnDelete=true;Object.defineProperties(this,{$$:{value:$$}});attachFinalizer(this);registerInheritedInstance(registeredClass,$$.ptr,this)};wrapperPrototype["__destruct"]=function __destruct(){if(this===wrapperPrototype){throwBindingError("Pass correct 'this' to __destruct")}detachFinalizer(this);unregisterInheritedInstance(registeredClass,this.$$.ptr)};ctor.prototype=Object.create(wrapperPrototype);Object.assign(ctor.prototype,properties);return Emval.toHandle(ctor)};var structRegistrations={};var runDestructors=destructors=>{while(destructors.length){var ptr=destructors.pop();var del=destructors.pop();del(ptr)}};function readPointer(pointer){return this.fromWireType(HEAPU32[pointer>>2])}var awaitingDependencies={};var typeDependencies={};var InternalError=class InternalError extends Error{constructor(message){super(message);this.name="InternalError"}};var throwInternalError=message=>{throw new InternalError(message)};var whenDependentTypesAreResolved=(myTypes,dependentTypes,getTypeConverters)=>{myTypes.forEach(type=>typeDependencies[type]=dependentTypes);function onComplete(typeConverters){var myTypeConverters=getTypeConverters(typeConverters);if(myTypeConverters.length!==myTypes.length){throwInternalError("Mismatched type converter count")}for(var i=0;i{typeConverters[i]=registeredTypes[dt];++registered;if(registered===unregisteredTypes.length){onComplete(typeConverters)}})}}if(0===unregisteredTypes.length){onComplete(typeConverters)}};var __embind_finalize_value_object=structType=>{var reg=structRegistrations[structType];delete structRegistrations[structType];var rawConstructor=reg.rawConstructor;var rawDestructor=reg.rawDestructor;var fieldRecords=reg.fields;var fieldTypes=fieldRecords.map(field=>field.getterReturnType).concat(fieldRecords.map(field=>field.setterArgumentType));whenDependentTypesAreResolved([structType],fieldTypes,fieldTypes=>{var fields={};for(var[i,field]of fieldRecords.entries()){const getterReturnType=fieldTypes[i];const getter=field.getter;const getterContext=field.getterContext;const setterArgumentType=fieldTypes[i+fieldRecords.length];const setter=field.setter;const setterContext=field.setterContext;fields[field.fieldName]={read:ptr=>getterReturnType.fromWireType(getter(getterContext,ptr)),write:(ptr,o)=>{var destructors=[];setter(setterContext,ptr,setterArgumentType.toWireType(destructors,o));runDestructors(destructors)},optional:getterReturnType.optional}}return[{name:reg.name,fromWireType:ptr=>{var rv={};for(var i in fields){rv[i]=fields[i].read(ptr)}rawDestructor(ptr);return rv},toWireType:(destructors,o)=>{for(var fieldName in fields){if(!(fieldName in o)&&!fields[fieldName].optional){throw new TypeError(`Missing field: "${fieldName}"`)}}var ptr=rawConstructor();for(fieldName in fields){fields[fieldName].write(ptr,o[fieldName])}if(destructors!==null){destructors.push(rawDestructor,ptr)}return ptr},readValueFromPointer:readPointer,destructorFunction:rawDestructor}]})};function sharedRegisterType(rawType,registeredInstance,options={}){var name=registeredInstance.name;if(!rawType){throwBindingError(`type "${name}" must have a positive integer typeid pointer`)}if(registeredTypes.hasOwnProperty(rawType)){if(options.ignoreDuplicateRegistrations){return}else{throwBindingError(`Cannot register type '${name}' twice`)}}registeredTypes[rawType]=registeredInstance;delete typeDependencies[rawType];if(awaitingDependencies.hasOwnProperty(rawType)){var callbacks=awaitingDependencies[rawType];delete awaitingDependencies[rawType];callbacks.forEach(cb=>cb())}}function registerType(rawType,registeredInstance,options={}){return sharedRegisterType(rawType,registeredInstance,options)}var integerReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?pointer=>HEAP8[pointer]:pointer=>HEAPU8[pointer];case 2:return signed?pointer=>HEAP16[pointer>>1]:pointer=>HEAPU16[pointer>>1];case 4:return signed?pointer=>HEAP32[pointer>>2]:pointer=>HEAPU32[pointer>>2];case 8:return signed?pointer=>HEAP64[pointer>>3]:pointer=>HEAPU64[pointer>>3];default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};var __embind_register_bigint=(primitiveType,name,size,minRange,maxRange)=>{name=AsciiToString(name);const isUnsignedType=minRange===0n;let fromWireType=value=>value;if(isUnsignedType){const bitSize=size*8;fromWireType=value=>BigInt.asUintN(bitSize,value);maxRange=fromWireType(maxRange)}registerType(primitiveType,{name,fromWireType,toWireType:(destructors,value)=>{if(typeof value=="number"){value=BigInt(value)}return value},readValueFromPointer:integerReadValueFromPointer(name,size,!isUnsignedType),destructorFunction:null})};var __embind_register_bool=(rawType,name,trueValue,falseValue)=>{name=AsciiToString(name);registerType(rawType,{name,fromWireType:function(wt){return!!wt},toWireType:function(destructors,o){return o?trueValue:falseValue},readValueFromPointer:function(pointer){return this.fromWireType(HEAPU8[pointer])},destructorFunction:null})};var shallowCopyInternalPointer=o=>({count:o.count,deleteScheduled:o.deleteScheduled,preservePointerOnDelete:o.preservePointerOnDelete,ptr:o.ptr,ptrType:o.ptrType,smartPtr:o.smartPtr,smartPtrType:o.smartPtrType});var throwInstanceAlreadyDeleted=obj=>{function getInstanceTypeName(handle){return handle.$$.ptrType.registeredClass.name}throwBindingError(getInstanceTypeName(obj)+" instance already deleted")};var deletionQueue=[];var flushPendingDeletes=()=>{while(deletionQueue.length){var obj=deletionQueue.pop();obj.$$.deleteScheduled=false;obj["delete"]()}};var delayFunction;var init_ClassHandle=()=>{let proto=ClassHandle.prototype;Object.assign(proto,{isAliasOf(other){if(!(this instanceof ClassHandle)){return false}if(!(other instanceof ClassHandle)){return false}var leftClass=this.$$.ptrType.registeredClass;var left=this.$$.ptr;other.$$=other.$$;var rightClass=other.$$.ptrType.registeredClass;var right=other.$$.ptr;while(leftClass.baseClass){left=leftClass.upcast(left);leftClass=leftClass.baseClass}while(rightClass.baseClass){right=rightClass.upcast(right);rightClass=rightClass.baseClass}return leftClass===rightClass&&left===right},clone(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.preservePointerOnDelete){this.$$.count.value+=1;return this}else{var clone=attachFinalizer(Object.create(Object.getPrototypeOf(this),{$$:{value:shallowCopyInternalPointer(this.$$)}}));clone.$$.count.value+=1;clone.$$.deleteScheduled=false;return clone}},delete(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}detachFinalizer(this);releaseClassHandle(this.$$);if(!this.$$.preservePointerOnDelete){this.$$.smartPtr=undefined;this.$$.ptr=undefined}},isDeleted(){return!this.$$.ptr},deleteLater(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}deletionQueue.push(this);if(deletionQueue.length===1&&delayFunction){delayFunction(flushPendingDeletes)}this.$$.deleteScheduled=true;return this}});const symbolDispose=Symbol.dispose;if(symbolDispose){proto[symbolDispose]=proto["delete"]}};function ClassHandle(){}var registeredPointers={};var ensureOverloadTable=(proto,methodName,humanName)=>{if(undefined===proto[methodName].overloadTable){var prevFunc=proto[methodName];proto[methodName]=function(...args){if(!proto[methodName].overloadTable.hasOwnProperty(args.length)){throwBindingError(`Function '${humanName}' called with an invalid number of arguments (${args.length}) - expects one of (${proto[methodName].overloadTable})!`)}return proto[methodName].overloadTable[args.length].apply(this,args)};proto[methodName].overloadTable=[];proto[methodName].overloadTable[prevFunc.argCount]=prevFunc}};var exposePublicSymbol=(name,value,numArguments)=>{if(Module.hasOwnProperty(name)){if(undefined===numArguments||undefined!==Module[name].overloadTable&&undefined!==Module[name].overloadTable[numArguments]){throwBindingError(`Cannot register public name '${name}' twice`)}ensureOverloadTable(Module,name,name);if(Module[name].overloadTable.hasOwnProperty(numArguments)){throwBindingError(`Cannot register multiple overloads of a function with the same number of arguments (${numArguments})!`)}Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}};var char_0=48;var char_9=57;var makeLegalFunctionName=name=>{name=name.replace(/[^a-zA-Z0-9_]/g,"$");var f=name.charCodeAt(0);if(f>=char_0&&f<=char_9){return`_${name}`}return name};function RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast){this.name=name;this.constructor=constructor;this.instancePrototype=instancePrototype;this.rawDestructor=rawDestructor;this.baseClass=baseClass;this.getActualType=getActualType;this.upcast=upcast;this.downcast=downcast;this.pureVirtualFunctions=[]}var upcastPointer=(ptr,ptrClass,desiredClass)=>{while(ptrClass!==desiredClass){if(!ptrClass.upcast){throwBindingError(`Expected null or instance of ${desiredClass.name}, got an instance of ${ptrClass.name}`)}ptr=ptrClass.upcast(ptr);ptrClass=ptrClass.baseClass}return ptr};var embindRepr=v=>{if(v===null){return"null"}var t=typeof v;if(t==="object"||t==="array"||t==="function"){return v.toString()}else{return""+v}};function constNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}function genericPointerToWireType(destructors,handle){var ptr;if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}if(this.isSmartPointer){ptr=this.rawConstructor();if(destructors!==null){destructors.push(this.rawDestructor,ptr)}return ptr}else{return 0}}if(!handle||!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(!this.isConst&&handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);if(this.isSmartPointer){if(undefined===handle.$$.smartPtr){throwBindingError("Passing raw pointer to smart pointer is illegal")}switch(this.sharingPolicy){case 0:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}break;case 1:ptr=handle.$$.smartPtr;break;case 2:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{var clonedHandle=handle["clone"]();ptr=this.rawShare(ptr,Emval.toHandle(()=>clonedHandle["delete"]()));if(destructors!==null){destructors.push(this.rawDestructor,ptr)}}break;default:throwBindingError("Unsupported sharing policy")}}return ptr}function nonConstNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}var downcastPointer=(ptr,ptrClass,desiredClass)=>{if(ptrClass===desiredClass){return ptr}if(undefined===desiredClass.baseClass){return null}var rv=downcastPointer(ptr,ptrClass,desiredClass.baseClass);if(rv===null){return null}return desiredClass.downcast(rv)};var getInheritedInstance=(class_,ptr)=>{ptr=getBasestPointer(class_,ptr);return registeredInstances[ptr]};var makeClassHandle=(prototype,record)=>{if(!record.ptrType||!record.ptr){throwInternalError("makeClassHandle requires ptr and ptrType")}var hasSmartPtrType=!!record.smartPtrType;var hasSmartPtr=!!record.smartPtr;if(hasSmartPtrType!==hasSmartPtr){throwInternalError("Both smartPtrType and smartPtr must be specified")}record.count={value:1};return attachFinalizer(Object.create(prototype,{$$:{value:record,writable:true}}))};function RegisteredPointer_fromWireType(ptr){var rawPointer=this.getPointee(ptr);if(!rawPointer){this.destructor(ptr);return null}var registeredInstance=getInheritedInstance(this.registeredClass,rawPointer);if(undefined!==registeredInstance){if(0===registeredInstance.$$.count.value){registeredInstance.$$.ptr=rawPointer;registeredInstance.$$.smartPtr=ptr;return registeredInstance["clone"]()}else{var rv=registeredInstance["clone"]();this.destructor(ptr);return rv}}function makeDefaultHandle(){if(this.isSmartPointer){return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this.pointeeType,ptr:rawPointer,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this,ptr})}}var actualType=this.registeredClass.getActualType(rawPointer);var registeredPointerRecord=registeredPointers[actualType];if(!registeredPointerRecord){return makeDefaultHandle.call(this)}var toType;if(this.isConst){toType=registeredPointerRecord.constPointerType}else{toType=registeredPointerRecord.pointerType}var dp=downcastPointer(rawPointer,this.registeredClass,toType.registeredClass);if(dp===null){return makeDefaultHandle.call(this)}if(this.isSmartPointer){return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp})}}var init_RegisteredPointer=()=>{Object.assign(RegisteredPointer.prototype,{getPointee(ptr){if(this.rawGetPointee){ptr=this.rawGetPointee(ptr)}return ptr},destructor(ptr){this.rawDestructor?.(ptr)},readValueFromPointer:readPointer,fromWireType:RegisteredPointer_fromWireType})};function RegisteredPointer(name,registeredClass,isReference,isConst,isSmartPointer,pointeeType,sharingPolicy,rawGetPointee,rawConstructor,rawShare,rawDestructor){this.name=name;this.registeredClass=registeredClass;this.isReference=isReference;this.isConst=isConst;this.isSmartPointer=isSmartPointer;this.pointeeType=pointeeType;this.sharingPolicy=sharingPolicy;this.rawGetPointee=rawGetPointee;this.rawConstructor=rawConstructor;this.rawShare=rawShare;this.rawDestructor=rawDestructor;if(!isSmartPointer&®isteredClass.baseClass===undefined){if(isConst){this.toWireType=constNoSmartPtrRawPointerToWireType;this.destructorFunction=null}else{this.toWireType=nonConstNoSmartPtrRawPointerToWireType;this.destructorFunction=null}}else{this.toWireType=genericPointerToWireType}}var replacePublicSymbol=(name,value,numArguments)=>{if(!Module.hasOwnProperty(name)){throwInternalError("Replacing nonexistent public symbol")}if(undefined!==Module[name].overloadTable&&undefined!==numArguments){Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}};var wasmTableMirror=[];var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var embind__requireFunction=(signature,rawFunction,isAsync=false)=>{signature=AsciiToString(signature);function makeDynCaller(){var rtn=getWasmTableEntry(rawFunction);return rtn}var fp=makeDynCaller();if(typeof fp!="function"){throwBindingError(`unknown function pointer with signature ${signature}: ${rawFunction}`)}return fp};class UnboundTypeError extends Error{}var throwUnboundTypeError=(message,types)=>{var unboundTypes=[];var seen={};function visit(type){if(seen[type]){return}if(registeredTypes[type]){return}if(typeDependencies[type]){typeDependencies[type].forEach(visit);return}unboundTypes.push(type);seen[type]=true}types.forEach(visit);throw new UnboundTypeError(`${message}: `+unboundTypes.map(getTypeName).join([", "]))};var __embind_register_class=(rawType,rawPointerType,rawConstPointerType,baseClassRawType,getActualTypeSignature,getActualType,upcastSignature,upcast,downcastSignature,downcast,name,destructorSignature,rawDestructor)=>{name=AsciiToString(name);getActualType=embind__requireFunction(getActualTypeSignature,getActualType);upcast&&=embind__requireFunction(upcastSignature,upcast);downcast&&=embind__requireFunction(downcastSignature,downcast);rawDestructor=embind__requireFunction(destructorSignature,rawDestructor);var legalFunctionName=makeLegalFunctionName(name);exposePublicSymbol(legalFunctionName,function(){throwUnboundTypeError(`Cannot construct ${name} due to unbound types`,[baseClassRawType])});whenDependentTypesAreResolved([rawType,rawPointerType,rawConstPointerType],baseClassRawType?[baseClassRawType]:[],base=>{base=base[0];var baseClass;var basePrototype;if(baseClassRawType){baseClass=base.registeredClass;basePrototype=baseClass.instancePrototype}else{basePrototype=ClassHandle.prototype}var constructor=createNamedFunction(name,function(...args){if(Object.getPrototypeOf(this)!==instancePrototype){throw new BindingError(`Use 'new' to construct ${name}`)}if(undefined===registeredClass.constructor_body){throw new BindingError(`${name} has no accessible constructor`)}var body=registeredClass.constructor_body[args.length];if(undefined===body){throw new BindingError(`Tried to invoke ctor of ${name} with invalid number of parameters (${args.length}) - expected (${Object.keys(registeredClass.constructor_body).toString()}) parameters instead!`)}return body.apply(this,args)});var instancePrototype=Object.create(basePrototype,{constructor:{value:constructor}});constructor.prototype=instancePrototype;var registeredClass=new RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast);if(registeredClass.baseClass){registeredClass.baseClass.__derivedClasses??=[];registeredClass.baseClass.__derivedClasses.push(registeredClass)}var referenceConverter=new RegisteredPointer(name,registeredClass,true,false,false);var pointerConverter=new RegisteredPointer(name+"*",registeredClass,false,false,false);var constPointerConverter=new RegisteredPointer(name+" const*",registeredClass,false,true,false);registeredPointers[rawType]={pointerType:pointerConverter,constPointerType:constPointerConverter};replacePublicSymbol(legalFunctionName,constructor);return[referenceConverter,pointerConverter,constPointerConverter]})};function usesDestructorStack(argTypes){for(var i=1;i{var array=[];for(var i=0;i>2])}return array};var getFunctionName=signature=>{signature=signature.trim();const argsIndex=signature.indexOf("(");if(argsIndex===-1)return signature;return signature.slice(0,argsIndex)};var __embind_register_class_class_function=(rawClassType,methodName,argCount,rawArgTypesAddr,invokerSignature,rawInvoker,fn,isAsync,isNonnullReturn)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);methodName=AsciiToString(methodName);methodName=getFunctionName(methodName);rawInvoker=embind__requireFunction(invokerSignature,rawInvoker,isAsync);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`${classType.name}.${methodName}`;function unboundTypesHandler(){throwUnboundTypeError(`Cannot call ${humanName} due to unbound types`,rawArgTypes)}if(methodName.startsWith("@@")){methodName=Symbol[methodName.substring(2)]}var proto=classType.registeredClass.constructor;if(undefined===proto[methodName]){unboundTypesHandler.argCount=argCount-1;proto[methodName]=unboundTypesHandler}else{ensureOverloadTable(proto,methodName,humanName);proto[methodName].overloadTable[argCount-1]=unboundTypesHandler}whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{var invokerArgsArray=[argTypes[0],null].concat(argTypes.slice(1));var func=craftInvokerFunction(humanName,invokerArgsArray,null,rawInvoker,fn,isAsync);if(undefined===proto[methodName].overloadTable){func.argCount=argCount-1;proto[methodName]=func}else{proto[methodName].overloadTable[argCount-1]=func}if(classType.registeredClass.__derivedClasses){for(const derivedClass of classType.registeredClass.__derivedClasses){if(!derivedClass.constructor.hasOwnProperty(methodName)){derivedClass.constructor[methodName]=func}}}return[]});return[]})};var __embind_register_class_constructor=(rawClassType,argCount,rawArgTypesAddr,invokerSignature,invoker,rawConstructor)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);invoker=embind__requireFunction(invokerSignature,invoker);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`constructor ${classType.name}`;if(undefined===classType.registeredClass.constructor_body){classType.registeredClass.constructor_body=[]}if(undefined!==classType.registeredClass.constructor_body[argCount-1]){throw new BindingError(`Cannot register multiple constructors with identical number of parameters (${argCount-1}) for class '${classType.name}'! Overload resolution is currently only performed using the parameter count, not actual type info!`)}classType.registeredClass.constructor_body[argCount-1]=()=>{throwUnboundTypeError(`Cannot construct ${classType.name} due to unbound types`,rawArgTypes)};whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{argTypes.splice(1,0,null);classType.registeredClass.constructor_body[argCount-1]=craftInvokerFunction(humanName,argTypes,null,invoker,rawConstructor);return[]});return[]})};var __embind_register_class_function=(rawClassType,methodName,argCount,rawArgTypesAddr,invokerSignature,rawInvoker,context,isPureVirtual,isAsync,isNonnullReturn)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);methodName=AsciiToString(methodName);methodName=getFunctionName(methodName);rawInvoker=embind__requireFunction(invokerSignature,rawInvoker,isAsync);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`${classType.name}.${methodName}`;if(methodName.startsWith("@@")){methodName=Symbol[methodName.substring(2)]}if(isPureVirtual){classType.registeredClass.pureVirtualFunctions.push(methodName)}function unboundTypesHandler(){throwUnboundTypeError(`Cannot call ${humanName} due to unbound types`,rawArgTypes)}var proto=classType.registeredClass.instancePrototype;var method=proto[methodName];if(undefined===method||undefined===method.overloadTable&&method.className!==classType.name&&method.argCount===argCount-2){unboundTypesHandler.argCount=argCount-2;unboundTypesHandler.className=classType.name;proto[methodName]=unboundTypesHandler}else{ensureOverloadTable(proto,methodName,humanName);proto[methodName].overloadTable[argCount-2]=unboundTypesHandler}whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{var memberFunction=craftInvokerFunction(humanName,argTypes,classType,rawInvoker,context,isAsync);if(undefined===proto[methodName].overloadTable){memberFunction.argCount=argCount-2;proto[methodName]=memberFunction}else{proto[methodName].overloadTable[argCount-2]=memberFunction}return[]});return[]})};var validateThis=(this_,classType,humanName)=>{if(!(this_ instanceof Object)){throwBindingError(`${humanName} with invalid "this": ${this_}`)}if(!(this_ instanceof classType.registeredClass.constructor)){throwBindingError(`${humanName} incompatible with "this" of type ${this_.constructor.name}`)}if(!this_.$$.ptr){throwBindingError(`cannot call emscripten binding method ${humanName} on deleted object`)}return upcastPointer(this_.$$.ptr,this_.$$.ptrType.registeredClass,classType.registeredClass)};var __embind_register_class_property=(classType,fieldName,getterReturnType,getterSignature,getter,getterContext,setterArgumentType,setterSignature,setter,setterContext)=>{fieldName=AsciiToString(fieldName);getter=embind__requireFunction(getterSignature,getter);whenDependentTypesAreResolved([],[classType],classType=>{classType=classType[0];var humanName=`${classType.name}.${fieldName}`;var desc={get(){throwUnboundTypeError(`Cannot access ${humanName} due to unbound types`,[getterReturnType,setterArgumentType])},enumerable:true,configurable:true};if(setter){desc.set=()=>throwUnboundTypeError(`Cannot access ${humanName} due to unbound types`,[getterReturnType,setterArgumentType])}else{desc.set=v=>throwBindingError(humanName+" is a read-only property")}Object.defineProperty(classType.registeredClass.instancePrototype,fieldName,desc);whenDependentTypesAreResolved([],setter?[getterReturnType,setterArgumentType]:[getterReturnType],types=>{var getterReturnType=types[0];var desc={get(){var ptr=validateThis(this,classType,humanName+" getter");return getterReturnType.fromWireType(getter(getterContext,ptr))},enumerable:true};if(setter){setter=embind__requireFunction(setterSignature,setter);var setterArgumentType=types[1];desc.set=function(v){var ptr=validateThis(this,classType,humanName+" setter");var destructors=[];setter(setterContext,ptr,setterArgumentType.toWireType(destructors,v));runDestructors(destructors)}}Object.defineProperty(classType.registeredClass.instancePrototype,fieldName,desc);return[]});return[]})};var __embind_register_constant=(name,type,value)=>{name=AsciiToString(name);whenDependentTypesAreResolved([],[type],type=>{type=type[0];Module[name]=type.fromWireType(value);return[]})};var __emval_decref=handle=>{if(handle>9&&0===--emval_handles[handle+1]){emval_handles[handle]=undefined;emval_freelist.push(handle)}};var EmValType={name:"emscripten::val",fromWireType:handle=>{var rv=Emval.toValue(handle);__emval_decref(handle);return rv},toWireType:(destructors,value)=>Emval.toHandle(value),readValueFromPointer:readPointer,destructorFunction:null};var __embind_register_emval=rawType=>registerType(rawType,EmValType);var enumReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?function(pointer){return this.fromWireType(HEAP8[pointer])}:function(pointer){return this.fromWireType(HEAPU8[pointer])};case 2:return signed?function(pointer){return this.fromWireType(HEAP16[pointer>>1])}:function(pointer){return this.fromWireType(HEAPU16[pointer>>1])};case 4:return signed?function(pointer){return this.fromWireType(HEAP32[pointer>>2])}:function(pointer){return this.fromWireType(HEAPU32[pointer>>2])};default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};function getEnumValueType(rawValueType){return rawValueType===0?"object":rawValueType===1?"number":"string"}var __embind_register_enum=(rawType,name,size,isSigned,rawValueType)=>{name=AsciiToString(name);const valueType=getEnumValueType(rawValueType);switch(valueType){case"object":{function ctor(){}ctor.values={};registerType(rawType,{name,constructor:ctor,valueType,fromWireType:function(c){return this.constructor.values[c]},toWireType:(destructors,c)=>c.value,readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,ctor);break}case"number":{var keysMap={};registerType(rawType,{name,keysMap,valueType,fromWireType:c=>c,toWireType:(destructors,c)=>c,readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,keysMap);delete Module[name].argCount;break}case"string":{var valuesMap={};var reverseMap={};var keysMap={};registerType(rawType,{name,valuesMap,reverseMap,keysMap,valueType,fromWireType:function(c){return this.reverseMap[c]},toWireType:function(destructors,c){return this.valuesMap[c]},readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,keysMap);delete Module[name].argCount;break}}};var __embind_register_enum_value=(rawEnumType,name,enumValue)=>{var enumType=requireRegisteredType(rawEnumType,"enum");name=AsciiToString(name);switch(enumType.valueType){case"object":{var Enum=enumType.constructor;var Value=Object.create(enumType.constructor.prototype,{value:{value:enumValue},constructor:{value:createNamedFunction(`${enumType.name}_${name}`,function(){})}});Enum.values[enumValue]=Value;Enum[name]=Value;break}case"number":{enumType.keysMap[name]=enumValue;break}case"string":{enumType.valuesMap[name]=enumValue;enumType.reverseMap[enumValue]=name;enumType.keysMap[name]=name;break}}};var floatReadValueFromPointer=(name,width)=>{switch(width){case 4:return function(pointer){return this.fromWireType(HEAPF32[pointer>>2])};case 8:return function(pointer){return this.fromWireType(HEAPF64[pointer>>3])};default:throw new TypeError(`invalid float width (${width}): ${name}`)}};var __embind_register_float=(rawType,name,size)=>{name=AsciiToString(name);registerType(rawType,{name,fromWireType:value=>value,toWireType:(destructors,value)=>value,readValueFromPointer:floatReadValueFromPointer(name,size),destructorFunction:null})};var __embind_register_function=(name,argCount,rawArgTypesAddr,signature,rawInvoker,fn,isAsync,isNonnullReturn)=>{var argTypes=heap32VectorToArray(argCount,rawArgTypesAddr);name=AsciiToString(name);name=getFunctionName(name);rawInvoker=embind__requireFunction(signature,rawInvoker,isAsync);exposePublicSymbol(name,function(){throwUnboundTypeError(`Cannot call ${name} due to unbound types`,argTypes)},argCount-1);whenDependentTypesAreResolved([],argTypes,argTypes=>{var invokerArgsArray=[argTypes[0],null].concat(argTypes.slice(1));replacePublicSymbol(name,craftInvokerFunction(name,invokerArgsArray,null,rawInvoker,fn,isAsync),argCount-1);return[]})};var __embind_register_integer=(primitiveType,name,size,minRange,maxRange)=>{name=AsciiToString(name);const isUnsignedType=minRange===0;let fromWireType=value=>value;if(isUnsignedType){var bitshift=32-8*size;fromWireType=value=>value<>>bitshift;maxRange=fromWireType(maxRange)}registerType(primitiveType,{name,fromWireType,toWireType:(destructors,value)=>value,readValueFromPointer:integerReadValueFromPointer(name,size,minRange!==0),destructorFunction:null})};var installIndexedIterator=(proto,sizeMethodName,getMethodName)=>{const makeIterator=(size,getValue)=>{let index=0;return{next(){if(index>=size){return{done:true}}const current=index;index++;const value=getValue(current);return{value,done:false}},[Symbol.iterator](){return this}}};if(!proto[Symbol.iterator]){proto[Symbol.iterator]=function(){const size=this[sizeMethodName]();return makeIterator(size,i=>this[getMethodName](i))}}};var __embind_register_iterable=(rawClassType,rawElementType,sizeMethodName,getMethodName)=>{sizeMethodName=AsciiToString(sizeMethodName);getMethodName=AsciiToString(getMethodName);whenDependentTypesAreResolved([],[rawClassType,rawElementType],types=>{const classType=types[0];installIndexedIterator(classType.registeredClass.instancePrototype,sizeMethodName,getMethodName);return[]})};var __embind_register_memory_view=(rawType,dataTypeIndex,name)=>{var typeMapping=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array,BigInt64Array,BigUint64Array];var TA=typeMapping[dataTypeIndex];function decodeMemoryView(handle){var size=HEAPU32[handle>>2];var data=HEAPU32[handle+4>>2];return new TA(HEAP8.buffer,data,size)}name=AsciiToString(name);registerType(rawType,{name,fromWireType:decodeMemoryView,readValueFromPointer:decodeMemoryView},{ignoreDuplicateRegistrations:true})};var EmValOptionalType=Object.assign({optional:true},EmValType);var __embind_register_optional=(rawOptionalType,rawType)=>{registerType(rawOptionalType,EmValOptionalType)};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var __embind_register_std_string=(rawType,name)=>{name=AsciiToString(name);var stdStringIsUTF8=true;registerType(rawType,{name,fromWireType(value){var length=HEAPU32[value>>2];var payload=value+4;var str;if(stdStringIsUTF8){str=UTF8ToString(payload,length,true)}else{str="";for(var i=0;i>2]=length;if(valueIsOfTypeString){if(stdStringIsUTF8){stringToUTF8(value,ptr,length+1)}else{for(var i=0;i255){_free(base);throwBindingError("String has UTF-16 code units that do not fit in 8 bits")}HEAPU8[ptr+i]=charCode}}}else{HEAPU8.set(value,ptr)}if(destructors!==null){destructors.push(_free,base)}return base},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var UTF16Decoder=globalThis.TextDecoder?new TextDecoder("utf-16le"):undefined;var UTF16ToString=(ptr,maxBytesToRead,ignoreNul)=>{var idx=ptr>>1;var endIdx=findStringEnd(HEAPU16,idx,maxBytesToRead/2,ignoreNul);if(endIdx-idx>16&&UTF16Decoder)return UTF16Decoder.decode(HEAPU16.subarray(idx,endIdx));var str="";for(var i=idx;i{maxBytesToWrite??=2147483647;if(maxBytesToWrite<2)return 0;maxBytesToWrite-=2;var startPtr=outPtr;var numCharsToWrite=maxBytesToWrite>1]=codeUnit;outPtr+=2}HEAP16[outPtr>>1]=0;return outPtr-startPtr};var lengthBytesUTF16=str=>str.length*2;var UTF32ToString=(ptr,maxBytesToRead,ignoreNul)=>{var str="";var startIdx=ptr>>2;for(var i=0;!(i>=maxBytesToRead/4);i++){var utf32=HEAPU32[startIdx+i];if(!utf32&&!ignoreNul)break;str+=String.fromCodePoint(utf32)}return str};var stringToUTF32=(str,outPtr,maxBytesToWrite)=>{maxBytesToWrite??=2147483647;if(maxBytesToWrite<4)return 0;var startPtr=outPtr;var endPtr=startPtr+maxBytesToWrite-4;for(var i=0;i65535){i++}HEAP32[outPtr>>2]=codePoint;outPtr+=4;if(outPtr+4>endPtr)break}HEAP32[outPtr>>2]=0;return outPtr-startPtr};var lengthBytesUTF32=str=>{var len=0;for(var i=0;i65535){i++}len+=4}return len};var __embind_register_std_wstring=(rawType,charSize,name)=>{name=AsciiToString(name);var decodeString,encodeString,lengthBytesUTF;if(charSize===2){decodeString=UTF16ToString;encodeString=stringToUTF16;lengthBytesUTF=lengthBytesUTF16}else{decodeString=UTF32ToString;encodeString=stringToUTF32;lengthBytesUTF=lengthBytesUTF32}registerType(rawType,{name,fromWireType:value=>{var length=HEAPU32[value>>2];var str=decodeString(value+4,length*charSize,true);_free(value);return str},toWireType:(destructors,value)=>{if(!(typeof value=="string")){throwBindingError(`Cannot pass non-string to C++ string type ${name}`)}var length=lengthBytesUTF(value);var ptr=_malloc(4+length+charSize);HEAPU32[ptr>>2]=length/charSize;encodeString(value,ptr+4,length+charSize);if(destructors!==null){destructors.push(_free,ptr)}return ptr},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var __embind_register_value_object=(rawType,name,constructorSignature,rawConstructor,destructorSignature,rawDestructor)=>{structRegistrations[rawType]={name:AsciiToString(name),rawConstructor:embind__requireFunction(constructorSignature,rawConstructor),rawDestructor:embind__requireFunction(destructorSignature,rawDestructor),fields:[]}};var __embind_register_value_object_field=(structType,fieldName,getterReturnType,getterSignature,getter,getterContext,setterArgumentType,setterSignature,setter,setterContext)=>{structRegistrations[structType].fields.push({fieldName:AsciiToString(fieldName),getterReturnType,getter:embind__requireFunction(getterSignature,getter),getterContext,setterArgumentType,setter:embind__requireFunction(setterSignature,setter),setterContext})};var __embind_register_void=(rawType,name)=>{name=AsciiToString(name);registerType(rawType,{isVoid:true,name,fromWireType:()=>undefined,toWireType:(destructors,o)=>undefined})};var runtimeKeepaliveCounter=0;var __emscripten_runtime_keepalive_clear=()=>{noExitRuntime=false;runtimeKeepaliveCounter=0};var emval_methodCallers=[];var emval_addMethodCaller=caller=>{var id=emval_methodCallers.length;emval_methodCallers.push(caller);return id};var emval_lookupTypes=(argCount,argTypes)=>{var a=new Array(argCount);for(var i=0;i>2],`parameter ${i}`)}return a};var emval_returnValue=(toReturnWire,destructorsRef,handle)=>{var destructors=[];var result=toReturnWire(destructors,handle);if(destructors.length){HEAPU32[destructorsRef>>2]=Emval.toHandle(destructors)}return result};var emval_symbols={};var getStringOrSymbol=address=>{var symbol=emval_symbols[address];if(symbol===undefined){return AsciiToString(address)}return symbol};var __emval_create_invoker=(argCount,argTypesPtr,kind)=>{var GenericWireTypeSize=8;var[retType,...argTypes]=emval_lookupTypes(argCount,argTypesPtr);var toReturnWire=retType.toWireType.bind(retType);var argFromPtr=argTypes.map(type=>type.readValueFromPointer.bind(type));argCount--;var argN=new Array(argCount);var invokerFunction=(handle,methodName,destructorsRef,args)=>{var offset=0;for(var i=0;it.name)}) => ${retType.name}>`;return emval_addMethodCaller(createNamedFunction(functionName,invokerFunction))};var __emval_invoke=(caller,handle,methodName,destructorsRef,args)=>emval_methodCallers[caller](handle,methodName,destructorsRef,args);var __emval_new_cstring=v=>Emval.toHandle(getStringOrSymbol(v));var __emval_new_object=()=>Emval.toHandle({});var __emval_run_destructors=handle=>{var destructors=Emval.toValue(handle);runDestructors(destructors);__emval_decref(handle)};var __emval_set_property=(handle,key,value)=>{handle=Emval.toValue(handle);key=Emval.toValue(key);value=Emval.toValue(value);handle[key]=value};var timers={};var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;var maybeExit=()=>{if(!keepRuntimeAlive()){try{_exit(EXITSTATUS)}catch(e){handleException(e)}}};var callUserCallback=func=>{if(ABORT){return}try{return func()}catch(e){handleException(e)}finally{maybeExit()}};var _emscripten_get_now=()=>performance.now();var __setitimer_js=(which,timeout_ms)=>{if(timers[which]){clearTimeout(timers[which].id);delete timers[which]}if(!timeout_ms)return 0;var id=setTimeout(()=>{delete timers[which];callUserCallback(()=>__emscripten_timeout(which,_emscripten_get_now()))},timeout_ms);timers[which]={id,timeout_ms};return 0};var _emscripten_date_now=()=>Date.now();var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var printCharBuffers=[null,[],[]];var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(curr===0||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer));buffer.length=0}else{buffer.push(curr)}};var _fd_write=(fd,iov,iovcnt,pnum)=>{var num=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j>2]=num;return 0};init_ClassHandle();init_RegisteredPointer();{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}var ___getTypeName,_free,_malloc,__emscripten_timeout,memory,__indirect_function_table,wasmMemory,wasmTable;function assignWasmExports(wasmExports){___getTypeName=wasmExports["Q"];_free=Module["_free"]=wasmExports["R"];_malloc=Module["_malloc"]=wasmExports["S"];__emscripten_timeout=wasmExports["U"];memory=wasmMemory=wasmExports["O"];__indirect_function_table=wasmTable=wasmExports["T"]}var wasmImports={J:__abort_js,x:__embind_create_inheriting_constructor,s:__embind_finalize_value_object,z:__embind_register_bigint,A:__embind_register_bool,b:__embind_register_class,p:__embind_register_class_class_function,e:__embind_register_class_constructor,a:__embind_register_class_function,d:__embind_register_class_property,K:__embind_register_constant,M:__embind_register_emval,l:__embind_register_enum,f:__embind_register_enum_value,y:__embind_register_float,g:__embind_register_function,o:__embind_register_integer,q:__embind_register_iterable,h:__embind_register_memory_view,r:__embind_register_optional,N:__embind_register_std_string,u:__embind_register_std_wstring,t:__embind_register_value_object,m:__embind_register_value_object_field,B:__embind_register_void,D:__emscripten_runtime_keepalive_clear,k:__emval_create_invoker,n:__emval_decref,j:__emval_invoke,v:__emval_new_cstring,L:__emval_new_object,i:__emval_run_destructors,w:__emval_set_property,E:__setitimer_js,I:_emscripten_date_now,c:_emscripten_get_now,F:_emscripten_resize_heap,G:_exit,H:_fd_write,C:_proc_exit};function run(){preRun();function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} +var PHYSX=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};if(typeof __filename!="undefined"){_scriptName=__filename}else if(ENVIRONMENT_IS_WORKER){_scriptName=self.location.href}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{if(isFileURI(url)){return new Promise((resolve,reject)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){resolve(xhr.response);return}reject(xhr.status)};xhr.onerror=reject;xhr.send(null)})}var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var EXITSTATUS;var isFileURI=filename=>filename.startsWith("file://");var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["P"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("physx.release.simd.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!isFileURI(binaryFile)&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var __abort_js=()=>abort("");var createNamedFunction=(name,func)=>Object.defineProperty(func,"name",{value:name});var emval_freelist=[];var emval_handles=[0,1,,1,null,1,true,1,false,1];var BindingError=class BindingError extends Error{constructor(message){super(message);this.name="BindingError"}};var throwBindingError=message=>{throw new BindingError(message)};var Emval={toValue:handle=>{if(!handle){throwBindingError(`Cannot use deleted val. handle = ${handle}`)}return emval_handles[handle]},toHandle:value=>{switch(value){case undefined:return 2;case null:return 4;case true:return 6;case false:return 8;default:{const handle=emval_freelist.pop()||emval_handles.length;emval_handles[handle]=value;emval_handles[handle+1]=1;return handle}}}};class PureVirtualError extends Error{}var AsciiToString=ptr=>{var str="";while(1){var ch=HEAPU8[ptr++];if(!ch)return str;str+=String.fromCharCode(ch)}};var registeredInstances={};var getBasestPointer=(class_,ptr)=>{if(ptr===undefined){throwBindingError("ptr should not be undefined")}while(class_.baseClass){ptr=class_.upcast(ptr);class_=class_.baseClass}return ptr};var registerInheritedInstance=(class_,ptr,instance)=>{ptr=getBasestPointer(class_,ptr);if(registeredInstances.hasOwnProperty(ptr)){throwBindingError(`Tried to register registered instance: ${ptr}`)}else{registeredInstances[ptr]=instance}};var registeredTypes={};var getTypeName=type=>{var ptr=___getTypeName(type);var rv=AsciiToString(ptr);_free(ptr);return rv};var requireRegisteredType=(rawType,humanName)=>{var impl=registeredTypes[rawType];if(undefined===impl){throwBindingError(`${humanName} has unknown type ${getTypeName(rawType)}`)}return impl};var unregisterInheritedInstance=(class_,ptr)=>{ptr=getBasestPointer(class_,ptr);if(registeredInstances.hasOwnProperty(ptr)){delete registeredInstances[ptr]}else{throwBindingError(`Tried to unregister unregistered instance: ${ptr}`)}};var detachFinalizer=handle=>{};var finalizationRegistry=false;var runDestructor=$$=>{if($$.smartPtr){$$.smartPtrType.rawDestructor($$.smartPtr)}else{$$.ptrType.registeredClass.rawDestructor($$.ptr)}};var releaseClassHandle=$$=>{$$.count.value-=1;var toDelete=0===$$.count.value;if(toDelete){runDestructor($$)}};var attachFinalizer=handle=>{if(!globalThis.FinalizationRegistry){attachFinalizer=handle=>handle;return handle}finalizationRegistry=new FinalizationRegistry(info=>{releaseClassHandle(info.$$)});attachFinalizer=handle=>{var $$=handle.$$;var hasSmartPtr=!!$$.smartPtr;if(hasSmartPtr){var info={$$};finalizationRegistry.register(handle,info,handle)}return handle};detachFinalizer=handle=>finalizationRegistry.unregister(handle);return attachFinalizer(handle)};var __embind_create_inheriting_constructor=(constructorName,wrapperType,properties)=>{constructorName=AsciiToString(constructorName);wrapperType=requireRegisteredType(wrapperType,"wrapper");properties=Emval.toValue(properties);var registeredClass=wrapperType.registeredClass;var wrapperPrototype=registeredClass.instancePrototype;var baseClass=registeredClass.baseClass;var baseClassPrototype=baseClass.instancePrototype;var baseConstructor=registeredClass.baseClass.constructor;var ctor=createNamedFunction(constructorName,function(...args){for(var name of registeredClass.baseClass.pureVirtualFunctions){if(this[name]===baseClassPrototype[name]){throw new PureVirtualError(`Pure virtual function ${name} must be implemented in JavaScript`)}}Object.defineProperty(this,"__parent",{value:wrapperPrototype});this["__construct"](...args)});wrapperPrototype["__construct"]=function __construct(...args){if(this===wrapperPrototype){throwBindingError("Pass correct 'this' to __construct")}var inner=baseConstructor["implement"](this,...args);detachFinalizer(inner);var $$=inner.$$;inner["notifyOnDestruction"]();$$.preservePointerOnDelete=true;Object.defineProperties(this,{$$:{value:$$}});attachFinalizer(this);registerInheritedInstance(registeredClass,$$.ptr,this)};wrapperPrototype["__destruct"]=function __destruct(){if(this===wrapperPrototype){throwBindingError("Pass correct 'this' to __destruct")}detachFinalizer(this);unregisterInheritedInstance(registeredClass,this.$$.ptr)};ctor.prototype=Object.create(wrapperPrototype);Object.assign(ctor.prototype,properties);return Emval.toHandle(ctor)};var structRegistrations={};var runDestructors=destructors=>{while(destructors.length){var ptr=destructors.pop();var del=destructors.pop();del(ptr)}};function readPointer(pointer){return this.fromWireType(HEAPU32[pointer>>2])}var awaitingDependencies={};var typeDependencies={};var InternalError=class InternalError extends Error{constructor(message){super(message);this.name="InternalError"}};var throwInternalError=message=>{throw new InternalError(message)};var whenDependentTypesAreResolved=(myTypes,dependentTypes,getTypeConverters)=>{myTypes.forEach(type=>typeDependencies[type]=dependentTypes);function onComplete(typeConverters){var myTypeConverters=getTypeConverters(typeConverters);if(myTypeConverters.length!==myTypes.length){throwInternalError("Mismatched type converter count")}for(var i=0;i{typeConverters[i]=registeredTypes[dt];++registered;if(registered===unregisteredTypes.length){onComplete(typeConverters)}})}}if(0===unregisteredTypes.length){onComplete(typeConverters)}};var __embind_finalize_value_object=structType=>{var reg=structRegistrations[structType];delete structRegistrations[structType];var rawConstructor=reg.rawConstructor;var rawDestructor=reg.rawDestructor;var fieldRecords=reg.fields;var fieldTypes=fieldRecords.map(field=>field.getterReturnType).concat(fieldRecords.map(field=>field.setterArgumentType));whenDependentTypesAreResolved([structType],fieldTypes,fieldTypes=>{var fields={};for(var[i,field]of fieldRecords.entries()){const getterReturnType=fieldTypes[i];const getter=field.getter;const getterContext=field.getterContext;const setterArgumentType=fieldTypes[i+fieldRecords.length];const setter=field.setter;const setterContext=field.setterContext;fields[field.fieldName]={read:ptr=>getterReturnType.fromWireType(getter(getterContext,ptr)),write:(ptr,o)=>{var destructors=[];setter(setterContext,ptr,setterArgumentType.toWireType(destructors,o));runDestructors(destructors)},optional:getterReturnType.optional}}return[{name:reg.name,fromWireType:ptr=>{var rv={};for(var i in fields){rv[i]=fields[i].read(ptr)}rawDestructor(ptr);return rv},toWireType:(destructors,o)=>{for(var fieldName in fields){if(!(fieldName in o)&&!fields[fieldName].optional){throw new TypeError(`Missing field: "${fieldName}"`)}}var ptr=rawConstructor();for(fieldName in fields){fields[fieldName].write(ptr,o[fieldName])}if(destructors!==null){destructors.push(rawDestructor,ptr)}return ptr},readValueFromPointer:readPointer,destructorFunction:rawDestructor}]})};function sharedRegisterType(rawType,registeredInstance,options={}){var name=registeredInstance.name;if(!rawType){throwBindingError(`type "${name}" must have a positive integer typeid pointer`)}if(registeredTypes.hasOwnProperty(rawType)){if(options.ignoreDuplicateRegistrations){return}else{throwBindingError(`Cannot register type '${name}' twice`)}}registeredTypes[rawType]=registeredInstance;delete typeDependencies[rawType];if(awaitingDependencies.hasOwnProperty(rawType)){var callbacks=awaitingDependencies[rawType];delete awaitingDependencies[rawType];callbacks.forEach(cb=>cb())}}function registerType(rawType,registeredInstance,options={}){return sharedRegisterType(rawType,registeredInstance,options)}var integerReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?pointer=>HEAP8[pointer]:pointer=>HEAPU8[pointer];case 2:return signed?pointer=>HEAP16[pointer>>1]:pointer=>HEAPU16[pointer>>1];case 4:return signed?pointer=>HEAP32[pointer>>2]:pointer=>HEAPU32[pointer>>2];case 8:return signed?pointer=>HEAP64[pointer>>3]:pointer=>HEAPU64[pointer>>3];default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};var __embind_register_bigint=(primitiveType,name,size,minRange,maxRange)=>{name=AsciiToString(name);const isUnsignedType=minRange===0n;let fromWireType=value=>value;if(isUnsignedType){const bitSize=size*8;fromWireType=value=>BigInt.asUintN(bitSize,value);maxRange=fromWireType(maxRange)}registerType(primitiveType,{name,fromWireType,toWireType:(destructors,value)=>{if(typeof value=="number"){value=BigInt(value)}return value},readValueFromPointer:integerReadValueFromPointer(name,size,!isUnsignedType),destructorFunction:null})};var __embind_register_bool=(rawType,name,trueValue,falseValue)=>{name=AsciiToString(name);registerType(rawType,{name,fromWireType:function(wt){return!!wt},toWireType:function(destructors,o){return o?trueValue:falseValue},readValueFromPointer:function(pointer){return this.fromWireType(HEAPU8[pointer])},destructorFunction:null})};var shallowCopyInternalPointer=o=>({count:o.count,deleteScheduled:o.deleteScheduled,preservePointerOnDelete:o.preservePointerOnDelete,ptr:o.ptr,ptrType:o.ptrType,smartPtr:o.smartPtr,smartPtrType:o.smartPtrType});var throwInstanceAlreadyDeleted=obj=>{function getInstanceTypeName(handle){return handle.$$.ptrType.registeredClass.name}throwBindingError(getInstanceTypeName(obj)+" instance already deleted")};var deletionQueue=[];var flushPendingDeletes=()=>{while(deletionQueue.length){var obj=deletionQueue.pop();obj.$$.deleteScheduled=false;obj["delete"]()}};var delayFunction;var init_ClassHandle=()=>{let proto=ClassHandle.prototype;Object.assign(proto,{isAliasOf(other){if(!(this instanceof ClassHandle)){return false}if(!(other instanceof ClassHandle)){return false}var leftClass=this.$$.ptrType.registeredClass;var left=this.$$.ptr;other.$$=other.$$;var rightClass=other.$$.ptrType.registeredClass;var right=other.$$.ptr;while(leftClass.baseClass){left=leftClass.upcast(left);leftClass=leftClass.baseClass}while(rightClass.baseClass){right=rightClass.upcast(right);rightClass=rightClass.baseClass}return leftClass===rightClass&&left===right},clone(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.preservePointerOnDelete){this.$$.count.value+=1;return this}else{var clone=attachFinalizer(Object.create(Object.getPrototypeOf(this),{$$:{value:shallowCopyInternalPointer(this.$$)}}));clone.$$.count.value+=1;clone.$$.deleteScheduled=false;return clone}},delete(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}detachFinalizer(this);releaseClassHandle(this.$$);if(!this.$$.preservePointerOnDelete){this.$$.smartPtr=undefined;this.$$.ptr=undefined}},isDeleted(){return!this.$$.ptr},deleteLater(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}deletionQueue.push(this);if(deletionQueue.length===1&&delayFunction){delayFunction(flushPendingDeletes)}this.$$.deleteScheduled=true;return this}});const symbolDispose=Symbol.dispose;if(symbolDispose){proto[symbolDispose]=proto["delete"]}};function ClassHandle(){}var registeredPointers={};var ensureOverloadTable=(proto,methodName,humanName)=>{if(undefined===proto[methodName].overloadTable){var prevFunc=proto[methodName];proto[methodName]=function(...args){if(!proto[methodName].overloadTable.hasOwnProperty(args.length)){throwBindingError(`Function '${humanName}' called with an invalid number of arguments (${args.length}) - expects one of (${proto[methodName].overloadTable})!`)}return proto[methodName].overloadTable[args.length].apply(this,args)};proto[methodName].overloadTable=[];proto[methodName].overloadTable[prevFunc.argCount]=prevFunc}};var exposePublicSymbol=(name,value,numArguments)=>{if(Module.hasOwnProperty(name)){if(undefined===numArguments||undefined!==Module[name].overloadTable&&undefined!==Module[name].overloadTable[numArguments]){throwBindingError(`Cannot register public name '${name}' twice`)}ensureOverloadTable(Module,name,name);if(Module[name].overloadTable.hasOwnProperty(numArguments)){throwBindingError(`Cannot register multiple overloads of a function with the same number of arguments (${numArguments})!`)}Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}};var char_0=48;var char_9=57;var makeLegalFunctionName=name=>{name=name.replace(/[^a-zA-Z0-9_]/g,"$");var f=name.charCodeAt(0);if(f>=char_0&&f<=char_9){return`_${name}`}return name};function RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast){this.name=name;this.constructor=constructor;this.instancePrototype=instancePrototype;this.rawDestructor=rawDestructor;this.baseClass=baseClass;this.getActualType=getActualType;this.upcast=upcast;this.downcast=downcast;this.pureVirtualFunctions=[]}var upcastPointer=(ptr,ptrClass,desiredClass)=>{while(ptrClass!==desiredClass){if(!ptrClass.upcast){throwBindingError(`Expected null or instance of ${desiredClass.name}, got an instance of ${ptrClass.name}`)}ptr=ptrClass.upcast(ptr);ptrClass=ptrClass.baseClass}return ptr};var embindRepr=v=>{if(v===null){return"null"}var t=typeof v;if(t==="object"||t==="array"||t==="function"){return v.toString()}else{return""+v}};function constNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}function genericPointerToWireType(destructors,handle){var ptr;if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}if(this.isSmartPointer){ptr=this.rawConstructor();if(destructors!==null){destructors.push(this.rawDestructor,ptr)}return ptr}else{return 0}}if(!handle||!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(!this.isConst&&handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);if(this.isSmartPointer){if(undefined===handle.$$.smartPtr){throwBindingError("Passing raw pointer to smart pointer is illegal")}switch(this.sharingPolicy){case 0:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}break;case 1:ptr=handle.$$.smartPtr;break;case 2:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{var clonedHandle=handle["clone"]();ptr=this.rawShare(ptr,Emval.toHandle(()=>clonedHandle["delete"]()));if(destructors!==null){destructors.push(this.rawDestructor,ptr)}}break;default:throwBindingError("Unsupported sharing policy")}}return ptr}function nonConstNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}var downcastPointer=(ptr,ptrClass,desiredClass)=>{if(ptrClass===desiredClass){return ptr}if(undefined===desiredClass.baseClass){return null}var rv=downcastPointer(ptr,ptrClass,desiredClass.baseClass);if(rv===null){return null}return desiredClass.downcast(rv)};var getInheritedInstance=(class_,ptr)=>{ptr=getBasestPointer(class_,ptr);return registeredInstances[ptr]};var makeClassHandle=(prototype,record)=>{if(!record.ptrType||!record.ptr){throwInternalError("makeClassHandle requires ptr and ptrType")}var hasSmartPtrType=!!record.smartPtrType;var hasSmartPtr=!!record.smartPtr;if(hasSmartPtrType!==hasSmartPtr){throwInternalError("Both smartPtrType and smartPtr must be specified")}record.count={value:1};return attachFinalizer(Object.create(prototype,{$$:{value:record,writable:true}}))};function RegisteredPointer_fromWireType(ptr){var rawPointer=this.getPointee(ptr);if(!rawPointer){this.destructor(ptr);return null}var registeredInstance=getInheritedInstance(this.registeredClass,rawPointer);if(undefined!==registeredInstance){if(0===registeredInstance.$$.count.value){registeredInstance.$$.ptr=rawPointer;registeredInstance.$$.smartPtr=ptr;return registeredInstance["clone"]()}else{var rv=registeredInstance["clone"]();this.destructor(ptr);return rv}}function makeDefaultHandle(){if(this.isSmartPointer){return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this.pointeeType,ptr:rawPointer,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this,ptr})}}var actualType=this.registeredClass.getActualType(rawPointer);var registeredPointerRecord=registeredPointers[actualType];if(!registeredPointerRecord){return makeDefaultHandle.call(this)}var toType;if(this.isConst){toType=registeredPointerRecord.constPointerType}else{toType=registeredPointerRecord.pointerType}var dp=downcastPointer(rawPointer,this.registeredClass,toType.registeredClass);if(dp===null){return makeDefaultHandle.call(this)}if(this.isSmartPointer){return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp})}}var init_RegisteredPointer=()=>{Object.assign(RegisteredPointer.prototype,{getPointee(ptr){if(this.rawGetPointee){ptr=this.rawGetPointee(ptr)}return ptr},destructor(ptr){this.rawDestructor?.(ptr)},readValueFromPointer:readPointer,fromWireType:RegisteredPointer_fromWireType})};function RegisteredPointer(name,registeredClass,isReference,isConst,isSmartPointer,pointeeType,sharingPolicy,rawGetPointee,rawConstructor,rawShare,rawDestructor){this.name=name;this.registeredClass=registeredClass;this.isReference=isReference;this.isConst=isConst;this.isSmartPointer=isSmartPointer;this.pointeeType=pointeeType;this.sharingPolicy=sharingPolicy;this.rawGetPointee=rawGetPointee;this.rawConstructor=rawConstructor;this.rawShare=rawShare;this.rawDestructor=rawDestructor;if(!isSmartPointer&®isteredClass.baseClass===undefined){if(isConst){this.toWireType=constNoSmartPtrRawPointerToWireType;this.destructorFunction=null}else{this.toWireType=nonConstNoSmartPtrRawPointerToWireType;this.destructorFunction=null}}else{this.toWireType=genericPointerToWireType}}var replacePublicSymbol=(name,value,numArguments)=>{if(!Module.hasOwnProperty(name)){throwInternalError("Replacing nonexistent public symbol")}if(undefined!==Module[name].overloadTable&&undefined!==numArguments){Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}};var wasmTableMirror=[];var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var embind__requireFunction=(signature,rawFunction,isAsync=false)=>{signature=AsciiToString(signature);function makeDynCaller(){var rtn=getWasmTableEntry(rawFunction);return rtn}var fp=makeDynCaller();if(typeof fp!="function"){throwBindingError(`unknown function pointer with signature ${signature}: ${rawFunction}`)}return fp};class UnboundTypeError extends Error{}var throwUnboundTypeError=(message,types)=>{var unboundTypes=[];var seen={};function visit(type){if(seen[type]){return}if(registeredTypes[type]){return}if(typeDependencies[type]){typeDependencies[type].forEach(visit);return}unboundTypes.push(type);seen[type]=true}types.forEach(visit);throw new UnboundTypeError(`${message}: `+unboundTypes.map(getTypeName).join([", "]))};var __embind_register_class=(rawType,rawPointerType,rawConstPointerType,baseClassRawType,getActualTypeSignature,getActualType,upcastSignature,upcast,downcastSignature,downcast,name,destructorSignature,rawDestructor)=>{name=AsciiToString(name);getActualType=embind__requireFunction(getActualTypeSignature,getActualType);upcast&&=embind__requireFunction(upcastSignature,upcast);downcast&&=embind__requireFunction(downcastSignature,downcast);rawDestructor=embind__requireFunction(destructorSignature,rawDestructor);var legalFunctionName=makeLegalFunctionName(name);exposePublicSymbol(legalFunctionName,function(){throwUnboundTypeError(`Cannot construct ${name} due to unbound types`,[baseClassRawType])});whenDependentTypesAreResolved([rawType,rawPointerType,rawConstPointerType],baseClassRawType?[baseClassRawType]:[],base=>{base=base[0];var baseClass;var basePrototype;if(baseClassRawType){baseClass=base.registeredClass;basePrototype=baseClass.instancePrototype}else{basePrototype=ClassHandle.prototype}var constructor=createNamedFunction(name,function(...args){if(Object.getPrototypeOf(this)!==instancePrototype){throw new BindingError(`Use 'new' to construct ${name}`)}if(undefined===registeredClass.constructor_body){throw new BindingError(`${name} has no accessible constructor`)}var body=registeredClass.constructor_body[args.length];if(undefined===body){throw new BindingError(`Tried to invoke ctor of ${name} with invalid number of parameters (${args.length}) - expected (${Object.keys(registeredClass.constructor_body).toString()}) parameters instead!`)}return body.apply(this,args)});var instancePrototype=Object.create(basePrototype,{constructor:{value:constructor}});constructor.prototype=instancePrototype;var registeredClass=new RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast);if(registeredClass.baseClass){registeredClass.baseClass.__derivedClasses??=[];registeredClass.baseClass.__derivedClasses.push(registeredClass)}var referenceConverter=new RegisteredPointer(name,registeredClass,true,false,false);var pointerConverter=new RegisteredPointer(name+"*",registeredClass,false,false,false);var constPointerConverter=new RegisteredPointer(name+" const*",registeredClass,false,true,false);registeredPointers[rawType]={pointerType:pointerConverter,constPointerType:constPointerConverter};replacePublicSymbol(legalFunctionName,constructor);return[referenceConverter,pointerConverter,constPointerConverter]})};function usesDestructorStack(argTypes){for(var i=1;i{var array=[];for(var i=0;i>2])}return array};var getFunctionName=signature=>{signature=signature.trim();const argsIndex=signature.indexOf("(");if(argsIndex===-1)return signature;return signature.slice(0,argsIndex)};var __embind_register_class_class_function=(rawClassType,methodName,argCount,rawArgTypesAddr,invokerSignature,rawInvoker,fn,isAsync,isNonnullReturn)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);methodName=AsciiToString(methodName);methodName=getFunctionName(methodName);rawInvoker=embind__requireFunction(invokerSignature,rawInvoker,isAsync);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`${classType.name}.${methodName}`;function unboundTypesHandler(){throwUnboundTypeError(`Cannot call ${humanName} due to unbound types`,rawArgTypes)}if(methodName.startsWith("@@")){methodName=Symbol[methodName.substring(2)]}var proto=classType.registeredClass.constructor;if(undefined===proto[methodName]){unboundTypesHandler.argCount=argCount-1;proto[methodName]=unboundTypesHandler}else{ensureOverloadTable(proto,methodName,humanName);proto[methodName].overloadTable[argCount-1]=unboundTypesHandler}whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{var invokerArgsArray=[argTypes[0],null].concat(argTypes.slice(1));var func=craftInvokerFunction(humanName,invokerArgsArray,null,rawInvoker,fn,isAsync);if(undefined===proto[methodName].overloadTable){func.argCount=argCount-1;proto[methodName]=func}else{proto[methodName].overloadTable[argCount-1]=func}if(classType.registeredClass.__derivedClasses){for(const derivedClass of classType.registeredClass.__derivedClasses){if(!derivedClass.constructor.hasOwnProperty(methodName)){derivedClass.constructor[methodName]=func}}}return[]});return[]})};var __embind_register_class_constructor=(rawClassType,argCount,rawArgTypesAddr,invokerSignature,invoker,rawConstructor)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);invoker=embind__requireFunction(invokerSignature,invoker);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`constructor ${classType.name}`;if(undefined===classType.registeredClass.constructor_body){classType.registeredClass.constructor_body=[]}if(undefined!==classType.registeredClass.constructor_body[argCount-1]){throw new BindingError(`Cannot register multiple constructors with identical number of parameters (${argCount-1}) for class '${classType.name}'! Overload resolution is currently only performed using the parameter count, not actual type info!`)}classType.registeredClass.constructor_body[argCount-1]=()=>{throwUnboundTypeError(`Cannot construct ${classType.name} due to unbound types`,rawArgTypes)};whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{argTypes.splice(1,0,null);classType.registeredClass.constructor_body[argCount-1]=craftInvokerFunction(humanName,argTypes,null,invoker,rawConstructor);return[]});return[]})};var __embind_register_class_function=(rawClassType,methodName,argCount,rawArgTypesAddr,invokerSignature,rawInvoker,context,isPureVirtual,isAsync,isNonnullReturn)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);methodName=AsciiToString(methodName);methodName=getFunctionName(methodName);rawInvoker=embind__requireFunction(invokerSignature,rawInvoker,isAsync);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`${classType.name}.${methodName}`;if(methodName.startsWith("@@")){methodName=Symbol[methodName.substring(2)]}if(isPureVirtual){classType.registeredClass.pureVirtualFunctions.push(methodName)}function unboundTypesHandler(){throwUnboundTypeError(`Cannot call ${humanName} due to unbound types`,rawArgTypes)}var proto=classType.registeredClass.instancePrototype;var method=proto[methodName];if(undefined===method||undefined===method.overloadTable&&method.className!==classType.name&&method.argCount===argCount-2){unboundTypesHandler.argCount=argCount-2;unboundTypesHandler.className=classType.name;proto[methodName]=unboundTypesHandler}else{ensureOverloadTable(proto,methodName,humanName);proto[methodName].overloadTable[argCount-2]=unboundTypesHandler}whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{var memberFunction=craftInvokerFunction(humanName,argTypes,classType,rawInvoker,context,isAsync);if(undefined===proto[methodName].overloadTable){memberFunction.argCount=argCount-2;proto[methodName]=memberFunction}else{proto[methodName].overloadTable[argCount-2]=memberFunction}return[]});return[]})};var validateThis=(this_,classType,humanName)=>{if(!(this_ instanceof Object)){throwBindingError(`${humanName} with invalid "this": ${this_}`)}if(!(this_ instanceof classType.registeredClass.constructor)){throwBindingError(`${humanName} incompatible with "this" of type ${this_.constructor.name}`)}if(!this_.$$.ptr){throwBindingError(`cannot call emscripten binding method ${humanName} on deleted object`)}return upcastPointer(this_.$$.ptr,this_.$$.ptrType.registeredClass,classType.registeredClass)};var __embind_register_class_property=(classType,fieldName,getterReturnType,getterSignature,getter,getterContext,setterArgumentType,setterSignature,setter,setterContext)=>{fieldName=AsciiToString(fieldName);getter=embind__requireFunction(getterSignature,getter);whenDependentTypesAreResolved([],[classType],classType=>{classType=classType[0];var humanName=`${classType.name}.${fieldName}`;var desc={get(){throwUnboundTypeError(`Cannot access ${humanName} due to unbound types`,[getterReturnType,setterArgumentType])},enumerable:true,configurable:true};if(setter){desc.set=()=>throwUnboundTypeError(`Cannot access ${humanName} due to unbound types`,[getterReturnType,setterArgumentType])}else{desc.set=v=>throwBindingError(humanName+" is a read-only property")}Object.defineProperty(classType.registeredClass.instancePrototype,fieldName,desc);whenDependentTypesAreResolved([],setter?[getterReturnType,setterArgumentType]:[getterReturnType],types=>{var getterReturnType=types[0];var desc={get(){var ptr=validateThis(this,classType,humanName+" getter");return getterReturnType.fromWireType(getter(getterContext,ptr))},enumerable:true};if(setter){setter=embind__requireFunction(setterSignature,setter);var setterArgumentType=types[1];desc.set=function(v){var ptr=validateThis(this,classType,humanName+" setter");var destructors=[];setter(setterContext,ptr,setterArgumentType.toWireType(destructors,v));runDestructors(destructors)}}Object.defineProperty(classType.registeredClass.instancePrototype,fieldName,desc);return[]});return[]})};var __embind_register_constant=(name,type,value)=>{name=AsciiToString(name);whenDependentTypesAreResolved([],[type],type=>{type=type[0];Module[name]=type.fromWireType(value);return[]})};var __emval_decref=handle=>{if(handle>9&&0===--emval_handles[handle+1]){emval_handles[handle]=undefined;emval_freelist.push(handle)}};var EmValType={name:"emscripten::val",fromWireType:handle=>{var rv=Emval.toValue(handle);__emval_decref(handle);return rv},toWireType:(destructors,value)=>Emval.toHandle(value),readValueFromPointer:readPointer,destructorFunction:null};var __embind_register_emval=rawType=>registerType(rawType,EmValType);var enumReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?function(pointer){return this.fromWireType(HEAP8[pointer])}:function(pointer){return this.fromWireType(HEAPU8[pointer])};case 2:return signed?function(pointer){return this.fromWireType(HEAP16[pointer>>1])}:function(pointer){return this.fromWireType(HEAPU16[pointer>>1])};case 4:return signed?function(pointer){return this.fromWireType(HEAP32[pointer>>2])}:function(pointer){return this.fromWireType(HEAPU32[pointer>>2])};default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};function getEnumValueType(rawValueType){return rawValueType===0?"object":rawValueType===1?"number":"string"}var __embind_register_enum=(rawType,name,size,isSigned,rawValueType)=>{name=AsciiToString(name);const valueType=getEnumValueType(rawValueType);switch(valueType){case"object":{function ctor(){}ctor.values={};registerType(rawType,{name,constructor:ctor,valueType,fromWireType:function(c){return this.constructor.values[c]},toWireType:(destructors,c)=>c.value,readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,ctor);break}case"number":{var keysMap={};registerType(rawType,{name,keysMap,valueType,fromWireType:c=>c,toWireType:(destructors,c)=>c,readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,keysMap);delete Module[name].argCount;break}case"string":{var valuesMap={};var reverseMap={};var keysMap={};registerType(rawType,{name,valuesMap,reverseMap,keysMap,valueType,fromWireType:function(c){return this.reverseMap[c]},toWireType:function(destructors,c){return this.valuesMap[c]},readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,keysMap);delete Module[name].argCount;break}}};var __embind_register_enum_value=(rawEnumType,name,enumValue)=>{var enumType=requireRegisteredType(rawEnumType,"enum");name=AsciiToString(name);switch(enumType.valueType){case"object":{var Enum=enumType.constructor;var Value=Object.create(enumType.constructor.prototype,{value:{value:enumValue},constructor:{value:createNamedFunction(`${enumType.name}_${name}`,function(){})}});Enum.values[enumValue]=Value;Enum[name]=Value;break}case"number":{enumType.keysMap[name]=enumValue;break}case"string":{enumType.valuesMap[name]=enumValue;enumType.reverseMap[enumValue]=name;enumType.keysMap[name]=name;break}}};var floatReadValueFromPointer=(name,width)=>{switch(width){case 4:return function(pointer){return this.fromWireType(HEAPF32[pointer>>2])};case 8:return function(pointer){return this.fromWireType(HEAPF64[pointer>>3])};default:throw new TypeError(`invalid float width (${width}): ${name}`)}};var __embind_register_float=(rawType,name,size)=>{name=AsciiToString(name);registerType(rawType,{name,fromWireType:value=>value,toWireType:(destructors,value)=>value,readValueFromPointer:floatReadValueFromPointer(name,size),destructorFunction:null})};var __embind_register_function=(name,argCount,rawArgTypesAddr,signature,rawInvoker,fn,isAsync,isNonnullReturn)=>{var argTypes=heap32VectorToArray(argCount,rawArgTypesAddr);name=AsciiToString(name);name=getFunctionName(name);rawInvoker=embind__requireFunction(signature,rawInvoker,isAsync);exposePublicSymbol(name,function(){throwUnboundTypeError(`Cannot call ${name} due to unbound types`,argTypes)},argCount-1);whenDependentTypesAreResolved([],argTypes,argTypes=>{var invokerArgsArray=[argTypes[0],null].concat(argTypes.slice(1));replacePublicSymbol(name,craftInvokerFunction(name,invokerArgsArray,null,rawInvoker,fn,isAsync),argCount-1);return[]})};var __embind_register_integer=(primitiveType,name,size,minRange,maxRange)=>{name=AsciiToString(name);const isUnsignedType=minRange===0;let fromWireType=value=>value;if(isUnsignedType){var bitshift=32-8*size;fromWireType=value=>value<>>bitshift;maxRange=fromWireType(maxRange)}registerType(primitiveType,{name,fromWireType,toWireType:(destructors,value)=>value,readValueFromPointer:integerReadValueFromPointer(name,size,minRange!==0),destructorFunction:null})};var installIndexedIterator=(proto,sizeMethodName,getMethodName)=>{const makeIterator=(size,getValue)=>{let index=0;return{next(){if(index>=size){return{done:true}}const current=index;index++;const value=getValue(current);return{value,done:false}},[Symbol.iterator](){return this}}};if(!proto[Symbol.iterator]){proto[Symbol.iterator]=function(){const size=this[sizeMethodName]();return makeIterator(size,i=>this[getMethodName](i))}}};var __embind_register_iterable=(rawClassType,rawElementType,sizeMethodName,getMethodName)=>{sizeMethodName=AsciiToString(sizeMethodName);getMethodName=AsciiToString(getMethodName);whenDependentTypesAreResolved([],[rawClassType,rawElementType],types=>{const classType=types[0];installIndexedIterator(classType.registeredClass.instancePrototype,sizeMethodName,getMethodName);return[]})};var __embind_register_memory_view=(rawType,dataTypeIndex,name)=>{var typeMapping=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array,BigInt64Array,BigUint64Array];var TA=typeMapping[dataTypeIndex];function decodeMemoryView(handle){var size=HEAPU32[handle>>2];var data=HEAPU32[handle+4>>2];return new TA(HEAP8.buffer,data,size)}name=AsciiToString(name);registerType(rawType,{name,fromWireType:decodeMemoryView,readValueFromPointer:decodeMemoryView},{ignoreDuplicateRegistrations:true})};var EmValOptionalType=Object.assign({optional:true},EmValType);var __embind_register_optional=(rawOptionalType,rawType)=>{registerType(rawOptionalType,EmValOptionalType)};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var __embind_register_std_string=(rawType,name)=>{name=AsciiToString(name);var stdStringIsUTF8=true;registerType(rawType,{name,fromWireType(value){var length=HEAPU32[value>>2];var payload=value+4;var str;if(stdStringIsUTF8){str=UTF8ToString(payload,length,true)}else{str="";for(var i=0;i>2]=length;if(valueIsOfTypeString){if(stdStringIsUTF8){stringToUTF8(value,ptr,length+1)}else{for(var i=0;i255){_free(base);throwBindingError("String has UTF-16 code units that do not fit in 8 bits")}HEAPU8[ptr+i]=charCode}}}else{HEAPU8.set(value,ptr)}if(destructors!==null){destructors.push(_free,base)}return base},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var UTF16Decoder=globalThis.TextDecoder?new TextDecoder("utf-16le"):undefined;var UTF16ToString=(ptr,maxBytesToRead,ignoreNul)=>{var idx=ptr>>1;var endIdx=findStringEnd(HEAPU16,idx,maxBytesToRead/2,ignoreNul);if(endIdx-idx>16&&UTF16Decoder)return UTF16Decoder.decode(HEAPU16.subarray(idx,endIdx));var str="";for(var i=idx;i{maxBytesToWrite??=2147483647;if(maxBytesToWrite<2)return 0;maxBytesToWrite-=2;var startPtr=outPtr;var numCharsToWrite=maxBytesToWrite>1]=codeUnit;outPtr+=2}HEAP16[outPtr>>1]=0;return outPtr-startPtr};var lengthBytesUTF16=str=>str.length*2;var UTF32ToString=(ptr,maxBytesToRead,ignoreNul)=>{var str="";var startIdx=ptr>>2;for(var i=0;!(i>=maxBytesToRead/4);i++){var utf32=HEAPU32[startIdx+i];if(!utf32&&!ignoreNul)break;str+=String.fromCodePoint(utf32)}return str};var stringToUTF32=(str,outPtr,maxBytesToWrite)=>{maxBytesToWrite??=2147483647;if(maxBytesToWrite<4)return 0;var startPtr=outPtr;var endPtr=startPtr+maxBytesToWrite-4;for(var i=0;i65535){i++}HEAP32[outPtr>>2]=codePoint;outPtr+=4;if(outPtr+4>endPtr)break}HEAP32[outPtr>>2]=0;return outPtr-startPtr};var lengthBytesUTF32=str=>{var len=0;for(var i=0;i65535){i++}len+=4}return len};var __embind_register_std_wstring=(rawType,charSize,name)=>{name=AsciiToString(name);var decodeString,encodeString,lengthBytesUTF;if(charSize===2){decodeString=UTF16ToString;encodeString=stringToUTF16;lengthBytesUTF=lengthBytesUTF16}else{decodeString=UTF32ToString;encodeString=stringToUTF32;lengthBytesUTF=lengthBytesUTF32}registerType(rawType,{name,fromWireType:value=>{var length=HEAPU32[value>>2];var str=decodeString(value+4,length*charSize,true);_free(value);return str},toWireType:(destructors,value)=>{if(!(typeof value=="string")){throwBindingError(`Cannot pass non-string to C++ string type ${name}`)}var length=lengthBytesUTF(value);var ptr=_malloc(4+length+charSize);HEAPU32[ptr>>2]=length/charSize;encodeString(value,ptr+4,length+charSize);if(destructors!==null){destructors.push(_free,ptr)}return ptr},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var __embind_register_value_object=(rawType,name,constructorSignature,rawConstructor,destructorSignature,rawDestructor)=>{structRegistrations[rawType]={name:AsciiToString(name),rawConstructor:embind__requireFunction(constructorSignature,rawConstructor),rawDestructor:embind__requireFunction(destructorSignature,rawDestructor),fields:[]}};var __embind_register_value_object_field=(structType,fieldName,getterReturnType,getterSignature,getter,getterContext,setterArgumentType,setterSignature,setter,setterContext)=>{structRegistrations[structType].fields.push({fieldName:AsciiToString(fieldName),getterReturnType,getter:embind__requireFunction(getterSignature,getter),getterContext,setterArgumentType,setter:embind__requireFunction(setterSignature,setter),setterContext})};var __embind_register_void=(rawType,name)=>{name=AsciiToString(name);registerType(rawType,{isVoid:true,name,fromWireType:()=>undefined,toWireType:(destructors,o)=>undefined})};var runtimeKeepaliveCounter=0;var __emscripten_runtime_keepalive_clear=()=>{noExitRuntime=false;runtimeKeepaliveCounter=0};var emval_methodCallers=[];var emval_addMethodCaller=caller=>{var id=emval_methodCallers.length;emval_methodCallers.push(caller);return id};var emval_lookupTypes=(argCount,argTypes)=>{var a=new Array(argCount);for(var i=0;i>2],`parameter ${i}`)}return a};var emval_returnValue=(toReturnWire,destructorsRef,handle)=>{var destructors=[];var result=toReturnWire(destructors,handle);if(destructors.length){HEAPU32[destructorsRef>>2]=Emval.toHandle(destructors)}return result};var emval_symbols={};var getStringOrSymbol=address=>{var symbol=emval_symbols[address];if(symbol===undefined){return AsciiToString(address)}return symbol};var __emval_create_invoker=(argCount,argTypesPtr,kind)=>{var GenericWireTypeSize=8;var[retType,...argTypes]=emval_lookupTypes(argCount,argTypesPtr);var toReturnWire=retType.toWireType.bind(retType);var argFromPtr=argTypes.map(type=>type.readValueFromPointer.bind(type));argCount--;var argN=new Array(argCount);var invokerFunction=(handle,methodName,destructorsRef,args)=>{var offset=0;for(var i=0;it.name)}) => ${retType.name}>`;return emval_addMethodCaller(createNamedFunction(functionName,invokerFunction))};var __emval_invoke=(caller,handle,methodName,destructorsRef,args)=>emval_methodCallers[caller](handle,methodName,destructorsRef,args);var __emval_new_cstring=v=>Emval.toHandle(getStringOrSymbol(v));var __emval_new_object=()=>Emval.toHandle({});var __emval_run_destructors=handle=>{var destructors=Emval.toValue(handle);runDestructors(destructors);__emval_decref(handle)};var __emval_set_property=(handle,key,value)=>{handle=Emval.toValue(handle);key=Emval.toValue(key);value=Emval.toValue(value);handle[key]=value};var timers={};var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;var maybeExit=()=>{if(!keepRuntimeAlive()){try{_exit(EXITSTATUS)}catch(e){handleException(e)}}};var callUserCallback=func=>{if(ABORT){return}try{return func()}catch(e){handleException(e)}finally{maybeExit()}};var _emscripten_get_now=()=>performance.now();var __setitimer_js=(which,timeout_ms)=>{if(timers[which]){clearTimeout(timers[which].id);delete timers[which]}if(!timeout_ms)return 0;var id=setTimeout(()=>{delete timers[which];callUserCallback(()=>__emscripten_timeout(which,_emscripten_get_now()))},timeout_ms);timers[which]={id,timeout_ms};return 0};var _emscripten_date_now=()=>Date.now();var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var printCharBuffers=[null,[],[]];var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(curr===0||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer));buffer.length=0}else{buffer.push(curr)}};var _fd_write=(fd,iov,iovcnt,pnum)=>{var num=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j>2]=num;return 0};init_ClassHandle();init_RegisteredPointer();{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}var ___getTypeName,_free,_malloc,__emscripten_timeout,memory,__indirect_function_table,wasmMemory,wasmTable;function assignWasmExports(wasmExports){___getTypeName=wasmExports["Q"];_free=Module["_free"]=wasmExports["R"];_malloc=Module["_malloc"]=wasmExports["S"];__emscripten_timeout=wasmExports["U"];memory=wasmMemory=wasmExports["O"];__indirect_function_table=wasmTable=wasmExports["T"]}var wasmImports={J:__abort_js,x:__embind_create_inheriting_constructor,s:__embind_finalize_value_object,z:__embind_register_bigint,A:__embind_register_bool,b:__embind_register_class,p:__embind_register_class_class_function,e:__embind_register_class_constructor,a:__embind_register_class_function,d:__embind_register_class_property,K:__embind_register_constant,M:__embind_register_emval,l:__embind_register_enum,f:__embind_register_enum_value,y:__embind_register_float,g:__embind_register_function,o:__embind_register_integer,q:__embind_register_iterable,h:__embind_register_memory_view,r:__embind_register_optional,N:__embind_register_std_string,u:__embind_register_std_wstring,t:__embind_register_value_object,m:__embind_register_value_object_field,B:__embind_register_void,D:__emscripten_runtime_keepalive_clear,k:__emval_create_invoker,n:__emval_decref,j:__emval_invoke,v:__emval_new_cstring,L:__emval_new_object,i:__emval_run_destructors,w:__emval_set_property,E:__setitimer_js,I:_emscripten_date_now,c:_emscripten_get_now,F:_emscripten_resize_heap,G:_exit,H:_fd_write,C:_proc_exit};function run(){preRun();function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} ;return moduleRtn}})();if(typeof exports==="object"&&typeof module==="object"){module.exports=PHYSX;module.exports.default=PHYSX}else if(typeof define==="function"&&define["amd"])define([],()=>PHYSX); diff --git a/packages/physics-physx/libs/physx.release.simd.wasm b/packages/physics-physx/libs/physx.release.simd.wasm index 91e5bab34b..f52b48110a 100755 Binary files a/packages/physics-physx/libs/physx.release.simd.wasm and b/packages/physics-physx/libs/physx.release.simd.wasm differ diff --git a/packages/physics-physx/libs/physx.release.wasm b/packages/physics-physx/libs/physx.release.wasm index d9e03eee6a..3efb26a288 100755 Binary files a/packages/physics-physx/libs/physx.release.wasm and b/packages/physics-physx/libs/physx.release.wasm differ diff --git a/packages/physics-physx/package.json b/packages/physics-physx/package.json index 3cdabdd9a9..ed881fc191 100644 --- a/packages/physics-physx/package.json +++ b/packages/physics-physx/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-physics-physx", - "version": "2.0.0-alpha.24", + "version": "0.0.0-experimental-2.0-game.19", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/physics-physx/src/PhysXDynamicCollider.ts b/packages/physics-physx/src/PhysXDynamicCollider.ts index 31510ae2de..05614dd5bf 100644 --- a/packages/physics-physx/src/PhysXDynamicCollider.ts +++ b/packages/physics-physx/src/PhysXDynamicCollider.ts @@ -24,6 +24,20 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli private static _tempTranslation = new Vector3(); private static _tempRotation = new Quaternion(); + /** + * Whether actor is currently kinematic. + * PhysX 拒绝在 kinematic actor 上启用 CCD(会打印警告并忽略), + * 所以 setCollisionDetectionMode 在 kinematic 状态下只缓存目标值, + * 等切回 dynamic 时再真正写到 PhysX。 + */ + private _isKinematic: boolean = false; + + /** + * Cached collision detection mode. Always reflects user's intent. + * 实际 PhysX CCD flag 可能跟这个不一致(kinematic 时强制 Discrete)。 + */ + private _collisionDetectionMode: number = CollisionDetectionMode.Discrete; + constructor(physXPhysics: PhysXPhysics, position: Vector3, rotation: Quaternion) { super(physXPhysics); const transform = this._transform(position, rotation); @@ -158,10 +172,52 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli /** * {@inheritDoc IDynamicCollider.setCollisionDetectionMode } + * + * PhysX 在 kinematic actor 上调用 setRigidBodyFlag(eENABLE_CCD, true) 会触发警告: + * "kinematic bodies with CCD enabled are not supported! CCD will be ignored" + * 虽然 PhysX 会忽略这次调用而非真的拒绝(切回 dynamic 时 flag 不会自动恢复), + * 但每次 setIsKinematic 切换都会让这个 warning 重复打印,污染日志, + * 同时让 actor 在 dynamic 状态下 CCD flag 状态不确定。 + * + * 解决: 只在 dynamic 状态时立即 apply CCD flags。kinematic 时仅缓存到 + * `_collisionDetectionMode`,等切回 dynamic 时由 setIsKinematic 重新 apply。 */ setCollisionDetectionMode(value: number): void { + this._collisionDetectionMode = value; + if (!this._isKinematic) { + this._applyCollisionDetectionFlags(value); + } + } + + /** + * {@inheritDoc IDynamicCollider.setUseGravity } + */ + setUseGravity(value: boolean): void { + this._pxActor.setActorFlag(this._physXPhysics._physX.PxActorFlag.eDISABLE_GRAVITY, !value); + } + + /** + * {@inheritDoc IDynamicCollider.setIsKinematic } + * + * 切换 kinematic 状态时同步处理 CCD flag: + * - 切到 kinematic 前先关 CCD(避免 PhysX 警告 + 让状态显式) + * - 切回 dynamic 后恢复用户期望的 CCD mode(来自 `_collisionDetectionMode` 缓存) + */ + setIsKinematic(value: boolean): void { + if (this._isKinematic === value) return; const physX = this._physXPhysics._physX; + if (value) { + this._applyCollisionDetectionFlags(CollisionDetectionMode.Discrete); + this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eKINEMATIC, true); + } else { + this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eKINEMATIC, false); + this._applyCollisionDetectionFlags(this._collisionDetectionMode); + } + this._isKinematic = value; + } + private _applyCollisionDetectionFlags(value: number): void { + const physX = this._physXPhysics._physX; switch (value) { case CollisionDetectionMode.Continuous: this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eENABLE_CCD, true); @@ -186,24 +242,6 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli } } - /** - * {@inheritDoc IDynamicCollider.setUseGravity } - */ - setUseGravity(value: boolean): void { - this._pxActor.setActorFlag(this._physXPhysics._physX.PxActorFlag.eDISABLE_GRAVITY, !value); - } - - /** - * {@inheritDoc IDynamicCollider.setIsKinematic } - */ - setIsKinematic(value: boolean): void { - if (value) { - this._pxActor.setRigidBodyFlag(this._physXPhysics._physX.PxRigidBodyFlag.eKINEMATIC, true); - } else { - this._pxActor.setRigidBodyFlag(this._physXPhysics._physX.PxRigidBodyFlag.eKINEMATIC, false); - } - } - /** * {@inheritDoc IDynamicCollider.setConstraints } */ @@ -213,34 +251,52 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli /** * {@inheritDoc IDynamicCollider.addForce } + * + * PhysX 在 kinematic actor 上调 addForce 是 no-op(doc: "kinematic bodies don't + * respond to forces")。提前 return 避免无意义的 wasm boundary cross。 + * + * Sleeping actor 不需要显式 wakeUp — wasm binding 调用 `addForce(force, eFORCE, + * autowake=true)`,PhysX 自动唤醒(已通过 `applyForce on sleeping actor` 测试验证)。 */ addForce(force: Vector3) { + if (this._isKinematic) return; this._pxActor.addForce({ x: force.x, y: force.y, z: force.z }); } /** * {@inheritDoc IDynamicCollider.addTorque } + * + * 同 addForce — kinematic 提前 return,sleeping 由 PhysX autowake 自动处理。 */ addTorque(torque: Vector3) { + if (this._isKinematic) return; this._pxActor.addTorque({ x: torque.x, y: torque.y, z: torque.z }); } /** * {@inheritDoc IDynamicCollider.move } + * + * PhysX 要求 setKinematicTarget 的 rotation 是 normalized quaternion,否则会触发 + * 内部 assertion / 警告,并把 actor 转到错误的姿态。所以在写入 wasm 边界前统一 normalize。 */ move(positionOrRotation: Vector3 | Quaternion, rotation?: Quaternion): void { + const tempTranslation = PhysXDynamicCollider._tempTranslation; + const tempRotation = PhysXDynamicCollider._tempRotation; + if (rotation) { - this._pxActor.setKinematicTarget(positionOrRotation, rotation); + tempRotation.copyFrom(rotation).normalize(); + this._pxActor.setKinematicTarget(positionOrRotation, tempRotation); return; } - const tempTranslation = PhysXDynamicCollider._tempTranslation; - const tempRotation = PhysXDynamicCollider._tempRotation; - this.getWorldTransform(tempTranslation, tempRotation); if (positionOrRotation instanceof Vector3) { + this.getWorldTransform(tempTranslation, tempRotation); + // current rotation read from PhysX is already normalized; no extra work needed this._pxActor.setKinematicTarget(positionOrRotation, tempRotation); } else { - this._pxActor.setKinematicTarget(tempTranslation, positionOrRotation); + this.getWorldTransform(tempTranslation, tempRotation); + tempRotation.copyFrom(positionOrRotation).normalize(); + this._pxActor.setKinematicTarget(tempTranslation, tempRotation); } } diff --git a/packages/physics-physx/src/PhysXPhysics.ts b/packages/physics-physx/src/PhysXPhysics.ts index 77ca0b6157..867ee68dff 100644 --- a/packages/physics-physx/src/PhysXPhysics.ts +++ b/packages/physics-physx/src/PhysXPhysics.ts @@ -57,20 +57,35 @@ export class PhysXPhysics implements IPhysics { private _tolerancesScale: any; private _wasmSIMDModeUrl: string; private _wasmModeUrl: string; + private _tolerancesScaleOptions: PhysXTolerancesScale | undefined; + private _defaultContactOffset = 0.02; + private _defaultSleepThreshold = 5e-3; /** * Create a PhysXPhysics instance. * @param runtimeMode - Runtime mode, `Auto` prefers WebAssembly SIMD if supported @see {@link PhysXRuntimeMode} * @param runtimeUrls - Manually specify the runtime URLs + * @param options - PhysX options. */ - constructor(runtimeMode: PhysXRuntimeMode = PhysXRuntimeMode.Auto, runtimeUrls?: PhysXRuntimeUrls) { + constructor(runtimeMode?: PhysXRuntimeMode, runtimeUrls?: PhysXRuntimeUrls, options?: PhysXPhysicsOptions); + constructor(options?: PhysXPhysicsOptions); + constructor( + runtimeModeOrOptions: PhysXRuntimeMode | PhysXPhysicsOptions = PhysXRuntimeMode.Auto, + runtimeUrls?: PhysXRuntimeUrls, + options?: PhysXPhysicsOptions + ) { + const isOptionsObject = typeof runtimeModeOrOptions === "object"; + const runtimeMode = isOptionsObject ? PhysXRuntimeMode.Auto : (runtimeModeOrOptions ?? PhysXRuntimeMode.Auto); + const resolvedOptions = isOptionsObject ? runtimeModeOrOptions : options; this._runTimeMode = runtimeMode; this._wasmSIMDModeUrl = runtimeUrls?.wasmSIMDModeUrl ?? - "https://mdn.alipayobjects.com/rms/afts/file/A*FHYHS4_ZL5UAAAAAQ4AAAAgAehQnAQ/physx.release.simd.js"; + "https://mdn.alipayobjects.com/rms/afts/file/A*iHrYQKBrgTAAAAAAQ4AAAAgAehQnAQ/physx.release.simd.js"; this._wasmModeUrl = runtimeUrls?.wasmModeUrl ?? - "https://mdn.alipayobjects.com/rms/afts/file/A*2fv0RLMK1d0AAAAAQ4AAAAgAehQnAQ/physx.release.js"; + "https://mdn.alipayobjects.com/rms/afts/file/A*DFuvR6Mv5C0AAAAAQ4AAAAgAehQnAQ/physx.release.js"; + this._tolerancesScaleOptions = resolvedOptions?.tolerancesScale; + this._updateScaledDefaults(this._tolerancesScaleOptions?.length ?? 1, this._tolerancesScaleOptions?.speed ?? 10); } /** @@ -156,6 +171,20 @@ export class PhysXPhysics implements IPhysics { return scene; } + /** + * {@inheritDoc IPhysics.getDefaultContactOffset } + */ + getDefaultContactOffset(): number { + return this._defaultContactOffset; + } + + /** + * {@inheritDoc IPhysics.getDefaultSleepThreshold } + */ + getDefaultSleepThreshold(): number { + return this._defaultSleepThreshold; + } + /** * {@inheritDoc IPhysics.createStaticCollider } */ @@ -279,6 +308,7 @@ export class PhysXPhysics implements IPhysics { const allocator = new physX.PxDefaultAllocator(); const pxFoundation = physX.PxCreateFoundation(version, allocator, defaultErrorCallback); const tolerancesScale = new physX.PxTolerancesScale(); + this._applyTolerancesScale(tolerancesScale); const pxPhysics = physX.PxCreatePhysics(version, pxFoundation, tolerancesScale, false, null); physX.PxInitExtensions(pxPhysics, null); @@ -302,6 +332,29 @@ export class PhysXPhysics implements IPhysics { this._allocator = allocator; this._tolerancesScale = tolerancesScale; } + + private _applyTolerancesScale(tolerancesScale: any): void { + const length = this._tolerancesScaleOptions?.length ?? tolerancesScale.length; + const speed = this._tolerancesScaleOptions?.speed ?? tolerancesScale.speed; + + this._assertPositiveFinite(length, "tolerancesScale.length"); + this._assertPositiveFinite(speed, "tolerancesScale.speed"); + + tolerancesScale.length = length; + tolerancesScale.speed = speed; + this._updateScaledDefaults(length, speed); + } + + private _assertPositiveFinite(value: number, name: string): void { + if (!Number.isFinite(value) || value <= 0) { + throw new Error(`PhysXPhysics ${name} must be a positive finite number.`); + } + } + + private _updateScaledDefaults(length: number, speed: number): void { + this._defaultContactOffset = 0.02 * length; + this._defaultSleepThreshold = 5e-5 * speed * speed; + } } enum InitializeState { @@ -316,3 +369,15 @@ interface PhysXRuntimeUrls { /*** The URL of `PhysXRuntimeMode.WebAssemblySIMD` mode. */ wasmSIMDModeUrl?: string; } + +export interface PhysXTolerancesScale { + /** Approximate object length in the simulation unit. PhysX default is 1. */ + length?: number; + /** Typical object speed in the simulation unit. PhysX default is 10. */ + speed?: number; +} + +export interface PhysXPhysicsOptions { + /** PhysX world unit scale used before PxPhysics, PxSceneDesc and PxCookingParams are created. */ + tolerancesScale?: PhysXTolerancesScale; +} diff --git a/packages/physics-physx/src/PhysXPhysicsScene.ts b/packages/physics-physx/src/PhysXPhysicsScene.ts index a56a1fabce..bee2ab2dc2 100644 --- a/packages/physics-physx/src/PhysXPhysicsScene.ts +++ b/packages/physics-physx/src/PhysXPhysicsScene.ts @@ -29,14 +29,28 @@ export class PhysXPhysicsScene implements IPhysicsScene { private _physXPhysics: PhysXPhysics; private _physXManager: PhysXPhysicsManager; private _pxRaycastHit: any; + private _pxSweepHit: any; private _pxFilterData: any; + private _pxRaycastSweepFilterData: any; private _pxScene: any; private _physXSimulationCallbackInstance: any; + // A single persistent PhysX query filter callback is shared by raycast, + // sweep and overlap. PhysX SDK guarantees that `postFilter` is only invoked + // when `PxQueryFlag::ePOSTFILTER` is set on the query's filter data, so + // overlap (whose filter data omits POST_FILTER) safely uses the same + // callback that also handles the raycast/sweep initial-overlap skip. + // The user-supplied predicate is stored in `_currentOnQuery`; reentrant + // calls save the previous value on the call stack via a local in each + // query method, recreating C++-style RAII without an explicit stack array. + private _pxQueryCallback: any; + private _currentOnQuery: (obj: number) => boolean = null; + private _activeTriggers: DisorderedArray = new DisorderedArray(); private _contactEvents: ContactEvent[] = []; private _contactEventCount = 0; + private _contactEventEnabled = true; private _triggerEvents: TriggerEvent[] = []; private _physicsEvents: IPhysicsEvents = { contactEvents: [], contactEventCount: 0, triggerEvents: [] }; @@ -49,8 +63,13 @@ export class PhysXPhysicsScene implements IPhysicsScene { const physX = physXPhysics._physX; this._pxRaycastHit = new physX.PxRaycastHit(); + this._pxSweepHit = new physX.PxSweepHit(); this._pxFilterData = new physX.PxQueryFilterData(); this._pxFilterData.flags = new physX.PxQueryFlags(QueryFlag.STATIC | QueryFlag.DYNAMIC | QueryFlag.PRE_FILTER); + this._pxRaycastSweepFilterData = new physX.PxQueryFilterData(); + this._pxRaycastSweepFilterData.flags = new physX.PxQueryFlags( + QueryFlag.STATIC | QueryFlag.DYNAMIC | QueryFlag.PRE_FILTER | QueryFlag.POST_FILTER + ); const triggerCallback = { onContactBegin: (collision) => { @@ -91,6 +110,14 @@ export class PhysXPhysicsScene implements IPhysicsScene { ); this._pxScene = pxPhysics.createScene(sceneDesc); sceneDesc.delete(); + + this._pxQueryCallback = physX.PxQueryFilterCallback.implement({ + preFilter: (_filterData: any, index: number, _actor: any) => + this._currentOnQuery(index) ? QueryHitType.BLOCK : QueryHitType.NONE, + // distance <= 0 means initial overlap — drop the hit so subsequent hits can be considered. + // Only invoked when the query's filter data includes POST_FILTER (raycast/sweep, not overlap). + postFilter: (_filterData: any, distance: number) => (distance <= 0 ? QueryHitType.NONE : QueryHitType.BLOCK) + }); } /** @@ -165,6 +192,20 @@ export class PhysXPhysicsScene implements IPhysicsScene { this._fetchResults(); } + /** + * {@inheritDoc IPhysicsScene.setContactEventEnabled } + */ + setContactEventEnabled(enabled: boolean): void { + if (this._contactEventEnabled === enabled) { + return; + } + + this._contactEventEnabled = enabled; + if (!enabled) { + this._contactEventCount = 0; + } + } + /** * {@inheritDoc IPhysicsScene.updateEvents } */ @@ -207,27 +248,21 @@ export class PhysXPhysicsScene implements IPhysicsScene { const { _pxRaycastHit: pxHitResult } = this; distance = Math.min(distance, 3.4e38); // float32 max value limit in physX raycast. - const raycastCallback = { - preFilter: (filterData, index, actor) => { - if (onRaycast(index)) { - return 2; // eBLOCK - } else { - return 0; // eNONE - } - } - }; - - const pxRaycastCallback = this._physXPhysics._physX.PxQueryFilterCallback.implement(raycastCallback); - const result = this._pxScene.raycastSingle( - ray.origin, - ray.direction, - distance, - pxHitResult, - this._pxFilterData, - pxRaycastCallback - ); - - pxRaycastCallback.delete(); + const prevOnQuery = this._currentOnQuery; + this._currentOnQuery = onRaycast; + let result: boolean; + try { + result = this._pxScene.raycastSingle( + ray.origin, + ray.direction, + distance, + pxHitResult, + this._pxRaycastSweepFilterData, + this._pxQueryCallback + ); + } finally { + this._currentOnQuery = prevOnQuery; + } if (result && hit != undefined) { const { _tempPosition: position, _tempNormal: normal } = PhysXPhysicsScene; @@ -390,8 +425,12 @@ export class PhysXPhysicsScene implements IPhysicsScene { this._physXSimulationCallbackInstance.delete(); this._pxRaycastHit.delete(); + this._pxSweepHit.delete(); this._pxFilterData.flags.delete(); this._pxFilterData.delete(); + this._pxRaycastSweepFilterData.flags.delete(); + this._pxRaycastSweepFilterData.delete(); + this._pxQueryCallback.delete(); // Need to release the controller manager before release the scene. this._pxControllerManager?.release(); this._pxScene.release(); @@ -443,29 +482,25 @@ export class PhysXPhysicsScene implements IPhysicsScene { onSweep: (obj: number) => boolean, outHitResult?: (shapeUniqueID: number, distance: number, position: Vector3, normal: Vector3) => void ): boolean { + const { _pxSweepHit: pxSweepHit } = this; distance = Math.min(distance, 3.4e38); // float32 max value limit in physx sweep - const sweepCallback = { - preFilter: (filterData, index, actor) => { - if (onSweep(index)) { - return 2; // eBLOCK - } else { - return 0; // eNONE - } - } - }; - - const pxSweepCallback = this._physXPhysics._physX.PxQueryFilterCallback.implement(sweepCallback); - const pxSweepHit = new this._physXPhysics._physX.PxSweepHit(); - const result = this._pxScene.sweepSingle( - geometry, - pose, - direction, - distance, - pxSweepHit, - this._pxFilterData, - pxSweepCallback - ); + const prevOnQuery = this._currentOnQuery; + this._currentOnQuery = onSweep; + let result: boolean; + try { + result = this._pxScene.sweepSingle( + geometry, + pose, + direction, + distance, + pxSweepHit, + this._pxRaycastSweepFilterData, + this._pxQueryCallback + ); + } finally { + this._currentOnQuery = prevOnQuery; + } if (result && outHitResult != undefined) { const { _tempPosition: position, _tempNormal: normal } = PhysXPhysicsScene; @@ -474,10 +509,6 @@ export class PhysXPhysicsScene implements IPhysicsScene { normal.set(pxNormal.x, pxNormal.y, pxNormal.z); outHitResult(pxSweepHit.getShape().getUUID(), pxSweepHit.distance, position, normal); } - - pxSweepCallback.delete(); - pxSweepHit.delete(); - return result; } @@ -486,19 +517,15 @@ export class PhysXPhysicsScene implements IPhysicsScene { pose: { translation: Vector3; rotation: Quaternion }, onOverlap: (obj: number) => boolean ): number[] { - const overlapCallback = { - preFilter: (filterData, index, actor) => (onOverlap(index) ? 2 : 0) - }; - - const pxOverlapCallback = this._physXPhysics._physX.PxQueryFilterCallback.implement(overlapCallback); + const prevOnQuery = this._currentOnQuery; + this._currentOnQuery = onOverlap; const maxHits = 256; - const hits: any = (this._pxScene as any).overlapMultiple( - geometry, - pose, - maxHits, - this._pxFilterData, - pxOverlapCallback - ); + let hits: any; + try { + hits = (this._pxScene as any).overlapMultiple(geometry, pose, maxHits, this._pxFilterData, this._pxQueryCallback); + } finally { + this._currentOnQuery = prevOnQuery; + } const result = PhysXPhysicsScene._tempShapeIDs; result.length = 0; @@ -509,7 +536,6 @@ export class PhysXPhysicsScene implements IPhysicsScene { } } - pxOverlapCallback.delete(); hits?.delete(); return result; } @@ -536,6 +562,8 @@ export class PhysXPhysicsScene implements IPhysicsScene { } private _bufferContactEvent(collision: ICollision, state: number): void { + if (!this._contactEventEnabled) return; + const index = this._contactEventCount++; const event = (this._contactEvents[index] ||= new ContactEvent()); event.shape0Id = collision.shape0Id; @@ -570,6 +598,16 @@ enum QueryFlag { NO_BLOCK = 1 << 5 } +/** + * Result returned from a PhysX query filter callback (mirrors `PxQueryHitType`). + */ +enum QueryHitType { + /** Filter the hit out (no further processing). */ + NONE = 0, + /** Treat the hit as a blocking hit (terminates query for single-hit modes). */ + BLOCK = 2 +} + enum PhysicsEventState { Enter = 0, Stay = 1, diff --git a/packages/physics-physx/src/shape/PhysXColliderShape.ts b/packages/physics-physx/src/shape/PhysXColliderShape.ts index ea5a3df8b4..61b9fdf6a3 100644 --- a/packages/physics-physx/src/shape/PhysXColliderShape.ts +++ b/packages/physics-physx/src/shape/PhysXColliderShape.ts @@ -56,6 +56,7 @@ export abstract class PhysXColliderShape implements IColliderShape { constructor(physXPhysics: PhysXPhysics) { this._physXPhysics = physXPhysics; + this._contractOffset = physXPhysics.getDefaultContactOffset(); } /** diff --git a/packages/rhi-webgl/package.json b/packages/rhi-webgl/package.json index 9d22c1e383..6da1870132 100644 --- a/packages/rhi-webgl/package.json +++ b/packages/rhi-webgl/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-rhi-webgl", - "version": "2.0.0-alpha.24", + "version": "0.0.0-experimental-2.0-game.19", "repository": { "url": "https://github.com/galacean/engine.git" }, diff --git a/packages/rhi-webgl/src/GLBuffer.ts b/packages/rhi-webgl/src/GLBuffer.ts index 87f635dba9..aea7b38842 100644 --- a/packages/rhi-webgl/src/GLBuffer.ts +++ b/packages/rhi-webgl/src/GLBuffer.ts @@ -21,7 +21,18 @@ export class GLBuffer implements IPlatformBuffer { const gl = rhi.gl; const glBuffer = gl.createBuffer(); const glBufferUsage = this._getGLBufferUsage(gl, bufferUsage); - const glBindTarget = type === BufferBindFlag.VertexBuffer ? gl.ARRAY_BUFFER : gl.ELEMENT_ARRAY_BUFFER; + let glBindTarget: number; + switch (type) { + case BufferBindFlag.VertexBuffer: + glBindTarget = gl.ARRAY_BUFFER; + break; + case BufferBindFlag.IndexBuffer: + glBindTarget = gl.ELEMENT_ARRAY_BUFFER; + break; + case BufferBindFlag.ConstantBuffer: + glBindTarget = (gl).UNIFORM_BUFFER; + break; + } this._gl = gl; this._glBuffer = glBuffer; this._glBufferUsage = glBufferUsage; diff --git a/packages/rhi-webgl/src/GLPrimitive.ts b/packages/rhi-webgl/src/GLPrimitive.ts index fa7d89f1e8..5a7890d735 100644 --- a/packages/rhi-webgl/src/GLPrimitive.ts +++ b/packages/rhi-webgl/src/GLPrimitive.ts @@ -118,6 +118,7 @@ export class GLPrimitive implements IPlatformPrimitive { const element = attributes[name]; if (element) { + if (!vertexBufferBindings[element.bindingIndex]) continue; const { buffer, stride } = vertexBufferBindings[element.bindingIndex]; vbo = buffer._platformBuffer._glBuffer; // prevent binding the vbo which already bound at the last loop, e.g. a buffer with multiple attributes. diff --git a/packages/rhi-webgl/src/WebGLGraphicDevice.ts b/packages/rhi-webgl/src/WebGLGraphicDevice.ts index c937742f36..7394b0409a 100644 --- a/packages/rhi-webgl/src/WebGLGraphicDevice.ts +++ b/packages/rhi-webgl/src/WebGLGraphicDevice.ts @@ -89,6 +89,8 @@ export interface WebGLGraphicDeviceOptions { * WebGL graphic device, including WebGL1.0 and WebGL2.0. */ export class WebGLGraphicDevice implements IHardwareRenderer { + maxUniformBlockSize: number; + /** @internal */ _readFrameBuffer: WebGLFramebuffer = null; /** @internal */ @@ -280,6 +282,20 @@ export class WebGLGraphicDevice implements IHardwareRenderer { return new GLTransformFeedbackPrimitive(this._gl); } + bindUniformBufferBase(bindingPoint: number, buffer: IPlatformBuffer): void { + const gl = this._gl; + gl.bindBufferBase(gl.UNIFORM_BUFFER, bindingPoint, (buffer)._glBuffer); + } + + bindUniformBlock(program: WebGLProgram, blockName: string, bindingPoint: number): number { + const gl = this._gl; + const blockIndex = gl.getUniformBlockIndex(program, blockName); + if (blockIndex !== gl.INVALID_INDEX) { + gl.uniformBlockBinding(program, blockIndex, bindingPoint); + } + return blockIndex; + } + /** * Enable GL_RASTERIZER_DISCARD (WebGL2 only). */ @@ -623,6 +639,11 @@ export class WebGLGraphicDevice implements IHardwareRenderer { if (debugRenderInfo != null) { this._renderer = gl.getParameter(debugRenderInfo.UNMASKED_RENDERER_WEBGL); } + if (this._isWebGL2) { + this.maxUniformBlockSize = (gl).getParameter( + (gl).MAX_UNIFORM_BLOCK_SIZE + ); + } } destroy(): void { diff --git a/packages/shader-lab/package.json b/packages/shader-lab/package.json index c6220b9a53..5f1bd3b797 100644 --- a/packages/shader-lab/package.json +++ b/packages/shader-lab/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-shaderlab", - "version": "2.0.0-alpha.24", + "version": "0.0.0-experimental-2.0-game.19", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/shader-lab/src/Preprocessor.ts b/packages/shader-lab/src/Preprocessor.ts index 17e907da73..8f3e1abb27 100644 --- a/packages/shader-lab/src/Preprocessor.ts +++ b/packages/shader-lab/src/Preprocessor.ts @@ -5,6 +5,7 @@ import { ShaderLib } from "@galacean/engine"; export enum MacroValueType { Number, // 1, 1.1 Symbol, // variable name + MemberAccess, // member access, e.g. input.v_uv, v.rgb FunctionCall, // function call, e.g. clamp(a, 0.0, 1.0) Other // shaderLab does not check this } @@ -27,6 +28,7 @@ export class Preprocessor { private static readonly _macroRegex = /^\s*#define\s+(\w+)[ ]*(\(([^)]*)\))?[ ]+(\(?\w+\)?.*?)(?:\/\/.*|\/\*.*?\*\/)?\s*$/gm; private static readonly _symbolReg = /^[a-zA-Z_][a-zA-Z0-9_]*$/; + private static readonly _memberAccessReg = /^([a-zA-Z_][a-zA-Z0-9_]*)(\.[a-zA-Z_][a-zA-Z0-9_]*)+$/; private static readonly _funcCallReg = /^([a-zA-Z_][a-zA-Z0-9_]*)\s*\((.*)\)$/; private static readonly _macroDefineIncludeMap = new Map(); @@ -61,6 +63,10 @@ export class Preprocessor { const referencedName = valueType === MacroValueType.FunctionCall ? info.functionCallName : info.value; if (info.params.indexOf(referencedName) !== -1) continue; if (out.indexOf(referencedName) === -1) out.push(referencedName); + } else if (valueType === MacroValueType.MemberAccess) { + // Extract root symbol: "input.v_uv" → "input" + const rootName = info.value.substring(0, info.value.indexOf(".")); + if (out.indexOf(rootName) === -1) out.push(rootName); } else if (valueType === MacroValueType.Other) { // #if _VERBOSE Logger.warn( @@ -110,6 +116,8 @@ export class Preprocessor { valueType = MacroValueType.Number; } else if (this._symbolReg.test(value)) { valueType = MacroValueType.Symbol; + } else if (this._memberAccessReg.test(value)) { + valueType = MacroValueType.MemberAccess; } else { const callMatch = this._funcCallReg.exec(value); if (callMatch) { diff --git a/packages/shader-lab/src/ShaderLab.ts b/packages/shader-lab/src/ShaderLab.ts index 16dab2619e..7a608c640c 100644 --- a/packages/shader-lab/src/ShaderLab.ts +++ b/packages/shader-lab/src/ShaderLab.ts @@ -37,6 +37,20 @@ export class ShaderLab implements IShaderLab { return range; } + /** + * Release compilation caches: the token/AST object pools, which otherwise stay + * at their compilation-time peak for the whole engine lifetime (~9MB plus GC + * mark cost in real projects; the LALR table-construction scaffold is already + * released automatically once the parse table is built). + * + * Call when no further shader compilation is expected soon — typically after + * shader warm-up. Compiling again afterwards is still fully supported: pools + * simply re-allocate on demand. + */ + static releaseCompilationCache(): void { + ShaderLabUtils.releaseAllShaderLabObjectPool(); + } + _parseShaderSource(sourceCode: string): IShaderSource { ShaderLabUtils.clearAllShaderLabObjectPool(); const shaderSource = ShaderSourceParser.parse(sourceCode); diff --git a/packages/shader-lab/src/ShaderLabUtils.ts b/packages/shader-lab/src/ShaderLabUtils.ts index 230e190494..5a9d39094d 100644 --- a/packages/shader-lab/src/ShaderLabUtils.ts +++ b/packages/shader-lab/src/ShaderLabUtils.ts @@ -21,6 +21,19 @@ export class ShaderLabUtils { } } + /** + * Truly release all pooled objects, in contrast to `clearAllShaderLabObjectPool` + * which only resets the used counter and keeps every element referenced (pool + * capacity stays at the compilation-time peak — ~150k token/AST objects in real + * projects). Call when compilation has converged (e.g. after shader warm-up); + * pools transparently re-allocate on demand if compilation happens again. + */ + static releaseAllShaderLabObjectPool() { + for (let i = 0, n = ShaderLabUtils._shaderLabObjectPoolSet.length; i < n; i++) { + ShaderLabUtils._shaderLabObjectPoolSet[i].garbageCollection(); + } + } + static createGSError( message: string, errorName: GSErrorName, diff --git a/packages/shader-lab/src/codeGen/CodeGenVisitor.ts b/packages/shader-lab/src/codeGen/CodeGenVisitor.ts index 35beaa1d98..f4d560c987 100644 --- a/packages/shader-lab/src/codeGen/CodeGenVisitor.ts +++ b/packages/shader-lab/src/codeGen/CodeGenVisitor.ts @@ -31,13 +31,19 @@ export abstract class CodeGenVisitor { protected static _tmpArrayPool = new ReturnableObjectPool(TempArray, 10); + protected static readonly _memberAccessReg = /\b(\w+)\.(\w+)\b/g; + defaultCodeGen(children: NodeChild[]) { const pool = CodeGenVisitor._tmpArrayPool; let ret = pool.get(); ret.dispose(); for (const child of children) { if (child instanceof BaseToken) { - ret.array.push(child.lexeme); + if (child.type === Keyword.MACRO_DEFINE_EXPRESSION) { + ret.array.push(this._transformMacroDefineValue(child.lexeme)); + } else { + ret.array.push(child.lexeme); + } } else { ret.array.push(child.codeGen(this)); } @@ -46,6 +52,32 @@ export abstract class CodeGenVisitor { return ret.array.join(" "); } + protected _transformMacroDefineValue( + lexeme: string, + overrideMap?: Record + ): string { + const context = VisitorContext.context; + const structVarMap = overrideMap ?? context._structVarMap; + if (!structVarMap) return lexeme; + + const spaceIdx = lexeme.indexOf(" "); + if (spaceIdx === -1) return lexeme; + + const macroName = lexeme.substring(0, spaceIdx); + let value = lexeme.substring(spaceIdx); + + const reg = CodeGenVisitor._memberAccessReg; + reg.lastIndex = 0; + value = value.replace(reg, (match, varName, propName) => { + const role = structVarMap[varName]; + if (!role) return match; + context.referenceStructPropByName(role, propName); + return propName; + }); + + return macroName + value; + } + visitPostfixExpression(node: ASTNode.PostfixExpression): string { const children = node.children; const derivationLength = children.length; @@ -212,7 +244,19 @@ export abstract class CodeGenVisitor { const children = node.children; const fullType = children[0]; if (fullType instanceof ASTNode.FullySpecifiedType && fullType.typeSpecifier.isCustom) { - VisitorContext.context.referenceGlobal(fullType.type, ESymbolType.STRUCT); + const context = VisitorContext.context; + const typeLexeme = fullType.typeSpecifier.lexeme; + const role = context.getStructRole(typeLexeme); + if (role) { + // Global variable of a varying/attribute/mrt struct type (e.g. "Varyings o;"). + // Don't output as uniform; register the variable in struct var maps instead. + const ident = children[1]; + if (ident instanceof BaseToken) { + context.registerStructVar(ident.lexeme, role); + } + return ""; + } + context.referenceGlobal(fullType.type, ESymbolType.STRUCT); } return `uniform ${this.defaultCodeGen(children)}`; } @@ -351,6 +395,8 @@ export abstract class CodeGenVisitor { const fnName = fnNode.protoType.ident.lexeme; const context = VisitorContext.context; + this._collectStructVars(fnNode, context); + if (fnName == context.stageEntry) { const statements = fnNode.statements.codeGen(this); return `void main() ${statements}`; @@ -359,6 +405,74 @@ export abstract class CodeGenVisitor { } } + private _collectStructVars(fnNode: ASTNode.FunctionDefinition, context: VisitorContext): void { + const map = context._structVarMap; + // Clear previous function's mappings + for (const key in map) delete map[key]; + + // Collect from function parameters + const paramList = fnNode.protoType.parameterList; + if (paramList) { + for (const param of paramList) { + if (param.ident && param.typeInfo && typeof param.typeInfo.type === "string") { + const role = context.getStructRole(param.typeInfo.typeLexeme); + if (role) map[param.ident.lexeme] = role; + } + } + } + + // Collect from local variable declarations in function body + this._collectStructVarsFromNode(fnNode.statements, context, map); + } + + private _collectStructVarsFromNode( + node: TreeNode, + context: VisitorContext, + map: Record + ): void { + const children = node.children; + for (let i = 0; i < children.length; i++) { + const child = children[i]; + if (child instanceof ASTNode.InitDeclaratorList) { + const typeLexeme = child.typeInfo?.typeLexeme; + if (typeLexeme) { + const role = context.getStructRole(typeLexeme); + if (role) { + // Extract variable name from SingleDeclaration or comma-separated identifiers + this._extractVarNamesFromInitDeclaratorList(child, map, role); + } + } + } else if (child instanceof TreeNode) { + this._collectStructVarsFromNode(child, context, map); + } + } + } + + protected _extractVarNamesFromInitDeclaratorList( + node: ASTNode.InitDeclaratorList, + map: Record, + role: "varying" | "attribute" | "mrt" + ): void { + const children = node.children; + if (children.length === 1) { + // SingleDeclaration: type ident + const singleDecl = children[0] as ASTNode.SingleDeclaration; + const identChildren = singleDecl.children; + if (identChildren.length >= 2 && identChildren[1] instanceof BaseToken) { + map[identChildren[1].lexeme] = role; + } + } else if (children.length >= 3) { + // InitDeclaratorList , ident ... + const initDeclList = children[0]; + if (initDeclList instanceof ASTNode.InitDeclaratorList) { + this._extractVarNamesFromInitDeclaratorList(initDeclList, map, role); + } + if (children[2] instanceof BaseToken) { + map[children[2].lexeme] = role; + } + } + } + protected _reportError(loc: ShaderRange | ShaderPosition, message: string): void { // #if _VERBOSE this.errors.push(new GSError(GSErrorName.CompilationError, message, loc, ShaderLab._processingPassText)); diff --git a/packages/shader-lab/src/codeGen/GLES100.ts b/packages/shader-lab/src/codeGen/GLES100.ts index 995dccdeec..2c2e0faa93 100644 --- a/packages/shader-lab/src/codeGen/GLES100.ts +++ b/packages/shader-lab/src/codeGen/GLES100.ts @@ -47,7 +47,7 @@ export class GLES100Visitor extends GLESVisitor { return ""; } const expression = node.children[1] as ASTNode.Expression; - return `gl_FragColor = ${expression.codeGen(this)}`; + return `gl_FragColor = ${expression.codeGen(this)};`; } return super.visitJumpStatement(node); } diff --git a/packages/shader-lab/src/codeGen/GLESVisitor.ts b/packages/shader-lab/src/codeGen/GLESVisitor.ts index fbf611729f..d7596ae9ab 100644 --- a/packages/shader-lab/src/codeGen/GLESVisitor.ts +++ b/packages/shader-lab/src/codeGen/GLESVisitor.ts @@ -5,6 +5,7 @@ import { Keyword } from "../common/enums/Keyword"; import { ASTNode, TreeNode } from "../parser/AST"; import { ShaderData } from "../parser/ShaderInfo"; import { ESymbolType, FnSymbol, StructSymbol, SymbolInfo } from "../parser/symbolTable"; +import { NodeChild } from "../parser/types"; import { CodeGenVisitor } from "./CodeGenVisitor"; import { ICodeSegment } from "./types"; import { VisitorContext } from "./VisitorContext"; @@ -37,10 +38,15 @@ export abstract class GLESVisitor extends CodeGenVisitor { this.reset(); const shaderData = node.shaderData; - VisitorContext.context._passSymbolTable = shaderData.symbolTable; + const context = VisitorContext.context; + context._passSymbolTable = shaderData.symbolTable; const outerGlobalMacroDeclarations = shaderData.getOuterGlobalMacroDeclarations(); + // Build combined _globalStructVarMap from both entry functions before per-stage processing. + // This must happen here because vertex runs first and doesn't yet know fragment's variables. + this._buildGlobalStructVarMap(vertexEntry, fragmentEntry, shaderData, outerGlobalMacroDeclarations, context); + return { vertex: this._vertexMain(vertexEntry, shaderData, outerGlobalMacroDeclarations), fragment: this._fragmentMain(fragmentEntry, shaderData, outerGlobalMacroDeclarations) @@ -109,6 +115,9 @@ export abstract class GLESVisitor extends CodeGenVisitor { } }); + // Pre-register global #define member access references for this stage + this._registerGlobalMacroReferences(outerGlobalMacroDeclarations, context); + const globalCodeArray = this._globalCodeArray; VisitorContext.context.referenceGlobal(entry, ESymbolType.FN); @@ -173,6 +182,9 @@ export abstract class GLESVisitor extends CodeGenVisitor { } }); + // Pre-register global #define member access references for this stage + this._registerGlobalMacroReferences(outerGlobalMacroStatements, context); + const globalCodeArray = this._globalCodeArray; VisitorContext.context.referenceGlobal(entry, ESymbolType.FN); @@ -193,10 +205,178 @@ export abstract class GLESVisitor extends CodeGenVisitor { return globalCode; } + /** + * Build _globalStructVarMap from both entry functions before per-stage processing. + * Classifies struct types by their position in function signatures: + * - vertex param[0] → attribute, vertex return type → varying + * - fragment param[0] → varying, fragment return type → mrt + */ + private _buildGlobalStructVarMap( + vertexEntry: string, + fragmentEntry: string, + data: ShaderData, + globalMacros: ASTNode.GlobalDeclaration[], + context: VisitorContext + ): void { + const map = context._globalStructVarMap; + const lookupSymbol = GLESVisitor._lookupSymbol; + const { symbolTable } = data; + + // Map struct type names to roles based on function signature positions + const structTypeRoles: Record = Object.create(null); + + // Vertex entry: param[0] type → attribute, return type → varying + lookupSymbol.set(vertexEntry, ESymbolType.FN); + const vertexFns = symbolTable.getSymbols(lookupSymbol, true, []); + for (const fn of vertexFns) { + const proto = fn.astNode.protoType; + const param0 = proto.parameterList?.[0]; + if (param0 && typeof param0.typeInfo.type === "string") { + structTypeRoles[param0.typeInfo.typeLexeme] = "attribute"; + } + if (typeof proto.returnType.type === "string") { + structTypeRoles[proto.returnType.type] = "varying"; + } + } + + // Fragment entry: param[0] type → varying, return type → mrt + lookupSymbol.set(fragmentEntry, ESymbolType.FN); + const fragmentFns = symbolTable.getSymbols(lookupSymbol, true, []); + for (const fn of fragmentFns) { + const proto = fn.astNode.protoType; + const param0 = proto.parameterList?.[0]; + if (param0 && typeof param0.typeInfo.type === "string") { + structTypeRoles[param0.typeInfo.typeLexeme] = "varying"; + } + if (typeof proto.returnType.type === "string") { + structTypeRoles[proto.returnType.type] = "mrt"; + } + } + + // Scan all entry functions' params and local vars, classify by structTypeRoles + for (const fn of [...vertexFns, ...fragmentFns]) { + const fnNode = fn.astNode; + const paramList = fnNode.protoType.parameterList; + if (paramList) { + for (const param of paramList) { + if (param.ident && param.typeInfo && typeof param.typeInfo.type === "string") { + const role = structTypeRoles[param.typeInfo.typeLexeme]; + if (role) map[param.ident.lexeme] = role; + } + } + } + this._collectStructVarsFromBody(fnNode.statements, structTypeRoles, map); + } + + // Also scan global macros for root variable names that might be global struct variables. + // e.g. #define VSOutput_worldPos o.v_worldPos → root "o" → look up in symbol table + let hasRoles = false; + for (const _ in structTypeRoles) { + hasRoles = true; + break; + } + if (hasRoles) { + const checked = new Set(); + const symOut: SymbolInfo[] = []; + this._forEachMacroMemberAccess(globalMacros, (rootName) => { + if (map[rootName] || checked.has(rootName)) return; + checked.add(rootName); + lookupSymbol.set(rootName, ESymbolType.VAR); + symbolTable.getSymbols(lookupSymbol, true, symOut); + for (const sym of symOut) { + if (sym.dataType) { + const role = structTypeRoles[sym.dataType.typeLexeme]; + if (role) { + map[rootName] = role; + break; + } + } + } + }); + } + } + + private _collectStructVarsFromBody( + node: TreeNode, + structTypeRoles: Record, + map: Record + ): void { + const children = node.children; + for (let i = 0; i < children.length; i++) { + const child = children[i]; + if (child instanceof ASTNode.InitDeclaratorList) { + const typeLexeme = child.typeInfo?.typeLexeme; + if (typeLexeme) { + const role = structTypeRoles[typeLexeme]; + if (role) { + this._extractVarNamesFromInitDeclaratorList(child, map, role); + } + } + } else if (child instanceof TreeNode) { + this._collectStructVarsFromBody(child, structTypeRoles, map); + } + } + } + + /** + * Pre-register attribute/varying/mrt references from global #define member access patterns, + * so that declarations are emitted by _getCustomStruct for the current stage. + */ + private _registerGlobalMacroReferences(globalMacros: ASTNode.GlobalDeclaration[], context: VisitorContext): void { + const map = context._globalStructVarMap; + let hasEntries = false; + for (const _ in map) { + hasEntries = true; + break; + } + if (!hasEntries) return; + this._forEachMacroMemberAccess(globalMacros, (rootName, propName) => { + const role = map[rootName]; + if (role) context.referenceStructPropByName(role, propName); + }); + } + + /** + * Traverse global macro declarations, extracting member access patterns (e.g. "o.v_uv") + * and invoking the callback with (rootName, propName) for each match. + */ + private _forEachMacroMemberAccess( + macros: ASTNode.GlobalDeclaration[], + callback: (rootName: string, propName: string) => void + ): void { + const reg = CodeGenVisitor._memberAccessReg; + for (const macro of macros) { + this._walkMacroChildren(macro.children, reg, callback); + } + } + + private _walkMacroChildren( + children: NodeChild[], + reg: RegExp, + callback: (rootName: string, propName: string) => void + ): void { + for (const child of children) { + if (child instanceof BaseToken && child.type === Keyword.MACRO_DEFINE_EXPRESSION) { + const spaceIdx = child.lexeme.indexOf(" "); + if (spaceIdx === -1) continue; + const value = child.lexeme.substring(spaceIdx); + reg.lastIndex = 0; + let match: RegExpExecArray | null; + while ((match = reg.exec(value)) !== null) { + callback(match[1], match[2]); + } + } else if (child instanceof TreeNode) { + this._walkMacroChildren(child.children, reg, callback); + } + } + } + private _getGlobalSymbol(out: ICodeSegment[]): void { - const { _referencedGlobals } = VisitorContext.context; + const context = VisitorContext.context; + const { _referencedGlobals } = context; - const lastLength = Object.keys(_referencedGlobals).length; + let lastLength = 0; + for (const _ in _referencedGlobals) lastLength++; if (lastLength === 0) return; for (const ident in _referencedGlobals) { @@ -206,7 +386,9 @@ export abstract class GLESVisitor extends CodeGenVisitor { const symbols = _referencedGlobals[ident]; for (let i = 0; i < symbols.length; i++) { const sm = symbols[i]; - const text = sm.astNode.codeGen(this) + (sm.type === ESymbolType.VAR ? ";" : ""); + const codeGenResult = sm.astNode.codeGen(this); + if (!codeGenResult) continue; + const text = codeGenResult + (sm.type === ESymbolType.VAR ? ";" : ""); if (!sm.isInMacroBranch) { out.push({ text, @@ -216,7 +398,9 @@ export abstract class GLESVisitor extends CodeGenVisitor { } } - if (Object.keys(_referencedGlobals).length !== lastLength) { + let newLength = 0; + for (const _ in _referencedGlobals) newLength++; + if (newLength !== lastLength) { this._getGlobalSymbol(out); } } @@ -233,6 +417,7 @@ export abstract class GLESVisitor extends CodeGenVisitor { private _getGlobalMacroDeclarations(macros: ASTNode.GlobalDeclaration[], out: ICodeSegment[]): void { const context = VisitorContext.context; + const globalMap = context._globalStructVarMap; const referencedGlobals = context._referencedGlobals; const referencedGlobalMacroASTs = context._referencedGlobalMacroASTs; referencedGlobalMacroASTs.length = 0; @@ -253,7 +438,12 @@ export abstract class GLESVisitor extends CodeGenVisitor { let result: ICodeSegment[] = []; result.push( ...macro.macroExpressions.map((item) => ({ - text: item instanceof BaseToken ? item.lexeme : item.codeGen(this), + text: + item instanceof BaseToken + ? item.type === Keyword.MACRO_DEFINE_EXPRESSION + ? this._transformMacroDefineValue(item.lexeme, globalMap) + : item.lexeme + : item.codeGen(this), index: item.location.start.index })) ); @@ -264,6 +454,8 @@ export abstract class GLESVisitor extends CodeGenVisitor { .sort((a, b) => a.index - b.index) .map((item) => item.text) .join("\n"); + } else if (child instanceof BaseToken && child.type === Keyword.MACRO_DEFINE_EXPRESSION) { + text = this._transformMacroDefineValue(child.lexeme, globalMap); } else { text = macro.codeGen(this); } diff --git a/packages/shader-lab/src/codeGen/VisitorContext.ts b/packages/shader-lab/src/codeGen/VisitorContext.ts index 25211f008a..1c1972c0db 100644 --- a/packages/shader-lab/src/codeGen/VisitorContext.ts +++ b/packages/shader-lab/src/codeGen/VisitorContext.ts @@ -38,6 +38,10 @@ export class VisitorContext { _referencedMRTList: Record; _referencedGlobals: Record; _referencedGlobalMacroASTs: TreeNode[] = []; + /** Maps variable names to their struct role for function-body #define value transformation. */ + _structVarMap: Record; + /** Combined mapping from all entry functions for global #define transformation. */ + _globalStructVarMap: Record; _passSymbolTable: SymbolTable; @@ -56,6 +60,36 @@ export class VisitorContext { this._referencedMRTList = Object.create(null); this._referencedGlobals = Object.create(null); this._referencedGlobalMacroASTs.length = 0; + this._structVarMap = Object.create(null); + if (resetAll) { + this._globalStructVarMap = Object.create(null); + } + } + + getStructRole(typeLexeme: string): "varying" | "attribute" | "mrt" | undefined { + if (this.isAttributeStruct(typeLexeme)) return "attribute"; + if (this.isVaryingStruct(typeLexeme)) return "varying"; + if (this.isMRTStruct(typeLexeme)) return "mrt"; + } + + registerStructVar(varName: string, role: "varying" | "attribute" | "mrt"): void { + this._structVarMap[varName] = role; + this._globalStructVarMap[varName] = role; + } + + referenceStructPropByName(role: "varying" | "attribute" | "mrt", propName: string): void { + const list = role === "varying" ? this.varyingList : role === "attribute" ? this.attributeList : this.mrtList; + const refList = + role === "varying" + ? this._referencedVaryingList + : role === "attribute" + ? this._referencedAttributeList + : this._referencedMRTList; + if (refList[propName]) return; + const props = list.filter((item) => item.ident.lexeme === propName); + if (props.length) { + refList[propName] = props; + } } isAttributeStruct(type: string) { diff --git a/packages/shader-lab/src/lalr/LALR1.ts b/packages/shader-lab/src/lalr/LALR1.ts index a333c67ca9..cda6dd2cb6 100644 --- a/packages/shader-lab/src/lalr/LALR1.ts +++ b/packages/shader-lab/src/lalr/LALR1.ts @@ -29,6 +29,11 @@ export class LALR1 { generate() { this.computeFirstSet(); this.buildStateTable(); + // The action/goto tables are self-contained (numeric state/production ids), + // so the State/StateItem closure graph used to build them is never read at + // runtime. Release it here. `Production.pool` must stay alive: reduce + // actions resolve productions by id while parsing. + State.clearPool(); } private buildStateTable() { diff --git a/packages/shader-lab/src/lalr/State.ts b/packages/shader-lab/src/lalr/State.ts index 6ef782249e..63796ad4fc 100644 --- a/packages/shader-lab/src/lalr/State.ts +++ b/packages/shader-lab/src/lalr/State.ts @@ -7,6 +7,20 @@ export default class State { static pool: Map = new Map(); static _id = 0; + /** + * Release the table-construction scaffold (the whole State/StateItem closure graph). + * Runtime parsing only reads the action/goto tables, whose entries are numeric + * state/production ids, so this graph is garbage once `LALR1.generate` finishes — + * yet it pins 10k+ objects (each `StateItem.lookaheadSet` costs ~2KB of hash + * buckets on JavaScriptCore, ~30MB total in real projects). + * Note: `printStatePool` (_VERBOSE debug helper) dumps nothing after this runs; + * break before `LALR1.generate` returns if you need the state graph. + */ + static clearPool() { + this.closureMap.clear(); + this.pool.clear(); + } + readonly id: number; readonly cores: Set; private _items: Set; diff --git a/packages/shader-lab/src/macroProcessor/MacroParser.ts b/packages/shader-lab/src/macroProcessor/MacroParser.ts index d845102e1a..a2ed90c504 100644 --- a/packages/shader-lab/src/macroProcessor/MacroParser.ts +++ b/packages/shader-lab/src/macroProcessor/MacroParser.ts @@ -355,7 +355,7 @@ export class MacroParser { scanner.advance(1); scanner.skipSpace(false); const parenExpr = this._parseParenthesisExpression(scanner); - if ((operator === "!" && typeof parenExpr !== "boolean") || (operator !== "!" && typeof parenExpr !== "number")) { + if (operator !== "!" && typeof parenExpr !== "number") { this._reportError(opPos, "invalid operator.", scanner.source, scanner.file); } diff --git a/packages/shader-lab/src/parser/AST.ts b/packages/shader-lab/src/parser/AST.ts index 4c90c01444..243944df8f 100644 --- a/packages/shader-lab/src/parser/AST.ts +++ b/packages/shader-lab/src/parser/AST.ts @@ -4,7 +4,7 @@ import { ETokenType, GalaceanDataType, ShaderRange, TokenType, TypeAny } from ". import { BaseToken } from "../common/BaseToken"; import { Keyword } from "../common/enums/Keyword"; import { ParserUtils } from "../ParserUtils"; -import { Preprocessor } from "../Preprocessor"; +import { MacroValueType, Preprocessor } from "../Preprocessor"; import { ShaderLabUtils } from "../ShaderLabUtils"; import { BuiltinFunction, BuiltinVariable, NonGenericGalaceanType } from "./builtin"; import { NoneTerminal } from "./GrammarSymbol"; @@ -1424,7 +1424,12 @@ export namespace ASTNode { sa.reportWarning(this.location, `Please sure the identifier "${name}" will be declared before used.`); // #endif } else { - this.typeInfo = symbols[0].dataType?.type; + // For member access macros (e.g. #define FRAG_UV v.v_uv), the referenceSymbolNames + // contains the root variable ("v") whose type is the struct ("Varyings"), not the + // member type ("vec2"). Skip type inference in this case — keep TypeAny. + if (child instanceof BaseToken || !this._isMemberAccessMacro(sa, child)) { + this.typeInfo = symbols[0].dataType?.type; + } const currentScopeSymbol = sa.symbolTableStack.scope.getSymbol(lookupSymbol, true); if (currentScopeSymbol) { if ( @@ -1443,6 +1448,12 @@ export namespace ASTNode { } } + private _isMemberAccessMacro(sa: SemanticAnalyzer, child: MacroCallSymbol | MacroCallFunction): boolean { + const macroName = child.macroName; + const infos = sa.macroDefineList[macroName]; + return infos?.some((info) => info.valueType === MacroValueType.MemberAccess) ?? false; + } + override codeGen(visitor: CodeGenVisitor): string { return this.setCache(visitor.visitVariableIdentifier(this)); } diff --git a/packages/shader-lab/src/parser/builtin/functions.ts b/packages/shader-lab/src/parser/builtin/functions.ts index 46c0d49d29..859951641c 100644 --- a/packages/shader-lab/src/parser/builtin/functions.ts +++ b/packages/shader-lab/src/parser/builtin/functions.ts @@ -26,6 +26,39 @@ function isGenericType(t: BuiltinType) { return t >= EGenType.GenType && t <= EGenType.GSampler2DArray; } +/** + * Resolve a generic return type from the actual type of a generic parameter. + * + * For GVec4 return type, maps sampler variants to the correct vec4 type: + * sampler2D/sampler3D/samplerCube → vec4 + * isampler2D/isampler3D/... → ivec4 + * usampler2D/usampler3D/... → uvec4 + * + * For all other generic return types (GenType etc.), passes through the actual param type directly. + */ +function resolveGenericReturnType( + genericReturnType: EGenType, + actualParamType: NonGenericGalaceanType +): NonGenericGalaceanType { + if (genericReturnType === EGenType.GVec4) { + switch (actualParamType) { + case Keyword.I_SAMPLER2D: + case Keyword.I_SAMPLER3D: + case Keyword.I_SAMPLER_CUBE: + case Keyword.I_SAMPLER2D_ARRAY: + return Keyword.IVEC4; + case Keyword.U_SAMPLER2D: + case Keyword.U_SAMPLER3D: + case Keyword.U_SAMPLER_CUBE: + case Keyword.U_SAMPLER2D_ARRAY: + return Keyword.UVEC4; + default: + return Keyword.VEC4; + } + } + return actualParamType; +} + const BuiltinFunctionTable: Map = new Map(); export class BuiltinFunction { @@ -78,12 +111,14 @@ export class BuiltinFunction { const argLength = fnArgs.length; if (argLength !== parameterTypes.length) continue; // Try to match generic parameter type. - let returnType = TypeAny; + let resolvedReturnType: NonGenericGalaceanType = TypeAny; let found = true; for (let i = 0; i < argLength; i++) { const curFnArg = fnArgs[i]; if (isGenericType(curFnArg)) { - if (returnType === TypeAny) returnType = parameterTypes[i]; + if (resolvedReturnType === TypeAny) { + resolvedReturnType = resolveGenericReturnType(fn._returnType as EGenType, parameterTypes[i]); + } } else { if (curFnArg !== parameterTypes[i] && parameterTypes[i] !== TypeAny) { found = false; @@ -92,7 +127,9 @@ export class BuiltinFunction { } } if (found) { - fn._realReturnType = returnType; + fn._realReturnType = isGenericType(fn._returnType) + ? resolvedReturnType + : (fn._returnType as NonGenericGalaceanType); return fn; } } @@ -300,13 +337,14 @@ BuiltinFunction._create("textureLod", EGenType.GVec4, EGenType.GSampler3D, Keywo BuiltinFunction._create("textureLod", EGenType.GVec4, EGenType.GSamplerCube, Keyword.VEC3, Keyword.FLOAT); BuiltinFunction._create("textureLod", Keyword.FLOAT, Keyword.SAMPLER2D_SHADOW, Keyword.VEC3, Keyword.FLOAT); BuiltinFunction._create("textureLod", EGenType.GVec4, EGenType.GSampler2DArray, Keyword.VEC3, Keyword.FLOAT); +BuiltinFunction._create("texture2DLod", Keyword.VEC4, Keyword.SAMPLER2D, Keyword.VEC2, Keyword.FLOAT); BuiltinFunction._create("texture2DLodEXT", EGenType.GVec4, EGenType.GSampler2D, Keyword.VEC2, Keyword.FLOAT); BuiltinFunction._create("texture2DLodEXT", EGenType.GVec4, EGenType.GSampler3D, Keyword.VEC3, Keyword.FLOAT); -BuiltinFunction._create("textureCube", Keyword.SAMPLER_CUBE, Keyword.VEC3); -BuiltinFunction._create("textureCube", Keyword.SAMPLER_CUBE, Keyword.VEC3, Keyword.FLOAT); +BuiltinFunction._create("textureCube", Keyword.VEC4, Keyword.SAMPLER_CUBE, Keyword.VEC3); +BuiltinFunction._create("textureCube", Keyword.VEC4, Keyword.SAMPLER_CUBE, Keyword.VEC3, Keyword.FLOAT); BuiltinFunction._create("textureCube", EGenType.GVec4, EGenType.GSamplerCube, Keyword.VEC3, Keyword.FLOAT); -BuiltinFunction._create("textureCubeLod", Keyword.SAMPLER_CUBE, Keyword.VEC3, Keyword.FLOAT); +BuiltinFunction._create("textureCubeLod", Keyword.VEC4, Keyword.SAMPLER_CUBE, Keyword.VEC3, Keyword.FLOAT); BuiltinFunction._create("textureCubeLodEXT", EGenType.GVec4, EGenType.GSamplerCube, Keyword.VEC3, Keyword.FLOAT); BuiltinFunction._create( diff --git a/packages/shader/package.json b/packages/shader/package.json index e7e7eff423..471d4ee963 100644 --- a/packages/shader/package.json +++ b/packages/shader/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-shader", - "version": "2.0.0-alpha.24", + "version": "0.0.0-experimental-2.0-game.19", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/shader/src/global.d.ts b/packages/shader/src/global.d.ts index d0abf1234f..8ce5f33757 100644 --- a/packages/shader/src/global.d.ts +++ b/packages/shader/src/global.d.ts @@ -3,7 +3,7 @@ declare module "*.glsl" { export default value; } -declare module "*.gs" { +declare module "*.shader" { const value: string; export default value; } diff --git a/packages/shader/src/shaders/PBR.gs b/packages/shader/src/shaders/PBR.shader similarity index 100% rename from packages/shader/src/shaders/PBR.gs rename to packages/shader/src/shaders/PBR.shader diff --git a/packages/shader/src/shaders/Transform.glsl b/packages/shader/src/shaders/Transform.glsl index b5d1a52a68..1af76a0f0e 100644 --- a/packages/shader/src/shaders/Transform.glsl +++ b/packages/shader/src/shaders/Transform.glsl @@ -1,16 +1,17 @@ #ifndef TRANSFORM_INCLUDED #define TRANSFORM_INCLUDED -mat4 renderer_LocalMat; -mat4 renderer_ModelMat; mat4 camera_ViewMat; mat4 camera_ProjMat; -mat4 renderer_MVMat; -mat4 renderer_MVPMat; -mat4 renderer_NormalMat; +mat4 camera_VPMat; vec3 camera_Position; -vec3 camera_Forward; +vec3 camera_Forward; vec4 camera_ProjectionParams; -#endif \ No newline at end of file +mat4 renderer_ModelMat; +mat4 renderer_MVMat; +mat4 renderer_MVPMat; +mat4 renderer_NormalMat; + +#endif diff --git a/packages/shader/src/shaders/index.ts b/packages/shader/src/shaders/index.ts index de23d2e392..3a0430c975 100644 --- a/packages/shader/src/shaders/index.ts +++ b/packages/shader/src/shaders/index.ts @@ -3,7 +3,7 @@ import Common from "./Common.glsl"; import Fog from "./Fog.glsl"; import Light from "./Light.glsl"; import Normal from "./Normal.glsl"; -import PBRSource from "./PBR.gs"; +import PBRSource from "./PBR.shader"; import Shadow from "./Shadow.glsl"; import ShadowSampleTent from "./ShadowSampleTent.glsl"; import Skin from "./Skin.glsl"; diff --git a/packages/shader/src/shaders/shadingPBR/VertexPBR.glsl b/packages/shader/src/shaders/shadingPBR/VertexPBR.glsl index 134f2405f2..c9e964d6ec 100644 --- a/packages/shader/src/shaders/shadingPBR/VertexPBR.glsl +++ b/packages/shader/src/shaders/shadingPBR/VertexPBR.glsl @@ -69,10 +69,11 @@ VertexInputs getVertexInputs(Attributes attributes){ // TBN world space #ifdef RENDERER_HAS_NORMAL - inputs.normalWS = normalize( mat3(renderer_NormalMat) * normal ); + mat3 normalMat = mat3(renderer_NormalMat); + inputs.normalWS = normalize( normalMat * normal ); #ifdef RENDERER_HAS_TANGENT - vec3 tangentWS = normalize( mat3(renderer_NormalMat) * tangent.xyz ); + vec3 tangentWS = normalize( normalMat * tangent.xyz ); vec3 bitangentWS = cross( inputs.normalWS, tangentWS ) * tangent.w; inputs.tangentWS = tangentWS; @@ -85,7 +86,7 @@ VertexInputs getVertexInputs(Attributes attributes){ vec4 positionWS = renderer_ModelMat * position; inputs.positionWS = positionWS.xyz / positionWS.w; - #if SCENE_FOG_MODE != 0 + #if SCENE_FOG_MODE != 0 vec4 positionVS = renderer_MVMat * position; inputs.positionVS = positionVS.xyz / positionVS.w; #endif diff --git a/packages/spine-core-3.8/package.json b/packages/spine-core-3.8/package.json new file mode 100644 index 0000000000..815ae42878 --- /dev/null +++ b/packages/spine-core-3.8/package.json @@ -0,0 +1,40 @@ +{ + "name": "@galacean/engine-spine-core-3.8", + "version": "0.0.0-experimental-2.0-game.19", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, + "repository": { + "url": "https://github.com/galacean/engine.git" + }, + "license": "MIT", + "main": "dist/main.js", + "module": "dist/module.js", + "debug": "src/index.ts", + "browser": "dist/browser.js", + "types": "types/index.d.ts", + "scripts": { + "b:types": "tsc" + }, + "umd": { + "name": "Galacean.SpineCore38", + "globals": { + "@galacean/engine": "Galacean", + "@galacean/engine-spine": "Galacean.Spine" + } + }, + "files": [ + "dist/**/*", + "types/**/*" + ], + "dependencies": { + "@galacean/engine-spine": "workspace:*" + }, + "devDependencies": { + "@galacean/engine": "workspace:*" + }, + "peerDependencies": { + "@galacean/engine": "workspace:*" + } +} diff --git a/packages/spine-core-3.8/src/Spine38Runtime.ts b/packages/spine-core-3.8/src/Spine38Runtime.ts new file mode 100644 index 0000000000..f2d8a9c7e9 --- /dev/null +++ b/packages/spine-core-3.8/src/Spine38Runtime.ts @@ -0,0 +1,94 @@ +import { + AnimationState, + AnimationStateData, + AtlasAttachmentLoader, + Skeleton, + SkeletonBinary, + SkeletonData, + SkeletonJson, + TextureAtlas +} from "./spine-core"; +import type { Texture2D } from "@galacean/engine"; +import type { ISpineRenderTarget } from "@galacean/engine-spine"; +import { SpineGenerator } from "./SpineGenerator"; +import { SpineTexture } from "./SpineTexture"; + +/** + * Spine 3.8 runtime backend. + * + * @remarks + * Owns everything specific to the spine 3.8 runtime line: parsing (`SkeletonJson`/`SkeletonBinary`/ + * `TextureAtlas`), object-model construction, world-transform stepping (3.8 has no `Physics` argument), + * and the attachment-reading mesh generator. Registered automatically when this package is imported. + * + * Deliberately does not `implements ISpineRuntime`: that interface types its methods against the + * 4.2 npm package as the "canonical contract", but 4.2 and 3.8 have actually diverged enough + * (4.2 added `TextureAtlas.setTextures`; 4.2 dropped `Skeleton`/`SkeletonData`'s `updateCacheReset`/ + * `findBoneIndex`/`findSlotIndex`/`findPathConstraintIndex`, all present in 3.8) that TS's structural + * check fails even though every member `ISpineRuntime` actually calls lines up. Registered via a cast + * in `index.ts` instead of fighting the type checker method-by-method. + */ +export class Spine38Runtime { + private _generator = new SpineGenerator(); + + parseAtlasPageNames(atlasText: string): string[] { + // Unlike 4.2, spine 3.8's `TextureAtlas` constructor requires a synchronous texture loader and + // calls `setFilters`/`setWraps` on whatever it returns immediately during parsing, so a bare + // `null` loader would throw; hand back a no-op stub instead (its return type is `any`). + const pageNames: string[] = []; + new TextureAtlas(atlasText, (path) => { + pageNames.push(path); + return { setFilters() {}, setWraps() {} }; + }); + return pageNames; + } + + createTextureAtlas(atlasText: string, textures: Texture2D[]): TextureAtlas { + let index = 0; + return new TextureAtlas(atlasText, (path) => { + const engineTexture = textures.find((item) => item.name === path) || textures[index]; + index++; + return new SpineTexture(engineTexture); + }); + } + + createSkeletonData( + skeletonRawData: string | ArrayBuffer, + textureAtlas: TextureAtlas, + skeletonDataScale: number + ): SkeletonData { + const atlasLoader = new AtlasAttachmentLoader(textureAtlas); + if (typeof skeletonRawData === "string") { + const skeletonJson = new SkeletonJson(atlasLoader); + skeletonJson.scale = skeletonDataScale; + return skeletonJson.readSkeletonData(skeletonRawData); + } else { + const skeletonBinary = new SkeletonBinary(atlasLoader); + skeletonBinary.scale = skeletonDataScale; + return skeletonBinary.readSkeletonData(new Uint8Array(skeletonRawData as ArrayBuffer)); + } + } + + createAnimationStateData(skeletonData: SkeletonData): AnimationStateData { + return new AnimationStateData(skeletonData); + } + + createSkeleton(skeletonData: SkeletonData): Skeleton { + return new Skeleton(skeletonData); + } + + createAnimationState(stateData: AnimationStateData): AnimationState { + return new AnimationState(stateData); + } + + updateState(skeleton: Skeleton, state: AnimationState, delta: number): void { + state.update(delta); + state.apply(skeleton); + skeleton.update(delta); + skeleton.updateWorldTransform(); + } + + buildPrimitive(skeleton: Skeleton, target: ISpineRenderTarget): void { + this._generator.buildPrimitive(skeleton, target); + } +} diff --git a/packages/spine-core-3.8/src/SpineGenerator.ts b/packages/spine-core-3.8/src/SpineGenerator.ts new file mode 100644 index 0000000000..5ba048f320 --- /dev/null +++ b/packages/spine-core-3.8/src/SpineGenerator.ts @@ -0,0 +1,358 @@ +import { + ArrayLike, + BlendMode, + ClippingAttachment, + Color, + MeshAttachment, + RegionAttachment, + Skeleton, + SkeletonClipping, + TextureAtlasRegion +} from "./spine-core"; +import { BoundingBox, SubPrimitive } from "@galacean/engine"; +import { SpineTexture } from "./SpineTexture"; +import { ClearablePool } from "./util/ClearablePool"; +import { ReturnablePool } from "./util/ReturnablePool"; +import type { ISpineRenderTarget } from "@galacean/engine-spine"; +import { SpineBlendMode, SpineVertexStride } from "@galacean/engine-spine"; + +class SubRenderItem { + subPrimitive: SubPrimitive; + blendMode: BlendMode; + texture: any; +} + +/** + * @internal + */ +export class SpineGenerator { + static tempDark = new Color(); + static tempColor = new Color(); + static tempVerts = new Array(8); + static QUAD_TRIANGLES = [0, 1, 2, 2, 3, 0]; + static subPrimitivePool = new ReturnablePool(SubPrimitive); + static subRenderItemPool = new ClearablePool(SubRenderItem); + + private _subRenderItems: SubRenderItem[] = []; + private _clipper: SkeletonClipping = new SkeletonClipping(); + + buildPrimitive(skeleton: Skeleton, renderer: ISpineRenderTarget) { + const { _indices, _vertices, _localBounds, _vertexCount, _subPrimitives, zSpacing, premultipliedAlpha, tintBlack } = + renderer; + + _localBounds.min.set(Infinity, Infinity, Infinity); + _localBounds.max.set(-Infinity, -Infinity, -Infinity); + + const { _clipper, _subRenderItems } = this; + + const { tempVerts, subRenderItemPool, subPrimitivePool } = SpineGenerator; + const { withTint: vertexStrideWithTint, withoutTint: vertexStrideWithoutTint } = SpineVertexStride; + + _subRenderItems.length = 0; + subRenderItemPool.clear(); + + let triangles: Array; + let uvs: ArrayLike; + + let verticesLength = 0; + let indicesLength = 0; + let start = 0; + let count = 0; + + let blend = BlendMode.Normal; + let texture: SpineTexture = null; + let tempBlendMode: BlendMode | null = null; + let tempTexture: SpineTexture | null = null; + + let primitiveIndex = 0; + + const drawOrder = skeleton.drawOrder; + for (let slotIndex = 0, n = drawOrder.length; slotIndex < n; ++slotIndex) { + const slot = drawOrder[slotIndex]; + if (!slot.bone.active) { + _clipper.clipEndWithSlot(slot); + continue; + } + + const attachment = slot.getAttachment(); + if (!attachment) { + _clipper.clipEndWithSlot(slot); + continue; + } + + const z = zSpacing * slotIndex; + const isClipping = _clipper.isClipping(); + let numFloats = 0; + let attachmentColor: Color = null; + + // This vertexSize will be passed to spine-core's computeWorldVertices function. + // + // Expected format by computeWorldVertices: + // - Without tintBlack: [x, y, u, v, r, g, b, a] = 8 components + // - With tintBlack: [x, y, u, v, r, g, b, a, dr, dg, db, da] = 12 components + // + // Our actual vertex buffer format: + // - vertexStrideWithoutTint: [x, y, z, u, v, r, g, b, a] = 9 components + // - vertexStrideWithTint: [x, y, z, u, v, r, g, b, a, dr, dg, db] = 12 components + // (Note: we optimized 'da' as uniform instead of buffer attribute) + // + // Calculation: + // - Without tintBlack: 9 - 1 (remove z) = 8 ✓ + // - With tintBlack: 12 - 1 (remove z) + 1 (add back da) = 12 ✓ + const vertexSize = tintBlack ? vertexStrideWithTint : vertexStrideWithoutTint - 1; + const clippedVertexSize = isClipping ? 2 : vertexSize; + + switch (attachment.constructor) { + case RegionAttachment: + const regionAttachment = attachment; + attachmentColor = regionAttachment.color; + numFloats = clippedVertexSize << 2; + // spine 3.8's RegionAttachment.computeWorldVertices takes the Bone, not the Slot (4.2 takes Slot). + regionAttachment.computeWorldVertices(slot.bone, tempVerts, 0, clippedVertexSize); + triangles = SpineGenerator.QUAD_TRIANGLES; + uvs = regionAttachment.uvs; + // `region` is statically typed as the base TextureRegion (no `texture` field); it's + // always a TextureAtlasRegion in practice, since AtlasAttachmentLoader is the only + // source of regions. Its `.texture` is in turn always a SpineTexture, since + // createTextureAtlas is the only place that constructs one. + texture = (regionAttachment.region as TextureAtlasRegion).texture as unknown as SpineTexture; + break; + case MeshAttachment: + const meshAttachment = attachment; + attachmentColor = meshAttachment.color; + numFloats = (meshAttachment.worldVerticesLength >> 1) * clippedVertexSize; + if (numFloats > _vertices.length) { + SpineGenerator.tempVerts = new Array(numFloats); + } + meshAttachment.computeWorldVertices( + slot, + 0, + meshAttachment.worldVerticesLength, + tempVerts, + 0, + clippedVertexSize + ); + triangles = meshAttachment.triangles; + uvs = meshAttachment.uvs; + texture = (meshAttachment.region as TextureAtlasRegion).texture as unknown as SpineTexture; + break; + case ClippingAttachment: + const clip = attachment; + _clipper.clipStart(slot, clip); + continue; + default: + _clipper.clipEndWithSlot(slot); + continue; + } + + if (texture != null) { + let finalVertices: ArrayLike; + let finalVerticesLength: number; + let finalIndices: ArrayLike; + let finalIndicesLength: number; + + const skeleton = slot.bone.skeleton; + const skeletonColor = skeleton.color; + const slotColor = slot.color; + const finalColor = SpineGenerator.tempColor; + const finalAlpha = skeletonColor.a * slotColor.a * attachmentColor.a; + + finalColor.r = skeletonColor.r * slotColor.r * attachmentColor.r; + finalColor.g = skeletonColor.g * slotColor.g * attachmentColor.g; + finalColor.b = skeletonColor.b * slotColor.b * attachmentColor.b; + finalColor.a = finalAlpha; + + if (premultipliedAlpha) { + finalColor.r *= finalAlpha; + finalColor.g *= finalAlpha; + finalColor.b *= finalAlpha; + } + + const darkColor = SpineGenerator.tempDark; + const slotDarkColor = slot.darkColor; + if (!slotDarkColor) { + darkColor.set(0, 0, 0, 1); + } else { + if (premultipliedAlpha) { + darkColor.r = slotDarkColor.r * finalAlpha; + darkColor.g = slotDarkColor.g * finalAlpha; + darkColor.b = slotDarkColor.b * finalAlpha; + } else { + darkColor.setFromColor(slotDarkColor); + } + } + + if (isClipping) { + // spine 3.8's SkeletonClipping.clipTriangles takes an extra (unused) verticesLength param + // right after `vertices` that 4.2 dropped. + _clipper.clipTriangles( + tempVerts, + numFloats, + triangles, + triangles.length, + uvs, + finalColor, + darkColor, + tintBlack + ); + finalVertices = _clipper.clippedVertices; + finalVerticesLength = finalVertices.length; + finalIndices = _clipper.clippedTriangles; + finalIndicesLength = finalIndices.length; + } else { + const { r, g, b, a } = finalColor; + for (let v = 2, u = 0, n = numFloats; v < n; v += vertexSize, u += 2) { + tempVerts[v] = r; + tempVerts[v + 1] = g; + tempVerts[v + 2] = b; + tempVerts[v + 3] = a; + tempVerts[v + 4] = uvs[u]; + tempVerts[v + 5] = uvs[u + 1]; + if (tintBlack) { + tempVerts[v + 6] = darkColor.r; + tempVerts[v + 7] = darkColor.g; + tempVerts[v + 8] = darkColor.b; + tempVerts[v + 9] = darkColor.a; + } + } + finalVertices = tempVerts; + finalVerticesLength = numFloats; + finalIndices = triangles; + finalIndicesLength = triangles.length; + } + + if (finalVerticesLength == 0 || finalIndicesLength == 0) { + _clipper.clipEndWithSlot(slot); + continue; + } + + const stride = tintBlack ? vertexStrideWithTint : vertexStrideWithoutTint; + const indexStart = verticesLength / stride; + let i = verticesLength; + let j = 0; + for (; j < finalVerticesLength; ) { + const x = finalVertices[j++]; + const y = finalVertices[j++]; + _vertices[i++] = x; + _vertices[i++] = y; + _vertices[i++] = z; + _vertices[i++] = finalVertices[j++]; // u + _vertices[i++] = finalVertices[j++]; // v + _vertices[i++] = finalVertices[j++]; // r + _vertices[i++] = finalVertices[j++]; // g + _vertices[i++] = finalVertices[j++]; // b + _vertices[i++] = finalVertices[j++]; // a + if (tintBlack) { + _vertices[i++] = finalVertices[j++]; // darkR + _vertices[i++] = finalVertices[j++]; // darkG + _vertices[i++] = finalVertices[j++]; // darkB + j++; // darkA + } + this._expandBounds(x, y, z, _localBounds); + } + verticesLength = i; + + for (i = indicesLength, j = 0; j < finalIndicesLength; i++, j++) { + _indices[i] = finalIndices[j] + indexStart; + } + indicesLength += finalIndicesLength; + + const textureChanged = tempTexture !== null && tempTexture !== texture; + blend = slot.data.blendMode; + const blendModeChanged = tempBlendMode !== null && tempBlendMode !== blend; + + if (blendModeChanged || textureChanged) { + // Finish accumulated count first + if (count > 0) { + primitiveIndex = this._createRenderItem( + _subPrimitives, + primitiveIndex, + start, + count, + tempTexture, + tempBlendMode + ); + start += count; + count = 0; + } + } + count += finalIndicesLength; + tempTexture = texture; + tempBlendMode = blend; + } + + _clipper.clipEndWithSlot(slot); + } // slot traverse end + + // add reset sub primitive + if (count > 0) { + primitiveIndex = this._createRenderItem(_subPrimitives, primitiveIndex, start, count, texture, blend); + count = 0; + } + + _clipper.clipEnd(); + + const lastLen = _subPrimitives.length; + const curLen = _subRenderItems.length; + for (let i = curLen; i < lastLen; i++) { + const item = _subPrimitives[i]; + subPrimitivePool.return(item); + } + + renderer._clearSubPrimitives(); + for (let i = 0, l = curLen; i < l; ++i) { + const item = _subRenderItems[i]; + const { blendMode, texture } = item; + renderer._addSubPrimitive(item.subPrimitive); + const material = renderer._getMaterial(texture.getImage(), blendMode as unknown as SpineBlendMode); + renderer.setMaterial(i, material); + } + + if (indicesLength > _vertexCount || renderer._needResizeBuffer) { + renderer._createAndBindBuffer(indicesLength); + renderer._needResizeBuffer = false; + this.buildPrimitive(skeleton, renderer); + return; + } + + if (renderer._vertexBuffer) { + renderer._vertexBuffer.setData(_vertices); + renderer._indexBuffer.setData(_indices); + } + } + + private _createRenderItem( + subPrimitives: SubPrimitive[], + primitiveIndex: number, + start: number, + count: number, + texture: SpineTexture, + blend: BlendMode + ): number { + const { subPrimitivePool, subRenderItemPool } = SpineGenerator; + const origin = subPrimitives[primitiveIndex]; + + if (origin) { + primitiveIndex++; + } + + const subPrimitive = origin || subPrimitivePool.get(); + subPrimitive.start = start; + subPrimitive.count = count; + + const renderItem = subRenderItemPool.get(); + renderItem.blendMode = blend; + renderItem.subPrimitive = subPrimitive; + renderItem.texture = texture; + + this._subRenderItems.push(renderItem); + + return primitiveIndex; + } + + private _expandBounds(x: number, y: number, z: number, localBounds: BoundingBox) { + const { min, max } = localBounds; + min.set(Math.min(min.x, x), Math.min(min.y, y), Math.min(min.z, z)); + max.set(Math.max(max.x, x), Math.max(max.y, y), Math.max(max.z, z)); + } +} diff --git a/packages/spine-core-3.8/src/SpineTexture.ts b/packages/spine-core-3.8/src/SpineTexture.ts new file mode 100644 index 0000000000..cd604451ab --- /dev/null +++ b/packages/spine-core-3.8/src/SpineTexture.ts @@ -0,0 +1,50 @@ +import { Texture, TextureFilter, TextureWrap } from "./spine-core/Texture"; +import { Texture2D, TextureFilterMode, TextureWrapMode } from "@galacean/engine"; + +/** + * @internal + */ +export class SpineTexture extends Texture { + constructor(image: Texture2D) { + super(image); + image.generateMipmaps(); + } + + // The vendored 3.8 `Texture` base class exposes the Texture2D via a `texture` getter instead of + // spine-core 4.2's `getImage()`; this alias keeps SpineGenerator's call sites identical across backends. + getImage(): Texture2D { + return this._texture; + } + + setFilters(minFilter: TextureFilter, magFilter: TextureFilter) { + if (minFilter === TextureFilter.Nearest) { + this._texture.filterMode = TextureFilterMode.Point; + } else if (minFilter === TextureFilter.Linear) { + this._texture.filterMode = TextureFilterMode.Bilinear; + } else { + // The remaining min filters are the MipMap* family (mag filters are never MipMap*); + // mipmaps are generated in the constructor. + this._texture.filterMode = TextureFilterMode.Trilinear; + } + } + + setWraps(uWrap: TextureWrap, vWrap: TextureWrap) { + this._texture.wrapModeU = this._convertWrapMode(uWrap); + this._texture.wrapModeV = this._convertWrapMode(vWrap); + } + + dispose() {} + + private _convertWrapMode(wrap: TextureWrap): TextureWrapMode { + switch (wrap) { + case TextureWrap.ClampToEdge: + return TextureWrapMode.Clamp; + case TextureWrap.Repeat: + return TextureWrapMode.Repeat; + case TextureWrap.MirroredRepeat: + return TextureWrapMode.Mirror; + default: + throw new Error("Unsupported texture wrap mode."); + } + } +} diff --git a/packages/spine-core-3.8/src/index.ts b/packages/spine-core-3.8/src/index.ts new file mode 100644 index 0000000000..c9e0710bc4 --- /dev/null +++ b/packages/spine-core-3.8/src/index.ts @@ -0,0 +1,17 @@ +import type { ISpineRuntime } from "@galacean/engine-spine"; +import { registerSpineRuntime } from "@galacean/engine-spine"; +import { Spine38Runtime } from "./Spine38Runtime"; + +// Self-register on import: `import "@galacean/engine-spine-core-3.8"` wires the 3.8 backend +// into the shared `@galacean/engine-spine` package. Cast needed because Spine38Runtime doesn't +// `implements ISpineRuntime` at the type level — see the class doc comment for why. +registerSpineRuntime(new Spine38Runtime() as unknown as ISpineRuntime); + +export { Spine38Runtime }; + +// Re-export the vendored spine 3.8 runtime API. Power users that construct spine-core objects +// directly (custom attachments, manual skeleton parsing) import them from this core package. +export * from "./spine-core"; + +export const version = `__buildVersion`; +console.log(`Galacean spine-core-3.8 version: ${version}`); diff --git a/packages/spine-core-3.8/src/spine-core/Animation.ts b/packages/spine-core-3.8/src/spine-core/Animation.ts new file mode 100644 index 0000000000..e15edbdc9d --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/Animation.ts @@ -0,0 +1,1828 @@ +import { Skeleton } from "./Skeleton"; +import { Utils, MathUtils, ArrayLike } from "./Utils"; +import { Slot } from "./Slot"; +import { VertexAttachment, Attachment } from "./attachments/Attachment"; +import { IkConstraint } from "./IkConstraint"; +import { TransformConstraint } from "./TransformConstraint"; +import { PathConstraint } from "./PathConstraint"; +import { Event } from "./Event"; + +/** A simple container for a list of timelines and a name. */ +export class Animation { + /** The animation's name, which is unique across all animations in the skeleton. */ + name: string; + timelines: Array; + timelineIds: Array; + + /** The duration of the animation in seconds, which is the highest time of all keys in the timeline. */ + duration: number; + + constructor(name: string, timelines: Array, duration: number) { + if (name == null) throw new Error("name cannot be null."); + if (timelines == null) throw new Error("timelines cannot be null."); + this.name = name; + this.timelines = timelines; + this.timelineIds = []; + for (let i = 0; i < timelines.length; i++) this.timelineIds[timelines[i].getPropertyId()] = true; + this.duration = duration; + } + + hasTimeline(id: number) { + return this.timelineIds[id] == true; + } + + /** Applies all the animation's timelines to the specified skeleton. + * + * See Timeline {@link Timeline#apply(Skeleton, float, float, Array, float, MixBlend, MixDirection)}. + * @param loop If true, the animation repeats after {@link #getDuration()}. + * @param events May be null to ignore fired events. */ + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + loop: boolean, + events: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + if (skeleton == null) throw new Error("skeleton cannot be null."); + + if (loop && this.duration != 0) { + time %= this.duration; + if (lastTime > 0) lastTime %= this.duration; + } + + const timelines = this.timelines; + for (let i = 0, n = timelines.length; i < n; i++) + timelines[i].apply(skeleton, lastTime, time, events, alpha, blend, direction); + } + + /** @param target After the first and before the last value. + * @returns index of first value greater than the target. */ + static binarySearch(values: ArrayLike, target: number, step: number = 1) { + let low = 0; + let high = values.length / step - 2; + if (high == 0) return step; + let current = high >>> 1; + while (true) { + if (values[(current + 1) * step] <= target) low = current + 1; + else high = current; + if (low == high) return (low + 1) * step; + current = (low + high) >>> 1; + } + } + + static linearSearch(values: ArrayLike, target: number, step: number) { + for (let i = 0, last = values.length - step; i <= last; i += step) if (values[i] > target) return i; + return -1; + } +} + +/** The interface for all timelines. */ +export interface Timeline { + /** Applies this timeline to the skeleton. + * @param skeleton The skeleton the timeline is being applied to. This provides access to the bones, slots, and other + * skeleton components the timeline may change. + * @param lastTime The time this timeline was last applied. Timelines such as {@link EventTimeline}} trigger only at specific + * times rather than every frame. In that case, the timeline triggers everything between `lastTime` + * (exclusive) and `time` (inclusive). + * @param time The time within the animation. Most timelines find the key before and the key after this time so they can + * interpolate between the keys. + * @param events If any events are fired, they are added to this list. Can be null to ignore fired events or if the timeline + * does not fire events. + * @param alpha 0 applies the current or setup value (depending on `blend`). 1 applies the timeline value. + * Between 0 and 1 applies a value between the current or setup value and the timeline value. By adjusting + * `alpha` over time, an animation can be mixed in or out. `alpha` can also be useful to + * apply animations on top of each other (layering). + * @param blend Controls how mixing is applied when `alpha` < 1. + * @param direction Indicates whether the timeline is mixing in or out. Used by timelines which perform instant transitions, + * such as {@link DrawOrderTimeline} or {@link AttachmentTimeline}. */ + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + events: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ): void; + + /** Uniquely encodes both the type of this timeline and the skeleton property that it affects. */ + getPropertyId(): number; +} + +/** Controls how a timeline value is mixed with the setup pose value or current pose value when a timeline's `alpha` + * < 1. + * + * See Timeline {@link Timeline#apply(Skeleton, float, float, Array, float, MixBlend, MixDirection)}. */ +export enum MixBlend { + /** Transitions from the setup value to the timeline value (the current value is not used). Before the first key, the setup + * value is set. */ + setup, + /** Transitions from the current value to the timeline value. Before the first key, transitions from the current value to + * the setup value. Timelines which perform instant transitions, such as {@link DrawOrderTimeline} or + * {@link AttachmentTimeline}, use the setup value before the first key. + * + * `first` is intended for the first animations applied, not for animations layered on top of those. */ + first, + /** Transitions from the current value to the timeline value. No change is made before the first key (the current value is + * kept until the first key). + * + * `replace` is intended for animations layered on top of others, not for the first animations applied. */ + replace, + /** Transitions from the current value to the current value plus the timeline value. No change is made before the first key + * (the current value is kept until the first key). + * + * `add` is intended for animations layered on top of others, not for the first animations applied. Properties + * keyed by additive animations must be set manually or by another animation before applying the additive animations, else + * the property values will increase continually. */ + add +} + +/** Indicates whether a timeline's `alpha` is mixing out over time toward 0 (the setup or current pose value) or + * mixing in toward 1 (the timeline's value). + * + * See Timeline {@link Timeline#apply(Skeleton, float, float, Array, float, MixBlend, MixDirection)}. */ +export enum MixDirection { + mixIn, + mixOut +} + +export enum TimelineType { + rotate, + translate, + scale, + shear, + attachment, + color, + deform, + event, + drawOrder, + ikConstraint, + transformConstraint, + pathConstraintPosition, + pathConstraintSpacing, + pathConstraintMix, + twoColor +} + +/** The base class for timelines that use interpolation between key frame values. */ +export abstract class CurveTimeline implements Timeline { + static LINEAR = 0; + static STEPPED = 1; + static BEZIER = 2; + static BEZIER_SIZE = 10 * 2 - 1; + + private curves: ArrayLike; // type, x, y, ... + + abstract getPropertyId(): number; + + constructor(frameCount: number) { + if (frameCount <= 0) throw new Error("frameCount must be > 0: " + frameCount); + this.curves = Utils.newFloatArray((frameCount - 1) * CurveTimeline.BEZIER_SIZE); + } + + /** The number of key frames for this timeline. */ + getFrameCount() { + return this.curves.length / CurveTimeline.BEZIER_SIZE + 1; + } + + /** Sets the specified key frame to linear interpolation. */ + setLinear(frameIndex: number) { + this.curves[frameIndex * CurveTimeline.BEZIER_SIZE] = CurveTimeline.LINEAR; + } + + /** Sets the specified key frame to stepped interpolation. */ + setStepped(frameIndex: number) { + this.curves[frameIndex * CurveTimeline.BEZIER_SIZE] = CurveTimeline.STEPPED; + } + + /** Returns the interpolation type for the specified key frame. + * @returns Linear is 0, stepped is 1, Bezier is 2. */ + getCurveType(frameIndex: number): number { + const index = frameIndex * CurveTimeline.BEZIER_SIZE; + if (index == this.curves.length) return CurveTimeline.LINEAR; + const type = this.curves[index]; + if (type == CurveTimeline.LINEAR) return CurveTimeline.LINEAR; + if (type == CurveTimeline.STEPPED) return CurveTimeline.STEPPED; + return CurveTimeline.BEZIER; + } + + /** Sets the specified key frame to Bezier interpolation. `cx1` and `cx2` are from 0 to 1, + * representing the percent of time between the two key frames. `cy1` and `cy2` are the percent of the + * difference between the key frame's values. */ + setCurve(frameIndex: number, cx1: number, cy1: number, cx2: number, cy2: number) { + const tmpx = (-cx1 * 2 + cx2) * 0.03, + tmpy = (-cy1 * 2 + cy2) * 0.03; + const dddfx = ((cx1 - cx2) * 3 + 1) * 0.006, + dddfy = ((cy1 - cy2) * 3 + 1) * 0.006; + let ddfx = tmpx * 2 + dddfx, + ddfy = tmpy * 2 + dddfy; + let dfx = cx1 * 0.3 + tmpx + dddfx * 0.16666667, + dfy = cy1 * 0.3 + tmpy + dddfy * 0.16666667; + + let i = frameIndex * CurveTimeline.BEZIER_SIZE; + const curves = this.curves; + curves[i++] = CurveTimeline.BEZIER; + + let x = dfx, + y = dfy; + for (let n = i + CurveTimeline.BEZIER_SIZE - 1; i < n; i += 2) { + curves[i] = x; + curves[i + 1] = y; + dfx += ddfx; + dfy += ddfy; + ddfx += dddfx; + ddfy += dddfy; + x += dfx; + y += dfy; + } + } + + /** Returns the interpolated percentage for the specified key frame and linear percentage. */ + getCurvePercent(frameIndex: number, percent: number) { + percent = MathUtils.clamp(percent, 0, 1); + const curves = this.curves; + let i = frameIndex * CurveTimeline.BEZIER_SIZE; + const type = curves[i]; + if (type == CurveTimeline.LINEAR) return percent; + if (type == CurveTimeline.STEPPED) return 0; + i++; + let x = 0; + for (let start = i, n = i + CurveTimeline.BEZIER_SIZE - 1; i < n; i += 2) { + x = curves[i]; + if (x >= percent) { + let prevX: number, prevY: number; + if (i == start) { + prevX = 0; + prevY = 0; + } else { + prevX = curves[i - 2]; + prevY = curves[i - 1]; + } + return prevY + ((curves[i + 1] - prevY) * (percent - prevX)) / (x - prevX); + } + } + const y = curves[i - 1]; + return y + ((1 - y) * (percent - x)) / (1 - x); // Last point is 1,1. + } + + abstract apply( + skeleton: Skeleton, + lastTime: number, + time: number, + events: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ): void; +} + +/** Changes a bone's local {@link Bone#rotation}. */ +export class RotateTimeline extends CurveTimeline { + static ENTRIES = 2; + static PREV_TIME = -2; + static PREV_ROTATION = -1; + static ROTATION = 1; + + /** The index of the bone in {@link Skeleton#bones} that will be changed. */ + boneIndex: number; + + /** The time in seconds and rotation in degrees for each key frame. */ + frames: ArrayLike; // time, degrees, ... + + constructor(frameCount: number) { + super(frameCount); + this.frames = Utils.newFloatArray(frameCount << 1); + } + + getPropertyId() { + return (TimelineType.rotate << 24) + this.boneIndex; + } + + /** Sets the time and angle of the specified keyframe. */ + setFrame(frameIndex: number, time: number, degrees: number) { + frameIndex <<= 1; + this.frames[frameIndex] = time; + this.frames[frameIndex + RotateTimeline.ROTATION] = degrees; + } + + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + events: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const frames = this.frames; + + const bone = skeleton.bones[this.boneIndex]; + if (!bone.active) return; + if (time < frames[0]) { + switch (blend) { + case MixBlend.setup: + bone.rotation = bone.data.rotation; + return; + case MixBlend.first: + const r = bone.data.rotation - bone.rotation; + bone.rotation += (r - (16384 - ((16384.499999999996 - r / 360) | 0)) * 360) * alpha; + } + return; + } + + if (time >= frames[frames.length - RotateTimeline.ENTRIES]) { + // Time is after last frame. + let r = frames[frames.length + RotateTimeline.PREV_ROTATION]; + switch (blend) { + case MixBlend.setup: + bone.rotation = bone.data.rotation + r * alpha; + break; + case MixBlend.first: + case MixBlend.replace: + r += bone.data.rotation - bone.rotation; + r -= (16384 - ((16384.499999999996 - r / 360) | 0)) * 360; // Wrap within -180 and 180. + case MixBlend.add: + bone.rotation += r * alpha; + } + return; + } + + // Interpolate between the previous frame and the current frame. + const frame = Animation.binarySearch(frames, time, RotateTimeline.ENTRIES); + const prevRotation = frames[frame + RotateTimeline.PREV_ROTATION]; + const frameTime = frames[frame]; + const percent = this.getCurvePercent( + (frame >> 1) - 1, + 1 - (time - frameTime) / (frames[frame + RotateTimeline.PREV_TIME] - frameTime) + ); + + let r = frames[frame + RotateTimeline.ROTATION] - prevRotation; + r = prevRotation + (r - (16384 - ((16384.499999999996 - r / 360) | 0)) * 360) * percent; + switch (blend) { + case MixBlend.setup: + bone.rotation = bone.data.rotation + (r - (16384 - ((16384.499999999996 - r / 360) | 0)) * 360) * alpha; + break; + case MixBlend.first: + case MixBlend.replace: + r += bone.data.rotation - bone.rotation; + case MixBlend.add: + bone.rotation += (r - (16384 - ((16384.499999999996 - r / 360) | 0)) * 360) * alpha; + } + } +} + +/** Changes a bone's local {@link Bone#x} and {@link Bone#y}. */ +export class TranslateTimeline extends CurveTimeline { + static ENTRIES = 3; + static PREV_TIME = -3; + static PREV_X = -2; + static PREV_Y = -1; + static X = 1; + static Y = 2; + + /** The index of the bone in {@link Skeleton#bones} that will be changed. */ + boneIndex: number; + + /** The time in seconds, x, and y values for each key frame. */ + frames: ArrayLike; // time, x, y, ... + + constructor(frameCount: number) { + super(frameCount); + this.frames = Utils.newFloatArray(frameCount * TranslateTimeline.ENTRIES); + } + + getPropertyId() { + return (TimelineType.translate << 24) + this.boneIndex; + } + + /** Sets the time in seconds, x, and y values for the specified key frame. */ + setFrame(frameIndex: number, time: number, x: number, y: number) { + frameIndex *= TranslateTimeline.ENTRIES; + this.frames[frameIndex] = time; + this.frames[frameIndex + TranslateTimeline.X] = x; + this.frames[frameIndex + TranslateTimeline.Y] = y; + } + + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + events: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const frames = this.frames; + + const bone = skeleton.bones[this.boneIndex]; + if (!bone.active) return; + if (time < frames[0]) { + switch (blend) { + case MixBlend.setup: + bone.x = bone.data.x; + bone.y = bone.data.y; + return; + case MixBlend.first: + bone.x += (bone.data.x - bone.x) * alpha; + bone.y += (bone.data.y - bone.y) * alpha; + } + return; + } + + let x = 0, + y = 0; + if (time >= frames[frames.length - TranslateTimeline.ENTRIES]) { + // Time is after last frame. + x = frames[frames.length + TranslateTimeline.PREV_X]; + y = frames[frames.length + TranslateTimeline.PREV_Y]; + } else { + // Interpolate between the previous frame and the current frame. + const frame = Animation.binarySearch(frames, time, TranslateTimeline.ENTRIES); + x = frames[frame + TranslateTimeline.PREV_X]; + y = frames[frame + TranslateTimeline.PREV_Y]; + const frameTime = frames[frame]; + const percent = this.getCurvePercent( + frame / TranslateTimeline.ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + TranslateTimeline.PREV_TIME] - frameTime) + ); + + x += (frames[frame + TranslateTimeline.X] - x) * percent; + y += (frames[frame + TranslateTimeline.Y] - y) * percent; + } + switch (blend) { + case MixBlend.setup: + bone.x = bone.data.x + x * alpha; + bone.y = bone.data.y + y * alpha; + break; + case MixBlend.first: + case MixBlend.replace: + bone.x += (bone.data.x + x - bone.x) * alpha; + bone.y += (bone.data.y + y - bone.y) * alpha; + break; + case MixBlend.add: + bone.x += x * alpha; + bone.y += y * alpha; + } + } +} + +/** Changes a bone's local {@link Bone#scaleX)} and {@link Bone#scaleY}. */ +export class ScaleTimeline extends TranslateTimeline { + constructor(frameCount: number) { + super(frameCount); + } + + override getPropertyId() { + return (TimelineType.scale << 24) + this.boneIndex; + } + + override apply( + skeleton: Skeleton, + lastTime: number, + time: number, + events: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const frames = this.frames; + + const bone = skeleton.bones[this.boneIndex]; + if (!bone.active) return; + if (time < frames[0]) { + switch (blend) { + case MixBlend.setup: + bone.scaleX = bone.data.scaleX; + bone.scaleY = bone.data.scaleY; + return; + case MixBlend.first: + bone.scaleX += (bone.data.scaleX - bone.scaleX) * alpha; + bone.scaleY += (bone.data.scaleY - bone.scaleY) * alpha; + } + return; + } + + let x = 0, + y = 0; + if (time >= frames[frames.length - ScaleTimeline.ENTRIES]) { + // Time is after last frame. + x = frames[frames.length + ScaleTimeline.PREV_X] * bone.data.scaleX; + y = frames[frames.length + ScaleTimeline.PREV_Y] * bone.data.scaleY; + } else { + // Interpolate between the previous frame and the current frame. + const frame = Animation.binarySearch(frames, time, ScaleTimeline.ENTRIES); + x = frames[frame + ScaleTimeline.PREV_X]; + y = frames[frame + ScaleTimeline.PREV_Y]; + const frameTime = frames[frame]; + const percent = this.getCurvePercent( + frame / ScaleTimeline.ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + ScaleTimeline.PREV_TIME] - frameTime) + ); + + x = (x + (frames[frame + ScaleTimeline.X] - x) * percent) * bone.data.scaleX; + y = (y + (frames[frame + ScaleTimeline.Y] - y) * percent) * bone.data.scaleY; + } + if (alpha == 1) { + if (blend == MixBlend.add) { + bone.scaleX += x - bone.data.scaleX; + bone.scaleY += y - bone.data.scaleY; + } else { + bone.scaleX = x; + bone.scaleY = y; + } + } else { + let bx = 0, + by = 0; + if (direction == MixDirection.mixOut) { + switch (blend) { + case MixBlend.setup: + bx = bone.data.scaleX; + by = bone.data.scaleY; + bone.scaleX = bx + (Math.abs(x) * MathUtils.signum(bx) - bx) * alpha; + bone.scaleY = by + (Math.abs(y) * MathUtils.signum(by) - by) * alpha; + break; + case MixBlend.first: + case MixBlend.replace: + bx = bone.scaleX; + by = bone.scaleY; + bone.scaleX = bx + (Math.abs(x) * MathUtils.signum(bx) - bx) * alpha; + bone.scaleY = by + (Math.abs(y) * MathUtils.signum(by) - by) * alpha; + break; + case MixBlend.add: + bx = bone.scaleX; + by = bone.scaleY; + bone.scaleX = bx + (Math.abs(x) * MathUtils.signum(bx) - bone.data.scaleX) * alpha; + bone.scaleY = by + (Math.abs(y) * MathUtils.signum(by) - bone.data.scaleY) * alpha; + } + } else { + switch (blend) { + case MixBlend.setup: + bx = Math.abs(bone.data.scaleX) * MathUtils.signum(x); + by = Math.abs(bone.data.scaleY) * MathUtils.signum(y); + bone.scaleX = bx + (x - bx) * alpha; + bone.scaleY = by + (y - by) * alpha; + break; + case MixBlend.first: + case MixBlend.replace: + bx = Math.abs(bone.scaleX) * MathUtils.signum(x); + by = Math.abs(bone.scaleY) * MathUtils.signum(y); + bone.scaleX = bx + (x - bx) * alpha; + bone.scaleY = by + (y - by) * alpha; + break; + case MixBlend.add: + bx = MathUtils.signum(x); + by = MathUtils.signum(y); + bone.scaleX = Math.abs(bone.scaleX) * bx + (x - Math.abs(bone.data.scaleX) * bx) * alpha; + bone.scaleY = Math.abs(bone.scaleY) * by + (y - Math.abs(bone.data.scaleY) * by) * alpha; + } + } + } + } +} + +/** Changes a bone's local {@link Bone#shearX} and {@link Bone#shearY}. */ +export class ShearTimeline extends TranslateTimeline { + constructor(frameCount: number) { + super(frameCount); + } + + override getPropertyId() { + return (TimelineType.shear << 24) + this.boneIndex; + } + + override apply( + skeleton: Skeleton, + lastTime: number, + time: number, + events: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const frames = this.frames; + + const bone = skeleton.bones[this.boneIndex]; + if (!bone.active) return; + if (time < frames[0]) { + switch (blend) { + case MixBlend.setup: + bone.shearX = bone.data.shearX; + bone.shearY = bone.data.shearY; + return; + case MixBlend.first: + bone.shearX += (bone.data.shearX - bone.shearX) * alpha; + bone.shearY += (bone.data.shearY - bone.shearY) * alpha; + } + return; + } + + let x = 0, + y = 0; + if (time >= frames[frames.length - ShearTimeline.ENTRIES]) { + // Time is after last frame. + x = frames[frames.length + ShearTimeline.PREV_X]; + y = frames[frames.length + ShearTimeline.PREV_Y]; + } else { + // Interpolate between the previous frame and the current frame. + const frame = Animation.binarySearch(frames, time, ShearTimeline.ENTRIES); + x = frames[frame + ShearTimeline.PREV_X]; + y = frames[frame + ShearTimeline.PREV_Y]; + const frameTime = frames[frame]; + const percent = this.getCurvePercent( + frame / ShearTimeline.ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + ShearTimeline.PREV_TIME] - frameTime) + ); + + x = x + (frames[frame + ShearTimeline.X] - x) * percent; + y = y + (frames[frame + ShearTimeline.Y] - y) * percent; + } + switch (blend) { + case MixBlend.setup: + bone.shearX = bone.data.shearX + x * alpha; + bone.shearY = bone.data.shearY + y * alpha; + break; + case MixBlend.first: + case MixBlend.replace: + bone.shearX += (bone.data.shearX + x - bone.shearX) * alpha; + bone.shearY += (bone.data.shearY + y - bone.shearY) * alpha; + break; + case MixBlend.add: + bone.shearX += x * alpha; + bone.shearY += y * alpha; + } + } +} + +/** Changes a slot's {@link Slot#color}. */ +export class ColorTimeline extends CurveTimeline { + static ENTRIES = 5; + static PREV_TIME = -5; + static PREV_R = -4; + static PREV_G = -3; + static PREV_B = -2; + static PREV_A = -1; + static R = 1; + static G = 2; + static B = 3; + static A = 4; + + /** The index of the slot in {@link Skeleton#slots} that will be changed. */ + slotIndex: number; + + /** The time in seconds, red, green, blue, and alpha values for each key frame. */ + frames: ArrayLike; // time, r, g, b, a, ... + + constructor(frameCount: number) { + super(frameCount); + this.frames = Utils.newFloatArray(frameCount * ColorTimeline.ENTRIES); + } + + getPropertyId() { + return (TimelineType.color << 24) + this.slotIndex; + } + + /** Sets the time in seconds, red, green, blue, and alpha for the specified key frame. */ + setFrame(frameIndex: number, time: number, r: number, g: number, b: number, a: number) { + frameIndex *= ColorTimeline.ENTRIES; + this.frames[frameIndex] = time; + this.frames[frameIndex + ColorTimeline.R] = r; + this.frames[frameIndex + ColorTimeline.G] = g; + this.frames[frameIndex + ColorTimeline.B] = b; + this.frames[frameIndex + ColorTimeline.A] = a; + } + + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + events: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const slot = skeleton.slots[this.slotIndex]; + if (!slot.bone.active) return; + const frames = this.frames; + if (time < frames[0]) { + switch (blend) { + case MixBlend.setup: + slot.color.setFromColor(slot.data.color); + return; + case MixBlend.first: + const color = slot.color, + setup = slot.data.color; + color.add( + (setup.r - color.r) * alpha, + (setup.g - color.g) * alpha, + (setup.b - color.b) * alpha, + (setup.a - color.a) * alpha + ); + } + return; + } + + let r = 0, + g = 0, + b = 0, + a = 0; + if (time >= frames[frames.length - ColorTimeline.ENTRIES]) { + // Time is after last frame. + const i = frames.length; + r = frames[i + ColorTimeline.PREV_R]; + g = frames[i + ColorTimeline.PREV_G]; + b = frames[i + ColorTimeline.PREV_B]; + a = frames[i + ColorTimeline.PREV_A]; + } else { + // Interpolate between the previous frame and the current frame. + const frame = Animation.binarySearch(frames, time, ColorTimeline.ENTRIES); + r = frames[frame + ColorTimeline.PREV_R]; + g = frames[frame + ColorTimeline.PREV_G]; + b = frames[frame + ColorTimeline.PREV_B]; + a = frames[frame + ColorTimeline.PREV_A]; + const frameTime = frames[frame]; + const percent = this.getCurvePercent( + frame / ColorTimeline.ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + ColorTimeline.PREV_TIME] - frameTime) + ); + + r += (frames[frame + ColorTimeline.R] - r) * percent; + g += (frames[frame + ColorTimeline.G] - g) * percent; + b += (frames[frame + ColorTimeline.B] - b) * percent; + a += (frames[frame + ColorTimeline.A] - a) * percent; + } + if (alpha == 1) slot.color.set(r, g, b, a); + else { + const color = slot.color; + if (blend == MixBlend.setup) color.setFromColor(slot.data.color); + color.add((r - color.r) * alpha, (g - color.g) * alpha, (b - color.b) * alpha, (a - color.a) * alpha); + } + } +} + +/** Changes a slot's {@link Slot#color} and {@link Slot#darkColor} for two color tinting. */ +export class TwoColorTimeline extends CurveTimeline { + static ENTRIES = 8; + static PREV_TIME = -8; + static PREV_R = -7; + static PREV_G = -6; + static PREV_B = -5; + static PREV_A = -4; + static PREV_R2 = -3; + static PREV_G2 = -2; + static PREV_B2 = -1; + static R = 1; + static G = 2; + static B = 3; + static A = 4; + static R2 = 5; + static G2 = 6; + static B2 = 7; + + /** The index of the slot in {@link Skeleton#slots()} that will be changed. The {@link Slot#darkColor()} must not be + * null. */ + slotIndex: number; + + /** The time in seconds, red, green, blue, and alpha values of the color, red, green, blue of the dark color, for each key frame. */ + frames: ArrayLike; // time, r, g, b, a, r2, g2, b2, ... + + constructor(frameCount: number) { + super(frameCount); + this.frames = Utils.newFloatArray(frameCount * TwoColorTimeline.ENTRIES); + } + + getPropertyId() { + return (TimelineType.twoColor << 24) + this.slotIndex; + } + + /** Sets the time in seconds, light, and dark colors for the specified key frame. */ + setFrame( + frameIndex: number, + time: number, + r: number, + g: number, + b: number, + a: number, + r2: number, + g2: number, + b2: number + ) { + frameIndex *= TwoColorTimeline.ENTRIES; + this.frames[frameIndex] = time; + this.frames[frameIndex + TwoColorTimeline.R] = r; + this.frames[frameIndex + TwoColorTimeline.G] = g; + this.frames[frameIndex + TwoColorTimeline.B] = b; + this.frames[frameIndex + TwoColorTimeline.A] = a; + this.frames[frameIndex + TwoColorTimeline.R2] = r2; + this.frames[frameIndex + TwoColorTimeline.G2] = g2; + this.frames[frameIndex + TwoColorTimeline.B2] = b2; + } + + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + events: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const slot = skeleton.slots[this.slotIndex]; + if (!slot.bone.active) return; + const frames = this.frames; + if (time < frames[0]) { + switch (blend) { + case MixBlend.setup: + slot.color.setFromColor(slot.data.color); + slot.darkColor.setFromColor(slot.data.darkColor); + return; + case MixBlend.first: + const light = slot.color, + dark = slot.darkColor, + setupLight = slot.data.color, + setupDark = slot.data.darkColor; + light.add( + (setupLight.r - light.r) * alpha, + (setupLight.g - light.g) * alpha, + (setupLight.b - light.b) * alpha, + (setupLight.a - light.a) * alpha + ); + dark.add((setupDark.r - dark.r) * alpha, (setupDark.g - dark.g) * alpha, (setupDark.b - dark.b) * alpha, 0); + } + return; + } + + let r = 0, + g = 0, + b = 0, + a = 0, + r2 = 0, + g2 = 0, + b2 = 0; + if (time >= frames[frames.length - TwoColorTimeline.ENTRIES]) { + // Time is after last frame. + const i = frames.length; + r = frames[i + TwoColorTimeline.PREV_R]; + g = frames[i + TwoColorTimeline.PREV_G]; + b = frames[i + TwoColorTimeline.PREV_B]; + a = frames[i + TwoColorTimeline.PREV_A]; + r2 = frames[i + TwoColorTimeline.PREV_R2]; + g2 = frames[i + TwoColorTimeline.PREV_G2]; + b2 = frames[i + TwoColorTimeline.PREV_B2]; + } else { + // Interpolate between the previous frame and the current frame. + const frame = Animation.binarySearch(frames, time, TwoColorTimeline.ENTRIES); + r = frames[frame + TwoColorTimeline.PREV_R]; + g = frames[frame + TwoColorTimeline.PREV_G]; + b = frames[frame + TwoColorTimeline.PREV_B]; + a = frames[frame + TwoColorTimeline.PREV_A]; + r2 = frames[frame + TwoColorTimeline.PREV_R2]; + g2 = frames[frame + TwoColorTimeline.PREV_G2]; + b2 = frames[frame + TwoColorTimeline.PREV_B2]; + const frameTime = frames[frame]; + const percent = this.getCurvePercent( + frame / TwoColorTimeline.ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + TwoColorTimeline.PREV_TIME] - frameTime) + ); + + r += (frames[frame + TwoColorTimeline.R] - r) * percent; + g += (frames[frame + TwoColorTimeline.G] - g) * percent; + b += (frames[frame + TwoColorTimeline.B] - b) * percent; + a += (frames[frame + TwoColorTimeline.A] - a) * percent; + r2 += (frames[frame + TwoColorTimeline.R2] - r2) * percent; + g2 += (frames[frame + TwoColorTimeline.G2] - g2) * percent; + b2 += (frames[frame + TwoColorTimeline.B2] - b2) * percent; + } + if (alpha == 1) { + slot.color.set(r, g, b, a); + slot.darkColor.set(r2, g2, b2, 1); + } else { + const light = slot.color, + dark = slot.darkColor; + if (blend == MixBlend.setup) { + light.setFromColor(slot.data.color); + dark.setFromColor(slot.data.darkColor); + } + light.add((r - light.r) * alpha, (g - light.g) * alpha, (b - light.b) * alpha, (a - light.a) * alpha); + dark.add((r2 - dark.r) * alpha, (g2 - dark.g) * alpha, (b2 - dark.b) * alpha, 0); + } + } +} + +/** Changes a slot's {@link Slot#attachment}. */ +export class AttachmentTimeline implements Timeline { + /** The index of the slot in {@link Skeleton#slots} that will be changed. */ + slotIndex: number; + + /** The time in seconds for each key frame. */ + frames: ArrayLike; // time, ... + + /** The attachment name for each key frame. May contain null values to clear the attachment. */ + attachmentNames: Array; + + constructor(frameCount: number) { + this.frames = Utils.newFloatArray(frameCount); + this.attachmentNames = new Array(frameCount); + } + + getPropertyId() { + return (TimelineType.attachment << 24) + this.slotIndex; + } + + /** The number of key frames for this timeline. */ + getFrameCount() { + return this.frames.length; + } + + /** Sets the time in seconds and the attachment name for the specified key frame. */ + setFrame(frameIndex: number, time: number, attachmentName: string) { + this.frames[frameIndex] = time; + this.attachmentNames[frameIndex] = attachmentName; + } + + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + events: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const slot = skeleton.slots[this.slotIndex]; + if (!slot.bone.active) return; + if (direction == MixDirection.mixOut) { + if (blend == MixBlend.setup) this.setAttachment(skeleton, slot, slot.data.attachmentName); + return; + } + + const frames = this.frames; + if (time < frames[0]) { + if (blend == MixBlend.setup || blend == MixBlend.first) + this.setAttachment(skeleton, slot, slot.data.attachmentName); + return; + } + + let frameIndex = 0; + if (time >= frames[frames.length - 1]) + // Time is after last frame. + frameIndex = frames.length - 1; + else frameIndex = Animation.binarySearch(frames, time, 1) - 1; + + const attachmentName = this.attachmentNames[frameIndex]; + skeleton.slots[this.slotIndex].setAttachment( + attachmentName == null ? null : skeleton.getAttachment(this.slotIndex, attachmentName) + ); + } + + setAttachment(skeleton: Skeleton, slot: Slot, attachmentName: string) { + slot.attachment = attachmentName == null ? null : skeleton.getAttachment(this.slotIndex, attachmentName); + } +} + +let zeros: ArrayLike = null; + +/** Changes a slot's {@link Slot#deform} to deform a {@link VertexAttachment}. */ +export class DeformTimeline extends CurveTimeline { + /** The index of the slot in {@link Skeleton#getSlots()} that will be changed. */ + slotIndex: number; + + /** The attachment that will be deformed. */ + attachment: VertexAttachment; + + /** The time in seconds for each key frame. */ + frames: ArrayLike; // time, ... + + /** The vertices for each key frame. */ + frameVertices: Array>; + + constructor(frameCount: number) { + super(frameCount); + this.frames = Utils.newFloatArray(frameCount); + this.frameVertices = new Array>(frameCount); + if (zeros == null) zeros = Utils.newFloatArray(64); + } + + getPropertyId() { + return (TimelineType.deform << 27) + +this.attachment.id + this.slotIndex; + } + + /** Sets the time in seconds and the vertices for the specified key frame. + * @param vertices Vertex positions for an unweighted VertexAttachment, or deform offsets if it has weights. */ + setFrame(frameIndex: number, time: number, vertices: ArrayLike) { + this.frames[frameIndex] = time; + this.frameVertices[frameIndex] = vertices; + } + + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + firedEvents: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const slot: Slot = skeleton.slots[this.slotIndex]; + if (!slot.bone.active) return; + const slotAttachment: Attachment = slot.getAttachment(); + if ( + !(slotAttachment instanceof VertexAttachment) || + !((slotAttachment).deformAttachment == this.attachment) + ) + return; + + const deformArray: Array = slot.deform; + if (deformArray.length == 0) blend = MixBlend.setup; + + const frameVertices = this.frameVertices; + const vertexCount = frameVertices[0].length; + + const frames = this.frames; + if (time < frames[0]) { + const vertexAttachment = slotAttachment; + switch (blend) { + case MixBlend.setup: + deformArray.length = 0; + return; + case MixBlend.first: + if (alpha == 1) { + deformArray.length = 0; + break; + } + const deform: Array = Utils.setArraySize(deformArray, vertexCount); + if (vertexAttachment.bones == null) { + // Unweighted vertex positions. + const setupVertices = vertexAttachment.vertices; + for (var i = 0; i < vertexCount; i++) deform[i] += (setupVertices[i] - deform[i]) * alpha; + } else { + // Weighted deform offsets. + alpha = 1 - alpha; + for (var i = 0; i < vertexCount; i++) deform[i] *= alpha; + } + } + return; + } + + const deform: Array = Utils.setArraySize(deformArray, vertexCount); + if (time >= frames[frames.length - 1]) { + // Time is after last frame. + const lastVertices = frameVertices[frames.length - 1]; + if (alpha == 1) { + if (blend == MixBlend.add) { + const vertexAttachment = slotAttachment as VertexAttachment; + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, with alpha. + const setupVertices = vertexAttachment.vertices; + for (let i = 0; i < vertexCount; i++) { + deform[i] += lastVertices[i] - setupVertices[i]; + } + } else { + // Weighted deform offsets, with alpha. + for (let i = 0; i < vertexCount; i++) deform[i] += lastVertices[i]; + } + } else { + Utils.arrayCopy(lastVertices, 0, deform, 0, vertexCount); + } + } else { + switch (blend) { + case MixBlend.setup: { + const vertexAttachment = slotAttachment as VertexAttachment; + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, with alpha. + const setupVertices = vertexAttachment.vertices; + for (let i = 0; i < vertexCount; i++) { + const setup = setupVertices[i]; + deform[i] = setup + (lastVertices[i] - setup) * alpha; + } + } else { + // Weighted deform offsets, with alpha. + for (let i = 0; i < vertexCount; i++) deform[i] = lastVertices[i] * alpha; + } + break; + } + case MixBlend.first: + case MixBlend.replace: + for (let i = 0; i < vertexCount; i++) deform[i] += (lastVertices[i] - deform[i]) * alpha; + break; + case MixBlend.add: + const vertexAttachment = slotAttachment as VertexAttachment; + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, with alpha. + const setupVertices = vertexAttachment.vertices; + for (let i = 0; i < vertexCount; i++) { + deform[i] += (lastVertices[i] - setupVertices[i]) * alpha; + } + } else { + // Weighted deform offsets, with alpha. + for (let i = 0; i < vertexCount; i++) deform[i] += lastVertices[i] * alpha; + } + } + } + return; + } + + // Interpolate between the previous frame and the current frame. + const frame = Animation.binarySearch(frames, time); + const prevVertices = frameVertices[frame - 1]; + const nextVertices = frameVertices[frame]; + const frameTime = frames[frame]; + const percent = this.getCurvePercent(frame - 1, 1 - (time - frameTime) / (frames[frame - 1] - frameTime)); + + if (alpha == 1) { + if (blend == MixBlend.add) { + const vertexAttachment = slotAttachment as VertexAttachment; + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, with alpha. + const setupVertices = vertexAttachment.vertices; + for (let i = 0; i < vertexCount; i++) { + const prev = prevVertices[i]; + deform[i] += prev + (nextVertices[i] - prev) * percent - setupVertices[i]; + } + } else { + // Weighted deform offsets, with alpha. + for (let i = 0; i < vertexCount; i++) { + const prev = prevVertices[i]; + deform[i] += prev + (nextVertices[i] - prev) * percent; + } + } + } else { + for (let i = 0; i < vertexCount; i++) { + const prev = prevVertices[i]; + deform[i] = prev + (nextVertices[i] - prev) * percent; + } + } + } else { + switch (blend) { + case MixBlend.setup: { + const vertexAttachment = slotAttachment as VertexAttachment; + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, with alpha. + const setupVertices = vertexAttachment.vertices; + for (let i = 0; i < vertexCount; i++) { + const prev = prevVertices[i], + setup = setupVertices[i]; + deform[i] = setup + (prev + (nextVertices[i] - prev) * percent - setup) * alpha; + } + } else { + // Weighted deform offsets, with alpha. + for (let i = 0; i < vertexCount; i++) { + const prev = prevVertices[i]; + deform[i] = (prev + (nextVertices[i] - prev) * percent) * alpha; + } + } + break; + } + case MixBlend.first: + case MixBlend.replace: + for (let i = 0; i < vertexCount; i++) { + const prev = prevVertices[i]; + deform[i] += (prev + (nextVertices[i] - prev) * percent - deform[i]) * alpha; + } + break; + case MixBlend.add: + const vertexAttachment = slotAttachment as VertexAttachment; + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, with alpha. + const setupVertices = vertexAttachment.vertices; + for (let i = 0; i < vertexCount; i++) { + const prev = prevVertices[i]; + deform[i] += (prev + (nextVertices[i] - prev) * percent - setupVertices[i]) * alpha; + } + } else { + // Weighted deform offsets, with alpha. + for (let i = 0; i < vertexCount; i++) { + const prev = prevVertices[i]; + deform[i] += (prev + (nextVertices[i] - prev) * percent) * alpha; + } + } + } + } + } +} + +/** Fires an {@link Event} when specific animation times are reached. */ +export class EventTimeline implements Timeline { + /** The time in seconds for each key frame. */ + frames: ArrayLike; // time, ... + + /** The event for each key frame. */ + events: Array; + + constructor(frameCount: number) { + this.frames = Utils.newFloatArray(frameCount); + this.events = new Array(frameCount); + } + + getPropertyId() { + return TimelineType.event << 24; + } + + /** The number of key frames for this timeline. */ + getFrameCount() { + return this.frames.length; + } + + /** Sets the time in seconds and the event for the specified key frame. */ + setFrame(frameIndex: number, event: Event) { + this.frames[frameIndex] = event.time; + this.events[frameIndex] = event; + } + + /** Fires events for frames > `lastTime` and <= `time`. */ + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + firedEvents: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + if (firedEvents == null) return; + const frames = this.frames; + const frameCount = this.frames.length; + + if (lastTime > time) { + // Fire events after last time for looped animations. + this.apply(skeleton, lastTime, Number.MAX_VALUE, firedEvents, alpha, blend, direction); + lastTime = -1; + } else if (lastTime >= frames[frameCount - 1]) + // Last time is after last frame. + return; + if (time < frames[0]) return; // Time is before first frame. + + let frame = 0; + if (lastTime < frames[0]) frame = 0; + else { + frame = Animation.binarySearch(frames, lastTime); + const frameTime = frames[frame]; + while (frame > 0) { + // Fire multiple events with the same frame. + if (frames[frame - 1] != frameTime) break; + frame--; + } + } + for (; frame < frameCount && time >= frames[frame]; frame++) firedEvents.push(this.events[frame]); + } +} + +/** Changes a skeleton's {@link Skeleton#drawOrder}. */ +export class DrawOrderTimeline implements Timeline { + /** The time in seconds for each key frame. */ + frames: ArrayLike; // time, ... + + /** The draw order for each key frame. See {@link #setFrame(int, float, int[])}. */ + drawOrders: Array>; + + constructor(frameCount: number) { + this.frames = Utils.newFloatArray(frameCount); + this.drawOrders = new Array>(frameCount); + } + + getPropertyId() { + return TimelineType.drawOrder << 24; + } + + /** The number of key frames for this timeline. */ + getFrameCount() { + return this.frames.length; + } + + /** Sets the time in seconds and the draw order for the specified key frame. + * @param drawOrder For each slot in {@link Skeleton#slots}, the index of the new draw order. May be null to use setup pose + * draw order. */ + setFrame(frameIndex: number, time: number, drawOrder: Array) { + this.frames[frameIndex] = time; + this.drawOrders[frameIndex] = drawOrder; + } + + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + firedEvents: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const drawOrder: Array = skeleton.drawOrder; + const slots: Array = skeleton.slots; + if (direction == MixDirection.mixOut) { + if (blend == MixBlend.setup) Utils.arrayCopy(skeleton.slots, 0, skeleton.drawOrder, 0, skeleton.slots.length); + return; + } + + const frames = this.frames; + if (time < frames[0]) { + if (blend == MixBlend.setup || blend == MixBlend.first) + Utils.arrayCopy(skeleton.slots, 0, skeleton.drawOrder, 0, skeleton.slots.length); + return; + } + + let frame = 0; + if (time >= frames[frames.length - 1]) + // Time is after last frame. + frame = frames.length - 1; + else frame = Animation.binarySearch(frames, time) - 1; + + const drawOrderToSetupIndex = this.drawOrders[frame]; + if (drawOrderToSetupIndex == null) Utils.arrayCopy(slots, 0, drawOrder, 0, slots.length); + else { + for (let i = 0, n = drawOrderToSetupIndex.length; i < n; i++) drawOrder[i] = slots[drawOrderToSetupIndex[i]]; + } + } +} + +/** Changes an IK constraint's {@link IkConstraint#mix}, {@link IkConstraint#softness}, + * {@link IkConstraint#bendDirection}, {@link IkConstraint#stretch}, and {@link IkConstraint#compress}. */ +export class IkConstraintTimeline extends CurveTimeline { + static ENTRIES = 6; + static PREV_TIME = -6; + static PREV_MIX = -5; + static PREV_SOFTNESS = -4; + static PREV_BEND_DIRECTION = -3; + static PREV_COMPRESS = -2; + static PREV_STRETCH = -1; + static MIX = 1; + static SOFTNESS = 2; + static BEND_DIRECTION = 3; + static COMPRESS = 4; + static STRETCH = 5; + + /** The index of the IK constraint slot in {@link Skeleton#ikConstraints} that will be changed. */ + ikConstraintIndex: number; + + /** The time in seconds, mix, softness, bend direction, compress, and stretch for each key frame. */ + frames: ArrayLike; // time, mix, softness, bendDirection, compress, stretch, ... + + constructor(frameCount: number) { + super(frameCount); + this.frames = Utils.newFloatArray(frameCount * IkConstraintTimeline.ENTRIES); + } + + getPropertyId() { + return (TimelineType.ikConstraint << 24) + this.ikConstraintIndex; + } + + /** Sets the time in seconds, mix, softness, bend direction, compress, and stretch for the specified key frame. */ + setFrame( + frameIndex: number, + time: number, + mix: number, + softness: number, + bendDirection: number, + compress: boolean, + stretch: boolean + ) { + frameIndex *= IkConstraintTimeline.ENTRIES; + this.frames[frameIndex] = time; + this.frames[frameIndex + IkConstraintTimeline.MIX] = mix; + this.frames[frameIndex + IkConstraintTimeline.SOFTNESS] = softness; + this.frames[frameIndex + IkConstraintTimeline.BEND_DIRECTION] = bendDirection; + this.frames[frameIndex + IkConstraintTimeline.COMPRESS] = compress ? 1 : 0; + this.frames[frameIndex + IkConstraintTimeline.STRETCH] = stretch ? 1 : 0; + } + + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + firedEvents: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const frames = this.frames; + const constraint: IkConstraint = skeleton.ikConstraints[this.ikConstraintIndex]; + if (!constraint.active) return; + if (time < frames[0]) { + switch (blend) { + case MixBlend.setup: + constraint.mix = constraint.data.mix; + constraint.softness = constraint.data.softness; + constraint.bendDirection = constraint.data.bendDirection; + constraint.compress = constraint.data.compress; + constraint.stretch = constraint.data.stretch; + return; + case MixBlend.first: + constraint.mix += (constraint.data.mix - constraint.mix) * alpha; + constraint.softness += (constraint.data.softness - constraint.softness) * alpha; + constraint.bendDirection = constraint.data.bendDirection; + constraint.compress = constraint.data.compress; + constraint.stretch = constraint.data.stretch; + } + return; + } + + if (time >= frames[frames.length - IkConstraintTimeline.ENTRIES]) { + // Time is after last frame. + if (blend == MixBlend.setup) { + constraint.mix = + constraint.data.mix + (frames[frames.length + IkConstraintTimeline.PREV_MIX] - constraint.data.mix) * alpha; + constraint.softness = + constraint.data.softness + + (frames[frames.length + IkConstraintTimeline.PREV_SOFTNESS] - constraint.data.softness) * alpha; + if (direction == MixDirection.mixOut) { + constraint.bendDirection = constraint.data.bendDirection; + constraint.compress = constraint.data.compress; + constraint.stretch = constraint.data.stretch; + } else { + constraint.bendDirection = frames[frames.length + IkConstraintTimeline.PREV_BEND_DIRECTION]; + constraint.compress = frames[frames.length + IkConstraintTimeline.PREV_COMPRESS] != 0; + constraint.stretch = frames[frames.length + IkConstraintTimeline.PREV_STRETCH] != 0; + } + } else { + constraint.mix += (frames[frames.length + IkConstraintTimeline.PREV_MIX] - constraint.mix) * alpha; + constraint.softness += + (frames[frames.length + IkConstraintTimeline.PREV_SOFTNESS] - constraint.softness) * alpha; + if (direction == MixDirection.mixIn) { + constraint.bendDirection = frames[frames.length + IkConstraintTimeline.PREV_BEND_DIRECTION]; + constraint.compress = frames[frames.length + IkConstraintTimeline.PREV_COMPRESS] != 0; + constraint.stretch = frames[frames.length + IkConstraintTimeline.PREV_STRETCH] != 0; + } + } + return; + } + + // Interpolate between the previous frame and the current frame. + const frame = Animation.binarySearch(frames, time, IkConstraintTimeline.ENTRIES); + const mix = frames[frame + IkConstraintTimeline.PREV_MIX]; + const softness = frames[frame + IkConstraintTimeline.PREV_SOFTNESS]; + const frameTime = frames[frame]; + const percent = this.getCurvePercent( + frame / IkConstraintTimeline.ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + IkConstraintTimeline.PREV_TIME] - frameTime) + ); + + if (blend == MixBlend.setup) { + constraint.mix = + constraint.data.mix + + (mix + (frames[frame + IkConstraintTimeline.MIX] - mix) * percent - constraint.data.mix) * alpha; + constraint.softness = + constraint.data.softness + + (softness + (frames[frame + IkConstraintTimeline.SOFTNESS] - softness) * percent - constraint.data.softness) * + alpha; + if (direction == MixDirection.mixOut) { + constraint.bendDirection = constraint.data.bendDirection; + constraint.compress = constraint.data.compress; + constraint.stretch = constraint.data.stretch; + } else { + constraint.bendDirection = frames[frame + IkConstraintTimeline.PREV_BEND_DIRECTION]; + constraint.compress = frames[frame + IkConstraintTimeline.PREV_COMPRESS] != 0; + constraint.stretch = frames[frame + IkConstraintTimeline.PREV_STRETCH] != 0; + } + } else { + constraint.mix += (mix + (frames[frame + IkConstraintTimeline.MIX] - mix) * percent - constraint.mix) * alpha; + constraint.softness += + (softness + (frames[frame + IkConstraintTimeline.SOFTNESS] - softness) * percent - constraint.softness) * alpha; + if (direction == MixDirection.mixIn) { + constraint.bendDirection = frames[frame + IkConstraintTimeline.PREV_BEND_DIRECTION]; + constraint.compress = frames[frame + IkConstraintTimeline.PREV_COMPRESS] != 0; + constraint.stretch = frames[frame + IkConstraintTimeline.PREV_STRETCH] != 0; + } + } + } +} + +/** Changes a transform constraint's {@link TransformConstraint#rotateMix}, {@link TransformConstraint#translateMix}, + * {@link TransformConstraint#scaleMix}, and {@link TransformConstraint#shearMix}. */ +export class TransformConstraintTimeline extends CurveTimeline { + static ENTRIES = 5; + static PREV_TIME = -5; + static PREV_ROTATE = -4; + static PREV_TRANSLATE = -3; + static PREV_SCALE = -2; + static PREV_SHEAR = -1; + static ROTATE = 1; + static TRANSLATE = 2; + static SCALE = 3; + static SHEAR = 4; + + /** The index of the transform constraint slot in {@link Skeleton#transformConstraints} that will be changed. */ + transformConstraintIndex: number; + + /** The time in seconds, rotate mix, translate mix, scale mix, and shear mix for each key frame. */ + frames: ArrayLike; // time, rotate mix, translate mix, scale mix, shear mix, ... + + constructor(frameCount: number) { + super(frameCount); + this.frames = Utils.newFloatArray(frameCount * TransformConstraintTimeline.ENTRIES); + } + + getPropertyId() { + return (TimelineType.transformConstraint << 24) + this.transformConstraintIndex; + } + + /** The time in seconds, rotate mix, translate mix, scale mix, and shear mix for the specified key frame. */ + setFrame( + frameIndex: number, + time: number, + rotateMix: number, + translateMix: number, + scaleMix: number, + shearMix: number + ) { + frameIndex *= TransformConstraintTimeline.ENTRIES; + this.frames[frameIndex] = time; + this.frames[frameIndex + TransformConstraintTimeline.ROTATE] = rotateMix; + this.frames[frameIndex + TransformConstraintTimeline.TRANSLATE] = translateMix; + this.frames[frameIndex + TransformConstraintTimeline.SCALE] = scaleMix; + this.frames[frameIndex + TransformConstraintTimeline.SHEAR] = shearMix; + } + + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + firedEvents: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const frames = this.frames; + + const constraint: TransformConstraint = skeleton.transformConstraints[this.transformConstraintIndex]; + if (!constraint.active) return; + if (time < frames[0]) { + const data = constraint.data; + switch (blend) { + case MixBlend.setup: + constraint.rotateMix = data.rotateMix; + constraint.translateMix = data.translateMix; + constraint.scaleMix = data.scaleMix; + constraint.shearMix = data.shearMix; + return; + case MixBlend.first: + constraint.rotateMix += (data.rotateMix - constraint.rotateMix) * alpha; + constraint.translateMix += (data.translateMix - constraint.translateMix) * alpha; + constraint.scaleMix += (data.scaleMix - constraint.scaleMix) * alpha; + constraint.shearMix += (data.shearMix - constraint.shearMix) * alpha; + } + return; + } + + let rotate = 0, + translate = 0, + scale = 0, + shear = 0; + if (time >= frames[frames.length - TransformConstraintTimeline.ENTRIES]) { + // Time is after last frame. + const i = frames.length; + rotate = frames[i + TransformConstraintTimeline.PREV_ROTATE]; + translate = frames[i + TransformConstraintTimeline.PREV_TRANSLATE]; + scale = frames[i + TransformConstraintTimeline.PREV_SCALE]; + shear = frames[i + TransformConstraintTimeline.PREV_SHEAR]; + } else { + // Interpolate between the previous frame and the current frame. + const frame = Animation.binarySearch(frames, time, TransformConstraintTimeline.ENTRIES); + rotate = frames[frame + TransformConstraintTimeline.PREV_ROTATE]; + translate = frames[frame + TransformConstraintTimeline.PREV_TRANSLATE]; + scale = frames[frame + TransformConstraintTimeline.PREV_SCALE]; + shear = frames[frame + TransformConstraintTimeline.PREV_SHEAR]; + const frameTime = frames[frame]; + const percent = this.getCurvePercent( + frame / TransformConstraintTimeline.ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + TransformConstraintTimeline.PREV_TIME] - frameTime) + ); + + rotate += (frames[frame + TransformConstraintTimeline.ROTATE] - rotate) * percent; + translate += (frames[frame + TransformConstraintTimeline.TRANSLATE] - translate) * percent; + scale += (frames[frame + TransformConstraintTimeline.SCALE] - scale) * percent; + shear += (frames[frame + TransformConstraintTimeline.SHEAR] - shear) * percent; + } + if (blend == MixBlend.setup) { + const data = constraint.data; + constraint.rotateMix = data.rotateMix + (rotate - data.rotateMix) * alpha; + constraint.translateMix = data.translateMix + (translate - data.translateMix) * alpha; + constraint.scaleMix = data.scaleMix + (scale - data.scaleMix) * alpha; + constraint.shearMix = data.shearMix + (shear - data.shearMix) * alpha; + } else { + constraint.rotateMix += (rotate - constraint.rotateMix) * alpha; + constraint.translateMix += (translate - constraint.translateMix) * alpha; + constraint.scaleMix += (scale - constraint.scaleMix) * alpha; + constraint.shearMix += (shear - constraint.shearMix) * alpha; + } + } +} + +/** Changes a path constraint's {@link PathConstraint#position}. */ +export class PathConstraintPositionTimeline extends CurveTimeline { + static ENTRIES = 2; + static PREV_TIME = -2; + static PREV_VALUE = -1; + static VALUE = 1; + + /** The index of the path constraint slot in {@link Skeleton#pathConstraints} that will be changed. */ + pathConstraintIndex: number; + + /** The time in seconds and path constraint position for each key frame. */ + frames: ArrayLike; // time, position, ... + + constructor(frameCount: number) { + super(frameCount); + this.frames = Utils.newFloatArray(frameCount * PathConstraintPositionTimeline.ENTRIES); + } + + getPropertyId() { + return (TimelineType.pathConstraintPosition << 24) + this.pathConstraintIndex; + } + + /** Sets the time in seconds and path constraint position for the specified key frame. */ + setFrame(frameIndex: number, time: number, value: number) { + frameIndex *= PathConstraintPositionTimeline.ENTRIES; + this.frames[frameIndex] = time; + this.frames[frameIndex + PathConstraintPositionTimeline.VALUE] = value; + } + + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + firedEvents: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const frames = this.frames; + const constraint: PathConstraint = skeleton.pathConstraints[this.pathConstraintIndex]; + if (!constraint.active) return; + if (time < frames[0]) { + switch (blend) { + case MixBlend.setup: + constraint.position = constraint.data.position; + return; + case MixBlend.first: + constraint.position += (constraint.data.position - constraint.position) * alpha; + } + return; + } + + let position = 0; + if (time >= frames[frames.length - PathConstraintPositionTimeline.ENTRIES]) + // Time is after last frame. + position = frames[frames.length + PathConstraintPositionTimeline.PREV_VALUE]; + else { + // Interpolate between the previous frame and the current frame. + const frame = Animation.binarySearch(frames, time, PathConstraintPositionTimeline.ENTRIES); + position = frames[frame + PathConstraintPositionTimeline.PREV_VALUE]; + const frameTime = frames[frame]; + const percent = this.getCurvePercent( + frame / PathConstraintPositionTimeline.ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + PathConstraintPositionTimeline.PREV_TIME] - frameTime) + ); + + position += (frames[frame + PathConstraintPositionTimeline.VALUE] - position) * percent; + } + if (blend == MixBlend.setup) + constraint.position = constraint.data.position + (position - constraint.data.position) * alpha; + else constraint.position += (position - constraint.position) * alpha; + } +} + +/** Changes a path constraint's {@link PathConstraint#spacing}. */ +export class PathConstraintSpacingTimeline extends PathConstraintPositionTimeline { + constructor(frameCount: number) { + super(frameCount); + } + + override getPropertyId() { + return (TimelineType.pathConstraintSpacing << 24) + this.pathConstraintIndex; + } + + override apply( + skeleton: Skeleton, + lastTime: number, + time: number, + firedEvents: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const frames = this.frames; + const constraint: PathConstraint = skeleton.pathConstraints[this.pathConstraintIndex]; + if (!constraint.active) return; + if (time < frames[0]) { + switch (blend) { + case MixBlend.setup: + constraint.spacing = constraint.data.spacing; + return; + case MixBlend.first: + constraint.spacing += (constraint.data.spacing - constraint.spacing) * alpha; + } + return; + } + + let spacing = 0; + if (time >= frames[frames.length - PathConstraintSpacingTimeline.ENTRIES]) + // Time is after last frame. + spacing = frames[frames.length + PathConstraintSpacingTimeline.PREV_VALUE]; + else { + // Interpolate between the previous frame and the current frame. + const frame = Animation.binarySearch(frames, time, PathConstraintSpacingTimeline.ENTRIES); + spacing = frames[frame + PathConstraintSpacingTimeline.PREV_VALUE]; + const frameTime = frames[frame]; + const percent = this.getCurvePercent( + frame / PathConstraintSpacingTimeline.ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + PathConstraintSpacingTimeline.PREV_TIME] - frameTime) + ); + + spacing += (frames[frame + PathConstraintSpacingTimeline.VALUE] - spacing) * percent; + } + + if (blend == MixBlend.setup) + constraint.spacing = constraint.data.spacing + (spacing - constraint.data.spacing) * alpha; + else constraint.spacing += (spacing - constraint.spacing) * alpha; + } +} + +/** Changes a transform constraint's {@link PathConstraint#rotateMix} and + * {@link TransformConstraint#translateMix}. */ +export class PathConstraintMixTimeline extends CurveTimeline { + static ENTRIES = 3; + static PREV_TIME = -3; + static PREV_ROTATE = -2; + static PREV_TRANSLATE = -1; + static ROTATE = 1; + static TRANSLATE = 2; + + /** The index of the path constraint slot in {@link Skeleton#getPathConstraints()} that will be changed. */ + pathConstraintIndex: number; + + /** The time in seconds, rotate mix, and translate mix for each key frame. */ + frames: ArrayLike; // time, rotate mix, translate mix, ... + + constructor(frameCount: number) { + super(frameCount); + this.frames = Utils.newFloatArray(frameCount * PathConstraintMixTimeline.ENTRIES); + } + + getPropertyId() { + return (TimelineType.pathConstraintMix << 24) + this.pathConstraintIndex; + } + + /** The time in seconds, rotate mix, and translate mix for the specified key frame. */ + setFrame(frameIndex: number, time: number, rotateMix: number, translateMix: number) { + frameIndex *= PathConstraintMixTimeline.ENTRIES; + this.frames[frameIndex] = time; + this.frames[frameIndex + PathConstraintMixTimeline.ROTATE] = rotateMix; + this.frames[frameIndex + PathConstraintMixTimeline.TRANSLATE] = translateMix; + } + + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + firedEvents: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const frames = this.frames; + const constraint: PathConstraint = skeleton.pathConstraints[this.pathConstraintIndex]; + if (!constraint.active) return; + if (time < frames[0]) { + switch (blend) { + case MixBlend.setup: + constraint.rotateMix = constraint.data.rotateMix; + constraint.translateMix = constraint.data.translateMix; + return; + case MixBlend.first: + constraint.rotateMix += (constraint.data.rotateMix - constraint.rotateMix) * alpha; + constraint.translateMix += (constraint.data.translateMix - constraint.translateMix) * alpha; + } + return; + } + + let rotate = 0, + translate = 0; + if (time >= frames[frames.length - PathConstraintMixTimeline.ENTRIES]) { + // Time is after last frame. + rotate = frames[frames.length + PathConstraintMixTimeline.PREV_ROTATE]; + translate = frames[frames.length + PathConstraintMixTimeline.PREV_TRANSLATE]; + } else { + // Interpolate between the previous frame and the current frame. + const frame = Animation.binarySearch(frames, time, PathConstraintMixTimeline.ENTRIES); + rotate = frames[frame + PathConstraintMixTimeline.PREV_ROTATE]; + translate = frames[frame + PathConstraintMixTimeline.PREV_TRANSLATE]; + const frameTime = frames[frame]; + const percent = this.getCurvePercent( + frame / PathConstraintMixTimeline.ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + PathConstraintMixTimeline.PREV_TIME] - frameTime) + ); + + rotate += (frames[frame + PathConstraintMixTimeline.ROTATE] - rotate) * percent; + translate += (frames[frame + PathConstraintMixTimeline.TRANSLATE] - translate) * percent; + } + + if (blend == MixBlend.setup) { + constraint.rotateMix = constraint.data.rotateMix + (rotate - constraint.data.rotateMix) * alpha; + constraint.translateMix = constraint.data.translateMix + (translate - constraint.data.translateMix) * alpha; + } else { + constraint.rotateMix += (rotate - constraint.rotateMix) * alpha; + constraint.translateMix += (translate - constraint.translateMix) * alpha; + } + } +} diff --git a/packages/spine-core-3.8/src/spine-core/AnimationState.ts b/packages/spine-core-3.8/src/spine-core/AnimationState.ts new file mode 100644 index 0000000000..7bb6b64177 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/AnimationState.ts @@ -0,0 +1,1193 @@ +import { AnimationStateData } from "./AnimationStateData"; +import { IntSet, Pool, Utils, MathUtils } from "./Utils"; +import { Skeleton } from "./Skeleton"; +import { + MixBlend, + AttachmentTimeline, + MixDirection, + RotateTimeline, + DrawOrderTimeline, + Timeline, + EventTimeline +} from "./Animation"; +import { Slot } from "./Slot"; +import { Animation } from "./Animation"; +import { Event } from "./Event"; + +/** Applies animations over time, queues animations for later playback, mixes (crossfading) between animations, and applies + * multiple animations on top of each other (layering). + * + * See [Applying Animations](http://esotericsoftware.com/spine-applying-animations/) in the Spine Runtimes Guide. */ +export class AnimationState { + static emptyAnimation = new Animation("", [], 0); + + /** 1. A previously applied timeline has set this property. + * + * Result: Mix from the current pose to the timeline pose. */ + static SUBSEQUENT = 0; + /** 1. This is the first timeline to set this property. + * 2. The next track entry applied after this one does not have a timeline to set this property. + * + * Result: Mix from the setup pose to the timeline pose. */ + static FIRST = 1; + /** 1) A previously applied timeline has set this property.
+ * 2) The next track entry to be applied does have a timeline to set this property.
+ * 3) The next track entry after that one does not have a timeline to set this property.
+ * Result: Mix from the current pose to the timeline pose, but do not mix out. This avoids "dipping" when crossfading + * animations that key the same property. A subsequent timeline will set this property using a mix. */ + static HOLD_SUBSEQUENT = 2; + /** 1) This is the first timeline to set this property.
+ * 2) The next track entry to be applied does have a timeline to set this property.
+ * 3) The next track entry after that one does not have a timeline to set this property.
+ * Result: Mix from the setup pose to the timeline pose, but do not mix out. This avoids "dipping" when crossfading animations + * that key the same property. A subsequent timeline will set this property using a mix. */ + static HOLD_FIRST = 3; + /** 1. This is the first timeline to set this property. + * 2. The next track entry to be applied does have a timeline to set this property. + * 3. The next track entry after that one does have a timeline to set this property. + * 4. timelineHoldMix stores the first subsequent track entry that does not have a timeline to set this property. + * + * Result: The same as HOLD except the mix percentage from the timelineHoldMix track entry is used. This handles when more than + * 2 track entries in a row have a timeline that sets the same property. + * + * Eg, A -> B -> C -> D where A, B, and C have a timeline setting same property, but D does not. When A is applied, to avoid + * "dipping" A is not mixed out, however D (the first entry that doesn't set the property) mixing in is used to mix out A + * (which affects B and C). Without using D to mix out, A would be applied fully until mixing completes, then snap into + * place. */ + static HOLD_MIX = 4; + + static SETUP = 1; + static CURRENT = 2; + + /** The AnimationStateData to look up mix durations. */ + data: AnimationStateData; + + /** The list of tracks that currently have animations, which may contain null entries. */ + tracks = new Array(); + + /** Multiplier for the delta time when the animation state is updated, causing time for all animations and mixes to play slower + * or faster. Defaults to 1. + * + * See TrackEntry {@link TrackEntry#timeScale} for affecting a single animation. */ + timeScale = 1; + unkeyedState = 0; + + events = new Array(); + listeners = new Array(); + queue = new EventQueue(this); + propertyIDs = new IntSet(); + animationsChanged = false; + + trackEntryPool = new Pool(() => new TrackEntry()); + + constructor(data: AnimationStateData) { + this.data = data; + } + + /** Increments each track entry {@link TrackEntry#trackTime()}, setting queued animations as current if needed. */ + update(delta: number) { + delta *= this.timeScale; + const tracks = this.tracks; + for (let i = 0, n = tracks.length; i < n; i++) { + const current = tracks[i]; + if (current == null) continue; + + current.animationLast = current.nextAnimationLast; + current.trackLast = current.nextTrackLast; + + let currentDelta = delta * current.timeScale; + + if (current.delay > 0) { + current.delay -= currentDelta; + if (current.delay > 0) continue; + currentDelta = -current.delay; + current.delay = 0; + } + + let next = current.next; + if (next != null) { + // When the next entry's delay is passed, change to the next entry, preserving leftover time. + const nextTime = current.trackLast - next.delay; + if (nextTime >= 0) { + next.delay = 0; + next.trackTime += current.timeScale == 0 ? 0 : (nextTime / current.timeScale + delta) * next.timeScale; + current.trackTime += currentDelta; + this.setCurrent(i, next, true); + while (next.mixingFrom != null) { + next.mixTime += delta; + next = next.mixingFrom; + } + continue; + } + } else if (current.trackLast >= current.trackEnd && current.mixingFrom == null) { + tracks[i] = null; + this.queue.end(current); + this.disposeNext(current); + continue; + } + if (current.mixingFrom != null && this.updateMixingFrom(current, delta)) { + // End mixing from entries once all have completed. + let from = current.mixingFrom; + current.mixingFrom = null; + if (from != null) from.mixingTo = null; + while (from != null) { + this.queue.end(from); + from = from.mixingFrom; + } + } + + current.trackTime += currentDelta; + } + + this.queue.drain(); + } + + /** Returns true when all mixing from entries are complete. */ + updateMixingFrom(to: TrackEntry, delta: number): boolean { + const from = to.mixingFrom; + if (from == null) return true; + + const finished = this.updateMixingFrom(from, delta); + + from.animationLast = from.nextAnimationLast; + from.trackLast = from.nextTrackLast; + + // Require mixTime > 0 to ensure the mixing from entry was applied at least once. + if (to.mixTime > 0 && to.mixTime >= to.mixDuration) { + // Require totalAlpha == 0 to ensure mixing is complete, unless mixDuration == 0 (the transition is a single frame). + if (from.totalAlpha == 0 || to.mixDuration == 0) { + to.mixingFrom = from.mixingFrom; + if (from.mixingFrom != null) from.mixingFrom.mixingTo = to; + to.interruptAlpha = from.interruptAlpha; + this.queue.end(from); + } + return finished; + } + + from.trackTime += delta * from.timeScale; + to.mixTime += delta; + return false; + } + + /** Poses the skeleton using the track entry animations. There are no side effects other than invoking listeners, so the + * animation state can be applied to multiple skeletons to pose them identically. + * @returns True if any animations were applied. */ + apply(skeleton: Skeleton): boolean { + if (skeleton == null) throw new Error("skeleton cannot be null."); + if (this.animationsChanged) this._animationsChanged(); + + const events = this.events; + const tracks = this.tracks; + let applied = false; + + for (let i = 0, n = tracks.length; i < n; i++) { + const current = tracks[i]; + if (current == null || current.delay > 0) continue; + applied = true; + const blend: MixBlend = i == 0 ? MixBlend.first : current.mixBlend; + + // Apply mixing from entries first. + let mix = current.alpha; + if (current.mixingFrom != null) mix *= this.applyMixingFrom(current, skeleton, blend); + else if (current.trackTime >= current.trackEnd && current.next == null) mix = 0; + + // Apply current entry. + const animationLast = current.animationLast, + animationTime = current.getAnimationTime(); + const timelineCount = current.animation.timelines.length; + const timelines = current.animation.timelines; + if ((i == 0 && mix == 1) || blend == MixBlend.add) { + for (let ii = 0; ii < timelineCount; ii++) { + // Fixes issue #302 on IOS9 where mix, blend sometimes became undefined and caused assets + // to sometimes stop rendering when using color correction, as their RGBA values become NaN. + // (https://github.com/pixijs/pixi-spine/issues/302) + Utils.webkit602BugfixHelper(mix, blend); + const timeline = timelines[ii]; + if (timeline instanceof AttachmentTimeline) + this.applyAttachmentTimeline(timeline, skeleton, animationTime, blend, true); + else timeline.apply(skeleton, animationLast, animationTime, events, mix, blend, MixDirection.mixIn); + } + } else { + const timelineMode = current.timelineMode; + + const firstFrame = current.timelinesRotation.length == 0; + if (firstFrame) Utils.setArraySize(current.timelinesRotation, timelineCount << 1, null); + const timelinesRotation = current.timelinesRotation; + + for (let ii = 0; ii < timelineCount; ii++) { + const timeline = timelines[ii]; + const timelineBlend = timelineMode[ii] == AnimationState.SUBSEQUENT ? blend : MixBlend.setup; + if (timeline instanceof RotateTimeline) { + this.applyRotateTimeline( + timeline, + skeleton, + animationTime, + mix, + timelineBlend, + timelinesRotation, + ii << 1, + firstFrame + ); + } else if (timeline instanceof AttachmentTimeline) { + this.applyAttachmentTimeline(timeline, skeleton, animationTime, blend, true); + } else { + // This fixes the WebKit 602 specific issue described at http://esotericsoftware.com/forum/iOS-10-disappearing-graphics-10109 + Utils.webkit602BugfixHelper(mix, blend); + timeline.apply(skeleton, animationLast, animationTime, events, mix, timelineBlend, MixDirection.mixIn); + } + } + } + this.queueEvents(current, animationTime); + events.length = 0; + current.nextAnimationLast = animationTime; + current.nextTrackLast = current.trackTime; + } + + // Set slots attachments to the setup pose, if needed. This occurs if an animation that is mixing out sets attachments so + // subsequent timelines see any deform, but the subsequent timelines don't set an attachment (eg they are also mixing out or + // the time is before the first key). + const setupState = this.unkeyedState + AnimationState.SETUP; + const slots = skeleton.slots; + for (let i = 0, n = skeleton.slots.length; i < n; i++) { + const slot = slots[i]; + if (slot.attachmentState == setupState) { + const attachmentName = slot.data.attachmentName; + slot.attachment = attachmentName == null ? null : skeleton.getAttachment(slot.data.index, attachmentName); + } + } + this.unkeyedState += 2; // Increasing after each use avoids the need to reset attachmentState for every slot. + + this.queue.drain(); + return applied; + } + + applyMixingFrom(to: TrackEntry, skeleton: Skeleton, blend: MixBlend) { + const from = to.mixingFrom; + if (from.mixingFrom != null) this.applyMixingFrom(from, skeleton, blend); + + let mix = 0; + if (to.mixDuration == 0) { + // Single frame mix to undo mixingFrom changes. + mix = 1; + if (blend == MixBlend.first) blend = MixBlend.setup; + } else { + mix = to.mixTime / to.mixDuration; + if (mix > 1) mix = 1; + if (blend != MixBlend.first) blend = from.mixBlend; + } + + const events = mix < from.eventThreshold ? this.events : null; + const attachments = mix < from.attachmentThreshold, + drawOrder = mix < from.drawOrderThreshold; + const animationLast = from.animationLast, + animationTime = from.getAnimationTime(); + const timelineCount = from.animation.timelines.length; + const timelines = from.animation.timelines; + const alphaHold = from.alpha * to.interruptAlpha, + alphaMix = alphaHold * (1 - mix); + if (blend == MixBlend.add) { + for (let i = 0; i < timelineCount; i++) + timelines[i].apply(skeleton, animationLast, animationTime, events, alphaMix, blend, MixDirection.mixOut); + } else { + const timelineMode = from.timelineMode; + const timelineHoldMix = from.timelineHoldMix; + + const firstFrame = from.timelinesRotation.length == 0; + if (firstFrame) Utils.setArraySize(from.timelinesRotation, timelineCount << 1, null); + const timelinesRotation = from.timelinesRotation; + + from.totalAlpha = 0; + for (let i = 0; i < timelineCount; i++) { + const timeline = timelines[i]; + let direction = MixDirection.mixOut; + let timelineBlend: MixBlend; + let alpha = 0; + switch (timelineMode[i]) { + case AnimationState.SUBSEQUENT: + if (!drawOrder && timeline instanceof DrawOrderTimeline) continue; + timelineBlend = blend; + alpha = alphaMix; + break; + case AnimationState.FIRST: + timelineBlend = MixBlend.setup; + alpha = alphaMix; + break; + case AnimationState.HOLD_SUBSEQUENT: + timelineBlend = blend; + alpha = alphaHold; + break; + case AnimationState.HOLD_FIRST: + timelineBlend = MixBlend.setup; + alpha = alphaHold; + break; + default: + timelineBlend = MixBlend.setup; + const holdMix = timelineHoldMix[i]; + alpha = alphaHold * Math.max(0, 1 - holdMix.mixTime / holdMix.mixDuration); + break; + } + from.totalAlpha += alpha; + + if (timeline instanceof RotateTimeline) + this.applyRotateTimeline( + timeline, + skeleton, + animationTime, + alpha, + timelineBlend, + timelinesRotation, + i << 1, + firstFrame + ); + else if (timeline instanceof AttachmentTimeline) + this.applyAttachmentTimeline(timeline, skeleton, animationTime, timelineBlend, attachments); + else { + // This fixes the WebKit 602 specific issue described at http://esotericsoftware.com/forum/iOS-10-disappearing-graphics-10109 + Utils.webkit602BugfixHelper(alpha, blend); + if (drawOrder && timeline instanceof DrawOrderTimeline && timelineBlend == MixBlend.setup) + direction = MixDirection.mixIn; + timeline.apply(skeleton, animationLast, animationTime, events, alpha, timelineBlend, direction); + } + } + } + + if (to.mixDuration > 0) this.queueEvents(from, animationTime); + this.events.length = 0; + from.nextAnimationLast = animationTime; + from.nextTrackLast = from.trackTime; + + return mix; + } + + applyAttachmentTimeline( + timeline: AttachmentTimeline, + skeleton: Skeleton, + time: number, + blend: MixBlend, + attachments: boolean + ) { + const slot = skeleton.slots[timeline.slotIndex]; + if (!slot.bone.active) return; + + const frames = timeline.frames; + if (time < frames[0]) { + // Time is before first frame. + if (blend == MixBlend.setup || blend == MixBlend.first) + this.setAttachment(skeleton, slot, slot.data.attachmentName, attachments); + } else { + let frameIndex; + if (time >= frames[frames.length - 1]) + // Time is after last frame. + frameIndex = frames.length - 1; + else frameIndex = Animation.binarySearch(frames, time) - 1; + this.setAttachment(skeleton, slot, timeline.attachmentNames[frameIndex], attachments); + } + + // If an attachment wasn't set (ie before the first frame or attachments is false), set the setup attachment later. + if (slot.attachmentState <= this.unkeyedState) slot.attachmentState = this.unkeyedState + AnimationState.SETUP; + } + + setAttachment(skeleton: Skeleton, slot: Slot, attachmentName: string, attachments: boolean) { + slot.attachment = attachmentName == null ? null : skeleton.getAttachment(slot.data.index, attachmentName); + if (attachments) slot.attachmentState = this.unkeyedState + AnimationState.CURRENT; + } + + applyRotateTimeline( + timeline: Timeline, + skeleton: Skeleton, + time: number, + alpha: number, + blend: MixBlend, + timelinesRotation: Array, + i: number, + firstFrame: boolean + ) { + if (firstFrame) timelinesRotation[i] = 0; + + if (alpha == 1) { + timeline.apply(skeleton, 0, time, null, 1, blend, MixDirection.mixIn); + return; + } + + const rotateTimeline = timeline as RotateTimeline; + const frames = rotateTimeline.frames; + const bone = skeleton.bones[rotateTimeline.boneIndex]; + if (!bone.active) return; + let r1 = 0, + r2 = 0; + if (time < frames[0]) { + switch (blend) { + case MixBlend.setup: + bone.rotation = bone.data.rotation; + default: + return; + case MixBlend.first: + r1 = bone.rotation; + r2 = bone.data.rotation; + } + } else { + r1 = blend == MixBlend.setup ? bone.data.rotation : bone.rotation; + if (time >= frames[frames.length - RotateTimeline.ENTRIES]) + // Time is after last frame. + r2 = bone.data.rotation + frames[frames.length + RotateTimeline.PREV_ROTATION]; + else { + // Interpolate between the previous frame and the current frame. + const frame = Animation.binarySearch(frames, time, RotateTimeline.ENTRIES); + const prevRotation = frames[frame + RotateTimeline.PREV_ROTATION]; + const frameTime = frames[frame]; + const percent = rotateTimeline.getCurvePercent( + (frame >> 1) - 1, + 1 - (time - frameTime) / (frames[frame + RotateTimeline.PREV_TIME] - frameTime) + ); + + r2 = frames[frame + RotateTimeline.ROTATION] - prevRotation; + r2 -= (16384 - ((16384.499999999996 - r2 / 360) | 0)) * 360; + r2 = prevRotation + r2 * percent + bone.data.rotation; + r2 -= (16384 - ((16384.499999999996 - r2 / 360) | 0)) * 360; + } + } + + // Mix between rotations using the direction of the shortest route on the first frame while detecting crosses. + let total = 0, + diff = r2 - r1; + diff -= (16384 - ((16384.499999999996 - diff / 360) | 0)) * 360; + if (diff == 0) { + total = timelinesRotation[i]; + } else { + let lastTotal = 0, + lastDiff = 0; + if (firstFrame) { + lastTotal = 0; + lastDiff = diff; + } else { + lastTotal = timelinesRotation[i]; // Angle and direction of mix, including loops. + lastDiff = timelinesRotation[i + 1]; // Difference between bones. + } + let current = diff > 0, + dir = lastTotal >= 0; + // Detect cross at 0 (not 180). + if (MathUtils.signum(lastDiff) != MathUtils.signum(diff) && Math.abs(lastDiff) <= 90) { + // A cross after a 360 rotation is a loop. + if (Math.abs(lastTotal) > 180) lastTotal += 360 * MathUtils.signum(lastTotal); + dir = current; + } + total = diff + lastTotal - (lastTotal % 360); // Store loops as part of lastTotal. + if (dir != current) total += 360 * MathUtils.signum(lastTotal); + timelinesRotation[i] = total; + } + timelinesRotation[i + 1] = diff; + r1 += total * alpha; + bone.rotation = r1 - (16384 - ((16384.499999999996 - r1 / 360) | 0)) * 360; + } + + queueEvents(entry: TrackEntry, animationTime: number) { + const animationStart = entry.animationStart, + animationEnd = entry.animationEnd; + const duration = animationEnd - animationStart; + const trackLastWrapped = entry.trackLast % duration; + + // Queue events before complete. + const events = this.events; + let i = 0, + n = events.length; + for (; i < n; i++) { + const event = events[i]; + if (event.time < trackLastWrapped) break; + if (event.time > animationEnd) continue; // Discard events outside animation start/end. + this.queue.event(entry, event); + } + + // Queue complete if completed a loop iteration or the animation. + let complete = false; + if (entry.loop) complete = duration == 0 || trackLastWrapped > entry.trackTime % duration; + else complete = animationTime >= animationEnd && entry.animationLast < animationEnd; + if (complete) this.queue.complete(entry); + + // Queue events after complete. + for (; i < n; i++) { + const event = events[i]; + if (event.time < animationStart) continue; // Discard events outside animation start/end. + this.queue.event(entry, events[i]); + } + } + + /** Removes all animations from all tracks, leaving skeletons in their current pose. + * + * It may be desired to use {@link AnimationState#setEmptyAnimation()} to mix the skeletons back to the setup pose, + * rather than leaving them in their current pose. */ + clearTracks() { + const oldDrainDisabled = this.queue.drainDisabled; + this.queue.drainDisabled = true; + for (let i = 0, n = this.tracks.length; i < n; i++) this.clearTrack(i); + this.tracks.length = 0; + this.queue.drainDisabled = oldDrainDisabled; + this.queue.drain(); + } + + /** Removes all animations from the track, leaving skeletons in their current pose. + * + * It may be desired to use {@link AnimationState#setEmptyAnimation()} to mix the skeletons back to the setup pose, + * rather than leaving them in their current pose. */ + clearTrack(trackIndex: number) { + if (trackIndex >= this.tracks.length) return; + const current = this.tracks[trackIndex]; + if (current == null) return; + + this.queue.end(current); + + this.disposeNext(current); + + let entry = current; + while (true) { + const from = entry.mixingFrom; + if (from == null) break; + this.queue.end(from); + entry.mixingFrom = null; + entry.mixingTo = null; + entry = from; + } + + this.tracks[current.trackIndex] = null; + + this.queue.drain(); + } + + setCurrent(index: number, current: TrackEntry, interrupt: boolean) { + const from = this.expandToIndex(index); + this.tracks[index] = current; + + if (from != null) { + if (interrupt) this.queue.interrupt(from); + current.mixingFrom = from; + from.mixingTo = current; + current.mixTime = 0; + + // Store the interrupted mix percentage. + if (from.mixingFrom != null && from.mixDuration > 0) + current.interruptAlpha *= Math.min(1, from.mixTime / from.mixDuration); + + from.timelinesRotation.length = 0; // Reset rotation for mixing out, in case entry was mixed in. + } + + this.queue.start(current); + } + + /** Sets an animation by name. + * + * {@link #setAnimationWith(}. */ + setAnimation(trackIndex: number, animationName: string, loop: boolean) { + const animation = this.data.skeletonData.findAnimation(animationName); + if (animation == null) throw new Error("Animation not found: " + animationName); + return this.setAnimationWith(trackIndex, animation, loop); + } + + /** Sets the current animation for a track, discarding any queued animations. If the formerly current track entry was never + * applied to a skeleton, it is replaced (not mixed from). + * @param loop If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its + * duration. In either case {@link TrackEntry#trackEnd} determines when the track is cleared. + * @returns A track entry to allow further customization of animation playback. References to the track entry must not be kept + * after the {@link AnimationStateListener#dispose()} event occurs. */ + setAnimationWith(trackIndex: number, animation: Animation, loop: boolean) { + if (animation == null) throw new Error("animation cannot be null."); + let interrupt = true; + let current = this.expandToIndex(trackIndex); + if (current != null) { + if (current.nextTrackLast == -1) { + // Don't mix from an entry that was never applied. + this.tracks[trackIndex] = current.mixingFrom; + this.queue.interrupt(current); + this.queue.end(current); + this.disposeNext(current); + current = current.mixingFrom; + interrupt = false; + } else this.disposeNext(current); + } + const entry = this.trackEntry(trackIndex, animation, loop, current); + this.setCurrent(trackIndex, entry, interrupt); + this.queue.drain(); + return entry; + } + + /** Queues an animation by name. + * + * See {@link #addAnimationWith()}. */ + addAnimation(trackIndex: number, animationName: string, loop: boolean, delay: number) { + const animation = this.data.skeletonData.findAnimation(animationName); + if (animation == null) throw new Error("Animation not found: " + animationName); + return this.addAnimationWith(trackIndex, animation, loop, delay); + } + + /** Adds an animation to be played after the current or last queued animation for a track. If the track is empty, it is + * equivalent to calling {@link #setAnimationWith()}. + * @param delay If > 0, sets {@link TrackEntry#delay}. If <= 0, the delay set is the duration of the previous track entry + * minus any mix duration (from the {@link AnimationStateData}) plus the specified `delay` (ie the mix + * ends at (`delay` = 0) or before (`delay` < 0) the previous track entry duration). If the + * previous entry is looping, its next loop completion is used instead of its duration. + * @returns A track entry to allow further customization of animation playback. References to the track entry must not be kept + * after the {@link AnimationStateListener#dispose()} event occurs. */ + addAnimationWith(trackIndex: number, animation: Animation, loop: boolean, delay: number) { + if (animation == null) throw new Error("animation cannot be null."); + + let last = this.expandToIndex(trackIndex); + if (last != null) { + while (last.next != null) last = last.next; + } + + const entry = this.trackEntry(trackIndex, animation, loop, last); + + if (last == null) { + this.setCurrent(trackIndex, entry, true); + this.queue.drain(); + } else { + last.next = entry; + if (delay <= 0) { + const duration = last.animationEnd - last.animationStart; + if (duration != 0) { + if (last.loop) delay += duration * (1 + ((last.trackTime / duration) | 0)); + else delay += Math.max(duration, last.trackTime); + delay -= this.data.getMix(last.animation, animation); + } else delay = last.trackTime; + } + } + + entry.delay = delay; + return entry; + } + + /** Sets an empty animation for a track, discarding any queued animations, and sets the track entry's + * {@link TrackEntry#mixduration}. An empty animation has no timelines and serves as a placeholder for mixing in or out. + * + * Mixing out is done by setting an empty animation with a mix duration using either {@link #setEmptyAnimation()}, + * {@link #setEmptyAnimations()}, or {@link #addEmptyAnimation()}. Mixing to an empty animation causes + * the previous animation to be applied less and less over the mix duration. Properties keyed in the previous animation + * transition to the value from lower tracks or to the setup pose value if no lower tracks key the property. A mix duration of + * 0 still mixes out over one frame. + * + * Mixing in is done by first setting an empty animation, then adding an animation using + * {@link #addAnimation()} and on the returned track entry, set the + * {@link TrackEntry#setMixDuration()}. Mixing from an empty animation causes the new animation to be applied more and + * more over the mix duration. Properties keyed in the new animation transition from the value from lower tracks or from the + * setup pose value if no lower tracks key the property to the value keyed in the new animation. */ + setEmptyAnimation(trackIndex: number, mixDuration: number) { + const entry = this.setAnimationWith(trackIndex, AnimationState.emptyAnimation, false); + entry.mixDuration = mixDuration; + entry.trackEnd = mixDuration; + return entry; + } + + /** Adds an empty animation to be played after the current or last queued animation for a track, and sets the track entry's + * {@link TrackEntry#mixDuration}. If the track is empty, it is equivalent to calling + * {@link #setEmptyAnimation()}. + * + * See {@link #setEmptyAnimation()}. + * @param delay If > 0, sets {@link TrackEntry#delay}. If <= 0, the delay set is the duration of the previous track entry + * minus any mix duration plus the specified `delay` (ie the mix ends at (`delay` = 0) or + * before (`delay` < 0) the previous track entry duration). If the previous entry is looping, its next + * loop completion is used instead of its duration. + * @return A track entry to allow further customization of animation playback. References to the track entry must not be kept + * after the {@link AnimationStateListener#dispose()} event occurs. */ + addEmptyAnimation(trackIndex: number, mixDuration: number, delay: number) { + if (delay <= 0) delay -= mixDuration; + const entry = this.addAnimationWith(trackIndex, AnimationState.emptyAnimation, false, delay); + entry.mixDuration = mixDuration; + entry.trackEnd = mixDuration; + return entry; + } + + /** Sets an empty animation for every track, discarding any queued animations, and mixes to it over the specified mix + * duration. */ + setEmptyAnimations(mixDuration: number) { + const oldDrainDisabled = this.queue.drainDisabled; + this.queue.drainDisabled = true; + for (let i = 0, n = this.tracks.length; i < n; i++) { + const current = this.tracks[i]; + if (current != null) this.setEmptyAnimation(current.trackIndex, mixDuration); + } + this.queue.drainDisabled = oldDrainDisabled; + this.queue.drain(); + } + + expandToIndex(index: number) { + if (index < this.tracks.length) return this.tracks[index]; + Utils.ensureArrayCapacity(this.tracks, index + 1, null); + this.tracks.length = index + 1; + return null; + } + + /** @param last May be null. */ + trackEntry(trackIndex: number, animation: Animation, loop: boolean, last: TrackEntry) { + const entry = this.trackEntryPool.obtain(); + entry.trackIndex = trackIndex; + entry.animation = animation; + entry.loop = loop; + entry.holdPrevious = false; + + entry.eventThreshold = 0; + entry.attachmentThreshold = 0; + entry.drawOrderThreshold = 0; + + entry.animationStart = 0; + entry.animationEnd = animation.duration; + entry.animationLast = -1; + entry.nextAnimationLast = -1; + + entry.delay = 0; + entry.trackTime = 0; + entry.trackLast = -1; + entry.nextTrackLast = -1; + entry.trackEnd = Number.MAX_VALUE; + entry.timeScale = 1; + + entry.alpha = 1; + entry.interruptAlpha = 1; + entry.mixTime = 0; + entry.mixDuration = last == null ? 0 : this.data.getMix(last.animation, animation); + entry.mixBlend = MixBlend.replace; + return entry; + } + + disposeNext(entry: TrackEntry) { + let next = entry.next; + while (next != null) { + this.queue.dispose(next); + next = next.next; + } + entry.next = null; + } + + _animationsChanged() { + this.animationsChanged = false; + + this.propertyIDs.clear(); + + for (let i = 0, n = this.tracks.length; i < n; i++) { + let entry = this.tracks[i]; + if (entry == null) continue; + while (entry.mixingFrom != null) entry = entry.mixingFrom; + + do { + if (entry.mixingFrom == null || entry.mixBlend != MixBlend.add) this.computeHold(entry); + entry = entry.mixingTo; + } while (entry != null); + } + } + + computeHold(entry: TrackEntry) { + const to = entry.mixingTo; + const timelines = entry.animation.timelines; + const timelinesCount = entry.animation.timelines.length; + const timelineMode = Utils.setArraySize(entry.timelineMode, timelinesCount); + entry.timelineHoldMix.length = 0; + const timelineDipMix = Utils.setArraySize(entry.timelineHoldMix, timelinesCount); + const propertyIDs = this.propertyIDs; + + if (to != null && to.holdPrevious) { + for (let i = 0; i < timelinesCount; i++) { + timelineMode[i] = propertyIDs.add(timelines[i].getPropertyId()) + ? AnimationState.HOLD_FIRST + : AnimationState.HOLD_SUBSEQUENT; + } + return; + } + + outer: for (let i = 0; i < timelinesCount; i++) { + const timeline = timelines[i]; + const id = timeline.getPropertyId(); + if (!propertyIDs.add(id)) timelineMode[i] = AnimationState.SUBSEQUENT; + else if ( + to == null || + timeline instanceof AttachmentTimeline || + timeline instanceof DrawOrderTimeline || + timeline instanceof EventTimeline || + !to.animation.hasTimeline(id) + ) { + timelineMode[i] = AnimationState.FIRST; + } else { + for (let next = to.mixingTo; next != null; next = next.mixingTo) { + if (next.animation.hasTimeline(id)) continue; + if (entry.mixDuration > 0) { + timelineMode[i] = AnimationState.HOLD_MIX; + timelineDipMix[i] = next; + continue outer; + } + break; + } + timelineMode[i] = AnimationState.HOLD_FIRST; + } + } + } + + /** Returns the track entry for the animation currently playing on the track, or null if no animation is currently playing. */ + getCurrent(trackIndex: number) { + if (trackIndex >= this.tracks.length) return null; + return this.tracks[trackIndex]; + } + + /** Adds a listener to receive events for all track entries. */ + addListener(listener: AnimationStateListener) { + if (listener == null) throw new Error("listener cannot be null."); + this.listeners.push(listener); + } + + /** Removes the listener added with {@link #addListener()}. */ + removeListener(listener: AnimationStateListener) { + const index = this.listeners.indexOf(listener); + if (index >= 0) this.listeners.splice(index, 1); + } + + /** Removes all listeners added with {@link #addListener()}. */ + clearListeners() { + this.listeners.length = 0; + } + + /** Discards all listener notifications that have not yet been delivered. This can be useful to call from an + * {@link AnimationStateListener} when it is known that further notifications that may have been already queued for delivery + * are not wanted because new animations are being set. */ + clearListenerNotifications() { + this.queue.clear(); + } +} + +/** Stores settings and other state for the playback of an animation on an {@link AnimationState} track. + * + * References to a track entry must not be kept after the {@link AnimationStateListener#dispose()} event occurs. */ +export class TrackEntry { + /** The animation to apply for this track entry. */ + animation: Animation; + + /** The animation queued to start after this animation, or null. `next` makes up a linked list. */ + next: TrackEntry; + + /** The track entry for the previous animation when mixing from the previous animation to this animation, or null if no + * mixing is currently occuring. When mixing from multiple animations, `mixingFrom` makes up a linked list. */ + mixingFrom: TrackEntry; + + /** The track entry for the next animation when mixing from this animation to the next animation, or null if no mixing is + * currently occuring. When mixing to multiple animations, `mixingTo` makes up a linked list. */ + mixingTo: TrackEntry; + + /** The listener for events generated by this track entry, or null. + * + * A track entry returned from {@link AnimationState#setAnimation()} is already the current animation + * for the track, so the track entry listener {@link AnimationStateListener#start()} will not be called. */ + listener: AnimationStateListener; + + /** The index of the track where this track entry is either current or queued. + * + * See {@link AnimationState#getCurrent()}. */ + trackIndex: number; + + /** If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its + * duration. */ + loop: boolean; + + /** If true, when mixing from the previous animation to this animation, the previous animation is applied as normal instead + * of being mixed out. + * + * When mixing between animations that key the same property, if a lower track also keys that property then the value will + * briefly dip toward the lower track value during the mix. This happens because the first animation mixes from 100% to 0% + * while the second animation mixes from 0% to 100%. Setting `holdPrevious` to true applies the first animation + * at 100% during the mix so the lower track value is overwritten. Such dipping does not occur on the lowest track which + * keys the property, only when a higher track also keys the property. + * + * Snapping will occur if `holdPrevious` is true and this animation does not key all the same properties as the + * previous animation. */ + holdPrevious: boolean; + + /** When the mix percentage ({@link #mixTime} / {@link #mixDuration}) is less than the + * `eventThreshold`, event timelines are applied while this animation is being mixed out. Defaults to 0, so event + * timelines are not applied while this animation is being mixed out. */ + eventThreshold: number; + + /** When the mix percentage ({@link #mixtime} / {@link #mixDuration}) is less than the + * `attachmentThreshold`, attachment timelines are applied while this animation is being mixed out. Defaults to + * 0, so attachment timelines are not applied while this animation is being mixed out. */ + attachmentThreshold: number; + + /** When the mix percentage ({@link #mixTime} / {@link #mixDuration}) is less than the + * `drawOrderThreshold`, draw order timelines are applied while this animation is being mixed out. Defaults to 0, + * so draw order timelines are not applied while this animation is being mixed out. */ + drawOrderThreshold: number; + + /** Seconds when this animation starts, both initially and after looping. Defaults to 0. + * + * When changing the `animationStart` time, it often makes sense to set {@link #animationLast} to the same + * value to prevent timeline keys before the start time from triggering. */ + animationStart: number; + + /** Seconds for the last frame of this animation. Non-looping animations won't play past this time. Looping animations will + * loop back to {@link #animationStart} at this time. Defaults to the animation {@link Animation#duration}. */ + animationEnd: number; + + /** The time in seconds this animation was last applied. Some timelines use this for one-time triggers. Eg, when this + * animation is applied, event timelines will fire all events between the `animationLast` time (exclusive) and + * `animationTime` (inclusive). Defaults to -1 to ensure triggers on frame 0 happen the first time this animation + * is applied. */ + animationLast: number; + + nextAnimationLast: number; + + /** Seconds to postpone playing the animation. When this track entry is the current track entry, `delay` + * postpones incrementing the {@link #trackTime}. When this track entry is queued, `delay` is the time from + * the start of the previous animation to when this track entry will become the current track entry (ie when the previous + * track entry {@link TrackEntry#trackTime} >= this track entry's `delay`). + * + * {@link #timeScale} affects the delay. */ + delay: number; + + /** Current time in seconds this track entry has been the current track entry. The track time determines + * {@link #animationTime}. The track time can be set to start the animation at a time other than 0, without affecting + * looping. */ + trackTime: number; + + trackLast: number; + nextTrackLast: number; + + /** The track time in seconds when this animation will be removed from the track. Defaults to the highest possible float + * value, meaning the animation will be applied until a new animation is set or the track is cleared. If the track end time + * is reached, no other animations are queued for playback, and mixing from any previous animations is complete, then the + * properties keyed by the animation are set to the setup pose and the track is cleared. + * + * It may be desired to use {@link AnimationState#addEmptyAnimation()} rather than have the animation + * abruptly cease being applied. */ + trackEnd: number; + + /** Multiplier for the delta time when this track entry is updated, causing time for this animation to pass slower or + * faster. Defaults to 1. + * + * {@link #mixTime} is not affected by track entry time scale, so {@link #mixDuration} may need to be adjusted to + * match the animation speed. + * + * When using {@link AnimationState#addAnimation()} with a `delay` <= 0, note the + * {@link #delay} is set using the mix duration from the {@link AnimationStateData}, assuming time scale to be 1. If + * the time scale is not 1, the delay may need to be adjusted. + * + * See AnimationState {@link AnimationState#timeScale} for affecting all animations. */ + timeScale: number; + + /** Values < 1 mix this animation with the skeleton's current pose (usually the pose resulting from lower tracks). Defaults + * to 1, which overwrites the skeleton's current pose with this animation. + * + * Typically track 0 is used to completely pose the skeleton, then alpha is used on higher tracks. It doesn't make sense to + * use alpha on track 0 if the skeleton pose is from the last frame render. */ + alpha: number; + + /** Seconds from 0 to the {@link #getMixDuration()} when mixing from the previous animation to this animation. May be + * slightly more than `mixDuration` when the mix is complete. */ + mixTime: number; + + /** Seconds for mixing from the previous animation to this animation. Defaults to the value provided by AnimationStateData + * {@link AnimationStateData#getMix()} based on the animation before this animation (if any). + * + * A mix duration of 0 still mixes out over one frame to provide the track entry being mixed out a chance to revert the + * properties it was animating. + * + * The `mixDuration` can be set manually rather than use the value from + * {@link AnimationStateData#getMix()}. In that case, the `mixDuration` can be set for a new + * track entry only before {@link AnimationState#update(float)} is first called. + * + * When using {@link AnimationState#addAnimation()} with a `delay` <= 0, note the + * {@link #delay} is set using the mix duration from the {@link AnimationStateData}, not a mix duration set + * afterward. */ + mixDuration: number; + interruptAlpha: number; + totalAlpha: number; + + /** Controls how properties keyed in the animation are mixed with lower tracks. Defaults to {@link MixBlend#replace}, which + * replaces the values from the lower tracks with the animation values. {@link MixBlend#add} adds the animation values to + * the values from the lower tracks. + * + * The `mixBlend` can be set for a new track entry only before {@link AnimationState#apply()} is first + * called. */ + mixBlend = MixBlend.replace; + timelineMode = new Array(); + timelineHoldMix = new Array(); + timelinesRotation = new Array(); + + reset() { + this.next = null; + this.mixingFrom = null; + this.mixingTo = null; + this.animation = null; + this.listener = null; + this.timelineMode.length = 0; + this.timelineHoldMix.length = 0; + this.timelinesRotation.length = 0; + } + + /** Uses {@link #trackTime} to compute the `animationTime`, which is between {@link #animationStart} + * and {@link #animationEnd}. When the `trackTime` is 0, the `animationTime` is equal to the + * `animationStart` time. */ + getAnimationTime() { + if (this.loop) { + const duration = this.animationEnd - this.animationStart; + if (duration == 0) return this.animationStart; + return (this.trackTime % duration) + this.animationStart; + } + return Math.min(this.trackTime + this.animationStart, this.animationEnd); + } + + setAnimationLast(animationLast: number) { + this.animationLast = animationLast; + this.nextAnimationLast = animationLast; + } + + /** Returns true if at least one loop has been completed. + * + * See {@link AnimationStateListener#complete()}. */ + isComplete() { + return this.trackTime >= this.animationEnd - this.animationStart; + } + + /** Resets the rotation directions for mixing this entry's rotate timelines. This can be useful to avoid bones rotating the + * long way around when using {@link #alpha} and starting animations on other tracks. + * + * Mixing with {@link MixBlend#replace} involves finding a rotation between two others, which has two possible solutions: + * the short way or the long way around. The two rotations likely change over time, so which direction is the short or long + * way also changes. If the short way was always chosen, bones would flip to the other side when that direction became the + * long way. TrackEntry chooses the short way the first time it is applied and remembers that direction. */ + resetRotationDirections() { + this.timelinesRotation.length = 0; + } +} + +export class EventQueue { + objects: Array = []; + drainDisabled = false; + animState: AnimationState; + + constructor(animState: AnimationState) { + this.animState = animState; + } + + start(entry: TrackEntry) { + this.objects.push(EventType.start); + this.objects.push(entry); + this.animState.animationsChanged = true; + } + + interrupt(entry: TrackEntry) { + this.objects.push(EventType.interrupt); + this.objects.push(entry); + } + + end(entry: TrackEntry) { + this.objects.push(EventType.end); + this.objects.push(entry); + this.animState.animationsChanged = true; + } + + dispose(entry: TrackEntry) { + this.objects.push(EventType.dispose); + this.objects.push(entry); + } + + complete(entry: TrackEntry) { + this.objects.push(EventType.complete); + this.objects.push(entry); + } + + event(entry: TrackEntry, event: Event) { + this.objects.push(EventType.event); + this.objects.push(entry); + this.objects.push(event); + } + + drain() { + if (this.drainDisabled) return; + this.drainDisabled = true; + + const objects = this.objects; + const listeners = this.animState.listeners; + + for (let i = 0; i < objects.length; i += 2) { + const type = objects[i] as EventType; + const entry = objects[i + 1] as TrackEntry; + switch (type) { + case EventType.start: + if (entry.listener != null && entry.listener.start) entry.listener.start(entry); + for (let ii = 0; ii < listeners.length; ii++) if (listeners[ii].start) listeners[ii].start(entry); + break; + case EventType.interrupt: + if (entry.listener != null && entry.listener.interrupt) entry.listener.interrupt(entry); + for (let ii = 0; ii < listeners.length; ii++) if (listeners[ii].interrupt) listeners[ii].interrupt(entry); + break; + case EventType.end: + if (entry.listener != null && entry.listener.end) entry.listener.end(entry); + for (let ii = 0; ii < listeners.length; ii++) if (listeners[ii].end) listeners[ii].end(entry); + // Fall through. + case EventType.dispose: + if (entry.listener != null && entry.listener.dispose) entry.listener.dispose(entry); + for (let ii = 0; ii < listeners.length; ii++) if (listeners[ii].dispose) listeners[ii].dispose(entry); + this.animState.trackEntryPool.free(entry); + break; + case EventType.complete: + if (entry.listener != null && entry.listener.complete) entry.listener.complete(entry); + for (let ii = 0; ii < listeners.length; ii++) if (listeners[ii].complete) listeners[ii].complete(entry); + break; + case EventType.event: + const event = objects[i++ + 2] as Event; + if (entry.listener != null && entry.listener.event) entry.listener.event(entry, event); + for (let ii = 0; ii < listeners.length; ii++) if (listeners[ii].event) listeners[ii].event(entry, event); + break; + } + } + this.clear(); + + this.drainDisabled = false; + } + + clear() { + this.objects.length = 0; + } +} + +export enum EventType { + start, + interrupt, + end, + dispose, + complete, + event +} + +/** The interface to implement for receiving TrackEntry events. It is always safe to call AnimationState methods when receiving + * events. + * + * See TrackEntry {@link TrackEntry#listener} and AnimationState + * {@link AnimationState#addListener()}. */ +export interface AnimationStateListener { + /** Invoked when this entry has been set as the current entry. */ + start(entry: TrackEntry): void; + + /** Invoked when another entry has replaced this entry as the current entry. This entry may continue being applied for + * mixing. */ + interrupt(entry: TrackEntry): void; + + /** Invoked when this entry is no longer the current entry and will never be applied again. */ + end(entry: TrackEntry): void; + + /** Invoked when this entry will be disposed. This may occur without the entry ever being set as the current entry. + * References to the entry should not be kept after dispose is called, as it may be destroyed or reused. */ + dispose(entry: TrackEntry): void; + + /** Invoked every time this entry's animation completes a loop. */ + complete(entry: TrackEntry): void; + + /** Invoked when this entry's animation triggers an event. */ + event(entry: TrackEntry, event: Event): void; +} + +export abstract class AnimationStateAdapter implements AnimationStateListener { + start(entry: TrackEntry) {} + + interrupt(entry: TrackEntry) {} + + end(entry: TrackEntry) {} + + dispose(entry: TrackEntry) {} + + complete(entry: TrackEntry) {} + + event(entry: TrackEntry, event: Event) {} +} diff --git a/packages/spine-core-3.8/src/spine-core/AnimationStateData.ts b/packages/spine-core-3.8/src/spine-core/AnimationStateData.ts new file mode 100644 index 0000000000..009fcfad95 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/AnimationStateData.ts @@ -0,0 +1,48 @@ +import { SkeletonData } from "./SkeletonData"; +import { Animation } from "./Animation"; +import { Map } from "./Utils"; + +/** Stores mix (crossfade) durations to be applied when {@link AnimationState} animations are changed. */ +export class AnimationStateData { + /** The SkeletonData to look up animations when they are specified by name. */ + skeletonData: SkeletonData; + + animationToMixTime: Map = {}; + + /** The mix duration to use when no mix duration has been defined between two animations. */ + defaultMix = 0; + + constructor(skeletonData: SkeletonData) { + if (skeletonData == null) throw new Error("skeletonData cannot be null."); + this.skeletonData = skeletonData; + } + + /** Sets a mix duration by animation name. + * + * See {@link #setMixWith()}. */ + setMix(fromName: string, toName: string, duration: number) { + const from = this.skeletonData.findAnimation(fromName); + if (from == null) throw new Error("Animation not found: " + fromName); + const to = this.skeletonData.findAnimation(toName); + if (to == null) throw new Error("Animation not found: " + toName); + this.setMixWith(from, to, duration); + } + + /** Sets the mix duration when changing from the specified animation to the other. + * + * See {@link TrackEntry#mixDuration}. */ + setMixWith(from: Animation, to: Animation, duration: number) { + if (from == null) throw new Error("from cannot be null."); + if (to == null) throw new Error("to cannot be null."); + const key = from.name + "." + to.name; + this.animationToMixTime[key] = duration; + } + + /** Returns the mix duration to use when changing from the specified animation to the other, or the {@link #defaultMix} if + * no mix duration has been set. */ + getMix(from: Animation, to: Animation) { + const key = from.name + "." + to.name; + const value = this.animationToMixTime[key]; + return value === undefined ? this.defaultMix : value; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/AtlasAttachmentLoader.ts b/packages/spine-core-3.8/src/spine-core/AtlasAttachmentLoader.ts new file mode 100644 index 0000000000..eca31cbf2e --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/AtlasAttachmentLoader.ts @@ -0,0 +1,55 @@ +import { AttachmentLoader } from "./attachments/AttachmentLoader"; +import { TextureAtlas } from "./TextureAtlas"; +import { Skin } from "./Skin"; +import { RegionAttachment } from "./attachments/RegionAttachment"; +import { MeshAttachment } from "./attachments/MeshAttachment"; +import { BoundingBoxAttachment } from "./attachments/BoundingBoxAttachment"; +import { PathAttachment } from "./attachments/PathAttachment"; +import { PointAttachment } from "./attachments/PointAttachment"; +import { ClippingAttachment } from "./attachments/ClippingAttachment"; + +/** An {@link AttachmentLoader} that configures attachments using texture regions from an {@link TextureAtlas}. + * + * See [Loading skeleton data](http://esotericsoftware.com/spine-loading-skeleton-data#JSON-and-binary-data) in the + * Spine Runtimes Guide. */ +export class AtlasAttachmentLoader implements AttachmentLoader { + atlas: TextureAtlas; + + constructor(atlas: TextureAtlas) { + this.atlas = atlas; + } + + newRegionAttachment(skin: Skin, name: string, path: string): RegionAttachment { + const region = this.atlas.findRegion(path); + if (region == null) throw new Error("Region not found in atlas: " + path + " (region attachment: " + name + ")"); + region.renderObject = region; + const attachment = new RegionAttachment(name); + attachment.setRegion(region); + return attachment; + } + + newMeshAttachment(skin: Skin, name: string, path: string): MeshAttachment { + const region = this.atlas.findRegion(path); + if (region == null) throw new Error("Region not found in atlas: " + path + " (mesh attachment: " + name + ")"); + region.renderObject = region; + const attachment = new MeshAttachment(name); + attachment.region = region; + return attachment; + } + + newBoundingBoxAttachment(skin: Skin, name: string): BoundingBoxAttachment { + return new BoundingBoxAttachment(name); + } + + newPathAttachment(skin: Skin, name: string): PathAttachment { + return new PathAttachment(name); + } + + newPointAttachment(skin: Skin, name: string): PointAttachment { + return new PointAttachment(name); + } + + newClippingAttachment(skin: Skin, name: string): ClippingAttachment { + return new ClippingAttachment(name); + } +} diff --git a/packages/spine-core-3.8/src/spine-core/BlendMode.ts b/packages/spine-core-3.8/src/spine-core/BlendMode.ts new file mode 100644 index 0000000000..2f004954b4 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/BlendMode.ts @@ -0,0 +1,7 @@ +/** Determines how images are blended with existing pixels when drawn. */ +export enum BlendMode { + Normal, + Additive, + Multiply, + Screen +} diff --git a/packages/spine-core-3.8/src/spine-core/Bone.ts b/packages/spine-core-3.8/src/spine-core/Bone.ts new file mode 100644 index 0000000000..1d4c9b7ee3 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/Bone.ts @@ -0,0 +1,391 @@ +import { Updatable } from "./Updatable"; +import { BoneData, TransformMode } from "./BoneData"; +import { Skeleton } from "./Skeleton"; +import { MathUtils, Vector2 } from "./Utils"; + +/** Stores a bone's current pose. + * + * A bone has a local transform which is used to compute its world transform. A bone also has an applied transform, which is a + * local transform that can be applied to compute the world transform. The local transform and applied transform may differ if a + * constraint or application code modifies the world transform after it was computed from the local transform. */ +export class Bone implements Updatable { + /** The bone's setup pose data. */ + data: BoneData; + + /** The skeleton this bone belongs to. */ + skeleton: Skeleton; + + /** The parent bone, or null if this is the root bone. */ + parent: Bone; + + /** The immediate children of this bone. */ + children = new Array(); + + /** The local x translation. */ + x = 0; + + /** The local y translation. */ + y = 0; + + /** The local rotation in degrees, counter clockwise. */ + rotation = 0; + + /** The local scaleX. */ + scaleX = 0; + + /** The local scaleY. */ + scaleY = 0; + + /** The local shearX. */ + shearX = 0; + + /** The local shearY. */ + shearY = 0; + + /** The applied local x translation. */ + ax = 0; + + /** The applied local y translation. */ + ay = 0; + + /** The applied local rotation in degrees, counter clockwise. */ + arotation = 0; + + /** The applied local scaleX. */ + ascaleX = 0; + + /** The applied local scaleY. */ + ascaleY = 0; + + /** The applied local shearX. */ + ashearX = 0; + + /** The applied local shearY. */ + ashearY = 0; + + /** If true, the applied transform matches the world transform. If false, the world transform has been modified since it was + * computed and {@link #updateAppliedTransform()} must be called before accessing the applied transform. */ + appliedValid = false; + + /** Part of the world transform matrix for the X axis. If changed, {@link #appliedValid} should be set to false. */ + a = 0; + + /** Part of the world transform matrix for the Y axis. If changed, {@link #appliedValid} should be set to false. */ + b = 0; + + /** Part of the world transform matrix for the X axis. If changed, {@link #appliedValid} should be set to false. */ + c = 0; + + /** Part of the world transform matrix for the Y axis. If changed, {@link #appliedValid} should be set to false. */ + d = 0; + + /** The world X position. If changed, {@link #appliedValid} should be set to false. */ + worldY = 0; + + /** The world Y position. If changed, {@link #appliedValid} should be set to false. */ + worldX = 0; + + sorted = false; + active = false; + + /** @param parent May be null. */ + constructor(data: BoneData, skeleton: Skeleton, parent: Bone) { + if (data == null) throw new Error("data cannot be null."); + if (skeleton == null) throw new Error("skeleton cannot be null."); + this.data = data; + this.skeleton = skeleton; + this.parent = parent; + this.setToSetupPose(); + } + + /** Returns false when the bone has not been computed because {@link BoneData#skinRequired} is true and the + * {@link Skeleton#skin active skin} does not {@link Skin#bones contain} this bone. */ + isActive() { + return this.active; + } + + /** Same as {@link #updateWorldTransform()}. This method exists for Bone to implement {@link Updatable}. */ + update() { + this.updateWorldTransformWith(this.x, this.y, this.rotation, this.scaleX, this.scaleY, this.shearX, this.shearY); + } + + /** Computes the world transform using the parent bone and this bone's local transform. + * + * See {@link #updateWorldTransformWith()}. */ + updateWorldTransform() { + this.updateWorldTransformWith(this.x, this.y, this.rotation, this.scaleX, this.scaleY, this.shearX, this.shearY); + } + + /** Computes the world transform using the parent bone and the specified local transform. Child bones are not updated. + * + * See [World transforms](http://esotericsoftware.com/spine-runtime-skeletons#World-transforms) in the Spine + * Runtimes Guide. */ + updateWorldTransformWith( + x: number, + y: number, + rotation: number, + scaleX: number, + scaleY: number, + shearX: number, + shearY: number + ) { + this.ax = x; + this.ay = y; + this.arotation = rotation; + this.ascaleX = scaleX; + this.ascaleY = scaleY; + this.ashearX = shearX; + this.ashearY = shearY; + this.appliedValid = true; + + const parent = this.parent; + if (parent == null) { + // Root bone. + const skeleton = this.skeleton; + const rotationY = rotation + 90 + shearY; + const sx = skeleton.scaleX; + const sy = skeleton.scaleY; + this.a = MathUtils.cosDeg(rotation + shearX) * scaleX * sx; + this.b = MathUtils.cosDeg(rotationY) * scaleY * sx; + this.c = MathUtils.sinDeg(rotation + shearX) * scaleX * sy; + this.d = MathUtils.sinDeg(rotationY) * scaleY * sy; + this.worldX = x * sx + skeleton.x; + this.worldY = y * sy + skeleton.y; + return; + } + + let pa = parent.a, + pb = parent.b, + pc = parent.c, + pd = parent.d; + this.worldX = pa * x + pb * y + parent.worldX; + this.worldY = pc * x + pd * y + parent.worldY; + + switch (this.data.transformMode) { + case TransformMode.Normal: { + const rotationY = rotation + 90 + shearY; + const la = MathUtils.cosDeg(rotation + shearX) * scaleX; + const lb = MathUtils.cosDeg(rotationY) * scaleY; + const lc = MathUtils.sinDeg(rotation + shearX) * scaleX; + const ld = MathUtils.sinDeg(rotationY) * scaleY; + this.a = pa * la + pb * lc; + this.b = pa * lb + pb * ld; + this.c = pc * la + pd * lc; + this.d = pc * lb + pd * ld; + return; + } + case TransformMode.OnlyTranslation: { + const rotationY = rotation + 90 + shearY; + this.a = MathUtils.cosDeg(rotation + shearX) * scaleX; + this.b = MathUtils.cosDeg(rotationY) * scaleY; + this.c = MathUtils.sinDeg(rotation + shearX) * scaleX; + this.d = MathUtils.sinDeg(rotationY) * scaleY; + break; + } + case TransformMode.NoRotationOrReflection: { + let s = pa * pa + pc * pc; + let prx = 0; + if (s > 0.0001) { + s = Math.abs(pa * pd - pb * pc) / s; + pa /= this.skeleton.scaleX; + pc /= this.skeleton.scaleY; + pb = pc * s; + pd = pa * s; + prx = Math.atan2(pc, pa) * MathUtils.radDeg; + } else { + pa = 0; + pc = 0; + prx = 90 - Math.atan2(pd, pb) * MathUtils.radDeg; + } + const rx = rotation + shearX - prx; + const ry = rotation + shearY - prx + 90; + const la = MathUtils.cosDeg(rx) * scaleX; + const lb = MathUtils.cosDeg(ry) * scaleY; + const lc = MathUtils.sinDeg(rx) * scaleX; + const ld = MathUtils.sinDeg(ry) * scaleY; + this.a = pa * la - pb * lc; + this.b = pa * lb - pb * ld; + this.c = pc * la + pd * lc; + this.d = pc * lb + pd * ld; + break; + } + case TransformMode.NoScale: + case TransformMode.NoScaleOrReflection: { + const cos = MathUtils.cosDeg(rotation); + const sin = MathUtils.sinDeg(rotation); + let za = (pa * cos + pb * sin) / this.skeleton.scaleX; + let zc = (pc * cos + pd * sin) / this.skeleton.scaleY; + let s = Math.sqrt(za * za + zc * zc); + if (s > 0.00001) s = 1 / s; + za *= s; + zc *= s; + s = Math.sqrt(za * za + zc * zc); + if ( + this.data.transformMode == TransformMode.NoScale && + pa * pd - pb * pc < 0 != (this.skeleton.scaleX < 0 != this.skeleton.scaleY < 0) + ) + s = -s; + const r = Math.PI / 2 + Math.atan2(zc, za); + const zb = Math.cos(r) * s; + const zd = Math.sin(r) * s; + const la = MathUtils.cosDeg(shearX) * scaleX; + const lb = MathUtils.cosDeg(90 + shearY) * scaleY; + const lc = MathUtils.sinDeg(shearX) * scaleX; + const ld = MathUtils.sinDeg(90 + shearY) * scaleY; + this.a = za * la + zb * lc; + this.b = za * lb + zb * ld; + this.c = zc * la + zd * lc; + this.d = zc * lb + zd * ld; + break; + } + } + this.a *= this.skeleton.scaleX; + this.b *= this.skeleton.scaleX; + this.c *= this.skeleton.scaleY; + this.d *= this.skeleton.scaleY; + } + + /** Sets this bone's local transform to the setup pose. */ + setToSetupPose() { + const data = this.data; + this.x = data.x; + this.y = data.y; + this.rotation = data.rotation; + this.scaleX = data.scaleX; + this.scaleY = data.scaleY; + this.shearX = data.shearX; + this.shearY = data.shearY; + } + + /** The world rotation for the X axis, calculated using {@link #a} and {@link #c}. */ + getWorldRotationX() { + return Math.atan2(this.c, this.a) * MathUtils.radDeg; + } + + /** The world rotation for the Y axis, calculated using {@link #b} and {@link #d}. */ + getWorldRotationY() { + return Math.atan2(this.d, this.b) * MathUtils.radDeg; + } + + /** The magnitude (always positive) of the world scale X, calculated using {@link #a} and {@link #c}. */ + getWorldScaleX() { + return Math.sqrt(this.a * this.a + this.c * this.c); + } + + /** The magnitude (always positive) of the world scale Y, calculated using {@link #b} and {@link #d}. */ + getWorldScaleY() { + return Math.sqrt(this.b * this.b + this.d * this.d); + } + + /** Computes the applied transform values from the world transform. This allows the applied transform to be accessed after the + * world transform has been modified (by a constraint, {@link #rotateWorld()}, etc). + * + * If {@link #updateWorldTransform()} has been called for a bone and {@link #appliedValid} is false, then + * {@link #updateAppliedTransform()} must be called before accessing the applied transform. + * + * Some information is ambiguous in the world transform, such as -1,-1 scale versus 180 rotation. The applied transform after + * calling this method is equivalent to the local tranform used to compute the world transform, but may not be identical. */ + updateAppliedTransform() { + this.appliedValid = true; + const parent = this.parent; + if (parent == null) { + this.ax = this.worldX; + this.ay = this.worldY; + this.arotation = Math.atan2(this.c, this.a) * MathUtils.radDeg; + this.ascaleX = Math.sqrt(this.a * this.a + this.c * this.c); + this.ascaleY = Math.sqrt(this.b * this.b + this.d * this.d); + this.ashearX = 0; + this.ashearY = + Math.atan2(this.a * this.b + this.c * this.d, this.a * this.d - this.b * this.c) * MathUtils.radDeg; + return; + } + const pa = parent.a, + pb = parent.b, + pc = parent.c, + pd = parent.d; + const pid = 1 / (pa * pd - pb * pc); + const dx = this.worldX - parent.worldX, + dy = this.worldY - parent.worldY; + this.ax = dx * pd * pid - dy * pb * pid; + this.ay = dy * pa * pid - dx * pc * pid; + const ia = pid * pd; + const id = pid * pa; + const ib = pid * pb; + const ic = pid * pc; + const ra = ia * this.a - ib * this.c; + const rb = ia * this.b - ib * this.d; + const rc = id * this.c - ic * this.a; + const rd = id * this.d - ic * this.b; + this.ashearX = 0; + this.ascaleX = Math.sqrt(ra * ra + rc * rc); + if (this.ascaleX > 0.0001) { + const det = ra * rd - rb * rc; + this.ascaleY = det / this.ascaleX; + this.ashearY = Math.atan2(ra * rb + rc * rd, det) * MathUtils.radDeg; + this.arotation = Math.atan2(rc, ra) * MathUtils.radDeg; + } else { + this.ascaleX = 0; + this.ascaleY = Math.sqrt(rb * rb + rd * rd); + this.ashearY = 0; + this.arotation = 90 - Math.atan2(rd, rb) * MathUtils.radDeg; + } + } + + /** Transforms a point from world coordinates to the bone's local coordinates. */ + worldToLocal(world: Vector2) { + const a = this.a, + b = this.b, + c = this.c, + d = this.d; + const invDet = 1 / (a * d - b * c); + const x = world.x - this.worldX, + y = world.y - this.worldY; + world.x = x * d * invDet - y * b * invDet; + world.y = y * a * invDet - x * c * invDet; + return world; + } + + /** Transforms a point from the bone's local coordinates to world coordinates. */ + localToWorld(local: Vector2) { + const x = local.x, + y = local.y; + local.x = x * this.a + y * this.b + this.worldX; + local.y = x * this.c + y * this.d + this.worldY; + return local; + } + + /** Transforms a world rotation to a local rotation. */ + worldToLocalRotation(worldRotation: number) { + const sin = MathUtils.sinDeg(worldRotation), + cos = MathUtils.cosDeg(worldRotation); + return ( + Math.atan2(this.a * sin - this.c * cos, this.d * cos - this.b * sin) * MathUtils.radDeg + + this.rotation - + this.shearX + ); + } + + /** Transforms a local rotation to a world rotation. */ + localToWorldRotation(localRotation: number) { + localRotation -= this.rotation - this.shearX; + const sin = MathUtils.sinDeg(localRotation), + cos = MathUtils.cosDeg(localRotation); + return Math.atan2(cos * this.c + sin * this.d, cos * this.a + sin * this.b) * MathUtils.radDeg; + } + + /** Rotates the world transform the specified amount and sets {@link #appliedValid} to false. + * {@link #updateWorldTransform()} will need to be called on any child bones, recursively, and any constraints reapplied. */ + rotateWorld(degrees: number) { + const a = this.a, + b = this.b, + c = this.c, + d = this.d; + const cos = MathUtils.cosDeg(degrees), + sin = MathUtils.sinDeg(degrees); + this.a = cos * a - sin * c; + this.b = cos * b - sin * d; + this.c = sin * a + cos * c; + this.d = sin * b + cos * d; + this.appliedValid = false; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/BoneData.ts b/packages/spine-core-3.8/src/spine-core/BoneData.ts new file mode 100644 index 0000000000..a51d1b16cf --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/BoneData.ts @@ -0,0 +1,66 @@ +import { Color } from "./Utils"; + +/** Stores the setup pose for a {@link Bone}. */ +export class BoneData { + /** The index of the bone in {@link Skeleton#getBones()}. */ + index: number; + + /** The name of the bone, which is unique across all bones in the skeleton. */ + name: string; + + /** @returns May be null. */ + parent: BoneData; + + /** The bone's length. */ + length: number; + + /** The local x translation. */ + x = 0; + + /** The local y translation. */ + y = 0; + + /** The local rotation. */ + rotation = 0; + + /** The local scaleX. */ + scaleX = 1; + + /** The local scaleY. */ + scaleY = 1; + + /** The local shearX. */ + shearX = 0; + + /** The local shearX. */ + shearY = 0; + + /** The transform mode for how parent world transforms affect this bone. */ + transformMode = TransformMode.Normal; + + /** When true, {@link Skeleton#updateWorldTransform()} only updates this bone if the {@link Skeleton#skin} contains this + * bone. + * @see Skin#bones */ + skinRequired = false; + + /** The color of the bone as it was in Spine. Available only when nonessential data was exported. Bones are not usually + * rendered at runtime. */ + color = new Color(); + + constructor(index: number, name: string, parent: BoneData) { + if (index < 0) throw new Error("index must be >= 0."); + if (name == null) throw new Error("name cannot be null."); + this.index = index; + this.name = name; + this.parent = parent; + } +} + +/** Determines how a bone inherits world transforms from parent bones. */ +export enum TransformMode { + Normal, + OnlyTranslation, + NoRotationOrReflection, + NoScale, + NoScaleOrReflection +} diff --git a/packages/spine-core-3.8/src/spine-core/ConstraintData.ts b/packages/spine-core-3.8/src/spine-core/ConstraintData.ts new file mode 100644 index 0000000000..5d928d2169 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/ConstraintData.ts @@ -0,0 +1,8 @@ +/** The base class for all constraint datas. */ +export abstract class ConstraintData { + constructor( + public name: string, + public order: number, + public skinRequired: boolean + ) {} +} diff --git a/packages/spine-core-3.8/src/spine-core/Event.ts b/packages/spine-core-3.8/src/spine-core/Event.ts new file mode 100644 index 0000000000..a6318c90a9 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/Event.ts @@ -0,0 +1,22 @@ +import { EventData } from "./EventData"; + +/** Stores the current pose values for an {@link Event}. + * + * See Timeline {@link Timeline#apply()}, + * AnimationStateListener {@link AnimationStateListener#event()}, and + * [Events](http://esotericsoftware.com/spine-events) in the Spine User Guide. */ +export class Event { + data: EventData; + intValue: number; + floatValue: number; + stringValue: string; + time: number; + volume: number; + balance: number; + + constructor(time: number, data: EventData) { + if (data == null) throw new Error("data cannot be null."); + this.time = time; + this.data = data; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/EventData.ts b/packages/spine-core-3.8/src/spine-core/EventData.ts new file mode 100644 index 0000000000..46aa71702e --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/EventData.ts @@ -0,0 +1,16 @@ +/** Stores the setup pose values for an {@link Event}. + * + * See [Events](http://esotericsoftware.com/spine-events) in the Spine User Guide. */ +export class EventData { + name: string; + intValue: number; + floatValue: number; + stringValue: string; + audioPath: string; + volume: number; + balance: number; + + constructor(name: string) { + this.name = name; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/IkConstraint.ts b/packages/spine-core-3.8/src/spine-core/IkConstraint.ts new file mode 100644 index 0000000000..7f05ce8e04 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/IkConstraint.ts @@ -0,0 +1,346 @@ +import { Updatable } from "./Updatable"; +import { IkConstraintData } from "./IkConstraintData"; +import { Bone } from "./Bone"; +import { Skeleton } from "./Skeleton"; +import { TransformMode } from "./BoneData"; +import { MathUtils } from "./Utils"; + +/** Stores the current pose for an IK constraint. An IK constraint adjusts the rotation of 1 or 2 constrained bones so the tip of + * the last bone is as close to the target bone as possible. + * + * See [IK constraints](http://esotericsoftware.com/spine-ik-constraints) in the Spine User Guide. */ +export class IkConstraint implements Updatable { + /** The IK constraint's setup pose data. */ + data: IkConstraintData; + + /** The bones that will be modified by this IK constraint. */ + bones: Array; + + /** The bone that is the IK target. */ + target: Bone; + + /** Controls the bend direction of the IK bones, either 1 or -1. */ + bendDirection = 0; + + /** When true and only a single bone is being constrained, if the target is too close, the bone is scaled to reach it. */ + compress = false; + + /** When true, if the target is out of range, the parent bone is scaled to reach it. If more than one bone is being constrained + * and the parent bone has local nonuniform scale, stretch is not applied. */ + stretch = false; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained rotations. */ + mix = 1; + + /** For two bone IK, the distance from the maximum reach of the bones that rotation will slow. */ + softness = 0; + active = false; + + constructor(data: IkConstraintData, skeleton: Skeleton) { + if (data == null) throw new Error("data cannot be null."); + if (skeleton == null) throw new Error("skeleton cannot be null."); + this.data = data; + this.mix = data.mix; + this.softness = data.softness; + this.bendDirection = data.bendDirection; + this.compress = data.compress; + this.stretch = data.stretch; + + this.bones = new Array(); + for (let i = 0; i < data.bones.length; i++) this.bones.push(skeleton.findBone(data.bones[i].name)); + this.target = skeleton.findBone(data.target.name); + } + + isActive() { + return this.active; + } + + /** Applies the constraint to the constrained bones. */ + apply() { + this.update(); + } + + update() { + const target = this.target; + const bones = this.bones; + switch (bones.length) { + case 1: + this.apply1(bones[0], target.worldX, target.worldY, this.compress, this.stretch, this.data.uniform, this.mix); + break; + case 2: + this.apply2( + bones[0], + bones[1], + target.worldX, + target.worldY, + this.bendDirection, + this.stretch, + this.softness, + this.mix + ); + break; + } + } + + /** Applies 1 bone IK. The target is specified in the world coordinate system. */ + apply1( + bone: Bone, + targetX: number, + targetY: number, + compress: boolean, + stretch: boolean, + uniform: boolean, + alpha: number + ) { + if (!bone.appliedValid) bone.updateAppliedTransform(); + const p = bone.parent; + + let pa = p.a, + pb = p.b, + pc = p.c, + pd = p.d; + let rotationIK = -bone.ashearX - bone.arotation, + tx = 0, + ty = 0; + + switch (bone.data.transformMode) { + case TransformMode.OnlyTranslation: + tx = targetX - bone.worldX; + ty = targetY - bone.worldY; + break; + case TransformMode.NoRotationOrReflection: + const s = Math.abs(pa * pd - pb * pc) / (pa * pa + pc * pc); + const sa = pa / bone.skeleton.scaleX; + const sc = pc / bone.skeleton.scaleY; + pb = -sc * s * bone.skeleton.scaleX; + pd = sa * s * bone.skeleton.scaleY; + rotationIK += Math.atan2(sc, sa) * MathUtils.radDeg; + // Fall through + default: + const x = targetX - p.worldX, + y = targetY - p.worldY; + const d = pa * pd - pb * pc; + tx = (x * pd - y * pb) / d - bone.ax; + ty = (y * pa - x * pc) / d - bone.ay; + } + rotationIK += Math.atan2(ty, tx) * MathUtils.radDeg; + if (bone.ascaleX < 0) rotationIK += 180; + if (rotationIK > 180) rotationIK -= 360; + else if (rotationIK < -180) rotationIK += 360; + let sx = bone.ascaleX, + sy = bone.ascaleY; + if (compress || stretch) { + switch (bone.data.transformMode) { + case TransformMode.NoScale: + case TransformMode.NoScaleOrReflection: + tx = targetX - bone.worldX; + ty = targetY - bone.worldY; + } + const b = bone.data.length * sx, + dd = Math.sqrt(tx * tx + ty * ty); + if ((compress && dd < b) || (stretch && dd > b && b > 0.0001)) { + const s = (dd / b - 1) * alpha + 1; + sx *= s; + if (uniform) sy *= s; + } + } + bone.updateWorldTransformWith( + bone.ax, + bone.ay, + bone.arotation + rotationIK * alpha, + sx, + sy, + bone.ashearX, + bone.ashearY + ); + } + + /** Applies 2 bone IK. The target is specified in the world coordinate system. + * @param child A direct descendant of the parent bone. */ + apply2( + parent: Bone, + child: Bone, + targetX: number, + targetY: number, + bendDir: number, + stretch: boolean, + softness: number, + alpha: number + ) { + if (alpha == 0) { + child.updateWorldTransform(); + return; + } + if (!parent.appliedValid) parent.updateAppliedTransform(); + if (!child.appliedValid) child.updateAppliedTransform(); + let px = parent.ax, + py = parent.ay, + psx = parent.ascaleX, + sx = psx, + psy = parent.ascaleY, + csx = child.ascaleX; + let os1 = 0, + os2 = 0, + s2 = 0; + if (psx < 0) { + psx = -psx; + os1 = 180; + s2 = -1; + } else { + os1 = 0; + s2 = 1; + } + if (psy < 0) { + psy = -psy; + s2 = -s2; + } + if (csx < 0) { + csx = -csx; + os2 = 180; + } else os2 = 0; + let cx = child.ax, + cy = 0, + cwx = 0, + cwy = 0, + a = parent.a, + b = parent.b, + c = parent.c, + d = parent.d; + const u = Math.abs(psx - psy) <= 0.0001; + if (!u) { + cy = 0; + cwx = a * cx + parent.worldX; + cwy = c * cx + parent.worldY; + } else { + cy = child.ay; + cwx = a * cx + b * cy + parent.worldX; + cwy = c * cx + d * cy + parent.worldY; + } + const pp = parent.parent; + a = pp.a; + b = pp.b; + c = pp.c; + d = pp.d; + let id = 1 / (a * d - b * c), + x = cwx - pp.worldX, + y = cwy - pp.worldY; + const dx = (x * d - y * b) * id - px, + dy = (y * a - x * c) * id - py; + let l1 = Math.sqrt(dx * dx + dy * dy), + l2 = child.data.length * csx, + a1, + a2; + if (l1 < 0.0001) { + this.apply1(parent, targetX, targetY, false, stretch, false, alpha); + child.updateWorldTransformWith(cx, cy, 0, child.ascaleX, child.ascaleY, child.ashearX, child.ashearY); + return; + } + x = targetX - pp.worldX; + y = targetY - pp.worldY; + let tx = (x * d - y * b) * id - px, + ty = (y * a - x * c) * id - py; + let dd = tx * tx + ty * ty; + if (softness != 0) { + softness *= (psx * (csx + 1)) / 2; + const td = Math.sqrt(dd), + sd = td - l1 - l2 * psx + softness; + if (sd > 0) { + let p = Math.min(1, sd / (softness * 2)) - 1; + p = (sd - softness * (1 - p * p)) / td; + tx -= p * tx; + ty -= p * ty; + dd = tx * tx + ty * ty; + } + } + outer: if (u) { + l2 *= psx; + let cos = (dd - l1 * l1 - l2 * l2) / (2 * l1 * l2); + if (cos < -1) cos = -1; + else if (cos > 1) { + cos = 1; + if (stretch) sx *= (Math.sqrt(dd) / (l1 + l2) - 1) * alpha + 1; + } + a2 = Math.acos(cos) * bendDir; + a = l1 + l2 * cos; + b = l2 * Math.sin(a2); + a1 = Math.atan2(ty * a - tx * b, tx * a + ty * b); + } else { + a = psx * l2; + b = psy * l2; + const aa = a * a, + bb = b * b, + ta = Math.atan2(ty, tx); + c = bb * l1 * l1 + aa * dd - aa * bb; + const c1 = -2 * bb * l1, + c2 = bb - aa; + d = c1 * c1 - 4 * c2 * c; + if (d >= 0) { + let q = Math.sqrt(d); + if (c1 < 0) q = -q; + q = -(c1 + q) / 2; + const r0 = q / c2, + r1 = c / q; + const r = Math.abs(r0) < Math.abs(r1) ? r0 : r1; + if (r * r <= dd) { + y = Math.sqrt(dd - r * r) * bendDir; + a1 = ta - Math.atan2(y, r); + a2 = Math.atan2(y / psy, (r - l1) / psx); + break outer; + } + } + let minAngle = MathUtils.PI, + minX = l1 - a, + minDist = minX * minX, + minY = 0; + let maxAngle = 0, + maxX = l1 + a, + maxDist = maxX * maxX, + maxY = 0; + c = (-a * l1) / (aa - bb); + if (c >= -1 && c <= 1) { + c = Math.acos(c); + x = a * Math.cos(c) + l1; + y = b * Math.sin(c); + d = x * x + y * y; + if (d < minDist) { + minAngle = c; + minDist = d; + minX = x; + minY = y; + } + if (d > maxDist) { + maxAngle = c; + maxDist = d; + maxX = x; + maxY = y; + } + } + if (dd <= (minDist + maxDist) / 2) { + a1 = ta - Math.atan2(minY * bendDir, minX); + a2 = minAngle * bendDir; + } else { + a1 = ta - Math.atan2(maxY * bendDir, maxX); + a2 = maxAngle * bendDir; + } + } + const os = Math.atan2(cy, cx) * s2; + let rotation = parent.arotation; + a1 = (a1 - os) * MathUtils.radDeg + os1 - rotation; + if (a1 > 180) a1 -= 360; + else if (a1 < -180) a1 += 360; + parent.updateWorldTransformWith(px, py, rotation + a1 * alpha, sx, parent.ascaleY, 0, 0); + rotation = child.arotation; + a2 = ((a2 + os) * MathUtils.radDeg - child.ashearX) * s2 + os2 - rotation; + if (a2 > 180) a2 -= 360; + else if (a2 < -180) a2 += 360; + child.updateWorldTransformWith( + cx, + cy, + rotation + a2 * alpha, + child.ascaleX, + child.ascaleY, + child.ashearX, + child.ashearY + ); + } +} diff --git a/packages/spine-core-3.8/src/spine-core/IkConstraintData.ts b/packages/spine-core-3.8/src/spine-core/IkConstraintData.ts new file mode 100644 index 0000000000..200085b990 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/IkConstraintData.ts @@ -0,0 +1,37 @@ +import { ConstraintData } from "./ConstraintData"; +import { BoneData } from "./BoneData"; + +/** Stores the setup pose for an {@link IkConstraint}. + *

+ * See [IK constraints](http://esotericsoftware.com/spine-ik-constraints) in the Spine User Guide. */ +export class IkConstraintData extends ConstraintData { + /** The bones that are constrained by this IK constraint. */ + bones = new Array(); + + /** The bone that is the IK target. */ + target: BoneData; + + /** Controls the bend direction of the IK bones, either 1 or -1. */ + bendDirection = 1; + + /** When true and only a single bone is being constrained, if the target is too close, the bone is scaled to reach it. */ + compress = false; + + /** When true, if the target is out of range, the parent bone is scaled to reach it. If more than one bone is being constrained + * and the parent bone has local nonuniform scale, stretch is not applied. */ + stretch = false; + + /** When true, only a single bone is being constrained, and {@link #getCompress()} or {@link #getStretch()} is used, the bone + * is scaled on both the X and Y axes. */ + uniform = false; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained rotations. */ + mix = 1; + + /** For two bone IK, the distance from the maximum reach of the bones that rotation will slow. */ + softness = 0; + + constructor(name: string) { + super(name, 0, false); + } +} diff --git a/packages/spine-core-3.8/src/spine-core/PathConstraint.ts b/packages/spine-core-3.8/src/spine-core/PathConstraint.ts new file mode 100644 index 0000000000..057ebd1f20 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/PathConstraint.ts @@ -0,0 +1,500 @@ +import { Updatable } from "./Updatable"; +import { PathConstraintData, SpacingMode, RotateMode, PositionMode } from "./PathConstraintData"; +import { Bone } from "./Bone"; +import { Slot } from "./Slot"; +import { Skeleton } from "./Skeleton"; +import { PathAttachment } from "./attachments/PathAttachment"; +import { Utils, MathUtils } from "./Utils"; + +/** Stores the current pose for a path constraint. A path constraint adjusts the rotation, translation, and scale of the + * constrained bones so they follow a {@link PathAttachment}. + * + * See [Path constraints](http://esotericsoftware.com/spine-path-constraints) in the Spine User Guide. */ +export class PathConstraint implements Updatable { + static NONE = -1; + static BEFORE = -2; + static AFTER = -3; + static epsilon = 0.00001; + + /** The path constraint's setup pose data. */ + data: PathConstraintData; + + /** The bones that will be modified by this path constraint. */ + bones: Array; + + /** The slot whose path attachment will be used to constrained the bones. */ + target: Slot; + + /** The position along the path. */ + position = 0; + + /** The spacing between bones. */ + spacing = 0; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained rotations. */ + rotateMix = 0; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained translations. */ + translateMix = 0; + + spaces = new Array(); + positions = new Array(); + world = new Array(); + curves = new Array(); + lengths = new Array(); + segments = new Array(); + + active = false; + + constructor(data: PathConstraintData, skeleton: Skeleton) { + if (data == null) throw new Error("data cannot be null."); + if (skeleton == null) throw new Error("skeleton cannot be null."); + this.data = data; + this.bones = new Array(); + for (let i = 0, n = data.bones.length; i < n; i++) this.bones.push(skeleton.findBone(data.bones[i].name)); + this.target = skeleton.findSlot(data.target.name); + this.position = data.position; + this.spacing = data.spacing; + this.rotateMix = data.rotateMix; + this.translateMix = data.translateMix; + } + + isActive() { + return this.active; + } + + /** Applies the constraint to the constrained bones. */ + apply() { + this.update(); + } + + update() { + const attachment = this.target.getAttachment(); + if (!(attachment instanceof PathAttachment)) return; + + const rotateMix = this.rotateMix, + translateMix = this.translateMix; + const translate = translateMix > 0, + rotate = rotateMix > 0; + if (!translate && !rotate) return; + + const data = this.data; + const percentSpacing = data.spacingMode == SpacingMode.Percent; + const rotateMode = data.rotateMode; + const tangents = rotateMode == RotateMode.Tangent, + scale = rotateMode == RotateMode.ChainScale; + const boneCount = this.bones.length, + spacesCount = tangents ? boneCount : boneCount + 1; + const bones = this.bones; + let spaces = Utils.setArraySize(this.spaces, spacesCount), + lengths: Array = null; + const spacing = this.spacing; + if (scale || !percentSpacing) { + if (scale) lengths = Utils.setArraySize(this.lengths, boneCount); + const lengthSpacing = data.spacingMode == SpacingMode.Length; + for (let i = 0, n = spacesCount - 1; i < n; ) { + const bone = bones[i]; + const setupLength = bone.data.length; + if (setupLength < PathConstraint.epsilon) { + if (scale) lengths[i] = 0; + spaces[++i] = 0; + } else if (percentSpacing) { + if (scale) { + const x = setupLength * bone.a, + y = setupLength * bone.c; + const length = Math.sqrt(x * x + y * y); + lengths[i] = length; + } + spaces[++i] = spacing; + } else { + const x = setupLength * bone.a, + y = setupLength * bone.c; + const length = Math.sqrt(x * x + y * y); + if (scale) lengths[i] = length; + spaces[++i] = ((lengthSpacing ? setupLength + spacing : spacing) * length) / setupLength; + } + } + } else { + for (let i = 1; i < spacesCount; i++) spaces[i] = spacing; + } + + const positions = this.computeWorldPositions( + attachment, + spacesCount, + tangents, + data.positionMode == PositionMode.Percent, + percentSpacing + ); + let boneX = positions[0], + boneY = positions[1], + offsetRotation = data.offsetRotation; + let tip = false; + if (offsetRotation == 0) tip = rotateMode == RotateMode.Chain; + else { + tip = false; + const p = this.target.bone; + offsetRotation *= p.a * p.d - p.b * p.c > 0 ? MathUtils.degRad : -MathUtils.degRad; + } + for (let i = 0, p = 3; i < boneCount; i++, p += 3) { + const bone = bones[i]; + bone.worldX += (boneX - bone.worldX) * translateMix; + bone.worldY += (boneY - bone.worldY) * translateMix; + const x = positions[p], + y = positions[p + 1], + dx = x - boneX, + dy = y - boneY; + if (scale) { + const length = lengths[i]; + if (length != 0) { + const s = (Math.sqrt(dx * dx + dy * dy) / length - 1) * rotateMix + 1; + bone.a *= s; + bone.c *= s; + } + } + boneX = x; + boneY = y; + if (rotate) { + let a = bone.a, + b = bone.b, + c = bone.c, + d = bone.d, + r = 0, + cos = 0, + sin = 0; + if (tangents) r = positions[p - 1]; + else if (spaces[i + 1] == 0) r = positions[p + 2]; + else r = Math.atan2(dy, dx); + r -= Math.atan2(c, a); + if (tip) { + cos = Math.cos(r); + sin = Math.sin(r); + const length = bone.data.length; + boneX += (length * (cos * a - sin * c) - dx) * rotateMix; + boneY += (length * (sin * a + cos * c) - dy) * rotateMix; + } else { + r += offsetRotation; + } + if (r > MathUtils.PI) r -= MathUtils.PI2; + else if (r < -MathUtils.PI) + // + r += MathUtils.PI2; + r *= rotateMix; + cos = Math.cos(r); + sin = Math.sin(r); + bone.a = cos * a - sin * c; + bone.b = cos * b - sin * d; + bone.c = sin * a + cos * c; + bone.d = sin * b + cos * d; + } + bone.appliedValid = false; + } + } + + computeWorldPositions( + path: PathAttachment, + spacesCount: number, + tangents: boolean, + percentPosition: boolean, + percentSpacing: boolean + ) { + const target = this.target; + let position = this.position; + let spaces = this.spaces, + out = Utils.setArraySize(this.positions, spacesCount * 3 + 2), + world: Array = null; + const closed = path.closed; + let verticesLength = path.worldVerticesLength, + curveCount = verticesLength / 6, + prevCurve = PathConstraint.NONE; + + if (!path.constantSpeed) { + const lengths = path.lengths; + curveCount -= closed ? 1 : 2; + const pathLength = lengths[curveCount]; + if (percentPosition) position *= pathLength; + if (percentSpacing) { + for (let i = 1; i < spacesCount; i++) spaces[i] *= pathLength; + } + world = Utils.setArraySize(this.world, 8); + for (let i = 0, o = 0, curve = 0; i < spacesCount; i++, o += 3) { + const space = spaces[i]; + position += space; + let p = position; + + if (closed) { + p %= pathLength; + if (p < 0) p += pathLength; + curve = 0; + } else if (p < 0) { + if (prevCurve != PathConstraint.BEFORE) { + prevCurve = PathConstraint.BEFORE; + path.computeWorldVertices(target, 2, 4, world, 0, 2); + } + this.addBeforePosition(p, world, 0, out, o); + continue; + } else if (p > pathLength) { + if (prevCurve != PathConstraint.AFTER) { + prevCurve = PathConstraint.AFTER; + path.computeWorldVertices(target, verticesLength - 6, 4, world, 0, 2); + } + this.addAfterPosition(p - pathLength, world, 0, out, o); + continue; + } + + // Determine curve containing position. + for (; ; curve++) { + const length = lengths[curve]; + if (p > length) continue; + if (curve == 0) p /= length; + else { + const prev = lengths[curve - 1]; + p = (p - prev) / (length - prev); + } + break; + } + if (curve != prevCurve) { + prevCurve = curve; + if (closed && curve == curveCount) { + path.computeWorldVertices(target, verticesLength - 4, 4, world, 0, 2); + path.computeWorldVertices(target, 0, 4, world, 4, 2); + } else path.computeWorldVertices(target, curve * 6 + 2, 8, world, 0, 2); + } + this.addCurvePosition( + p, + world[0], + world[1], + world[2], + world[3], + world[4], + world[5], + world[6], + world[7], + out, + o, + tangents || (i > 0 && space == 0) + ); + } + return out; + } + + // World vertices. + if (closed) { + verticesLength += 2; + world = Utils.setArraySize(this.world, verticesLength); + path.computeWorldVertices(target, 2, verticesLength - 4, world, 0, 2); + path.computeWorldVertices(target, 0, 2, world, verticesLength - 4, 2); + world[verticesLength - 2] = world[0]; + world[verticesLength - 1] = world[1]; + } else { + curveCount--; + verticesLength -= 4; + world = Utils.setArraySize(this.world, verticesLength); + path.computeWorldVertices(target, 2, verticesLength, world, 0, 2); + } + + // Curve lengths. + const curves = Utils.setArraySize(this.curves, curveCount); + let pathLength = 0; + let x1 = world[0], + y1 = world[1], + cx1 = 0, + cy1 = 0, + cx2 = 0, + cy2 = 0, + x2 = 0, + y2 = 0; + let tmpx = 0, + tmpy = 0, + dddfx = 0, + dddfy = 0, + ddfx = 0, + ddfy = 0, + dfx = 0, + dfy = 0; + for (let i = 0, w = 2; i < curveCount; i++, w += 6) { + cx1 = world[w]; + cy1 = world[w + 1]; + cx2 = world[w + 2]; + cy2 = world[w + 3]; + x2 = world[w + 4]; + y2 = world[w + 5]; + tmpx = (x1 - cx1 * 2 + cx2) * 0.1875; + tmpy = (y1 - cy1 * 2 + cy2) * 0.1875; + dddfx = ((cx1 - cx2) * 3 - x1 + x2) * 0.09375; + dddfy = ((cy1 - cy2) * 3 - y1 + y2) * 0.09375; + ddfx = tmpx * 2 + dddfx; + ddfy = tmpy * 2 + dddfy; + dfx = (cx1 - x1) * 0.75 + tmpx + dddfx * 0.16666667; + dfy = (cy1 - y1) * 0.75 + tmpy + dddfy * 0.16666667; + pathLength += Math.sqrt(dfx * dfx + dfy * dfy); + dfx += ddfx; + dfy += ddfy; + ddfx += dddfx; + ddfy += dddfy; + pathLength += Math.sqrt(dfx * dfx + dfy * dfy); + dfx += ddfx; + dfy += ddfy; + pathLength += Math.sqrt(dfx * dfx + dfy * dfy); + dfx += ddfx + dddfx; + dfy += ddfy + dddfy; + pathLength += Math.sqrt(dfx * dfx + dfy * dfy); + curves[i] = pathLength; + x1 = x2; + y1 = y2; + } + if (percentPosition) position *= pathLength; + else position *= pathLength / path.lengths[curveCount - 1]; + if (percentSpacing) { + for (let i = 1; i < spacesCount; i++) spaces[i] *= pathLength; + } + + const segments = this.segments; + let curveLength = 0; + for (let i = 0, o = 0, curve = 0, segment = 0; i < spacesCount; i++, o += 3) { + const space = spaces[i]; + position += space; + let p = position; + + if (closed) { + p %= pathLength; + if (p < 0) p += pathLength; + curve = 0; + } else if (p < 0) { + this.addBeforePosition(p, world, 0, out, o); + continue; + } else if (p > pathLength) { + this.addAfterPosition(p - pathLength, world, verticesLength - 4, out, o); + continue; + } + + // Determine curve containing position. + for (; ; curve++) { + const length = curves[curve]; + if (p > length) continue; + if (curve == 0) p /= length; + else { + const prev = curves[curve - 1]; + p = (p - prev) / (length - prev); + } + break; + } + + // Curve segment lengths. + if (curve != prevCurve) { + prevCurve = curve; + let ii = curve * 6; + x1 = world[ii]; + y1 = world[ii + 1]; + cx1 = world[ii + 2]; + cy1 = world[ii + 3]; + cx2 = world[ii + 4]; + cy2 = world[ii + 5]; + x2 = world[ii + 6]; + y2 = world[ii + 7]; + tmpx = (x1 - cx1 * 2 + cx2) * 0.03; + tmpy = (y1 - cy1 * 2 + cy2) * 0.03; + dddfx = ((cx1 - cx2) * 3 - x1 + x2) * 0.006; + dddfy = ((cy1 - cy2) * 3 - y1 + y2) * 0.006; + ddfx = tmpx * 2 + dddfx; + ddfy = tmpy * 2 + dddfy; + dfx = (cx1 - x1) * 0.3 + tmpx + dddfx * 0.16666667; + dfy = (cy1 - y1) * 0.3 + tmpy + dddfy * 0.16666667; + curveLength = Math.sqrt(dfx * dfx + dfy * dfy); + segments[0] = curveLength; + for (ii = 1; ii < 8; ii++) { + dfx += ddfx; + dfy += ddfy; + ddfx += dddfx; + ddfy += dddfy; + curveLength += Math.sqrt(dfx * dfx + dfy * dfy); + segments[ii] = curveLength; + } + dfx += ddfx; + dfy += ddfy; + curveLength += Math.sqrt(dfx * dfx + dfy * dfy); + segments[8] = curveLength; + dfx += ddfx + dddfx; + dfy += ddfy + dddfy; + curveLength += Math.sqrt(dfx * dfx + dfy * dfy); + segments[9] = curveLength; + segment = 0; + } + + // Weight by segment length. + p *= curveLength; + for (; ; segment++) { + const length = segments[segment]; + if (p > length) continue; + if (segment == 0) p /= length; + else { + const prev = segments[segment - 1]; + p = segment + (p - prev) / (length - prev); + } + break; + } + this.addCurvePosition(p * 0.1, x1, y1, cx1, cy1, cx2, cy2, x2, y2, out, o, tangents || (i > 0 && space == 0)); + } + return out; + } + + addBeforePosition(p: number, temp: Array, i: number, out: Array, o: number) { + const x1 = temp[i], + y1 = temp[i + 1], + dx = temp[i + 2] - x1, + dy = temp[i + 3] - y1, + r = Math.atan2(dy, dx); + out[o] = x1 + p * Math.cos(r); + out[o + 1] = y1 + p * Math.sin(r); + out[o + 2] = r; + } + + addAfterPosition(p: number, temp: Array, i: number, out: Array, o: number) { + const x1 = temp[i + 2], + y1 = temp[i + 3], + dx = x1 - temp[i], + dy = y1 - temp[i + 1], + r = Math.atan2(dy, dx); + out[o] = x1 + p * Math.cos(r); + out[o + 1] = y1 + p * Math.sin(r); + out[o + 2] = r; + } + + addCurvePosition( + p: number, + x1: number, + y1: number, + cx1: number, + cy1: number, + cx2: number, + cy2: number, + x2: number, + y2: number, + out: Array, + o: number, + tangents: boolean + ) { + if (p == 0 || isNaN(p)) { + out[o] = x1; + out[o + 1] = y1; + out[o + 2] = Math.atan2(cy1 - y1, cx1 - x1); + return; + } + const tt = p * p, + ttt = tt * p, + u = 1 - p, + uu = u * u, + uuu = uu * u; + const ut = u * p, + ut3 = ut * 3, + uut3 = u * ut3, + utt3 = ut3 * p; + const x = x1 * uuu + cx1 * uut3 + cx2 * utt3 + x2 * ttt, + y = y1 * uuu + cy1 * uut3 + cy2 * utt3 + y2 * ttt; + out[o] = x; + out[o + 1] = y; + if (tangents) { + if (p < 0.001) out[o + 2] = Math.atan2(cy1 - y1, cx1 - x1); + else out[o + 2] = Math.atan2(y - (y1 * uu + cy1 * ut * 2 + cy2 * tt), x - (x1 * uu + cx1 * ut * 2 + cx2 * tt)); + } + } +} diff --git a/packages/spine-core-3.8/src/spine-core/PathConstraintData.ts b/packages/spine-core-3.8/src/spine-core/PathConstraintData.ts new file mode 100644 index 0000000000..32f5bafe50 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/PathConstraintData.ts @@ -0,0 +1,68 @@ +import { ConstraintData } from "./ConstraintData"; +import { BoneData } from "./BoneData"; +import { SlotData } from "./SlotData"; + +/** Stores the setup pose for a {@link PathConstraint}. + * + * See [Path constraints](http://esotericsoftware.com/spine-path-constraints) in the Spine User Guide. */ +export class PathConstraintData extends ConstraintData { + /** The bones that will be modified by this path constraint. */ + bones = new Array(); + + /** The slot whose path attachment will be used to constrained the bones. */ + target: SlotData; + + /** The mode for positioning the first bone on the path. */ + positionMode: PositionMode; + + /** The mode for positioning the bones after the first bone on the path. */ + spacingMode: SpacingMode; + + /** The mode for adjusting the rotation of the bones. */ + rotateMode: RotateMode; + + /** An offset added to the constrained bone rotation. */ + offsetRotation: number; + + /** The position along the path. */ + position: number; + + /** The spacing between bones. */ + spacing: number; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained rotations. */ + rotateMix: number; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained translations. */ + translateMix: number; + + constructor(name: string) { + super(name, 0, false); + } +} + +/** Controls how the first bone is positioned along the path. + * + * See [Position mode](http://esotericsoftware.com/spine-path-constraints#Position-mode) in the Spine User Guide. */ +export enum PositionMode { + Fixed, + Percent +} + +/** Controls how bones after the first bone are positioned along the path. + * + * [Spacing mode](http://esotericsoftware.com/spine-path-constraints#Spacing-mode) in the Spine User Guide. */ +export enum SpacingMode { + Length, + Fixed, + Percent +} + +/** Controls how bones are rotated, translated, and scaled to match the path. + * + * [Rotate mode](http://esotericsoftware.com/spine-path-constraints#Rotate-mod) in the Spine User Guide. */ +export enum RotateMode { + Tangent, + Chain, + ChainScale +} diff --git a/packages/spine-core-3.8/src/spine-core/Skeleton.ts b/packages/spine-core-3.8/src/spine-core/Skeleton.ts new file mode 100644 index 0000000000..2573e25dd9 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/Skeleton.ts @@ -0,0 +1,592 @@ +import { SkeletonData } from "./SkeletonData"; +import { Bone } from "./Bone"; +import { Slot } from "./Slot"; +import { IkConstraint } from "./IkConstraint"; +import { TransformConstraint } from "./TransformConstraint"; +import { PathConstraint } from "./PathConstraint"; +import { Updatable } from "./Updatable"; +import { Skin } from "./Skin"; +import { Color, Utils, Vector2 } from "./Utils"; +import { PathAttachment } from "./attachments/PathAttachment"; +import { Attachment } from "./attachments/Attachment"; +import { RegionAttachment } from "./attachments/RegionAttachment"; +import { MeshAttachment } from "./attachments/MeshAttachment"; + +/** Stores the current pose for a skeleton. + * + * See [Instance objects](http://esotericsoftware.com/spine-runtime-architecture#Instance-objects) in the Spine Runtimes Guide. */ +export class Skeleton { + /** The skeleton's setup pose data. */ + data: SkeletonData; + + /** The skeleton's bones, sorted parent first. The root bone is always the first bone. */ + bones: Array; + + /** The skeleton's slots. */ + slots: Array; + + /** The skeleton's slots in the order they should be drawn. The returned array may be modified to change the draw order. */ + drawOrder: Array; + + /** The skeleton's IK constraints. */ + ikConstraints: Array; + + /** The skeleton's transform constraints. */ + transformConstraints: Array; + + /** The skeleton's path constraints. */ + pathConstraints: Array; + + /** The list of bones and constraints, sorted in the order they should be updated, as computed by {@link #updateCache()}. */ + _updateCache = new Array(); + updateCacheReset = new Array(); + + /** The skeleton's current skin. May be null. */ + skin: Skin; + + /** The color to tint all the skeleton's attachments. */ + color: Color; + + /** Returns the skeleton's time. This can be used for tracking, such as with Slot {@link Slot#attachmentTime}. + *

+ * See {@link #update()}. */ + time = 0; + + /** Scales the entire skeleton on the X axis. This affects all bones, even if the bone's transform mode disallows scale + * inheritance. */ + scaleX = 1; + + /** Scales the entire skeleton on the Y axis. This affects all bones, even if the bone's transform mode disallows scale + * inheritance. */ + scaleY = 1; + + /** Sets the skeleton X position, which is added to the root bone worldX position. */ + x = 0; + + /** Sets the skeleton Y position, which is added to the root bone worldY position. */ + y = 0; + + constructor(data: SkeletonData) { + if (data == null) throw new Error("data cannot be null."); + this.data = data; + + this.bones = new Array(); + for (let i = 0; i < data.bones.length; i++) { + const boneData = data.bones[i]; + let bone: Bone; + if (boneData.parent == null) bone = new Bone(boneData, this, null); + else { + const parent = this.bones[boneData.parent.index]; + bone = new Bone(boneData, this, parent); + parent.children.push(bone); + } + this.bones.push(bone); + } + + this.slots = new Array(); + this.drawOrder = new Array(); + for (let i = 0; i < data.slots.length; i++) { + const slotData = data.slots[i]; + const bone = this.bones[slotData.boneData.index]; + const slot = new Slot(slotData, bone); + this.slots.push(slot); + this.drawOrder.push(slot); + } + + this.ikConstraints = new Array(); + for (let i = 0; i < data.ikConstraints.length; i++) { + const ikConstraintData = data.ikConstraints[i]; + this.ikConstraints.push(new IkConstraint(ikConstraintData, this)); + } + + this.transformConstraints = new Array(); + for (let i = 0; i < data.transformConstraints.length; i++) { + const transformConstraintData = data.transformConstraints[i]; + this.transformConstraints.push(new TransformConstraint(transformConstraintData, this)); + } + + this.pathConstraints = new Array(); + for (let i = 0; i < data.pathConstraints.length; i++) { + const pathConstraintData = data.pathConstraints[i]; + this.pathConstraints.push(new PathConstraint(pathConstraintData, this)); + } + + this.color = new Color(1, 1, 1, 1); + this.updateCache(); + } + + /** Caches information about bones and constraints. Must be called if the {@link #getSkin()} is modified or if bones, + * constraints, or weighted path attachments are added or removed. */ + updateCache() { + const updateCache = this._updateCache; + updateCache.length = 0; + this.updateCacheReset.length = 0; + + const bones = this.bones; + for (let i = 0, n = bones.length; i < n; i++) { + const bone = bones[i]; + bone.sorted = bone.data.skinRequired; + bone.active = !bone.sorted; + } + + if (this.skin != null) { + const skinBones = this.skin.bones; + for (let i = 0, n = this.skin.bones.length; i < n; i++) { + let bone = this.bones[skinBones[i].index]; + do { + bone.sorted = false; + bone.active = true; + bone = bone.parent; + } while (bone != null); + } + } + + // IK first, lowest hierarchy depth first. + const ikConstraints = this.ikConstraints; + const transformConstraints = this.transformConstraints; + const pathConstraints = this.pathConstraints; + const ikCount = ikConstraints.length, + transformCount = transformConstraints.length, + pathCount = pathConstraints.length; + const constraintCount = ikCount + transformCount + pathCount; + + outer: for (let i = 0; i < constraintCount; i++) { + for (let ii = 0; ii < ikCount; ii++) { + const constraint = ikConstraints[ii]; + if (constraint.data.order == i) { + this.sortIkConstraint(constraint); + continue outer; + } + } + for (let ii = 0; ii < transformCount; ii++) { + const constraint = transformConstraints[ii]; + if (constraint.data.order == i) { + this.sortTransformConstraint(constraint); + continue outer; + } + } + for (let ii = 0; ii < pathCount; ii++) { + const constraint = pathConstraints[ii]; + if (constraint.data.order == i) { + this.sortPathConstraint(constraint); + continue outer; + } + } + } + + for (let i = 0, n = bones.length; i < n; i++) this.sortBone(bones[i]); + } + + sortIkConstraint(constraint: IkConstraint) { + constraint.active = + constraint.target.isActive() && + (!constraint.data.skinRequired || + (this.skin != null && Utils.contains(this.skin.constraints, constraint.data, true))); + if (!constraint.active) return; + + const target = constraint.target; + this.sortBone(target); + + const constrained = constraint.bones; + const parent = constrained[0]; + this.sortBone(parent); + + if (constrained.length > 1) { + const child = constrained[constrained.length - 1]; + if (!(this._updateCache.indexOf(child) > -1)) this.updateCacheReset.push(child); + } + + this._updateCache.push(constraint); + + this.sortReset(parent.children); + constrained[constrained.length - 1].sorted = true; + } + + sortPathConstraint(constraint: PathConstraint) { + constraint.active = + constraint.target.bone.isActive() && + (!constraint.data.skinRequired || + (this.skin != null && Utils.contains(this.skin.constraints, constraint.data, true))); + if (!constraint.active) return; + + const slot = constraint.target; + const slotIndex = slot.data.index; + const slotBone = slot.bone; + if (this.skin != null) this.sortPathConstraintAttachment(this.skin, slotIndex, slotBone); + if (this.data.defaultSkin != null && this.data.defaultSkin != this.skin) + this.sortPathConstraintAttachment(this.data.defaultSkin, slotIndex, slotBone); + for (let i = 0, n = this.data.skins.length; i < n; i++) + this.sortPathConstraintAttachment(this.data.skins[i], slotIndex, slotBone); + + const attachment = slot.getAttachment(); + if (attachment instanceof PathAttachment) this.sortPathConstraintAttachmentWith(attachment, slotBone); + + const constrained = constraint.bones; + const boneCount = constrained.length; + for (let i = 0; i < boneCount; i++) this.sortBone(constrained[i]); + + this._updateCache.push(constraint); + + for (let i = 0; i < boneCount; i++) this.sortReset(constrained[i].children); + for (let i = 0; i < boneCount; i++) constrained[i].sorted = true; + } + + sortTransformConstraint(constraint: TransformConstraint) { + constraint.active = + constraint.target.isActive() && + (!constraint.data.skinRequired || + (this.skin != null && Utils.contains(this.skin.constraints, constraint.data, true))); + if (!constraint.active) return; + + this.sortBone(constraint.target); + + const constrained = constraint.bones; + const boneCount = constrained.length; + if (constraint.data.local) { + for (let i = 0; i < boneCount; i++) { + const child = constrained[i]; + this.sortBone(child.parent); + if (!(this._updateCache.indexOf(child) > -1)) this.updateCacheReset.push(child); + } + } else { + for (let i = 0; i < boneCount; i++) { + this.sortBone(constrained[i]); + } + } + + this._updateCache.push(constraint); + + for (let ii = 0; ii < boneCount; ii++) this.sortReset(constrained[ii].children); + for (let ii = 0; ii < boneCount; ii++) constrained[ii].sorted = true; + } + + sortPathConstraintAttachment(skin: Skin, slotIndex: number, slotBone: Bone) { + const attachments = skin.attachments[slotIndex]; + if (!attachments) return; + for (const key in attachments) { + this.sortPathConstraintAttachmentWith(attachments[key], slotBone); + } + } + + sortPathConstraintAttachmentWith(attachment: Attachment, slotBone: Bone) { + if (!(attachment instanceof PathAttachment)) return; + const pathBones = (attachment).bones; + if (pathBones == null) this.sortBone(slotBone); + else { + const bones = this.bones; + let i = 0; + while (i < pathBones.length) { + const boneCount = pathBones[i++]; + for (let n = i + boneCount; i < n; i++) { + const boneIndex = pathBones[i]; + this.sortBone(bones[boneIndex]); + } + } + } + } + + sortBone(bone: Bone) { + if (bone.sorted) return; + const parent = bone.parent; + if (parent != null) this.sortBone(parent); + bone.sorted = true; + this._updateCache.push(bone); + } + + sortReset(bones: Array) { + for (let i = 0, n = bones.length; i < n; i++) { + const bone = bones[i]; + if (!bone.active) continue; + if (bone.sorted) this.sortReset(bone.children); + bone.sorted = false; + } + } + + /** Updates the world transform for each bone and applies all constraints. + * + * See [World transforms](http://esotericsoftware.com/spine-runtime-skeletons#World-transforms) in the Spine + * Runtimes Guide. */ + updateWorldTransform() { + const updateCacheReset = this.updateCacheReset; + for (let i = 0, n = updateCacheReset.length; i < n; i++) { + const bone = updateCacheReset[i] as Bone; + bone.ax = bone.x; + bone.ay = bone.y; + bone.arotation = bone.rotation; + bone.ascaleX = bone.scaleX; + bone.ascaleY = bone.scaleY; + bone.ashearX = bone.shearX; + bone.ashearY = bone.shearY; + bone.appliedValid = true; + } + const updateCache = this._updateCache; + for (let i = 0, n = updateCache.length; i < n; i++) updateCache[i].update(); + } + + /** Sets the bones, constraints, and slots to their setup pose values. */ + setToSetupPose() { + this.setBonesToSetupPose(); + this.setSlotsToSetupPose(); + } + + /** Sets the bones and constraints to their setup pose values. */ + setBonesToSetupPose() { + const bones = this.bones; + for (let i = 0, n = bones.length; i < n; i++) bones[i].setToSetupPose(); + + const ikConstraints = this.ikConstraints; + for (let i = 0, n = ikConstraints.length; i < n; i++) { + const constraint = ikConstraints[i]; + constraint.mix = constraint.data.mix; + constraint.softness = constraint.data.softness; + constraint.bendDirection = constraint.data.bendDirection; + constraint.compress = constraint.data.compress; + constraint.stretch = constraint.data.stretch; + } + + const transformConstraints = this.transformConstraints; + for (let i = 0, n = transformConstraints.length; i < n; i++) { + const constraint = transformConstraints[i]; + const data = constraint.data; + constraint.rotateMix = data.rotateMix; + constraint.translateMix = data.translateMix; + constraint.scaleMix = data.scaleMix; + constraint.shearMix = data.shearMix; + } + + const pathConstraints = this.pathConstraints; + for (let i = 0, n = pathConstraints.length; i < n; i++) { + const constraint = pathConstraints[i]; + const data = constraint.data; + constraint.position = data.position; + constraint.spacing = data.spacing; + constraint.rotateMix = data.rotateMix; + constraint.translateMix = data.translateMix; + } + } + + /** Sets the slots and draw order to their setup pose values. */ + setSlotsToSetupPose() { + const slots = this.slots; + Utils.arrayCopy(slots, 0, this.drawOrder, 0, slots.length); + for (let i = 0, n = slots.length; i < n; i++) slots[i].setToSetupPose(); + } + + /** @returns May return null. */ + getRootBone() { + if (this.bones.length == 0) return null; + return this.bones[0]; + } + + /** @returns May be null. */ + findBone(boneName: string) { + if (boneName == null) throw new Error("boneName cannot be null."); + const bones = this.bones; + for (let i = 0, n = bones.length; i < n; i++) { + const bone = bones[i]; + if (bone.data.name == boneName) return bone; + } + return null; + } + + /** @returns -1 if the bone was not found. */ + findBoneIndex(boneName: string) { + if (boneName == null) throw new Error("boneName cannot be null."); + const bones = this.bones; + for (let i = 0, n = bones.length; i < n; i++) if (bones[i].data.name == boneName) return i; + return -1; + } + + /** Finds a slot by comparing each slot's name. It is more efficient to cache the results of this method than to call it + * repeatedly. + * @returns May be null. */ + findSlot(slotName: string) { + if (slotName == null) throw new Error("slotName cannot be null."); + const slots = this.slots; + for (let i = 0, n = slots.length; i < n; i++) { + const slot = slots[i]; + if (slot.data.name == slotName) return slot; + } + return null; + } + + /** @returns -1 if the bone was not found. */ + findSlotIndex(slotName: string) { + if (slotName == null) throw new Error("slotName cannot be null."); + const slots = this.slots; + for (let i = 0, n = slots.length; i < n; i++) if (slots[i].data.name == slotName) return i; + return -1; + } + + /** Sets a skin by name. + * + * See {@link #setSkin()}. */ + setSkinByName(skinName: string) { + const skin = this.data.findSkin(skinName); + if (skin == null) throw new Error("Skin not found: " + skinName); + this.setSkin(skin); + } + + /** Sets the skin used to look up attachments before looking in the {@link SkeletonData#defaultSkin default skin}. If the + * skin is changed, {@link #updateCache()} is called. + * + * Attachments from the new skin are attached if the corresponding attachment from the old skin was attached. If there was no + * old skin, each slot's setup mode attachment is attached from the new skin. + * + * After changing the skin, the visible attachments can be reset to those attached in the setup pose by calling + * {@link #setSlotsToSetupPose()}. Also, often {@link AnimationState#apply()} is called before the next time the + * skeleton is rendered to allow any attachment keys in the current animation(s) to hide or show attachments from the new skin. + * @param newSkin May be null. */ + setSkin(newSkin: Skin) { + if (newSkin == this.skin) return; + if (newSkin != null) { + if (this.skin != null) newSkin.attachAll(this, this.skin); + else { + const slots = this.slots; + for (let i = 0, n = slots.length; i < n; i++) { + const slot = slots[i]; + const name = slot.data.attachmentName; + if (name != null) { + const attachment: Attachment = newSkin.getAttachment(i, name); + if (attachment != null) slot.setAttachment(attachment); + } + } + } + } + this.skin = newSkin; + this.updateCache(); + } + + /** Finds an attachment by looking in the {@link #skin} and {@link SkeletonData#defaultSkin} using the slot name and attachment + * name. + * + * See {@link #getAttachment()}. + * @returns May be null. */ + getAttachmentByName(slotName: string, attachmentName: string): Attachment { + return this.getAttachment(this.data.findSlotIndex(slotName), attachmentName); + } + + /** Finds an attachment by looking in the {@link #skin} and {@link SkeletonData#defaultSkin} using the slot index and + * attachment name. First the skin is checked and if the attachment was not found, the default skin is checked. + * + * See [Runtime skins](http://esotericsoftware.com/spine-runtime-skins) in the Spine Runtimes Guide. + * @returns May be null. */ + getAttachment(slotIndex: number, attachmentName: string): Attachment { + if (attachmentName == null) throw new Error("attachmentName cannot be null."); + if (this.skin != null) { + const attachment: Attachment = this.skin.getAttachment(slotIndex, attachmentName); + if (attachment != null) return attachment; + } + if (this.data.defaultSkin != null) return this.data.defaultSkin.getAttachment(slotIndex, attachmentName); + return null; + } + + /** A convenience method to set an attachment by finding the slot with {@link #findSlot()}, finding the attachment with + * {@link #getAttachment()}, then setting the slot's {@link Slot#attachment}. + * @param attachmentName May be null to clear the slot's attachment. */ + setAttachment(slotName: string, attachmentName: string) { + if (slotName == null) throw new Error("slotName cannot be null."); + const slots = this.slots; + for (let i = 0, n = slots.length; i < n; i++) { + const slot = slots[i]; + if (slot.data.name == slotName) { + let attachment: Attachment = null; + if (attachmentName != null) { + attachment = this.getAttachment(i, attachmentName); + if (attachment == null) + throw new Error("Attachment not found: " + attachmentName + ", for slot: " + slotName); + } + slot.setAttachment(attachment); + return; + } + } + throw new Error("Slot not found: " + slotName); + } + + /** Finds an IK constraint by comparing each IK constraint's name. It is more efficient to cache the results of this method + * than to call it repeatedly. + * @return May be null. */ + findIkConstraint(constraintName: string) { + if (constraintName == null) throw new Error("constraintName cannot be null."); + const ikConstraints = this.ikConstraints; + for (let i = 0, n = ikConstraints.length; i < n; i++) { + const ikConstraint = ikConstraints[i]; + if (ikConstraint.data.name == constraintName) return ikConstraint; + } + return null; + } + + /** Finds a transform constraint by comparing each transform constraint's name. It is more efficient to cache the results of + * this method than to call it repeatedly. + * @return May be null. */ + findTransformConstraint(constraintName: string) { + if (constraintName == null) throw new Error("constraintName cannot be null."); + const transformConstraints = this.transformConstraints; + for (let i = 0, n = transformConstraints.length; i < n; i++) { + const constraint = transformConstraints[i]; + if (constraint.data.name == constraintName) return constraint; + } + return null; + } + + /** Finds a path constraint by comparing each path constraint's name. It is more efficient to cache the results of this method + * than to call it repeatedly. + * @return May be null. */ + findPathConstraint(constraintName: string) { + if (constraintName == null) throw new Error("constraintName cannot be null."); + const pathConstraints = this.pathConstraints; + for (let i = 0, n = pathConstraints.length; i < n; i++) { + const constraint = pathConstraints[i]; + if (constraint.data.name == constraintName) return constraint; + } + return null; + } + + /** Returns the axis aligned bounding box (AABB) of the region and mesh attachments for the current pose. + * @param offset An output value, the distance from the skeleton origin to the bottom left corner of the AABB. + * @param size An output value, the width and height of the AABB. + * @param temp Working memory to temporarily store attachments' computed world vertices. */ + getBounds(offset: Vector2, size: Vector2, temp: Array = new Array(2)) { + if (offset == null) throw new Error("offset cannot be null."); + if (size == null) throw new Error("size cannot be null."); + const drawOrder = this.drawOrder; + let minX = Number.POSITIVE_INFINITY, + minY = Number.POSITIVE_INFINITY, + maxX = Number.NEGATIVE_INFINITY, + maxY = Number.NEGATIVE_INFINITY; + for (let i = 0, n = drawOrder.length; i < n; i++) { + const slot = drawOrder[i]; + if (!slot.bone.active) continue; + let verticesLength = 0; + let vertices: ArrayLike = null; + const attachment = slot.getAttachment(); + if (attachment instanceof RegionAttachment) { + verticesLength = 8; + vertices = Utils.setArraySize(temp, verticesLength, 0); + (attachment).computeWorldVertices(slot.bone, vertices, 0, 2); + } else if (attachment instanceof MeshAttachment) { + const mesh = attachment; + verticesLength = mesh.worldVerticesLength; + vertices = Utils.setArraySize(temp, verticesLength, 0); + mesh.computeWorldVertices(slot, 0, verticesLength, vertices, 0, 2); + } + if (vertices != null) { + for (let ii = 0, nn = vertices.length; ii < nn; ii += 2) { + const x = vertices[ii], + y = vertices[ii + 1]; + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x); + maxY = Math.max(maxY, y); + } + } + } + offset.set(minX, minY); + size.set(maxX - minX, maxY - minY); + } + + /** Increments the skeleton's {@link #time}. */ + update(delta: number) { + this.time += delta; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/SkeletonBinary.ts b/packages/spine-core-3.8/src/spine-core/SkeletonBinary.ts new file mode 100644 index 0000000000..86ec2f722c --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/SkeletonBinary.ts @@ -0,0 +1,942 @@ +import { TransformMode, BoneData } from "./BoneData"; +import { PositionMode, SpacingMode, RotateMode, PathConstraintData } from "./PathConstraintData"; +import { BlendMode } from "./BlendMode"; +import { AttachmentLoader } from "./attachments/AttachmentLoader"; +import { SkeletonData } from "./SkeletonData"; +import { Color, Utils } from "./Utils"; +import { SlotData } from "./SlotData"; +import { IkConstraintData } from "./IkConstraintData"; +import { TransformConstraintData } from "./TransformConstraintData"; +import { VertexAttachment, Attachment } from "./attachments/Attachment"; +import { MeshAttachment } from "./attachments/MeshAttachment"; +import { EventData } from "./EventData"; +import { Skin } from "./Skin"; +import { AttachmentType } from "./attachments/AttachmentType"; +import { + Timeline, + AttachmentTimeline, + ColorTimeline, + TwoColorTimeline, + RotateTimeline, + ScaleTimeline, + ShearTimeline, + TranslateTimeline, + IkConstraintTimeline, + TransformConstraintTimeline, + PathConstraintSpacingTimeline, + PathConstraintPositionTimeline, + PathConstraintMixTimeline, + DeformTimeline, + DrawOrderTimeline, + EventTimeline, + CurveTimeline +} from "./Animation"; +import { Animation } from "./Animation"; +import { Event } from "./Event"; + +/** Loads skeleton data in the Spine binary format. + * + * See [Spine binary format](http://esotericsoftware.com/spine-binary-format) and + * [JSON and binary data](http://esotericsoftware.com/spine-loading-skeleton-data#JSON-and-binary-data) in the Spine + * Runtimes Guide. */ +export class SkeletonBinary { + static AttachmentTypeValues = [ + 0 /*AttachmentType.Region*/, 1 /*AttachmentType.BoundingBox*/, 2 /*AttachmentType.Mesh*/, + 3 /*AttachmentType.LinkedMesh*/, 4 /*AttachmentType.Path*/, 5 /*AttachmentType.Point*/, + 6 /*AttachmentType.Clipping*/ + ]; + static TransformModeValues = [ + TransformMode.Normal, + TransformMode.OnlyTranslation, + TransformMode.NoRotationOrReflection, + TransformMode.NoScale, + TransformMode.NoScaleOrReflection + ]; + static PositionModeValues = [PositionMode.Fixed, PositionMode.Percent]; + static SpacingModeValues = [SpacingMode.Length, SpacingMode.Fixed, SpacingMode.Percent]; + static RotateModeValues = [RotateMode.Tangent, RotateMode.Chain, RotateMode.ChainScale]; + static BlendModeValues = [BlendMode.Normal, BlendMode.Additive, BlendMode.Multiply, BlendMode.Screen]; + + static BONE_ROTATE = 0; + static BONE_TRANSLATE = 1; + static BONE_SCALE = 2; + static BONE_SHEAR = 3; + + static SLOT_ATTACHMENT = 0; + static SLOT_COLOR = 1; + static SLOT_TWO_COLOR = 2; + + static PATH_POSITION = 0; + static PATH_SPACING = 1; + static PATH_MIX = 2; + + static CURVE_LINEAR = 0; + static CURVE_STEPPED = 1; + static CURVE_BEZIER = 2; + + /** Scales bone positions, image sizes, and translations as they are loaded. This allows different size images to be used at + * runtime than were used in Spine. + * + * See [Scaling](http://esotericsoftware.com/spine-loading-skeleton-data#Scaling) in the Spine Runtimes Guide. */ + scale = 1; + + attachmentLoader: AttachmentLoader; + private linkedMeshes = new Array(); + + constructor(attachmentLoader: AttachmentLoader) { + this.attachmentLoader = attachmentLoader; + } + + readSkeletonData(binary: Uint8Array): SkeletonData { + const scale = this.scale; + + const skeletonData = new SkeletonData(); + skeletonData.name = ""; // BOZO + + const input = new BinaryInput(binary); + + skeletonData.hash = input.readString(); + skeletonData.version = input.readString(); + if ("3.8.75" == skeletonData.version) + throw new Error("Unsupported skeleton data, please export with a newer version of Spine."); + skeletonData.x = input.readFloat(); + skeletonData.y = input.readFloat(); + skeletonData.width = input.readFloat(); + skeletonData.height = input.readFloat(); + + const nonessential = input.readBoolean(); + if (nonessential) { + skeletonData.fps = input.readFloat(); + + skeletonData.imagesPath = input.readString(); + skeletonData.audioPath = input.readString(); + } + + let n = 0; + // Strings. + n = input.readInt(true); + for (let i = 0; i < n; i++) input.strings.push(input.readString()); + + // Bones. + n = input.readInt(true); + for (let i = 0; i < n; i++) { + const name = input.readString(); + const parent = i == 0 ? null : skeletonData.bones[input.readInt(true)]; + const data = new BoneData(i, name, parent); + data.rotation = input.readFloat(); + data.x = input.readFloat() * scale; + data.y = input.readFloat() * scale; + data.scaleX = input.readFloat(); + data.scaleY = input.readFloat(); + data.shearX = input.readFloat(); + data.shearY = input.readFloat(); + data.length = input.readFloat() * scale; + data.transformMode = SkeletonBinary.TransformModeValues[input.readInt(true)]; + data.skinRequired = input.readBoolean(); + if (nonessential) Color.rgba8888ToColor(data.color, input.readInt32()); + skeletonData.bones.push(data); + } + + // Slots. + n = input.readInt(true); + for (let i = 0; i < n; i++) { + const slotName = input.readString(); + const boneData = skeletonData.bones[input.readInt(true)]; + const data = new SlotData(i, slotName, boneData); + Color.rgba8888ToColor(data.color, input.readInt32()); + + const darkColor = input.readInt32(); + if (darkColor != -1) Color.rgb888ToColor((data.darkColor = new Color()), darkColor); + + data.attachmentName = input.readStringRef(); + data.blendMode = SkeletonBinary.BlendModeValues[input.readInt(true)]; + skeletonData.slots.push(data); + } + + // IK constraints. + n = input.readInt(true); + for (let i = 0, nn; i < n; i++) { + const data = new IkConstraintData(input.readString()); + data.order = input.readInt(true); + data.skinRequired = input.readBoolean(); + nn = input.readInt(true); + for (let ii = 0; ii < nn; ii++) data.bones.push(skeletonData.bones[input.readInt(true)]); + data.target = skeletonData.bones[input.readInt(true)]; + data.mix = input.readFloat(); + data.softness = input.readFloat() * scale; + data.bendDirection = input.readByte(); + data.compress = input.readBoolean(); + data.stretch = input.readBoolean(); + data.uniform = input.readBoolean(); + skeletonData.ikConstraints.push(data); + } + + // Transform constraints. + n = input.readInt(true); + for (let i = 0, nn; i < n; i++) { + const data = new TransformConstraintData(input.readString()); + data.order = input.readInt(true); + data.skinRequired = input.readBoolean(); + nn = input.readInt(true); + for (let ii = 0; ii < nn; ii++) data.bones.push(skeletonData.bones[input.readInt(true)]); + data.target = skeletonData.bones[input.readInt(true)]; + data.local = input.readBoolean(); + data.relative = input.readBoolean(); + data.offsetRotation = input.readFloat(); + data.offsetX = input.readFloat() * scale; + data.offsetY = input.readFloat() * scale; + data.offsetScaleX = input.readFloat(); + data.offsetScaleY = input.readFloat(); + data.offsetShearY = input.readFloat(); + data.rotateMix = input.readFloat(); + data.translateMix = input.readFloat(); + data.scaleMix = input.readFloat(); + data.shearMix = input.readFloat(); + skeletonData.transformConstraints.push(data); + } + + // Path constraints. + n = input.readInt(true); + for (let i = 0, nn; i < n; i++) { + const data = new PathConstraintData(input.readString()); + data.order = input.readInt(true); + data.skinRequired = input.readBoolean(); + nn = input.readInt(true); + for (let ii = 0; ii < nn; ii++) data.bones.push(skeletonData.bones[input.readInt(true)]); + data.target = skeletonData.slots[input.readInt(true)]; + data.positionMode = SkeletonBinary.PositionModeValues[input.readInt(true)]; + data.spacingMode = SkeletonBinary.SpacingModeValues[input.readInt(true)]; + data.rotateMode = SkeletonBinary.RotateModeValues[input.readInt(true)]; + data.offsetRotation = input.readFloat(); + data.position = input.readFloat(); + if (data.positionMode == PositionMode.Fixed) data.position *= scale; + data.spacing = input.readFloat(); + if (data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed) data.spacing *= scale; + data.rotateMix = input.readFloat(); + data.translateMix = input.readFloat(); + skeletonData.pathConstraints.push(data); + } + + // Default skin. + const defaultSkin = this.readSkin(input, skeletonData, true, nonessential); + if (defaultSkin != null) { + skeletonData.defaultSkin = defaultSkin; + skeletonData.skins.push(defaultSkin); + } + + // Skins. + { + let i = skeletonData.skins.length; + Utils.setArraySize(skeletonData.skins, (n = i + input.readInt(true))); + for (; i < n; i++) skeletonData.skins[i] = this.readSkin(input, skeletonData, false, nonessential); + } + + // Linked meshes. + n = this.linkedMeshes.length; + for (let i = 0; i < n; i++) { + const linkedMesh = this.linkedMeshes[i]; + const skin = linkedMesh.skin == null ? skeletonData.defaultSkin : skeletonData.findSkin(linkedMesh.skin); + if (skin == null) throw new Error("Skin not found: " + linkedMesh.skin); + const parent = skin.getAttachment(linkedMesh.slotIndex, linkedMesh.parent); + if (parent == null) throw new Error("Parent mesh not found: " + linkedMesh.parent); + linkedMesh.mesh.deformAttachment = linkedMesh.inheritDeform ? (parent as VertexAttachment) : linkedMesh.mesh; + linkedMesh.mesh.setParentMesh(parent as MeshAttachment); + linkedMesh.mesh.updateUVs(); + } + this.linkedMeshes.length = 0; + + // Events. + n = input.readInt(true); + for (let i = 0; i < n; i++) { + const data = new EventData(input.readStringRef()); + data.intValue = input.readInt(false); + data.floatValue = input.readFloat(); + data.stringValue = input.readString(); + data.audioPath = input.readString(); + if (data.audioPath != null) { + data.volume = input.readFloat(); + data.balance = input.readFloat(); + } + skeletonData.events.push(data); + } + + // Animations. + n = input.readInt(true); + for (let i = 0; i < n; i++) + skeletonData.animations.push(this.readAnimation(input, input.readString(), skeletonData)); + return skeletonData; + } + + private readSkin(input: BinaryInput, skeletonData: SkeletonData, defaultSkin: boolean, nonessential: boolean): Skin { + let skin = null; + let slotCount = 0; + + if (defaultSkin) { + slotCount = input.readInt(true); + if (slotCount == 0) return null; + skin = new Skin("default"); + } else { + skin = new Skin(input.readStringRef()); + skin.bones.length = input.readInt(true); + for (let i = 0, n = skin.bones.length; i < n; i++) skin.bones[i] = skeletonData.bones[input.readInt(true)]; + + for (let i = 0, n = input.readInt(true); i < n; i++) + skin.constraints.push(skeletonData.ikConstraints[input.readInt(true)]); + for (let i = 0, n = input.readInt(true); i < n; i++) + skin.constraints.push(skeletonData.transformConstraints[input.readInt(true)]); + for (let i = 0, n = input.readInt(true); i < n; i++) + skin.constraints.push(skeletonData.pathConstraints[input.readInt(true)]); + + slotCount = input.readInt(true); + } + + for (let i = 0; i < slotCount; i++) { + const slotIndex = input.readInt(true); + for (let ii = 0, nn = input.readInt(true); ii < nn; ii++) { + const name = input.readStringRef(); + const attachment = this.readAttachment(input, skeletonData, skin, slotIndex, name, nonessential); + if (attachment != null) skin.setAttachment(slotIndex, name, attachment); + } + } + return skin; + } + + private readAttachment( + input: BinaryInput, + skeletonData: SkeletonData, + skin: Skin, + slotIndex: number, + attachmentName: string, + nonessential: boolean + ): Attachment { + const scale = this.scale; + + let name = input.readStringRef(); + if (name == null) name = attachmentName; + + const typeIndex = input.readByte(); + const type = SkeletonBinary.AttachmentTypeValues[typeIndex]; + switch (type) { + case AttachmentType.Region: { + let path = input.readStringRef(); + const rotation = input.readFloat(); + const x = input.readFloat(); + const y = input.readFloat(); + const scaleX = input.readFloat(); + const scaleY = input.readFloat(); + const width = input.readFloat(); + const height = input.readFloat(); + const color = input.readInt32(); + + if (path == null) path = name; + const region = this.attachmentLoader.newRegionAttachment(skin, name, path); + if (region == null) return null; + region.path = path; + region.x = x * scale; + region.y = y * scale; + region.scaleX = scaleX; + region.scaleY = scaleY; + region.rotation = rotation; + region.width = width * scale; + region.height = height * scale; + Color.rgba8888ToColor(region.color, color); + region.updateOffset(); + return region; + } + case AttachmentType.BoundingBox: { + const vertexCount = input.readInt(true); + const vertices = this.readVertices(input, vertexCount); + const color = nonessential ? input.readInt32() : 0; + + const box = this.attachmentLoader.newBoundingBoxAttachment(skin, name); + if (box == null) return null; + box.worldVerticesLength = vertexCount << 1; + box.vertices = vertices.vertices; + box.bones = vertices.bones; + if (nonessential) Color.rgba8888ToColor(box.color, color); + return box; + } + case AttachmentType.Mesh: { + let path = input.readStringRef(); + const color = input.readInt32(); + const vertexCount = input.readInt(true); + const uvs = this.readFloatArray(input, vertexCount << 1, 1); + const triangles = this.readShortArray(input); + const vertices = this.readVertices(input, vertexCount); + const hullLength = input.readInt(true); + let edges = null; + let width = 0, + height = 0; + if (nonessential) { + edges = this.readShortArray(input); + width = input.readFloat(); + height = input.readFloat(); + } + + if (path == null) path = name; + const mesh = this.attachmentLoader.newMeshAttachment(skin, name, path); + if (mesh == null) return null; + mesh.path = path; + Color.rgba8888ToColor(mesh.color, color); + mesh.bones = vertices.bones; + mesh.vertices = vertices.vertices; + mesh.worldVerticesLength = vertexCount << 1; + mesh.triangles = triangles; + mesh.regionUVs = uvs; + mesh.updateUVs(); + mesh.hullLength = hullLength << 1; + if (nonessential) { + mesh.edges = edges; + mesh.width = width * scale; + mesh.height = height * scale; + } + return mesh; + } + case AttachmentType.LinkedMesh: { + let path = input.readStringRef(); + const color = input.readInt32(); + const skinName = input.readStringRef(); + const parent = input.readStringRef(); + const inheritDeform = input.readBoolean(); + let width = 0, + height = 0; + if (nonessential) { + width = input.readFloat(); + height = input.readFloat(); + } + + if (path == null) path = name; + const mesh = this.attachmentLoader.newMeshAttachment(skin, name, path); + if (mesh == null) return null; + mesh.path = path; + Color.rgba8888ToColor(mesh.color, color); + if (nonessential) { + mesh.width = width * scale; + mesh.height = height * scale; + } + this.linkedMeshes.push(new LinkedMesh(mesh, skinName, slotIndex, parent, inheritDeform)); + return mesh; + } + case AttachmentType.Path: { + const closed = input.readBoolean(); + const constantSpeed = input.readBoolean(); + const vertexCount = input.readInt(true); + const vertices = this.readVertices(input, vertexCount); + const lengths = Utils.newArray(vertexCount / 3, 0); + for (let i = 0, n = lengths.length; i < n; i++) lengths[i] = input.readFloat() * scale; + const color = nonessential ? input.readInt32() : 0; + + const path = this.attachmentLoader.newPathAttachment(skin, name); + if (path == null) return null; + path.closed = closed; + path.constantSpeed = constantSpeed; + path.worldVerticesLength = vertexCount << 1; + path.vertices = vertices.vertices; + path.bones = vertices.bones; + path.lengths = lengths; + if (nonessential) Color.rgba8888ToColor(path.color, color); + return path; + } + case AttachmentType.Point: { + const rotation = input.readFloat(); + const x = input.readFloat(); + const y = input.readFloat(); + const color = nonessential ? input.readInt32() : 0; + + const point = this.attachmentLoader.newPointAttachment(skin, name); + if (point == null) return null; + point.x = x * scale; + point.y = y * scale; + point.rotation = rotation; + if (nonessential) Color.rgba8888ToColor(point.color, color); + return point; + } + case AttachmentType.Clipping: { + const endSlotIndex = input.readInt(true); + const vertexCount = input.readInt(true); + const vertices = this.readVertices(input, vertexCount); + const color = nonessential ? input.readInt32() : 0; + + const clip = this.attachmentLoader.newClippingAttachment(skin, name); + if (clip == null) return null; + clip.endSlot = skeletonData.slots[endSlotIndex]; + clip.worldVerticesLength = vertexCount << 1; + clip.vertices = vertices.vertices; + clip.bones = vertices.bones; + if (nonessential) Color.rgba8888ToColor(clip.color, color); + return clip; + } + } + return null; + } + + private readVertices(input: BinaryInput, vertexCount: number): Vertices { + const verticesLength = vertexCount << 1; + const vertices = new Vertices(); + const scale = this.scale; + if (!input.readBoolean()) { + vertices.vertices = this.readFloatArray(input, verticesLength, scale); + return vertices; + } + const weights = new Array(); + const bonesArray = new Array(); + for (let i = 0; i < vertexCount; i++) { + const boneCount = input.readInt(true); + bonesArray.push(boneCount); + for (let ii = 0; ii < boneCount; ii++) { + bonesArray.push(input.readInt(true)); + weights.push(input.readFloat() * scale); + weights.push(input.readFloat() * scale); + weights.push(input.readFloat()); + } + } + vertices.vertices = Utils.toFloatArray(weights); + vertices.bones = bonesArray; + return vertices; + } + + private readFloatArray(input: BinaryInput, n: number, scale: number): number[] { + const array = new Array(n); + if (scale == 1) { + for (let i = 0; i < n; i++) array[i] = input.readFloat(); + } else { + for (let i = 0; i < n; i++) array[i] = input.readFloat() * scale; + } + return array; + } + + private readShortArray(input: BinaryInput): number[] { + const n = input.readInt(true); + const array = new Array(n); + for (let i = 0; i < n; i++) array[i] = input.readShort(); + return array; + } + + private readAnimation(input: BinaryInput, name: string, skeletonData: SkeletonData): Animation { + const timelines = new Array(); + const scale = this.scale; + let duration = 0; + const tempColor1 = new Color(); + const tempColor2 = new Color(); + + // Slot timelines. + for (let i = 0, n = input.readInt(true); i < n; i++) { + const slotIndex = input.readInt(true); + for (let ii = 0, nn = input.readInt(true); ii < nn; ii++) { + const timelineType = input.readByte(); + const frameCount = input.readInt(true); + switch (timelineType) { + case SkeletonBinary.SLOT_ATTACHMENT: { + const timeline = new AttachmentTimeline(frameCount); + timeline.slotIndex = slotIndex; + for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) + timeline.setFrame(frameIndex, input.readFloat(), input.readStringRef()); + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[frameCount - 1]); + break; + } + case SkeletonBinary.SLOT_COLOR: { + const timeline = new ColorTimeline(frameCount); + timeline.slotIndex = slotIndex; + for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) { + const time = input.readFloat(); + Color.rgba8888ToColor(tempColor1, input.readInt32()); + timeline.setFrame(frameIndex, time, tempColor1.r, tempColor1.g, tempColor1.b, tempColor1.a); + if (frameIndex < frameCount - 1) this.readCurve(input, frameIndex, timeline); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[(frameCount - 1) * ColorTimeline.ENTRIES]); + break; + } + case SkeletonBinary.SLOT_TWO_COLOR: { + const timeline = new TwoColorTimeline(frameCount); + timeline.slotIndex = slotIndex; + for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) { + const time = input.readFloat(); + Color.rgba8888ToColor(tempColor1, input.readInt32()); + Color.rgb888ToColor(tempColor2, input.readInt32()); + timeline.setFrame( + frameIndex, + time, + tempColor1.r, + tempColor1.g, + tempColor1.b, + tempColor1.a, + tempColor2.r, + tempColor2.g, + tempColor2.b + ); + if (frameIndex < frameCount - 1) this.readCurve(input, frameIndex, timeline); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[(frameCount - 1) * TwoColorTimeline.ENTRIES]); + break; + } + } + } + } + + // Bone timelines. + for (let i = 0, n = input.readInt(true); i < n; i++) { + const boneIndex = input.readInt(true); + for (let ii = 0, nn = input.readInt(true); ii < nn; ii++) { + const timelineType = input.readByte(); + const frameCount = input.readInt(true); + switch (timelineType) { + case SkeletonBinary.BONE_ROTATE: { + const timeline = new RotateTimeline(frameCount); + timeline.boneIndex = boneIndex; + for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) { + timeline.setFrame(frameIndex, input.readFloat(), input.readFloat()); + if (frameIndex < frameCount - 1) this.readCurve(input, frameIndex, timeline); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[(frameCount - 1) * RotateTimeline.ENTRIES]); + break; + } + case SkeletonBinary.BONE_TRANSLATE: + case SkeletonBinary.BONE_SCALE: + case SkeletonBinary.BONE_SHEAR: { + let timeline; + let timelineScale = 1; + if (timelineType == SkeletonBinary.BONE_SCALE) timeline = new ScaleTimeline(frameCount); + else if (timelineType == SkeletonBinary.BONE_SHEAR) timeline = new ShearTimeline(frameCount); + else { + timeline = new TranslateTimeline(frameCount); + timelineScale = scale; + } + timeline.boneIndex = boneIndex; + for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) { + timeline.setFrame( + frameIndex, + input.readFloat(), + input.readFloat() * timelineScale, + input.readFloat() * timelineScale + ); + if (frameIndex < frameCount - 1) this.readCurve(input, frameIndex, timeline); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[(frameCount - 1) * TranslateTimeline.ENTRIES]); + break; + } + } + } + } + + // IK constraint timelines. + for (let i = 0, n = input.readInt(true); i < n; i++) { + const index = input.readInt(true); + const frameCount = input.readInt(true); + const timeline = new IkConstraintTimeline(frameCount); + timeline.ikConstraintIndex = index; + for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) { + timeline.setFrame( + frameIndex, + input.readFloat(), + input.readFloat(), + input.readFloat() * scale, + input.readByte(), + input.readBoolean(), + input.readBoolean() + ); + if (frameIndex < frameCount - 1) this.readCurve(input, frameIndex, timeline); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[(frameCount - 1) * IkConstraintTimeline.ENTRIES]); + } + + // Transform constraint timelines. + for (let i = 0, n = input.readInt(true); i < n; i++) { + const index = input.readInt(true); + const frameCount = input.readInt(true); + const timeline = new TransformConstraintTimeline(frameCount); + timeline.transformConstraintIndex = index; + for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) { + timeline.setFrame( + frameIndex, + input.readFloat(), + input.readFloat(), + input.readFloat(), + input.readFloat(), + input.readFloat() + ); + if (frameIndex < frameCount - 1) this.readCurve(input, frameIndex, timeline); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[(frameCount - 1) * TransformConstraintTimeline.ENTRIES]); + } + + // Path constraint timelines. + for (let i = 0, n = input.readInt(true); i < n; i++) { + const index = input.readInt(true); + const data = skeletonData.pathConstraints[index]; + for (let ii = 0, nn = input.readInt(true); ii < nn; ii++) { + const timelineType = input.readByte(); + const frameCount = input.readInt(true); + switch (timelineType) { + case SkeletonBinary.PATH_POSITION: + case SkeletonBinary.PATH_SPACING: { + let timeline; + let timelineScale = 1; + if (timelineType == SkeletonBinary.PATH_SPACING) { + timeline = new PathConstraintSpacingTimeline(frameCount); + if (data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed) + timelineScale = scale; + } else { + timeline = new PathConstraintPositionTimeline(frameCount); + if (data.positionMode == PositionMode.Fixed) timelineScale = scale; + } + timeline.pathConstraintIndex = index; + for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) { + timeline.setFrame(frameIndex, input.readFloat(), input.readFloat() * timelineScale); + if (frameIndex < frameCount - 1) this.readCurve(input, frameIndex, timeline); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[(frameCount - 1) * PathConstraintPositionTimeline.ENTRIES]); + break; + } + case SkeletonBinary.PATH_MIX: { + const timeline = new PathConstraintMixTimeline(frameCount); + timeline.pathConstraintIndex = index; + for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) { + timeline.setFrame(frameIndex, input.readFloat(), input.readFloat(), input.readFloat()); + if (frameIndex < frameCount - 1) this.readCurve(input, frameIndex, timeline); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[(frameCount - 1) * PathConstraintMixTimeline.ENTRIES]); + break; + } + } + } + } + + // Deform timelines. + for (let i = 0, n = input.readInt(true); i < n; i++) { + const skin = skeletonData.skins[input.readInt(true)]; + for (let ii = 0, nn = input.readInt(true); ii < nn; ii++) { + const slotIndex = input.readInt(true); + for (let iii = 0, nnn = input.readInt(true); iii < nnn; iii++) { + const attachment = skin.getAttachment(slotIndex, input.readStringRef()) as VertexAttachment; + const weighted = attachment.bones != null; + const vertices = attachment.vertices; + const deformLength = weighted ? (vertices.length / 3) * 2 : vertices.length; + + const frameCount = input.readInt(true); + const timeline = new DeformTimeline(frameCount); + timeline.slotIndex = slotIndex; + timeline.attachment = attachment; + + for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) { + const time = input.readFloat(); + let deform; + let end = input.readInt(true); + if (end == 0) deform = weighted ? Utils.newFloatArray(deformLength) : vertices; + else { + deform = Utils.newFloatArray(deformLength); + const start = input.readInt(true); + end += start; + if (scale == 1) { + for (let v = start; v < end; v++) deform[v] = input.readFloat(); + } else { + for (let v = start; v < end; v++) deform[v] = input.readFloat() * scale; + } + if (!weighted) { + for (let v = 0, vn = deform.length; v < vn; v++) deform[v] += vertices[v]; + } + } + + timeline.setFrame(frameIndex, time, deform); + if (frameIndex < frameCount - 1) this.readCurve(input, frameIndex, timeline); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[frameCount - 1]); + } + } + } + + // Draw order timeline. + const drawOrderCount = input.readInt(true); + if (drawOrderCount > 0) { + const timeline = new DrawOrderTimeline(drawOrderCount); + const slotCount = skeletonData.slots.length; + for (let i = 0; i < drawOrderCount; i++) { + const time = input.readFloat(); + const offsetCount = input.readInt(true); + const drawOrder = Utils.newArray(slotCount, 0); + for (let ii = slotCount - 1; ii >= 0; ii--) drawOrder[ii] = -1; + const unchanged = Utils.newArray(slotCount - offsetCount, 0); + let originalIndex = 0, + unchangedIndex = 0; + for (let ii = 0; ii < offsetCount; ii++) { + const slotIndex = input.readInt(true); + // Collect unchanged items. + while (originalIndex != slotIndex) unchanged[unchangedIndex++] = originalIndex++; + // Set changed items. + drawOrder[originalIndex + input.readInt(true)] = originalIndex++; + } + // Collect remaining unchanged items. + while (originalIndex < slotCount) unchanged[unchangedIndex++] = originalIndex++; + // Fill in unchanged items. + for (let ii = slotCount - 1; ii >= 0; ii--) + if (drawOrder[ii] == -1) drawOrder[ii] = unchanged[--unchangedIndex]; + timeline.setFrame(i, time, drawOrder); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[drawOrderCount - 1]); + } + + // Event timeline. + const eventCount = input.readInt(true); + if (eventCount > 0) { + const timeline = new EventTimeline(eventCount); + for (let i = 0; i < eventCount; i++) { + const time = input.readFloat(); + const eventData = skeletonData.events[input.readInt(true)]; + const event = new Event(time, eventData); + event.intValue = input.readInt(false); + event.floatValue = input.readFloat(); + event.stringValue = input.readBoolean() ? input.readString() : eventData.stringValue; + if (event.data.audioPath != null) { + event.volume = input.readFloat(); + event.balance = input.readFloat(); + } + timeline.setFrame(i, event); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[eventCount - 1]); + } + + return new Animation(name, timelines, duration); + } + + private readCurve(input: BinaryInput, frameIndex: number, timeline: CurveTimeline) { + switch (input.readByte()) { + case SkeletonBinary.CURVE_STEPPED: + timeline.setStepped(frameIndex); + break; + case SkeletonBinary.CURVE_BEZIER: + this.setCurve(timeline, frameIndex, input.readFloat(), input.readFloat(), input.readFloat(), input.readFloat()); + break; + } + } + + setCurve(timeline: CurveTimeline, frameIndex: number, cx1: number, cy1: number, cx2: number, cy2: number) { + timeline.setCurve(frameIndex, cx1, cy1, cx2, cy2); + } +} + +class BinaryInput { + constructor( + data: Uint8Array, + public strings = new Array(), + private index: number = 0, + private buffer = new DataView(data.buffer) + ) {} + + readByte(): number { + return this.buffer.getInt8(this.index++); + } + + readShort(): number { + const value = this.buffer.getInt16(this.index); + this.index += 2; + return value; + } + + readInt32(): number { + const value = this.buffer.getInt32(this.index); + this.index += 4; + return value; + } + + readInt(optimizePositive: boolean) { + let b = this.readByte(); + let result = b & 0x7f; + if ((b & 0x80) != 0) { + b = this.readByte(); + result |= (b & 0x7f) << 7; + if ((b & 0x80) != 0) { + b = this.readByte(); + result |= (b & 0x7f) << 14; + if ((b & 0x80) != 0) { + b = this.readByte(); + result |= (b & 0x7f) << 21; + if ((b & 0x80) != 0) { + b = this.readByte(); + result |= (b & 0x7f) << 28; + } + } + } + } + return optimizePositive ? result : (result >>> 1) ^ -(result & 1); + } + + readStringRef(): string { + const index = this.readInt(true); + return index == 0 ? null : this.strings[index - 1]; + } + + readString(): string { + let byteCount = this.readInt(true); + switch (byteCount) { + case 0: + return null; + case 1: + return ""; + } + byteCount--; + let chars = ""; + const charCount = 0; + for (let i = 0; i < byteCount; ) { + const b = this.readByte(); + switch (b >> 4) { + case 12: + case 13: + chars += String.fromCharCode(((b & 0x1f) << 6) | (this.readByte() & 0x3f)); + i += 2; + break; + case 14: + chars += String.fromCharCode(((b & 0x0f) << 12) | ((this.readByte() & 0x3f) << 6) | (this.readByte() & 0x3f)); + i += 3; + break; + default: + chars += String.fromCharCode(b); + i++; + } + } + return chars; + } + + readFloat(): number { + const value = this.buffer.getFloat32(this.index); + this.index += 4; + return value; + } + + readBoolean(): boolean { + return this.readByte() != 0; + } +} + +class LinkedMesh { + parent: string; + skin: string; + slotIndex: number; + mesh: MeshAttachment; + inheritDeform: boolean; + + constructor(mesh: MeshAttachment, skin: string, slotIndex: number, parent: string, inheritDeform: boolean) { + this.mesh = mesh; + this.skin = skin; + this.slotIndex = slotIndex; + this.parent = parent; + this.inheritDeform = inheritDeform; + } +} + +class Vertices { + constructor( + public bones: Array = null, + public vertices: Array | Float32Array = null + ) {} +} diff --git a/packages/spine-core-3.8/src/spine-core/SkeletonBounds.ts b/packages/spine-core-3.8/src/spine-core/SkeletonBounds.ts new file mode 100644 index 0000000000..6c7323ba77 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/SkeletonBounds.ts @@ -0,0 +1,215 @@ +import { BoundingBoxAttachment } from "./attachments/BoundingBoxAttachment"; +import { Pool, Utils } from "./Utils"; +import { Skeleton } from "./Skeleton"; + +/** Collects each visible {@link BoundingBoxAttachment} and computes the world vertices for its polygon. The polygon vertices are + * provided along with convenience methods for doing hit detection. */ +export class SkeletonBounds { + /** The left edge of the axis aligned bounding box. */ + minX = 0; + + /** The bottom edge of the axis aligned bounding box. */ + minY = 0; + + /** The right edge of the axis aligned bounding box. */ + maxX = 0; + + /** The top edge of the axis aligned bounding box. */ + maxY = 0; + + /** The visible bounding boxes. */ + boundingBoxes = new Array(); + + /** The world vertices for the bounding box polygons. */ + polygons = new Array>(); + + private polygonPool = new Pool>(() => { + return Utils.newFloatArray(16); + }); + + /** Clears any previous polygons, finds all visible bounding box attachments, and computes the world vertices for each bounding + * box's polygon. + * @param updateAabb If true, the axis aligned bounding box containing all the polygons is computed. If false, the + * SkeletonBounds AABB methods will always return true. */ + update(skeleton: Skeleton, updateAabb: boolean) { + if (skeleton == null) throw new Error("skeleton cannot be null."); + const boundingBoxes = this.boundingBoxes; + const polygons = this.polygons; + const polygonPool = this.polygonPool; + const slots = skeleton.slots; + const slotCount = slots.length; + + boundingBoxes.length = 0; + polygonPool.freeAll(polygons); + polygons.length = 0; + + for (let i = 0; i < slotCount; i++) { + const slot = slots[i]; + if (!slot.bone.active) continue; + const attachment = slot.getAttachment(); + if (attachment instanceof BoundingBoxAttachment) { + const boundingBox = attachment as BoundingBoxAttachment; + boundingBoxes.push(boundingBox); + + let polygon = polygonPool.obtain(); + if (polygon.length != boundingBox.worldVerticesLength) { + polygon = Utils.newFloatArray(boundingBox.worldVerticesLength); + } + polygons.push(polygon); + boundingBox.computeWorldVertices(slot, 0, boundingBox.worldVerticesLength, polygon, 0, 2); + } + } + + if (updateAabb) { + this.aabbCompute(); + } else { + this.minX = Number.POSITIVE_INFINITY; + this.minY = Number.POSITIVE_INFINITY; + this.maxX = Number.NEGATIVE_INFINITY; + this.maxY = Number.NEGATIVE_INFINITY; + } + } + + aabbCompute() { + let minX = Number.POSITIVE_INFINITY, + minY = Number.POSITIVE_INFINITY, + maxX = Number.NEGATIVE_INFINITY, + maxY = Number.NEGATIVE_INFINITY; + const polygons = this.polygons; + for (let i = 0, n = polygons.length; i < n; i++) { + const polygon = polygons[i]; + const vertices = polygon; + for (let ii = 0, nn = polygon.length; ii < nn; ii += 2) { + const x = vertices[ii]; + const y = vertices[ii + 1]; + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x); + maxY = Math.max(maxY, y); + } + } + this.minX = minX; + this.minY = minY; + this.maxX = maxX; + this.maxY = maxY; + } + + /** Returns true if the axis aligned bounding box contains the point. */ + aabbContainsPoint(x: number, y: number) { + return x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY; + } + + /** Returns true if the axis aligned bounding box intersects the line segment. */ + aabbIntersectsSegment(x1: number, y1: number, x2: number, y2: number) { + const minX = this.minX; + const minY = this.minY; + const maxX = this.maxX; + const maxY = this.maxY; + if ( + (x1 <= minX && x2 <= minX) || + (y1 <= minY && y2 <= minY) || + (x1 >= maxX && x2 >= maxX) || + (y1 >= maxY && y2 >= maxY) + ) + return false; + const m = (y2 - y1) / (x2 - x1); + let y = m * (minX - x1) + y1; + if (y > minY && y < maxY) return true; + y = m * (maxX - x1) + y1; + if (y > minY && y < maxY) return true; + let x = (minY - y1) / m + x1; + if (x > minX && x < maxX) return true; + x = (maxY - y1) / m + x1; + if (x > minX && x < maxX) return true; + return false; + } + + /** Returns true if the axis aligned bounding box intersects the axis aligned bounding box of the specified bounds. */ + aabbIntersectsSkeleton(bounds: SkeletonBounds) { + return this.minX < bounds.maxX && this.maxX > bounds.minX && this.minY < bounds.maxY && this.maxY > bounds.minY; + } + + /** Returns the first bounding box attachment that contains the point, or null. When doing many checks, it is usually more + * efficient to only call this method if {@link #aabbContainsPoint(float, float)} returns true. */ + containsPoint(x: number, y: number): BoundingBoxAttachment { + const polygons = this.polygons; + for (let i = 0, n = polygons.length; i < n; i++) + if (this.containsPointPolygon(polygons[i], x, y)) return this.boundingBoxes[i]; + return null; + } + + /** Returns true if the polygon contains the point. */ + containsPointPolygon(polygon: ArrayLike, x: number, y: number) { + const vertices = polygon; + const nn = polygon.length; + + let prevIndex = nn - 2; + let inside = false; + for (let ii = 0; ii < nn; ii += 2) { + const vertexY = vertices[ii + 1]; + const prevY = vertices[prevIndex + 1]; + if ((vertexY < y && prevY >= y) || (prevY < y && vertexY >= y)) { + const vertexX = vertices[ii]; + if (vertexX + ((y - vertexY) / (prevY - vertexY)) * (vertices[prevIndex] - vertexX) < x) inside = !inside; + } + prevIndex = ii; + } + return inside; + } + + /** Returns the first bounding box attachment that contains any part of the line segment, or null. When doing many checks, it + * is usually more efficient to only call this method if {@link #aabbIntersectsSegment()} returns + * true. */ + intersectsSegment(x1: number, y1: number, x2: number, y2: number) { + const polygons = this.polygons; + for (let i = 0, n = polygons.length; i < n; i++) + if (this.intersectsSegmentPolygon(polygons[i], x1, y1, x2, y2)) return this.boundingBoxes[i]; + return null; + } + + /** Returns true if the polygon contains any part of the line segment. */ + intersectsSegmentPolygon(polygon: ArrayLike, x1: number, y1: number, x2: number, y2: number) { + const vertices = polygon; + const nn = polygon.length; + + const width12 = x1 - x2, + height12 = y1 - y2; + const det1 = x1 * y2 - y1 * x2; + let x3 = vertices[nn - 2], + y3 = vertices[nn - 1]; + for (let ii = 0; ii < nn; ii += 2) { + const x4 = vertices[ii], + y4 = vertices[ii + 1]; + const det2 = x3 * y4 - y3 * x4; + const width34 = x3 - x4, + height34 = y3 - y4; + const det3 = width12 * height34 - height12 * width34; + const x = (det1 * width34 - width12 * det2) / det3; + if (((x >= x3 && x <= x4) || (x >= x4 && x <= x3)) && ((x >= x1 && x <= x2) || (x >= x2 && x <= x1))) { + const y = (det1 * height34 - height12 * det2) / det3; + if (((y >= y3 && y <= y4) || (y >= y4 && y <= y3)) && ((y >= y1 && y <= y2) || (y >= y2 && y <= y1))) + return true; + } + x3 = x4; + y3 = y4; + } + return false; + } + + /** Returns the polygon for the specified bounding box, or null. */ + getPolygon(boundingBox: BoundingBoxAttachment) { + if (boundingBox == null) throw new Error("boundingBox cannot be null."); + const index = this.boundingBoxes.indexOf(boundingBox); + return index == -1 ? null : this.polygons[index]; + } + + /** The width of the axis aligned bounding box. */ + getWidth() { + return this.maxX - this.minX; + } + + /** The height of the axis aligned bounding box. */ + getHeight() { + return this.maxY - this.minY; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/SkeletonClipping.ts b/packages/spine-core-3.8/src/spine-core/SkeletonClipping.ts new file mode 100644 index 0000000000..563a93c1f6 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/SkeletonClipping.ts @@ -0,0 +1,363 @@ +import { Triangulator } from "./Triangulator"; +import { ClippingAttachment } from "./attachments/ClippingAttachment"; +import { Slot } from "./Slot"; +import { Utils, Color, ArrayLike } from "./Utils"; + +export class SkeletonClipping { + private triangulator = new Triangulator(); + private clippingPolygon = new Array(); + private clipOutput = new Array(); + clippedVertices = new Array(); + clippedTriangles = new Array(); + private scratch = new Array(); + + private clipAttachment: ClippingAttachment; + private clippingPolygons: Array>; + + clipStart(slot: Slot, clip: ClippingAttachment): number { + if (this.clipAttachment != null) return 0; + this.clipAttachment = clip; + + const n = clip.worldVerticesLength; + const vertices = Utils.setArraySize(this.clippingPolygon, n); + clip.computeWorldVertices(slot, 0, n, vertices, 0, 2); + const clippingPolygon = this.clippingPolygon; + SkeletonClipping.makeClockwise(clippingPolygon); + const clippingPolygons = (this.clippingPolygons = this.triangulator.decompose( + clippingPolygon, + this.triangulator.triangulate(clippingPolygon) + )); + for (let i = 0, n = clippingPolygons.length; i < n; i++) { + const polygon = clippingPolygons[i]; + SkeletonClipping.makeClockwise(polygon); + polygon.push(polygon[0]); + polygon.push(polygon[1]); + } + + return clippingPolygons.length; + } + + clipEndWithSlot(slot: Slot) { + if (this.clipAttachment != null && this.clipAttachment.endSlot == slot.data) this.clipEnd(); + } + + clipEnd() { + if (this.clipAttachment == null) return; + this.clipAttachment = null; + this.clippingPolygons = null; + this.clippedVertices.length = 0; + this.clippedTriangles.length = 0; + this.clippingPolygon.length = 0; + } + + isClipping(): boolean { + return this.clipAttachment != null; + } + + clipTriangles( + vertices: ArrayLike, + verticesLength: number, + triangles: ArrayLike, + trianglesLength: number, + uvs: ArrayLike, + light: Color, + dark: Color, + twoColor: boolean + ) { + const clipOutput = this.clipOutput, + clippedVertices = this.clippedVertices; + const clippedTriangles = this.clippedTriangles; + const polygons = this.clippingPolygons; + const polygonsCount = this.clippingPolygons.length; + const vertexSize = twoColor ? 12 : 8; + + let index = 0; + clippedVertices.length = 0; + clippedTriangles.length = 0; + outer: for (let i = 0; i < trianglesLength; i += 3) { + let vertexOffset = triangles[i] << 1; + const x1 = vertices[vertexOffset], + y1 = vertices[vertexOffset + 1]; + const u1 = uvs[vertexOffset], + v1 = uvs[vertexOffset + 1]; + + vertexOffset = triangles[i + 1] << 1; + const x2 = vertices[vertexOffset], + y2 = vertices[vertexOffset + 1]; + const u2 = uvs[vertexOffset], + v2 = uvs[vertexOffset + 1]; + + vertexOffset = triangles[i + 2] << 1; + const x3 = vertices[vertexOffset], + y3 = vertices[vertexOffset + 1]; + const u3 = uvs[vertexOffset], + v3 = uvs[vertexOffset + 1]; + + for (let p = 0; p < polygonsCount; p++) { + let s = clippedVertices.length; + if (this.clip(x1, y1, x2, y2, x3, y3, polygons[p], clipOutput)) { + const clipOutputLength = clipOutput.length; + if (clipOutputLength == 0) continue; + const d0 = y2 - y3, + d1 = x3 - x2, + d2 = x1 - x3, + d4 = y3 - y1; + const d = 1 / (d0 * d2 + d1 * (y1 - y3)); + + let clipOutputCount = clipOutputLength >> 1; + const clipOutputItems = this.clipOutput; + const clippedVerticesItems = Utils.setArraySize(clippedVertices, s + clipOutputCount * vertexSize); + for (let ii = 0; ii < clipOutputLength; ii += 2) { + const x = clipOutputItems[ii], + y = clipOutputItems[ii + 1]; + clippedVerticesItems[s] = x; + clippedVerticesItems[s + 1] = y; + clippedVerticesItems[s + 2] = light.r; + clippedVerticesItems[s + 3] = light.g; + clippedVerticesItems[s + 4] = light.b; + clippedVerticesItems[s + 5] = light.a; + const c0 = x - x3, + c1 = y - y3; + const a = (d0 * c0 + d1 * c1) * d; + const b = (d4 * c0 + d2 * c1) * d; + const c = 1 - a - b; + clippedVerticesItems[s + 6] = u1 * a + u2 * b + u3 * c; + clippedVerticesItems[s + 7] = v1 * a + v2 * b + v3 * c; + if (twoColor) { + clippedVerticesItems[s + 8] = dark.r; + clippedVerticesItems[s + 9] = dark.g; + clippedVerticesItems[s + 10] = dark.b; + clippedVerticesItems[s + 11] = dark.a; + } + s += vertexSize; + } + + s = clippedTriangles.length; + const clippedTrianglesItems = Utils.setArraySize(clippedTriangles, s + 3 * (clipOutputCount - 2)); + clipOutputCount--; + for (let ii = 1; ii < clipOutputCount; ii++) { + clippedTrianglesItems[s] = index; + clippedTrianglesItems[s + 1] = index + ii; + clippedTrianglesItems[s + 2] = index + ii + 1; + s += 3; + } + index += clipOutputCount + 1; + } else { + const clippedVerticesItems = Utils.setArraySize(clippedVertices, s + 3 * vertexSize); + clippedVerticesItems[s] = x1; + clippedVerticesItems[s + 1] = y1; + clippedVerticesItems[s + 2] = light.r; + clippedVerticesItems[s + 3] = light.g; + clippedVerticesItems[s + 4] = light.b; + clippedVerticesItems[s + 5] = light.a; + if (!twoColor) { + clippedVerticesItems[s + 6] = u1; + clippedVerticesItems[s + 7] = v1; + + clippedVerticesItems[s + 8] = x2; + clippedVerticesItems[s + 9] = y2; + clippedVerticesItems[s + 10] = light.r; + clippedVerticesItems[s + 11] = light.g; + clippedVerticesItems[s + 12] = light.b; + clippedVerticesItems[s + 13] = light.a; + clippedVerticesItems[s + 14] = u2; + clippedVerticesItems[s + 15] = v2; + + clippedVerticesItems[s + 16] = x3; + clippedVerticesItems[s + 17] = y3; + clippedVerticesItems[s + 18] = light.r; + clippedVerticesItems[s + 19] = light.g; + clippedVerticesItems[s + 20] = light.b; + clippedVerticesItems[s + 21] = light.a; + clippedVerticesItems[s + 22] = u3; + clippedVerticesItems[s + 23] = v3; + } else { + clippedVerticesItems[s + 6] = u1; + clippedVerticesItems[s + 7] = v1; + clippedVerticesItems[s + 8] = dark.r; + clippedVerticesItems[s + 9] = dark.g; + clippedVerticesItems[s + 10] = dark.b; + clippedVerticesItems[s + 11] = dark.a; + + clippedVerticesItems[s + 12] = x2; + clippedVerticesItems[s + 13] = y2; + clippedVerticesItems[s + 14] = light.r; + clippedVerticesItems[s + 15] = light.g; + clippedVerticesItems[s + 16] = light.b; + clippedVerticesItems[s + 17] = light.a; + clippedVerticesItems[s + 18] = u2; + clippedVerticesItems[s + 19] = v2; + clippedVerticesItems[s + 20] = dark.r; + clippedVerticesItems[s + 21] = dark.g; + clippedVerticesItems[s + 22] = dark.b; + clippedVerticesItems[s + 23] = dark.a; + + clippedVerticesItems[s + 24] = x3; + clippedVerticesItems[s + 25] = y3; + clippedVerticesItems[s + 26] = light.r; + clippedVerticesItems[s + 27] = light.g; + clippedVerticesItems[s + 28] = light.b; + clippedVerticesItems[s + 29] = light.a; + clippedVerticesItems[s + 30] = u3; + clippedVerticesItems[s + 31] = v3; + clippedVerticesItems[s + 32] = dark.r; + clippedVerticesItems[s + 33] = dark.g; + clippedVerticesItems[s + 34] = dark.b; + clippedVerticesItems[s + 35] = dark.a; + } + + s = clippedTriangles.length; + const clippedTrianglesItems = Utils.setArraySize(clippedTriangles, s + 3); + clippedTrianglesItems[s] = index; + clippedTrianglesItems[s + 1] = index + 1; + clippedTrianglesItems[s + 2] = index + 2; + index += 3; + continue outer; + } + } + } + } + + /** Clips the input triangle against the convex, clockwise clipping area. If the triangle lies entirely within the clipping + * area, false is returned. The clipping area must duplicate the first vertex at the end of the vertices list. */ + clip( + x1: number, + y1: number, + x2: number, + y2: number, + x3: number, + y3: number, + clippingArea: Array, + output: Array + ) { + const originalOutput = output; + let clipped = false; + + // Avoid copy at the end. + let input: Array = null; + if (clippingArea.length % 4 >= 2) { + input = output; + output = this.scratch; + } else input = this.scratch; + + input.length = 0; + input.push(x1); + input.push(y1); + input.push(x2); + input.push(y2); + input.push(x3); + input.push(y3); + input.push(x1); + input.push(y1); + output.length = 0; + + const clippingVertices = clippingArea; + const clippingVerticesLast = clippingArea.length - 4; + for (let i = 0; ; i += 2) { + const edgeX = clippingVertices[i], + edgeY = clippingVertices[i + 1]; + const edgeX2 = clippingVertices[i + 2], + edgeY2 = clippingVertices[i + 3]; + const deltaX = edgeX - edgeX2, + deltaY = edgeY - edgeY2; + + const inputVertices = input; + const inputVerticesLength = input.length - 2, + outputStart = output.length; + for (let ii = 0; ii < inputVerticesLength; ii += 2) { + const inputX = inputVertices[ii], + inputY = inputVertices[ii + 1]; + const inputX2 = inputVertices[ii + 2], + inputY2 = inputVertices[ii + 3]; + const side2 = deltaX * (inputY2 - edgeY2) - deltaY * (inputX2 - edgeX2) > 0; + if (deltaX * (inputY - edgeY2) - deltaY * (inputX - edgeX2) > 0) { + if (side2) { + // v1 inside, v2 inside + output.push(inputX2); + output.push(inputY2); + continue; + } + // v1 inside, v2 outside + const c0 = inputY2 - inputY, + c2 = inputX2 - inputX; + const s = c0 * (edgeX2 - edgeX) - c2 * (edgeY2 - edgeY); + if (Math.abs(s) > 0.000001) { + const ua = (c2 * (edgeY - inputY) - c0 * (edgeX - inputX)) / s; + output.push(edgeX + (edgeX2 - edgeX) * ua); + output.push(edgeY + (edgeY2 - edgeY) * ua); + } else { + output.push(edgeX); + output.push(edgeY); + } + } else if (side2) { + // v1 outside, v2 inside + const c0 = inputY2 - inputY, + c2 = inputX2 - inputX; + const s = c0 * (edgeX2 - edgeX) - c2 * (edgeY2 - edgeY); + if (Math.abs(s) > 0.000001) { + const ua = (c2 * (edgeY - inputY) - c0 * (edgeX - inputX)) / s; + output.push(edgeX + (edgeX2 - edgeX) * ua); + output.push(edgeY + (edgeY2 - edgeY) * ua); + } else { + output.push(edgeX); + output.push(edgeY); + } + output.push(inputX2); + output.push(inputY2); + } + clipped = true; + } + + if (outputStart == output.length) { + // All edges outside. + originalOutput.length = 0; + return true; + } + + output.push(output[0]); + output.push(output[1]); + + if (i == clippingVerticesLast) break; + const temp = output; + output = input; + output.length = 0; + input = temp; + } + + if (originalOutput != output) { + originalOutput.length = 0; + for (let i = 0, n = output.length - 2; i < n; i++) originalOutput[i] = output[i]; + } else originalOutput.length = originalOutput.length - 2; + + return clipped; + } + + public static makeClockwise(polygon: ArrayLike) { + const vertices = polygon; + const verticeslength = polygon.length; + + let area = vertices[verticeslength - 2] * vertices[1] - vertices[0] * vertices[verticeslength - 1], + p1x = 0, + p1y = 0, + p2x = 0, + p2y = 0; + for (let i = 0, n = verticeslength - 3; i < n; i += 2) { + p1x = vertices[i]; + p1y = vertices[i + 1]; + p2x = vertices[i + 2]; + p2y = vertices[i + 3]; + area += p1x * p2y - p2x * p1y; + } + if (area < 0) return; + + for (let i = 0, lastX = verticeslength - 2, n = verticeslength >> 1; i < n; i += 2) { + const x = vertices[i], + y = vertices[i + 1]; + const other = lastX - i; + vertices[i] = vertices[other]; + vertices[i + 1] = vertices[other + 1]; + vertices[other] = x; + vertices[other + 1] = y; + } + } +} diff --git a/packages/spine-core-3.8/src/spine-core/SkeletonData.ts b/packages/spine-core-3.8/src/spine-core/SkeletonData.ts new file mode 100644 index 0000000000..67c4f657bb --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/SkeletonData.ts @@ -0,0 +1,198 @@ +import { BoneData } from "./BoneData"; +import { SlotData } from "./SlotData"; +import { Skin } from "./Skin"; +import { EventData } from "./EventData"; +import { IkConstraintData } from "./IkConstraintData"; +import { TransformConstraintData } from "./TransformConstraintData"; +import { PathConstraintData } from "./PathConstraintData"; +import { Animation } from "./Animation"; + +/** Stores the setup pose and all of the stateless data for a skeleton. + * + * See [Data objects](http://esotericsoftware.com/spine-runtime-architecture#Data-objects) in the Spine Runtimes + * Guide. */ +export class SkeletonData { + /** The skeleton's name, which by default is the name of the skeleton data file, if possible. May be null. */ + name: string; + + /** The skeleton's bones, sorted parent first. The root bone is always the first bone. */ + bones = new Array(); // Ordered parents first. + + /** The skeleton's slots. */ + slots = new Array(); // Setup pose draw order. + skins = new Array(); + + /** The skeleton's default skin. By default this skin contains all attachments that were not in a skin in Spine. + * + * See {@link Skeleton#getAttachmentByName()}. + * May be null. */ + defaultSkin: Skin; + + /** The skeleton's events. */ + events = new Array(); + + /** The skeleton's animations. */ + animations = new Array(); + + /** The skeleton's IK constraints. */ + ikConstraints = new Array(); + + /** The skeleton's transform constraints. */ + transformConstraints = new Array(); + + /** The skeleton's path constraints. */ + pathConstraints = new Array(); + + /** The X coordinate of the skeleton's axis aligned bounding box in the setup pose. */ + x: number; + + /** The Y coordinate of the skeleton's axis aligned bounding box in the setup pose. */ + y: number; + + /** The width of the skeleton's axis aligned bounding box in the setup pose. */ + width: number; + + /** The height of the skeleton's axis aligned bounding box in the setup pose. */ + height: number; + + /** The Spine version used to export the skeleton data, or null. */ + version: string; + + /** The skeleton data hash. This value will change if any of the skeleton data has changed. May be null. */ + hash: string; + + // Nonessential + /** The dopesheet FPS in Spine. Available only when nonessential data was exported. */ + fps = 0; + + /** The path to the images directory as defined in Spine. Available only when nonessential data was exported. May be null. */ + imagesPath: string; + + /** The path to the audio directory as defined in Spine. Available only when nonessential data was exported. May be null. */ + audioPath: string; + + /** Finds a bone by comparing each bone's name. It is more efficient to cache the results of this method than to call it + * multiple times. + * @returns May be null. */ + findBone(boneName: string) { + if (boneName == null) throw new Error("boneName cannot be null."); + const bones = this.bones; + for (let i = 0, n = bones.length; i < n; i++) { + const bone = bones[i]; + if (bone.name == boneName) return bone; + } + return null; + } + + findBoneIndex(boneName: string) { + if (boneName == null) throw new Error("boneName cannot be null."); + const bones = this.bones; + for (let i = 0, n = bones.length; i < n; i++) if (bones[i].name == boneName) return i; + return -1; + } + + /** Finds a slot by comparing each slot's name. It is more efficient to cache the results of this method than to call it + * multiple times. + * @returns May be null. */ + findSlot(slotName: string) { + if (slotName == null) throw new Error("slotName cannot be null."); + const slots = this.slots; + for (let i = 0, n = slots.length; i < n; i++) { + const slot = slots[i]; + if (slot.name == slotName) return slot; + } + return null; + } + + findSlotIndex(slotName: string) { + if (slotName == null) throw new Error("slotName cannot be null."); + const slots = this.slots; + for (let i = 0, n = slots.length; i < n; i++) if (slots[i].name == slotName) return i; + return -1; + } + + /** Finds a skin by comparing each skin's name. It is more efficient to cache the results of this method than to call it + * multiple times. + * @returns May be null. */ + findSkin(skinName: string) { + if (skinName == null) throw new Error("skinName cannot be null."); + const skins = this.skins; + for (let i = 0, n = skins.length; i < n; i++) { + const skin = skins[i]; + if (skin.name == skinName) return skin; + } + return null; + } + + /** Finds an event by comparing each events's name. It is more efficient to cache the results of this method than to call it + * multiple times. + * @returns May be null. */ + findEvent(eventDataName: string) { + if (eventDataName == null) throw new Error("eventDataName cannot be null."); + const events = this.events; + for (let i = 0, n = events.length; i < n; i++) { + const event = events[i]; + if (event.name == eventDataName) return event; + } + return null; + } + + /** Finds an animation by comparing each animation's name. It is more efficient to cache the results of this method than to + * call it multiple times. + * @returns May be null. */ + findAnimation(animationName: string) { + if (animationName == null) throw new Error("animationName cannot be null."); + const animations = this.animations; + for (let i = 0, n = animations.length; i < n; i++) { + const animation = animations[i]; + if (animation.name == animationName) return animation; + } + return null; + } + + /** Finds an IK constraint by comparing each IK constraint's name. It is more efficient to cache the results of this method + * than to call it multiple times. + * @return May be null. */ + findIkConstraint(constraintName: string) { + if (constraintName == null) throw new Error("constraintName cannot be null."); + const ikConstraints = this.ikConstraints; + for (let i = 0, n = ikConstraints.length; i < n; i++) { + const constraint = ikConstraints[i]; + if (constraint.name == constraintName) return constraint; + } + return null; + } + + /** Finds a transform constraint by comparing each transform constraint's name. It is more efficient to cache the results of + * this method than to call it multiple times. + * @return May be null. */ + findTransformConstraint(constraintName: string) { + if (constraintName == null) throw new Error("constraintName cannot be null."); + const transformConstraints = this.transformConstraints; + for (let i = 0, n = transformConstraints.length; i < n; i++) { + const constraint = transformConstraints[i]; + if (constraint.name == constraintName) return constraint; + } + return null; + } + + /** Finds a path constraint by comparing each path constraint's name. It is more efficient to cache the results of this method + * than to call it multiple times. + * @return May be null. */ + findPathConstraint(constraintName: string) { + if (constraintName == null) throw new Error("constraintName cannot be null."); + const pathConstraints = this.pathConstraints; + for (let i = 0, n = pathConstraints.length; i < n; i++) { + const constraint = pathConstraints[i]; + if (constraint.name == constraintName) return constraint; + } + return null; + } + + findPathConstraintIndex(pathConstraintName: string) { + if (pathConstraintName == null) throw new Error("pathConstraintName cannot be null."); + const pathConstraints = this.pathConstraints; + for (let i = 0, n = pathConstraints.length; i < n; i++) if (pathConstraints[i].name == pathConstraintName) return i; + return -1; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/SkeletonJson.ts b/packages/spine-core-3.8/src/spine-core/SkeletonJson.ts new file mode 100644 index 0000000000..9c55054752 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/SkeletonJson.ts @@ -0,0 +1,906 @@ +import { AttachmentLoader } from "./attachments/AttachmentLoader"; +import { SkeletonData } from "./SkeletonData"; +import { BoneData, TransformMode } from "./BoneData"; +import { SlotData } from "./SlotData"; +import { Color, Utils, ArrayLike } from "./Utils"; +import { IkConstraintData } from "./IkConstraintData"; +import { TransformConstraintData } from "./TransformConstraintData"; +import { PathConstraintData, PositionMode, SpacingMode, RotateMode } from "./PathConstraintData"; +import { Skin } from "./Skin"; +import { VertexAttachment, Attachment } from "./attachments/Attachment"; +import { MeshAttachment } from "./attachments/MeshAttachment"; +import { EventData } from "./EventData"; +import { + Timeline, + AttachmentTimeline, + ColorTimeline, + TwoColorTimeline, + RotateTimeline, + TranslateTimeline, + ScaleTimeline, + ShearTimeline, + IkConstraintTimeline, + TransformConstraintTimeline, + PathConstraintPositionTimeline, + PathConstraintSpacingTimeline, + PathConstraintMixTimeline, + DeformTimeline, + DrawOrderTimeline, + EventTimeline, + CurveTimeline +} from "./Animation"; +import { BlendMode } from "./BlendMode"; +import { Event } from "./Event"; +import { Animation } from "./Animation"; + +/** Loads skeleton data in the Spine JSON format. + * + * See [Spine JSON format](http://esotericsoftware.com/spine-json-format) and + * [JSON and binary data](http://esotericsoftware.com/spine-loading-skeleton-data#JSON-and-binary-data) in the Spine + * Runtimes Guide. */ +export class SkeletonJson { + attachmentLoader: AttachmentLoader; + + /** Scales bone positions, image sizes, and translations as they are loaded. This allows different size images to be used at + * runtime than were used in Spine. + * + * See [Scaling](http://esotericsoftware.com/spine-loading-skeleton-data#Scaling) in the Spine Runtimes Guide. */ + scale = 1; + private linkedMeshes = new Array(); + + constructor(attachmentLoader: AttachmentLoader) { + this.attachmentLoader = attachmentLoader; + } + + readSkeletonData(json: string | any): SkeletonData { + const scale = this.scale; + const skeletonData = new SkeletonData(); + const root = typeof json === "string" ? JSON.parse(json) : json; + + // Skeleton + const skeletonMap = root.skeleton; + if (skeletonMap != null) { + skeletonData.hash = skeletonMap.hash; + skeletonData.version = skeletonMap.spine; + if ("3.8.75" == skeletonData.version) + throw new Error("Unsupported skeleton data, please export with a newer version of Spine."); + skeletonData.x = skeletonMap.x; + skeletonData.y = skeletonMap.y; + skeletonData.width = skeletonMap.width; + skeletonData.height = skeletonMap.height; + skeletonData.fps = skeletonMap.fps; + skeletonData.imagesPath = skeletonMap.images; + } + + // Bones + if (root.bones) { + for (let i = 0; i < root.bones.length; i++) { + const boneMap = root.bones[i]; + + let parent: BoneData = null; + const parentName: string = this.getValue(boneMap, "parent", null); + if (parentName != null) { + parent = skeletonData.findBone(parentName); + if (parent == null) throw new Error("Parent bone not found: " + parentName); + } + const data = new BoneData(skeletonData.bones.length, boneMap.name, parent); + data.length = this.getValue(boneMap, "length", 0) * scale; + data.x = this.getValue(boneMap, "x", 0) * scale; + data.y = this.getValue(boneMap, "y", 0) * scale; + data.rotation = this.getValue(boneMap, "rotation", 0); + data.scaleX = this.getValue(boneMap, "scaleX", 1); + data.scaleY = this.getValue(boneMap, "scaleY", 1); + data.shearX = this.getValue(boneMap, "shearX", 0); + data.shearY = this.getValue(boneMap, "shearY", 0); + data.transformMode = SkeletonJson.transformModeFromString(this.getValue(boneMap, "transform", "normal")); + data.skinRequired = this.getValue(boneMap, "skin", false); + + skeletonData.bones.push(data); + } + } + + // Slots. + if (root.slots) { + for (let i = 0; i < root.slots.length; i++) { + const slotMap = root.slots[i]; + const slotName: string = slotMap.name; + const boneName: string = slotMap.bone; + const boneData = skeletonData.findBone(boneName); + if (boneData == null) throw new Error("Slot bone not found: " + boneName); + const data = new SlotData(skeletonData.slots.length, slotName, boneData); + + const color: string = this.getValue(slotMap, "color", null); + if (color != null) data.color.setFromString(color); + + const dark: string = this.getValue(slotMap, "dark", null); + if (dark != null) { + data.darkColor = new Color(1, 1, 1, 1); + data.darkColor.setFromString(dark); + } + + data.attachmentName = this.getValue(slotMap, "attachment", null); + data.blendMode = SkeletonJson.blendModeFromString(this.getValue(slotMap, "blend", "normal")); + skeletonData.slots.push(data); + } + } + + // IK constraints + if (root.ik) { + for (let i = 0; i < root.ik.length; i++) { + const constraintMap = root.ik[i]; + const data = new IkConstraintData(constraintMap.name); + data.order = this.getValue(constraintMap, "order", 0); + data.skinRequired = this.getValue(constraintMap, "skin", false); + + for (let j = 0; j < constraintMap.bones.length; j++) { + const boneName = constraintMap.bones[j]; + const bone = skeletonData.findBone(boneName); + if (bone == null) throw new Error("IK bone not found: " + boneName); + data.bones.push(bone); + } + + const targetName: string = constraintMap.target; + data.target = skeletonData.findBone(targetName); + if (data.target == null) throw new Error("IK target bone not found: " + targetName); + + data.mix = this.getValue(constraintMap, "mix", 1); + data.softness = this.getValue(constraintMap, "softness", 0) * scale; + data.bendDirection = this.getValue(constraintMap, "bendPositive", true) ? 1 : -1; + data.compress = this.getValue(constraintMap, "compress", false); + data.stretch = this.getValue(constraintMap, "stretch", false); + data.uniform = this.getValue(constraintMap, "uniform", false); + + skeletonData.ikConstraints.push(data); + } + } + + // Transform constraints. + if (root.transform) { + for (let i = 0; i < root.transform.length; i++) { + const constraintMap = root.transform[i]; + const data = new TransformConstraintData(constraintMap.name); + data.order = this.getValue(constraintMap, "order", 0); + data.skinRequired = this.getValue(constraintMap, "skin", false); + + for (let j = 0; j < constraintMap.bones.length; j++) { + const boneName = constraintMap.bones[j]; + const bone = skeletonData.findBone(boneName); + if (bone == null) throw new Error("Transform constraint bone not found: " + boneName); + data.bones.push(bone); + } + + const targetName: string = constraintMap.target; + data.target = skeletonData.findBone(targetName); + if (data.target == null) throw new Error("Transform constraint target bone not found: " + targetName); + + data.local = this.getValue(constraintMap, "local", false); + data.relative = this.getValue(constraintMap, "relative", false); + data.offsetRotation = this.getValue(constraintMap, "rotation", 0); + data.offsetX = this.getValue(constraintMap, "x", 0) * scale; + data.offsetY = this.getValue(constraintMap, "y", 0) * scale; + data.offsetScaleX = this.getValue(constraintMap, "scaleX", 0); + data.offsetScaleY = this.getValue(constraintMap, "scaleY", 0); + data.offsetShearY = this.getValue(constraintMap, "shearY", 0); + + data.rotateMix = this.getValue(constraintMap, "rotateMix", 1); + data.translateMix = this.getValue(constraintMap, "translateMix", 1); + data.scaleMix = this.getValue(constraintMap, "scaleMix", 1); + data.shearMix = this.getValue(constraintMap, "shearMix", 1); + + skeletonData.transformConstraints.push(data); + } + } + + // Path constraints. + if (root.path) { + for (let i = 0; i < root.path.length; i++) { + const constraintMap = root.path[i]; + const data = new PathConstraintData(constraintMap.name); + data.order = this.getValue(constraintMap, "order", 0); + data.skinRequired = this.getValue(constraintMap, "skin", false); + + for (let j = 0; j < constraintMap.bones.length; j++) { + const boneName = constraintMap.bones[j]; + const bone = skeletonData.findBone(boneName); + if (bone == null) throw new Error("Transform constraint bone not found: " + boneName); + data.bones.push(bone); + } + + const targetName: string = constraintMap.target; + data.target = skeletonData.findSlot(targetName); + if (data.target == null) throw new Error("Path target slot not found: " + targetName); + + data.positionMode = SkeletonJson.positionModeFromString( + this.getValue(constraintMap, "positionMode", "percent") + ); + data.spacingMode = SkeletonJson.spacingModeFromString(this.getValue(constraintMap, "spacingMode", "length")); + data.rotateMode = SkeletonJson.rotateModeFromString(this.getValue(constraintMap, "rotateMode", "tangent")); + data.offsetRotation = this.getValue(constraintMap, "rotation", 0); + data.position = this.getValue(constraintMap, "position", 0); + if (data.positionMode == PositionMode.Fixed) data.position *= scale; + data.spacing = this.getValue(constraintMap, "spacing", 0); + if (data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed) data.spacing *= scale; + data.rotateMix = this.getValue(constraintMap, "rotateMix", 1); + data.translateMix = this.getValue(constraintMap, "translateMix", 1); + + skeletonData.pathConstraints.push(data); + } + } + + // Skins. + if (root.skins) { + for (let i = 0; i < root.skins.length; i++) { + const skinMap = root.skins[i]; + const skin = new Skin(skinMap.name); + + if (skinMap.bones) { + for (let ii = 0; ii < skinMap.bones.length; ii++) { + const bone = skeletonData.findBone(skinMap.bones[ii]); + if (bone == null) throw new Error("Skin bone not found: " + skinMap.bones[i]); + skin.bones.push(bone); + } + } + + if (skinMap.ik) { + for (let ii = 0; ii < skinMap.ik.length; ii++) { + const constraint = skeletonData.findIkConstraint(skinMap.ik[ii]); + if (constraint == null) throw new Error("Skin IK constraint not found: " + skinMap.ik[i]); + skin.constraints.push(constraint); + } + } + + if (skinMap.transform) { + for (let ii = 0; ii < skinMap.transform.length; ii++) { + const constraint = skeletonData.findTransformConstraint(skinMap.transform[ii]); + if (constraint == null) throw new Error("Skin transform constraint not found: " + skinMap.transform[i]); + skin.constraints.push(constraint); + } + } + + if (skinMap.path) { + for (let ii = 0; ii < skinMap.path.length; ii++) { + const constraint = skeletonData.findPathConstraint(skinMap.path[ii]); + if (constraint == null) throw new Error("Skin path constraint not found: " + skinMap.path[i]); + skin.constraints.push(constraint); + } + } + + for (const slotName in skinMap.attachments) { + const slot = skeletonData.findSlot(slotName); + if (slot == null) throw new Error("Slot not found: " + slotName); + const slotMap = skinMap.attachments[slotName]; + for (const entryName in slotMap) { + const attachment = this.readAttachment(slotMap[entryName], skin, slot.index, entryName, skeletonData); + if (attachment != null) skin.setAttachment(slot.index, entryName, attachment); + } + } + skeletonData.skins.push(skin); + if (skin.name == "default") skeletonData.defaultSkin = skin; + } + } + + // Linked meshes. + for (let i = 0, n = this.linkedMeshes.length; i < n; i++) { + const linkedMesh = this.linkedMeshes[i]; + const skin = linkedMesh.skin == null ? skeletonData.defaultSkin : skeletonData.findSkin(linkedMesh.skin); + if (skin == null) throw new Error("Skin not found: " + linkedMesh.skin); + const parent = skin.getAttachment(linkedMesh.slotIndex, linkedMesh.parent); + if (parent == null) throw new Error("Parent mesh not found: " + linkedMesh.parent); + linkedMesh.mesh.deformAttachment = linkedMesh.inheritDeform + ? parent + : linkedMesh.mesh; + linkedMesh.mesh.setParentMesh(parent); + linkedMesh.mesh.updateUVs(); + } + this.linkedMeshes.length = 0; + + // Events. + if (root.events) { + for (const eventName in root.events) { + const eventMap = root.events[eventName]; + const data = new EventData(eventName); + data.intValue = this.getValue(eventMap, "int", 0); + data.floatValue = this.getValue(eventMap, "float", 0); + data.stringValue = this.getValue(eventMap, "string", ""); + data.audioPath = this.getValue(eventMap, "audio", null); + if (data.audioPath != null) { + data.volume = this.getValue(eventMap, "volume", 1); + data.balance = this.getValue(eventMap, "balance", 0); + } + skeletonData.events.push(data); + } + } + + // Animations. + if (root.animations) { + for (const animationName in root.animations) { + const animationMap = root.animations[animationName]; + this.readAnimation(animationMap, animationName, skeletonData); + } + } + + return skeletonData; + } + + readAttachment(map: any, skin: Skin, slotIndex: number, name: string, skeletonData: SkeletonData): Attachment { + const scale = this.scale; + name = this.getValue(map, "name", name); + + const type = this.getValue(map, "type", "region"); + + switch (type) { + case "region": { + const path = this.getValue(map, "path", name); + const region = this.attachmentLoader.newRegionAttachment(skin, name, path); + if (region == null) return null; + region.path = path; + region.x = this.getValue(map, "x", 0) * scale; + region.y = this.getValue(map, "y", 0) * scale; + region.scaleX = this.getValue(map, "scaleX", 1); + region.scaleY = this.getValue(map, "scaleY", 1); + region.rotation = this.getValue(map, "rotation", 0); + region.width = map.width * scale; + region.height = map.height * scale; + + const color: string = this.getValue(map, "color", null); + if (color != null) region.color.setFromString(color); + + region.updateOffset(); + return region; + } + case "boundingbox": { + const box = this.attachmentLoader.newBoundingBoxAttachment(skin, name); + if (box == null) return null; + this.readVertices(map, box, map.vertexCount << 1); + const color: string = this.getValue(map, "color", null); + if (color != null) box.color.setFromString(color); + return box; + } + case "mesh": + case "linkedmesh": { + const path = this.getValue(map, "path", name); + const mesh = this.attachmentLoader.newMeshAttachment(skin, name, path); + if (mesh == null) return null; + mesh.path = path; + + const color = this.getValue(map, "color", null); + if (color != null) mesh.color.setFromString(color); + + mesh.width = this.getValue(map, "width", 0) * scale; + mesh.height = this.getValue(map, "height", 0) * scale; + + const parent: string = this.getValue(map, "parent", null); + if (parent != null) { + this.linkedMeshes.push( + new LinkedMesh( + mesh, + this.getValue(map, "skin", null), + slotIndex, + parent, + this.getValue(map, "deform", true) + ) + ); + return mesh; + } + + const uvs: Array = map.uvs; + this.readVertices(map, mesh, uvs.length); + mesh.triangles = map.triangles; + mesh.regionUVs = uvs; + mesh.updateUVs(); + + mesh.edges = this.getValue(map, "edges", null); + mesh.hullLength = this.getValue(map, "hull", 0) * 2; + return mesh; + } + case "path": { + const path = this.attachmentLoader.newPathAttachment(skin, name); + if (path == null) return null; + path.closed = this.getValue(map, "closed", false); + path.constantSpeed = this.getValue(map, "constantSpeed", true); + + const vertexCount = map.vertexCount; + this.readVertices(map, path, vertexCount << 1); + + const lengths: Array = Utils.newArray(vertexCount / 3, 0); + for (let i = 0; i < map.lengths.length; i++) lengths[i] = map.lengths[i] * scale; + path.lengths = lengths; + + const color: string = this.getValue(map, "color", null); + if (color != null) path.color.setFromString(color); + return path; + } + case "point": { + const point = this.attachmentLoader.newPointAttachment(skin, name); + if (point == null) return null; + point.x = this.getValue(map, "x", 0) * scale; + point.y = this.getValue(map, "y", 0) * scale; + point.rotation = this.getValue(map, "rotation", 0); + + const color = this.getValue(map, "color", null); + if (color != null) point.color.setFromString(color); + return point; + } + case "clipping": { + const clip = this.attachmentLoader.newClippingAttachment(skin, name); + if (clip == null) return null; + + const end = this.getValue(map, "end", null); + if (end != null) { + const slot = skeletonData.findSlot(end); + if (slot == null) throw new Error("Clipping end slot not found: " + end); + clip.endSlot = slot; + } + + const vertexCount = map.vertexCount; + this.readVertices(map, clip, vertexCount << 1); + + const color: string = this.getValue(map, "color", null); + if (color != null) clip.color.setFromString(color); + return clip; + } + } + return null; + } + + readVertices(map: any, attachment: VertexAttachment, verticesLength: number) { + const scale = this.scale; + attachment.worldVerticesLength = verticesLength; + const vertices: Array = map.vertices; + if (verticesLength == vertices.length) { + const scaledVertices = Utils.toFloatArray(vertices); + if (scale != 1) { + for (let i = 0, n = vertices.length; i < n; i++) scaledVertices[i] *= scale; + } + attachment.vertices = scaledVertices; + return; + } + const weights = new Array(); + const bones = new Array(); + for (let i = 0, n = vertices.length; i < n; ) { + const boneCount = vertices[i++]; + bones.push(boneCount); + for (let nn = i + boneCount * 4; i < nn; i += 4) { + bones.push(vertices[i]); + weights.push(vertices[i + 1] * scale); + weights.push(vertices[i + 2] * scale); + weights.push(vertices[i + 3]); + } + } + attachment.bones = bones; + attachment.vertices = Utils.toFloatArray(weights); + } + + readAnimation(map: any, name: string, skeletonData: SkeletonData) { + const scale = this.scale; + const timelines = new Array(); + let duration = 0; + + // Slot timelines. + if (map.slots) { + for (const slotName in map.slots) { + const slotMap = map.slots[slotName]; + const slotIndex = skeletonData.findSlotIndex(slotName); + if (slotIndex == -1) throw new Error("Slot not found: " + slotName); + for (const timelineName in slotMap) { + const timelineMap = slotMap[timelineName]; + if (timelineName == "attachment") { + const timeline = new AttachmentTimeline(timelineMap.length); + timeline.slotIndex = slotIndex; + + let frameIndex = 0; + for (let i = 0; i < timelineMap.length; i++) { + const valueMap = timelineMap[i]; + timeline.setFrame(frameIndex++, this.getValue(valueMap, "time", 0), valueMap.name); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[timeline.getFrameCount() - 1]); + } else if (timelineName == "color") { + const timeline = new ColorTimeline(timelineMap.length); + timeline.slotIndex = slotIndex; + + let frameIndex = 0; + for (let i = 0; i < timelineMap.length; i++) { + const valueMap = timelineMap[i]; + const color = new Color(); + color.setFromString(valueMap.color); + timeline.setFrame(frameIndex, this.getValue(valueMap, "time", 0), color.r, color.g, color.b, color.a); + this.readCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[(timeline.getFrameCount() - 1) * ColorTimeline.ENTRIES]); + } else if (timelineName == "twoColor") { + const timeline = new TwoColorTimeline(timelineMap.length); + timeline.slotIndex = slotIndex; + + let frameIndex = 0; + for (let i = 0; i < timelineMap.length; i++) { + const valueMap = timelineMap[i]; + const light = new Color(); + const dark = new Color(); + light.setFromString(valueMap.light); + dark.setFromString(valueMap.dark); + timeline.setFrame( + frameIndex, + this.getValue(valueMap, "time", 0), + light.r, + light.g, + light.b, + light.a, + dark.r, + dark.g, + dark.b + ); + this.readCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[(timeline.getFrameCount() - 1) * TwoColorTimeline.ENTRIES]); + } else throw new Error("Invalid timeline type for a slot: " + timelineName + " (" + slotName + ")"); + } + } + } + + // Bone timelines. + if (map.bones) { + for (const boneName in map.bones) { + const boneMap = map.bones[boneName]; + const boneIndex = skeletonData.findBoneIndex(boneName); + if (boneIndex == -1) throw new Error("Bone not found: " + boneName); + for (const timelineName in boneMap) { + const timelineMap = boneMap[timelineName]; + if (timelineName === "rotate") { + const timeline = new RotateTimeline(timelineMap.length); + timeline.boneIndex = boneIndex; + + let frameIndex = 0; + for (let i = 0; i < timelineMap.length; i++) { + const valueMap = timelineMap[i]; + timeline.setFrame(frameIndex, this.getValue(valueMap, "time", 0), this.getValue(valueMap, "angle", 0)); + this.readCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[(timeline.getFrameCount() - 1) * RotateTimeline.ENTRIES]); + } else if (timelineName === "translate" || timelineName === "scale" || timelineName === "shear") { + let timeline: TranslateTimeline = null; + let timelineScale = 1, + defaultValue = 0; + if (timelineName === "scale") { + timeline = new ScaleTimeline(timelineMap.length); + defaultValue = 1; + } else if (timelineName === "shear") timeline = new ShearTimeline(timelineMap.length); + else { + timeline = new TranslateTimeline(timelineMap.length); + timelineScale = scale; + } + timeline.boneIndex = boneIndex; + + let frameIndex = 0; + for (let i = 0; i < timelineMap.length; i++) { + const valueMap = timelineMap[i]; + const x = this.getValue(valueMap, "x", defaultValue), + y = this.getValue(valueMap, "y", defaultValue); + timeline.setFrame(frameIndex, this.getValue(valueMap, "time", 0), x * timelineScale, y * timelineScale); + this.readCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[(timeline.getFrameCount() - 1) * TranslateTimeline.ENTRIES]); + } else throw new Error("Invalid timeline type for a bone: " + timelineName + " (" + boneName + ")"); + } + } + } + + // IK constraint timelines. + if (map.ik) { + for (const constraintName in map.ik) { + const constraintMap = map.ik[constraintName]; + const constraint = skeletonData.findIkConstraint(constraintName); + const timeline = new IkConstraintTimeline(constraintMap.length); + timeline.ikConstraintIndex = skeletonData.ikConstraints.indexOf(constraint); + let frameIndex = 0; + for (let i = 0; i < constraintMap.length; i++) { + const valueMap = constraintMap[i]; + timeline.setFrame( + frameIndex, + this.getValue(valueMap, "time", 0), + this.getValue(valueMap, "mix", 1), + this.getValue(valueMap, "softness", 0) * scale, + this.getValue(valueMap, "bendPositive", true) ? 1 : -1, + this.getValue(valueMap, "compress", false), + this.getValue(valueMap, "stretch", false) + ); + this.readCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[(timeline.getFrameCount() - 1) * IkConstraintTimeline.ENTRIES]); + } + } + + // Transform constraint timelines. + if (map.transform) { + for (const constraintName in map.transform) { + const constraintMap = map.transform[constraintName]; + const constraint = skeletonData.findTransformConstraint(constraintName); + const timeline = new TransformConstraintTimeline(constraintMap.length); + timeline.transformConstraintIndex = skeletonData.transformConstraints.indexOf(constraint); + let frameIndex = 0; + for (let i = 0; i < constraintMap.length; i++) { + const valueMap = constraintMap[i]; + timeline.setFrame( + frameIndex, + this.getValue(valueMap, "time", 0), + this.getValue(valueMap, "rotateMix", 1), + this.getValue(valueMap, "translateMix", 1), + this.getValue(valueMap, "scaleMix", 1), + this.getValue(valueMap, "shearMix", 1) + ); + this.readCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.push(timeline); + duration = Math.max( + duration, + timeline.frames[(timeline.getFrameCount() - 1) * TransformConstraintTimeline.ENTRIES] + ); + } + } + + // Path constraint timelines. + if (map.path) { + for (const constraintName in map.path) { + const constraintMap = map.path[constraintName]; + const index = skeletonData.findPathConstraintIndex(constraintName); + if (index == -1) throw new Error("Path constraint not found: " + constraintName); + const data = skeletonData.pathConstraints[index]; + for (const timelineName in constraintMap) { + const timelineMap = constraintMap[timelineName]; + if (timelineName === "position" || timelineName === "spacing") { + let timeline: PathConstraintPositionTimeline = null; + let timelineScale = 1; + if (timelineName === "spacing") { + timeline = new PathConstraintSpacingTimeline(timelineMap.length); + if (data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed) + timelineScale = scale; + } else { + timeline = new PathConstraintPositionTimeline(timelineMap.length); + if (data.positionMode == PositionMode.Fixed) timelineScale = scale; + } + timeline.pathConstraintIndex = index; + let frameIndex = 0; + for (let i = 0; i < timelineMap.length; i++) { + const valueMap = timelineMap[i]; + timeline.setFrame( + frameIndex, + this.getValue(valueMap, "time", 0), + this.getValue(valueMap, timelineName, 0) * timelineScale + ); + this.readCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.push(timeline); + duration = Math.max( + duration, + timeline.frames[(timeline.getFrameCount() - 1) * PathConstraintPositionTimeline.ENTRIES] + ); + } else if (timelineName === "mix") { + const timeline = new PathConstraintMixTimeline(timelineMap.length); + timeline.pathConstraintIndex = index; + let frameIndex = 0; + for (let i = 0; i < timelineMap.length; i++) { + const valueMap = timelineMap[i]; + timeline.setFrame( + frameIndex, + this.getValue(valueMap, "time", 0), + this.getValue(valueMap, "rotateMix", 1), + this.getValue(valueMap, "translateMix", 1) + ); + this.readCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.push(timeline); + duration = Math.max( + duration, + timeline.frames[(timeline.getFrameCount() - 1) * PathConstraintMixTimeline.ENTRIES] + ); + } + } + } + } + + // Deform timelines. + if (map.deform) { + for (const deformName in map.deform) { + const deformMap = map.deform[deformName]; + const skin = skeletonData.findSkin(deformName); + if (skin == null) throw new Error("Skin not found: " + deformName); + for (const slotName in deformMap) { + const slotMap = deformMap[slotName]; + const slotIndex = skeletonData.findSlotIndex(slotName); + if (slotIndex == -1) throw new Error("Slot not found: " + slotMap.name); + for (const timelineName in slotMap) { + const timelineMap = slotMap[timelineName]; + const attachment = skin.getAttachment(slotIndex, timelineName); + if (attachment == null) throw new Error("Deform attachment not found: " + timelineMap.name); + const weighted = attachment.bones != null; + const vertices = attachment.vertices; + const deformLength = weighted ? (vertices.length / 3) * 2 : vertices.length; + + const timeline = new DeformTimeline(timelineMap.length); + timeline.slotIndex = slotIndex; + timeline.attachment = attachment; + + let frameIndex = 0; + for (let j = 0; j < timelineMap.length; j++) { + const valueMap = timelineMap[j]; + let deform: ArrayLike; + const verticesValue: Array = this.getValue(valueMap, "vertices", null); + if (verticesValue == null) deform = weighted ? Utils.newFloatArray(deformLength) : vertices; + else { + deform = Utils.newFloatArray(deformLength); + const start = this.getValue(valueMap, "offset", 0); + Utils.arrayCopy(verticesValue, 0, deform, start, verticesValue.length); + if (scale != 1) { + for (let i = start, n = i + verticesValue.length; i < n; i++) deform[i] *= scale; + } + if (!weighted) { + for (let i = 0; i < deformLength; i++) deform[i] += vertices[i]; + } + } + + timeline.setFrame(frameIndex, this.getValue(valueMap, "time", 0), deform); + this.readCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[timeline.getFrameCount() - 1]); + } + } + } + } + + // Draw order timeline. + let drawOrderNode = map.drawOrder; + if (drawOrderNode == null) drawOrderNode = map.draworder; + if (drawOrderNode != null) { + const timeline = new DrawOrderTimeline(drawOrderNode.length); + const slotCount = skeletonData.slots.length; + let frameIndex = 0; + for (let j = 0; j < drawOrderNode.length; j++) { + const drawOrderMap = drawOrderNode[j]; + let drawOrder: Array = null; + const offsets = this.getValue(drawOrderMap, "offsets", null); + if (offsets != null) { + drawOrder = Utils.newArray(slotCount, -1); + const unchanged = Utils.newArray(slotCount - offsets.length, 0); + let originalIndex = 0, + unchangedIndex = 0; + for (let i = 0; i < offsets.length; i++) { + const offsetMap = offsets[i]; + const slotIndex = skeletonData.findSlotIndex(offsetMap.slot); + if (slotIndex == -1) throw new Error("Slot not found: " + offsetMap.slot); + // Collect unchanged items. + while (originalIndex != slotIndex) unchanged[unchangedIndex++] = originalIndex++; + // Set changed items. + drawOrder[originalIndex + offsetMap.offset] = originalIndex++; + } + // Collect remaining unchanged items. + while (originalIndex < slotCount) unchanged[unchangedIndex++] = originalIndex++; + // Fill in unchanged items. + for (let i = slotCount - 1; i >= 0; i--) if (drawOrder[i] == -1) drawOrder[i] = unchanged[--unchangedIndex]; + } + timeline.setFrame(frameIndex++, this.getValue(drawOrderMap, "time", 0), drawOrder); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[timeline.getFrameCount() - 1]); + } + + // Event timeline. + if (map.events) { + const timeline = new EventTimeline(map.events.length); + let frameIndex = 0; + for (let i = 0; i < map.events.length; i++) { + const eventMap = map.events[i]; + const eventData = skeletonData.findEvent(eventMap.name); + if (eventData == null) throw new Error("Event not found: " + eventMap.name); + const event = new Event(Utils.toSinglePrecision(this.getValue(eventMap, "time", 0)), eventData); + event.intValue = this.getValue(eventMap, "int", eventData.intValue); + event.floatValue = this.getValue(eventMap, "float", eventData.floatValue); + event.stringValue = this.getValue(eventMap, "string", eventData.stringValue); + if (event.data.audioPath != null) { + event.volume = this.getValue(eventMap, "volume", 1); + event.balance = this.getValue(eventMap, "balance", 0); + } + timeline.setFrame(frameIndex++, event); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[timeline.getFrameCount() - 1]); + } + + if (isNaN(duration)) { + throw new Error("Error while parsing animation, duration is NaN"); + } + + skeletonData.animations.push(new Animation(name, timelines, duration)); + } + + readCurve(map: any, timeline: CurveTimeline, frameIndex: number) { + if (!map.hasOwnProperty("curve")) return; + if (map.curve == "stepped") timeline.setStepped(frameIndex); + else { + const curve: number = map.curve; + timeline.setCurve( + frameIndex, + curve, + this.getValue(map, "c2", 0), + this.getValue(map, "c3", 1), + this.getValue(map, "c4", 1) + ); + } + } + + getValue(map: any, prop: string, defaultValue: any) { + return map[prop] !== undefined ? map[prop] : defaultValue; + } + + static blendModeFromString(str: string) { + str = str.toLowerCase(); + if (str == "normal") return BlendMode.Normal; + if (str == "additive") return BlendMode.Additive; + if (str == "multiply") return BlendMode.Multiply; + if (str == "screen") return BlendMode.Screen; + throw new Error(`Unknown blend mode: ${str}`); + } + + static positionModeFromString(str: string) { + str = str.toLowerCase(); + if (str == "fixed") return PositionMode.Fixed; + if (str == "percent") return PositionMode.Percent; + throw new Error(`Unknown position mode: ${str}`); + } + + static spacingModeFromString(str: string) { + str = str.toLowerCase(); + if (str == "length") return SpacingMode.Length; + if (str == "fixed") return SpacingMode.Fixed; + if (str == "percent") return SpacingMode.Percent; + throw new Error(`Unknown position mode: ${str}`); + } + + static rotateModeFromString(str: string) { + str = str.toLowerCase(); + if (str == "tangent") return RotateMode.Tangent; + if (str == "chain") return RotateMode.Chain; + if (str == "chainscale") return RotateMode.ChainScale; + throw new Error(`Unknown rotate mode: ${str}`); + } + + static transformModeFromString(str: string) { + str = str.toLowerCase(); + if (str == "normal") return TransformMode.Normal; + if (str == "onlytranslation") return TransformMode.OnlyTranslation; + if (str == "norotationorreflection") return TransformMode.NoRotationOrReflection; + if (str == "noscale") return TransformMode.NoScale; + if (str == "noscaleorreflection") return TransformMode.NoScaleOrReflection; + throw new Error(`Unknown transform mode: ${str}`); + } +} + +class LinkedMesh { + parent: string; + skin: string; + slotIndex: number; + mesh: MeshAttachment; + inheritDeform: boolean; + + constructor(mesh: MeshAttachment, skin: string, slotIndex: number, parent: string, inheritDeform: boolean) { + this.mesh = mesh; + this.skin = skin; + this.slotIndex = slotIndex; + this.parent = parent; + this.inheritDeform = inheritDeform; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/Skin.ts b/packages/spine-core-3.8/src/spine-core/Skin.ts new file mode 100644 index 0000000000..3081742665 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/Skin.ts @@ -0,0 +1,182 @@ +import { Attachment } from "./attachments/Attachment"; +import { BoneData } from "./BoneData"; +import { ConstraintData } from "./ConstraintData"; +import { Skeleton } from "./Skeleton"; +import { MeshAttachment } from "./attachments/MeshAttachment"; +import { Map } from "./Utils"; + +/** Stores an entry in the skin consisting of the slot index, name, and attachment **/ +export class SkinEntry { + constructor( + public slotIndex: number, + public name: string, + public attachment: Attachment + ) {} +} + +/** Stores attachments by slot index and attachment name. + * + * See SkeletonData {@link SkeletonData#defaultSkin}, Skeleton {@link Skeleton#skin}, and + * [Runtime skins](http://esotericsoftware.com/spine-runtime-skins) in the Spine Runtimes Guide. */ +export class Skin { + /** The skin's name, which is unique across all skins in the skeleton. */ + name: string; + + attachments = new Array>(); + bones = Array(); + constraints = new Array(); + + constructor(name: string) { + if (name == null) throw new Error("name cannot be null."); + this.name = name; + } + + /** Adds an attachment to the skin for the specified slot index and name. */ + setAttachment(slotIndex: number, name: string, attachment: Attachment) { + if (attachment == null) throw new Error("attachment cannot be null."); + const attachments = this.attachments; + if (slotIndex >= attachments.length) attachments.length = slotIndex + 1; + if (!attachments[slotIndex]) attachments[slotIndex] = {}; + attachments[slotIndex][name] = attachment; + } + + /** Adds all attachments, bones, and constraints from the specified skin to this skin. */ + addSkin(skin: Skin) { + for (let i = 0; i < skin.bones.length; i++) { + const bone = skin.bones[i]; + let contained = false; + for (let j = 0; j < this.bones.length; j++) { + if (this.bones[j] == bone) { + contained = true; + break; + } + } + if (!contained) this.bones.push(bone); + } + + for (let i = 0; i < skin.constraints.length; i++) { + const constraint = skin.constraints[i]; + let contained = false; + for (let j = 0; j < this.constraints.length; j++) { + if (this.constraints[j] == constraint) { + contained = true; + break; + } + } + if (!contained) this.constraints.push(constraint); + } + + const attachments = skin.getAttachments(); + for (let i = 0; i < attachments.length; i++) { + const attachment = attachments[i]; + this.setAttachment(attachment.slotIndex, attachment.name, attachment.attachment); + } + } + + /** Adds all bones and constraints and copies of all attachments from the specified skin to this skin. Mesh attachments are not + * copied, instead a new linked mesh is created. The attachment copies can be modified without affecting the originals. */ + copySkin(skin: Skin) { + for (let i = 0; i < skin.bones.length; i++) { + const bone = skin.bones[i]; + let contained = false; + for (let j = 0; j < this.bones.length; j++) { + if (this.bones[j] == bone) { + contained = true; + break; + } + } + if (!contained) this.bones.push(bone); + } + + for (let i = 0; i < skin.constraints.length; i++) { + const constraint = skin.constraints[i]; + let contained = false; + for (let j = 0; j < this.constraints.length; j++) { + if (this.constraints[j] == constraint) { + contained = true; + break; + } + } + if (!contained) this.constraints.push(constraint); + } + + const attachments = skin.getAttachments(); + for (let i = 0; i < attachments.length; i++) { + const attachment = attachments[i]; + if (attachment.attachment == null) continue; + if (attachment.attachment instanceof MeshAttachment) { + attachment.attachment = attachment.attachment.newLinkedMesh(); + this.setAttachment(attachment.slotIndex, attachment.name, attachment.attachment); + } else { + attachment.attachment = attachment.attachment.copy(); + this.setAttachment(attachment.slotIndex, attachment.name, attachment.attachment); + } + } + } + + /** Returns the attachment for the specified slot index and name, or null. */ + getAttachment(slotIndex: number, name: string): Attachment { + const dictionary = this.attachments[slotIndex]; + return dictionary ? dictionary[name] : null; + } + + /** Removes the attachment in the skin for the specified slot index and name, if any. */ + removeAttachment(slotIndex: number, name: string) { + const dictionary = this.attachments[slotIndex]; + if (dictionary) dictionary[name] = null; + } + + /** Returns all attachments in this skin. */ + getAttachments(): Array { + const entries = new Array(); + for (let i = 0; i < this.attachments.length; i++) { + const slotAttachments = this.attachments[i]; + if (slotAttachments) { + for (const name in slotAttachments) { + const attachment = slotAttachments[name]; + if (attachment) entries.push(new SkinEntry(i, name, attachment)); + } + } + } + return entries; + } + + /** Returns all attachments in this skin for the specified slot index. */ + getAttachmentsForSlot(slotIndex: number, attachments: Array) { + const slotAttachments = this.attachments[slotIndex]; + if (slotAttachments) { + for (const name in slotAttachments) { + const attachment = slotAttachments[name]; + if (attachment) attachments.push(new SkinEntry(slotIndex, name, attachment)); + } + } + } + + /** Clears all attachments, bones, and constraints. */ + clear() { + this.attachments.length = 0; + this.bones.length = 0; + this.constraints.length = 0; + } + + /** Attach each attachment in this skin if the corresponding attachment in the old skin is currently attached. */ + attachAll(skeleton: Skeleton, oldSkin: Skin) { + let slotIndex = 0; + for (let i = 0; i < skeleton.slots.length; i++) { + const slot = skeleton.slots[i]; + const slotAttachment = slot.getAttachment(); + if (slotAttachment && slotIndex < oldSkin.attachments.length) { + const dictionary = oldSkin.attachments[slotIndex]; + for (const key in dictionary) { + const skinAttachment: Attachment = dictionary[key]; + if (slotAttachment == skinAttachment) { + const attachment = this.getAttachment(slotIndex, key); + if (attachment != null) slot.setAttachment(attachment); + break; + } + } + } + slotIndex++; + } + } +} diff --git a/packages/spine-core-3.8/src/spine-core/Slot.ts b/packages/spine-core-3.8/src/spine-core/Slot.ts new file mode 100644 index 0000000000..5e4903193e --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/Slot.ts @@ -0,0 +1,86 @@ +import { SlotData } from "./SlotData"; +import { Bone } from "./Bone"; +import { Color } from "./Utils"; +import { Attachment } from "./attachments/Attachment"; +import { Skeleton } from "./Skeleton"; + +/** Stores a slot's current pose. Slots organize attachments for {@link Skeleton#drawOrder} purposes and provide a place to store + * state for an attachment. State cannot be stored in an attachment itself because attachments are stateless and may be shared + * across multiple skeletons. */ +export class Slot { + /** The slot's setup pose data. */ + data: SlotData; + + /** The bone this slot belongs to. */ + bone: Bone; + + /** The color used to tint the slot's attachment. If {@link #getDarkColor()} is set, this is used as the light color for two + * color tinting. */ + color: Color; + + /** The dark color used to tint the slot's attachment for two color tinting, or null if two color tinting is not used. The dark + * color's alpha is not used. */ + darkColor: Color; + + attachment: Attachment; + + private attachmentTime: number; + + attachmentState: number; + + /** Values to deform the slot's attachment. For an unweighted mesh, the entries are local positions for each vertex. For a + * weighted mesh, the entries are an offset for each vertex which will be added to the mesh's local vertex positions. + * + * See {@link VertexAttachment#computeWorldVertices()} and {@link DeformTimeline}. */ + deform = new Array(); + + constructor(data: SlotData, bone: Bone) { + if (data == null) throw new Error("data cannot be null."); + if (bone == null) throw new Error("bone cannot be null."); + this.data = data; + this.bone = bone; + this.color = new Color(); + this.darkColor = data.darkColor == null ? null : new Color(); + this.setToSetupPose(); + } + + /** The skeleton this slot belongs to. */ + getSkeleton(): Skeleton { + return this.bone.skeleton; + } + + /** The current attachment for the slot, or null if the slot has no attachment. */ + getAttachment(): Attachment { + return this.attachment; + } + + /** Sets the slot's attachment and, if the attachment changed, resets {@link #attachmentTime} and clears {@link #deform}. + * @param attachment May be null. */ + setAttachment(attachment: Attachment) { + if (this.attachment == attachment) return; + this.attachment = attachment; + this.attachmentTime = this.bone.skeleton.time; + this.deform.length = 0; + } + + setAttachmentTime(time: number) { + this.attachmentTime = this.bone.skeleton.time - time; + } + + /** The time that has elapsed since the last time the attachment was set or cleared. Relies on Skeleton + * {@link Skeleton#time}. */ + getAttachmentTime(): number { + return this.bone.skeleton.time - this.attachmentTime; + } + + /** Sets this slot to the setup pose. */ + setToSetupPose() { + this.color.setFromColor(this.data.color); + if (this.darkColor != null) this.darkColor.setFromColor(this.data.darkColor); + if (this.data.attachmentName == null) this.attachment = null; + else { + this.attachment = null; + this.setAttachment(this.bone.skeleton.getAttachment(this.data.index, this.data.attachmentName)); + } + } +} diff --git a/packages/spine-core-3.8/src/spine-core/SlotData.ts b/packages/spine-core-3.8/src/spine-core/SlotData.ts new file mode 100644 index 0000000000..c5d3705b79 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/SlotData.ts @@ -0,0 +1,38 @@ +import { BoneData } from "./BoneData"; +import { Color } from "./Utils"; +import { BlendMode } from "./BlendMode"; + +/** Stores the setup pose for a {@link Slot}. */ +export class SlotData { + /** The index of the slot in {@link Skeleton#getSlots()}. */ + index: number; + + /** The name of the slot, which is unique across all slots in the skeleton. */ + name: string; + + /** The bone this slot belongs to. */ + boneData: BoneData; + + /** The color used to tint the slot's attachment. If {@link #getDarkColor()} is set, this is used as the light color for two + * color tinting. */ + color = new Color(1, 1, 1, 1); + + /** The dark color used to tint the slot's attachment for two color tinting, or null if two color tinting is not used. The dark + * color's alpha is not used. */ + darkColor: Color; + + /** The name of the attachment that is visible for this slot in the setup pose, or null if no attachment is visible. */ + attachmentName: string; + + /** The blend mode for drawing the slot's attachment. */ + blendMode: BlendMode; + + constructor(index: number, name: string, boneData: BoneData) { + if (index < 0) throw new Error("index must be >= 0."); + if (name == null) throw new Error("name cannot be null."); + if (boneData == null) throw new Error("boneData cannot be null."); + this.index = index; + this.name = name; + this.boneData = boneData; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/Texture.ts b/packages/spine-core-3.8/src/spine-core/Texture.ts new file mode 100644 index 0000000000..f911a5625c --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/Texture.ts @@ -0,0 +1,96 @@ +import { Engine, Texture2D } from "@galacean/engine"; + +export abstract class Texture { + protected _texture: Texture2D; + + constructor(texture: Texture2D) { + this._texture = texture; + } + + get width(): number { + return this._texture.width; + } + + get height(): number { + return this._texture.height; + } + + get texture(): Texture2D { + return this._texture; + } + + abstract setFilters(minFilter: TextureFilter, magFilter: TextureFilter): void; + abstract setWraps(uWrap: TextureWrap, vWrap: TextureWrap): void; + abstract dispose(): void; + + public static filterFromString(text: string): TextureFilter { + switch (text.toLowerCase()) { + case "nearest": + return TextureFilter.Nearest; + case "linear": + return TextureFilter.Linear; + case "mipmap": + return TextureFilter.MipMap; + case "mipmapnearestnearest": + return TextureFilter.MipMapNearestNearest; + case "mipmaplinearnearest": + return TextureFilter.MipMapLinearNearest; + case "mipmapnearestlinear": + return TextureFilter.MipMapNearestLinear; + case "mipmaplinearlinear": + return TextureFilter.MipMapLinearLinear; + default: + throw new Error(`Unknown texture filter ${text}`); + } + } + + public static wrapFromString(text: string): TextureWrap { + switch (text.toLowerCase()) { + case "mirroredtepeat": + return TextureWrap.MirroredRepeat; + case "clamptoedge": + return TextureWrap.ClampToEdge; + case "repeat": + return TextureWrap.Repeat; + default: + throw new Error(`Unknown texture wrap ${text}`); + } + } +} + +export enum TextureFilter { + Nearest = 9728, // WebGLRenderingContext.NEAREST + Linear = 9729, // WebGLRenderingContext.LINEAR + MipMap = 9987, // WebGLRenderingContext.LINEAR_MIPMAP_LINEAR + MipMapNearestNearest = 9984, // WebGLRenderingContext.NEAREST_MIPMAP_NEAREST + MipMapLinearNearest = 9985, // WebGLRenderingContext.LINEAR_MIPMAP_NEAREST + MipMapNearestLinear = 9986, // WebGLRenderingContext.NEAREST_MIPMAP_LINEAR + MipMapLinearLinear = 9987 // WebGLRenderingContext.LINEAR_MIPMAP_LINEAR +} + +export enum TextureWrap { + MirroredRepeat = 33648, // WebGLRenderingContext.MIRRORED_REPEAT + ClampToEdge = 33071, // WebGLRenderingContext.CLAMP_TO_EDGE + Repeat = 10497 // WebGLRenderingContext.REPEAT +} + +export class TextureRegion { + renderObject: any; + u = 0; + v = 0; + u2 = 0; + v2 = 0; + width = 0; + height = 0; + rotate = false; + offsetX = 0; + offsetY = 0; + originalWidth = 0; + originalHeight = 0; +} + +export class FakeTexture extends Texture { + setFilters(minFilter: TextureFilter, magFilter: TextureFilter) {} + setWraps(uWrap: TextureWrap, vWrap: TextureWrap) {} + dispose() {} +} diff --git a/packages/spine-core-3.8/src/spine-core/TextureAtlas.ts b/packages/spine-core-3.8/src/spine-core/TextureAtlas.ts new file mode 100644 index 0000000000..299e9f94be --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/TextureAtlas.ts @@ -0,0 +1,187 @@ +import { Disposable } from "./Utils"; +import { Texture, TextureFilter } from "./Texture"; +import { TextureWrap, TextureRegion } from "./Texture"; + +export class TextureAtlas implements Disposable { + pages = new Array(); + regions = new Array(); + + constructor(atlasText: string, textureLoader: (path: string, width?: number, height?: number) => any) { + this.load(atlasText, textureLoader); + } + + private load(atlasText: string, textureLoader: (path: string, width?: number, height?: number) => any) { + if (textureLoader == null) throw new Error("textureLoader cannot be null."); + + const reader = new TextureAtlasReader(atlasText); + const tuple = new Array(4); + let page: TextureAtlasPage = null; + while (true) { + let line = reader.readLine(); + if (line == null) break; + line = line.trim(); + if (line.length == 0) page = null; + else if (!page) { + page = new TextureAtlasPage(); + page.name = line; + + if (reader.readTuple(tuple) == 2) { + // size is only optional for an atlas packed with an old TexturePacker. + page.width = parseInt(tuple[0]); + page.height = parseInt(tuple[1]); + reader.readTuple(tuple); + } + // page.format = Format[tuple[0]]; we don't need format in WebGL + + reader.readTuple(tuple); + page.minFilter = Texture.filterFromString(tuple[0]); + page.magFilter = Texture.filterFromString(tuple[1]); + + const direction = reader.readValue(); + page.uWrap = TextureWrap.ClampToEdge; + page.vWrap = TextureWrap.ClampToEdge; + if (direction == "x") page.uWrap = TextureWrap.Repeat; + else if (direction == "y") page.vWrap = TextureWrap.Repeat; + else if (direction == "xy") page.uWrap = page.vWrap = TextureWrap.Repeat; + + page.texture = textureLoader(line); + page.texture.setFilters(page.minFilter, page.magFilter); + page.texture.setWraps(page.uWrap, page.vWrap); + page.width = page.texture.width; + page.height = page.texture.height; + this.pages.push(page); + } else { + const region: TextureAtlasRegion = new TextureAtlasRegion(); + region.name = line; + region.page = page; + + const rotateValue = reader.readValue(); + if (rotateValue.toLocaleLowerCase() == "true") { + region.degrees = 90; + } else if (rotateValue.toLocaleLowerCase() == "false") { + region.degrees = 0; + } else { + region.degrees = parseFloat(rotateValue); + } + region.rotate = region.degrees == 90; + + reader.readTuple(tuple); + const x = parseInt(tuple[0]); + const y = parseInt(tuple[1]); + + reader.readTuple(tuple); + const width = parseInt(tuple[0]); + const height = parseInt(tuple[1]); + + region.u = x / page.width; + region.v = y / page.height; + if (region.rotate) { + region.u2 = (x + height) / page.width; + region.v2 = (y + width) / page.height; + } else { + region.u2 = (x + width) / page.width; + region.v2 = (y + height) / page.height; + } + region.x = x; + region.y = y; + region.width = Math.abs(width); + region.height = Math.abs(height); + + if (reader.readTuple(tuple) == 4) { + // split is optional + // region.splits = new Vector.(parseInt(tuple[0]), parseInt(tuple[1]), parseInt(tuple[2]), parseInt(tuple[3])); + if (reader.readTuple(tuple) == 4) { + // pad is optional, but only present with splits + //region.pads = Vector.(parseInt(tuple[0]), parseInt(tuple[1]), parseInt(tuple[2]), parseInt(tuple[3])); + reader.readTuple(tuple); + } + } + + region.originalWidth = parseInt(tuple[0]); + region.originalHeight = parseInt(tuple[1]); + + reader.readTuple(tuple); + region.offsetX = parseInt(tuple[0]); + region.offsetY = parseInt(tuple[1]); + + region.index = parseInt(reader.readValue()); + + region.texture = page.texture; + this.regions.push(region); + } + } + } + + findRegion(name: string): TextureAtlasRegion { + for (let i = 0; i < this.regions.length; i++) { + if (this.regions[i].name == name) { + return this.regions[i]; + } + } + return null; + } + + dispose() { + for (let i = 0; i < this.pages.length; i++) { + this.pages[i].texture.dispose(); + } + } +} + +class TextureAtlasReader { + lines: Array; + index: number = 0; + + constructor(text: string) { + this.lines = text.split(/\r\n|\r|\n/); + } + + readLine(): string { + if (this.index >= this.lines.length) return null; + return this.lines[this.index++]; + } + + readValue(): string { + const line = this.readLine(); + const colon = line.indexOf(":"); + if (colon == -1) throw new Error("Invalid line: " + line); + return line.substring(colon + 1).trim(); + } + + readTuple(tuple: Array): number { + const line = this.readLine(); + const colon = line.indexOf(":"); + if (colon == -1) throw new Error("Invalid line: " + line); + let i = 0, + lastMatch = colon + 1; + for (; i < 3; i++) { + const comma = line.indexOf(",", lastMatch); + if (comma == -1) break; + tuple[i] = line.substr(lastMatch, comma - lastMatch).trim(); + lastMatch = comma + 1; + } + tuple[i] = line.substring(lastMatch).trim(); + return i + 1; + } +} + +export class TextureAtlasPage { + name: string; + minFilter: TextureFilter; + magFilter: TextureFilter; + uWrap: TextureWrap; + vWrap: TextureWrap; + texture: Texture; + width: number; + height: number; +} + +export class TextureAtlasRegion extends TextureRegion { + page: TextureAtlasPage; + name: string; + x: number; + y: number; + index: number; + degrees: number; + texture: Texture; +} diff --git a/packages/spine-core-3.8/src/spine-core/TransformConstraint.ts b/packages/spine-core-3.8/src/spine-core/TransformConstraint.ts new file mode 100644 index 0000000000..ab841eb62e --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/TransformConstraint.ts @@ -0,0 +1,296 @@ +import { Updatable } from "./Updatable"; +import { TransformConstraintData } from "./TransformConstraintData"; +import { Bone } from "./Bone"; +import { Skeleton } from "./Skeleton"; +import { MathUtils, Vector2 } from "./Utils"; + +/** Stores the current pose for a transform constraint. A transform constraint adjusts the world transform of the constrained + * bones to match that of the target bone. + * + * See [Transform constraints](http://esotericsoftware.com/spine-transform-constraints) in the Spine User Guide. */ +export class TransformConstraint implements Updatable { + /** The transform constraint's setup pose data. */ + data: TransformConstraintData; + + /** The bones that will be modified by this transform constraint. */ + bones: Array; + + /** The target bone whose world transform will be copied to the constrained bones. */ + target: Bone; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained rotations. */ + rotateMix = 0; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained translations. */ + translateMix = 0; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained scales. */ + scaleMix = 0; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained scales. */ + shearMix = 0; + + temp = new Vector2(); + active = false; + + constructor(data: TransformConstraintData, skeleton: Skeleton) { + if (data == null) throw new Error("data cannot be null."); + if (skeleton == null) throw new Error("skeleton cannot be null."); + this.data = data; + this.rotateMix = data.rotateMix; + this.translateMix = data.translateMix; + this.scaleMix = data.scaleMix; + this.shearMix = data.shearMix; + this.bones = new Array(); + for (let i = 0; i < data.bones.length; i++) this.bones.push(skeleton.findBone(data.bones[i].name)); + this.target = skeleton.findBone(data.target.name); + } + + isActive() { + return this.active; + } + + /** Applies the constraint to the constrained bones. */ + apply() { + this.update(); + } + + update() { + if (this.data.local) { + if (this.data.relative) this.applyRelativeLocal(); + else this.applyAbsoluteLocal(); + } else { + if (this.data.relative) this.applyRelativeWorld(); + else this.applyAbsoluteWorld(); + } + } + + applyAbsoluteWorld() { + const rotateMix = this.rotateMix, + translateMix = this.translateMix, + scaleMix = this.scaleMix, + shearMix = this.shearMix; + const target = this.target; + const ta = target.a, + tb = target.b, + tc = target.c, + td = target.d; + const degRadReflect = ta * td - tb * tc > 0 ? MathUtils.degRad : -MathUtils.degRad; + const offsetRotation = this.data.offsetRotation * degRadReflect; + const offsetShearY = this.data.offsetShearY * degRadReflect; + const bones = this.bones; + for (let i = 0, n = bones.length; i < n; i++) { + const bone = bones[i]; + let modified = false; + + if (rotateMix != 0) { + const a = bone.a, + b = bone.b, + c = bone.c, + d = bone.d; + let r = Math.atan2(tc, ta) - Math.atan2(c, a) + offsetRotation; + if (r > MathUtils.PI) r -= MathUtils.PI2; + else if (r < -MathUtils.PI) r += MathUtils.PI2; + r *= rotateMix; + const cos = Math.cos(r), + sin = Math.sin(r); + bone.a = cos * a - sin * c; + bone.b = cos * b - sin * d; + bone.c = sin * a + cos * c; + bone.d = sin * b + cos * d; + modified = true; + } + + if (translateMix != 0) { + const temp = this.temp; + target.localToWorld(temp.set(this.data.offsetX, this.data.offsetY)); + bone.worldX += (temp.x - bone.worldX) * translateMix; + bone.worldY += (temp.y - bone.worldY) * translateMix; + modified = true; + } + + if (scaleMix > 0) { + let s = Math.sqrt(bone.a * bone.a + bone.c * bone.c); + let ts = Math.sqrt(ta * ta + tc * tc); + if (s > 0.00001) s = (s + (ts - s + this.data.offsetScaleX) * scaleMix) / s; + bone.a *= s; + bone.c *= s; + s = Math.sqrt(bone.b * bone.b + bone.d * bone.d); + ts = Math.sqrt(tb * tb + td * td); + if (s > 0.00001) s = (s + (ts - s + this.data.offsetScaleY) * scaleMix) / s; + bone.b *= s; + bone.d *= s; + modified = true; + } + + if (shearMix > 0) { + const b = bone.b, + d = bone.d; + const by = Math.atan2(d, b); + let r = Math.atan2(td, tb) - Math.atan2(tc, ta) - (by - Math.atan2(bone.c, bone.a)); + if (r > MathUtils.PI) r -= MathUtils.PI2; + else if (r < -MathUtils.PI) r += MathUtils.PI2; + r = by + (r + offsetShearY) * shearMix; + const s = Math.sqrt(b * b + d * d); + bone.b = Math.cos(r) * s; + bone.d = Math.sin(r) * s; + modified = true; + } + + if (modified) bone.appliedValid = false; + } + } + + applyRelativeWorld() { + const rotateMix = this.rotateMix, + translateMix = this.translateMix, + scaleMix = this.scaleMix, + shearMix = this.shearMix; + const target = this.target; + const ta = target.a, + tb = target.b, + tc = target.c, + td = target.d; + const degRadReflect = ta * td - tb * tc > 0 ? MathUtils.degRad : -MathUtils.degRad; + const offsetRotation = this.data.offsetRotation * degRadReflect, + offsetShearY = this.data.offsetShearY * degRadReflect; + const bones = this.bones; + for (let i = 0, n = bones.length; i < n; i++) { + const bone = bones[i]; + let modified = false; + + if (rotateMix != 0) { + const a = bone.a, + b = bone.b, + c = bone.c, + d = bone.d; + let r = Math.atan2(tc, ta) + offsetRotation; + if (r > MathUtils.PI) r -= MathUtils.PI2; + else if (r < -MathUtils.PI) r += MathUtils.PI2; + r *= rotateMix; + const cos = Math.cos(r), + sin = Math.sin(r); + bone.a = cos * a - sin * c; + bone.b = cos * b - sin * d; + bone.c = sin * a + cos * c; + bone.d = sin * b + cos * d; + modified = true; + } + + if (translateMix != 0) { + const temp = this.temp; + target.localToWorld(temp.set(this.data.offsetX, this.data.offsetY)); + bone.worldX += temp.x * translateMix; + bone.worldY += temp.y * translateMix; + modified = true; + } + + if (scaleMix > 0) { + let s = (Math.sqrt(ta * ta + tc * tc) - 1 + this.data.offsetScaleX) * scaleMix + 1; + bone.a *= s; + bone.c *= s; + s = (Math.sqrt(tb * tb + td * td) - 1 + this.data.offsetScaleY) * scaleMix + 1; + bone.b *= s; + bone.d *= s; + modified = true; + } + + if (shearMix > 0) { + let r = Math.atan2(td, tb) - Math.atan2(tc, ta); + if (r > MathUtils.PI) r -= MathUtils.PI2; + else if (r < -MathUtils.PI) r += MathUtils.PI2; + const b = bone.b, + d = bone.d; + r = Math.atan2(d, b) + (r - MathUtils.PI / 2 + offsetShearY) * shearMix; + const s = Math.sqrt(b * b + d * d); + bone.b = Math.cos(r) * s; + bone.d = Math.sin(r) * s; + modified = true; + } + + if (modified) bone.appliedValid = false; + } + } + + applyAbsoluteLocal() { + const rotateMix = this.rotateMix, + translateMix = this.translateMix, + scaleMix = this.scaleMix, + shearMix = this.shearMix; + const target = this.target; + if (!target.appliedValid) target.updateAppliedTransform(); + const bones = this.bones; + for (let i = 0, n = bones.length; i < n; i++) { + const bone = bones[i]; + if (!bone.appliedValid) bone.updateAppliedTransform(); + + let rotation = bone.arotation; + if (rotateMix != 0) { + let r = target.arotation - rotation + this.data.offsetRotation; + r -= (16384 - ((16384.499999999996 - r / 360) | 0)) * 360; + rotation += r * rotateMix; + } + + let x = bone.ax, + y = bone.ay; + if (translateMix != 0) { + x += (target.ax - x + this.data.offsetX) * translateMix; + y += (target.ay - y + this.data.offsetY) * translateMix; + } + + let scaleX = bone.ascaleX, + scaleY = bone.ascaleY; + if (scaleMix != 0) { + if (scaleX > 0.00001) + scaleX = (scaleX + (target.ascaleX - scaleX + this.data.offsetScaleX) * scaleMix) / scaleX; + if (scaleY > 0.00001) + scaleY = (scaleY + (target.ascaleY - scaleY + this.data.offsetScaleY) * scaleMix) / scaleY; + } + + const shearY = bone.ashearY; + if (shearMix != 0) { + let r = target.ashearY - shearY + this.data.offsetShearY; + r -= (16384 - ((16384.499999999996 - r / 360) | 0)) * 360; + bone.shearY += r * shearMix; + } + + bone.updateWorldTransformWith(x, y, rotation, scaleX, scaleY, bone.ashearX, shearY); + } + } + + applyRelativeLocal() { + const rotateMix = this.rotateMix, + translateMix = this.translateMix, + scaleMix = this.scaleMix, + shearMix = this.shearMix; + const target = this.target; + if (!target.appliedValid) target.updateAppliedTransform(); + const bones = this.bones; + for (let i = 0, n = bones.length; i < n; i++) { + const bone = bones[i]; + if (!bone.appliedValid) bone.updateAppliedTransform(); + + let rotation = bone.arotation; + if (rotateMix != 0) rotation += (target.arotation + this.data.offsetRotation) * rotateMix; + + let x = bone.ax, + y = bone.ay; + if (translateMix != 0) { + x += (target.ax + this.data.offsetX) * translateMix; + y += (target.ay + this.data.offsetY) * translateMix; + } + + let scaleX = bone.ascaleX, + scaleY = bone.ascaleY; + if (scaleMix != 0) { + if (scaleX > 0.00001) scaleX *= (target.ascaleX - 1 + this.data.offsetScaleX) * scaleMix + 1; + if (scaleY > 0.00001) scaleY *= (target.ascaleY - 1 + this.data.offsetScaleY) * scaleMix + 1; + } + + let shearY = bone.ashearY; + if (shearMix != 0) shearY += (target.ashearY + this.data.offsetShearY) * shearMix; + + bone.updateWorldTransformWith(x, y, rotation, scaleX, scaleY, bone.ashearX, shearY); + } + } +} diff --git a/packages/spine-core-3.8/src/spine-core/TransformConstraintData.ts b/packages/spine-core-3.8/src/spine-core/TransformConstraintData.ts new file mode 100644 index 0000000000..c135697c08 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/TransformConstraintData.ts @@ -0,0 +1,50 @@ +import { ConstraintData } from "./ConstraintData"; +import { BoneData } from "./BoneData"; + +/** Stores the setup pose for a {@link TransformConstraint}. + * + * See [Transform constraints](http://esotericsoftware.com/spine-transform-constraints) in the Spine User Guide. */ +export class TransformConstraintData extends ConstraintData { + /** The bones that will be modified by this transform constraint. */ + bones = new Array(); + + /** The target bone whose world transform will be copied to the constrained bones. */ + target: BoneData; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained rotations. */ + rotateMix = 0; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained translations. */ + translateMix = 0; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained scales. */ + scaleMix = 0; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained shears. */ + shearMix = 0; + + /** An offset added to the constrained bone rotation. */ + offsetRotation = 0; + + /** An offset added to the constrained bone X translation. */ + offsetX = 0; + + /** An offset added to the constrained bone Y translation. */ + offsetY = 0; + + /** An offset added to the constrained bone scaleX. */ + offsetScaleX = 0; + + /** An offset added to the constrained bone scaleY. */ + offsetScaleY = 0; + + /** An offset added to the constrained bone shearY. */ + offsetShearY = 0; + + relative = false; + local = false; + + constructor(name: string) { + super(name, 0, false); + } +} diff --git a/packages/spine-core-3.8/src/spine-core/Triangulator.ts b/packages/spine-core-3.8/src/spine-core/Triangulator.ts new file mode 100644 index 0000000000..bd6efff41d --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/Triangulator.ts @@ -0,0 +1,269 @@ +import { Pool } from "./Utils"; + +export class Triangulator { + private convexPolygons = new Array>(); + private convexPolygonsIndices = new Array>(); + + private indicesArray = new Array(); + private isConcaveArray = new Array(); + private triangles = new Array(); + + private polygonPool = new Pool>(() => { + return new Array(); + }); + + private polygonIndicesPool = new Pool>(() => { + return new Array(); + }); + + public triangulate(verticesArray: ArrayLike): Array { + const vertices = verticesArray; + let vertexCount = verticesArray.length >> 1; + + const indices = this.indicesArray; + indices.length = 0; + for (let i = 0; i < vertexCount; i++) indices[i] = i; + + const isConcave = this.isConcaveArray; + isConcave.length = 0; + for (let i = 0, n = vertexCount; i < n; ++i) + isConcave[i] = Triangulator.isConcave(i, vertexCount, vertices, indices); + + const triangles = this.triangles; + triangles.length = 0; + + while (vertexCount > 3) { + // Find ear tip. + let previous = vertexCount - 1, + i = 0, + next = 1; + while (true) { + outer: if (!isConcave[i]) { + const p1 = indices[previous] << 1, + p2 = indices[i] << 1, + p3 = indices[next] << 1; + const p1x = vertices[p1], + p1y = vertices[p1 + 1]; + const p2x = vertices[p2], + p2y = vertices[p2 + 1]; + const p3x = vertices[p3], + p3y = vertices[p3 + 1]; + for (let ii = (next + 1) % vertexCount; ii != previous; ii = (ii + 1) % vertexCount) { + if (!isConcave[ii]) continue; + const v = indices[ii] << 1; + const vx = vertices[v], + vy = vertices[v + 1]; + if (Triangulator.positiveArea(p3x, p3y, p1x, p1y, vx, vy)) { + if (Triangulator.positiveArea(p1x, p1y, p2x, p2y, vx, vy)) { + if (Triangulator.positiveArea(p2x, p2y, p3x, p3y, vx, vy)) break outer; + } + } + } + break; + } + + if (next == 0) { + do { + if (!isConcave[i]) break; + i--; + } while (i > 0); + break; + } + + previous = i; + i = next; + next = (next + 1) % vertexCount; + } + + // Cut ear tip. + triangles.push(indices[(vertexCount + i - 1) % vertexCount]); + triangles.push(indices[i]); + triangles.push(indices[(i + 1) % vertexCount]); + indices.splice(i, 1); + isConcave.splice(i, 1); + vertexCount--; + + const previousIndex = (vertexCount + i - 1) % vertexCount; + const nextIndex = i == vertexCount ? 0 : i; + isConcave[previousIndex] = Triangulator.isConcave(previousIndex, vertexCount, vertices, indices); + isConcave[nextIndex] = Triangulator.isConcave(nextIndex, vertexCount, vertices, indices); + } + + if (vertexCount == 3) { + triangles.push(indices[2]); + triangles.push(indices[0]); + triangles.push(indices[1]); + } + + return triangles; + } + + decompose(verticesArray: Array, triangles: Array): Array> { + const vertices = verticesArray; + const convexPolygons = this.convexPolygons; + this.polygonPool.freeAll(convexPolygons); + convexPolygons.length = 0; + + const convexPolygonsIndices = this.convexPolygonsIndices; + this.polygonIndicesPool.freeAll(convexPolygonsIndices); + convexPolygonsIndices.length = 0; + + let polygonIndices = this.polygonIndicesPool.obtain(); + polygonIndices.length = 0; + + let polygon = this.polygonPool.obtain(); + polygon.length = 0; + + // Merge subsequent triangles if they form a triangle fan. + let fanBaseIndex = -1, + lastWinding = 0; + for (let i = 0, n = triangles.length; i < n; i += 3) { + const t1 = triangles[i] << 1, + t2 = triangles[i + 1] << 1, + t3 = triangles[i + 2] << 1; + const x1 = vertices[t1], + y1 = vertices[t1 + 1]; + const x2 = vertices[t2], + y2 = vertices[t2 + 1]; + const x3 = vertices[t3], + y3 = vertices[t3 + 1]; + + // If the base of the last triangle is the same as this triangle, check if they form a convex polygon (triangle fan). + let merged = false; + if (fanBaseIndex == t1) { + const o = polygon.length - 4; + const winding1 = Triangulator.winding(polygon[o], polygon[o + 1], polygon[o + 2], polygon[o + 3], x3, y3); + const winding2 = Triangulator.winding(x3, y3, polygon[0], polygon[1], polygon[2], polygon[3]); + if (winding1 == lastWinding && winding2 == lastWinding) { + polygon.push(x3); + polygon.push(y3); + polygonIndices.push(t3); + merged = true; + } + } + + // Otherwise make this triangle the new base. + if (!merged) { + if (polygon.length > 0) { + convexPolygons.push(polygon); + convexPolygonsIndices.push(polygonIndices); + } else { + this.polygonPool.free(polygon); + this.polygonIndicesPool.free(polygonIndices); + } + polygon = this.polygonPool.obtain(); + polygon.length = 0; + polygon.push(x1); + polygon.push(y1); + polygon.push(x2); + polygon.push(y2); + polygon.push(x3); + polygon.push(y3); + polygonIndices = this.polygonIndicesPool.obtain(); + polygonIndices.length = 0; + polygonIndices.push(t1); + polygonIndices.push(t2); + polygonIndices.push(t3); + lastWinding = Triangulator.winding(x1, y1, x2, y2, x3, y3); + fanBaseIndex = t1; + } + } + + if (polygon.length > 0) { + convexPolygons.push(polygon); + convexPolygonsIndices.push(polygonIndices); + } + + // Go through the list of polygons and try to merge the remaining triangles with the found triangle fans. + for (let i = 0, n = convexPolygons.length; i < n; i++) { + polygonIndices = convexPolygonsIndices[i]; + if (polygonIndices.length == 0) continue; + const firstIndex = polygonIndices[0]; + const lastIndex = polygonIndices[polygonIndices.length - 1]; + + polygon = convexPolygons[i]; + const o = polygon.length - 4; + let prevPrevX = polygon[o], + prevPrevY = polygon[o + 1]; + let prevX = polygon[o + 2], + prevY = polygon[o + 3]; + const firstX = polygon[0], + firstY = polygon[1]; + const secondX = polygon[2], + secondY = polygon[3]; + const winding = Triangulator.winding(prevPrevX, prevPrevY, prevX, prevY, firstX, firstY); + + for (let ii = 0; ii < n; ii++) { + if (ii == i) continue; + const otherIndices = convexPolygonsIndices[ii]; + if (otherIndices.length != 3) continue; + const otherFirstIndex = otherIndices[0]; + const otherSecondIndex = otherIndices[1]; + const otherLastIndex = otherIndices[2]; + + const otherPoly = convexPolygons[ii]; + const x3 = otherPoly[otherPoly.length - 2], + y3 = otherPoly[otherPoly.length - 1]; + + if (otherFirstIndex != firstIndex || otherSecondIndex != lastIndex) continue; + const winding1 = Triangulator.winding(prevPrevX, prevPrevY, prevX, prevY, x3, y3); + const winding2 = Triangulator.winding(x3, y3, firstX, firstY, secondX, secondY); + if (winding1 == winding && winding2 == winding) { + otherPoly.length = 0; + otherIndices.length = 0; + polygon.push(x3); + polygon.push(y3); + polygonIndices.push(otherLastIndex); + prevPrevX = prevX; + prevPrevY = prevY; + prevX = x3; + prevY = y3; + ii = 0; + } + } + } + + // Remove empty polygons that resulted from the merge step above. + for (let i = convexPolygons.length - 1; i >= 0; i--) { + polygon = convexPolygons[i]; + if (polygon.length == 0) { + convexPolygons.splice(i, 1); + this.polygonPool.free(polygon); + polygonIndices = convexPolygonsIndices[i]; + convexPolygonsIndices.splice(i, 1); + this.polygonIndicesPool.free(polygonIndices); + } + } + + return convexPolygons; + } + + private static isConcave( + index: number, + vertexCount: number, + vertices: ArrayLike, + indices: ArrayLike + ): boolean { + const previous = indices[(vertexCount + index - 1) % vertexCount] << 1; + const current = indices[index] << 1; + const next = indices[(index + 1) % vertexCount] << 1; + return !this.positiveArea( + vertices[previous], + vertices[previous + 1], + vertices[current], + vertices[current + 1], + vertices[next], + vertices[next + 1] + ); + } + + private static positiveArea(p1x: number, p1y: number, p2x: number, p2y: number, p3x: number, p3y: number): boolean { + return p1x * (p3y - p2y) + p2x * (p1y - p3y) + p3x * (p2y - p1y) >= 0; + } + + private static winding(p1x: number, p1y: number, p2x: number, p2y: number, p3x: number, p3y: number): number { + const px = p2x - p1x, + py = p2y - p1y; + return p3x * py - p3y * px + px * p1y - p1x * py >= 0 ? 1 : -1; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/Updatable.ts b/packages/spine-core-3.8/src/spine-core/Updatable.ts new file mode 100644 index 0000000000..64e1965de1 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/Updatable.ts @@ -0,0 +1,10 @@ +/** The interface for items updated by {@link Skeleton#updateWorldTransform()}. */ +export interface Updatable { + update(): void; + + /** Returns false when this item has not been updated because a skin is required and the {@link Skeleton#skin active skin} + * does not contain this item. + * @see Skin#getBones() + * @see Skin#getConstraints() */ + isActive(): boolean; +} diff --git a/packages/spine-core-3.8/src/spine-core/Utils.ts b/packages/spine-core-3.8/src/spine-core/Utils.ts new file mode 100644 index 0000000000..510e4cbba4 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/Utils.ts @@ -0,0 +1,417 @@ +import { MixBlend } from "./Animation"; +import { Skeleton } from "./Skeleton"; + +export interface Map { + [key: string]: T; +} + +export class IntSet { + array = new Array(); + + add(value: number): boolean { + const contains = this.contains(value); + this.array[value | 0] = value | 0; + return !contains; + } + + contains(value: number) { + return this.array[value | 0] != undefined; + } + + remove(value: number) { + this.array[value | 0] = undefined; + } + + clear() { + this.array.length = 0; + } +} + +export interface Disposable { + dispose(): void; +} + +export interface Restorable { + restore(): void; +} + +export class Color { + public static WHITE = new Color(1, 1, 1, 1); + public static RED = new Color(1, 0, 0, 1); + public static GREEN = new Color(0, 1, 0, 1); + public static BLUE = new Color(0, 0, 1, 1); + public static MAGENTA = new Color(1, 0, 1, 1); + + constructor( + public r: number = 0, + public g: number = 0, + public b: number = 0, + public a: number = 0 + ) {} + + set(r: number, g: number, b: number, a: number) { + this.r = r; + this.g = g; + this.b = b; + this.a = a; + this.clamp(); + return this; + } + + setFromColor(c: Color) { + this.r = c.r; + this.g = c.g; + this.b = c.b; + this.a = c.a; + return this; + } + + setFromString(hex: string) { + hex = hex.charAt(0) == "#" ? hex.substr(1) : hex; + this.r = parseInt(hex.substr(0, 2), 16) / 255.0; + this.g = parseInt(hex.substr(2, 2), 16) / 255.0; + this.b = parseInt(hex.substr(4, 2), 16) / 255.0; + this.a = (hex.length != 8 ? 255 : parseInt(hex.substr(6, 2), 16)) / 255.0; + return this; + } + + add(r: number, g: number, b: number, a: number) { + this.r += r; + this.g += g; + this.b += b; + this.a += a; + this.clamp(); + return this; + } + + clamp() { + if (this.r < 0) this.r = 0; + else if (this.r > 1) this.r = 1; + + if (this.g < 0) this.g = 0; + else if (this.g > 1) this.g = 1; + + if (this.b < 0) this.b = 0; + else if (this.b > 1) this.b = 1; + + if (this.a < 0) this.a = 0; + else if (this.a > 1) this.a = 1; + return this; + } + + static rgba8888ToColor(color: Color, value: number) { + color.r = ((value & 0xff000000) >>> 24) / 255; + color.g = ((value & 0x00ff0000) >>> 16) / 255; + color.b = ((value & 0x0000ff00) >>> 8) / 255; + color.a = (value & 0x000000ff) / 255; + } + + static rgb888ToColor(color: Color, value: number) { + color.r = ((value & 0x00ff0000) >>> 16) / 255; + color.g = ((value & 0x0000ff00) >>> 8) / 255; + color.b = (value & 0x000000ff) / 255; + } +} + +export class MathUtils { + static PI = 3.1415927; + static PI2 = MathUtils.PI * 2; + static radiansToDegrees = 180 / MathUtils.PI; + static radDeg = MathUtils.radiansToDegrees; + static degreesToRadians = MathUtils.PI / 180; + static degRad = MathUtils.degreesToRadians; + + static clamp(value: number, min: number, max: number) { + if (value < min) return min; + if (value > max) return max; + return value; + } + + static cosDeg(degrees: number) { + return Math.cos(degrees * MathUtils.degRad); + } + + static sinDeg(degrees: number) { + return Math.sin(degrees * MathUtils.degRad); + } + + static signum(value: number): number { + return value > 0 ? 1 : value < 0 ? -1 : 0; + } + + static toInt(x: number) { + return x > 0 ? Math.floor(x) : Math.ceil(x); + } + + static cbrt(x: number) { + const y = Math.pow(Math.abs(x), 1 / 3); + return x < 0 ? -y : y; + } + + static randomTriangular(min: number, max: number): number { + return MathUtils.randomTriangularWith(min, max, (min + max) * 0.5); + } + + static randomTriangularWith(min: number, max: number, mode: number): number { + const u = Math.random(); + const d = max - min; + if (u <= (mode - min) / d) return min + Math.sqrt(u * d * (mode - min)); + return max - Math.sqrt((1 - u) * d * (max - mode)); + } +} + +export abstract class Interpolation { + protected abstract applyInternal(a: number): number; + apply(start: number, end: number, a: number): number { + return start + (end - start) * this.applyInternal(a); + } +} + +export class Pow extends Interpolation { + protected power = 2; + + constructor(power: number) { + super(); + this.power = power; + } + + applyInternal(a: number): number { + if (a <= 0.5) return Math.pow(a * 2, this.power) / 2; + return Math.pow((a - 1) * 2, this.power) / (this.power % 2 == 0 ? -2 : 2) + 1; + } +} + +export class PowOut extends Pow { + constructor(power: number) { + super(power); + } + + override applyInternal(a: number): number { + return Math.pow(a - 1, this.power) * (this.power % 2 == 0 ? -1 : 1) + 1; + } +} + +export class Utils { + static SUPPORTS_TYPED_ARRAYS = typeof Float32Array !== "undefined"; + + static arrayCopy( + source: ArrayLike, + sourceStart: number, + dest: ArrayLike, + destStart: number, + numElements: number + ) { + for (let i = sourceStart, j = destStart; i < sourceStart + numElements; i++, j++) { + dest[j] = source[i]; + } + } + + static setArraySize(array: Array, size: number, value: any = 0): Array { + const oldSize = array.length; + if (oldSize == size) return array; + array.length = size; + if (oldSize < size) { + for (let i = oldSize; i < size; i++) array[i] = value; + } + return array; + } + + static ensureArrayCapacity(array: Array, size: number, value: any = 0): Array { + if (array.length >= size) return array; + return Utils.setArraySize(array, size, value); + } + + static newArray(size: number, defaultValue: T): Array { + const array = new Array(size); + for (let i = 0; i < size; i++) array[i] = defaultValue; + return array; + } + + static newFloatArray(size: number): ArrayLike { + if (Utils.SUPPORTS_TYPED_ARRAYS) { + return new Float32Array(size); + } else { + const array = new Array(size); + for (let i = 0; i < array.length; i++) array[i] = 0; + return array; + } + } + + static newShortArray(size: number): ArrayLike { + if (Utils.SUPPORTS_TYPED_ARRAYS) { + return new Int16Array(size); + } else { + const array = new Array(size); + for (let i = 0; i < array.length; i++) array[i] = 0; + return array; + } + } + + static toFloatArray(array: Array) { + return Utils.SUPPORTS_TYPED_ARRAYS ? new Float32Array(array) : array; + } + + static toSinglePrecision(value: number) { + return Utils.SUPPORTS_TYPED_ARRAYS ? Math.fround(value) : value; + } + + // This function is used to fix WebKit 602 specific issue described at http://esotericsoftware.com/forum/iOS-10-disappearing-graphics-10109 + static webkit602BugfixHelper(alpha: number, blend: MixBlend) {} + + static contains(array: Array, element: T, identity = true) { + for (let i = 0; i < array.length; i++) { + if (array[i] == element) return true; + } + return false; + } +} + +export class DebugUtils { + static logBones(skeleton: Skeleton) { + for (let i = 0; i < skeleton.bones.length; i++) { + const bone = skeleton.bones[i]; + console.log( + bone.data.name + + ", " + + bone.a + + ", " + + bone.b + + ", " + + bone.c + + ", " + + bone.d + + ", " + + bone.worldX + + ", " + + bone.worldY + ); + } + } +} + +export class Pool { + private items = new Array(); + private instantiator: () => T; + + constructor(instantiator: () => T) { + this.instantiator = instantiator; + } + + obtain() { + return this.items.length > 0 ? this.items.pop() : this.instantiator(); + } + + free(item: T) { + if ((item as any).reset) (item as any).reset(); + this.items.push(item); + } + + freeAll(items: ArrayLike) { + for (let i = 0; i < items.length; i++) { + this.free(items[i]); + } + } + + clear() { + this.items.length = 0; + } +} + +export class Vector2 { + constructor( + public x = 0, + public y = 0 + ) {} + + set(x: number, y: number): Vector2 { + this.x = x; + this.y = y; + return this; + } + + length() { + const x = this.x; + const y = this.y; + return Math.sqrt(x * x + y * y); + } + + normalize() { + const len = this.length(); + if (len != 0) { + this.x /= len; + this.y /= len; + } + return this; + } +} + +export class TimeKeeper { + maxDelta = 0.064; + framesPerSecond = 0; + delta = 0; + totalTime = 0; + + private lastTime = Date.now() / 1000; + private frameCount = 0; + private frameTime = 0; + + update() { + const now = Date.now() / 1000; + this.delta = now - this.lastTime; + this.frameTime += this.delta; + this.totalTime += this.delta; + if (this.delta > this.maxDelta) this.delta = this.maxDelta; + this.lastTime = now; + + this.frameCount++; + if (this.frameTime > 1) { + this.framesPerSecond = this.frameCount / this.frameTime; + this.frameTime = 0; + this.frameCount = 0; + } + } +} + +export interface ArrayLike { + length: number; + [n: number]: T; +} + +export class WindowedMean { + values: Array; + addedValues = 0; + lastValue = 0; + mean = 0; + dirty = true; + + constructor(windowSize: number = 32) { + this.values = new Array(windowSize); + } + + hasEnoughData() { + return this.addedValues >= this.values.length; + } + + addValue(value: number) { + if (this.addedValues < this.values.length) this.addedValues++; + this.values[this.lastValue++] = value; + if (this.lastValue > this.values.length - 1) this.lastValue = 0; + this.dirty = true; + } + + getMean() { + if (this.hasEnoughData()) { + if (this.dirty) { + let mean = 0; + for (let i = 0; i < this.values.length; i++) { + mean += this.values[i]; + } + this.mean = mean / this.values.length; + this.dirty = false; + } + return this.mean; + } else { + return 0; + } + } +} diff --git a/packages/spine-core-3.8/src/spine-core/VertexEffect.ts b/packages/spine-core-3.8/src/spine-core/VertexEffect.ts new file mode 100644 index 0000000000..ae23cd16d2 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/VertexEffect.ts @@ -0,0 +1,8 @@ +import { Skeleton } from "./Skeleton"; +import { Color, Vector2 } from "./Utils"; + +export interface VertexEffect { + begin(skeleton: Skeleton): void; + transform(position: Vector2, uv: Vector2, light: Color, dark: Color): void; + end(): void; +} diff --git a/packages/spine-core-3.8/src/spine-core/attachments/Attachment.ts b/packages/spine-core-3.8/src/spine-core/attachments/Attachment.ts new file mode 100644 index 0000000000..e13cf7b936 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/attachments/Attachment.ts @@ -0,0 +1,147 @@ +import { Slot } from "../Slot"; +import { Utils, ArrayLike } from "../Utils"; + +/** The base class for all attachments. */ +export abstract class Attachment { + name: string; + + constructor(name: string) { + if (name == null) throw new Error("name cannot be null."); + this.name = name; + } + + abstract copy(): Attachment; +} + +/** Base class for an attachment with vertices that are transformed by one or more bones and can be deformed by a slot's + * {@link Slot#deform}. */ +export abstract class VertexAttachment extends Attachment { + private static nextID = 0; + + /** The unique ID for this attachment. */ + id = (VertexAttachment.nextID++ & 65535) << 11; + + /** The bones which affect the {@link #getVertices()}. The array entries are, for each vertex, the number of bones affecting + * the vertex followed by that many bone indices, which is the index of the bone in {@link Skeleton#bones}. Will be null + * if this attachment has no weights. */ + bones: Array; + + /** The vertex positions in the bone's coordinate system. For a non-weighted attachment, the values are `x,y` + * entries for each vertex. For a weighted attachment, the values are `x,y,weight` entries for each bone affecting + * each vertex. */ + vertices: ArrayLike; + + /** The maximum number of world vertex values that can be output by + * {@link #computeWorldVertices()} using the `count` parameter. */ + worldVerticesLength = 0; + + /** Deform keys for the deform attachment are also applied to this attachment. May be null if no deform keys should be applied. */ + deformAttachment: VertexAttachment = this; + + constructor(name: string) { + super(name); + } + + /** Transforms the attachment's local {@link vertices} to world coordinates. If the slot's {@link Slot#deform} is + * not empty, it is used to deform the vertices. + * + * See [World transforms](http://esotericsoftware.com/spine-runtime-skeletons#World-transforms) in the Spine + * Runtimes Guide. + * @param start The index of the first {@link #vertices} value to transform. Each vertex has 2 values, x and y. + * @param count The number of world vertex values to output. Must be <= {@link #worldVerticesLength} - `start`. + * @param worldVertices The output world vertices. Must have a length >= `offset` + `count` * + * `stride` / 2. + * @param offset The `worldVertices` index to begin writing values. + * @param stride The number of `worldVertices` entries between the value pairs written. */ + computeWorldVertices( + slot: Slot, + start: number, + count: number, + worldVertices: ArrayLike, + offset: number, + stride: number + ) { + count = offset + (count >> 1) * stride; + const skeleton = slot.bone.skeleton; + const deformArray = slot.deform; + let vertices = this.vertices; + const bones = this.bones; + if (bones == null) { + if (deformArray.length > 0) vertices = deformArray; + const bone = slot.bone; + const x = bone.worldX; + const y = bone.worldY; + const a = bone.a, + b = bone.b, + c = bone.c, + d = bone.d; + for (let v = start, w = offset; w < count; v += 2, w += stride) { + const vx = vertices[v], + vy = vertices[v + 1]; + worldVertices[w] = vx * a + vy * b + x; + worldVertices[w + 1] = vx * c + vy * d + y; + } + return; + } + let v = 0, + skip = 0; + for (let i = 0; i < start; i += 2) { + const n = bones[v]; + v += n + 1; + skip += n; + } + const skeletonBones = skeleton.bones; + if (deformArray.length == 0) { + for (let w = offset, b = skip * 3; w < count; w += stride) { + let wx = 0, + wy = 0; + let n = bones[v++]; + n += v; + for (; v < n; v++, b += 3) { + const bone = skeletonBones[bones[v]]; + const vx = vertices[b], + vy = vertices[b + 1], + weight = vertices[b + 2]; + wx += (vx * bone.a + vy * bone.b + bone.worldX) * weight; + wy += (vx * bone.c + vy * bone.d + bone.worldY) * weight; + } + worldVertices[w] = wx; + worldVertices[w + 1] = wy; + } + } else { + const deform = deformArray; + for (let w = offset, b = skip * 3, f = skip << 1; w < count; w += stride) { + let wx = 0, + wy = 0; + let n = bones[v++]; + n += v; + for (; v < n; v++, b += 3, f += 2) { + const bone = skeletonBones[bones[v]]; + const vx = vertices[b] + deform[f], + vy = vertices[b + 1] + deform[f + 1], + weight = vertices[b + 2]; + wx += (vx * bone.a + vy * bone.b + bone.worldX) * weight; + wy += (vx * bone.c + vy * bone.d + bone.worldY) * weight; + } + worldVertices[w] = wx; + worldVertices[w + 1] = wy; + } + } + } + + /** Does not copy id (generated) or name (set on construction). **/ + copyTo(attachment: VertexAttachment) { + if (this.bones != null) { + attachment.bones = new Array(this.bones.length); + Utils.arrayCopy(this.bones, 0, attachment.bones, 0, this.bones.length); + } else attachment.bones = null; + + if (this.vertices != null) { + attachment.vertices = Utils.newFloatArray(this.vertices.length); + Utils.arrayCopy(this.vertices, 0, attachment.vertices, 0, this.vertices.length); + } else attachment.vertices = null; + + attachment.worldVerticesLength = this.worldVerticesLength; + attachment.deformAttachment = this.deformAttachment; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/attachments/AttachmentLoader.ts b/packages/spine-core-3.8/src/spine-core/attachments/AttachmentLoader.ts new file mode 100644 index 0000000000..7d14c9f22c --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/attachments/AttachmentLoader.ts @@ -0,0 +1,31 @@ +import { RegionAttachment } from "./RegionAttachment"; +import { Skin } from "../Skin"; +import { BoundingBoxAttachment } from "./BoundingBoxAttachment"; +import { PathAttachment } from "./PathAttachment"; +import { PointAttachment } from "./PointAttachment"; +import { ClippingAttachment } from "./ClippingAttachment"; +import { MeshAttachment } from "./MeshAttachment"; + +/** The interface which can be implemented to customize creating and populating attachments. + * + * See [Loading skeleton data](http://esotericsoftware.com/spine-loading-skeleton-data#AttachmentLoader) in the Spine + * Runtimes Guide. */ +export interface AttachmentLoader { + /** @return May be null to not load an attachment. */ + newRegionAttachment(skin: Skin, name: string, path: string): RegionAttachment; + + /** @return May be null to not load an attachment. */ + newMeshAttachment(skin: Skin, name: string, path: string): MeshAttachment; + + /** @return May be null to not load an attachment. */ + newBoundingBoxAttachment(skin: Skin, name: string): BoundingBoxAttachment; + + /** @return May be null to not load an attachment */ + newPathAttachment(skin: Skin, name: string): PathAttachment; + + /** @return May be null to not load an attachment */ + newPointAttachment(skin: Skin, name: string): PointAttachment; + + /** @return May be null to not load an attachment */ + newClippingAttachment(skin: Skin, name: string): ClippingAttachment; +} diff --git a/packages/spine-core-3.8/src/spine-core/attachments/AttachmentType.ts b/packages/spine-core-3.8/src/spine-core/attachments/AttachmentType.ts new file mode 100644 index 0000000000..193fa62007 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/attachments/AttachmentType.ts @@ -0,0 +1,9 @@ +export enum AttachmentType { + Region, + BoundingBox, + Mesh, + LinkedMesh, + Path, + Point, + Clipping +} diff --git a/packages/spine-core-3.8/src/spine-core/attachments/BoundingBoxAttachment.ts b/packages/spine-core-3.8/src/spine-core/attachments/BoundingBoxAttachment.ts new file mode 100644 index 0000000000..b23cec9733 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/attachments/BoundingBoxAttachment.ts @@ -0,0 +1,22 @@ +import { VertexAttachment, Attachment } from "./Attachment"; +import { Color } from "../Utils"; + +/** An attachment with vertices that make up a polygon. Can be used for hit detection, creating physics bodies, spawning particle + * effects, and more. + * + * See {@link SkeletonBounds} and [Bounding Boxes](http://esotericsoftware.com/spine-bounding-boxes) in the Spine User + * Guide. */ +export class BoundingBoxAttachment extends VertexAttachment { + color = new Color(1, 1, 1, 1); + + constructor(name: string) { + super(name); + } + + copy(): Attachment { + const copy = new BoundingBoxAttachment(this.name); + this.copyTo(copy); + copy.color.setFromColor(this.color); + return copy; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/attachments/ClippingAttachment.ts b/packages/spine-core-3.8/src/spine-core/attachments/ClippingAttachment.ts new file mode 100644 index 0000000000..8ebc184346 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/attachments/ClippingAttachment.ts @@ -0,0 +1,27 @@ +import { VertexAttachment, Attachment } from "./Attachment"; +import { SlotData } from "../SlotData"; +import { Color } from "../Utils"; + +/** An attachment with vertices that make up a polygon used for clipping the rendering of other attachments. */ +export class ClippingAttachment extends VertexAttachment { + /** Clipping is performed between the clipping polygon's slot and the end slot. Returns null if clipping is done until the end of + * the skeleton's rendering. */ + endSlot: SlotData; + + // Nonessential. + /** The color of the clipping polygon as it was in Spine. Available only when nonessential data was exported. Clipping polygons + * are not usually rendered at runtime. */ + color = new Color(0.2275, 0.2275, 0.8078, 1); // ce3a3aff + + constructor(name: string) { + super(name); + } + + copy(): Attachment { + const copy = new ClippingAttachment(this.name); + this.copyTo(copy); + copy.endSlot = this.endSlot; + copy.color.setFromColor(this.color); + return copy; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/attachments/MeshAttachment.ts b/packages/spine-core-3.8/src/spine-core/attachments/MeshAttachment.ts new file mode 100644 index 0000000000..02deaa68ff --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/attachments/MeshAttachment.ts @@ -0,0 +1,175 @@ +import { VertexAttachment, Attachment } from "./Attachment"; +import { TextureRegion } from "../Texture"; +import { Color, Utils, ArrayLike } from "../Utils"; +import { TextureAtlasRegion } from "../TextureAtlas"; + +/** An attachment that displays a textured mesh. A mesh has hull vertices and internal vertices within the hull. Holes are not + * supported. Each vertex has UVs (texture coordinates) and triangles are used to map an image on to the mesh. + * + * See [Mesh attachments](http://esotericsoftware.com/spine-meshes) in the Spine User Guide. */ +export class MeshAttachment extends VertexAttachment { + region: TextureRegion; + + /** The name of the texture region for this attachment. */ + path: string; + + /** The UV pair for each vertex, normalized within the texture region. */ + regionUVs: ArrayLike; + + /** The UV pair for each vertex, normalized within the entire texture. + * + * See {@link #updateUVs}. */ + uvs: ArrayLike; + + /** Triplets of vertex indices which describe the mesh's triangulation. */ + triangles: Array; + + /** The color to tint the mesh. */ + color = new Color(1, 1, 1, 1); + + /** The width of the mesh's image. Available only when nonessential data was exported. */ + width: number; + + /** The height of the mesh's image. Available only when nonessential data was exported. */ + height: number; + + /** The number of entries at the beginning of {@link #vertices} that make up the mesh hull. */ + hullLength: number; + + /** Vertex index pairs describing edges for controling triangulation. Mesh triangles will never cross edges. Only available if + * nonessential data was exported. Triangulation is not performed at runtime. */ + edges: Array; + + private parentMesh: MeshAttachment; + tempColor = new Color(0, 0, 0, 0); + + constructor(name: string) { + super(name); + } + + /** Calculates {@link #uvs} using {@link #regionUVs} and the {@link #region}. Must be called after changing the region UVs or + * region. */ + updateUVs() { + const regionUVs = this.regionUVs; + if (this.uvs == null || this.uvs.length != regionUVs.length) this.uvs = Utils.newFloatArray(regionUVs.length); + const uvs = this.uvs; + const n = this.uvs.length; + let u = this.region.u, + v = this.region.v, + width = 0, + height = 0; + if (this.region instanceof TextureAtlasRegion) { + const region = this.region; + const textureWidth = region.texture.width, + textureHeight = region.texture.height; + switch (region.degrees) { + case 90: + u -= (region.originalHeight - region.offsetY - region.height) / textureWidth; + v -= (region.originalWidth - region.offsetX - region.width) / textureHeight; + width = region.originalHeight / textureWidth; + height = region.originalWidth / textureHeight; + for (let i = 0; i < n; i += 2) { + uvs[i] = u + regionUVs[i + 1] * width; + uvs[i + 1] = v + (1 - regionUVs[i]) * height; + } + return; + case 180: + u -= (region.originalWidth - region.offsetX - region.width) / textureWidth; + v -= region.offsetY / textureHeight; + width = region.originalWidth / textureWidth; + height = region.originalHeight / textureHeight; + for (let i = 0; i < n; i += 2) { + uvs[i] = u + (1 - regionUVs[i]) * width; + uvs[i + 1] = v + (1 - regionUVs[i + 1]) * height; + } + return; + case 270: + u -= region.offsetY / textureWidth; + v -= region.offsetX / textureHeight; + width = region.originalHeight / textureWidth; + height = region.originalWidth / textureHeight; + for (let i = 0; i < n; i += 2) { + uvs[i] = u + (1 - regionUVs[i + 1]) * width; + uvs[i + 1] = v + regionUVs[i] * height; + } + return; + } + u -= region.offsetX / textureWidth; + v -= (region.originalHeight - region.offsetY - region.height) / textureHeight; + width = region.originalWidth / textureWidth; + height = region.originalHeight / textureHeight; + } else if (this.region == null) { + u = v = 0; + width = height = 1; + } else { + width = this.region.u2 - u; + height = this.region.v2 - v; + } + + for (let i = 0; i < n; i += 2) { + uvs[i] = u + regionUVs[i] * width; + uvs[i + 1] = v + regionUVs[i + 1] * height; + } + } + + /** The parent mesh if this is a linked mesh, else null. A linked mesh shares the {@link #bones}, {@link #vertices}, + * {@link #regionUVs}, {@link #triangles}, {@link #hullLength}, {@link #edges}, {@link #width}, and {@link #height} with the + * parent mesh, but may have a different {@link #name} or {@link #path} (and therefore a different texture). */ + getParentMesh() { + return this.parentMesh; + } + + /** @param parentMesh May be null. */ + setParentMesh(parentMesh: MeshAttachment) { + this.parentMesh = parentMesh; + if (parentMesh != null) { + this.bones = parentMesh.bones; + this.vertices = parentMesh.vertices; + this.worldVerticesLength = parentMesh.worldVerticesLength; + this.regionUVs = parentMesh.regionUVs; + this.triangles = parentMesh.triangles; + this.hullLength = parentMesh.hullLength; + this.worldVerticesLength = parentMesh.worldVerticesLength; + } + } + + copy(): Attachment { + if (this.parentMesh != null) return this.newLinkedMesh(); + + const copy = new MeshAttachment(this.name); + copy.region = this.region; + copy.path = this.path; + copy.color.setFromColor(this.color); + + this.copyTo(copy); + copy.regionUVs = new Array(this.regionUVs.length); + Utils.arrayCopy(this.regionUVs, 0, copy.regionUVs, 0, this.regionUVs.length); + copy.uvs = new Array(this.uvs.length); + Utils.arrayCopy(this.uvs, 0, copy.uvs, 0, this.uvs.length); + copy.triangles = new Array(this.triangles.length); + Utils.arrayCopy(this.triangles, 0, copy.triangles, 0, this.triangles.length); + copy.hullLength = this.hullLength; + + // Nonessential. + if (this.edges != null) { + copy.edges = new Array(this.edges.length); + Utils.arrayCopy(this.edges, 0, copy.edges, 0, this.edges.length); + } + copy.width = this.width; + copy.height = this.height; + + return copy; + } + + /** Returns a new mesh with the {@link #parentMesh} set to this mesh's parent mesh, if any, else to this mesh. **/ + newLinkedMesh(): MeshAttachment { + const copy = new MeshAttachment(this.name); + copy.region = this.region; + copy.path = this.path; + copy.color.setFromColor(this.color); + copy.deformAttachment = this.deformAttachment; + copy.setParentMesh(this.parentMesh != null ? this.parentMesh : this); + copy.updateUVs(); + return copy; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/attachments/PathAttachment.ts b/packages/spine-core-3.8/src/spine-core/attachments/PathAttachment.ts new file mode 100644 index 0000000000..53562b7657 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/attachments/PathAttachment.ts @@ -0,0 +1,36 @@ +import { VertexAttachment, Attachment } from "./Attachment"; +import { Color, Utils } from "../Utils"; + +/** An attachment whose vertices make up a composite Bezier curve. + * + * See {@link PathConstraint} and [Paths](http://esotericsoftware.com/spine-paths) in the Spine User Guide. */ +export class PathAttachment extends VertexAttachment { + /** The lengths along the path in the setup pose from the start of the path to the end of each Bezier curve. */ + lengths: Array; + + /** If true, the start and end knots are connected. */ + closed = false; + + /** If true, additional calculations are performed to make calculating positions along the path more accurate. If false, fewer + * calculations are performed but calculating positions along the path is less accurate. */ + constantSpeed = false; + + /** The color of the path as it was in Spine. Available only when nonessential data was exported. Paths are not usually + * rendered at runtime. */ + color = new Color(1, 1, 1, 1); + + constructor(name: string) { + super(name); + } + + copy(): Attachment { + const copy = new PathAttachment(this.name); + this.copyTo(copy); + copy.lengths = new Array(this.lengths.length); + Utils.arrayCopy(this.lengths, 0, copy.lengths, 0, this.lengths.length); + copy.closed = closed; + copy.constantSpeed = this.constantSpeed; + copy.color.setFromColor(this.color); + return copy; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/attachments/PointAttachment.ts b/packages/spine-core-3.8/src/spine-core/attachments/PointAttachment.ts new file mode 100644 index 0000000000..74de51a2ef --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/attachments/PointAttachment.ts @@ -0,0 +1,46 @@ +import { VertexAttachment, Attachment } from "./Attachment"; +import { Color, MathUtils, Vector2 } from "../Utils"; +import { Bone } from "../Bone"; + +/** An attachment which is a single point and a rotation. This can be used to spawn projectiles, particles, etc. A bone can be + * used in similar ways, but a PointAttachment is slightly less expensive to compute and can be hidden, shown, and placed in a + * skin. + * + * See [Point Attachments](http://esotericsoftware.com/spine-point-attachments) in the Spine User Guide. */ +export class PointAttachment extends VertexAttachment { + x: number; + y: number; + rotation: number; + + /** The color of the point attachment as it was in Spine. Available only when nonessential data was exported. Point attachments + * are not usually rendered at runtime. */ + color = new Color(0.38, 0.94, 0, 1); + + constructor(name: string) { + super(name); + this.name = name; + } + + computeWorldPosition(bone: Bone, point: Vector2) { + point.x = this.x * bone.a + this.y * bone.b + bone.worldX; + point.y = this.x * bone.c + this.y * bone.d + bone.worldY; + return point; + } + + computeWorldRotation(bone: Bone) { + const cos = MathUtils.cosDeg(this.rotation), + sin = MathUtils.sinDeg(this.rotation); + const x = cos * bone.a + sin * bone.b; + const y = cos * bone.c + sin * bone.d; + return Math.atan2(y, x) * MathUtils.radDeg; + } + + copy(): Attachment { + const copy = new PointAttachment(this.name); + copy.x = this.x; + copy.y = this.y; + copy.rotation = this.rotation; + copy.color.setFromColor(this.color); + return copy; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/attachments/RegionAttachment.ts b/packages/spine-core-3.8/src/spine-core/attachments/RegionAttachment.ts new file mode 100644 index 0000000000..1730e3880a --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/attachments/RegionAttachment.ts @@ -0,0 +1,211 @@ +import { Attachment } from "./Attachment"; +import { Color, Utils, ArrayLike } from "../Utils"; +import { TextureRegion } from "../Texture"; +import { Bone } from "../Bone"; + +/** An attachment that displays a textured quadrilateral. + * + * See [Region attachments](http://esotericsoftware.com/spine-regions) in the Spine User Guide. */ +export class RegionAttachment extends Attachment { + static OX1 = 0; + static OY1 = 1; + static OX2 = 2; + static OY2 = 3; + static OX3 = 4; + static OY3 = 5; + static OX4 = 6; + static OY4 = 7; + + static X1 = 0; + static Y1 = 1; + static C1R = 2; + static C1G = 3; + static C1B = 4; + static C1A = 5; + static U1 = 6; + static V1 = 7; + + static X2 = 8; + static Y2 = 9; + static C2R = 10; + static C2G = 11; + static C2B = 12; + static C2A = 13; + static U2 = 14; + static V2 = 15; + + static X3 = 16; + static Y3 = 17; + static C3R = 18; + static C3G = 19; + static C3B = 20; + static C3A = 21; + static U3 = 22; + static V3 = 23; + + static X4 = 24; + static Y4 = 25; + static C4R = 26; + static C4G = 27; + static C4B = 28; + static C4A = 29; + static U4 = 30; + static V4 = 31; + + /** The local x translation. */ + x = 0; + + /** The local y translation. */ + y = 0; + + /** The local scaleX. */ + scaleX = 1; + + /** The local scaleY. */ + scaleY = 1; + + /** The local rotation. */ + rotation = 0; + + /** The width of the region attachment in Spine. */ + width = 0; + + /** The height of the region attachment in Spine. */ + height = 0; + + /** The color to tint the region attachment. */ + color = new Color(1, 1, 1, 1); + + /** The name of the texture region for this attachment. */ + path: string; + + rendererObject: any; + region: TextureRegion; + + /** For each of the 4 vertices, a pair of x,y values that is the local position of the vertex. + * + * See {@link #updateOffset()}. */ + offset = Utils.newFloatArray(8); + + uvs = Utils.newFloatArray(8); + + tempColor = new Color(1, 1, 1, 1); + + constructor(name: string) { + super(name); + } + + /** Calculates the {@link #offset} using the region settings. Must be called after changing region settings. */ + updateOffset(): void { + const regionScaleX = (this.width / this.region.originalWidth) * this.scaleX; + const regionScaleY = (this.height / this.region.originalHeight) * this.scaleY; + const localX = (-this.width / 2) * this.scaleX + this.region.offsetX * regionScaleX; + const localY = (-this.height / 2) * this.scaleY + this.region.offsetY * regionScaleY; + const localX2 = localX + this.region.width * regionScaleX; + const localY2 = localY + this.region.height * regionScaleY; + const radians = (this.rotation * Math.PI) / 180; + const cos = Math.cos(radians); + const sin = Math.sin(radians); + const localXCos = localX * cos + this.x; + const localXSin = localX * sin; + const localYCos = localY * cos + this.y; + const localYSin = localY * sin; + const localX2Cos = localX2 * cos + this.x; + const localX2Sin = localX2 * sin; + const localY2Cos = localY2 * cos + this.y; + const localY2Sin = localY2 * sin; + const offset = this.offset; + offset[RegionAttachment.OX1] = localXCos - localYSin; + offset[RegionAttachment.OY1] = localYCos + localXSin; + offset[RegionAttachment.OX2] = localXCos - localY2Sin; + offset[RegionAttachment.OY2] = localY2Cos + localXSin; + offset[RegionAttachment.OX3] = localX2Cos - localY2Sin; + offset[RegionAttachment.OY3] = localY2Cos + localX2Sin; + offset[RegionAttachment.OX4] = localX2Cos - localYSin; + offset[RegionAttachment.OY4] = localYCos + localX2Sin; + } + + setRegion(region: TextureRegion): void { + this.region = region; + const uvs = this.uvs; + if (region.rotate) { + uvs[2] = region.u; + uvs[3] = region.v2; + uvs[4] = region.u; + uvs[5] = region.v; + uvs[6] = region.u2; + uvs[7] = region.v; + uvs[0] = region.u2; + uvs[1] = region.v2; + } else { + uvs[0] = region.u; + uvs[1] = region.v2; + uvs[2] = region.u; + uvs[3] = region.v; + uvs[4] = region.u2; + uvs[5] = region.v; + uvs[6] = region.u2; + uvs[7] = region.v2; + } + } + + /** Transforms the attachment's four vertices to world coordinates. + * + * See [World transforms](http://esotericsoftware.com/spine-runtime-skeletons#World-transforms) in the Spine + * Runtimes Guide. + * @param worldVertices The output world vertices. Must have a length >= `offset` + 8. + * @param offset The `worldVertices` index to begin writing values. + * @param stride The number of `worldVertices` entries between the value pairs written. */ + computeWorldVertices(bone: Bone, worldVertices: ArrayLike, offset: number, stride: number) { + const vertexOffset = this.offset; + const x = bone.worldX, + y = bone.worldY; + const a = bone.a, + b = bone.b, + c = bone.c, + d = bone.d; + let offsetX = 0, + offsetY = 0; + + offsetX = vertexOffset[RegionAttachment.OX1]; + offsetY = vertexOffset[RegionAttachment.OY1]; + worldVertices[offset] = offsetX * a + offsetY * b + x; // br + worldVertices[offset + 1] = offsetX * c + offsetY * d + y; + offset += stride; + + offsetX = vertexOffset[RegionAttachment.OX2]; + offsetY = vertexOffset[RegionAttachment.OY2]; + worldVertices[offset] = offsetX * a + offsetY * b + x; // bl + worldVertices[offset + 1] = offsetX * c + offsetY * d + y; + offset += stride; + + offsetX = vertexOffset[RegionAttachment.OX3]; + offsetY = vertexOffset[RegionAttachment.OY3]; + worldVertices[offset] = offsetX * a + offsetY * b + x; // ul + worldVertices[offset + 1] = offsetX * c + offsetY * d + y; + offset += stride; + + offsetX = vertexOffset[RegionAttachment.OX4]; + offsetY = vertexOffset[RegionAttachment.OY4]; + worldVertices[offset] = offsetX * a + offsetY * b + x; // ur + worldVertices[offset + 1] = offsetX * c + offsetY * d + y; + } + + copy(): Attachment { + const copy = new RegionAttachment(this.name); + copy.region = this.region; + copy.rendererObject = this.rendererObject; + copy.path = this.path; + copy.x = this.x; + copy.y = this.y; + copy.scaleX = this.scaleX; + copy.scaleY = this.scaleY; + copy.rotation = this.rotation; + copy.width = this.width; + copy.height = this.height; + Utils.arrayCopy(this.uvs, 0, copy.uvs, 0, 8); + Utils.arrayCopy(this.offset, 0, copy.offset, 0, 8); + copy.color.setFromColor(this.color); + return copy; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/index.ts b/packages/spine-core-3.8/src/spine-core/index.ts new file mode 100644 index 0000000000..9dfa8aee05 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/index.ts @@ -0,0 +1,50 @@ +// Barrel for the vendored spine 3.8 runtime. There is no `@esotericsoftware/spine-core` npm +// release for the 3.8 line (npm starts at 4.0.1), so this directory vendors the runtime source +// instead of depending on a package; this file is the re-export surface the 4.2 package gets for +// free from `export * from "@esotericsoftware/spine-core"`. +import "./polyfills"; + +export * from "./Animation"; +export * from "./AnimationState"; +export * from "./AnimationStateData"; +export * from "./AtlasAttachmentLoader"; +export * from "./BlendMode"; +export * from "./Bone"; +export * from "./BoneData"; +export * from "./ConstraintData"; +export * from "./Event"; +export * from "./EventData"; +export * from "./IkConstraint"; +export * from "./IkConstraintData"; +export * from "./PathConstraint"; +export * from "./PathConstraintData"; +export * from "./Skeleton"; +export * from "./SkeletonBinary"; +export * from "./SkeletonBounds"; +export * from "./SkeletonClipping"; +export * from "./SkeletonData"; +export * from "./SkeletonJson"; +export * from "./Skin"; +export * from "./Slot"; +export * from "./SlotData"; +export * from "./Texture"; +export * from "./TextureAtlas"; +export * from "./TransformConstraint"; +export * from "./TransformConstraintData"; +export * from "./Triangulator"; +export * from "./Updatable"; +export * from "./Utils"; +export * from "./VertexEffect"; + +export * from "./attachments/Attachment"; +export * from "./attachments/AttachmentLoader"; +export * from "./attachments/AttachmentType"; +export * from "./attachments/BoundingBoxAttachment"; +export * from "./attachments/ClippingAttachment"; +export * from "./attachments/MeshAttachment"; +export * from "./attachments/PathAttachment"; +export * from "./attachments/PointAttachment"; +export * from "./attachments/RegionAttachment"; + +export * from "./vertexeffects/JitterEffect"; +export * from "./vertexeffects/SwirlEffect"; diff --git a/packages/spine-core-3.8/src/spine-core/polyfills.ts b/packages/spine-core-3.8/src/spine-core/polyfills.ts new file mode 100644 index 0000000000..0531725d1c --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/polyfills.ts @@ -0,0 +1,13 @@ +interface Math { + fround(n: number): number; +} + +(() => { + if (!Math.fround) { + Math.fround = (function (array) { + return function (x: number) { + return (array[0] = x), array[0]; + }; + })(new Float32Array(1)); + } +})(); diff --git a/packages/spine-core-3.8/src/spine-core/vertexeffects/JitterEffect.ts b/packages/spine-core-3.8/src/spine-core/vertexeffects/JitterEffect.ts new file mode 100644 index 0000000000..f7060b06ba --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/vertexeffects/JitterEffect.ts @@ -0,0 +1,22 @@ +import { VertexEffect } from "../VertexEffect"; +import { Skeleton } from "../Skeleton"; +import { Color, MathUtils, Vector2 } from "../Utils"; + +export class JitterEffect implements VertexEffect { + jitterX = 0; + jitterY = 0; + + constructor(jitterX: number, jitterY: number) { + this.jitterX = jitterX; + this.jitterY = jitterY; + } + + begin(skeleton: Skeleton): void {} + + transform(position: Vector2, uv: Vector2, light: Color, dark: Color): void { + position.x += MathUtils.randomTriangular(-this.jitterX, this.jitterY); + position.y += MathUtils.randomTriangular(-this.jitterX, this.jitterY); + } + + end(): void {} +} diff --git a/packages/spine-core-3.8/src/spine-core/vertexeffects/SwirlEffect.ts b/packages/spine-core-3.8/src/spine-core/vertexeffects/SwirlEffect.ts new file mode 100644 index 0000000000..52f0169be5 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/vertexeffects/SwirlEffect.ts @@ -0,0 +1,38 @@ +import { VertexEffect } from "../VertexEffect"; +import { PowOut, Color, MathUtils, Vector2 } from "../Utils"; +import { Skeleton } from "../Skeleton"; + +export class SwirlEffect implements VertexEffect { + static interpolation = new PowOut(2); + centerX = 0; + centerY = 0; + radius = 0; + angle = 0; + private worldX = 0; + private worldY = 0; + + constructor(radius: number) { + this.radius = radius; + } + + begin(skeleton: Skeleton): void { + this.worldX = skeleton.x + this.centerX; + this.worldY = skeleton.y + this.centerY; + } + + transform(position: Vector2, uv: Vector2, light: Color, dark: Color): void { + const radAngle = this.angle * MathUtils.degreesToRadians; + const x = position.x - this.worldX; + const y = position.y - this.worldY; + const dist = Math.sqrt(x * x + y * y); + if (dist < this.radius) { + const theta = SwirlEffect.interpolation.apply(0, radAngle, (this.radius - dist) / this.radius); + const cos = Math.cos(theta); + const sin = Math.sin(theta); + position.x = cos * x - sin * y + this.worldX; + position.y = sin * x + cos * y + this.worldY; + } + } + + end(): void {} +} diff --git a/packages/spine-core-3.8/src/util/ClearablePool.ts b/packages/spine-core-3.8/src/util/ClearablePool.ts new file mode 100644 index 0000000000..ff8d6ccc6a --- /dev/null +++ b/packages/spine-core-3.8/src/util/ClearablePool.ts @@ -0,0 +1,26 @@ +export class ClearablePool { + private _type: new () => T; + private _elements: T[]; + private _usedElementCount: number = 0; + + constructor(type: new () => T) { + this._type = type; + this._elements = []; + } + + get(): T { + const { _usedElementCount: usedElementCount, _elements: elements } = this; + this._usedElementCount++; + if (elements.length === usedElementCount) { + const element = new this._type(); + elements.push(element); + return element; + } else { + return elements[usedElementCount]; + } + } + + clear(): void { + this._usedElementCount = 0; + } +} diff --git a/packages/spine-core-3.8/src/util/ReturnablePool.ts b/packages/spine-core-3.8/src/util/ReturnablePool.ts new file mode 100644 index 0000000000..77c1bcaa47 --- /dev/null +++ b/packages/spine-core-3.8/src/util/ReturnablePool.ts @@ -0,0 +1,25 @@ +export class ReturnablePool { + private _type: new () => T; + private _elements: T[]; + private _lastElementIndex: number; + + constructor(type: new () => T, initializeCount: number = 1) { + this._type = type; + this._lastElementIndex = initializeCount - 1; + const elements = (this._elements = new Array(initializeCount)); + for (let i = 0; i < initializeCount; ++i) { + elements[i] = new type(); + } + } + + get(): T { + if (this._lastElementIndex < 0) { + return new this._type(); + } + return this._elements[this._lastElementIndex--]; + } + + return(element: T): void { + this._elements[++this._lastElementIndex] = element; + } +} diff --git a/packages/spine-core-3.8/tsconfig.json b/packages/spine-core-3.8/tsconfig.json new file mode 100644 index 0000000000..588b0055c3 --- /dev/null +++ b/packages/spine-core-3.8/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "esnext", + "target": "esnext", + "declaration": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "declarationDir": "types", + "emitDeclarationOnly": true, + "noImplicitOverride": true, + "sourceMap": true, + "incremental": false, + "skipLibCheck": true, + "stripInternal": true + }, + "include": ["src/**/*"] +} diff --git a/packages/spine-core-4.2/package.json b/packages/spine-core-4.2/package.json new file mode 100644 index 0000000000..2411077e13 --- /dev/null +++ b/packages/spine-core-4.2/package.json @@ -0,0 +1,41 @@ +{ + "name": "@galacean/engine-spine-core-4.2", + "version": "0.0.0-experimental-2.0-game.19", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, + "repository": { + "url": "https://github.com/galacean/engine.git" + }, + "license": "MIT", + "main": "dist/main.js", + "module": "dist/module.js", + "debug": "src/index.ts", + "browser": "dist/browser.js", + "types": "types/index.d.ts", + "scripts": { + "b:types": "tsc" + }, + "umd": { + "name": "Galacean.SpineCore42", + "globals": { + "@galacean/engine": "Galacean", + "@galacean/engine-spine": "Galacean.Spine" + } + }, + "files": [ + "dist/**/*", + "types/**/*" + ], + "dependencies": { + "@galacean/engine-spine": "workspace:*" + }, + "devDependencies": { + "@esotericsoftware/spine-core": "~4.2.66", + "@galacean/engine": "workspace:*" + }, + "peerDependencies": { + "@galacean/engine": "workspace:*" + } +} diff --git a/packages/spine-core-4.2/src/Spine42Runtime.ts b/packages/spine-core-4.2/src/Spine42Runtime.ts new file mode 100644 index 0000000000..03082b3589 --- /dev/null +++ b/packages/spine-core-4.2/src/Spine42Runtime.ts @@ -0,0 +1,81 @@ +import { + AnimationState, + AnimationStateData, + AtlasAttachmentLoader, + Physics, + Skeleton, + SkeletonBinary, + SkeletonData, + SkeletonJson, + TextureAtlas +} from "@esotericsoftware/spine-core"; +import type { Texture2D } from "@galacean/engine"; +import type { ISpineRenderTarget, ISpineRuntime } from "@galacean/engine-spine"; +import { SpineGenerator } from "./SpineGenerator"; +import { SpineTexture } from "./SpineTexture"; + +/** + * Spine 4.2 runtime backend. + * + * @remarks + * Owns everything specific to the spine 4.2 runtime line: parsing (`SkeletonJson`/`SkeletonBinary`/ + * `TextureAtlas`), object-model construction, world-transform stepping (4.2's `Physics.update`), and + * the attachment-reading mesh generator. Registered automatically when this package is imported. + */ +export class Spine42Runtime implements ISpineRuntime { + private _generator = new SpineGenerator(); + + parseAtlasPageNames(atlasText: string): string[] { + return new TextureAtlas(atlasText).pages.map((page) => page.name); + } + + createTextureAtlas(atlasText: string, textures: Texture2D[]): TextureAtlas { + const textureAtlas = new TextureAtlas(atlasText); + textureAtlas.pages.forEach((page, index) => { + const engineTexture = textures.find((item) => item.name === page.name) || textures[index]; + const texture = new SpineTexture(engineTexture); + page.setTexture(texture); + }); + return textureAtlas; + } + + createSkeletonData( + skeletonRawData: string | ArrayBuffer, + textureAtlas: TextureAtlas, + skeletonDataScale: number + ): SkeletonData { + const atlasLoader = new AtlasAttachmentLoader(textureAtlas); + if (typeof skeletonRawData === "string") { + const skeletonJson = new SkeletonJson(atlasLoader); + skeletonJson.scale = skeletonDataScale; + return skeletonJson.readSkeletonData(skeletonRawData); + } else { + const skeletonBinary = new SkeletonBinary(atlasLoader); + skeletonBinary.scale = skeletonDataScale; + return skeletonBinary.readSkeletonData(new Uint8Array(skeletonRawData as ArrayBuffer)); + } + } + + createAnimationStateData(skeletonData: SkeletonData): AnimationStateData { + return new AnimationStateData(skeletonData); + } + + createSkeleton(skeletonData: SkeletonData): Skeleton { + return new Skeleton(skeletonData); + } + + createAnimationState(stateData: AnimationStateData): AnimationState { + return new AnimationState(stateData); + } + + updateState(skeleton: Skeleton, state: AnimationState, delta: number): void { + state.update(delta); + state.apply(skeleton); + skeleton.update(delta); + skeleton.updateWorldTransform(Physics.update); + } + + buildPrimitive(skeleton: Skeleton, target: ISpineRenderTarget): void { + this._generator.buildPrimitive(skeleton, target); + } +} diff --git a/packages/spine-core-4.2/src/SpineGenerator.ts b/packages/spine-core-4.2/src/SpineGenerator.ts new file mode 100644 index 0000000000..e995320a03 --- /dev/null +++ b/packages/spine-core-4.2/src/SpineGenerator.ts @@ -0,0 +1,342 @@ +import { + ArrayLike, + BlendMode, + ClippingAttachment, + Color, + MeshAttachment, + NumberArrayLike, + RegionAttachment, + Skeleton, + SkeletonClipping +} from "@esotericsoftware/spine-core"; +import { BoundingBox, SubPrimitive } from "@galacean/engine"; +import { SpineTexture } from "./SpineTexture"; +import { ClearablePool } from "./util/ClearablePool"; +import { ReturnablePool } from "./util/ReturnablePool"; +import type { ISpineRenderTarget } from "@galacean/engine-spine"; +import { SpineBlendMode, SpineVertexStride } from "@galacean/engine-spine"; + +class SubRenderItem { + subPrimitive: SubPrimitive; + blendMode: BlendMode; + texture: any; +} + +/** + * @internal + */ +export class SpineGenerator { + static tempDark = new Color(); + static tempColor = new Color(); + static tempVerts = new Array(8); + static QUAD_TRIANGLES = [0, 1, 2, 2, 3, 0]; + static subPrimitivePool = new ReturnablePool(SubPrimitive); + static subRenderItemPool = new ClearablePool(SubRenderItem); + + private _subRenderItems: SubRenderItem[] = []; + private _clipper: SkeletonClipping = new SkeletonClipping(); + + buildPrimitive(skeleton: Skeleton, renderer: ISpineRenderTarget) { + const { _indices, _vertices, _localBounds, _vertexCount, _subPrimitives, zSpacing, premultipliedAlpha, tintBlack } = + renderer; + + _localBounds.min.set(Infinity, Infinity, Infinity); + _localBounds.max.set(-Infinity, -Infinity, -Infinity); + + const { _clipper, _subRenderItems } = this; + + const { tempVerts, subRenderItemPool, subPrimitivePool } = SpineGenerator; + const { withTint: vertexStrideWithTint, withoutTint: vertexStrideWithoutTint } = SpineVertexStride; + + _subRenderItems.length = 0; + subRenderItemPool.clear(); + + let triangles: Array; + let uvs: NumberArrayLike; + + let verticesLength = 0; + let indicesLength = 0; + let start = 0; + let count = 0; + + let blend = BlendMode.Normal; + let texture: SpineTexture = null; + let tempBlendMode: BlendMode | null = null; + let tempTexture: SpineTexture | null = null; + + let primitiveIndex = 0; + + const drawOrder = skeleton.drawOrder; + for (let slotIndex = 0, n = drawOrder.length; slotIndex < n; ++slotIndex) { + const slot = drawOrder[slotIndex]; + if (!slot.bone.active) { + _clipper.clipEndWithSlot(slot); + continue; + } + + const attachment = slot.getAttachment(); + if (!attachment) { + _clipper.clipEndWithSlot(slot); + continue; + } + + const z = zSpacing * slotIndex; + const isClipping = _clipper.isClipping(); + let numFloats = 0; + let attachmentColor: Color = null; + + // This vertexSize will be passed to spine-core's computeWorldVertices function. + // + // Expected format by computeWorldVertices: + // - Without tintBlack: [x, y, u, v, r, g, b, a] = 8 components + // - With tintBlack: [x, y, u, v, r, g, b, a, dr, dg, db, da] = 12 components + // + // Our actual vertex buffer format: + // - vertexStrideWithoutTint: [x, y, z, u, v, r, g, b, a] = 9 components + // - vertexStrideWithTint: [x, y, z, u, v, r, g, b, a, dr, dg, db] = 12 components + // (Note: we optimized 'da' as uniform instead of buffer attribute) + // + // Calculation: + // - Without tintBlack: 9 - 1 (remove z) = 8 ✓ + // - With tintBlack: 12 - 1 (remove z) + 1 (add back da) = 12 ✓ + let vertexSize = tintBlack ? vertexStrideWithTint : vertexStrideWithoutTint - 1; + let clippedVertexSize = isClipping ? 2 : vertexSize; + + switch (attachment.constructor) { + case RegionAttachment: + const regionAttachment = attachment; + attachmentColor = regionAttachment.color; + numFloats = clippedVertexSize << 2; + regionAttachment.computeWorldVertices(slot, tempVerts, 0, clippedVertexSize); + triangles = SpineGenerator.QUAD_TRIANGLES; + uvs = regionAttachment.uvs; + texture = regionAttachment.region.texture; + break; + case MeshAttachment: + const meshAttachment = attachment; + attachmentColor = meshAttachment.color; + numFloats = (meshAttachment.worldVerticesLength >> 1) * clippedVertexSize; + if (numFloats > _vertices.length) { + SpineGenerator.tempVerts = new Array(numFloats); + } + meshAttachment.computeWorldVertices( + slot, + 0, + meshAttachment.worldVerticesLength, + tempVerts, + 0, + clippedVertexSize + ); + triangles = meshAttachment.triangles; + uvs = meshAttachment.uvs; + texture = meshAttachment.region.texture; + break; + case ClippingAttachment: + let clip = attachment; + _clipper.clipStart(slot, clip); + continue; + default: + _clipper.clipEndWithSlot(slot); + continue; + } + + if (texture != null) { + let finalVertices: ArrayLike; + let finalVerticesLength: number; + let finalIndices: ArrayLike; + let finalIndicesLength: number; + + const skeleton = slot.bone.skeleton; + const skeletonColor = skeleton.color; + const slotColor = slot.color; + const finalColor = SpineGenerator.tempColor; + const finalAlpha = skeletonColor.a * slotColor.a * attachmentColor.a; + + finalColor.r = skeletonColor.r * slotColor.r * attachmentColor.r; + finalColor.g = skeletonColor.g * slotColor.g * attachmentColor.g; + finalColor.b = skeletonColor.b * slotColor.b * attachmentColor.b; + finalColor.a = finalAlpha; + + if (premultipliedAlpha) { + finalColor.r *= finalAlpha; + finalColor.g *= finalAlpha; + finalColor.b *= finalAlpha; + } + + let darkColor = SpineGenerator.tempDark; + const slotDarkColor = slot.darkColor; + if (!slotDarkColor) { + darkColor.set(0, 0, 0, 1); + } else { + if (premultipliedAlpha) { + darkColor.r = slotDarkColor.r * finalAlpha; + darkColor.g = slotDarkColor.g * finalAlpha; + darkColor.b = slotDarkColor.b * finalAlpha; + } else { + darkColor.setFromColor(slotDarkColor); + } + } + + if (isClipping) { + _clipper.clipTriangles(tempVerts, triangles, triangles.length, uvs, finalColor, darkColor, tintBlack); + finalVertices = _clipper.clippedVertices; + finalVerticesLength = finalVertices.length; + finalIndices = _clipper.clippedTriangles; + finalIndicesLength = finalIndices.length; + } else { + const { r, g, b, a } = finalColor; + for (let v = 2, u = 0, n = numFloats; v < n; v += vertexSize, u += 2) { + tempVerts[v] = r; + tempVerts[v + 1] = g; + tempVerts[v + 2] = b; + tempVerts[v + 3] = a; + tempVerts[v + 4] = uvs[u]; + tempVerts[v + 5] = uvs[u + 1]; + if (tintBlack) { + tempVerts[v + 6] = darkColor.r; + tempVerts[v + 7] = darkColor.g; + tempVerts[v + 8] = darkColor.b; + tempVerts[v + 9] = darkColor.a; + } + } + finalVertices = tempVerts; + finalVerticesLength = numFloats; + finalIndices = triangles; + finalIndicesLength = triangles.length; + } + + if (finalVerticesLength == 0 || finalIndicesLength == 0) { + _clipper.clipEndWithSlot(slot); + continue; + } + + const stride = tintBlack ? vertexStrideWithTint : vertexStrideWithoutTint; + let indexStart = verticesLength / stride; + let i = verticesLength; + let j = 0; + for (; j < finalVerticesLength; ) { + let x = finalVertices[j++]; + let y = finalVertices[j++]; + _vertices[i++] = x; + _vertices[i++] = y; + _vertices[i++] = z; + _vertices[i++] = finalVertices[j++]; // u + _vertices[i++] = finalVertices[j++]; // v + _vertices[i++] = finalVertices[j++]; // r + _vertices[i++] = finalVertices[j++]; // g + _vertices[i++] = finalVertices[j++]; // b + _vertices[i++] = finalVertices[j++]; // a + if (tintBlack) { + _vertices[i++] = finalVertices[j++]; // darkR + _vertices[i++] = finalVertices[j++]; // darkG + _vertices[i++] = finalVertices[j++]; // darkB + j++; // darkA + } + this._expandBounds(x, y, z, _localBounds); + } + verticesLength = i; + + for (i = indicesLength, j = 0; j < finalIndicesLength; i++, j++) { + _indices[i] = finalIndices[j] + indexStart; + } + indicesLength += finalIndicesLength; + + const textureChanged = tempTexture !== null && tempTexture !== texture; + blend = slot.data.blendMode; + const blendModeChanged = tempBlendMode !== null && tempBlendMode !== blend; + + if (blendModeChanged || textureChanged) { + // Finish accumulated count first + if (count > 0) { + primitiveIndex = this._createRenderItem( + _subPrimitives, + primitiveIndex, + start, + count, + tempTexture, + tempBlendMode + ); + start += count; + count = 0; + } + } + count += finalIndicesLength; + tempTexture = texture; + tempBlendMode = blend; + } + + _clipper.clipEndWithSlot(slot); + } // slot traverse end + + // add reset sub primitive + if (count > 0) { + primitiveIndex = this._createRenderItem(_subPrimitives, primitiveIndex, start, count, texture, blend); + count = 0; + } + + _clipper.clipEnd(); + + const lastLen = _subPrimitives.length; + const curLen = _subRenderItems.length; + for (let i = curLen; i < lastLen; i++) { + const item = _subPrimitives[i]; + subPrimitivePool.return(item); + } + + renderer._clearSubPrimitives(); + for (let i = 0, l = curLen; i < l; ++i) { + const item = _subRenderItems[i]; + const { blendMode, texture } = item; + renderer._addSubPrimitive(item.subPrimitive); + const material = renderer._getMaterial(texture.getImage(), blendMode as unknown as SpineBlendMode); + renderer.setMaterial(i, material); + } + + if (indicesLength > _vertexCount || renderer._needResizeBuffer) { + renderer._createAndBindBuffer(indicesLength); + renderer._needResizeBuffer = false; + this.buildPrimitive(skeleton, renderer); + return; + } + + if (renderer._vertexBuffer) { + renderer._vertexBuffer.setData(_vertices); + renderer._indexBuffer.setData(_indices); + } + } + + private _createRenderItem( + subPrimitives: SubPrimitive[], + primitiveIndex: number, + start: number, + count: number, + texture: SpineTexture, + blend: BlendMode + ): number { + const { subPrimitivePool, subRenderItemPool } = SpineGenerator; + const origin = subPrimitives[primitiveIndex]; + + if (origin) { + primitiveIndex++; + } + + const subPrimitive = origin || subPrimitivePool.get(); + subPrimitive.start = start; + subPrimitive.count = count; + + const renderItem = subRenderItemPool.get(); + renderItem.blendMode = blend; + renderItem.subPrimitive = subPrimitive; + renderItem.texture = texture; + + this._subRenderItems.push(renderItem); + + return primitiveIndex; + } + + private _expandBounds(x: number, y: number, z: number, localBounds: BoundingBox) { + const { min, max } = localBounds; + min.set(Math.min(min.x, x), Math.min(min.y, y), Math.min(min.z, z)); + max.set(Math.max(max.x, x), Math.max(max.y, y), Math.max(max.z, z)); + } +} diff --git a/packages/spine-core-4.2/src/SpineTexture.ts b/packages/spine-core-4.2/src/SpineTexture.ts new file mode 100644 index 0000000000..5297919e1b --- /dev/null +++ b/packages/spine-core-4.2/src/SpineTexture.ts @@ -0,0 +1,49 @@ +import { Texture, TextureFilter, TextureWrap } from "@esotericsoftware/spine-core"; +import { Texture2D, TextureFilterMode, TextureWrapMode } from "@galacean/engine"; + +/** + * @internal + */ +export class SpineTexture extends Texture { + constructor(image: Texture2D) { + super(image); + image.generateMipmaps(); + } + + // rewrite getImage function, return galacean texture2D, then attachment can get size of texture + override getImage(): Texture2D { + return this._image; + } + + setFilters(minFilter: TextureFilter, magFilter: TextureFilter) { + if (minFilter === TextureFilter.Nearest) { + this._image.filterMode = TextureFilterMode.Point; + } else if (minFilter === TextureFilter.Linear) { + this._image.filterMode = TextureFilterMode.Bilinear; + } else { + // The remaining min filters are the MipMap* family (mag filters are never MipMap*); + // mipmaps are generated in the constructor. + this._image.filterMode = TextureFilterMode.Trilinear; + } + } + + setWraps(uWrap: TextureWrap, vWrap: TextureWrap) { + this._image.wrapModeU = this._convertWrapMode(uWrap); + this._image.wrapModeV = this._convertWrapMode(vWrap); + } + + dispose() {} + + private _convertWrapMode(wrap: TextureWrap): TextureWrapMode { + switch (wrap) { + case TextureWrap.ClampToEdge: + return TextureWrapMode.Clamp; + case TextureWrap.Repeat: + return TextureWrapMode.Repeat; + case TextureWrap.MirroredRepeat: + return TextureWrapMode.Mirror; + default: + throw new Error("Unsupported texture wrap mode."); + } + } +} diff --git a/packages/spine-core-4.2/src/index.ts b/packages/spine-core-4.2/src/index.ts new file mode 100644 index 0000000000..8822f0a362 --- /dev/null +++ b/packages/spine-core-4.2/src/index.ts @@ -0,0 +1,15 @@ +import { registerSpineRuntime } from "@galacean/engine-spine"; +import { Spine42Runtime } from "./Spine42Runtime"; + +// Self-register on import: `import "@galacean/engine-spine-core-4.2"` wires the 4.2 backend +// into the shared `@galacean/engine-spine` package. +registerSpineRuntime(new Spine42Runtime()); + +export { Spine42Runtime }; + +// Re-export the spine 4.2 runtime API. Power users that construct spine-core objects directly +// (custom attachments, manual skeleton parsing) import them from this core package. +export * from "@esotericsoftware/spine-core"; + +export const version = `__buildVersion`; +console.log(`Galacean spine-core-4.2 version: ${version}`); diff --git a/packages/spine-core-4.2/src/util/ClearablePool.ts b/packages/spine-core-4.2/src/util/ClearablePool.ts new file mode 100644 index 0000000000..ff8d6ccc6a --- /dev/null +++ b/packages/spine-core-4.2/src/util/ClearablePool.ts @@ -0,0 +1,26 @@ +export class ClearablePool { + private _type: new () => T; + private _elements: T[]; + private _usedElementCount: number = 0; + + constructor(type: new () => T) { + this._type = type; + this._elements = []; + } + + get(): T { + const { _usedElementCount: usedElementCount, _elements: elements } = this; + this._usedElementCount++; + if (elements.length === usedElementCount) { + const element = new this._type(); + elements.push(element); + return element; + } else { + return elements[usedElementCount]; + } + } + + clear(): void { + this._usedElementCount = 0; + } +} diff --git a/packages/spine-core-4.2/src/util/ReturnablePool.ts b/packages/spine-core-4.2/src/util/ReturnablePool.ts new file mode 100644 index 0000000000..77c1bcaa47 --- /dev/null +++ b/packages/spine-core-4.2/src/util/ReturnablePool.ts @@ -0,0 +1,25 @@ +export class ReturnablePool { + private _type: new () => T; + private _elements: T[]; + private _lastElementIndex: number; + + constructor(type: new () => T, initializeCount: number = 1) { + this._type = type; + this._lastElementIndex = initializeCount - 1; + const elements = (this._elements = new Array(initializeCount)); + for (let i = 0; i < initializeCount; ++i) { + elements[i] = new type(); + } + } + + get(): T { + if (this._lastElementIndex < 0) { + return new this._type(); + } + return this._elements[this._lastElementIndex--]; + } + + return(element: T): void { + this._elements[++this._lastElementIndex] = element; + } +} diff --git a/packages/spine-core-4.2/tsconfig.json b/packages/spine-core-4.2/tsconfig.json new file mode 100644 index 0000000000..588b0055c3 --- /dev/null +++ b/packages/spine-core-4.2/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "esnext", + "target": "esnext", + "declaration": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "declarationDir": "types", + "emitDeclarationOnly": true, + "noImplicitOverride": true, + "sourceMap": true, + "incremental": false, + "skipLibCheck": true, + "stripInternal": true + }, + "include": ["src/**/*"] +} diff --git a/packages/spine/package.json b/packages/spine/package.json new file mode 100644 index 0000000000..3f49b3b2e7 --- /dev/null +++ b/packages/spine/package.json @@ -0,0 +1,37 @@ +{ + "name": "@galacean/engine-spine", + "version": "0.0.0-experimental-2.0-game.19", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, + "repository": { + "url": "https://github.com/galacean/engine.git" + }, + "license": "MIT", + "main": "dist/main.js", + "module": "dist/module.js", + "debug": "src/index.ts", + "browser": "dist/browser.js", + "types": "types/index.d.ts", + "scripts": { + "b:types": "tsc" + }, + "umd": { + "name": "Galacean.Spine", + "globals": { + "@galacean/engine": "Galacean" + } + }, + "files": [ + "dist/**/*", + "types/**/*" + ], + "devDependencies": { + "@esotericsoftware/spine-core": "~4.2.66", + "@galacean/engine": "workspace:*" + }, + "peerDependencies": { + "@galacean/engine": "workspace:*" + } +} diff --git a/packages/spine/src/SpineConstant.ts b/packages/spine/src/SpineConstant.ts new file mode 100644 index 0000000000..679d01d2e9 --- /dev/null +++ b/packages/spine/src/SpineConstant.ts @@ -0,0 +1,13 @@ +/** + * Vertex stride (float count per vertex) of the spine vertex layout owned by `SpineAnimationRenderer`. + * + * - `withoutTint`: [x, y, z, u, v, r, g, b, a] = 9 floats + * - `withTint`: [x, y, z, u, v, r, g, b, a, dr, dg, db] = 12 floats + * + * Shared between the renderer (which declares the vertex elements and allocates buffers) and the + * version-specific generator in a spine core package (which fills them), so the two never drift. + */ +export const SpineVertexStride = { + withoutTint: 9, + withTint: 12 +} as const; diff --git a/packages/spine/src/enums/SpineBlendMode.ts b/packages/spine/src/enums/SpineBlendMode.ts new file mode 100644 index 0000000000..509a2b60af --- /dev/null +++ b/packages/spine/src/enums/SpineBlendMode.ts @@ -0,0 +1,14 @@ +/** + * Blend mode of a spine slot. + * + * @remarks + * Version-neutral mirror of spine-core's `BlendMode` enum. The values match spine-core across all + * supported runtime versions (3.8 / 4.2), so a core package can pass its `BlendMode` straight through. + * Owning it here keeps the shared package free of any runtime spine-core dependency. + */ +export enum SpineBlendMode { + Normal = 0, + Additive = 1, + Multiply = 2, + Screen = 3 +} diff --git a/packages/spine/src/index.ts b/packages/spine/src/index.ts new file mode 100644 index 0000000000..938f79117d --- /dev/null +++ b/packages/spine/src/index.ts @@ -0,0 +1,21 @@ +import * as LoaderObject from "./loader"; +import * as RendererObject from "./renderer"; +import { Loader } from "@galacean/engine"; + +for (let key in RendererObject) { + Loader.registerClass(key, RendererObject[key]); +} +for (let key in LoaderObject) { + Loader.registerClass(key, LoaderObject[key]); +} + +export * from "./loader/index"; +export * from "./renderer/index"; +export { SpineBlendMode } from "./enums/SpineBlendMode"; +export { SpineVertexStride } from "./SpineConstant"; +export { registerSpineRuntime, getSpineRuntime } from "./runtime/SpineRuntimeRegistry"; +export type { ISpineRuntime } from "./runtime/ISpineRuntime"; +export type { ISpineRenderTarget } from "./runtime/ISpineRenderTarget"; + +export const version = `__buildVersion`; +console.log(`Galacean spine version: ${version}`); diff --git a/packages/spine/src/loader/LoaderUtils.ts b/packages/spine/src/loader/LoaderUtils.ts new file mode 100644 index 0000000000..9ad7721420 --- /dev/null +++ b/packages/spine/src/loader/LoaderUtils.ts @@ -0,0 +1,96 @@ +import type { SkeletonData, TextureAtlas } from "@esotericsoftware/spine-core"; +import { AssetPromise, AssetType, Engine, Texture2D } from "@galacean/engine"; +import { getSpineRuntime } from "../runtime/SpineRuntimeRegistry"; + +/** + * @internal + */ +export class LoaderUtils { + static createSkeletonData( + skeletonRawData: string | ArrayBuffer, + textureAtlas: TextureAtlas, + skeletonDataScale: number + ): SkeletonData { + return getSpineRuntime().createSkeletonData(skeletonRawData, textureAtlas, skeletonDataScale); + } + + static createTextureAtlas(atlasText: string, textures: Texture2D[]): TextureAtlas { + return getSpineRuntime().createTextureAtlas(atlasText, textures); + } + + static loadTexturesByPaths( + imagePaths: string[], + imageExtensions: string[], + engine: Engine, + reject: (reason?: any) => void + ): Promise { + const resourceManager = engine.resourceManager; + // @ts-ignore + const virtualPathResourceMap = resourceManager._virtualPathResourceMap; + const texturePromises: AssetPromise[] = imagePaths.map((imagePath, index) => { + const ext = imageExtensions[index]; + let imageLoaderType = AssetType.Texture2D; + const virtualElement = virtualPathResourceMap[imagePath]; + if (virtualElement) { + imageLoaderType = virtualElement.type; + } else if (ext === "ktx") { + imageLoaderType = AssetType.KTX; + } else if (ext === "ktx2") { + imageLoaderType = AssetType.KTX2; + } + return resourceManager.load({ + url: imagePath, + type: imageLoaderType + }); + }); + return Promise.all(texturePromises).catch((error) => { + reject(error); + return []; + }); + } + + static loadTextureAtlas(atlasPath: string, engine: Engine, reject: (reason?: any) => void): Promise { + const baseUrl = LoaderUtils.getBaseUrl(atlasPath); + const resourceManager = engine.resourceManager; + let atlasText: string; + return ( + resourceManager + // @ts-ignore + ._request(atlasPath, { type: "text" }) + .then((text: string) => { + atlasText = text; + const runtime = getSpineRuntime(); + const pageNames = runtime.parseAtlasPageNames(atlasText); + const loadTexturePromises = pageNames.map((name) => { + const textureUrl = baseUrl + name; + return resourceManager.load({ + url: textureUrl, + type: AssetType.Texture2D + }) as unknown as Promise; + }); + return Promise.all(loadTexturePromises).then((textures) => { + return runtime.createTextureAtlas(atlasText, textures); + }); + }) + .catch((err) => { + reject(new Error(`Spine Atlas: ${atlasPath} load error: ${err}`)); + return null; + }) + ); + } + + static getBaseUrl(url: string): string { + const isLocalPath = !/^(http|https|ftp):\/\/.*/i.test(url); + if (isLocalPath) { + const lastSlashIndex = url.lastIndexOf("/"); + if (lastSlashIndex === -1) { + return ""; + } + return url.substring(0, lastSlashIndex + 1); + } else { + const parsedUrl = new URL(url); + const basePath = parsedUrl.origin + parsedUrl.pathname; + return basePath.endsWith("/") ? basePath : basePath.substring(0, basePath.lastIndexOf("/") + 1); + } + } +} diff --git a/packages/spine/src/loader/SpineAtlasLoader.ts b/packages/spine/src/loader/SpineAtlasLoader.ts new file mode 100644 index 0000000000..922f0fb7b0 --- /dev/null +++ b/packages/spine/src/loader/SpineAtlasLoader.ts @@ -0,0 +1,107 @@ +import type { TextureAtlas } from "@esotericsoftware/spine-core"; +import { AssetPromise, Loader, LoadItem, resourceLoader, ResourceManager } from "@galacean/engine"; +import { LoaderUtils } from "./LoaderUtils"; +import { SpineLoader } from "./SpineLoader"; + +interface SpineAtlasAsset { + atlasPath: string; + imagePaths?: string[]; + imageExtensions?: string[]; +} + +@resourceLoader("SpineAtlas", ["atlas"], false) +export class SpineAtlasLoader extends Loader { + private static _groupAssetsByExtension(url: string, assetPath: SpineAtlasAsset, resourceManager: ResourceManager) { + let ext = SpineLoader._getUrlExtension(url); + if (!ext) return; + + if (ext === "atlas") { + assetPath.atlasPath = url; + } + if (["png", "jpg", "webp", "jpeg", "ktx", "ktx2"].includes(ext)) { + assetPath.imagePaths.push(url); + } + } + + private static _assignAssetPathsFromUrl(url: string, assetPath: SpineAtlasAsset, resourceManager: ResourceManager) { + const ext = SpineLoader._getUrlExtension(url); + if (ext === "atlas") { + assetPath.atlasPath = url; + // @ts-ignore + const atlasDependency = resourceManager?._virtualPathResourceMap?.[url]?.dependentAssetMap; + if (atlasDependency) { + for (let key in atlasDependency) { + const imageVirtualPath = atlasDependency[key]; + assetPath.imagePaths.push(imageVirtualPath); + } + } + } + } + + load(item: LoadItem, resourceManager: ResourceManager): AssetPromise { + return new AssetPromise((resolve, reject) => { + const engine = resourceManager.engine; + const spineAtlasAsset = { + atlasPath: "", + imagePaths: [], + imageExtensions: [] + }; + + if (!item.urls) { + SpineAtlasLoader._assignAssetPathsFromUrl(item.url, spineAtlasAsset, resourceManager); + } else { + const urls = item.urls; + for (let i = 0, len = urls.length; i < len; i += 1) { + const url = urls[i]; + SpineAtlasLoader._groupAssetsByExtension(url, spineAtlasAsset, resourceManager); + } + } + + const { atlasPath } = spineAtlasAsset; + if (!atlasPath) { + reject( + new Error("Failed to load spine atlas. Please check the file path and ensure the file extension is included.") + ); + return; + } + + const imagePaths = spineAtlasAsset.imagePaths; + if (imagePaths.length === 0) { + // Use the parsed atlas path: `item.url` is undefined when the asset came in via `item.urls`. + LoaderUtils.loadTextureAtlas(atlasPath, engine, reject) + .then((textureAtlas) => { + resolve(textureAtlas); + }) + .catch((err) => { + reject(err); + }); + } else { + const { atlasPath, imagePaths, imageExtensions } = spineAtlasAsset; + if (imagePaths.length > 0) { + Promise.all([ + // @ts-ignore + resourceManager._request(atlasPath, { + type: "text" + }) as Promise, + LoaderUtils.loadTexturesByPaths(imagePaths, imageExtensions, engine, reject) + ]) + .then(([atlasText, textures]) => { + const textureAtlas = LoaderUtils.createTextureAtlas(atlasText, textures); + resolve(textureAtlas); + }) + .catch((err) => { + reject(err); + }); + } else { + LoaderUtils.loadTextureAtlas(atlasPath, engine, reject) + .then((textureAtlas) => { + resolve(textureAtlas); + }) + .catch((err) => { + reject(err); + }); + } + } + }); + } +} diff --git a/packages/spine/src/loader/SpineLoader.ts b/packages/spine/src/loader/SpineLoader.ts new file mode 100644 index 0000000000..41d613e822 --- /dev/null +++ b/packages/spine/src/loader/SpineLoader.ts @@ -0,0 +1,144 @@ +import type { TextureAtlas } from "@esotericsoftware/spine-core"; +import { AssetPromise, Loader, LoadItem, resourceLoader, ResourceManager } from "@galacean/engine"; +import { LoaderUtils } from "./LoaderUtils"; +import { SpineResource } from "./SpineResource"; + +type SpineAssetPath = { + atlasPath: string; + skeletonPath: string; + extraPaths: string[]; + fileExtensions?: string | string[]; +}; + +type SpineLoadContext = { + fileName: string; + spineAssetPath: SpineAssetPath; + skeletonRawData: string | ArrayBuffer; +}; + +export type SpineLoaderParams = { + fileExtensions?: string | string[]; +}; + +// Registering "json" makes this loader the default for bare `.json` urls (extension mappings are +// last-registration-wins, overriding the core JSONLoader) — matches the historical engine-spine +// behavior. Pass an explicit `type` (e.g. AssetType.JSON) to load plain JSON once this package +// is imported. +@resourceLoader("Spine", ["json", "bin", "skel"]) +export class SpineLoader extends Loader { + private static _decoder = new TextDecoder("utf-8"); + + private static _groupAssetsByExtension(url: string, assetPath: SpineAssetPath) { + const ext = SpineLoader._getUrlExtension(url); + if (!ext) return; + + if (["skel", "json", "bin"].includes(ext)) { + assetPath.skeletonPath = url; + } else if (ext === "atlas") { + assetPath.atlasPath = url; + } else { + assetPath.extraPaths.push(url); + } + } + + private static _assignAssetPathsFromUrl(url: string, assetPath: SpineAssetPath, resourceManager: ResourceManager) { + const ext = SpineLoader._getUrlExtension(url); + if (!ext) return; + assetPath.skeletonPath = url; + + // @ts-ignore + const skeletonDependency = resourceManager?._virtualPathResourceMap?.[url]?.dependentAssetMap; + if (skeletonDependency) { + assetPath.atlasPath = skeletonDependency.atlas; + } else { + const extensionPattern: RegExp = /\.(json|bin|skel)$/; + let baseUrl: string; + if (extensionPattern.test(url)) { + baseUrl = url.replace(extensionPattern, ""); + } + if (baseUrl) { + const atlasUrl = baseUrl + ".atlas"; + assetPath.atlasPath = atlasUrl; + } + } + } + + static _getUrlExtension(url: string): string | null { + const regex = /\.(\w+)(\?|$)/; + const match = url.match(regex); + return match ? match[1] : null; + } + + load(item: LoadItem, resourceManager: ResourceManager): AssetPromise { + return new AssetPromise((resolve, reject) => { + const spineLoadContext: SpineLoadContext = { + fileName: "", + skeletonRawData: "", + spineAssetPath: { + atlasPath: null, + skeletonPath: null, + extraPaths: [] + } + }; + const { spineAssetPath } = spineLoadContext; + if (!item.urls) { + SpineLoader._assignAssetPathsFromUrl(item.url, spineAssetPath, resourceManager); + } else { + const urls = item.urls; + for (let i = 0, len = urls.length; i < len; i += 1) { + const url = urls[i]; + SpineLoader._groupAssetsByExtension(url, spineAssetPath); + } + } + + const { skeletonPath, atlasPath } = spineAssetPath; + if (!skeletonPath || !atlasPath) { + reject( + new Error( + "Failed to load spine assets. Please check the file path and ensure the file extension is included." + ) + ); + return; + } + + resourceManager + // @ts-ignore + ._request(skeletonPath, { type: "arraybuffer" }) + .then((skeletonRawData: ArrayBuffer) => { + spineLoadContext.skeletonRawData = skeletonRawData; + const skeletonString = SpineLoader._decoder.decode(skeletonRawData); + if (skeletonString.startsWith("{")) { + spineLoadContext.skeletonRawData = skeletonString; + } + return this._loadAndCreateSpineResource(spineLoadContext, resourceManager); + }) + .then(resolve) + .catch(reject); + }); + } + + private _loadAndCreateSpineResource( + spineLoadContext: SpineLoadContext, + resourceManager: ResourceManager + ): Promise { + const { engine } = resourceManager; + const { skeletonRawData, spineAssetPath } = spineLoadContext; + const { skeletonPath, atlasPath, extraPaths } = spineAssetPath; + + const atlasLoadPromise = + extraPaths.length === 0 + ? (resourceManager.load({ + url: atlasPath, + type: "SpineAtlas" + }) as unknown as Promise) + : (resourceManager.load({ + urls: [atlasPath].concat(extraPaths), + type: "SpineAtlas" + }) as unknown as Promise); + + return atlasLoadPromise.then((textureAtlas) => { + const skeletonData = LoaderUtils.createSkeletonData(skeletonRawData, textureAtlas, 0.01); + return new SpineResource(engine, skeletonData, skeletonPath); + }); + } +} diff --git a/packages/spine/src/loader/SpineResource.ts b/packages/spine/src/loader/SpineResource.ts new file mode 100644 index 0000000000..a1532ad606 --- /dev/null +++ b/packages/spine/src/loader/SpineResource.ts @@ -0,0 +1,112 @@ +import type { + AnimationState, + AnimationStateData, + MeshAttachment, + RegionAttachment, + Skeleton, + SkeletonData +} from "@esotericsoftware/spine-core"; +import { Engine, Entity, ReferResource, Texture2D } from "@galacean/engine"; +import { SpineAnimationRenderer } from "../renderer/SpineAnimationRenderer"; +import { getSpineRuntime } from "../runtime/SpineRuntimeRegistry"; + +/** + * Represents a resource that manages Spine animation data, textures, and entity templates for the Galacean engine. + * + */ +export class SpineResource extends ReferResource { + /** The url of spine resource. */ + readonly url: string; + + private _texturesInSpineAtlas: Texture2D[] = []; + private _skeletonData: SkeletonData; + private _stateData: AnimationStateData; + + private _template: Entity; + + constructor(engine: Engine, skeletonData: SkeletonData, url?: string) { + super(engine); + this.url = url; + this._skeletonData = skeletonData; + this._stateData = getSpineRuntime().createAnimationStateData(skeletonData); + this._associationTextureInSkeletonData(skeletonData); + this._createTemplate(); + } + + /** + * The skeleton data associated with this Spine resource. + */ + get skeletonData(): SkeletonData { + return this._skeletonData; + } + + /** + * The animation state data associated with this Spine resource. + */ + get stateData(): AnimationStateData { + return this._stateData; + } + + /** + * Creates and returns a new instance of the spine entity template. + * @returns A instance of the spine entity template + */ + instantiate(): Entity { + return this._template.clone(); + } + + protected override _onDestroy(): void { + super._onDestroy(); + this._disassociationSuperResource(); + this._skeletonData = null; + this._stateData = null; + } + + private _createTemplate(): void { + const name = this._extractFileName(this.url); + const spineEntity = new Entity(this.engine, name); + const spineAnimationRenderer = spineEntity.addComponent(SpineAnimationRenderer); + const runtime = getSpineRuntime(); + const skeleton = runtime.createSkeleton(this._skeletonData); + const state = runtime.createAnimationState(this._stateData); + spineAnimationRenderer._setSkeleton(skeleton); + spineAnimationRenderer._setState(state); + // @ts-ignore + spineEntity._markAsTemplate(this); + this._template = spineEntity; + } + + private _associationTextureInSkeletonData(skeletonData: SkeletonData): void { + const { skins, slots } = skeletonData; + const textures = this._texturesInSpineAtlas; + + for (let i = 0, n = skins.length; i < n; i++) { + for (let j = 0, m = slots.length; j < m; j++) { + const slot = slots[j]; + const attachment = skins[i].getAttachment(slot.index, slot.name); + const texture = (attachment)?.region?.texture.getImage(); + if (texture) { + if (!textures.includes(texture)) { + textures.push(texture); + // @ts-ignore + texture._associationSuperResource(this); + } + } + } + } + } + + private _disassociationSuperResource(): void { + const textures = this._texturesInSpineAtlas; + for (let i = 0, n = textures.length; i < n; i++) { + // @ts-ignore + textures[i]._disassociationSuperResource(this); + } + } + + private _extractFileName(url: string): string { + if (!url) return "Spine Entity"; + const match = url.match(/\/([^\/]+?)(\.[^\/]*)?$/); + return match ? match[1] : "Spine Entity"; + } +} diff --git a/packages/spine/src/loader/index.ts b/packages/spine/src/loader/index.ts new file mode 100644 index 0000000000..b7367ba692 --- /dev/null +++ b/packages/spine/src/loader/index.ts @@ -0,0 +1,5 @@ +import "./SpineLoader"; +import "./SpineAtlasLoader"; + +export { SpineResource } from "./SpineResource"; +export { LoaderUtils } from "./LoaderUtils"; diff --git a/packages/spine/src/renderer/SpineAnimationRenderer.ts b/packages/spine/src/renderer/SpineAnimationRenderer.ts new file mode 100644 index 0000000000..2be54a129c --- /dev/null +++ b/packages/spine/src/renderer/SpineAnimationRenderer.ts @@ -0,0 +1,422 @@ +import type { AnimationState, Skeleton } from "@esotericsoftware/spine-core"; +import { + assignmentClone, + BoundingBox, + Buffer, + BufferBindFlag, + BufferUsage, + deepClone, + Entity, + ignoreClone, + IndexBufferBinding, + IndexFormat, + Material, + Primitive, + Renderer, + SubPrimitive, + Texture2D, + Vector3, + VertexBufferBinding, + VertexElement, + VertexElementFormat +} from "@galacean/engine"; +import { SpineResource } from "../loader/SpineResource"; +import { SpineMaterial } from "./SpineMaterial"; +import { SpineBlendMode } from "../enums/SpineBlendMode"; +import { SpineVertexStride } from "../SpineConstant"; +import { getSpineRuntime } from "../runtime/SpineRuntimeRegistry"; +import type { ISpineRenderTarget } from "../runtime/ISpineRenderTarget"; + +/** + * Spine animation renderer, capable of rendering spine animations and providing functions for animation and skeleton manipulation. + */ +export class SpineAnimationRenderer extends Renderer implements ISpineRenderTarget { + private static _positionVertexElement = new VertexElement("POSITION", 0, VertexElementFormat.Vector3, 0); + private static _lightColorVertexElement = new VertexElement("LIGHT_COLOR", 12, VertexElementFormat.Vector4, 0); + private static _uvVertexElement = new VertexElement("TEXCOORD_0", 28, VertexElementFormat.Vector2, 0); + private static _darkColorVertexElement = new VertexElement("DARK_COLOR", 36, VertexElementFormat.Vector3, 0); + + /** @internal */ + static _materialCacheMap = new Map(); + + /** + * The spacing between z layers in world units. + */ + @assignmentClone + zSpacing = 0.001; + + /** + * Whether to use premultiplied alpha mode for rendering. + * When enabled, vertex color values are multiplied by the alpha channel. + * @remarks + If this option is enabled, the Spine editor must export textures with "Premultiply Alpha" checked. + */ + @assignmentClone + premultipliedAlpha = false; + + @assignmentClone + private _tintBlack = false; + + /** + * Whether to enable tint black feature for dark color tinting. + * + * @remarks Should be enabled when using "Tint Black" feature in Spine editor. + */ + get tintBlack(): boolean { + return this._tintBlack; + } + + set tintBlack(value: boolean) { + if (this._tintBlack !== value) { + this._tintBlack = value; + this._needResizeBuffer = true; + } + } + + /** + * Default state for spine animation. + * Contains the default animation name to be played, whether this animation should loop, the default skin name. + */ + @deepClone + readonly defaultConfig: SpineAnimationDefaultConfig = new SpineAnimationDefaultConfig(); + + /** @internal */ + @ignoreClone + _primitive: Primitive; + /** @internal */ + @ignoreClone + _subPrimitives: SubPrimitive[] = []; + /** @internal */ + @ignoreClone + _indexBuffer: Buffer; + /** @internal */ + @ignoreClone + _vertexBuffer: Buffer; + /** @internal */ + @ignoreClone + _vertices = new Float32Array(); + /** @internal */ + @ignoreClone + _indices = new Uint16Array(); + /** @internal */ + @ignoreClone + _needResizeBuffer = false; + /** @internal */ + @ignoreClone + _vertexCount = 0; + /** @internal */ + @ignoreClone + _resource: SpineResource; + /** @internal */ + @ignoreClone + _localBounds = new BoundingBox( + new Vector3(Infinity, Infinity, Infinity), + new Vector3(-Infinity, -Infinity, -Infinity) + ); + + @ignoreClone + private _skeleton: Skeleton; + @ignoreClone + private _state: AnimationState; + + /** + * The Spine.AnimationState object of this SpineAnimationRenderer. + * Manage, blend, and transition between multiple simultaneous animations effectively. + */ + get state(): AnimationState { + return this._state; + } + + /** + * The Spine.Skeleton object of this SpineAnimationRenderer. + * Manipulate bone positions, rotations, scaling + * and change spine attachment to customize character appearances dynamically during runtime. + */ + get skeleton(): Skeleton { + return this._skeleton; + } + + /** + * @internal + */ + constructor(entity: Entity) { + super(entity); + const primitive = new Primitive(this.engine); + this._primitive = primitive; + this._primitive._addReferCount(1); + primitive.addVertexElement(SpineAnimationRenderer._positionVertexElement); + primitive.addVertexElement(SpineAnimationRenderer._lightColorVertexElement); + primitive.addVertexElement(SpineAnimationRenderer._uvVertexElement); + primitive.addVertexElement(SpineAnimationRenderer._darkColorVertexElement); + } + + /** + * @internal + */ + // @ts-ignore + override _onEnable(): void { + this._applyDefaultConfig(); + } + + /** + * @internal + */ + override update(delta: number): void { + const { _state: state, _skeleton: skeleton } = this; + if (!state || !skeleton) return; + const runtime = getSpineRuntime(); + runtime.updateState(skeleton, state, delta); + runtime.buildPrimitive(skeleton, this); + this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; + } + + /** + * @internal + */ + // @ts-ignore + override _render(context: any): void { + const { _primitive, _subPrimitives, _materials: materials } = this; + if (!_subPrimitives) return; + // Engine 2.0 render-element model: one RenderElement per sub-primitive (no sub-element pool). + const engine = this.engine as any; + const priority = this.priority; + const distanceForSort = (this as any)._distanceForSort; + const renderElementPool = engine._renderElementPool; + const renderPipeline = context.camera._renderPipeline; + for (let i = 0, n = _subPrimitives.length; i < n; i++) { + let material = materials[i]; + if (!material) { + continue; + } + if (material.destroyed || material.shader.destroyed) { + material = engine._basicResources.meshMagentaMaterial; + } + const renderElement = renderElementPool.get(); + renderElement.set(this, material, _primitive, _subPrimitives[i]); + renderElement.priority = priority; + renderElement.distanceForSort = distanceForSort; + renderPipeline.pushRenderElement(context, renderElement); + } + } + + /** + * @internal + */ + // @ts-ignore + override _updateBounds(worldBounds: BoundingBox): void { + BoundingBox.transform(this._localBounds, this.entity.transform.worldMatrix, worldBounds); + } + + /** + * @internal + */ + _setSkeleton(skeleton: Skeleton) { + this._skeleton = skeleton; + } + + /** + * @internal + */ + _setState(state: AnimationState) { + this._state = state; + } + + /** + * @internal + */ + // @ts-ignore + override _cloneTo(target: SpineAnimationRenderer): void { + // A renderer added manually and never bound to a resource has no skeleton/state to clone. + if (!this._skeleton || !this._state) return; + const runtime = getSpineRuntime(); + // Clones share the source's immutable SkeletonData and AnimationStateData (mix config), + // so mix times configured on the resource propagate to every instance; only the + // per-instance Skeleton / AnimationState are fresh. + const skeleton = runtime.createSkeleton(this._skeleton.data); + const state = runtime.createAnimationState(this._state.data); + target._setSkeleton(skeleton); + target._setState(state); + } + + /** + * @internal + */ + override _onDestroy(): void { + this._clearMaterialCache(); + this._subPrimitives.length = 0; + const primitive = this._primitive; + if (primitive) { + primitive._addReferCount(-1); + primitive.destroy(); + this._primitive = null; + } + // destroy() is refCount-guarded; the primitive released its buffer references above. + this._vertexBuffer?.destroy(); + this._indexBuffer?.destroy(); + this._vertexBuffer = null; + this._indexBuffer = null; + this._resource = null; + this._skeleton = null; + this._state = null; + super._onDestroy(); + } + + /** + * @internal + */ + _createAndBindBuffer(vertexCount: number): void { + const { engine: _engine, _primitive } = this; + const oldVertexBuffer = this._vertexBuffer; + const oldIndexBuffer = this._indexBuffer; + this._vertexCount = vertexCount; + const stride = this.tintBlack ? SpineVertexStride.withTint : SpineVertexStride.withoutTint; + this._vertices = new Float32Array(vertexCount * stride); + this._indices = new Uint16Array(vertexCount); + const vertexStride = stride << 2; + const vertexBuffer = new Buffer(_engine, BufferBindFlag.VertexBuffer, this._vertices, BufferUsage.Dynamic); + const indexBuffer = new Buffer(_engine, BufferBindFlag.IndexBuffer, this._indices, BufferUsage.Dynamic); + this._indexBuffer = indexBuffer; + this._vertexBuffer = vertexBuffer; + const vertexBufferBinding = new VertexBufferBinding(vertexBuffer, vertexStride); + this._primitive.setVertexBufferBinding(0, vertexBufferBinding); + const indexBufferBinding = new IndexBufferBinding(indexBuffer, IndexFormat.UInt16); + _primitive.setIndexBufferBinding(indexBufferBinding); + // Rebinding released the primitive's references to the old buffers; destroy() is + // refCount-guarded, so this frees their GPU resources without waiting for gc(). + oldVertexBuffer?.destroy(); + oldIndexBuffer?.destroy(); + } + + /** + * @internal + */ + _addSubPrimitive(subPrimitive: SubPrimitive): void { + this._subPrimitives.push(subPrimitive); + } + + /** + * @internal + */ + _clearSubPrimitives(): void { + this._subPrimitives.length = 0; + } + + /** + * @internal + */ + _getMaterial(texture: Texture2D, blendMode: SpineBlendMode): Material { + const engine = this.engine; + const premultipliedAlpha = this.premultipliedAlpha; + const tintBlack = this.tintBlack; + + // tintBlack must be part of the key: it toggles a per-material macro, so renderers that + // differ only in tintBlack cannot share a material. + const key = `${texture.instanceId}_${blendMode}_${premultipliedAlpha ? 1 : 0}_${tintBlack ? 1 : 0}`; + let cached = SpineAnimationRenderer._materialCacheMap.get(key); + if (!cached) { + cached = new SpineMaterial(engine); + cached.isGCIgnored = true; + cached._cacheKey = key; + SpineAnimationRenderer._materialCacheMap.set(key, cached); + } + cached._setBlendMode(blendMode, premultipliedAlpha); + cached._setTexture(texture); + cached._setTintBlack(tintBlack); + cached._setPremultipliedAlpha(premultipliedAlpha); + return cached; + } + + private _clearMaterialCache(): void { + const materialCache = SpineAnimationRenderer._materialCacheMap; + const materials = this._materials; + for (let i = 0, len = materials.length; i < len; i += 1) { + const material = materials[i]; + // `setMaterial` is public API, so entries may be user materials or null holes; a cached + // SpineMaterial is removed by the exact key it was registered under (recomputing the key + // from the renderer's current state would miss materials cached under older settings). + if (material instanceof SpineMaterial) { + materialCache.delete(material._cacheKey); + } + } + } + + private _applyDefaultConfig(): void { + const { skeleton, state } = this; + if (skeleton && state) { + const { animationName, skinName, loop } = this.defaultConfig; + if (skinName !== "default") { + skeleton.setSkinByName(skinName); + skeleton.setToSetupPose(); + } + if (animationName) { + state.setAnimation(0, animationName, loop); + } + } + } + + /** + * * @deprecated This property is deprecated and will be removed in future releases. + * Spine resource of current spine animation. + */ + get resource(): SpineResource { + return this._resource; + } + + /** + * * @deprecated This property is deprecated and will be removed in future releases. + * Sets the Spine resource for the current animation. This property allows switching to a different `SpineResource`. + * + * @param value - The new `SpineResource` to be used for the current animation + */ + set resource(value: SpineResource) { + if (!value) { + this._state = null; + this._skeleton = null; + this._resource = null; + return; + } + this._resource = value; + const { skeletonData, stateData } = value; + const runtime = getSpineRuntime(); + const skeleton = runtime.createSkeleton(skeletonData); + const state = runtime.createAnimationState(stateData); + this._setSkeleton(skeleton); + this._setState(state); + this._applyDefaultConfig(); + } +} + +/** + * @internal + */ +export enum RendererUpdateFlags { + /** Include world position and world bounds. */ + WorldVolume = 0x1 +} + +/** + * Default state for spine animation. + * Contains the default animation name to be played, whether this animation should loop, + * the default skin name, and the default scale of the skeleton. + */ +export class SpineAnimationDefaultConfig { + /** + * Creates an instance of default config + */ + constructor( + /** + * Whether the default animation should loop @defaultValue `true. The default animation should loop` + */ + public loop: boolean = true, + + /** + * The name of the default animation @defaultValue `null. Do not play any animation by default` + */ + public animationName: string | null = null, + + /** + * The name of the default skin @defaultValue `default` + */ + public skinName: string = "default" + ) {} +} diff --git a/packages/spine/src/renderer/SpineMaterial.ts b/packages/spine/src/renderer/SpineMaterial.ts new file mode 100644 index 0000000000..bf4bec3b97 --- /dev/null +++ b/packages/spine/src/renderer/SpineMaterial.ts @@ -0,0 +1,106 @@ +import { BlendFactor, Engine, Material, Shader, ShaderProperty, Texture2D } from "@galacean/engine"; +import { SpineBlendMode } from "../enums/SpineBlendMode"; + +const { SourceAlpha, One, DestinationColor, OneMinusSourceColor, OneMinusSourceAlpha } = BlendFactor; + +/** + * Material for spine rendering. + * + * @remarks + * The shader lives as a built-in engine shader (`2D/Spine`, registered by the engine's + * `ShaderPool`). Per-material blend factors are driven through shader data properties + * (`sourceColorBlendFactor` etc.), which the `2D/Spine` shader binds into its `BlendState`. + * The constant render state (transparent queue, depth-write off, cull off) is declared in + * the shader, so this material only varies the blend factors per blend mode. + */ +export class SpineMaterial extends Material { + private static _sourceColorBlendFactorProp = ShaderProperty.getByName("sourceColorBlendFactor"); + private static _destinationColorBlendFactorProp = ShaderProperty.getByName("destinationColorBlendFactor"); + private static _sourceAlphaBlendFactorProp = ShaderProperty.getByName("sourceAlphaBlendFactor"); + private static _destinationAlphaBlendFactorProp = ShaderProperty.getByName("destinationAlphaBlendFactor"); + + /** + * @internal + * The key this material is registered under in `SpineAnimationRenderer._materialCacheMap`. + */ + _cacheKey: string; + + private _blendMode: SpineBlendMode = SpineBlendMode.Normal; + + /** + * @internal + */ + _setTintBlack(enabled: boolean) { + if (enabled) { + this.shaderData.enableMacro("RENDERER_TINT_BLACK"); + } else { + this.shaderData.disableMacro("RENDERER_TINT_BLACK"); + } + } + + /** + * @internal + */ + _setPremultipliedAlpha(enabled: boolean) { + this.shaderData.setFloat("renderer_PremultipliedAlpha", enabled ? 1 : 0); + } + + /** + * @internal + */ + _setTexture(value: Texture2D) { + this.shaderData.setTexture("material_SpineTexture", value); + } + + constructor(engine: Engine) { + super(engine, Shader.find("2D/Spine")); + this._setBlendMode(SpineBlendMode.Normal, false); + } + + /** + * @internal + */ + _setBlendMode(blendMode: SpineBlendMode, premultipliedAlpha: boolean) { + const { shaderData } = this; + const { + _sourceColorBlendFactorProp: srcColor, + _destinationColorBlendFactorProp: dstColor, + _sourceAlphaBlendFactorProp: srcAlpha, + _destinationAlphaBlendFactorProp: dstAlpha + } = SpineMaterial; + this._blendMode = blendMode; + switch (blendMode) { + case SpineBlendMode.Additive: + shaderData.setInt(srcColor, premultipliedAlpha ? One : SourceAlpha); + shaderData.setInt(dstColor, One); + shaderData.setInt(srcAlpha, One); + shaderData.setInt(dstAlpha, One); + break; + case SpineBlendMode.Multiply: + shaderData.setInt(srcColor, DestinationColor); + shaderData.setInt(dstColor, OneMinusSourceAlpha); + shaderData.setInt(srcAlpha, One); + shaderData.setInt(dstAlpha, OneMinusSourceAlpha); + break; + case SpineBlendMode.Screen: + shaderData.setInt(srcColor, One); + shaderData.setInt(dstColor, OneMinusSourceColor); + shaderData.setInt(srcAlpha, One); + shaderData.setInt(dstAlpha, OneMinusSourceColor); + break; + default: // Normal + shaderData.setInt(srcColor, premultipliedAlpha ? One : SourceAlpha); + shaderData.setInt(dstColor, OneMinusSourceAlpha); + shaderData.setInt(srcAlpha, One); + shaderData.setInt(dstAlpha, OneMinusSourceAlpha); + break; + } + } + + /** + * @internal + */ + _getBlendMode(): SpineBlendMode { + return this._blendMode; + } +} diff --git a/packages/spine/src/renderer/index.ts b/packages/spine/src/renderer/index.ts new file mode 100644 index 0000000000..aca750bda7 --- /dev/null +++ b/packages/spine/src/renderer/index.ts @@ -0,0 +1,2 @@ +export { SpineAnimationRenderer } from "./SpineAnimationRenderer"; +export { SpineMaterial } from "./SpineMaterial"; diff --git a/packages/spine/src/runtime/ISpineRenderTarget.ts b/packages/spine/src/runtime/ISpineRenderTarget.ts new file mode 100644 index 0000000000..50133ae9f8 --- /dev/null +++ b/packages/spine/src/runtime/ISpineRenderTarget.ts @@ -0,0 +1,30 @@ +import type { BoundingBox, Buffer, Material, SubPrimitive, Texture2D } from "@galacean/engine"; +import type { SpineBlendMode } from "../enums/SpineBlendMode"; + +/** + * The write target a spine core package's generator fills while building the renderable primitive. + * + * @remarks + * Implemented by `SpineAnimationRenderer`. This is the narrow, spine-core-free contract that lets the + * version-specific generator live in a core package while the buffer/material/bounds bookkeeping stays + * in the shared renderer — the analogue of how engine physics colliders expose a native handle. + */ +export interface ISpineRenderTarget { + readonly _vertices: Float32Array; + readonly _indices: Uint16Array; + readonly _vertexCount: number; + readonly _localBounds: BoundingBox; + readonly _subPrimitives: SubPrimitive[]; + readonly _vertexBuffer: Buffer; + readonly _indexBuffer: Buffer; + readonly zSpacing: number; + readonly premultipliedAlpha: boolean; + readonly tintBlack: boolean; + _needResizeBuffer: boolean; + + _createAndBindBuffer(vertexCount: number): void; + _addSubPrimitive(subPrimitive: SubPrimitive): void; + _clearSubPrimitives(): void; + _getMaterial(texture: Texture2D, blendMode: SpineBlendMode): Material; + setMaterial(index: number, material: Material): void; +} diff --git a/packages/spine/src/runtime/ISpineRuntime.ts b/packages/spine/src/runtime/ISpineRuntime.ts new file mode 100644 index 0000000000..8f3d4202ea --- /dev/null +++ b/packages/spine/src/runtime/ISpineRuntime.ts @@ -0,0 +1,52 @@ +import type { + AnimationState, + AnimationStateData, + Skeleton, + SkeletonData, + TextureAtlas +} from "@esotericsoftware/spine-core"; +import type { Texture2D } from "@galacean/engine"; +import type { ISpineRenderTarget } from "./ISpineRenderTarget"; + +/** + * The version-specific spine runtime backend. + * + * @remarks + * The engine-spine analogue of `IPhysics`: a narrow factory + stepping interface that a spine core + * package (e.g. `@galacean/engine-spine-core-4.2`) implements and registers via {@link registerSpineRuntime}. + * It owns everything that differs between spine runtime versions — parsing, object-model construction, + * world-transform stepping (e.g. spine 4.2's `Physics` argument), and the attachment-reading mesh + * generator — so the shared renderer/loader never reference a concrete spine-core version at runtime. + * + * The spine-core types in these signatures are the 4.2 line, used as the canonical type contract; a + * 3.8 backend returns structurally-3.8 objects typed against the same contract. + */ +export interface ISpineRuntime { + /** Parse the page names declared in an atlas text, used to resolve which page textures to load. */ + parseAtlasPageNames(atlasText: string): string[]; + + /** Build a `TextureAtlas`, binding the given engine textures to its pages (matched by page name). */ + createTextureAtlas(atlasText: string, textures: Texture2D[]): TextureAtlas; + + /** Parse skeleton data (json or binary) against an atlas. */ + createSkeletonData( + skeletonRawData: string | ArrayBuffer, + textureAtlas: TextureAtlas, + skeletonDataScale: number + ): SkeletonData; + + /** Create the shared animation state data for a skeleton data. */ + createAnimationStateData(skeletonData: SkeletonData): AnimationStateData; + + /** Create a skeleton instance. */ + createSkeleton(skeletonData: SkeletonData): Skeleton; + + /** Create an animation state instance. */ + createAnimationState(stateData: AnimationStateData): AnimationState; + + /** Advance the animation state and apply it to the skeleton, then update world transforms. */ + updateState(skeleton: Skeleton, state: AnimationState, delta: number): void; + + /** Build the renderable primitive for the current skeleton pose into the given target. */ + buildPrimitive(skeleton: Skeleton, target: ISpineRenderTarget): void; +} diff --git a/packages/spine/src/runtime/SpineRuntimeRegistry.ts b/packages/spine/src/runtime/SpineRuntimeRegistry.ts new file mode 100644 index 0000000000..df865bc687 --- /dev/null +++ b/packages/spine/src/runtime/SpineRuntimeRegistry.ts @@ -0,0 +1,25 @@ +import type { ISpineRuntime } from "./ISpineRuntime"; + +let _runtime: ISpineRuntime | null = null; + +/** + * Register the spine runtime backend. Called for its side effect when a spine core package + * (e.g. `@galacean/engine-spine-core-4.2`) is imported. The last registration wins. + */ +export function registerSpineRuntime(runtime: ISpineRuntime): void { + _runtime = runtime; +} + +/** + * Get the registered spine runtime backend. + * @throws If no core package has been imported. + */ +export function getSpineRuntime(): ISpineRuntime { + if (!_runtime) { + throw new Error( + "@galacean/engine-spine: no spine runtime registered. Import a spine core package, " + + 'e.g. `import "@galacean/engine-spine-core-4.2"`.' + ); + } + return _runtime; +} diff --git a/packages/spine/tsconfig.json b/packages/spine/tsconfig.json new file mode 100644 index 0000000000..588b0055c3 --- /dev/null +++ b/packages/spine/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "esnext", + "target": "esnext", + "declaration": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "declarationDir": "types", + "emitDeclarationOnly": true, + "noImplicitOverride": true, + "sourceMap": true, + "incremental": false, + "skipLibCheck": true, + "stripInternal": true + }, + "include": ["src/**/*"] +} diff --git a/packages/ui/package.json b/packages/ui/package.json index 4b51ee5bc2..82632deda9 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-ui", - "version": "2.0.0-alpha.24", + "version": "0.0.0-experimental-2.0-game.19", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/ui/src/Utils.ts b/packages/ui/src/Utils.ts index f47e0a8053..60040d8e75 100644 --- a/packages/ui/src/Utils.ts +++ b/packages/ui/src/Utils.ts @@ -1,10 +1,67 @@ -import { Entity } from "@galacean/engine"; +import { Entity, Matrix, Plane, Ray, Vector2, Vector3 } from "@galacean/engine"; +import { UITransform } from "./component"; import { RootCanvasModifyFlags, UICanvas } from "./component/UICanvas"; import { GroupModifyFlags, UIGroup } from "./component/UIGroup"; +import { CanvasRenderMode } from "./enums/CanvasRenderMode"; import { IElement } from "./interface/IElement"; import { IGroupAble } from "./interface/IGroupAble"; export class Utils { + static _tempRay: Ray = new Ray(); + static _tempPlane: Plane = new Plane(); + static _tempVec3: Vector3 = new Vector3(); + static _tempMat: Matrix = new Matrix(); + + /** + * Local position of a screen point in the component + */ + static screenToLocalPoint(position: Vector2, transform: UITransform, out: Vector3): Boolean { + const engine = transform.engine; + // Get root canvas + let entity = transform.entity; + let rootCanvas: UICanvas; + while (entity) { + // @ts-ignore + const components = entity._components; + for (let i = 0, n = components.length; i < n; i++) { + const component = components[i]; + if (component.enabled && component instanceof UICanvas && component._isRootCanvas) { + rootCanvas = component; + } + } + entity = entity.parent; + } + if (!rootCanvas) return false; + // Calculate ray + const ray = this._tempRay; + switch (rootCanvas._realRenderMode) { + case CanvasRenderMode.ScreenSpaceOverlay: + // Screen to world ( Assume that world units have a one-to-one relationship with pixel units ) + ray.origin.set(position.x, engine.canvas.height - position.y, 1); + ray.direction.set(0, 0, -1); + break; + case CanvasRenderMode.ScreenSpaceCamera: + rootCanvas.renderCamera.screenPointToRay(position, ray); + break; + default: + // World space not yet supported, see issue #2793 + return false; + } + // Intersect ray with UI plane to get local coordinates + const plane = this._tempPlane; + const normal = plane.normal.copyFrom(transform.worldForward); + plane.distance = -Vector3.dot(normal, transform.worldPosition); + const curDistance = ray.intersectPlane(plane); + if (curDistance >= 0 && curDistance < Number.MAX_SAFE_INTEGER) { + const hitPointWorld = ray.getPoint(curDistance, this._tempVec3); + const worldMatrixInv = this._tempMat; + Matrix.invert(transform.worldMatrix, worldMatrixInv); + Vector3.transformCoordinate(hitPointWorld, worldMatrixInv, out); + return true; + } + return false; + } + static setRootCanvasDirty(element: IElement): void { if (element._isRootCanvasDirty) return; element._isRootCanvasDirty = true; diff --git a/packages/ui/src/component/UICanvas.ts b/packages/ui/src/component/UICanvas.ts index 9e5b33c405..77dad56014 100644 --- a/packages/ui/src/component/UICanvas.ts +++ b/packages/ui/src/component/UICanvas.ts @@ -25,6 +25,7 @@ import { ResolutionAdaptationMode } from "../enums/ResolutionAdaptationMode"; import { UIHitResult } from "../input/UIHitResult"; import { IElement } from "../interface/IElement"; import { IGroupAble } from "../interface/IGroupAble"; +import { RectMask2D } from "./advanced/RectMask2D"; import { UIGroup } from "./UIGroup"; import { UIRenderer } from "./UIRenderer"; import { UITransform } from "./UITransform"; @@ -39,6 +40,7 @@ export class UICanvas extends Component implements IElement { /** @internal */ static _hierarchyCounter: number = 1; private static _tempGroupAbleList: IGroupAble[] = []; + private static _tempRectMaskList: RectMask2D[] = []; private static _tempVec3: Vector3 = new Vector3(); private static _tempMat: Matrix = new Matrix(); @@ -62,7 +64,7 @@ export class UICanvas extends Component implements IElement { _isRootCanvas: boolean = false; /** @internal */ @ignoreClone - _renderElement: any; + _renderElements: any[] = []; /** @internal */ @ignoreClone _sortDistance: number = 0; @@ -307,11 +309,10 @@ export class UICanvas extends Component implements IElement { const { engine, _realRenderMode: mode } = this; const { enableFrustumCulling, cullingMask, _frustum: frustum } = context.camera; const { frameCount } = engine.time; - // @ts-ignore - const renderElement = (this._renderElement = engine._renderElementPool.get()); + const renderElements = this._renderElements; + renderElements.length = 0; const virtualCamera = context.virtualCamera; this._updateSortDistance(virtualCamera.isOrthographic, virtualCamera.position, virtualCamera.forward); - renderElement.set(this.sortOrder, this._sortDistance); const { width, height } = engine.canvas; const renderers = this._getRenderers(); for (let i = 0, n = renderers.length; i < n; i++) { @@ -418,7 +419,8 @@ export class UICanvas extends Component implements IElement { const { _orderedRenderers: renderers, entity } = this; const uiHierarchyVersion = entity._uiHierarchyVersion; if (this._hierarchyVersion !== uiHierarchyVersion) { - renderers.length = this._walk(this.entity, renderers); + UICanvas._tempRectMaskList.length = 0; + renderers.length = this._walk(this.entity, renderers, 0, null, 0); UICanvas._tempGroupAbleList.length = 0; this._hierarchyVersion = uiHierarchyVersion; ++UICanvas._hierarchyCounter; @@ -500,10 +502,18 @@ export class UICanvas extends Component implements IElement { transform.size.set(curWidth / expectX, curHeight / expectY); } - private _walk(entity: Entity, renderers: UIRenderer[], depth = 0, group: UIGroup = null): number { + private _walk( + entity: Entity, + renderers: UIRenderer[], + depth = 0, + group: UIGroup = null, + rectMaskCount: number = 0 + ): number { // @ts-ignore const components: Component[] = entity._components; const tempGroupAbleList = UICanvas._tempGroupAbleList; + const tempRectMaskList = UICanvas._tempRectMaskList; + let rectMask: RectMask2D = null; let groupAbleCount = 0; for (let i = 0, n = components.length; i < n; i++) { const component = components[i]; @@ -515,11 +525,14 @@ export class UICanvas extends Component implements IElement { if (component._isGroupDirty) { tempGroupAbleList[groupAbleCount++] = component; } + component._setRectMasks(tempRectMaskList, rectMaskCount); } else if (component instanceof UIInteractive) { component._isRootCanvasDirty && Utils.setRootCanvas(component, this); if (component._isGroupDirty) { tempGroupAbleList[groupAbleCount++] = component; } + } else if (component instanceof RectMask2D) { + rectMask = component; } else if (component instanceof UIGroup) { component._isRootCanvasDirty && Utils.setRootCanvas(component, this); component._isGroupDirty && Utils.setGroup(component, group); @@ -529,10 +542,13 @@ export class UICanvas extends Component implements IElement { for (let i = 0; i < groupAbleCount; i++) { Utils.setGroup(tempGroupAbleList[i], group); } + if (rectMask) { + tempRectMaskList[rectMaskCount++] = rectMask; + } const children = entity.children; for (let i = 0, n = children.length; i < n; i++) { const child = children[i]; - child.isActive && (depth = this._walk(child, renderers, depth, group)); + child.isActive && (depth = this._walk(child, renderers, depth, group, rectMaskCount)); } return depth; } diff --git a/packages/ui/src/component/UIRenderer.ts b/packages/ui/src/component/UIRenderer.ts index 59a2fc434c..527ad3bd23 100644 --- a/packages/ui/src/component/UIRenderer.ts +++ b/packages/ui/src/component/UIRenderer.ts @@ -1,5 +1,5 @@ import { - BatchUtils, + VertexMergeBatcher, Color, DependentMode, Entity, @@ -11,22 +11,28 @@ import { RendererUpdateFlags, ShaderMacroCollection, ShaderProperty, + SpriteMaskInteraction, + SpriteMaskLayer, Vector3, Vector4, assignmentClone, deepClone, dependentComponents, - ignoreClone + ignoreClone, + Vector2 } from "@galacean/engine"; import { Utils } from "../Utils"; import { UIHitResult } from "../input/UIHitResult"; import { IGraphics } from "../interface/IGraphics"; +import { RectMask2D } from "./advanced/RectMask2D"; import { EntityUIModifyFlags, UICanvas } from "./UICanvas"; import { GroupModifyFlags, UIGroup } from "./UIGroup"; import { UITransform } from "./UITransform"; @dependentComponents(UITransform, DependentMode.AutoAdd) export class UIRenderer extends Renderer implements IGraphics { + /** @internal */ + static _tempVec20: Vector2 = new Vector2(); /** @internal */ static _tempVec30: Vector3 = new Vector3(); /** @internal */ @@ -37,6 +43,16 @@ export class UIRenderer extends Renderer implements IGraphics { static _tempPlane: Plane = new Plane(); /** @internal */ static _textureProperty: ShaderProperty = ShaderProperty.getByName("renderer_UITexture"); + /** @internal */ + static _rectClipRectProperty: ShaderProperty = ShaderProperty.getByName("renderer_UIRectClipRect"); + /** @internal */ + static _rectClipEnabledProperty: ShaderProperty = ShaderProperty.getByName("renderer_UIRectClipEnabled"); + /** @internal */ + static _rectClipSoftnessProperty: ShaderProperty = ShaderProperty.getByName("renderer_UIRectClipSoftness"); + /** @internal */ + static _rectClipHardClipProperty: ShaderProperty = ShaderProperty.getByName("renderer_UIRectClipHardClip"); + /** @internal */ + static _tempRect: Vector4 = new Vector4(); /** * Custom boundary for raycast detection. @@ -69,9 +85,24 @@ export class UIRenderer extends Renderer implements IGraphics { /** @internal */ @ignoreClone _subChunk; + /** @internal */ + @ignoreClone + _rectMasks: RectMask2D[] = []; + /** @internal */ + @ignoreClone + _rectMaskRect: Vector4 = new Vector4(); + /** @internal */ + @ignoreClone + _rectMaskEnabled: boolean = false; + /** @internal */ + @ignoreClone + _rectMaskSoftness: Vector4 = new Vector4(); + /** @internal */ + @ignoreClone + _rectMaskHardClip: boolean = false; @assignmentClone - private _raycastEnabled: boolean = true; + private _raycastEnabled: boolean = false; @deepClone protected _color: Color = new Color(1, 1, 1, 1); @@ -88,6 +119,30 @@ export class UIRenderer extends Renderer implements IGraphics { } } + /** + * The mask layer the ui renderer belongs to. + */ + get maskLayer(): SpriteMaskLayer { + return this._maskLayer; + } + + set maskLayer(value: SpriteMaskLayer) { + this._maskLayer = value; + } + + /** + * Interacts with the masks. + */ + get maskInteraction(): SpriteMaskInteraction { + return this._maskInteraction; + } + + set maskInteraction(value: SpriteMaskInteraction) { + if (this._maskInteraction !== value) { + this._maskInteraction = value; + } + } + /** * Whether this renderer be picked up by raycast. */ @@ -110,22 +165,25 @@ export class UIRenderer extends Renderer implements IGraphics { this._color._onValueChanged = this._onColorChanged; this._groupListener = this._groupListener.bind(this); this._rootCanvasListener = this._rootCanvasListener.bind(this); + this.shaderData.setFloat(UIRenderer._rectClipEnabledProperty, 0); + this.shaderData.setVector4(UIRenderer._rectClipSoftnessProperty, this._rectMaskSoftness); + this.shaderData.setFloat(UIRenderer._rectClipHardClipProperty, 0); } // @ts-ignore - override _canBatch(elementA, elementB): boolean { - return BatchUtils.canBatchSprite(elementA, elementB); + override _canBatch(preElement, curElement): boolean { + return VertexMergeBatcher.canBatchSprite(preElement, curElement); } // @ts-ignore - override _batch(elementA, elementB?): void { - BatchUtils.batchFor2D(elementA, elementB); + override _batch(preElement, curElement): void { + VertexMergeBatcher.batch(preElement, curElement); } // @ts-ignore - override _updateTransformShaderData(context, onlyMVP: boolean, batched: boolean): void { + override _updateTransformShaderData(context, onlyMVP: boolean): void { // @ts-ignore - super._updateTransformShaderData(context, onlyMVP, true); + this._updateWorldSpaceTransformShaderData(context, onlyMVP); } // @ts-ignore @@ -135,6 +193,7 @@ export class UIRenderer extends Renderer implements IGraphics { this._update(context); } + this._updateRectMaskClipState(); this._render(context); // union camera global macro and renderer macro. @@ -237,6 +296,17 @@ export class UIRenderer extends Renderer implements IGraphics { return this.engine._batcherManager.primitiveChunkManagerUI; } + /** + * @internal + */ + _setRectMasks(rectMasks: RectMask2D[], count: number): void { + const targetMasks = this._rectMasks; + targetMasks.length = count; + for (let i = 0; i < count; i++) { + targetMasks[i] = rectMasks[i]; + } + } + /** * @internal */ @@ -252,7 +322,11 @@ export class UIRenderer extends Renderer implements IGraphics { Matrix.invert(transform.worldMatrix, worldMatrixInv); const localPosition = UIRenderer._tempVec31; Vector3.transformCoordinate(hitPointWorld, worldMatrixInv, localPosition); - if (this._hitTest(localPosition)) { + if ( + this._hitTest(localPosition) && + this._isRaycastVisibleByRectMask(hitPointWorld) && + this._isRaycastVisibleByMask(hitPointWorld) + ) { out.component = this; out.distance = curDistance; out.entity = this.entity; @@ -278,6 +352,143 @@ export class UIRenderer extends Renderer implements IGraphics { ); } + private _isRaycastVisibleByMask(hitPointWorld: Vector3): boolean { + const maskInteraction = this._maskInteraction; + if (maskInteraction === SpriteMaskInteraction.None) { + return true; + } + // @ts-ignore + return this.scene._maskManager.isVisibleByMask(maskInteraction, this._maskLayer, hitPointWorld); + } + + private _isRaycastVisibleByRectMask(hitPointWorld: Vector3): boolean { + const rectMasks = this._rectMasks; + for (let i = 0, n = rectMasks.length; i < n; i++) { + const rectMask = rectMasks[i]; + if (!rectMask.enabled || !rectMask.entity.isActiveInHierarchy) { + continue; + } + if (!rectMask._containsWorldPoint(hitPointWorld)) { + return false; + } + } + return true; + } + + private _updateRectMaskClipState(): void { + const rectMasks = this._rectMasks; + const count = rectMasks.length; + if (count <= 0) { + this._resetRectMaskClipState(); + return; + } + + let minX = Number.NEGATIVE_INFINITY; + let minY = Number.NEGATIVE_INFINITY; + let maxX = Number.POSITIVE_INFINITY; + let maxY = Number.POSITIVE_INFINITY; + let clipSoftnessLeft = 0; + let clipSoftnessBottom = 0; + let clipSoftnessRight = 0; + let clipSoftnessTop = 0; + let clipHardClip = false; + let hasActiveMask = false; + const tempRect = UIRenderer._tempRect; + for (let i = 0; i < count; i++) { + const rectMask = rectMasks[i]; + if (!rectMask.enabled || !rectMask.entity.isActiveInHierarchy) { + continue; + } + hasActiveMask = true; + const softness = rectMask.softness; + if (!clipHardClip && rectMask.alphaClip) { + clipHardClip = true; + } + if (!rectMask._getWorldRect(tempRect)) { + minX = 1; + minY = 1; + maxX = 0; + maxY = 0; + break; + } + if (tempRect.x > minX) { + minX = tempRect.x; + clipSoftnessLeft = softness.x; + } + if (tempRect.y > minY) { + minY = tempRect.y; + clipSoftnessBottom = softness.y; + } + if (tempRect.z < maxX) { + maxX = tempRect.z; + clipSoftnessRight = softness.x; + } + if (tempRect.w < maxY) { + maxY = tempRect.w; + clipSoftnessTop = softness.y; + } + } + + if (!hasActiveMask) { + this._resetRectMaskClipState(); + return; + } + + if (minX >= maxX || minY >= maxY) { + minX = 1; + minY = 1; + maxX = 0; + maxY = 0; + clipSoftnessLeft = 0; + clipSoftnessBottom = 0; + clipSoftnessRight = 0; + clipSoftnessTop = 0; + } + + const rectMaskRect = this._rectMaskRect; + if (rectMaskRect.x !== minX || rectMaskRect.y !== minY || rectMaskRect.z !== maxX || rectMaskRect.w !== maxY) { + rectMaskRect.set(minX, minY, maxX, maxY); + this.shaderData.setVector4(UIRenderer._rectClipRectProperty, rectMaskRect); + } + + const rectMaskSoftness = this._rectMaskSoftness; + if ( + rectMaskSoftness.x !== clipSoftnessLeft || + rectMaskSoftness.y !== clipSoftnessBottom || + rectMaskSoftness.z !== clipSoftnessRight || + rectMaskSoftness.w !== clipSoftnessTop + ) { + rectMaskSoftness.set(clipSoftnessLeft, clipSoftnessBottom, clipSoftnessRight, clipSoftnessTop); + this.shaderData.setVector4(UIRenderer._rectClipSoftnessProperty, rectMaskSoftness); + } + + if (this._rectMaskHardClip !== clipHardClip) { + this._rectMaskHardClip = clipHardClip; + this.shaderData.setFloat(UIRenderer._rectClipHardClipProperty, clipHardClip ? 1 : 0); + } + + if (!this._rectMaskEnabled) { + this._rectMaskEnabled = true; + this.shaderData.setFloat(UIRenderer._rectClipEnabledProperty, 1); + } + } + + private _resetRectMaskClipState(): void { + if (this._rectMaskEnabled) { + this._rectMaskEnabled = false; + this.shaderData.setFloat(UIRenderer._rectClipEnabledProperty, 0); + } + const rectMaskSoftness = this._rectMaskSoftness; + if (rectMaskSoftness.x !== 0 || rectMaskSoftness.y !== 0 || rectMaskSoftness.z !== 0 || rectMaskSoftness.w !== 0) { + rectMaskSoftness.set(0, 0, 0, 0); + this.shaderData.setVector4(UIRenderer._rectClipSoftnessProperty, rectMaskSoftness); + } + if (this._rectMaskHardClip) { + this._rectMaskHardClip = false; + this.shaderData.setFloat(UIRenderer._rectClipHardClipProperty, 0); + } + } + protected override _onDestroy(): void { if (this._subChunk) { this._getChunkManager().freeSubChunk(this._subChunk); @@ -287,6 +498,8 @@ export class UIRenderer extends Renderer implements IGraphics { //@ts-ignore this._color._onValueChanged = null; this._color = null; + this._rectMasks = null; + this._rectMaskSoftness = null; } } diff --git a/packages/ui/src/component/UITransform.ts b/packages/ui/src/component/UITransform.ts index 9144373472..28e0087575 100644 --- a/packages/ui/src/component/UITransform.ts +++ b/packages/ui/src/component/UITransform.ts @@ -251,7 +251,49 @@ export class UITransform extends Transform { */ _parentChange(): void { this._isParentDirty = true; - this._updateWorldFlagWithParentRectChange(TransformModifyFlags.WmWpWeWqWsWus); + // Reparent invalidates the world state of the entire subtree: + // 1) `_updateWorldFlagWithParentRectChange` has an early-exit that skips + // propagation when self's world dirty flags are already all set — + // invalid after reparent. + // 2) Descendants may have cached a stale `_parentTransformCache` + // (e.g. `_getParentTransform` called while ancestor chain was partially + // constructed during clone/instantiate). Force them to re-resolve on + // next access. + this._propagateReparentDirtyUI(TransformModifyFlags.WmWpWeWqWsWus); + } + + private _propagateReparentDirtyUI(flags: number): void { + let selfChange = false; + const { _horizontalAlignment: horizontalAlignment, _verticalAlignment: verticalAlignment } = this; + if (horizontalAlignment || verticalAlignment) { + if ( + horizontalAlignment === HorizontalAlignmentMode.LeftAndRight || + verticalAlignment === VerticalAlignmentMode.TopAndBottom + ) { + this._updateSizeByAlignment(); + this._updateRectBySizeAndPivot(); + selfChange = true; + } + this._updatePositionByAlignment(); + this._setDirtyFlagTrue(TransformModifyFlags.LocalMatrix); + flags |= TransformModifyFlags.WmWp; + } + this._worldAssociatedChange(flags); + const children = this.entity.children; + for (let i = 0, n = children.length; i < n; i++) { + const transform = children[i].transform as any; + if (!transform) continue; + transform._isParentDirty = true; + if (typeof transform._propagateReparentDirtyUI === "function") { + transform._propagateReparentDirtyUI(flags); + } else if (typeof transform._propagateReparentDirty === "function") { + transform._propagateReparentDirty(flags); + } + } + if (selfChange) { + // @ts-ignore + this._entity._updateFlagManager.dispatch(UITransformModifyFlags.Size); + } } // @ts-ignore diff --git a/packages/ui/src/component/advanced/Image.ts b/packages/ui/src/component/advanced/Image.ts index b8ca6ffe55..cfb2cf41ff 100644 --- a/packages/ui/src/component/advanced/Image.ts +++ b/packages/ui/src/component/advanced/Image.ts @@ -1,15 +1,17 @@ import { BoundingBox, Entity, + FilledSpriteAssembler, ISpriteAssembler, ISpriteRenderer, MathUtil, - RenderQueueFlags, RendererUpdateFlags, SimpleSpriteAssembler, SlicedSpriteAssembler, Sprite, SpriteDrawMode, + SpriteFilledMode, + SpriteFilledOrigin, SpriteModifyFlags, SpriteTileMode, TiledSpriteAssembler, @@ -35,6 +37,14 @@ export class Image extends UIRenderer implements ISpriteRenderer { private _tileMode: SpriteTileMode = SpriteTileMode.Continuous; @assignmentClone private _tiledAdaptiveThreshold: number = 0.5; + @assignmentClone + private _filledMode: SpriteFilledMode = SpriteFilledMode.Radial360; + @assignmentClone + private _filledAmount: number = 1; + @assignmentClone + private _filledOrigin: SpriteFilledOrigin = SpriteFilledOrigin.Bottom; + @assignmentClone + private _filledClockWise: boolean = true; /** * The draw mode of the image. @@ -56,6 +66,9 @@ export class Image extends UIRenderer implements ISpriteRenderer { case SpriteDrawMode.Tiled: this._assembler = TiledSpriteAssembler; break; + case SpriteDrawMode.Filled: + this._assembler = FilledSpriteAssembler; + break; default: break; } @@ -97,6 +110,73 @@ export class Image extends UIRenderer implements ISpriteRenderer { } } + /** + * The fill amount of the image, range from 0 to 1. (Only works in filled mode.) + */ + get filledAmount(): number { + return this._filledAmount; + } + + set filledAmount(value: number) { + value = MathUtil.clamp(value, 0, 1); + if (this._filledAmount !== value) { + this._filledAmount = value; + if (this._drawMode === SpriteDrawMode.Filled) { + this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeAndUV; + } + } + } + + /** + * The fill mode of the image. (Only works in filled mode.) + */ + get filledMode(): SpriteFilledMode { + return this._filledMode; + } + + set filledMode(value: SpriteFilledMode) { + if (this._filledMode !== value) { + this._filledMode = value; + this._filledOrigin = + value === SpriteFilledMode.Radial90 ? SpriteFilledOrigin.BottomLeft : SpriteFilledOrigin.Bottom; + if (this._drawMode === SpriteDrawMode.Filled) { + this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeAndUV; + } + } + } + + /** + * The fill origin of the image. (Only works in filled mode.) + */ + get filledOrigin(): SpriteFilledOrigin { + return this._filledOrigin; + } + + set filledOrigin(value: SpriteFilledOrigin) { + if (this._filledOrigin !== value) { + this._filledOrigin = value; + if (this._drawMode === SpriteDrawMode.Filled) { + this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeAndUV; + } + } + } + + /** + * Whether the fill is clockwise. (Only works in filled radial mode.) + */ + get filledClockWise(): boolean { + return this._filledClockWise; + } + + set filledClockWise(value: boolean) { + if (this._filledClockWise !== value) { + this._filledClockWise = value; + if (this._drawMode === SpriteDrawMode.Filled) { + this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeAndUV; + } + } + } + /** * The Sprite to render. */ @@ -236,16 +316,16 @@ export class Image extends UIRenderer implements ISpriteRenderer { } this._dirtyUpdateFlag = dirtyUpdateFlag; - // Init sub render element. const { engine } = context.camera; - const subRenderElement = engine._subRenderElementPool.get(); + const renderElement = engine._renderElementPool.get(); const subChunk = this._subChunk; - subRenderElement.set(this, material, subChunk.chunk.primitive, subChunk.subMesh, this.sprite.texture, subChunk); + renderElement.set(this, material, subChunk.chunk.primitive, subChunk.subMesh, this.sprite.texture, subChunk); if (canvas._realRenderMode === CanvasRenderMode.ScreenSpaceOverlay) { - subRenderElement.shaderPasses = material.shader.subShaders[0].passes; - subRenderElement.renderQueueFlags = RenderQueueFlags.All; + renderElement.subShader = material.shader.subShaders[0]; } - canvas._renderElement.addSubRenderElement(subRenderElement); + renderElement.priority = canvas.sortOrder; + renderElement.distanceForSort = canvas._sortDistance; + canvas._renderElements.push(renderElement); } @ignoreClone @@ -281,6 +361,9 @@ export class Image extends UIRenderer implements ISpriteRenderer { case SpriteDrawMode.Tiled: this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeUVAndColor; break; + case SpriteDrawMode.Filled: + this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeUVAndColor; + break; default: break; } diff --git a/packages/ui/src/component/advanced/Mask.ts b/packages/ui/src/component/advanced/Mask.ts new file mode 100644 index 0000000000..cff2fd85ab --- /dev/null +++ b/packages/ui/src/component/advanced/Mask.ts @@ -0,0 +1,80 @@ +import { BoundingBox, Entity, MaskRenderable, Vector2 } from "@galacean/engine"; +import type { IMaskRenderable } from "@galacean/engine"; +import { UIRenderer } from "../UIRenderer"; +import { UITransform } from "../UITransform"; + +/** + * UI component that uses a sprite to mask child UI renderers via stencil. + */ +export class Mask extends MaskRenderable(UIRenderer) { + /** + * @internal + */ + override _getChunkManager() { + // @ts-ignore + return this.engine._batcherManager.primitiveChunkManagerMask; + } + + /** + * @internal + */ + constructor(entity: Entity) { + super(entity); + this._initMask(); + this.raycastEnabled = false; + } + + /** + * @internal + */ + // @ts-ignore + _cloneTo(target: Mask): void { + // @ts-ignore + super._cloneTo(target); + this._cloneMaskData(target); + } + + protected override _updateBounds(worldBounds: BoundingBox): void { + const rootCanvas = this._getRootCanvas(); + if (this.sprite && rootCanvas) { + this._updateMaskBounds(worldBounds); + } else { + const { worldPosition } = this._transformEntity.transform; + worldBounds.min.copyFrom(worldPosition); + worldBounds.max.copyFrom(worldPosition); + } + } + + /** + * @inheritdoc + */ + protected override _render(context): void { + this._renderMask(0); + } + + /** + * @inheritdoc + */ + protected override _onDestroy(): void { + this._destroyMaskResources(); + + super._onDestroy(); + + if (this._subChunk) { + this._getChunkManager().freeSubChunk(this._subChunk); + this._subChunk = null; + } + } + + override _getSpriteWidth(): number { + return (this._transformEntity.transform).size.x; + } + + override _getSpriteHeight(): number { + return (this._transformEntity.transform).size.y; + } + + override _getSpritePivot(): Vector2 { + return (this._transformEntity.transform).pivot; + } +} diff --git a/packages/ui/src/component/advanced/RectMask2D.ts b/packages/ui/src/component/advanced/RectMask2D.ts new file mode 100644 index 0000000000..9cba135e84 --- /dev/null +++ b/packages/ui/src/component/advanced/RectMask2D.ts @@ -0,0 +1,157 @@ +import { + Component, + DependentMode, + Entity, + Vector2, + Vector3, + Vector4, + assignmentClone, + deepClone, + dependentComponents +} from "@galacean/engine"; +import { UICanvas } from "../UICanvas"; +import { UITransform } from "../UITransform"; + +/** + * UI component that clips descendant graphics by an axis-aligned rectangle. + */ +@dependentComponents(UITransform, DependentMode.AutoAdd) +export class RectMask2D extends Component { + private static _tempRect: Vector4 = new Vector4(); + private static _tempCorner0: Vector3 = new Vector3(); + private static _tempCorner1: Vector3 = new Vector3(); + private static _tempCorner2: Vector3 = new Vector3(); + private static _tempCorner3: Vector3 = new Vector3(); + + @deepClone + private _softness: Vector2 = new Vector2(0, 0); + @assignmentClone + private _alphaClip: boolean = false; + + /** + * Soft clipping width on X/Y axis in world space. + */ + get softness(): Vector2 { + return this._softness; + } + + set softness(value: Vector2) { + const softness = this._softness; + if (softness === value) { + return; + } + if (softness.x !== value.x || softness.y !== value.y) { + softness.copyFrom(value); + this._clampSoftness(); + } + } + + /** + * Whether to enable hard clip (discard) when outside the rect. + */ + get alphaClip(): boolean { + return this._alphaClip; + } + + set alphaClip(value: boolean) { + this._alphaClip = value; + } + + /** + * @internal + */ + _getWorldRect(out: Vector4): boolean { + const transform = this.entity.transform; + const { x: width, y: height } = transform.size; + if (!width || !height) { + return false; + } + + const { x: pivotX, y: pivotY } = transform.pivot; + const left = -width * pivotX; + const right = width * (1 - pivotX); + const bottom = -height * pivotY; + const top = height * (1 - pivotY); + + const worldMatrix = transform.worldMatrix; + const corner0 = RectMask2D._tempCorner0; + const corner1 = RectMask2D._tempCorner1; + const corner2 = RectMask2D._tempCorner2; + const corner3 = RectMask2D._tempCorner3; + Vector3.transformCoordinate(corner0.set(left, bottom, 0), worldMatrix, corner0); + Vector3.transformCoordinate(corner1.set(left, top, 0), worldMatrix, corner1); + Vector3.transformCoordinate(corner2.set(right, bottom, 0), worldMatrix, corner2); + Vector3.transformCoordinate(corner3.set(right, top, 0), worldMatrix, corner3); + + const minX = Math.min(corner0.x, corner1.x, corner2.x, corner3.x); + const minY = Math.min(corner0.y, corner1.y, corner2.y, corner3.y); + const maxX = Math.max(corner0.x, corner1.x, corner2.x, corner3.x); + const maxY = Math.max(corner0.y, corner1.y, corner2.y, corner3.y); + out.set(minX, minY, maxX, maxY); + return true; + } + + /** + * @internal + */ + _containsWorldPoint(worldPoint: Vector3): boolean { + const worldRect = RectMask2D._tempRect; + if (!this._getWorldRect(worldRect)) { + return false; + } + const { x, y } = worldPoint; + return x >= worldRect.x && x <= worldRect.z && y >= worldRect.y && y <= worldRect.w; + } + + constructor(entity: Entity) { + super(entity); + this._onSoftnessChanged = this._onSoftnessChanged.bind(this); + // @ts-ignore + this._softness._onValueChanged = this._onSoftnessChanged; + } + + // @ts-ignore + override _onEnableInScene(): void { + this.entity._updateUIHierarchyVersion(UICanvas._hierarchyCounter); + } + + // @ts-ignore + override _onDisableInScene(): void { + this.entity._updateUIHierarchyVersion(UICanvas._hierarchyCounter); + } + + // @ts-ignore + override _cloneTo(target: RectMask2D): void { + // RectMask2D extends Component directly; Component.prototype 上没有 _cloneTo, + // 不能 super._cloneTo(target) — 会拿到 undefined 报 "Cannot read properties of undefined (reading 'call')"。 + // (Image/Mask 走 Renderer 链路所以能 super;RectMask2D 不在 Renderer 链路里。) + const targetSoftness = target._softness; + // @ts-ignore + targetSoftness._onValueChanged = null; + targetSoftness.copyFrom(this._softness); + target._clampSoftness(); + // @ts-ignore + targetSoftness._onValueChanged = target._onSoftnessChanged; + } + + protected override _onDestroy(): void { + // @ts-ignore + this._softness._onValueChanged = null; + this._softness = null; + super._onDestroy(); + } + + private _onSoftnessChanged(): void { + this._clampSoftness(); + } + + private _clampSoftness(): void { + const softness = this._softness; + if (softness.x < 0) { + softness.x = 0; + } + if (softness.y < 0) { + softness.y = 0; + } + } +} diff --git a/packages/ui/src/component/advanced/Text.ts b/packages/ui/src/component/advanced/Text.ts index 0eb5edeb93..f1c329e6ef 100644 --- a/packages/ui/src/component/advanced/Text.ts +++ b/packages/ui/src/component/advanced/Text.ts @@ -1,13 +1,13 @@ import { BoundingBox, CharRenderInfo, + Color, Engine, Entity, Font, FontStyle, ITextRenderer, OverflowMode, - RenderQueueFlags, RendererUpdateFlags, ShaderData, ShaderDataGroup, @@ -18,7 +18,9 @@ import { TextVerticalAlignment, Texture2D, Vector3, + VertexMergeBatcher, assignmentClone, + deepClone, ignoreClone } from "@galacean/engine"; import { CanvasRenderMode } from "../../enums/CanvasRenderMode"; @@ -31,6 +33,9 @@ import { UITransform, UITransformModifyFlags } from "../UITransform"; */ export class Text extends UIRenderer implements ITextRenderer { private static _textTextureProperty = ShaderProperty.getByName("renderElement_TextTexture"); + private static _textTextureSizeProperty = ShaderProperty.getByName("renderElement_TextTextureSize"); + private static _outlineColorProperty = ShaderProperty.getByName("renderer_OutlineColor"); + private static _outlineWidthProperty = ShaderProperty.getByName("renderer_OutlineWidth"); private static _worldPositions = [new Vector3(), new Vector3(), new Vector3(), new Vector3()]; private static _charRenderInfos: CharRenderInfo[] = []; @@ -60,6 +65,10 @@ export class Text extends UIRenderer implements ITextRenderer { private _enableWrapping: boolean = false; @assignmentClone private _overflowMode: OverflowMode = OverflowMode.Overflow; + @deepClone + private _outlineColor: Color = new Color(0, 0, 0, 1); + @ignoreClone + private _outlineWidth: number = 0; /** * Rendering string for the Text. @@ -206,14 +215,32 @@ export class Text extends UIRenderer implements ITextRenderer { } /** - * The mask layer the sprite renderer belongs to. + * The outline width in pixels. 0 means outline is disabled. Clamped to [0, 8]. + */ + get outlineWidth(): number { + return this._outlineWidth; + } + + set outlineWidth(value: number) { + value = Math.max(0, Math.min(value, 3)); + if (this._outlineWidth !== value) { + this._outlineWidth = value; + this.shaderData.setFloat(Text._outlineWidthProperty, value); + this._setDirtyFlagTrue(DirtyFlag.Position); + } + } + + /** + * The outline color. Only effective when outlineWidth > 0. */ - get maskLayer(): number { - return this._maskLayer; + get outlineColor(): Color { + return this._outlineColor; } - set maskLayer(value: number) { - this._maskLayer = value; + set outlineColor(value: Color) { + if (this._outlineColor !== value) { + this._outlineColor.copyFrom(value); + } } /** @@ -247,6 +274,11 @@ export class Text extends UIRenderer implements ITextRenderer { this.raycastEnabled = false; // @ts-ignore this.setMaterial(engine._basicResources.textDefaultMaterial); + const shaderData = this.shaderData; + shaderData.setFloat(Text._outlineWidthProperty, this._outlineWidth); + shaderData.setColor(Text._outlineColorProperty, this._outlineColor); + // @ts-ignore + this._outlineColor._onValueChanged = this._onOutlineColorChanged.bind(this); } /** @@ -272,6 +304,7 @@ export class Text extends UIRenderer implements ITextRenderer { super._cloneTo(target); target.font = this._font; target._subFont = this._subFont; + target.outlineWidth = this._outlineWidth; } /** @@ -310,17 +343,19 @@ export class Text extends UIRenderer implements ITextRenderer { */ _onRootCanvasModify(flag: RootCanvasModifyFlags): void { if (flag === RootCanvasModifyFlags.ReferenceResolutionPerUnit) { - this._setDirtyFlagTrue(DirtyFlag.LocalPositionBounds); + this._setDirtyFlagTrue(DirtyFlag.Position); } } + /** + * @internal + */ + override _canBatch(preElement, curElement): boolean { + return VertexMergeBatcher.canBatchText(preElement, curElement); + } + protected override _updateBounds(worldBounds: BoundingBox): void { - const transform = this._transformEntity.transform; - const { x: width, y: height } = transform.size; - const { x: pivotX, y: pivotY } = transform.pivot; - worldBounds.min.set(-width * pivotX, -height * pivotY, 0); - worldBounds.max.set(width * (1 - pivotX), height * (1 - pivotY), 0); - BoundingBox.transform(worldBounds, this._transformEntity.transform.worldMatrix, worldBounds); + BoundingBox.transform(this._localBounds, this._transformEntity.transform.worldMatrix, worldBounds); } protected override _render(context): void { @@ -350,23 +385,31 @@ export class Text extends UIRenderer implements ITextRenderer { } const engine = context.camera.engine; - const textSubRenderElementPool = engine._textSubRenderElementPool; + const textRenderElementPool = engine._textRenderElementPool; const material = this.getMaterial(); - const renderElement = canvas._renderElement; + const renderElements = canvas._renderElements; + const priority = canvas.sortOrder; + const distanceForSort = canvas._sortDistance; const textChunks = this._textChunks; const isOverlay = canvas._realRenderMode === CanvasRenderMode.ScreenSpaceOverlay; + const textTextureSize = Text._tempVec20; for (let i = 0, n = textChunks.length; i < n; ++i) { const { subChunk, texture } = textChunks[i]; - const subRenderElement = textSubRenderElementPool.get(); - subRenderElement.set(this, material, subChunk.chunk.primitive, subChunk.subMesh, texture, subChunk); + const renderElement = textRenderElementPool.get(); + renderElement.set(this, material, subChunk.chunk.primitive, subChunk.subMesh, texture, subChunk); // @ts-ignore - subRenderElement.shaderData ||= new ShaderData(ShaderDataGroup.RenderElement); - subRenderElement.shaderData.setTexture(Text._textTextureProperty, texture); + renderElement.shaderData ||= new ShaderData(ShaderDataGroup.RenderElement); + renderElement.shaderData.setTexture(Text._textTextureProperty, texture); + renderElement.shaderData.setVector2( + Text._textTextureSizeProperty, + textTextureSize.set(texture.width, texture.height) + ); if (isOverlay) { - subRenderElement.shaderPasses = material.shader.subShaders[0].passes; - subRenderElement.renderQueueFlags = RenderQueueFlags.All; + renderElement.subShader = material.shader.subShaders[0]; } - renderElement.addSubRenderElement(subRenderElement); + renderElement.priority = priority; + renderElement.distanceForSort = distanceForSort; + renderElements.push(renderElement); } } @@ -377,6 +420,17 @@ export class Text extends UIRenderer implements ITextRenderer { this._subFont.nativeFontString = TextUtils.getNativeFontString(font.name, this.fontSize, this.fontStyle); } + /** + * Switch the sub font to a specific font size, used by the SHRINK overflow measurement. + */ + private _applyFontSizeForShrink(fontSize: number): void { + const font = this._font; + // @ts-ignore + const subFont = font._getSubFont(fontSize, this._fontStyle); + subFont.nativeFontString = TextUtils.getNativeFontString(font.name, fontSize, this._fontStyle); + this._subFont = subFont; + } + private _updatePosition(): void { const e = this._transformEntity.transform.worldMatrix.elements; @@ -447,27 +501,45 @@ export class Text extends UIRenderer implements ITextRenderer { const pixelsPerResolution = Engine._pixelsPerUnit / this._getRootCanvas().referenceResolutionPerUnit; const { min, max } = this._localBounds; const charRenderInfos = Text._charRenderInfos; - const charFont = this._getSubFont(); const { size, pivot } = this._transformEntity.transform; let rendererWidth = size.x; let rendererHeight = size.y; const offsetWidth = rendererWidth * (0.5 - pivot.x); const offsetHeight = rendererHeight * (0.5 - pivot.y); - const characterSpacing = this._characterSpacing * this._fontSize; - const textMetrics = this.enableWrapping - ? TextUtils.measureTextWithWrap( - this, - rendererWidth * pixelsPerResolution, - rendererHeight * pixelsPerResolution, - this._lineSpacing * this._fontSize, - characterSpacing - ) - : TextUtils.measureTextWithoutWrap( - this, - rendererHeight * pixelsPerResolution, - this._lineSpacing * this._fontSize, - characterSpacing - ); + let fontSize = this._fontSize; + let textMetrics: ReturnType; + if (this._overflowMode === OverflowMode.Shrink) { + const result = TextUtils.measureTextWithShrink( + this, + rendererWidth * pixelsPerResolution, + rendererHeight * pixelsPerResolution, + this._fontSize, + this._lineSpacing, + this._characterSpacing, + this.enableWrapping, + (sizeValue) => this._applyFontSizeForShrink(sizeValue) + ); + fontSize = result.fontSize; + textMetrics = result.metrics; + } else { + const characterSpacing = this._characterSpacing * fontSize; + textMetrics = this.enableWrapping + ? TextUtils.measureTextWithWrap( + this, + rendererWidth * pixelsPerResolution, + rendererHeight * pixelsPerResolution, + this._lineSpacing * fontSize, + characterSpacing + ) + : TextUtils.measureTextWithoutWrap( + this, + rendererHeight * pixelsPerResolution, + this._lineSpacing * fontSize, + characterSpacing + ); + } + const charFont = this._getSubFont(); + const characterSpacing = this._characterSpacing * fontSize; const { height, lines, lineWidths, lineHeight, lineMaxSizes } = textMetrics; // @ts-ignore const charRenderInfoPool = this.engine._charRenderInfoPool; @@ -490,7 +562,10 @@ export class Text extends UIRenderer implements ITextRenderer { startY = rendererHeight * 0.5 - halfLineHeight + topDiff; break; case TextVerticalAlignment.Center: - startY = height * 0.5 - halfLineHeight - (bottomDiff - topDiff) * 0.5; + // Center the text block (lineHeight * lineCount) within the renderer, independent of + // `height` — which equals the renderer height for Truncate/Shrink and would otherwise + // push the text upward by (rendererHeight - blockHeight) / 2 when the box is taller. + startY = lineHeight * linesLen * 0.5 - halfLineHeight - (bottomDiff - topDiff) * 0.5; break; case TextVerticalAlignment.Bottom: startY = height - rendererHeight * 0.5 - halfLineHeight - bottomDiff; @@ -532,10 +607,11 @@ export class Text extends UIRenderer implements ITextRenderer { charRenderInfo.texture = charFont._getTextureByIndex(charInfo.index); charRenderInfo.uvs = charInfo.uvs; const { w, ascent, descent } = charInfo; - const left = (startX + offsetWidth) * pixelsPerUnitReciprocal; - const right = (startX + w + offsetWidth) * pixelsPerUnitReciprocal; - const top = (startY + ascent + offsetHeight) * pixelsPerUnitReciprocal; - const bottom = (startY - descent + offsetHeight) * pixelsPerUnitReciprocal; + const ow = this._outlineWidth * pixelsPerUnitReciprocal; + const left = (startX + offsetWidth) * pixelsPerUnitReciprocal - ow; + const right = (startX + w + offsetWidth) * pixelsPerUnitReciprocal + ow; + const top = (startY + ascent + offsetHeight) * pixelsPerUnitReciprocal + ow; + const bottom = (startY - descent + offsetHeight) * pixelsPerUnitReciprocal - ow; localPositions.set(left, top, right, bottom); i === firstLine && (maxY = Math.max(maxY, top)); minY = Math.min(minY, bottom); @@ -618,7 +694,7 @@ export class Text extends UIRenderer implements ITextRenderer { this._text === "" || this._fontSize === 0 || (this.enableWrapping && size.x <= 0) || - (this.overflowMode === OverflowMode.Truncate && size.y <= 0) || + ((this.overflowMode === OverflowMode.Truncate || this.overflowMode === OverflowMode.Shrink) && size.y <= 0) || !this._getRootCanvas() ); } @@ -632,6 +708,10 @@ export class Text extends UIRenderer implements ITextRenderer { const vertices = subChunk.chunk.vertices; const indices = (subChunk.indices = []); const charRenderInfos = textChunk.charRenderInfos; + const ow = this._outlineWidth; + const texture = textChunk.texture; + const owU = ow > 0 ? ow / texture.width : 0; + const owV = ow > 0 ? ow / texture.height : 0; for (let i = 0, ii = 0, io = 0, vo = subChunk.vertexArea.start + 3; i < count; ++i, io += 4) { const charRenderInfo = charRenderInfos[i]; charRenderInfo.indexInChunk = i; @@ -641,10 +721,13 @@ export class Text extends UIRenderer implements ITextRenderer { indices[ii++] = tempIndices[j] + io; } - // Set uv and color for vertices + // Set uv and color for vertices, expand uv outward by outline width for (let j = 0; j < 4; ++j, vo += 9) { const uv = charRenderInfo.uvs[j]; - uv.copyToArray(vertices, vo); + const su = j === 1 || j === 2 ? 1 : -1; + const sv = j >= 2 ? 1 : -1; + vertices[vo] = uv.x + owU * su; + vertices[vo + 1] = uv.y + owV * sv; vertices[vo + 2] = r; vertices[vo + 3] = g; vertices[vo + 4] = b; @@ -673,6 +756,11 @@ export class Text extends UIRenderer implements ITextRenderer { } textChunks.length = 0; } + + @ignoreClone + private _onOutlineColorChanged(): void { + this.shaderData.setColor(Text._outlineColorProperty, this._outlineColor); + } } class TextChunk { diff --git a/packages/ui/src/component/index.ts b/packages/ui/src/component/index.ts index 1f89431265..8329d1f39a 100644 --- a/packages/ui/src/component/index.ts +++ b/packages/ui/src/component/index.ts @@ -1,11 +1,13 @@ -export { UICanvas } from "./UICanvas"; -export { UIGroup } from "./UIGroup"; -export { UIRenderer } from "./UIRenderer"; -export { UITransform } from "./UITransform"; export { Button } from "./advanced/Button"; export { Image } from "./advanced/Image"; +export { Mask } from "./advanced/Mask"; +export { RectMask2D } from "./advanced/RectMask2D"; export { Text } from "./advanced/Text"; export { ColorTransition } from "./interactive/transition/ColorTransition"; export { ScaleTransition } from "./interactive/transition/ScaleTransition"; export { SpriteTransition } from "./interactive/transition/SpriteTransition"; export { Transition } from "./interactive/transition/Transition"; +export { UICanvas } from "./UICanvas"; +export { UIGroup } from "./UIGroup"; +export { UIRenderer } from "./UIRenderer"; +export { UITransform } from "./UITransform"; diff --git a/packages/ui/src/input/UIPointerEventEmitter.ts b/packages/ui/src/input/UIPointerEventEmitter.ts index 83bf8f1484..d36e529876 100644 --- a/packages/ui/src/input/UIPointerEventEmitter.ts +++ b/packages/ui/src/input/UIPointerEventEmitter.ts @@ -91,11 +91,11 @@ export class UIPointerEventEmitter extends PointerEventEmitter { } } if (camera.clearFlags & CameraClearFlags.Color) { - this._updateRaycast(null); + this._updateRaycast(null, pointer); return; } } - this._updateRaycast(null); + this._updateRaycast(null, pointer); } } @@ -128,10 +128,7 @@ export class UIPointerEventEmitter extends PointerEventEmitter { if (pressedPath.length > 0) { const common = UIPointerEventEmitter._tempArray0; if (this._findCommonInPath(enteredPath, pressedPath, common)) { - const eventData = this._createEventData(pointer); - for (let i = 0, n = common.length; i < n; i++) { - this._fireClick(common[i], eventData); - } + this._bubble(common, pointer, this._fireClick); common.length = 0; } } @@ -170,18 +167,17 @@ export class UIPointerEventEmitter extends PointerEventEmitter { this._enteredPath.length = this._pressedPath.length = this._draggedPath.length = 0; } - private _updateRaycast(element: UIRenderer, pointer: Pointer = null): void { + private _updateRaycast(element: UIRenderer | null, pointer: Pointer): void { const enteredPath = this._enteredPath; const curPath = this._composedPath(element, UIPointerEventEmitter._path); const add = UIPointerEventEmitter._tempArray0; const del = UIPointerEventEmitter._tempArray1; if (this._findDiffInPath(enteredPath, curPath, add, del)) { - const eventData = this._createEventData(pointer); for (let i = 0, n = add.length; i < n; i++) { - this._fireEnter(add[i], eventData); + this._fireEnter(add[i], this._createEventData(pointer, add[i], add[i])); } for (let i = 0, n = del.length; i < n; i++) { - this._fireExit(del[i], eventData); + this._fireExit(del[i], this._createEventData(pointer, del[i], del[i])); } const length = (enteredPath.length = curPath.length); @@ -193,7 +189,7 @@ export class UIPointerEventEmitter extends PointerEventEmitter { curPath.length = 0; } - private _composedPath(element: UIRenderer, path: Entity[]): Entity[] { + private _composedPath(element: UIRenderer | null, path: Entity[]): Entity[] { if (!element) { path.length = 0; return path; @@ -256,8 +252,9 @@ export class UIPointerEventEmitter extends PointerEventEmitter { private _bubble(path: Entity[], pointer: Pointer, fireEvent: FireEvent): void { const length = path.length; if (length <= 0) return; - const eventData = this._createEventData(pointer); + const eventData = this._createEventData(pointer, path[0]); for (let i = 0; i < length; i++) { + eventData.currentTarget = path[i]; fireEvent(path[i], eventData); } } diff --git a/packages/ui/src/shader/global.d.ts b/packages/ui/src/shader/global.d.ts index d0abf1234f..8ce5f33757 100644 --- a/packages/ui/src/shader/global.d.ts +++ b/packages/ui/src/shader/global.d.ts @@ -3,7 +3,7 @@ declare module "*.glsl" { export default value; } -declare module "*.gs" { +declare module "*.shader" { const value: string; export default value; } diff --git a/packages/ui/src/shader/uiDefault.fs.glsl b/packages/ui/src/shader/uiDefault.fs.glsl index e4028405de..aa4ca2e0e9 100644 --- a/packages/ui/src/shader/uiDefault.fs.glsl +++ b/packages/ui/src/shader/uiDefault.fs.glsl @@ -1,14 +1,43 @@ #include uniform sampler2D renderer_UITexture; +uniform vec4 renderer_UIRectClipRect; +uniform float renderer_UIRectClipEnabled; +uniform vec4 renderer_UIRectClipSoftness; +uniform float renderer_UIRectClipHardClip; varying vec2 v_uv; varying vec4 v_color; +varying vec2 v_worldPosition; + +float getUIRectClipAlpha() { + vec4 edgeDistance = vec4( + v_worldPosition.x - renderer_UIRectClipRect.x, + v_worldPosition.y - renderer_UIRectClipRect.y, + renderer_UIRectClipRect.z - v_worldPosition.x, + renderer_UIRectClipRect.w - v_worldPosition.y + ); + vec4 hardClipFactor = step(vec4(0.0), edgeDistance); + vec4 softness = max(renderer_UIRectClipSoftness, vec4(1e-5)); + vec4 softClipFactor = clamp(edgeDistance / softness, 0.0, 1.0); + vec4 useSoftness = step(vec4(1e-5), renderer_UIRectClipSoftness); + vec4 clipFactor = mix(hardClipFactor, softClipFactor, useSoftness); + return clipFactor.x * clipFactor.y * clipFactor.z * clipFactor.w; +} void main() { + float rectClipAlpha = 1.0; + if (renderer_UIRectClipEnabled > 0.5) { + rectClipAlpha = getUIRectClipAlpha(); + } + vec4 baseColor = texture2DSRGB(renderer_UITexture, v_uv); vec4 finalColor = baseColor * v_color; + finalColor.a *= rectClipAlpha; + if (renderer_UIRectClipEnabled > 0.5 && renderer_UIRectClipHardClip > 0.5 && finalColor.a < 0.001) { + discard; + } #ifdef ENGINE_SHOULD_SRGB_CORRECT finalColor = outputSRGBCorrection(finalColor); #endif gl_FragColor = finalColor; -} \ No newline at end of file +} diff --git a/packages/ui/src/shader/uiDefault.vs.glsl b/packages/ui/src/shader/uiDefault.vs.glsl index 2a6b45be4e..52345d9abf 100644 --- a/packages/ui/src/shader/uiDefault.vs.glsl +++ b/packages/ui/src/shader/uiDefault.vs.glsl @@ -1,4 +1,5 @@ uniform mat4 renderer_MVPMat; +uniform mat4 renderer_ModelMat; attribute vec3 POSITION; attribute vec2 TEXCOORD_0; @@ -6,10 +7,12 @@ attribute vec4 COLOR_0; varying vec2 v_uv; varying vec4 v_color; +varying vec2 v_worldPosition; void main() { gl_Position = renderer_MVPMat * vec4(POSITION, 1.0); v_uv = TEXCOORD_0; v_color = COLOR_0; + v_worldPosition = POSITION.xy; } diff --git a/packages/xr-webxr/package.json b/packages/xr-webxr/package.json index 9663c2321f..9b7f28ed3a 100644 --- a/packages/xr-webxr/package.json +++ b/packages/xr-webxr/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-xr-webxr", - "version": "2.0.0-alpha.24", + "version": "0.0.0-experimental-2.0-game.19", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/xr/package.json b/packages/xr/package.json index ec2e3cc79e..20fbd1cf6e 100644 --- a/packages/xr/package.json +++ b/packages/xr/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-xr", - "version": "2.0.0-alpha.24", + "version": "0.0.0-experimental-2.0-game.19", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ed565e145b..6a5d492e97 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,11 +45,11 @@ importers: specifier: latest version: 0.5.22 '@typescript-eslint/eslint-plugin': - specifier: ^6.1.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3) + specifier: ^8.58.1 + version: 8.58.1(@typescript-eslint/parser@8.58.1(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3) '@typescript-eslint/parser': - specifier: ^6.1.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.6.3) + specifier: ^8.58.1 + version: 8.58.1(eslint@8.57.1)(typescript@5.6.3) '@vitest/coverage-v8': specifier: 2.1.3 version: 2.1.3(@vitest/browser@2.1.3(@types/node@18.19.64)(@vitest/spy@2.1.3)(typescript@5.6.3)(vite@5.4.11(@types/node@18.19.64)(sass@1.81.0)(terser@5.44.1))(vitest@2.1.3))(vitest@2.1.3(@types/node@18.19.64)(@vitest/browser@2.1.3)(msw@2.6.5(@types/node@18.19.64)(typescript@5.6.3))(sass@1.81.0)(terser@5.44.1)) @@ -63,7 +63,7 @@ importers: specifier: ^13 version: 13.6.9 eslint: - specifier: ^8.44.0 + specifier: ^8.57.1 version: 8.57.1 eslint-config-prettier: specifier: ^8.8.0 @@ -78,8 +78,8 @@ importers: specifier: ^8.0.0 version: 8.0.3 lint-staged: - specifier: ^10.5.3 - version: 10.5.4 + specifier: ^16.4.0 + version: 16.4.0 nyc: specifier: ^15.1.0 version: 15.1.0 @@ -143,6 +143,12 @@ importers: '@galacean/engine-shaderlab': specifier: workspace:* version: link:../packages/shader-lab + '@galacean/engine-spine': + specifier: workspace:* + version: link:../packages/spine + '@galacean/engine-spine-core-4.2': + specifier: workspace:* + version: link:../packages/spine-core-4.2 '@galacean/engine-toolkit': specifier: ^1.3.9 version: 1.3.9(@galacean/engine@packages+galacean) @@ -185,9 +191,21 @@ importers: '@galacean/engine-shaderlab': specifier: workspace:* version: link:../packages/shader-lab + '@galacean/engine-spine': + specifier: workspace:* + version: link:../packages/spine + '@galacean/engine-spine-core-3.8': + specifier: workspace:* + version: link:../packages/spine-core-3.8 + '@galacean/engine-spine-core-4.2': + specifier: workspace:* + version: link:../packages/spine-core-4.2 '@galacean/engine-toolkit': specifier: latest version: 1.5.3(@galacean/engine-ui@packages+ui)(@galacean/engine@packages+galacean) + '@galacean/engine-toolkit-stats': + specifier: latest + version: 1.6.0(@galacean/engine@packages+galacean) '@galacean/engine-ui': specifier: workspace:* version: link:../packages/ui @@ -290,6 +308,38 @@ importers: specifier: workspace:* version: link:../design + packages/spine: + devDependencies: + '@esotericsoftware/spine-core': + specifier: ~4.2.66 + version: 4.2.119 + '@galacean/engine': + specifier: workspace:* + version: link:../galacean + + packages/spine-core-3.8: + dependencies: + '@galacean/engine-spine': + specifier: workspace:* + version: link:../spine + devDependencies: + '@galacean/engine': + specifier: workspace:* + version: link:../galacean + + packages/spine-core-4.2: + dependencies: + '@galacean/engine-spine': + specifier: workspace:* + version: link:../spine + devDependencies: + '@esotericsoftware/spine-core': + specifier: ~4.2.66 + version: 4.2.119 + '@galacean/engine': + specifier: workspace:* + version: link:../galacean + packages/ui: devDependencies: '@galacean/engine': @@ -350,6 +400,12 @@ importers: '@galacean/engine-shaderlab': specifier: workspace:* version: link:../packages/shader-lab + '@galacean/engine-spine': + specifier: workspace:* + version: link:../packages/spine + '@galacean/engine-spine-core-4.2': + specifier: workspace:* + version: link:../packages/spine-core-4.2 '@galacean/engine-ui': specifier: workspace:* version: link:../packages/ui @@ -804,10 +860,20 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.12.1': resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@eslint/eslintrc@2.1.4': resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -816,6 +882,9 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@esotericsoftware/spine-core@4.2.119': + resolution: {integrity: sha512-lvaRECaQDScO758ZSAR1Fj+GWkBKVZxPdgT/gCiKqdkrjZCDu2UzgbZtdPhxnLPcKG/zsaGJkfbN4OS0wHsxZQ==} + '@fastify/deepmerge@1.3.0': resolution: {integrity: sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==} @@ -935,6 +1004,11 @@ packages: peerDependencies: '@galacean/engine': ^1.5.0 + '@galacean/engine-toolkit-stats@1.6.0': + resolution: {integrity: sha512-63LLxTWg15xR000jbtEONnK6lBBMylvl5m+3VqqC7b09YAuMWlm9CuPfaM8dlbctOYT6nmPu9bpQiq3JfdgtWg==} + peerDependencies: + '@galacean/engine': '>=1.6.0-0' + '@galacean/engine-toolkit@1.3.9': resolution: {integrity: sha512-sxE7QfzH61O9Q1wtwnjIEjcg3n0ZZVz9B6CyqBLOWyWgWsZmefcjZLnnH4HIkvc5ZLNA+gMuJ1ekmwJgfkck+g==} @@ -1481,9 +1555,6 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/keyv@3.1.4': resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} @@ -1508,9 +1579,6 @@ packages: '@types/responselike@1.0.3': resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} - '@types/semver@7.5.8': - resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} - '@types/statuses@2.0.5': resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==} @@ -1520,63 +1588,64 @@ packages: '@types/webxr@0.5.22': resolution: {integrity: sha512-Vr6Stjv5jPRqH690f5I5GLjVk8GSsoQSYJ2FVd/3jJF7KaqfwPi3ehfBS96mlQ2kPCwZaX6U0rG2+NGHBKkA/A==} - '@typescript-eslint/eslint-plugin@6.21.0': - resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} - engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/eslint-plugin@8.58.1': + resolution: {integrity: sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@typescript-eslint/parser': ^8.58.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@6.21.0': - resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} - engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/parser@8.58.1': + resolution: {integrity: sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.58.1': + resolution: {integrity: sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@6.21.0': - resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} - engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/scope-manager@8.58.1': + resolution: {integrity: sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/type-utils@6.21.0': - resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} - engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/tsconfig-utils@8.58.1': + resolution: {integrity: sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@6.21.0': - resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} - engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/type-utils@8.58.1': + resolution: {integrity: sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.58.1': + resolution: {integrity: sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@6.21.0': - resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} - engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/typescript-estree@8.58.1': + resolution: {integrity: sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@6.21.0': - resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} - engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/utils@8.58.1': + resolution: {integrity: sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^7.0.0 || ^8.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@6.21.0': - resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} - engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/visitor-keys@8.58.1': + resolution: {integrity: sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} @@ -1694,14 +1763,14 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - ansi-colors@4.1.3: - resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} - engines: {node: '>=6'} - ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1710,6 +1779,10 @@ packages: resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} engines: {node: '>=12'} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -1722,6 +1795,10 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -1748,10 +1825,6 @@ packages: array-ify@1.0.0: resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} - array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - arrify@1.0.1: resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} engines: {node: '>=0.10.0'} @@ -1760,10 +1833,6 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - astral-regex@2.0.0: - resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} - engines: {node: '>=8'} - at-least-node@1.0.0: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} @@ -1771,6 +1840,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -1785,6 +1858,10 @@ packages: brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -1882,13 +1959,13 @@ packages: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} - cli-cursor@3.1.0: - resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} - engines: {node: '>=8'} + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} - cli-truncate@2.1.0: - resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} - engines: {node: '>=8'} + cli-truncate@5.2.0: + resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} + engines: {node: '>=20'} cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} @@ -1914,13 +1991,13 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - commander@6.2.1: - resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} - engines: {node: '>= 6'} - commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} @@ -2017,6 +2094,15 @@ packages: supports-color: optional: true + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} engines: {node: '>=0.10.0'} @@ -2029,9 +2115,6 @@ packages: resolution: {integrity: sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==} engines: {node: '>=4'} - dedent@0.7.0: - resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} - deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -2080,10 +2163,6 @@ packages: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} - dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} - doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} @@ -2113,6 +2192,9 @@ packages: engines: {node: '>= 8.6'} hasBin: true + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2126,14 +2208,14 @@ packages: end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - enquirer@2.4.1: - resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} - engines: {node: '>=8.6'} - env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} @@ -2322,6 +2404,10 @@ packages: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint@8.57.1: resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2365,9 +2451,8 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - execa@4.1.0: - resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} - engines: {node: '>=10'} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} execa@8.0.1: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} @@ -2387,10 +2472,6 @@ packages: fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - fast-glob@3.3.2: - resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} - engines: {node: '>=8.6.0'} - fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -2411,6 +2492,15 @@ packages: picomatch: optional: true + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -2489,13 +2579,14 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + engines: {node: '>=18'} + get-intrinsic@1.2.4: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} - get-own-enumerable-property-symbols@3.0.2: - resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} - get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} @@ -2568,10 +2659,6 @@ packages: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} - globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} - gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} @@ -2632,10 +2719,6 @@ packages: http-cache-semantics@4.1.1: resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} - human-signals@1.1.1: - resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} - engines: {node: '>=8.12.0'} - human-signals@5.0.0: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} @@ -2649,6 +2732,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + immutable@5.0.2: resolution: {integrity: sha512-1NU7hWZDkV7hJ4PJ9dur9gTNQ4ePNPN4k9/0YhwjzykTi/+3Q5pF93YU5QoVj8BuOnhLgaY8gs0U2pj4kSYVcw==} @@ -2693,6 +2780,10 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -2707,10 +2798,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-obj@1.0.1: - resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} - engines: {node: '>=0.10.0'} - is-obj@2.0.0: resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} engines: {node: '>=8'} @@ -2726,10 +2813,6 @@ packages: is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} - is-regexp@1.0.0: - resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} - engines: {node: '>=0.10.0'} - is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -2745,10 +2828,6 @@ packages: is-typedarray@1.0.0: resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} - is-unicode-supported@0.1.0: - resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} - engines: {node: '>=10'} - is-windows@1.0.2: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} @@ -2875,18 +2954,14 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - lint-staged@10.5.4: - resolution: {integrity: sha512-EechC3DdFic/TdOPgj/RB3FicqE6932LTHCUm0Y2fsD9KGlLB+RwJl2q1IYBIvEsKzDOgn0D4gll+YxG5RsrKg==} + lint-staged@16.4.0: + resolution: {integrity: sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==} + engines: {node: '>=20.17'} hasBin: true - listr2@3.14.0: - resolution: {integrity: sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==} - engines: {node: '>=10.0.0'} - peerDependencies: - enquirer: '>= 2.3.0 < 3' - peerDependenciesMeta: - enquirer: - optional: true + listr2@9.0.5: + resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} + engines: {node: '>=20.0.0'} locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} @@ -2905,13 +2980,9 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - log-symbols@4.1.0: - resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} - engines: {node: '>=10'} - - log-update@4.0.0: - resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} - engines: {node: '>=10'} + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} loupe@3.1.2: resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} @@ -2977,10 +3048,6 @@ packages: merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -2990,14 +3057,14 @@ packages: engines: {node: '>=4.0.0'} hasBin: true - mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - mimic-fn@4.0.0: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + mimic-response@1.0.1: resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} engines: {node: '>=4'} @@ -3006,13 +3073,13 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - minimatch@9.0.3: - resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} - engines: {node: '>=16 || 14 >=14.17'} - minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -3119,10 +3186,6 @@ packages: resolution: {integrity: sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==} engines: {node: '>=4'} - npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} - npm-run-path@5.3.0: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3151,14 +3214,14 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - onetime@6.0.0: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + opener@1.5.2: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true @@ -3194,10 +3257,6 @@ packages: resolution: {integrity: sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==} engines: {node: '>=8'} - p-map@4.0.0: - resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} - engines: {node: '>=10'} - p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -3279,6 +3338,10 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pify@3.0.0: resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} engines: {node: '>=4'} @@ -3300,9 +3363,6 @@ packages: engines: {node: '>=18'} hasBin: true - please-upgrade-node@3.2.0: - resolution: {integrity: sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==} - postcss@8.4.49: resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} engines: {node: ^10 || ^12 || >=14} @@ -3446,9 +3506,9 @@ packages: responselike@1.0.2: resolution: {integrity: sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==} - restore-cursor@3.1.0: - resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} - engines: {node: '>=8'} + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} @@ -3514,9 +3574,6 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - rxjs@7.8.1: - resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} - safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -3549,6 +3606,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + serialize-error@7.0.1: resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} engines: {node: '>=10'} @@ -3593,17 +3655,13 @@ packages: resolution: {integrity: sha512-pEjMUbwJ5Pl/6Vn6FsamXHXItJXSRftcibixDmNCWbWhic0hzHrwkMZo0IZ7fMRH9KxcWDFSkzhccB4285PutA==} engines: {node: '>=4.2'} - slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - - slice-ansi@3.0.0: - resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} - engines: {node: '>=8'} + slice-ansi@7.1.2: + resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} + engines: {node: '>=18'} - slice-ansi@4.0.0: - resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} - engines: {node: '>=10'} + slice-ansi@8.0.0: + resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} + engines: {node: '>=20'} source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} @@ -3658,10 +3716,6 @@ packages: strict-event-emitter@0.5.1: resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} - string-argv@0.3.1: - resolution: {integrity: sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==} - engines: {node: '>=0.6.19'} - string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -3674,16 +3728,20 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.2.0: + resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} + engines: {node: '>=20'} + string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - stringify-object@3.3.0: - resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} - engines: {node: '>=4'} - strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -3692,14 +3750,14 @@ packages: resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} engines: {node: '>=12'} + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + strip-bom@4.0.0: resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} engines: {node: '>=8'} - strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} - strip-final-newline@3.0.0: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} @@ -3764,10 +3822,18 @@ packages: tinyexec@0.3.1: resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} + engines: {node: '>=18'} + tinyglobby@0.2.10: resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + tinypool@1.0.2: resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -3800,11 +3866,11 @@ packages: resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} engines: {node: '>=8'} - ts-api-utils@1.4.0: - resolution: {integrity: sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==} - engines: {node: '>=16'} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} peerDependencies: - typescript: '>=4.2.0' + typescript: '>=4.8.4' ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} @@ -4092,6 +4158,10 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -4127,6 +4197,11 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} @@ -4539,8 +4614,15 @@ snapshots: eslint: 8.57.1 eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + '@eslint-community/regexpp@4.12.1': {} + '@eslint-community/regexpp@4.12.2': {} + '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 @@ -4557,6 +4639,8 @@ snapshots: '@eslint/js@8.57.1': {} + '@esotericsoftware/spine-core@4.2.119': {} + '@fastify/deepmerge@1.3.0': {} '@galacean/engine-toolkit-auxiliary-lines@1.3.9(@galacean/engine@packages+galacean)': @@ -4662,6 +4746,10 @@ snapshots: dependencies: '@galacean/engine': link:packages/galacean + '@galacean/engine-toolkit-stats@1.6.0(@galacean/engine@packages+galacean)': + dependencies: + '@galacean/engine': link:packages/galacean + '@galacean/engine-toolkit@1.3.9(@galacean/engine@packages+galacean)': dependencies: '@galacean/engine-toolkit-auxiliary-lines': 1.3.9(@galacean/engine@packages+galacean) @@ -5149,8 +5237,6 @@ snapshots: '@types/estree@1.0.6': {} - '@types/json-schema@7.0.15': {} - '@types/keyv@3.1.4': dependencies: '@types/node': 18.19.64 @@ -5175,99 +5261,102 @@ snapshots: dependencies: '@types/node': 18.19.64 - '@types/semver@7.5.8': {} - '@types/statuses@2.0.5': {} '@types/tough-cookie@4.0.5': {} '@types/webxr@0.5.22': {} - '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)': + '@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)': dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.6.3) - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.1)(typescript@5.6.3) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.6.3) - '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.3.7 + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.58.1(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/type-utils': 8.58.1(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/utils': 8.58.1(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.58.1 eslint: 8.57.1 - graphemer: 1.4.0 - ignore: 5.3.2 + ignore: 7.0.5 natural-compare: 1.4.0 - semver: 7.6.3 - ts-api-utils: 1.4.0(typescript@5.6.3) - optionalDependencies: + ts-api-utils: 2.5.0(typescript@5.6.3) typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3)': + '@typescript-eslint/parser@8.58.1(eslint@8.57.1)(typescript@5.6.3)': dependencies: - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.6.3) - '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.3.7 + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/typescript-estree': 8.58.1(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.58.1 + debug: 4.4.3 eslint: 8.57.1 - optionalDependencies: typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@6.21.0': + '@typescript-eslint/project-service@8.58.1(typescript@5.6.3)': dependencies: - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/visitor-keys': 6.21.0 + '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@5.6.3) + '@typescript-eslint/types': 8.58.1 + debug: 4.4.3 + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color - '@typescript-eslint/type-utils@6.21.0(eslint@8.57.1)(typescript@5.6.3)': + '@typescript-eslint/scope-manager@8.58.1': dependencies: - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.6.3) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.6.3) - debug: 4.3.7 + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/visitor-keys': 8.58.1 + + '@typescript-eslint/tsconfig-utils@8.58.1(typescript@5.6.3)': + dependencies: + typescript: 5.6.3 + + '@typescript-eslint/type-utils@8.58.1(eslint@8.57.1)(typescript@5.6.3)': + dependencies: + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/typescript-estree': 8.58.1(typescript@5.6.3) + '@typescript-eslint/utils': 8.58.1(eslint@8.57.1)(typescript@5.6.3) + debug: 4.4.3 eslint: 8.57.1 - ts-api-utils: 1.4.0(typescript@5.6.3) - optionalDependencies: + ts-api-utils: 2.5.0(typescript@5.6.3) typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@6.21.0': {} + '@typescript-eslint/types@8.58.1': {} - '@typescript-eslint/typescript-estree@6.21.0(typescript@5.6.3)': + '@typescript-eslint/typescript-estree@8.58.1(typescript@5.6.3)': dependencies: - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.3.7 - globby: 11.1.0 - is-glob: 4.0.3 - minimatch: 9.0.3 - semver: 7.6.3 - ts-api-utils: 1.4.0(typescript@5.6.3) - optionalDependencies: + '@typescript-eslint/project-service': 8.58.1(typescript@5.6.3) + '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@5.6.3) + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/visitor-keys': 8.58.1 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.7.4 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@5.6.3) typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@6.21.0(eslint@8.57.1)(typescript@5.6.3)': + '@typescript-eslint/utils@8.58.1(eslint@8.57.1)(typescript@5.6.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) - '@types/json-schema': 7.0.15 - '@types/semver': 7.5.8 - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.6.3) + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/typescript-estree': 8.58.1(typescript@5.6.3) eslint: 8.57.1 - semver: 7.6.3 + typescript: 5.6.3 transitivePeerDependencies: - supports-color - - typescript - '@typescript-eslint/visitor-keys@6.21.0': + '@typescript-eslint/visitor-keys@8.58.1': dependencies: - '@typescript-eslint/types': 6.21.0 - eslint-visitor-keys: 3.4.3 + '@typescript-eslint/types': 8.58.1 + eslint-visitor-keys: 5.0.1 '@ungap/structured-clone@1.2.0': {} @@ -5455,16 +5544,20 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ansi-colors@4.1.3: {} - ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 + ansi-escapes@7.3.0: + dependencies: + environment: 1.1.0 + ansi-regex@5.0.1: {} ansi-regex@6.1.0: {} + ansi-regex@6.2.2: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -5473,6 +5566,8 @@ snapshots: ansi-styles@6.2.1: {} + ansi-styles@6.2.3: {} + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 @@ -5498,18 +5593,16 @@ snapshots: array-ify@1.0.0: {} - array-union@2.1.0: {} - arrify@1.0.1: {} assertion-error@2.0.1: {} - astral-regex@2.0.0: {} - at-least-node@1.0.0: {} balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + binary-extensions@2.3.0: {} boolean@3.2.0: @@ -5524,6 +5617,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -5649,14 +5746,14 @@ snapshots: clean-stack@2.2.0: {} - cli-cursor@3.1.0: + cli-cursor@5.0.0: dependencies: - restore-cursor: 3.1.0 + restore-cursor: 5.1.0 - cli-truncate@2.1.0: + cli-truncate@5.2.0: dependencies: - slice-ansi: 3.0.0 - string-width: 4.2.3 + slice-ansi: 8.0.0 + string-width: 8.2.0 cli-width@4.1.0: {} @@ -5684,11 +5781,11 @@ snapshots: colorette@2.0.20: {} + commander@14.0.3: {} + commander@2.20.3: optional: true - commander@6.2.1: {} - commondir@1.0.1: {} compare-func@2.0.0: @@ -5785,6 +5882,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.3: + dependencies: + ms: 2.1.3 + decamelize-keys@1.1.1: dependencies: decamelize: 1.2.0 @@ -5796,8 +5897,6 @@ snapshots: dependencies: mimic-response: 1.0.1 - dedent@0.7.0: {} - deep-eql@5.0.2: {} deep-is@0.1.4: {} @@ -5838,10 +5937,6 @@ snapshots: diff@4.0.2: {} - dir-glob@3.0.1: - dependencies: - path-type: 4.0.0 - doctrine@3.0.0: dependencies: esutils: 2.0.3 @@ -5868,6 +5963,8 @@ snapshots: transitivePeerDependencies: - supports-color + emoji-regex@10.6.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -5879,13 +5976,10 @@ snapshots: dependencies: once: 1.4.0 - enquirer@2.4.1: - dependencies: - ansi-colors: 4.1.3 - strip-ansi: 6.0.1 - env-paths@2.2.1: {} + environment@1.1.0: {} + error-ex@1.3.2: dependencies: is-arrayish: 0.2.1 @@ -6062,6 +6156,8 @@ snapshots: eslint-visitor-keys@3.4.3: {} + eslint-visitor-keys@5.0.1: {} + eslint@8.57.1: dependencies: '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) @@ -6135,17 +6231,7 @@ snapshots: esutils@2.0.3: {} - execa@4.1.0: - dependencies: - cross-spawn: 7.0.5 - get-stream: 5.2.0 - human-signals: 1.1.1 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 + eventemitter3@5.0.4: {} execa@8.0.1: dependencies: @@ -6174,14 +6260,6 @@ snapshots: fast-diff@1.3.0: {} - fast-glob@3.3.2: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} @@ -6198,6 +6276,10 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 @@ -6279,6 +6361,8 @@ snapshots: get-caller-file@2.0.5: {} + get-east-asian-width@1.5.0: {} + get-intrinsic@1.2.4: dependencies: es-errors: 1.3.0 @@ -6288,8 +6372,6 @@ snapshots: hasown: 2.0.2 optional: true - get-own-enumerable-property-symbols@3.0.2: {} - get-package-type@0.1.0: {} get-stdin@8.0.0: {} @@ -6387,15 +6469,6 @@ snapshots: gopd: 1.0.1 optional: true - globby@11.1.0: - dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.2 - ignore: 5.3.2 - merge2: 1.4.1 - slash: 3.0.0 - gopd@1.0.1: dependencies: get-intrinsic: 1.2.4 @@ -6459,14 +6532,14 @@ snapshots: http-cache-semantics@4.1.1: {} - human-signals@1.1.1: {} - human-signals@5.0.0: {} husky@8.0.3: {} ignore@5.3.2: {} + ignore@7.0.5: {} + immutable@5.0.2: {} import-fresh@3.3.0: @@ -6501,6 +6574,10 @@ snapshots: is-fullwidth-code-point@3.0.0: {} + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.5.0 + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -6511,8 +6588,6 @@ snapshots: is-number@7.0.0: {} - is-obj@1.0.1: {} - is-obj@2.0.0: {} is-path-inside@3.0.3: {} @@ -6523,8 +6598,6 @@ snapshots: dependencies: '@types/estree': 1.0.6 - is-regexp@1.0.0: {} - is-stream@2.0.1: {} is-stream@3.0.0: {} @@ -6535,8 +6608,6 @@ snapshots: is-typedarray@1.0.0: {} - is-unicode-supported@0.1.0: {} - is-windows@1.0.2: {} isarray@1.0.0: {} @@ -6671,38 +6742,23 @@ snapshots: lines-and-columns@1.2.4: {} - lint-staged@10.5.4: + lint-staged@16.4.0: dependencies: - chalk: 4.1.2 - cli-truncate: 2.1.0 - commander: 6.2.1 - cosmiconfig: 7.1.0 - debug: 4.3.7 - dedent: 0.7.0 - enquirer: 2.4.1 - execa: 4.1.0 - listr2: 3.14.0(enquirer@2.4.1) - log-symbols: 4.1.0 - micromatch: 4.0.8 - normalize-path: 3.0.0 - please-upgrade-node: 3.2.0 - string-argv: 0.3.1 - stringify-object: 3.3.0 - transitivePeerDependencies: - - supports-color + commander: 14.0.3 + listr2: 9.0.5 + picomatch: 4.0.4 + string-argv: 0.3.2 + tinyexec: 1.1.1 + yaml: 2.8.3 - listr2@3.14.0(enquirer@2.4.1): + listr2@9.0.5: dependencies: - cli-truncate: 2.1.0 + cli-truncate: 5.2.0 colorette: 2.0.20 - log-update: 4.0.0 - p-map: 4.0.0 + eventemitter3: 5.0.4 + log-update: 6.1.0 rfdc: 1.4.1 - rxjs: 7.8.1 - through: 2.3.8 - wrap-ansi: 7.0.0 - optionalDependencies: - enquirer: 2.4.1 + wrap-ansi: 9.0.2 locate-path@5.0.0: dependencies: @@ -6718,17 +6774,13 @@ snapshots: lodash@4.17.21: {} - log-symbols@4.1.0: - dependencies: - chalk: 4.1.2 - is-unicode-supported: 0.1.0 - - log-update@4.0.0: + log-update@6.1.0: dependencies: - ansi-escapes: 4.3.2 - cli-cursor: 3.1.0 - slice-ansi: 4.0.0 - wrap-ansi: 6.2.0 + ansi-escapes: 7.3.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.2 + strip-ansi: 7.1.0 + wrap-ansi: 9.0.2 loupe@3.1.2: {} @@ -6797,30 +6849,29 @@ snapshots: merge-stream@2.0.0: {} - merge2@1.4.1: {} - micromatch@4.0.8: dependencies: braces: 3.0.3 picomatch: 2.3.1 + optional: true mime@2.6.0: {} - mimic-fn@2.1.0: {} - mimic-fn@4.0.0: {} + mimic-function@5.0.1: {} + mimic-response@1.0.1: {} min-indent@1.0.1: {} - minimatch@3.1.2: + minimatch@10.2.5: dependencies: - brace-expansion: 1.1.11 + brace-expansion: 5.0.5 - minimatch@9.0.3: + minimatch@3.1.2: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 1.1.11 minimatch@9.0.5: dependencies: @@ -6934,10 +6985,6 @@ snapshots: pify: 3.0.0 optional: true - npm-run-path@4.0.1: - dependencies: - path-key: 3.1.1 - npm-run-path@5.3.0: dependencies: path-key: 4.0.0 @@ -6994,14 +7041,14 @@ snapshots: dependencies: wrappy: 1.0.2 - onetime@5.1.2: - dependencies: - mimic-fn: 2.1.0 - onetime@6.0.0: dependencies: mimic-fn: 4.0.0 + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + opener@1.5.2: {} optionator@0.9.4: @@ -7037,10 +7084,6 @@ snapshots: dependencies: aggregate-error: 3.1.0 - p-map@4.0.0: - dependencies: - aggregate-error: 3.1.0 - p-try@2.2.0: {} package-hash@4.0.0: @@ -7100,6 +7143,8 @@ snapshots: picomatch@4.0.2: {} + picomatch@4.0.4: {} + pify@3.0.0: optional: true @@ -7121,10 +7166,6 @@ snapshots: optionalDependencies: fsevents: 2.3.2 - please-upgrade-node@3.2.0: - dependencies: - semver-compare: 1.0.0 - postcss@8.4.49: dependencies: nanoid: 3.3.7 @@ -7261,10 +7302,10 @@ snapshots: dependencies: lowercase-keys: 1.0.1 - restore-cursor@3.1.0: + restore-cursor@5.1.0: dependencies: - onetime: 5.1.2 - signal-exit: 3.0.7 + onetime: 7.0.0 + signal-exit: 4.1.0 reusify@1.0.4: {} @@ -7355,10 +7396,6 @@ snapshots: dependencies: queue-microtask: 1.2.3 - rxjs@7.8.1: - dependencies: - tslib: 2.8.1 - safe-buffer@5.1.2: {} safe-buffer@5.2.1: {} @@ -7371,7 +7408,8 @@ snapshots: optionalDependencies: '@parcel/watcher': 2.5.0 - semver-compare@1.0.0: {} + semver-compare@1.0.0: + optional: true semver@5.7.2: {} @@ -7381,6 +7419,8 @@ snapshots: semver@7.6.3: {} + semver@7.7.4: {} + serialize-error@7.0.1: dependencies: type-fest: 0.13.1 @@ -7416,19 +7456,15 @@ snapshots: skip-regex@1.0.2: {} - slash@3.0.0: {} - - slice-ansi@3.0.0: + slice-ansi@7.1.2: dependencies: - ansi-styles: 4.3.0 - astral-regex: 2.0.0 - is-fullwidth-code-point: 3.0.0 + ansi-styles: 6.2.1 + is-fullwidth-code-point: 5.1.0 - slice-ansi@4.0.0: + slice-ansi@8.0.0: dependencies: - ansi-styles: 4.3.0 - astral-regex: 2.0.0 - is-fullwidth-code-point: 3.0.0 + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 source-map-js@1.2.1: {} @@ -7482,8 +7518,6 @@ snapshots: strict-event-emitter@0.5.1: {} - string-argv@0.3.1: {} - string-argv@0.3.2: {} string-width@4.2.3: @@ -7498,6 +7532,17 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.0 + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.5.0 + strip-ansi: 7.1.0 + + string-width@8.2.0: + dependencies: + get-east-asian-width: 1.5.0 + strip-ansi: 7.2.0 + string_decoder@1.1.1: dependencies: safe-buffer: 5.1.2 @@ -7506,12 +7551,6 @@ snapshots: dependencies: safe-buffer: 5.2.1 - stringify-object@3.3.0: - dependencies: - get-own-enumerable-property-symbols: 3.0.2 - is-obj: 1.0.1 - is-regexp: 1.0.0 - strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -7520,9 +7559,11 @@ snapshots: dependencies: ansi-regex: 6.1.0 - strip-bom@4.0.0: {} + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 - strip-final-newline@2.0.0: {} + strip-bom@4.0.0: {} strip-final-newline@3.0.0: {} @@ -7592,11 +7633,18 @@ snapshots: tinyexec@0.3.1: {} + tinyexec@1.1.1: {} + tinyglobby@0.2.10: dependencies: fdir: 6.4.2(picomatch@4.0.2) picomatch: 4.0.2 + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + tinypool@1.0.2: {} tinyrainbow@1.2.0: {} @@ -7620,7 +7668,7 @@ snapshots: trim-newlines@3.0.1: {} - ts-api-utils@1.4.0(typescript@5.6.3): + ts-api-utils@2.5.0(typescript@5.6.3): dependencies: typescript: 5.6.3 @@ -7738,7 +7786,7 @@ snapshots: vite-node@2.1.5(@types/node@18.19.64)(sass@1.81.0)(terser@5.44.1): dependencies: cac: 6.7.14 - debug: 4.3.7 + debug: 4.4.3 es-module-lexer: 1.5.4 pathe: 1.1.2 vite: 5.4.11(@types/node@18.19.64)(sass@1.81.0)(terser@5.44.1) @@ -7831,7 +7879,7 @@ snapshots: '@vitest/spy': 2.1.5 '@vitest/utils': 2.1.5 chai: 5.1.2 - debug: 4.3.7 + debug: 4.4.3 expect-type: 1.1.0 magic-string: 0.30.12 pathe: 1.1.2 @@ -7892,6 +7940,12 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.0 + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.1 + string-width: 7.2.0 + strip-ansi: 7.1.0 + wrappy@1.0.2: {} write-file-atomic@3.0.3: @@ -7913,6 +7967,8 @@ snapshots: yaml@1.10.2: {} + yaml@2.8.3: {} + yargs-parser@18.1.3: dependencies: camelcase: 5.3.1 diff --git a/rollup-plugin-glsl.js b/rollup-plugin-glsl.js index cc545c623b..3a48c3efc9 100644 --- a/rollup-plugin-glsl.js +++ b/rollup-plugin-glsl.js @@ -35,7 +35,7 @@ function compressShader(code) { export default function glsl(userOptions = {}) { const options = Object.assign( { - include: ["**/*.vs", "**/*.fs", "**/*.vert", "**/*.frag", "**/*.glsl"] + include: ["**/*.vs", "**/*.fs", "**/*.vert", "**/*.frag", "**/*.glsl", "**/*.shader"] }, userOptions ); diff --git a/rollup.config.js b/rollup.config.js index b72def7cb8..dd57ae8a1b 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -32,7 +32,7 @@ const extensions = [".js", ".jsx", ".ts", ".tsx"]; const mainFields = NODE_ENV === "development" ? ["debug", "module", "main"] : undefined; const glslPlugin = glsl({ - include: [/\.(glsl|gs)$/], + include: [/\.(glsl|shader)$/], compress: false }); @@ -90,7 +90,7 @@ function config({ location, pkgJson, verboseMode }) { glslifyPluginIdx, 1, glsl({ - include: [/\.(glsl|gs)$/], + include: [/\.(glsl|shader)$/], compress: true }) ); @@ -157,7 +157,7 @@ function config({ location, pkgJson, verboseMode }) { glslifyPluginIdx, 1, glsl({ - include: [/\.(glsl|gs)$/], + include: [/\.(glsl|shader)$/], compress: true }) ); diff --git a/tests/package.json b/tests/package.json index bcf0debcb4..a95ffceb25 100644 --- a/tests/package.json +++ b/tests/package.json @@ -1,7 +1,7 @@ { "name": "@galacean/engine-tests", "private": true, - "version": "2.0.0-alpha.24", + "version": "0.0.0-experimental-2.0-game.19", "license": "MIT", "main": "dist/main.js", "module": "dist/module.js", @@ -25,7 +25,9 @@ "@galacean/engine-shaderlab": "workspace:*", "@galacean/engine-physics-physx": "workspace:*", "@galacean/engine-ui": "workspace:*", - "@galacean/engine-shader": "workspace:*" + "@galacean/engine-shader": "workspace:*", + "@galacean/engine-spine": "workspace:*", + "@galacean/engine-spine-core-4.2": "workspace:*" }, "devDependencies": { "@vitest/browser": "2.1.3" diff --git a/tests/src/core/2d/text/TextUtils.test.ts b/tests/src/core/2d/text/TextUtils.test.ts index 972b3f593c..68accf6ff8 100644 --- a/tests/src/core/2d/text/TextUtils.test.ts +++ b/tests/src/core/2d/text/TextUtils.test.ts @@ -359,7 +359,7 @@ describe("TextUtils", () => { ); expect(result.width).to.be.equal(23); expect(result.height).to.be.equal(135); - expect(result.lines).to.be.deep.equal([' ', ' ', 'W', 'or', 'ld']); + expect(result.lines).to.be.deep.equal([" ", " ", "W", "or", "ld"]); expect(result.lineHeight).to.be.equal(27); wrap1TextRenderer.enableWrapping = true; @@ -564,6 +564,90 @@ describe("TextUtils", () => { ); }); + it("measureTextWithShrink", () => { + // @ts-ignore + const { _pixelsPerUnit } = Engine; + const r = textRendererTruncate; + r.overflowMode = OverflowMode.Shrink; + r.enableWrapping = false; + r.lineSpacing = 0; + r.characterSpacing = 0; + r.text = "15"; + // applyFontSize: switch the sub font for each measurement during the shrink search. + const apply = (fs: number) => { + // @ts-ignore + r._applyFontSizeForShrink(fs); + }; + const measure = (w: number, h: number, fontSize: number) => { + r.width = w; + r.height = h; + r.fontSize = fontSize; + r.bounds; + return TextUtils.measureTextWithShrink( + r, + w * _pixelsPerUnit, + h * _pixelsPerUnit, + fontSize, + r.lineSpacing, + r.characterSpacing, + r.enableWrapping, + apply + ); + }; + + // Fits at original size → keep it (never shrinks unnecessarily). + let res = measure(3, 1, 24); + expect(res.fontSize).to.be.equal(24); + expect(res.metrics.width).to.be.at.most(300); + expect(res.metrics.lineHeight * res.metrics.lines.length).to.be.at.most(100); + + // Box too short → shrink the font size until the content height fits. + res = measure(3, 0.3, 80); + expect(res.fontSize).to.be.below(80); + expect(res.metrics.lineHeight * res.metrics.lines.length).to.be.at.most(30); + + // Box too narrow (no wrap) → shrink the font size until the width fits. + res = measure(0.3, 3, 80); + expect(res.fontSize).to.be.below(80); + expect(res.metrics.width).to.be.at.most(30); + + // Only shrinks, never enlarges: even a huge box keeps the original font size. + res = measure(10, 10, 24); + expect(res.fontSize).to.be.equal(24); + + // Wrapping path: a long text that wraps to multiple lines and overflows the height + // must shrink until the real content height (lineHeight * lineCount) fits the box. + r.enableWrapping = true; + r.text = "这是一段会换行的较长文本"; + res = measure(2, 0.5, 60); // 200x50px box + expect(res.fontSize).to.be.below(60); + expect(res.metrics.width).to.be.at.most(200); + expect(res.metrics.lineHeight * res.metrics.lines.length).to.be.at.most(50); + }); + + it("_getCharInfo writes the atlas only when uploadTexture is true (used by SHRINK search)", () => { + const r = wrap2TextRenderer; + // @ts-ignore + const subFont = r._getSubFont(); + const fontString = subFont.nativeFontString; + const char = "Q"; // a glyph not measured by the other cases above + + // Sanity: the glyph is not in the atlas yet. + // @ts-ignore + expect(subFont._getCharInfo(char)).to.be.null; + + // measure-only path: returns valid metrics but must NOT upload to the atlas. + const info = TextUtils._getCharInfo(char, fontString, subFont, false); + expect(info.w).to.be.greaterThan(0); + // @ts-ignore + expect(subFont._getCharInfo(char)).to.be.null; + + // full path: uploads the glyph to the atlas. + TextUtils._getCharInfo(char, fontString, subFont, true); + // @ts-ignore + expect(subFont._getCharInfo(char)).to.not.be.null; + }); + afterAll(() => { engine.destroy(); }); diff --git a/tests/src/core/Animator.test.ts b/tests/src/core/Animator.test.ts index c37d476a92..22fe8b110b 100644 --- a/tests/src/core/Animator.test.ts +++ b/tests/src/core/Animator.test.ts @@ -238,6 +238,89 @@ describe("Animator test", function () { expect(layerState).to.eq(2); }); + it("crossFade advances with per-instance playData speed instead of shared AnimatorState speed", () => { + const sharedStates = animator.animatorController.layers[0].stateMachine.states; + const sharedWalkState = sharedStates.find((state) => state.name === "Walk"); + const sharedRunState = sharedStates.find((state) => state.name === "Run"); + const oldWalkSpeed = sharedWalkState.speed; + const oldRunSpeed = sharedRunState.speed; + + try { + animator.play("Walk"); + animator.crossFade("Run", 1.0, 0); + + const layerData = animator["_animatorLayersData"][0]; + layerData.srcPlayData.speed = 0.25; + layerData.destPlayData.speed = 0.25; + sharedWalkState.speed = 10; + sharedRunState.speed = 10; + + const srcPlayedTime = layerData.srcPlayData.playedTime; + const destPlayedTime = layerData.destPlayData.playedTime; + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(0.2); + + expect(layerData.srcPlayData.playedTime - srcPlayedTime).toBeCloseTo(0.05, 5); + expect(layerData.destPlayData.playedTime - destPlayedTime).toBeCloseTo(0.05, 5); + } finally { + sharedWalkState.speed = oldWalkSpeed; + sharedRunState.speed = oldRunSpeed; + } + }); + + it("playData wrapMode overrides shared AnimatorState wrapMode per instance", () => { + const sharedWalkState = animator.animatorController.layers[0].stateMachine.states.find( + (state) => state.name === "Walk" + ); + const oldWrapMode = sharedWalkState.wrapMode; + + try { + sharedWalkState.wrapMode = WrapMode.Loop; + animator.play("Walk"); + + const layerData = animator["_animatorLayersData"][0]; + const playData = layerData.srcPlayData; + playData.wrapMode = WrapMode.Once; + + expect(sharedWalkState.wrapMode).to.eq(WrapMode.Loop); + + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(playData.state.clip.length + 0.1); + + expect(layerData.layerState).to.eq(LayerState.Finished); + } finally { + sharedWalkState.wrapMode = oldWrapMode; + } + }); + + it("playData wrapMode does not leak between animators sharing one controller", () => { + const sharedWalkState = animator.animatorController.layers[0].stateMachine.states.find( + (state) => state.name === "Walk" + ); + const oldWrapMode = sharedWalkState.wrapMode; + const otherEntity = new Entity(engine); + const otherAnimator = otherEntity.addComponent(Animator); + otherAnimator.animatorController = animator.animatorController; + + try { + sharedWalkState.wrapMode = WrapMode.Loop; + animator.play("Walk"); + otherAnimator.play("Walk"); + + const playData = animator["_animatorLayersData"][0].srcPlayData; + const otherPlayData = otherAnimator["_animatorLayersData"][0].srcPlayData; + playData.wrapMode = WrapMode.Once; + + expect(otherPlayData.wrapMode).to.eq(WrapMode.Loop); + expect(sharedWalkState.wrapMode).to.eq(WrapMode.Loop); + } finally { + sharedWalkState.wrapMode = oldWrapMode; + otherEntity.destroy(); + } + }); + it("cross fade in fixed time", () => { const runState = animator.findAnimatorState("Run"); animator.play("Walk"); @@ -307,7 +390,7 @@ describe("Animator test", function () { const additiveLayer = new AnimatorControllerLayer("additiveLayer"); additiveLayer.stateMachine = animatorStateMachine; const mask = AnimatorLayerMask.createByEntity(animator.entity); - mask.setPathMaskActive("_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03/b_Neck_04", false, true); + mask.setPathMaskActive("root/_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03/b_Neck_04", false, true); additiveLayer.mask = mask; additiveLayer.blendingMode = AnimatorLayerBlendingMode.Additive; animatorController.addLayer(additiveLayer); @@ -319,12 +402,12 @@ describe("Animator test", function () { animator.play("Walk", 0); animator.play("Run", 1); - const parentEntity = animator.entity.findByPath("_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03/"); + const parentEntity = animator.entity.findByPath("root/_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03/"); const targetEntity = animator.entity.findByPath( - "_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03/b_Neck_04" + "root/_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03/b_Neck_04" ); const childEntity = animator.entity.findByPath( - "_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03/b_Neck_04/b_Head_05" + "root/_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03/b_Neck_04/b_Head_05" ); let layerData = animator["_animatorLayersData"][1]; @@ -338,7 +421,7 @@ describe("Animator test", function () { expect(childLayerCurveOwner.isActive).to.eq(false); animator.animatorController.removeLayer(1); - mask.removePathMask("_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03/b_Neck_04/b_Head_05"); + mask.removePathMask("root/_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03/b_Neck_04/b_Head_05"); animator.animatorController.addLayer(additiveLayer); animator.play("Run", 1); layerData = animator["_animatorLayersData"][1]; @@ -350,7 +433,7 @@ describe("Animator test", function () { animator.play("Walk"); class TestScript extends Script { - event0(): void { } + event0(): void {} } const testScript = animator.entity.addComponent(TestScript); @@ -366,6 +449,84 @@ describe("Animator test", function () { expect(testScriptSpy).toHaveBeenCalledTimes(1); }); + it("fireEvents gates AnimationEvent dispatch without consuming the event", () => { + animator.play("Walk"); + + class TestScript extends Script { + event0(): void {} + } + + const testScript = animator.entity.addComponent(TestScript); + const testScriptSpy = vi.spyOn(testScript, "event0"); + + const event0 = new AnimationEvent(); + event0.functionName = "event0"; + event0.time = 0; + + const state = animator.findAnimatorState("Walk"); + state.clip.addEvent(event0); + + animator.fireEvents = false; + animator.update(0); + expect(testScriptSpy).not.toHaveBeenCalled(); + + animator.fireEvents = true; + animator.update(0.1); + expect(testScriptSpy).toHaveBeenCalledTimes(1); + }); + + it("does not refire animation events when a once clip reaches the end", () => { + const entity = new Entity(engine); + const onceAnimator = entity.addComponent(Animator); + const controller = new AnimatorController(engine); + const layer = new AnimatorControllerLayer("Base Layer"); + controller.addLayer(layer); + + const state = layer.stateMachine.addState("once"); + state.wrapMode = WrapMode.Once; + + const clip = new AnimationClip("once-clip"); + const curve = new AnimationFloatCurve(); + const start = new Keyframe(); + const end = new Keyframe(); + start.time = 0; + start.value = 0; + end.time = 1; + end.value = 1; + curve.addKey(start); + curve.addKey(end); + clip.addCurveBinding("", Transform, "position.x", curve); + + class TestScript extends Script { + event0(): void {} + } + + const event0 = new AnimationEvent(); + event0.functionName = "event0"; + event0.time = 0.5; + clip.addEvent(event0); + state.clip = clip; + onceAnimator.animatorController = controller; + + const testScript = entity.addComponent(TestScript); + const testScriptSpy = vi.spyOn(testScript, "event0"); + + try { + onceAnimator.play("once"); + // @ts-ignore + onceAnimator.engine.time._frameCount++; + onceAnimator.update(0.75); + expect(testScriptSpy).toHaveBeenCalledTimes(1); + + // @ts-ignore + onceAnimator.engine.time._frameCount++; + onceAnimator.update(0.5); + expect(testScriptSpy).toHaveBeenCalledTimes(1); + } finally { + entity.destroy(); + } + }); + it("stateMachine", () => { animator.animatorController.addParameter("playerSpeed", 1); const stateMachine = animator.animatorController.layers[0].stateMachine; @@ -792,8 +953,8 @@ describe("Animator test", function () { animator.animatorController = animatorController; class TestScript extends StateMachineScript { - onStateEnter(animator) { } - onStateExit(animator) { } + onStateEnter(animator) {} + onStateExit(animator) {} } const testScript = state1.addStateMachineScript(TestScript); @@ -1046,6 +1207,82 @@ describe("Animator test", function () { expect(animator.entity.clone().getComponent(Animator).animatorController).to.eq(animator.animatorController); }); + it("samples self-name-prefixed curve paths on wrapped roots", () => { + const wrappedRoot = new Entity(engine, "GLTF_ROOT"); + const hips = new Entity(engine, "mixamorig:Hips"); + const spine = new Entity(engine, "mixamorig:Spine"); + hips.parent = wrappedRoot; + spine.parent = hips; + + const clip = new AnimationClip("idle"); + const hipsCurve = new AnimationFloatCurve(); + const spineCurve = new AnimationFloatCurve(); + const hipsStart = new Keyframe(); + const hipsEnd = new Keyframe(); + hipsStart.time = 0; + hipsStart.value = 0; + hipsEnd.time = 0.1; + hipsEnd.value = 1; + hipsCurve.addKey(hipsStart); + hipsCurve.addKey(hipsEnd); + + const spineStart = new Keyframe(); + const spineEnd = new Keyframe(); + spineStart.time = 0; + spineStart.value = 0; + spineEnd.time = 0.1; + spineEnd.value = 1; + spineCurve.addKey(spineStart); + spineCurve.addKey(spineEnd); + + clip.addCurveBinding("mixamorig:Hips", Transform, "position.x", hipsCurve); + clip.addCurveBinding("mixamorig:Hips/mixamorig:Spine", Transform, "position.y", spineCurve); + + expect(wrappedRoot.findByPath("mixamorig:Hips")).to.eq(hips); + expect(wrappedRoot.findByPath("mixamorig:Hips/mixamorig:Spine")).to.eq(spine); + + // @ts-ignore + clip._sampleAnimation(wrappedRoot, 0.1); + + expect(wrappedRoot.transform.position.x).to.eq(0); + expect(hips.transform.position.x).to.eq(1); + expect(spine.transform.position.y).to.eq(1); + }); + + it("sampleAnimation samples clip curves without firing AnimationEvents", () => { + const entity = new Entity(engine, "sample-root"); + const clip = new AnimationClip("sample"); + const curve = new AnimationFloatCurve(); + const start = new Keyframe(); + const end = new Keyframe(); + start.time = 0; + start.value = 0; + end.time = 1; + end.value = 3; + curve.addKey(start); + curve.addKey(end); + clip.addCurveBinding("", Transform, "position.x", curve); + + class TestScript extends Script { + event0(): void {} + } + + const script = entity.addComponent(TestScript); + const eventSpy = vi.spyOn(script, "event0"); + const event0 = new AnimationEvent(); + event0.functionName = "event0"; + event0.time = 0; + clip.addEvent(event0); + + try { + clip.sampleAnimation(entity, 1); + expect(entity.transform.position.x).to.eq(3); + expect(eventSpy).not.toHaveBeenCalled(); + } finally { + entity.destroy(); + } + }); + it("anyState transition interrupts crossFade", () => { const { animatorController } = animator; animatorController.addParameter("interrupt", false); diff --git a/tests/src/core/AnimatorHang.test.ts b/tests/src/core/AnimatorHang.test.ts new file mode 100644 index 0000000000..99a8a5ba14 --- /dev/null +++ b/tests/src/core/AnimatorHang.test.ts @@ -0,0 +1,20 @@ +import { Animator, Camera } from "@galacean/engine-core"; +import "@galacean/engine-loader"; +import type { GLTFResource } from "@galacean/engine-loader"; +import { WebGLEngine } from "@galacean/engine-rhi-webgl"; +import { describe, expect, it } from "vitest"; +import { glbResource } from "./model/fox"; +const canvasDOM = document.createElement("canvas"); +canvasDOM.width = 1024; +canvasDOM.height = 1024; +describe("Canvas 1024 test", async function () { + const engine = await WebGLEngine.create({ canvas: canvasDOM }); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + rootEntity.addComponent(Camera); + const resource = await engine.resourceManager.load(glbResource); + const defaultSceneRoot = resource.defaultSceneRoot; + rootEntity.addChild(defaultSceneRoot); + const animator = defaultSceneRoot.getComponent(Animator); + it("loaded", () => { expect(animator).not.eq(null); }); +}); diff --git a/tests/src/core/Camera.test.ts b/tests/src/core/Camera.test.ts index 52ae7890d1..5f77077f7f 100644 --- a/tests/src/core/Camera.test.ts +++ b/tests/src/core/Camera.test.ts @@ -245,10 +245,64 @@ describe("camera test", function () { expect(Math.abs(ray.direction.z)).not.eq(Infinity); }); + it("screenPointToRay should ignore inherited scale from parent entity", () => { + // Simulate UICanvas scenario: camera is a child of a scaled parent entity + const scene = engine.sceneManager.scenes[0]; + const parentEntity = scene.createRootEntity("scaledParent"); + parentEntity.transform.setScale(1.5, 1.5, 1.5); + parentEntity.transform.setPosition(100, 200, 0); + + const childEntity = parentEntity.createChild("cameraChild"); + childEntity.transform.setPosition(0, 0, 500); + const scaledCamera = childEntity.addComponent(Camera); + scaledCamera.isOrthographic = true; + scaledCamera.orthographicSize = 5; + scaledCamera.nearClipPlane = 0.1; + scaledCamera.farClipPlane = 1000; + + // A camera without inherited scale at the same world position/rotation for comparison + const refEntity = scene.createRootEntity("refCamera"); + refEntity.transform.setWorldPosition( + childEntity.transform.worldPosition.x, + childEntity.transform.worldPosition.y, + childEntity.transform.worldPosition.z + ); + const refCamera = refEntity.addComponent(Camera); + refCamera.isOrthographic = true; + refCamera.orthographicSize = 5; + refCamera.nearClipPlane = 0.1; + refCamera.farClipPlane = 1000; + + // Both cameras should produce the same ray for the same screen point + const screenPoint = new Vector2(128, 128); + const rayScaled = scaledCamera.screenPointToRay(screenPoint, new Ray()); + const rayRef = refCamera.screenPointToRay(screenPoint, new Ray()); + + expect(rayScaled.origin.x).to.be.closeTo(rayRef.origin.x, 0.001); + expect(rayScaled.origin.y).to.be.closeTo(rayRef.origin.y, 0.001); + expect(rayScaled.direction.x).to.be.closeTo(rayRef.direction.x, 0.001); + expect(rayScaled.direction.y).to.be.closeTo(rayRef.direction.y, 0.001); + expect(rayScaled.direction.z).to.be.closeTo(rayRef.direction.z, 0.001); + + // Round-trip: worldToViewportPoint -> viewportToWorldPoint should be accurate + const worldPoint = new Vector3(105, 210, 0); + const viewportPoint = scaledCamera.worldToViewportPoint(worldPoint, new Vector3()); + const recoveredPoint = scaledCamera.viewportToWorldPoint(viewportPoint, new Vector3()); + expect(recoveredPoint.x).to.be.closeTo(worldPoint.x, 0.01); + expect(recoveredPoint.y).to.be.closeTo(worldPoint.y, 0.01); + expect(recoveredPoint.z).to.be.closeTo(worldPoint.z, 0.01); + + // Clean up + scaledCamera.destroy(); + refCamera.destroy(); + parentEntity.destroy(); + refEntity.destroy(); + }); + /* Attention: - Below methods will change the default view of current Camera. - If executed in advance, it will affect the expected results of other test cases, + Below methods will change the default view of current Camera. + If executed in advance, it will affect the expected results of other test cases, so it should be placed at the end of the test case execution. */ it("projection matrix", () => { diff --git a/tests/src/core/CloneUtils.test.ts b/tests/src/core/CloneUtils.test.ts index 7d7195f323..80ffbe27dd 100644 --- a/tests/src/core/CloneUtils.test.ts +++ b/tests/src/core/CloneUtils.test.ts @@ -87,6 +87,26 @@ class NestedObjectScript extends Script { config: { target: Entity; label: string } = { target: null, label: "" }; } +/** Script with UNDECORATED array of entities (no @deepClone) */ +class UndecoratedArrayScript extends Script { + entities: Entity[] = []; +} + +/** Script with UNDECORATED nested object containing entity refs */ +class UndecoratedObjectScript extends Script { + config: { target: Entity; label: string } = { target: null, label: "" }; +} + +/** Script with UNDECORATED nested array of arrays containing entities */ +class NestedArrayScript extends Script { + groups: Entity[][] = []; +} + +/** Script with UNDECORATED Map containing entity values */ +class MapRefScript extends Script { + entityMap: Map = new Map(); +} + /** Script for testing multiple same-type components on one entity */ class CounterScript extends Script { value: number = 0; @@ -244,7 +264,9 @@ describe("Clone remap", async () => { expect(clonedScript.speed).eq(42); expect(clonedScript.name2).eq("test"); expect(clonedScript.flag).eq(true); - expect(clonedScript.data).eq(obj); + // Plain objects are now deep cloned (independent copy) for undecorated properties + expect(clonedScript.data).not.eq(obj); + expect((clonedScript.data).x).eq(1); rootEntity.destroy(); }); @@ -873,4 +895,142 @@ describe("Clone remap", async () => { rootEntity.destroy(); }); }); + + describe("Undecorated array auto clone + remap (type inference)", () => { + it("undecorated entity array should create new array and remap elements", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const childA = parent.createChild("childA"); + const childB = parent.createChild("childB"); + const script = parent.addComponent(UndecoratedArrayScript); + script.entities = [childA, childB]; + + const cloned = parent.clone(); + const cs = cloned.getComponent(UndecoratedArrayScript); + + expect(cs.entities).not.eq(script.entities); + expect(cs.entities.length).eq(2); + expect(cs.entities[0]).not.eq(childA); + expect(cs.entities[1]).not.eq(childB); + expect(cs.entities[0]).eq(cloned.children[0]); + expect(cs.entities[1]).eq(cloned.children[1]); + + rootEntity.destroy(); + }); + + it("undecorated entity array with external ref keeps original", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const external = rootEntity.createChild("external"); + const script = parent.addComponent(UndecoratedArrayScript); + script.entities = [child, external]; + + const cloned = parent.clone(); + const cs = cloned.getComponent(UndecoratedArrayScript); + + expect(cs.entities[0]).eq(cloned.children[0]); + expect(cs.entities[1]).eq(external); + + rootEntity.destroy(); + }); + + it("undecorated empty array stays empty with independent reference", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(UndecoratedArrayScript); + script.entities = []; + + const cloned = parent.clone(); + const cs = cloned.getComponent(UndecoratedArrayScript); + + expect(cs.entities).not.eq(script.entities); + expect(cs.entities.length).eq(0); + + rootEntity.destroy(); + }); + }); + + describe("Undecorated nested object auto clone + remap (type inference)", () => { + it("undecorated object with entity ref should deep clone and remap", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const script = parent.addComponent(UndecoratedObjectScript); + script.config = { target: child, label: "hello" }; + + const cloned = parent.clone(); + const cs = cloned.getComponent(UndecoratedObjectScript); + + expect(cs.config).not.eq(script.config); + expect(cs.config.label).eq("hello"); + expect(cs.config.target).not.eq(child); + expect(cs.config.target).eq(cloned.children[0]); + + rootEntity.destroy(); + }); + + it("undecorated object with external entity ref keeps original", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const external = rootEntity.createChild("external"); + const script = parent.addComponent(UndecoratedObjectScript); + script.config = { target: external, label: "ext" }; + + const cloned = parent.clone(); + const cs = cloned.getComponent(UndecoratedObjectScript); + + expect(cs.config.target).eq(external); + expect(cs.config.label).eq("ext"); + + rootEntity.destroy(); + }); + }); + + describe("Nested array of arrays with entity refs (type inference)", () => { + it("undecorated nested entity arrays should recursively clone and remap", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const childA = parent.createChild("childA"); + const childB = parent.createChild("childB"); + const childC = parent.createChild("childC"); + const script = parent.addComponent(NestedArrayScript); + script.groups = [[childA, childB], [childC]]; + + const cloned = parent.clone(); + const cs = cloned.getComponent(NestedArrayScript); + + expect(cs.groups).not.eq(script.groups); + expect(cs.groups.length).eq(2); + expect(cs.groups[0]).not.eq(script.groups[0]); + expect(cs.groups[1]).not.eq(script.groups[1]); + expect(cs.groups[0][0]).eq(cloned.children[0]); + expect(cs.groups[0][1]).eq(cloned.children[1]); + expect(cs.groups[1][0]).eq(cloned.children[2]); + + rootEntity.destroy(); + }); + }); + + describe("Map with entity values (type inference)", () => { + it("undecorated Map should create new Map and remap entity values", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const external = rootEntity.createChild("external"); + const script = parent.addComponent(MapRefScript); + script.entityMap.set("internal", child); + script.entityMap.set("external", external); + + const cloned = parent.clone(); + const cs = cloned.getComponent(MapRefScript); + + expect(cs.entityMap).not.eq(script.entityMap); + expect(cs.entityMap.size).eq(2); + expect(cs.entityMap.get("internal")).eq(cloned.children[0]); + expect(cs.entityMap.get("external")).eq(external); + + rootEntity.destroy(); + }); + }); }); diff --git a/tests/src/core/Entity.test.ts b/tests/src/core/Entity.test.ts index 2fe8f908ad..212ee84286 100644 --- a/tests/src/core/Entity.test.ts +++ b/tests/src/core/Entity.test.ts @@ -320,6 +320,19 @@ describe("Entity", async () => { expect(parent.findByPath("child/grandson")).eq(grandson2); }); + it("findByPath accepts self-name prefix", () => { + const parent = new Entity(engine, "parent"); + parent.parent = scene.getRootEntity(); + const child = new Entity(engine, "child"); + child.parent = parent; + const grandson = new Entity(engine, "grandson"); + grandson.parent = child; + + expect(parent.findByPath("parent")).eq(parent); + expect(parent.findByPath("parent/child")).eq(child); + expect(parent.findByPath("parent/child/grandson")).eq(grandson); + }); + it("clearChildren", () => { const parent = new Entity(engine, "parent"); @@ -328,8 +341,29 @@ describe("Entity", async () => { child.parent = parent; const child2 = new Entity(engine, "child2"); child2.parent = parent; + + const parentModifyCount = [0, 0, 0]; + const childModifyCount = [0, 0, 0]; + const child2ModifyCount = [0, 0, 0]; + // @ts-ignore + parent._registerModifyListener((flag: EntityModifyFlags) => ++parentModifyCount[flag]); + // @ts-ignore + child._registerModifyListener((flag: EntityModifyFlags) => ++childModifyCount[flag]); + // @ts-ignore + child2._registerModifyListener((flag: EntityModifyFlags) => ++child2ModifyCount[flag]); + parent.clearChildren(); expect(parent.children.length).eq(0); + + // Parent should receive a single `Child` modify event for the whole clear so + // listeners (e.g. UICanvas) can invalidate their cached state. + expect(parentModifyCount[EntityModifyFlags.Child]).eq(1); + // Each detached child should receive a `Parent` modify event. + expect(childModifyCount[EntityModifyFlags.Parent]).eq(1); + expect(child2ModifyCount[EntityModifyFlags.Parent]).eq(1); + // Sibling index must be reset so the entity is treated as lonely afterwards. + expect(child.siblingIndex).eq(-1); + expect(child2.siblingIndex).eq(-1); }); it("sibling index", () => { const root = scene.createRootEntity(); @@ -681,4 +715,4 @@ describe("Entity", async () => { expect(script.onDestroy).toHaveBeenCalledTimes(1); }); }); -}); \ No newline at end of file +}); diff --git a/tests/src/core/RenderPipeline/RenderTargetPool.test.ts b/tests/src/core/RenderPipeline/RenderTargetPool.test.ts new file mode 100644 index 0000000000..6403a41884 --- /dev/null +++ b/tests/src/core/RenderPipeline/RenderTargetPool.test.ts @@ -0,0 +1,141 @@ +import { TextureFilterMode, TextureFormat, TextureWrapMode } from "@galacean/engine-core"; +// `RenderTargetPool` is `@internal` and intentionally not re-exported from the core barrel. +// Import directly from the source file for test access. +import { RenderTargetPool } from "../../../../packages/core/src/RenderPipeline/RenderTargetPool"; +import { WebGLEngine } from "@galacean/engine-rhi-webgl"; +import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +/** + * Helper: allocate an RT through the pool with sane defaults; varies only the bits that affect matching. + */ +function alloc( + pool: RenderTargetPool, + width: number, + height: number, + opts: { colorFormat?: TextureFormat; depthFormat?: TextureFormat | null; aa?: number } = {} +) { + return pool.allocateRenderTarget( + width, + height, + opts.colorFormat ?? TextureFormat.R8G8B8A8, + opts.depthFormat === undefined ? TextureFormat.Depth24Stencil8 : opts.depthFormat, + false, + false, + false, + opts.aa ?? 1, + TextureWrapMode.Clamp, + TextureFilterMode.Bilinear + ); +} + +describe("RenderTargetPool", () => { + const canvas = document.createElement("canvas"); + let engine: WebGLEngine; + let pool: RenderTargetPool; + + beforeAll(async () => { + engine = await WebGLEngine.create({ canvas }); + }); + + afterAll(() => { + engine.destroy(); + }); + + beforeEach(() => { + // Each test gets a fresh pool so leaked entries from earlier tests don't bleed across. + pool = new RenderTargetPool(engine); + }); + + describe("matching reuse", () => { + it("returns the same RT instance when the next allocate matches a freed entry's shape", () => { + const a = alloc(pool, 512, 512); + pool.freeRenderTarget(a); + const b = alloc(pool, 512, 512); + expect(b).to.equal(a); + }); + + it("allocates a fresh RT when shape does not match any freed entry", () => { + const a = alloc(pool, 512, 512); + pool.freeRenderTarget(a); + const b = alloc(pool, 256, 256); + expect(b).to.not.equal(a); + }); + + it("simulates multi-camera frame-internal reuse: A free → B alloc returns A's RT", () => { + // Camera A renders at full canvas, then releases + const a = alloc(pool, 1024, 768); + pool.freeRenderTarget(a); + // Camera B renders next at the same shape and finds A's RT in the pool + const b = alloc(pool, 1024, 768); + expect(b).to.equal(a); + pool.freeRenderTarget(b); + // Pool is back to one entry after both cameras returned the same RT + // (we can't directly observe size, but the next match-alloc must return it too) + const c = alloc(pool, 1024, 768); + expect(c).to.equal(a); + }); + }); + + describe("frame-age eviction via tick()", () => { + it("does not evict entries within maxFreeAgeFrames", () => { + pool.maxFreeAgeFrames = 5; + const a = alloc(pool, 256, 256); + pool.freeRenderTarget(a); + const baseFrame = engine.time.frameCount; + pool.tick(baseFrame + 3); + const b = alloc(pool, 256, 256); + expect(b).to.equal(a); + }); + + it("destroys entries idle longer than maxFreeAgeFrames", () => { + pool.maxFreeAgeFrames = 5; + const a = alloc(pool, 256, 256); + const baseFrame = engine.time.frameCount; + pool.freeRenderTarget(a); + pool.tick(baseFrame + 100); + // Entry was destroyed; next allocate produces a fresh RT + const b = alloc(pool, 256, 256); + expect(b).to.not.equal(a); + expect(a.destroyed).to.equal(true); + }); + }); + + describe("evictBySize for canvas resize", () => { + it("destroys free-list entries matching the given size", () => { + const a = alloc(pool, 800, 600); + const b = alloc(pool, 1024, 768); + pool.freeRenderTarget(a); + pool.freeRenderTarget(b); + + pool.evictBySize(800, 600); + expect(a.destroyed).to.equal(true); + expect(b.destroyed).to.equal(false); + + // Re-allocating at the other size still returns the survivor + const reused = alloc(pool, 1024, 768); + expect(reused).to.equal(b); + }); + + it("ignores entries whose dimensions do not match", () => { + const a = alloc(pool, 800, 600); + pool.freeRenderTarget(a); + pool.evictBySize(1024, 768); + expect(a.destroyed).to.equal(false); + const b = alloc(pool, 800, 600); + expect(b).to.equal(a); + }); + }); + + describe("gc()", () => { + it("destroys all free-list entries", () => { + const a = alloc(pool, 256, 256); + const b = alloc(pool, 512, 512); + pool.freeRenderTarget(a); + pool.freeRenderTarget(b); + + pool.gc(); + expect(a.destroyed).to.equal(true); + expect(b.destroyed).to.equal(true); + }); + }); +}); diff --git a/tests/src/core/ShaderData.test.ts b/tests/src/core/ShaderData.test.ts new file mode 100644 index 0000000000..6903c67150 --- /dev/null +++ b/tests/src/core/ShaderData.test.ts @@ -0,0 +1,184 @@ +import { CloneManager, ShaderData, ShaderDataGroup, ShaderMacro } from "@galacean/engine-core"; +import { CloneMode } from "@galacean/engine-core/src/clone/enums/CloneMode"; +import { describe, expect, it } from "vitest"; + +describe("ShaderData", () => { + describe("Macro operations", () => { + it("enableMacro and disableMacro", () => { + const shaderData = new ShaderData(ShaderDataGroup.Renderer); + const macro = ShaderMacro.getByName("TEST_ENABLE_DISABLE"); + + shaderData.enableMacro("TEST_ENABLE_DISABLE"); + expect(shaderData._macroCollection.isEnable(macro)).to.be.true; + + const macros = shaderData.getMacros() as ShaderMacro[]; + expect(macros).to.have.lengthOf(1); + expect(macros[0]).to.equal(macro); + + shaderData.disableMacro("TEST_ENABLE_DISABLE"); + expect(shaderData._macroCollection.isEnable(macro)).to.be.false; + expect(shaderData.getMacros() as ShaderMacro[]).to.have.lengthOf(0); + }); + + it("enableMacro with value replaces same-name macro", () => { + const shaderData = new ShaderData(ShaderDataGroup.Renderer); + + shaderData.enableMacro("TEST_VALUE_MACRO", "1"); + const macro1 = ShaderMacro.getByName("TEST_VALUE_MACRO", "1"); + expect(shaderData._macroCollection.isEnable(macro1)).to.be.true; + + shaderData.enableMacro("TEST_VALUE_MACRO", "2"); + const macro2 = ShaderMacro.getByName("TEST_VALUE_MACRO", "2"); + expect(shaderData._macroCollection.isEnable(macro1)).to.be.false; + expect(shaderData._macroCollection.isEnable(macro2)).to.be.true; + expect(shaderData.getMacros() as ShaderMacro[]).to.have.lengthOf(1); + }); + }); + + describe("cloneTo", () => { + it("should produce identical macros in target", () => { + const source = new ShaderData(ShaderDataGroup.Renderer); + source.enableMacro("CLONE_MACRO_A"); + source.enableMacro("CLONE_MACRO_B"); + + const target = new ShaderData(ShaderDataGroup.Renderer); + source.cloneTo(target); + + const macroA = ShaderMacro.getByName("CLONE_MACRO_A"); + const macroB = ShaderMacro.getByName("CLONE_MACRO_B"); + expect(target._macroCollection.isEnable(macroA)).to.be.true; + expect(target._macroCollection.isEnable(macroB)).to.be.true; + + const targetMacros = target.getMacros() as ShaderMacro[]; + expect(targetMacros).to.have.lengthOf(2); + }); + + it("should clear stale macros in target before cloning", () => { + const source = new ShaderData(ShaderDataGroup.Renderer); + source.enableMacro("SOURCE_ONLY_MACRO"); + + const target = new ShaderData(ShaderDataGroup.Renderer); + target.enableMacro("TARGET_STALE_MACRO"); + + source.cloneTo(target); + + const staleMacro = ShaderMacro.getByName("TARGET_STALE_MACRO"); + const sourceMacro = ShaderMacro.getByName("SOURCE_ONLY_MACRO"); + + expect(target._macroCollection.isEnable(staleMacro)).to.be.false; + const targetMacros = target.getMacros() as ShaderMacro[]; + expect(targetMacros).to.have.lengthOf(1); + expect(targetMacros[0]).to.equal(sourceMacro); + + const macroMap = (target as any)._macroMap; + for (const key in macroMap) { + expect(Number(key)).to.equal(macroMap[key]._nameId); + } + }); + + it("should not have duplicate macros with same name under different keys", () => { + const target = new ShaderData(ShaderDataGroup.Renderer); + target.enableMacro("MACRO_X"); + target.enableMacro("MACRO_Y"); + target.enableMacro("MACRO_Z"); + + const source = new ShaderData(ShaderDataGroup.Renderer); + source.enableMacro("MACRO_Y"); + + source.cloneTo(target); + + const targetMacros = target.getMacros() as ShaderMacro[]; + expect(targetMacros).to.have.lengthOf(1); + expect(targetMacros[0].name).to.equal("MACRO_Y"); + + const names = targetMacros.map((m) => m.name); + const uniqueNames = [...new Set(names)]; + expect(names.length).to.equal(uniqueNames.length); + }); + + it("clone() should produce a clean independent copy", () => { + const source = new ShaderData(ShaderDataGroup.Renderer); + source.enableMacro("CLONE_INDEPENDENT_A"); + source.enableMacro("CLONE_INDEPENDENT_B"); + + const cloned = source.clone(); + + const macroA = ShaderMacro.getByName("CLONE_INDEPENDENT_A"); + const macroB = ShaderMacro.getByName("CLONE_INDEPENDENT_B"); + expect(cloned._macroCollection.isEnable(macroA)).to.be.true; + expect(cloned._macroCollection.isEnable(macroB)).to.be.true; + + source.disableMacro("CLONE_INDEPENDENT_A"); + expect(cloned._macroCollection.isEnable(macroA)).to.be.true; + }); + }); + + describe("CloneManager", () => { + it("default cloneMode deep-clones same-constructor objects", () => { + const sourceObj = { name: "B", id: 2 }; + const targetObj = { name: "A", id: 1 }; + const source = { _field: sourceObj }; + const target = { _field: targetObj }; + + CloneManager.cloneProperty(source, target, "_field", undefined, null, null, new Map()); + + expect(target._field).to.equal(targetObj); + expect(targetObj.name).to.equal("B"); + expect(targetObj.id).to.equal(2); + }); + + it("CloneMode.Assignment prevents singleton corruption", () => { + const macroA = ShaderMacro.getByName("SINGLETON_FIX_A"); + const macroB = ShaderMacro.getByName("SINGLETON_FIX_B"); + + const source = { _macro: macroB }; + const target = { _macro: macroA }; + + CloneManager.cloneProperty(source, target, "_macro", CloneMode.Assignment, null, null, new Map()); + + expect(macroA.name).to.equal("SINGLETON_FIX_A"); + expect(macroA._nameId).to.not.equal(macroB._nameId); + expect(target._macro).to.equal(macroB); + }); + + it("should not infinite loop on circular references", () => { + // Simulate AnimatorState ↔ AnimatorStateTransition cycle: + // State has transitions array, each Transition has destinationState pointing back to a State + class FakeState { + transitions: FakeTransition[] = []; + } + class FakeTransition { + destinationState: FakeState = null; + } + + // Build circular graph: stateA → transitionAB → stateB → transitionBA → stateA + const srcStateA = new FakeState(); + const srcStateB = new FakeState(); + const srcTransAB = new FakeTransition(); + const srcTransBA = new FakeTransition(); + srcTransAB.destinationState = srcStateB; + srcTransBA.destinationState = srcStateA; + srcStateA.transitions = [srcTransAB]; + srcStateB.transitions = [srcTransBA]; + + // Target has its own independent state graph + const tgtStateA = new FakeState(); + const tgtStateB = new FakeState(); + const tgtTransAB = new FakeTransition(); + const tgtTransBA = new FakeTransition(); + tgtTransAB.destinationState = tgtStateB; + tgtTransBA.destinationState = tgtStateA; + tgtStateA.transitions = [tgtTransAB]; + tgtStateB.transitions = [tgtTransBA]; + + const source = { state: srcStateA }; + const target = { state: tgtStateA }; + + // This must not hang — should complete within milliseconds + CloneManager.cloneProperty(source, target, "state", CloneMode.Deep, null, null, new Map()); + + // After clone, target's state graph should reflect source's data + expect(target.state.transitions).to.have.lengthOf(1); + }); + }); +}); diff --git a/tests/src/core/Transform.test.ts b/tests/src/core/Transform.test.ts index 58ffe3f4a8..bdc8dbc884 100644 --- a/tests/src/core/Transform.test.ts +++ b/tests/src/core/Transform.test.ts @@ -1,4 +1,4 @@ -import { deepClone, Entity, Scene, Transform } from "@galacean/engine-core"; +import { deepClone, Entity, Scene, Transform, TransformModifyFlags } from "@galacean/engine-core"; import { Vector2, Vector3 } from "@galacean/engine-math"; import { WebGLEngine } from "@galacean/engine-rhi-webgl"; import { beforeAll, describe, expect, it } from "vitest"; @@ -74,6 +74,174 @@ describe("Transform test", function () { expect(parent.transform.instanceId).eq(child.transform._getParentTransform()?.instanceId); }); + it("Reparent propagates world matrix dirty to deep descendants after clone", () => { + // Build source hierarchy: source -> middle -> inner (with local offset) + const source = new Entity(engine, "source"); + const srcMiddle = source.createChild("middle"); + const srcInner = srcMiddle.createChild("inner"); + srcInner.transform.setPosition(10, 20, 30); + + // Clone (equivalent to PrefabResource.instantiate) + const clone = source.clone(); + const cloneMiddle = clone.findByName("middle")!; + const cloneInner = cloneMiddle.findByName("inner")!; + + // Access cloneInner.worldMatrix before adding clone to a positioned parent. + const worldBeforeReparent = cloneInner.transform.worldMatrix; + expect(worldBeforeReparent.elements[12]).to.equal(10); + expect(worldBeforeReparent.elements[13]).to.equal(20); + expect(worldBeforeReparent.elements[14]).to.equal(30); + + // Reparent the clone under a positioned root — same as `table.addChild(levelNode)`. + const root = scene.createRootEntity("reparent-root"); + root.transform.setPosition(1000, 2000, 3000); + root.addChild(clone); + + // cloneInner.worldMatrix must reflect root's offset (deep descendant of moved subtree). + const worldAfterReparent = cloneInner.transform.worldMatrix; + expect(worldAfterReparent.elements[12]).to.equal(1010); + expect(worldAfterReparent.elements[13]).to.equal(2020); + expect(worldAfterReparent.elements[14]).to.equal(3030); + }); + + it("Reparent invalidates descendant world caches even when parent has all world flags set (engine dirty-flag bug)", () => { + // Reproduces a Galacean 2.0-alpha.24 engine bug observed in Screw game: + // Transform._parentChange() calls _updateAllWorldFlag which early-exits + // if `this` already has all target world dirty flags set. This skips + // propagation to descendants. After reparent, a descendant whose + // WorldMatrix flag was previously cleared keeps returning stale cache. + const parent = new Entity(engine, "parent"); + const child = parent.createChild("child"); + child.transform.setPosition(10, 20, 30); + + // 1) Access child.worldMatrix to CLEAR child's WorldMatrix dirty flag. + // The access also clears parent's WorldMatrix flag (chain compute). + const cached = child.transform.worldMatrix; + expect(cached.elements[12]).to.equal(10); + + // 2) Force parent into the failure state: + // "all world dirty flags set" (as if never accessed) — simulates the + // post-clone / lifecycle state where PARENT's world hasn't been read + // but a DESCENDANT's world was. + // @ts-ignore - white-box access for precise engine bug reproduction + parent.transform._dirtyFlag |= TransformModifyFlags.WmWpWeWqWsWus; + + // Sanity: child's WorldMatrix is CLEAR, parent has ALL world flags SET. + // @ts-ignore + expect(child.transform._dirtyFlag & TransformModifyFlags.WorldMatrix).to.equal(0); + // @ts-ignore + expect(parent.transform._dirtyFlag & TransformModifyFlags.WmWpWeWqWsWus).to.equal( + TransformModifyFlags.WmWpWeWqWsWus + ); + + // 3) Reparent `parent` under a positioned root (triggers _parentChange on parent). + const root = scene.createRootEntity("reparent-root"); + root.transform.setPosition(1000, 2000, 3000); + root.addChild(parent); + + // 4) Child's worldMatrix MUST now reflect root's offset. + // Under the bug: early-exit in _updateAllWorldFlag skips propagation → child's + // WorldMatrix flag stays CLEAR → getter returns stale cached (10, 20, 30). + const afterReparent = child.transform.worldMatrix; + expect(afterReparent.elements[12]).to.equal(1010); + expect(afterReparent.elements[13]).to.equal(2020); + expect(afterReparent.elements[14]).to.equal(3030); + }); + + it("Reparent re-resolves descendant parent cache even when cached as null", () => { + // Reproduces the second half of the Galacean 2.0-alpha.24 bug: a descendant + // whose `_parentTransformCache` was resolved to `null` (because + // `_getParentTransform` was called while its ancestor chain was partially + // constructed) keeps returning identity worldMatrix even after the + // ancestor chain is fully wired up. + const parent = new Entity(engine, "parent"); + const child = parent.createChild("child"); + child.transform.setPosition(10, 20, 30); + + // Force child's parent cache to null with `_isParentDirty = false` — + // simulates the state observed in Screw where layer-001 had + // `_parentTransformCache = null, _isParentDirty = false` after clone. + // @ts-ignore + child.transform._parentTransformCache = null; + // @ts-ignore + child.transform._isParentDirty = false; + + // Add parent under a positioned root. If _parentChange on parent fails to + // invalidate child's parent cache, child.worldMatrix returns identity. + const root = scene.createRootEntity("cache-null-root"); + root.transform.setPosition(500, 600, 700); + root.addChild(parent); + + const after = child.transform.worldMatrix; + expect(after.elements[12]).to.equal(510); // 500 + 10 + expect(after.elements[13]).to.equal(620); // 600 + 20 + expect(after.elements[14]).to.equal(730); // 700 + 30 + }); + + it("_cloneTo invalidates world cache cleared by other components' ctor reads", () => { + // Reproduces the Transform._cloneTo dirty-flag bug surfaced by the + // billiard-aim-line fix (see notes/3D台球游戏联机版/2026-05-15-billiard-aim-line-kinematic-pair.md). + // + // The cloneComponent sequence calls Component._cloneTo for each component + // in order. Components added BEFORE Transform on a cloned entity may read + // `entity.transform.worldPosition` in their ctor (DynamicCollider does this + // to seed the native PxRigidDynamic pose) — that getter clears WorldPosition + // & WorldMatrix dirty flags as a side effect of caching the computed value. + // + // When Transform._cloneTo later writes new local values, those world-derived + // caches are stale (they still hold the pre-clone defaults). The fix re-dirties + // & dispatches the world flag set so subsequent reads recompute correctly. + const source = new Entity(engine, "source"); + source.transform.setPosition(1, 2, 3); + + // Target entity in pre-_cloneTo state: default transform, but a "Component ctor" + // already queried worldPosition (simulates DynamicCollider native ctor side effect). + const target = new Entity(engine, "target"); + const beforeClone = target.transform.worldPosition; + expect(beforeClone.x).to.equal(0); + // White-box: WorldPosition dirty flag was cleared by the getter call above. + // @ts-ignore + expect(target.transform._dirtyFlag & TransformModifyFlags.WorldPosition).to.equal(0); + + // Exercise _cloneTo (the same call the cloneComponent path makes). + // @ts-ignore - internal API + source.transform._cloneTo(target.transform); + + // After _cloneTo, target.worldPosition must reflect cloned (1,2,3), not stale (0,0,0). + const afterClone = target.transform.worldPosition; + expect(afterClone.x).to.equal(1); + expect(afterClone.y).to.equal(2); + expect(afterClone.z).to.equal(3); + }); + + it("_cloneTo dispatches world-flag change to registered listeners (Collider._updateFlag fires)", () => { + // Reproduces the listener-side of the same bug. Collider._updateFlag is a + // BoolUpdateFlag registered via entity.registerWorldChangeFlag(); Collider._onUpdate + // checks it to decide whether to push transform to the native physics actor. + // + // If Transform._cloneTo only re-dirties local flags but doesn't dispatch the + // world flag set, the listener stays `false` after clone → Collider never + // re-syncs the native actor pose → physics state mismatch (Bug A / Joint clone + // box2 flies at 299 m/s). + const source = new Entity(engine, "source"); + source.transform.setPosition(7, 8, 9); + + const target = new Entity(engine, "target"); + // Simulate Component ctor: register a world-change listener BEFORE _cloneTo. + const updateFlag = target.registerWorldChangeFlag(); + // Then read worldPosition (mirrors DynamicCollider native ctor) — this clears + // both the dirty flag AND the listener's `flag` if it was true. + target.transform.worldPosition; + updateFlag.flag = false; + + // @ts-ignore - internal API + source.transform._cloneTo(target.transform); + + // The listener MUST observe the change; otherwise downstream consumers + // (Collider._onUpdate, animation rigs, etc.) silently miss the clone update. + expect(updateFlag.flag).to.equal(true); + }); + it("Subclasses of Transform", () => { // Create by constructor const entity0 = new Entity(engine, "entity"); diff --git a/tests/src/core/audio/AudioSourcePendingPlayback.test.ts b/tests/src/core/audio/AudioSourcePendingPlayback.test.ts new file mode 100644 index 0000000000..5e7e4645a1 --- /dev/null +++ b/tests/src/core/audio/AudioSourcePendingPlayback.test.ts @@ -0,0 +1,358 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { AudioManager, AudioSource } from "@galacean/engine-core"; + +class MockGainNode { + gain = { + setValueAtTime: vi.fn() + }; + + connect = vi.fn(); +} + +class MockBufferSourceNode { + buffer: unknown = null; + loop = false; + onended: (() => void) | null = null; + playbackRate = { + value: 1 + }; + + connect = vi.fn(); + disconnect = vi.fn(); + start = vi.fn(); + stop = vi.fn(); +} + +class MockAudioContext { + static shouldResumeSucceed = true; + static resumeResultQueue: Array | Error> | null = null; + + currentTime = 0; + destination = {}; + onstatechange: (() => void) | null = null; + state: AudioContextState = "suspended"; + + createBufferSource(): AudioBufferSourceNode { + return new MockBufferSourceNode() as unknown as AudioBufferSourceNode; + } + + createGain(): GainNode { + return new MockGainNode() as unknown as GainNode; + } + + resume(): Promise { + const queuedResult = MockAudioContext.resumeResultQueue?.shift(); + if (queuedResult instanceof Promise) { + return queuedResult; + } + if (queuedResult instanceof Error) { + return Promise.reject(queuedResult); + } + if (!MockAudioContext.shouldResumeSucceed) { + return Promise.reject(new Error("autoplay blocked")); + } + this.state = "running"; + this.onstatechange?.(); + return Promise.resolve(); + } + + suspend(): Promise { + this.state = "suspended"; + this.onstatechange?.(); + return Promise.resolve(); + } +} + +async function flushAsync(): Promise { + await Promise.resolve(); + await Promise.resolve(); +} + +function createAudioSource(): AudioSource { + const audioSource = new AudioSource({ + _isActiveInHierarchy: true, + _isActiveInScene: true, + _removeComponent() {}, + engine: {} + } as any); + + audioSource.clip = { + _addReferCount() {}, + _getAudioSource() { + return {}; + } + } as any; + + return audioSource; +} + +describe("AudioSource pending playback", () => { + beforeEach(() => { + (window as any).AudioContext = MockAudioContext; + (AudioManager as any)._context = null; + (AudioManager as any)._gainNode = null; + (AudioManager as any)._needsUserGestureResume = false; + (AudioManager as any)._pendingSources = new Set(); + (AudioManager as any)._playingSources = new Set(); + (AudioManager as any)._interruptedSources = new Set(); + (AudioManager as any)._foregroundRestoreTimer = undefined; + (AudioManager as any)._hidden = false; + MockAudioContext.shouldResumeSucceed = true; + MockAudioContext.resumeResultQueue = null; + AudioManager._playingCount = 0; + }); + + afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); + document.replaceChildren(); + }); + + it("replays pending playback on the next user gesture after autoplay blocking", async () => { + const audioSource = createAudioSource(); + + vi.spyOn(console, "warn").mockImplementation(() => {}); + MockAudioContext.shouldResumeSucceed = false; + + audioSource.play(); + await flushAsync(); + + expect((audioSource as any)._pendingPlay).to.be.true; + expect((AudioManager as any)._pendingSources.size).to.equal(1); + expect((AudioManager as any)._needsUserGestureResume).to.be.false; + expect(audioSource.isPlaying).to.be.false; + + MockAudioContext.shouldResumeSucceed = true; + document.dispatchEvent(new Event("click")); + await flushAsync(); + + expect(audioSource.isPlaying).to.be.true; + expect((audioSource as any)._pendingPlay).to.be.false; + expect((AudioManager as any)._pendingSources.size).to.equal(0); + expect((AudioManager as any)._needsUserGestureResume).to.be.false; + }); + + it("cancels pending playback before the unlocking gesture arrives", async () => { + const audioSource = createAudioSource(); + + vi.spyOn(console, "warn").mockImplementation(() => {}); + MockAudioContext.shouldResumeSucceed = false; + + audioSource.play(); + await flushAsync(); + + audioSource.stop(); + expect((audioSource as any)._pendingPlay).to.be.false; + expect((AudioManager as any)._pendingSources.size).to.equal(0); + + MockAudioContext.shouldResumeSucceed = true; + document.dispatchEvent(new Event("click")); + await flushAsync(); + + expect(audioSource.isPlaying).to.be.false; + expect((audioSource as any)._pendingPlay).to.be.false; + }); + + it("keeps resume a no-op until a context already exists", async () => { + expect((AudioManager as any)._context).to.be.null; + + await AudioManager.resume(); + + expect((AudioManager as any)._context).to.be.null; + }); + + it("does not resume foreground audio before a hide event", async () => { + createAudioSource(); + const context = (AudioManager as any)._context as MockAudioContext; + + vi.spyOn(document, "hidden", "get").mockReturnValue(false); + const resumeSpy = vi.spyOn(context, "resume"); + const suspendSpy = vi.spyOn(AudioManager, "suspend"); + + context.state = "suspended"; + AudioManager._playingCount = 1; + + document.dispatchEvent(new Event("visibilitychange")); + await flushAsync(); + + expect(resumeSpy).not.toHaveBeenCalled(); + expect(suspendSpy).not.toHaveBeenCalled(); + expect((AudioManager as any)._needsUserGestureResume).to.be.false; + }); + + it("recreates interrupted source nodes from a foreground gesture", async () => { + const audioSource = createAudioSource(); + const context = (AudioManager as any)._context as MockAudioContext; + + context.state = "running"; + audioSource.play(); + + const firstSourceNode = (audioSource as any)._sourceNode as MockBufferSourceNode; + expect(audioSource.isPlaying).to.be.true; + expect(AudioManager._playingCount).to.equal(1); + + const hiddenSpy = vi.spyOn(document, "hidden", "get").mockReturnValue(true); + document.dispatchEvent(new Event("visibilitychange")); + await flushAsync(); + + expect(firstSourceNode.stop).toHaveBeenCalledTimes(1); + expect(audioSource.isPlaying).to.be.false; + expect(AudioManager._playingCount).to.equal(0); + expect((AudioManager as any)._interruptedSources.size).to.equal(1); + + hiddenSpy.mockReturnValue(false); + document.dispatchEvent(new Event("visibilitychange")); + await flushAsync(); + + expect(audioSource.isPlaying).to.be.false; + expect((AudioManager as any)._interruptedSources.size).to.equal(1); + expect((AudioManager as any)._needsUserGestureResume).to.be.true; + + document.dispatchEvent(new Event("touchend")); + await flushAsync(); + + expect(audioSource.isPlaying).to.be.true; + expect(AudioManager._playingCount).to.equal(1); + expect((AudioManager as any)._interruptedSources.size).to.equal(0); + expect((audioSource as any)._sourceNode).not.to.equal(firstSourceNode); + }); + + it("recovers interrupted source nodes from foreground retry after the restore delay", async () => { + vi.useFakeTimers(); + const audioSource = createAudioSource(); + const context = (AudioManager as any)._context as MockAudioContext; + + context.state = "running"; + audioSource.play(); + + const hiddenSpy = vi.spyOn(document, "hidden", "get").mockReturnValue(true); + document.dispatchEvent(new Event("visibilitychange")); + await flushAsync(); + + hiddenSpy.mockReturnValue(false); + document.dispatchEvent(new Event("visibilitychange")); + await flushAsync(); + + expect(audioSource.isPlaying).to.be.false; + + await vi.advanceTimersByTimeAsync(299); + await flushAsync(); + + expect(audioSource.isPlaying).to.be.false; + + await vi.advanceTimersByTimeAsync(1); + await flushAsync(); + + expect(audioSource.isPlaying).to.be.true; + expect((AudioManager as any)._interruptedSources.size).to.equal(0); + }); + + it("handles document pagehide/pageshow and mouseup recovery", async () => { + const audioSource = createAudioSource(); + const context = (AudioManager as any)._context as MockAudioContext; + + context.state = "running"; + audioSource.play(); + + document.dispatchEvent(new Event("pagehide")); + await flushAsync(); + + expect(audioSource.isPlaying).to.be.false; + expect((AudioManager as any)._interruptedSources.size).to.equal(1); + + document.dispatchEvent(new Event("pageshow")); + await flushAsync(); + + expect(audioSource.isPlaying).to.be.false; + expect((AudioManager as any)._needsUserGestureResume).to.be.true; + + document.dispatchEvent(new Event("mouseup")); + await flushAsync(); + + expect(audioSource.isPlaying).to.be.true; + expect((AudioManager as any)._interruptedSources.size).to.equal(0); + }); + + it("keeps gesture recovery when foreground resume fails", async () => { + vi.useFakeTimers(); + const audioSource = createAudioSource(); + const context = (AudioManager as any)._context as MockAudioContext; + + vi.spyOn(console, "warn").mockImplementation(() => {}); + const hiddenSpy = vi.spyOn(document, "hidden", "get").mockReturnValue(true); + const resumeSpy = vi.spyOn(context, "resume"); + const suspendSpy = vi.spyOn(AudioManager, "suspend"); + + context.state = "running"; + audioSource.play(); + + document.dispatchEvent(new Event("visibilitychange")); + await flushAsync(); + + MockAudioContext.shouldResumeSucceed = false; + hiddenSpy.mockReturnValue(false); + document.dispatchEvent(new Event("visibilitychange")); + await flushAsync(); + + expect(resumeSpy).not.toHaveBeenCalled(); + expect(suspendSpy).toHaveBeenCalledTimes(2); + expect((AudioManager as any)._needsUserGestureResume).to.be.true; + + await vi.advanceTimersByTimeAsync(299); + await flushAsync(); + + expect(resumeSpy).not.toHaveBeenCalled(); + expect(suspendSpy).toHaveBeenCalledTimes(2); + expect((AudioManager as any)._needsUserGestureResume).to.be.true; + + await vi.advanceTimersByTimeAsync(1); + await flushAsync(); + + expect(resumeSpy).toHaveBeenCalledTimes(1); + expect(suspendSpy).toHaveBeenCalledTimes(3); + expect((AudioManager as any)._needsUserGestureResume).to.be.true; + + document.dispatchEvent(new Event("click")); + await flushAsync(); + + expect(resumeSpy).toHaveBeenCalledTimes(2); + expect((AudioManager as any)._needsUserGestureResume).to.be.true; + + MockAudioContext.shouldResumeSucceed = true; + document.dispatchEvent(new Event("click")); + await flushAsync(); + + expect(resumeSpy).toHaveBeenCalledTimes(3); + expect(context.state).to.equal("running"); + expect((AudioManager as any)._needsUserGestureResume).to.be.false; + }); + + it("retries context.resume inside a later user gesture even if an earlier resume is still pending", async () => { + createAudioSource(); + const context = (AudioManager as any)._context as MockAudioContext; + const firstResume = new Promise(() => {}); + + MockAudioContext.resumeResultQueue = [firstResume]; + const resumeSpy = vi.spyOn(context, "resume"); + + AudioManager.resume().catch(() => {}); + await flushAsync(); + + expect(resumeSpy).toHaveBeenCalledTimes(1); + + MockAudioContext.resumeResultQueue = [ + Promise.resolve().then(() => { + context.state = "running"; + context.onstatechange?.(); + }) + ]; + (AudioManager as any)._needsUserGestureResume = true; + + document.dispatchEvent(new Event("click")); + await flushAsync(); + + expect(resumeSpy).toHaveBeenCalledTimes(2); + expect(context.state).to.equal("running"); + expect((AudioManager as any)._needsUserGestureResume).to.be.false; + }); +}); diff --git a/tests/src/core/particle/ParticleStopResume.test.ts b/tests/src/core/particle/ParticleStopResume.test.ts new file mode 100644 index 0000000000..0e0db6ac05 --- /dev/null +++ b/tests/src/core/particle/ParticleStopResume.test.ts @@ -0,0 +1,142 @@ +import { + Burst, + Camera, + ParticleCompositeCurve, + ParticleRenderer, + ParticleStopMode, + Scene +} from "@galacean/engine-core"; +import { WebGLEngine } from "@galacean/engine-rhi-webgl"; +import { beforeAll, describe, expect, it } from "vitest"; + +describe("ParticleGenerator stop/resume timeline", () => { + let engine: WebGLEngine; + let scene: Scene; + + beforeAll(async () => { + engine = await WebGLEngine.create({ canvas: document.createElement("canvas") }); + scene = engine.sceneManager.activeScene; + const root = scene.createRootEntity("root"); + const camera = root.createChild("Camera"); + camera.addComponent(Camera); + camera.transform.setPosition(0, 0, 10); + }); + + /** + * Drive `generator._update(dt)` directly so we control the timeline. + * `ParticleRenderer._update` would call this with `engine.time.deltaTime`, + * which is wall-clock and not reproducible. + */ + function tick(generator: any, frames: number, dt: number): void { + for (let i = 0; i < frames; i++) { + generator._update(dt); + } + } + + it("rate-over-time: stop -> idle -> play emits a catch-up batch in one frame", () => { + const entity = scene.createRootEntity("rate"); + const renderer = entity.addComponent(ParticleRenderer); + const generator = renderer.generator; + generator.useAutoRandomSeed = false; + generator.main.duration = 1; + generator.main.startLifetime.constant = 1; + generator.main.maxParticles = 10000; + generator.emission.rateOverTime.constant = 100; + + let totalEmitted = 0; + const origEmit = (generator as any)._emit.bind(generator); + (generator as any)._emit = (playTime: number, count: number) => { + totalEmitted += count; + origEmit(playTime, count); + }; + + generator.play(); + // Run 0.5s at 60fps -> ~50 particles + tick(generator, 30, 1 / 60); + const emittedDuringPlay = totalEmitted; + const playTimeAfterPlay = generator._playTime; + + generator.stop(true, ParticleStopMode.StopEmitting); + const playTimeAtStop = generator._playTime; + const emittedAtStop = totalEmitted; + + // Idle 4.5s while stopped -> _emit must not run, but _playTime drifts + tick(generator, 270, 1 / 60); + const playTimeAfterIdle = generator._playTime; + const emittedDuringIdle = totalEmitted - emittedAtStop; + + generator.play(); + // Single frame after resume + tick(generator, 1, 1 / 60); + const emittedFirstFrameAfterResume = totalEmitted - emittedAtStop; + + entity.destroy(); + + // eslint-disable-next-line no-console + console.log("[bug-repro/rate]", { + emittedDuringPlay, + playTimeAfterPlay, + playTimeAtStop, + playTimeAfterIdle, + emittedDuringIdle, + emittedFirstFrameAfterResume + }); + + expect(emittedDuringIdle).toBe(0); + // Buggy behavior: emits a large catch-up batch (~ idleSeconds * rate). + expect(emittedFirstFrameAfterResume).toBeGreaterThan(100); + expect(playTimeAfterIdle - playTimeAtStop).toBeGreaterThan(4); + }); + + it("burst: stop -> idle -> play replays multiple cycles of bursts", () => { + const entity = scene.createRootEntity("burst"); + const renderer = entity.addComponent(ParticleRenderer); + const generator = renderer.generator; + generator.useAutoRandomSeed = false; + generator.main.duration = 1; + generator.main.isLoop = true; + generator.main.startLifetime.constant = 1; + generator.main.maxParticles = 10000; + generator.emission.rateOverTime.constant = 0; + generator.emission.addBurst(new Burst(0, new ParticleCompositeCurve(10))); + + let totalEmitted = 0; + const origEmit = (generator as any)._emit.bind(generator); + (generator as any)._emit = (playTime: number, count: number) => { + totalEmitted += count; + origEmit(playTime, count); + }; + + generator.play(); + // 1 full cycle -> burst at t=0 fires once (10 particles at frame 0) + tick(generator, 60, 1 / 60); + const emittedAfterOneSecond = totalEmitted; + + generator.stop(true, ParticleStopMode.StopEmitting); + const playTimeAtStop = generator._playTime; + const emittedAtStop = totalEmitted; + + // Idle 4 cycles + tick(generator, 240, 1 / 60); + const playTimeAfterIdle = generator._playTime; + + generator.play(); + tick(generator, 1, 1 / 60); // first frame after resume + const emittedFirstFrameAfterResume = totalEmitted - emittedAtStop; + + entity.destroy(); + + // eslint-disable-next-line no-console + console.log("[bug-repro/burst]", { + emittedAfterOneSecond, + playTimeAtStop, + playTimeAfterIdle, + emittedFirstFrameAfterResume + }); + + // After fix this should be 0 (next burst at t=0 of the new cycle hasn't reached yet). + // Buggy behavior: replays the burst once because `_currentBurstIndex` is 0 and + // `_emitBySubBurst(lastPlayTime, playTime, ...)` sees burst.time === startTime. + expect(emittedFirstFrameAfterResume).toBe(10); + }); +}); diff --git a/tests/src/core/particle/RateOverDistance.test.ts b/tests/src/core/particle/RateOverDistance.test.ts new file mode 100644 index 0000000000..483892f109 --- /dev/null +++ b/tests/src/core/particle/RateOverDistance.test.ts @@ -0,0 +1,326 @@ +import { + Camera, + Engine, + Entity, + ParticleMaterial, + ParticleRenderer, + ParticleSimulationSpace, + ParticleStopMode +} from "@galacean/engine-core"; +import { Color, Vector3 } from "@galacean/engine-math"; +import { WebGLEngine } from "@galacean/engine"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +function tick(engine: Engine, times: { value: number }, deltaMs: number = 100): void { + //@ts-ignore + engine._vSyncCount = Infinity; + //@ts-ignore + engine._time._lastSystemTime = 0; + performance.now = function () { + times.value += deltaMs; + return times.value; + }; + engine.update(); +} + +function buildEmitter(engine: Engine, name: string): { entity: Entity; renderer: ParticleRenderer } { + const scene = engine.sceneManager.activeScene; + const entity = scene.createRootEntity(name); + const renderer = entity.addComponent(ParticleRenderer); + const material = new ParticleMaterial(engine); + material.baseColor = new Color(1, 1, 1, 1); + renderer.setMaterial(material); + + const generator = renderer.generator; + generator.useAutoRandomSeed = false; + generator.main.duration = 5; + generator.main.isLoop = false; + generator.main.maxParticles = 1000; + generator.main.startLifetime.constant = 10; + // Zero out the time-based path so only rateOverDistance contributes. + generator.emission.rateOverTime.constant = 0; + return { entity, renderer }; +} + +describe("EmissionModule rateOverDistance", () => { + let engine: Engine; + let elapsed: { value: number }; + + beforeAll(async function () { + engine = await WebGLEngine.create({ + canvas: document.createElement("canvas") + }); + + const scene = engine.sceneManager.activeScene; + const cameraEntity = scene.createRootEntity("Camera"); + cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(0, 0, 10); + + engine.run(); + elapsed = { value: 0 }; + }); + + afterAll(function () { + engine.destroy(); + }); + + it("defaults to 0 (no emission triggered by movement)", () => { + const { entity, renderer } = buildEmitter(engine, "default-zero"); + const generator = renderer.generator; + expect(generator.emission.rateOverDistance.evaluate(undefined, undefined)).to.eq(0); + + generator.stop(true, ParticleStopMode.StopEmittingAndClear); + generator.play(); + + tick(engine, elapsed); + entity.transform.setPosition(10, 0, 0); + tick(engine, elapsed); + + expect(generator._getAliveParticleCount()).to.eq(0); + entity.destroy(); + }); + + it("emits ratePerUnit × distance particles", () => { + const { entity, renderer } = buildEmitter(engine, "rate-times-distance"); + const generator = renderer.generator; + generator.emission.rateOverDistance.constant = 10; + + generator.stop(true, ParticleStopMode.StopEmittingAndClear); + generator.play(); + + // First tick syncs the baseline position. + tick(engine, elapsed); + expect(generator._getAliveParticleCount()).to.eq(0); + + // Move 2 units → 10 * 2 = 20 particles. + entity.transform.setPosition(2, 0, 0); + tick(engine, elapsed); + expect(generator._getAliveParticleCount()).to.eq(20); + + entity.destroy(); + }); + + it("accumulates sub-interval movement across frames", () => { + const { entity, renderer } = buildEmitter(engine, "subinterval-carry"); + const generator = renderer.generator; + // 1 particle per 0.5 units → 2 particles per unit. + generator.emission.rateOverDistance.constant = 2; + + generator.stop(true, ParticleStopMode.StopEmittingAndClear); + generator.play(); + tick(engine, elapsed); // baseline sync + + // 3 moves of 0.3 units each = 0.9 total. With interval 0.5, that's + // exactly 1 emission (at 0.5) plus 0.4 carried forward. + entity.transform.setPosition(0.3, 0, 0); + tick(engine, elapsed); + entity.transform.setPosition(0.6, 0, 0); + tick(engine, elapsed); + entity.transform.setPosition(0.9, 0, 0); + tick(engine, elapsed); + expect(generator._getAliveParticleCount()).to.eq(1); + + // Move another 0.6 units → accumulator hits 1.0, then 0 carried fwd. Emit 2 more. + entity.transform.setPosition(1.5, 0, 0); + tick(engine, elapsed); + expect(generator._getAliveParticleCount()).to.eq(3); + + entity.destroy(); + }); + + it("static emitter never emits via distance", () => { + const { entity, renderer } = buildEmitter(engine, "static-emitter"); + const generator = renderer.generator; + generator.emission.rateOverDistance.constant = 100; + + generator.stop(true, ParticleStopMode.StopEmittingAndClear); + generator.play(); + tick(engine, elapsed); + tick(engine, elapsed); + tick(engine, elapsed); + + expect(generator._getAliveParticleCount()).to.eq(0); + entity.destroy(); + }); + + it("stop+clear resets the distance accumulator", () => { + const { entity, renderer } = buildEmitter(engine, "reset-on-clear"); + const generator = renderer.generator; + generator.emission.rateOverDistance.constant = 10; + + generator.stop(true, ParticleStopMode.StopEmittingAndClear); + generator.play(); + tick(engine, elapsed); + entity.transform.setPosition(0.5, 0, 0); + tick(engine, elapsed); + expect(generator._getAliveParticleCount()).to.eq(5); + + // After clear+play, the next tick must re-sync from the *current* position, + // not diff against the pre-clear baseline — so jumping back to origin + // should not emit anything until the next move. + generator.stop(true, ParticleStopMode.StopEmittingAndClear); + entity.transform.setPosition(0, 0, 0); + generator.play(); + tick(engine, elapsed); + tick(engine, elapsed); + expect(generator._getAliveParticleCount()).to.eq(0); + + entity.destroy(); + }); + + it("distributes particles along the movement path in World space", () => { + const { entity, renderer } = buildEmitter(engine, "world-space-distribution"); + const generator = renderer.generator; + generator.main.simulationSpace = ParticleSimulationSpace.World; + // 1 particle per unit; move 4 units → 4 particles spaced at world x = 1, 2, 3, 4 + generator.emission.rateOverDistance.constant = 1; + + generator.stop(true, ParticleStopMode.StopEmittingAndClear); + generator.play(); + + tick(engine, elapsed); // baseline sync at (0,0,0) + + entity.transform.setPosition(4, 0, 0); + tick(engine, elapsed); + expect(generator._getAliveParticleCount()).to.eq(4); + + // Verify each particle's stored world position is along [0,4] on x axis, not all at x=4. + // Particles are written sequentially starting at firstActiveElement=0. + //@ts-ignore - test reaches into instance buffer to verify spatial distribution + const verts = (generator as any)._instanceVertices as Float32Array; + // Per-instance stride = 168 bytes / 4 = 42 floats; world position lives at offset 27. + const stride = 42; + const xs: number[] = []; + for (let i = 0; i < 4; i++) { + xs.push(verts[i * stride + 27]); + } + xs.sort((a, b) => a - b); + // Expect roughly [1, 2, 3, 4] — accept loose tolerance for float ops. + expect(xs[0]).to.be.closeTo(1, 1e-4); + expect(xs[1]).to.be.closeTo(2, 1e-4); + expect(xs[2]).to.be.closeTo(3, 1e-4); + expect(xs[3]).to.be.closeTo(4, 1e-4); + + entity.destroy(); + }); + + it("distributes emit time along the movement path in World space", () => { + const { entity, renderer } = buildEmitter(engine, "world-space-time-distribution"); + const generator = renderer.generator; + generator.main.simulationSpace = ParticleSimulationSpace.World; + // 1 particle per unit; move 4 units across one 100ms tick → 4 particles, + // each born at a sub-frame offset. + generator.emission.rateOverDistance.constant = 1; + + generator.stop(true, ParticleStopMode.StopEmittingAndClear); + generator.play(); + + tick(engine, elapsed); // baseline sync at (0,0,0) + entity.transform.setPosition(4, 0, 0); + tick(engine, elapsed); + expect(generator._getAliveParticleCount()).to.eq(4); + + //@ts-ignore - reach into instance buffer to read per-particle emit time + const verts = (generator as any)._instanceVertices as Float32Array; + const stride = 42; + // a_DirectionTime is at byte 16 → float 4; the .w slot (emit time) is float 4+3=7. + const timeFloatOffset = 7; + const times: number[] = []; + for (let i = 0; i < 4; i++) { + times.push(verts[i * stride + timeFloatOffset]); + } + times.sort((a, b) => a - b); + + // The 4 emit times must form an arithmetic sequence (constant step = sStep * dt), + // and they must not all be equal — that's exactly the bug we're guarding against, + // where COL / SOL / FOL would otherwise render them as a uniform stamp. + const diff0 = times[1] - times[0]; + const diff1 = times[2] - times[1]; + const diff2 = times[3] - times[2]; + expect(diff0).to.be.greaterThan(1e-4); + expect(diff1).to.be.closeTo(diff0, 1e-4); + expect(diff2).to.be.closeTo(diff0, 1e-4); + + entity.destroy(); + }); + + it("clamps count and discards accumulator on teleport-sized moves", () => { + const { entity, renderer } = buildEmitter(engine, "teleport-clamp"); + const generator = renderer.generator; + generator.main.maxParticles = 50; + // Rate 10/unit × 10000 unit jump would otherwise demand 100,000 emissions + // in one frame — millions of `_addNewParticle` calls hitting the buffer-full + // early return. + generator.emission.rateOverDistance.constant = 10; + + generator.stop(true, ParticleStopMode.StopEmittingAndClear); + generator.play(); + tick(engine, elapsed); // baseline sync at (0,0,0) + + entity.transform.setPosition(10000, 0, 0); // teleport + tick(engine, elapsed); + + // Alive count must not exceed the configured cap. + expect(generator._getAliveParticleCount()).to.be.lessThanOrEqual(50); + + // Next frame without movement: accumulator should have been reset to 0 + // (residue dropped), so no further emission. + const aliveAfterTeleport = generator._getAliveParticleCount(); + tick(engine, elapsed); + expect(generator._getAliveParticleCount()).to.eq(aliveAfterTeleport); + + entity.destroy(); + }); + + it("does not burst on play() after emitter moves while stopped", () => { + const { entity, renderer } = buildEmitter(engine, "no-burst-on-replay"); + const generator = renderer.generator; + generator.emission.rateOverDistance.constant = 10; + + generator.stop(true, ParticleStopMode.StopEmittingAndClear); + generator.play(); + tick(engine, elapsed); // baseline sync at (0,0,0) + + // Stop *without* clear — accumulator/baseline retained by the old impl. + generator.stop(true, ParticleStopMode.StopEmitting); + // Emitter teleports a few units while stopped (kept inside the camera frustum + // so renderer culling doesn't mask the test). + entity.transform.setPosition(3, 0, 0); + generator.play(); + + // First tick after play() must resync the baseline at the new position, + // not diff (3 - 0) and dump 30 particles in one shot. + tick(engine, elapsed); + expect(generator._getAliveParticleCount()).to.eq(0); + + // Subsequent movement still emits normally — diffed against the resynced baseline. + entity.transform.setPosition(4, 0, 0); + tick(engine, elapsed); + expect(generator._getAliveParticleCount()).to.eq(10); + + entity.destroy(); + }); + + it("returns actual emitted count when buffer is full", () => { + // Documents the `_emit` return-value contract that lets distance emission detect + // mid-loop buffer exhaustion (and drop residual accumulator to avoid spinning + // through millions of no-op iterations on a teleport). + const { entity, renderer } = buildEmitter(engine, "emit-returns-actual"); + const generator = renderer.generator; + generator.main.maxParticles = 10; + generator.stop(true, ParticleStopMode.StopEmittingAndClear); + generator.play(); + tick(engine, elapsed); + + // Request 100, buffer only has 10 slots. + //@ts-ignore — reach into the internal _emit contract + const emitted = generator._emit(generator._playTime, 100); + expect(emitted).to.eq(10); + + // Subsequent request returns 0 — buffer is saturated. + //@ts-ignore + expect(generator._emit(generator._playTime, 5)).to.eq(0); + + entity.destroy(); + }); +}); diff --git a/tests/src/core/physics/Collision.test.ts b/tests/src/core/physics/Collision.test.ts index 870bc546bc..6f7e886df3 100644 --- a/tests/src/core/physics/Collision.test.ts +++ b/tests/src/core/physics/Collision.test.ts @@ -1,4 +1,13 @@ -import { BoxColliderShape, DynamicCollider, Entity, Engine, Script, StaticCollider } from "@galacean/engine-core"; +import { + BoxColliderShape, + DynamicCollider, + DynamicColliderConstraints, + Entity, + Engine, + Script, + SphereColliderShape, + StaticCollider +} from "@galacean/engine-core"; import { Vector3 } from "@galacean/engine-math"; import { PhysXPhysics } from "@galacean/engine-physics-physx"; import { WebGLEngine } from "@galacean/engine-rhi-webgl"; @@ -22,6 +31,20 @@ describe("Collision", function () { return boxEntity; } + function addSphere(radius: number, pos: Vector3) { + const sphereEntity = rootEntity.createChild("SphereEntity"); + sphereEntity.transform.setPosition(pos.x, pos.y, pos.z); + + const sphereShape = new SphereColliderShape(); + sphereShape.material.dynamicFriction = 0; + sphereShape.material.staticFriction = 0; + sphereShape.radius = radius; + const sphereCollider = sphereEntity.addComponent(DynamicCollider); + sphereCollider.addShape(sphereShape); + sphereCollider.useGravity = false; + return sphereEntity; + } + function formatValue(value: number) { return Math.round(value * 100000) / 100000; } @@ -164,4 +187,239 @@ describe("Collision", function () { engine.sceneManager.activeScene.physics._update(1); }); }); + + it("reports contact normal from static other shape to dynamic self shape", function () { + engine.sceneManager.activeScene.physics.gravity = new Vector3(0, 0, 0); + const dynamicBox = addBox(new Vector3(1, 1, 1), DynamicCollider, new Vector3(-3, 0, 0)); + const staticBox = addBox(new Vector3(1, 1, 1), StaticCollider, new Vector3(0, 0, 0)); + + return new Promise((done) => { + dynamicBox.addComponent( + class extends Script { + onCollisionEnter(other: Collision): void { + expect(other.shape).toBe(staticBox.getComponent(StaticCollider).shapes[0]); + const contacts = []; + other.getContacts(contacts); + expect(contacts.length).toBeGreaterThan(0); + expect(formatValue(contacts[0].normal.x)).toBe(-1); + + done(); + } + } + ); + + dynamicBox.getComponent(DynamicCollider).applyForce(new Vector3(1000, 0, 0)); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1); + }); + }); + + it("reports billiard hitBall sphere normal from kinematic other to dynamic target self", function () { + engine.sceneManager.activeScene.physics.gravity = new Vector3(0, 0, 0); + const targetBall = addSphere(0.5, new Vector3(0, 0, 0)); + const hitBall = addSphere(0.5, new Vector3(-3, 0, 0)); + const hitCollider = hitBall.getComponent(DynamicCollider); + hitCollider.isKinematic = true; + + return new Promise((done) => { + targetBall.addComponent( + class extends Script { + onCollisionEnter(other: Collision): void { + expect(other.shape).toBe(hitCollider.shapes[0]); + const contacts = []; + other.getContacts(contacts); + expect(contacts.length).toBeGreaterThan(0); + + const contactNormal = contacts[0].normal; + expect(formatValue(contactNormal.x)).toBe(1); + expect(formatValue(contactNormal.y)).toBe(0); + expect(formatValue(contactNormal.z)).toBe(0); + + const hitToTarget = new Vector3(); + Vector3.subtract(targetBall.transform.worldPosition, hitBall.transform.worldPosition, hitToTarget); + hitToTarget.normalize(); + expect(formatValue(Vector3.dot(contactNormal, hitToTarget))).toBe(1); + + const scaledNormal = new Vector3(); + Vector3.scale(contactNormal, 2 * Vector3.dot(hitToTarget, contactNormal), scaledNormal); + const reflected = new Vector3(); + Vector3.subtract(hitToTarget, scaledNormal, reflected); + reflected.normalize(); + expect(formatValue(reflected.x)).toBe(-1); + + done(); + } + } + ); + + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + hitCollider.move(new Vector3(-0.9, 0, 0)); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + }); + }); + + // ────────────────────────────────────────────────────────────────────────────── + // Kinematic-pair collision callback (3D billiard aim-line use case). + // PhysX 4.x defaults suppress kineKine + staticKine pairs. SceneBinding already + // sets kineKineFilteringMode = eKEEP and staticKineFilteringMode = eKEEP, and + // filter shader returns eNOTIFY_TOUCH_FOUND/PERSISTS/LOST for all pairs. + // Question: does the kinematic pair actually fire onCollisionEnter when a + // kinematic actor is moved into another actor's volume via setWorldTransform + // (i.e. setGlobalPose teleport, NOT setKinematicTarget)? + // + // Cocos parity expectation: yes — Cocos PhysX backend fires onCollisionEnter + // for kinematic↔dynamic and kinematic↔kinematic pairs on overlap. + // ────────────────────────────────────────────────────────────────────────────── + + function probeKinematicCallback(opts: { + aKine: boolean; + bKine: boolean; + timeoutMs?: number; + }): Promise<{ fired: boolean }> { + return new Promise((resolve) => { + engine.sceneManager.activeScene.physics.gravity = new Vector3(0, 0, 0); + const boxA = addBox(new Vector3(1, 1, 1), DynamicCollider, new Vector3(-3, 0, 0)); + const boxB = addBox(new Vector3(1, 1, 1), DynamicCollider, new Vector3(3, 0, 0)); + const colA = boxA.getComponent(DynamicCollider); + const colB = boxB.getComponent(DynamicCollider); + colA.useGravity = false; + colB.useGravity = false; + colA.isKinematic = opts.aKine; + colB.isKinematic = opts.bKine; + + let fired = false; + boxA.addComponent( + class extends Script { + onCollisionEnter(_other: Collision): void { + fired = true; + resolve({ fired: true }); + } + } + ); + + // Step a few frames to let PhysX settle initial state. + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + // Teleport B onto A → expect onCollisionEnter. + boxB.transform.setPosition(-3, 0, 0); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + + if (!fired) resolve({ fired: false }); + }); + } + + // Probes that the standard transform→PhysX sync path routes correctly for + // kinematic actors. With the fix in PhysXDynamicCollider.setWorldTransform, + // moving a kinematic actor via transform.setPosition() goes through + // setKinematicTarget(), which lets PhysX detect contact and fire the callback. + it("kinematic-kinematic overlap via transform.setPosition fires onCollisionEnter", async function () { + const r = await probeKinematicCallback({ aKine: true, bKine: true }); + expect(r.fired).toBe(true); + }); + + it("kinematic-dynamic overlap via transform.setPosition fires onCollisionEnter", async function () { + const r = await probeKinematicCallback({ aKine: true, bKine: false }); + expect(r.fired).toBe(true); + }); + + it("dynamic-dynamic overlap via transform.setPosition fires onCollisionEnter", async function () { + const r = await probeKinematicCallback({ aKine: false, bKine: false }); + expect(r.fired).toBe(true); + }); + + // Probe whether "dynamic actor + freeze 6 constraints + teleport via setGlobalPose" + // can substitute for a kinematic actor and still trigger contact callbacks. + // This is the proposed fix path for the 3D billiard hitBall: ditch kinematic, + // use a fully-frozen dynamic actor that is moved via setWorldPosition. + it("HYPOTHESIS: kine-kine fires onCollisionEnter when moved via setKinematicTarget (not setGlobalPose)", function () { + return new Promise((resolve, reject) => { + engine.sceneManager.activeScene.physics.gravity = new Vector3(0, 0, 0); + const boxA = addBox(new Vector3(1, 1, 1), DynamicCollider, new Vector3(-3, 0, 0)); + const boxB = addBox(new Vector3(1, 1, 1), DynamicCollider, new Vector3(3, 0, 0)); + const colA = boxA.getComponent(DynamicCollider); + const colB = boxB.getComponent(DynamicCollider); + colA.useGravity = false; + colB.useGravity = false; + colA.isKinematic = true; + colB.isKinematic = true; + + let fired = false; + boxA.addComponent( + class extends Script { + onCollisionEnter(_other: Collision): void { + fired = true; + resolve(); + } + } + ); + + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + // Move B onto A via DynamicCollider.move() — this internally calls setKinematicTarget. + colB.move(new Vector3(-3, 0, 0)); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + + if (!fired) reject(new Error("kine-kine setKinematicTarget did NOT fire onCollisionEnter")); + }); + }); + + it("dynamic + frozen-6 + teleport: overlap fires onCollisionEnter (fix candidate)", function () { + return new Promise((resolve) => { + engine.sceneManager.activeScene.physics.gravity = new Vector3(0, 0, 0); + const boxA = addBox(new Vector3(1, 1, 1), DynamicCollider, new Vector3(-3, 0, 0)); + const boxB = addBox(new Vector3(1, 1, 1), DynamicCollider, new Vector3(3, 0, 0)); + const colA = boxA.getComponent(DynamicCollider); + const colB = boxB.getComponent(DynamicCollider); + // Both fully frozen — emulates the cocos kinematic semantics (no gravity, no movement). + const FREEZE_ALL = + DynamicColliderConstraints.FreezePositionX | + DynamicColliderConstraints.FreezePositionY | + DynamicColliderConstraints.FreezePositionZ | + DynamicColliderConstraints.FreezeRotationX | + DynamicColliderConstraints.FreezeRotationY | + DynamicColliderConstraints.FreezeRotationZ; + colA.constraints = FREEZE_ALL; + colB.constraints = FREEZE_ALL; + colA.useGravity = false; + colB.useGravity = false; + colA.isKinematic = false; + colB.isKinematic = false; + + let fired = false; + boxA.addComponent( + class extends Script { + onCollisionEnter(_other: Collision): void { + fired = true; + resolve(); + } + } + ); + + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + boxB.transform.setPosition(-3, 0, 0); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + + if (!fired) { + expect.fail("expected onCollisionEnter to fire for dynamic-frozen pair after teleport"); + } + }); + }); }); diff --git a/tests/src/core/physics/DynamicCollider.test.ts b/tests/src/core/physics/DynamicCollider.test.ts index ae6771d05a..b441f300d4 100644 --- a/tests/src/core/physics/DynamicCollider.test.ts +++ b/tests/src/core/physics/DynamicCollider.test.ts @@ -6,6 +6,7 @@ import { DynamicCollider, DynamicColliderConstraints, CollisionDetectionMode, + DynamicColliderKinematicTransformSyncMode, StaticCollider, PlaneColliderShape } from "@galacean/engine-core"; @@ -278,6 +279,256 @@ describe("DynamicCollider", function () { expect(formatValue(boxCollider.inertiaTensor.y)).eq(1); }); + it("applyForceAtPosition - at center of mass produces only linear acceleration", function () { + const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); + const boxCollider = box.getComponent(DynamicCollider); + boxCollider.mass = 1; + boxCollider.useGravity = false; + boxCollider.centerOfMass = new Vector3(0, 0, 0); + boxCollider.inertiaTensor = new Vector3(1, 1, 1); + + boxCollider.applyForceAtPosition(new Vector3(1, 0, 0), new Vector3(0, 0, 0)); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1); + + expect(formatValue(boxCollider.linearVelocity.x)).eq(0.01667); + expect(formatValue(boxCollider.angularVelocity.x)).eq(0); + expect(formatValue(boxCollider.angularVelocity.y)).eq(0); + expect(formatValue(boxCollider.angularVelocity.z)).eq(0); + }); + + it("applyForceAtPosition - offset produces torque = (position - CoM) × force", function () { + // Reference: same physical setup but using applyForce + applyTorque(τ) directly. + // applyForceAtPosition(F, P) must produce the same result. + const setupBox = () => { + const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); + const collider = box.getComponent(DynamicCollider); + collider.mass = 1; + collider.useGravity = false; + collider.centerOfMass = new Vector3(0, 0, 0); + collider.inertiaTensor = new Vector3(1, 1, 1); + return collider; + }; + + const force = new Vector3(1, 0, 0); + const worldPos = new Vector3(0, 1, 0); // r = (0,1,0) - (0,0,0) = (0,1,0); r × F = (0,0,-1) + + const reference = setupBox(); + reference.applyForce(force); + reference.applyTorque(new Vector3(0, 0, -1)); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1); + const refLinear = reference.linearVelocity.clone(); + const refAngular = reference.angularVelocity.clone(); + + rootEntity.clearChildren(); + + const target = setupBox(); + target.applyForceAtPosition(force, worldPos); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1); + + expect(formatValue(target.linearVelocity.x)).eq(formatValue(refLinear.x)); + expect(formatValue(target.linearVelocity.y)).eq(formatValue(refLinear.y)); + expect(formatValue(target.linearVelocity.z)).eq(formatValue(refLinear.z)); + expect(formatValue(target.angularVelocity.x)).eq(formatValue(refAngular.x)); + expect(formatValue(target.angularVelocity.y)).eq(formatValue(refAngular.y)); + expect(formatValue(target.angularVelocity.z)).eq(formatValue(refAngular.z)); + expect(formatValue(target.angularVelocity.z)).lessThan(0); + }); + + it("applyForceAtPosition - respects centerOfMass offset (no torque when applied at CoM)", function () { + const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); + const boxCollider = box.getComponent(DynamicCollider); + boxCollider.mass = 1; + boxCollider.useGravity = false; + // Shift CoM to local (1, 0, 0). With entity at origin and identity rotation, world CoM = (1, 0, 0). + boxCollider.centerOfMass = new Vector3(1, 0, 0); + boxCollider.inertiaTensor = new Vector3(1, 1, 1); + + // Applying force at world (1,0,0) means r = 0 → no torque. + boxCollider.applyForceAtPosition(new Vector3(0, 0, 1), new Vector3(1, 0, 0)); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1); + + expect(formatValue(boxCollider.linearVelocity.z)).eq(0.01667); + expect(formatValue(boxCollider.angularVelocity.x)).eq(0); + expect(formatValue(boxCollider.angularVelocity.y)).eq(0); + expect(formatValue(boxCollider.angularVelocity.z)).eq(0); + }); + + it("applyForceAtPosition - respects entity world rotation when transforming local CoM", function () { + // entity rotated 90° around Y, CoM local (1, 0, 0) → world CoM offset (0, 0, -1) + // Apply force at world (0,0,-1), r=0, expect no torque. + const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); + box.transform.rotate(new Vector3(0, 90, 0)); + const boxCollider = box.getComponent(DynamicCollider); + boxCollider.mass = 1; + boxCollider.useGravity = false; + boxCollider.centerOfMass = new Vector3(1, 0, 0); + boxCollider.inertiaTensor = new Vector3(1, 1, 1); + + boxCollider.applyForceAtPosition(new Vector3(1, 0, 0), new Vector3(0, 0, -1)); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1); + + expect(formatValue(boxCollider.linearVelocity.x)).eq(0.01667); + expect(formatValue(boxCollider.angularVelocity.x)).eq(0); + expect(formatValue(boxCollider.angularVelocity.y)).eq(0); + expect(formatValue(boxCollider.angularVelocity.z)).eq(0); + }); + + it("applyForceAtPosition - position + rotation + CoM offset + r != 0 (full worldCoM coverage)", function () { + // Stress-test all three terms of worldCoM = entity.worldPos + worldRot * localCoM: + // entity position (5, 0, 0) — exercises the translation term + // entity rotation 90° around Y — exercises the rotation term (localCoM (1,0,0) → world (0,0,-1)) + // CoM local (1, 0, 0) — exercises the local CoM lookup + // worldCoM = (5,0,0) + (0,0,-1) = (5, 0, -1) + // force F = (1, 0, 0) at P = (5, 1, -1) + // r = P - worldCoM = (0, 1, 0) + // τ = r × F = (0, 0, -1) + // If any single term in worldCoM is wrong (missing translate / wrong quat / wrong localCoM), + // r becomes non-(0,1,0) and τ has spurious x/y components → catches the bug. + const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(5, 0, 0)); + box.transform.rotate(new Vector3(0, 90, 0)); + const boxCollider = box.getComponent(DynamicCollider); + boxCollider.mass = 1; + boxCollider.useGravity = false; + boxCollider.centerOfMass = new Vector3(1, 0, 0); + boxCollider.inertiaTensor = new Vector3(1, 1, 1); + + boxCollider.applyForceAtPosition(new Vector3(1, 0, 0), new Vector3(5, 1, -1)); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1); + + expect(formatValue(boxCollider.linearVelocity.x)).eq(0.01667); + expect(formatValue(boxCollider.angularVelocity.x)).eq(0); + expect(formatValue(boxCollider.angularVelocity.y)).eq(0); + expect(formatValue(boxCollider.angularVelocity.z)).lessThan(0); + }); + + it("applyForceAtPosition - works after entity.clone() (defends prefab clone path)", function () { + // R6/R8 both surfaced because clone() bypasses setters and leaves native state inconsistent. + // applyForceAtPosition reads native getCenterOfMass + entity.transform.worldRotationQuaternion. + // If a future change adds @ignoreClone fields or relies on setter side-effects, this test + // will catch the regression by exercising the API on a cloned collider. + const source = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); + const sourceCollider = source.getComponent(DynamicCollider); + sourceCollider.mass = 1; + sourceCollider.useGravity = false; + sourceCollider.centerOfMass = new Vector3(0, 0, 0); + sourceCollider.inertiaTensor = new Vector3(1, 1, 1); + + const cloneEntity = source.clone(); + source.destroy(); + rootEntity.addChild(cloneEntity); + cloneEntity.transform.setPosition(0, 0, 0); + + const cloneCollider = cloneEntity.getComponent(DynamicCollider); + // r = (0,1,0), F = (1,0,0), τ = (0,0,-1) → expect negative angular z + cloneCollider.applyForceAtPosition(new Vector3(1, 0, 0), new Vector3(0, 1, 0)); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1); + + expect(formatValue(cloneCollider.linearVelocity.x)).eq(0.01667); + expect(formatValue(cloneCollider.angularVelocity.z)).lessThan(0); + expect(formatValue(cloneCollider.angularVelocity.x)).eq(0); + expect(formatValue(cloneCollider.angularVelocity.y)).eq(0); + }); + + it("applyForce on sleeping actor must wake up and apply force", function () { + // Validates whether PhysX wasm `addForce(force, eFORCE, autowake=true)` actually wakes a + // sleeping actor on its own — or whether the engine's explicit wakeUp() call is required. + const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); + const boxCollider = box.getComponent(DynamicCollider); + boxCollider.mass = 1; + boxCollider.useGravity = false; + boxCollider.linearDamping = 0; + + boxCollider.sleep(); + expect(boxCollider.isSleeping()).toBe(true); + + boxCollider.applyForce(new Vector3(1, 0, 0)); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + + expect(formatValue(boxCollider.linearVelocity.x)).eq(0.01667); + expect(boxCollider.isSleeping()).toBe(false); + }); + + it("applyForce after kinematic→dynamic switch (mimic billiards game break flow)", function () { + // Game pattern: all balls set kinematic at init, switched back to dynamic on break, + // then applyForce. Verifies the original 'force lost' bug was actually from this path. + const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); + const boxCollider = box.getComponent(DynamicCollider); + boxCollider.mass = 1; + boxCollider.useGravity = false; + boxCollider.linearDamping = 0; + + boxCollider.isKinematic = true; + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + boxCollider.isKinematic = false; + + boxCollider.applyForce(new Vector3(1, 0, 0)); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + + expect(formatValue(boxCollider.linearVelocity.x)).eq(0.01667); + }); + + it("fixedTimeStep 1/60 vs 1/480: PhysX applyForce delivers 8x smaller dv at finer step", function () { + // ultrathink probe: does cocos-style `fixedTimeStep(true)→1/480` actually give + // *more force* than `fixedTimeStep(false)→1/60` in PhysX? + // + // Theory: + // Bullet (Cocos): clearForces runs ONCE after all substeps → dv = F·frame_dt/m + // → substep count doesn't affect dv. + // PhysX (Galacean): force cleared per simulate() call → only the first substep + // in a frame applies the force → dv = F·fixedTimeStep/m + // → 1/480 gives dv 8x smaller than 1/60. + // + // This test confirms the PhysX behavior empirically and quantifies the gap. + const scene = engine.sceneManager.activeScene; + const originalFTS = scene.physics.fixedTimeStep; + + const probe = (fts: number) => { + rootEntity.clearChildren(); + scene.physics.fixedTimeStep = fts; + const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); + const c = box.getComponent(DynamicCollider); + c.mass = 1; + c.useGravity = false; + c.linearDamping = 0; + c.angularDamping = 0; + c.applyForce(new Vector3(100, 0, 0)); + // Advance exactly one *frame* of wall time. Galacean's _update loops simulate + // until frame_dt accumulates: 1/60 → 1 substep; 1/480 → 8 substeps. + // @ts-ignore + scene.physics._update(1 / 60); + return c.linearVelocity.x; + }; + + const dv_1_60 = probe(1 / 60); + const dv_1_480 = probe(1 / 480); + + console.info( + `[fixedTimeStep probe] applyForce(F=100) over 1 frame (1/60s):\n` + + ` 1/60 step → dv = ${dv_1_60.toFixed(4)} m/s\n` + + ` 1/480 step → dv = ${dv_1_480.toFixed(4)} m/s\n` + + ` ratio (1/60 / 1/480) = ${(dv_1_60 / dv_1_480).toFixed(3)} (theory: 8)` + ); + + scene.physics.fixedTimeStep = originalFTS; + + // Theory: dv_1_60 = F·(1/60)/m = 100/60 ≈ 1.667 + expect(dv_1_60).toBeCloseTo(100 / 60, 2); + // Theory: dv_1_480 = F·(1/480)/m = 100/480 ≈ 0.208 + expect(dv_1_480).toBeCloseTo(100 / 480, 2); + // Ratio must be 8 (PhysX clears force per simulate) + expect(dv_1_60 / dv_1_480).toBeCloseTo(8, 1); + }); + it("maxAngularVelocity", function () { const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); const boxCollider = box.getComponent(DynamicCollider); @@ -398,6 +649,48 @@ describe("DynamicCollider", function () { expect(box.transform.position.y).below(1); }); + it("teleports kinematic target collider on re-enable instead of sweeping from stale native pose", function () { + const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(-10, 0, 0)); + const boxCollider = box.getComponent(DynamicCollider); + boxCollider.useGravity = false; + boxCollider.isKinematic = true; + boxCollider.kinematicTransformSyncMode = DynamicColliderKinematicTransformSyncMode.Target; + + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + + // @ts-ignore - intentionally observe the native boundary used by Collider sync. + const nativeCollider = boxCollider._nativeCollider; + const originalMove = nativeCollider.move.bind(nativeCollider); + const originalSetWorldTransform = nativeCollider.setWorldTransform.bind(nativeCollider); + let moveCalls = 0; + let setWorldTransformCalls = 0; + nativeCollider.move = (...args: Parameters) => { + moveCalls++; + return originalMove(...args); + }; + nativeCollider.setWorldTransform = (...args: Parameters) => { + setWorldTransformCalls++; + return originalSetWorldTransform(...args); + }; + + try { + box.isActive = false; + box.transform.setPosition(10, 0, 0); + box.isActive = true; + + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + + expect(moveCalls).eq(0); + expect(setWorldTransformCalls).eq(1); + expect(formatValue(box.transform.position.x)).eq(10); + } finally { + nativeCollider.move = originalMove; + nativeCollider.setWorldTransform = originalSetWorldTransform; + } + }); + it("constraints", function () { const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); const boxCollider = box.getComponent(DynamicCollider); @@ -442,6 +735,52 @@ describe("DynamicCollider", function () { ).toBeTruthy(); }); + it("R0: CCD mode survives kinematic toggle (PhysX rejects CCD on kinematic)", function () { + // RED verification for R0 fix: + // PhysX 4.1.1 forbids CCD on kinematic actors. The fix caches the user-intended mode + // and re-applies on kinematic→dynamic. Without the fix, switching to kinematic loses + // the CCD flag and a subsequent dynamic switch does not restore it. + const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); + const boxCollider = box.getComponent(DynamicCollider); + // @ts-ignore + const physX = boxCollider._nativeCollider._physXPhysics._physX; + const ccdFlag = () => + // @ts-ignore + boxCollider._nativeCollider._pxActor.getRigidBodyFlags(physX.PxRigidBodyFlag.eENABLE_CCD); + + boxCollider.collisionDetectionMode = CollisionDetectionMode.Continuous; + expect(ccdFlag()).toBeTruthy(); + + boxCollider.isKinematic = true; + expect(ccdFlag()).toBeFalsy(); + + boxCollider.isKinematic = false; + expect(ccdFlag()).toBeTruthy(); + expect(boxCollider.collisionDetectionMode).toEqual(CollisionDetectionMode.Continuous); + }); + + it("R0: setCollisionDetectionMode in kinematic state defers application", function () { + // RED verification: while kinematic, the CCD flag should not be touched (PhysX warns). + // User's intent is cached and applied on next dynamic switch. + const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); + const boxCollider = box.getComponent(DynamicCollider); + // @ts-ignore + const physX = boxCollider._nativeCollider._physXPhysics._physX; + const ccdFlag = () => + // @ts-ignore + boxCollider._nativeCollider._pxActor.getRigidBodyFlags(physX.PxRigidBodyFlag.eENABLE_CCD); + + boxCollider.isKinematic = true; + expect(ccdFlag()).toBeFalsy(); + + boxCollider.collisionDetectionMode = CollisionDetectionMode.Continuous; + expect(ccdFlag()).toBeFalsy(); + expect(boxCollider.collisionDetectionMode).toEqual(CollisionDetectionMode.Continuous); + + boxCollider.isKinematic = false; + expect(ccdFlag()).toBeTruthy(); + }); + it("sleep", function () { const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); const boxCollider = box.getComponent(DynamicCollider); diff --git a/tests/src/core/physics/MeshColliderShape.test.ts b/tests/src/core/physics/MeshColliderShape.test.ts index dfb12cf5c0..1b85709d29 100644 --- a/tests/src/core/physics/MeshColliderShape.test.ts +++ b/tests/src/core/physics/MeshColliderShape.test.ts @@ -31,11 +31,7 @@ class CollisionScript extends Script { * @param indices - Optional triangle indices * @returns A ModelMesh with readable data */ -function createModelMesh( - engine: WebGLEngine, - positions: number[], - indices?: number[] -): ModelMesh { +function createModelMesh(engine: WebGLEngine, positions: number[], indices?: number[]): ModelMesh { const mesh = new ModelMesh(engine); const vec3Positions: Vector3[] = []; for (let i = 0; i < positions.length; i += 3) { @@ -107,11 +103,7 @@ describe("MeshColliderShape PhysX", () => { const meshShape = new MeshColliderShape(); const meshMaterial = meshShape.material; // Ground plane at y=0, CCW winding -> normal +Y - const mesh = createModelMesh( - engine, - [-10, 0, -10, 10, 0, -10, -10, 0, 10, 10, 0, 10], - [0, 2, 1, 1, 2, 3] - ); + const mesh = createModelMesh(engine, [-10, 0, -10, 10, 0, -10, -10, 0, 10, 10, 0, 10], [0, 2, 1, 1, 2, 3]); meshShape.mesh = mesh; groundCollider.addShape(meshShape); @@ -190,6 +182,57 @@ describe("MeshColliderShape PhysX", () => { defaultMaterial?.destroy(); material?.destroy(); }); + + it("R6: cloned MeshColliderShape rebuilds its native PhysX shape", async () => { + // RED verification for R6 fix: + // MeshColliderShape's `_nativeShape` is `@ignoreClone` — without an + // override of `_cloneTo` cooking a fresh shape from cloned vertex/index + // buffers, the cloned entity has no physical surface (sphere falls + // straight through). + const groundEntity = root.createChild("meshGroundForClone"); + groundEntity.transform.setPosition(0, 0, 0); + const groundCollider = groundEntity.addComponent(StaticCollider); + const meshShape = new MeshColliderShape(); + const meshMaterial = meshShape.material; + const mesh = createModelMesh(engine, [-10, 0, -10, 10, 0, -10, -10, 0, 10, 10, 0, 10], [0, 2, 1, 1, 2, 3]); + meshShape.mesh = mesh; + groundCollider.addShape(meshShape); + + const clonedGround = groundEntity.clone(); + // Move the original aside so the cloned ground is the only surface below the sphere. + groundEntity.transform.setPosition(1000, 0, 0); + root.addChild(clonedGround); + clonedGround.transform.setPosition(0, 0, 0); + + const clonedShape = clonedGround.getComponent(StaticCollider).shapes[0] as MeshColliderShape; + // @ts-ignore — inspect that the cloned shape actually has a usable native PhysX handle + expect(clonedShape._nativeShape).not.toBeNull(); + // @ts-ignore + expect(clonedShape._nativeShape._pxShape).toBeDefined(); + + const sphereEntity = root.createChild("sphereForClone"); + sphereEntity.transform.setPosition(0, 2, 0); + const dynamicCollider = sphereEntity.addComponent(DynamicCollider); + const sphereShape = new SphereColliderShape(); + const sphereMaterial = sphereShape.material; + sphereShape.radius = 0.5; + dynamicCollider.addShape(sphereShape); + + for (let i = 0; i < 60; i++) { + physicsScene._update(1 / 60); + } + + // Sphere lands on cloned ground (y > -1), not falls forever (y < -10). + const sphereY = sphereEntity.transform.position.y; + expect(sphereY).toBeGreaterThan(-1); + expect(sphereY).toBeLessThan(2); + + groundEntity.destroy(); + clonedGround.destroy(); + sphereEntity.destroy(); + meshMaterial?.destroy(); + sphereMaterial?.destroy(); + }); }); describe("Convex Mesh (Dynamic)", () => { @@ -265,9 +308,10 @@ describe("MeshColliderShape PhysX", () => { const meshShape = new MeshColliderShape(); const meshMaterial = meshShape.material; meshShape.isConvex = true; - const mesh = createModelMesh(engine, [ - -1, -1, -1, 1, -1, -1, 1, 1, -1, -1, 1, -1, -1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1 - ]); + const mesh = createModelMesh( + engine, + [-1, -1, -1, 1, -1, -1, 1, 1, -1, -1, 1, -1, -1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1] + ); meshShape.mesh = mesh; meshShape.isTrigger = true; triggerCollider.addShape(meshShape); @@ -417,11 +461,7 @@ describe("MeshColliderShape PhysX", () => { staticCollider.addShape(meshShape); // Update mesh - const mesh2 = createModelMesh( - engine, - [0, 0, 0, 2, 0, 0, 0, 2, 0, 2, 0, 0, 2, 2, 0, 0, 2, 0], - [0, 1, 2, 3, 4, 5] - ); + const mesh2 = createModelMesh(engine, [0, 0, 0, 2, 0, 0, 0, 2, 0, 2, 0, 0, 2, 2, 0, 0, 2, 0], [0, 1, 2, 3, 4, 5]); meshShape.mesh = mesh2; expect(staticCollider.shapes.length).toBe(1); @@ -695,11 +735,7 @@ describe("MeshColliderShape PhysX", () => { expect(meshShape._nativeShape).toBeNull(); // Re-enable with new mesh - const mesh2 = createModelMesh( - engine, - [0, 0, 0, 2, 0, 0, 0, 2, 0, 2, 0, 0, 2, 2, 0, 0, 2, 0], - [0, 1, 2, 3, 4, 5] - ); + const mesh2 = createModelMesh(engine, [0, 0, 0, 2, 0, 0, 0, 2, 0, 2, 0, 0, 2, 2, 0, 0, 2, 0], [0, 1, 2, 3, 4, 5]); meshShape.mesh = mesh2; // @ts-ignore expect(meshShape._nativeShape).not.toBeNull(); diff --git a/tests/src/core/physics/PhysicsMaterial.test.ts b/tests/src/core/physics/PhysicsMaterial.test.ts index 7433197ade..7bfd0e0e88 100644 --- a/tests/src/core/physics/PhysicsMaterial.test.ts +++ b/tests/src/core/physics/PhysicsMaterial.test.ts @@ -79,6 +79,56 @@ describe("PhysicsMaterial", () => { expect(formatValue(boxEntity2.transform.position.y)).eq(0); }); + it("cloned collider shape material keeps native values", () => { + const scene = engine.sceneManager.activeScene; + const originalGravity = scene.physics.gravity.clone(); + const originalFixedTimeStep = scene.physics.fixedTimeStep; + scene.physics.gravity = new Vector3(0, 0, 0); + scene.physics.fixedTimeStep = 1 / 60; + + try { + const wallEntity = addBox(new Vector3(1, 8, 8), StaticCollider, new Vector3(0, 0, 0)); + const wallMaterial = wallEntity.getComponent(StaticCollider).shapes[0].material; + wallMaterial.bounciness = 1; + wallMaterial.dynamicFriction = 0; + wallMaterial.staticFriction = 0; + wallMaterial.bounceCombine = PhysicsMaterialCombineMode.Multiply; + wallMaterial.frictionCombine = PhysicsMaterialCombineMode.Multiply; + + const sourceEntity = addBox(new Vector3(1, 1, 1), DynamicCollider, new Vector3(-3, 0, 0)); + const sourceCollider = sourceEntity.getComponent(DynamicCollider); + sourceCollider.linearDamping = 0; + sourceCollider.angularDamping = 0; + sourceCollider.automaticCenterOfMass = false; + sourceCollider.automaticInertiaTensor = false; + + const sourceMaterial = sourceCollider.shapes[0].material; + sourceMaterial.bounciness = 1; + sourceMaterial.dynamicFriction = 0; + sourceMaterial.staticFriction = 0; + sourceMaterial.bounceCombine = PhysicsMaterialCombineMode.Multiply; + sourceMaterial.frictionCombine = PhysicsMaterialCombineMode.Multiply; + + const cloneEntity = sourceEntity.clone(); + sourceEntity.destroy(); + rootEntity.addChild(cloneEntity); + cloneEntity.transform.setPosition(-3, 0, 0); + + const cloneCollider = cloneEntity.getComponent(DynamicCollider); + cloneCollider.linearVelocity = new Vector3(10, 0, 0); + + for (let i = 0; i < 40; i++) { + // @ts-ignore + scene.physics._update(scene.physics.fixedTimeStep); + } + + expect(cloneCollider.linearVelocity.x).lessThan(-1); + } finally { + scene.physics.gravity = originalGravity; + scene.physics.fixedTimeStep = originalFixedTimeStep; + } + }); + it("bounceCombine Average", () => { const boxEntity = addBox(new Vector3(1, 1, 1), DynamicCollider, new Vector3(0, 5, 0)); const ground = addPlane(0, -0.5, 0); diff --git a/tests/src/core/physics/PhysicsScene.test.ts b/tests/src/core/physics/PhysicsScene.test.ts index 406306a4b6..9f258a1e2d 100644 --- a/tests/src/core/physics/PhysicsScene.test.ts +++ b/tests/src/core/physics/PhysicsScene.test.ts @@ -12,8 +12,7 @@ import { Scene, Script, SphereColliderShape, - StaticCollider, - OverlapHitResult + StaticCollider } from "@galacean/engine-core"; import { Ray, Vector3, Quaternion } from "@galacean/engine-math"; import { LitePhysics } from "@galacean/engine-physics-lite"; @@ -74,12 +73,44 @@ class CollisionTestScript extends Script { } } +class CollisionDemandScript extends Script { + onCollisionEnter(): void {} +} + +class TriggerDemandScript extends Script { + onTriggerEnter(): void {} +} + function updatePhysics(physics) { for (let i = 0; i < 5; ++i) { physics._update(8); } } +function watchNativeContactEventDemand(physicsScene: PhysicsScene) { + const nativeScene = (physicsScene as any)._nativePhysicsScene; + const original = nativeScene.setContactEventEnabled; + const calls: boolean[] = []; + nativeScene.setContactEventEnabled = (enabled: boolean) => { + calls.push(enabled); + original?.call(nativeScene, enabled); + }; + return { + calls, + restore() { + if (original) { + nativeScene.setContactEventEnabled = original; + } else { + delete nativeScene.setContactEventEnabled; + } + } + }; +} + +function getLastContactEventDemandCall(calls: boolean[]): boolean { + return calls[calls.length - 1]; +} + function resetSpy() { // reset spy on collision test script. CollisionTestScript.prototype.onCollisionEnter = vi.fn(CollisionTestScript.prototype.onCollisionEnter); @@ -422,6 +453,66 @@ describe("Physics Test", () => { expect(enginePhysX.sceneManager.scenes[0].physics.fixedTimeStep).to.eq(fixedTimeStep); }); + it("auto-disables native contact events when no active collision callback exists", () => { + const scene = enginePhysX.sceneManager.activeScene; + const physicsScene = scene.physics; + const root = scene.createRootEntity("contact-demand-disabled"); + const entity = root.createChild("body"); + const collider = entity.addComponent(StaticCollider); + collider.addShape(new BoxColliderShape()); + const contactEventDemand = watchNativeContactEventDemand(physicsScene); + + try { + physicsScene._update(physicsScene.fixedTimeStep); + expect(getLastContactEventDemandCall(contactEventDemand.calls)).to.eq(false); + } finally { + contactEventDemand.restore(); + root.destroy(); + } + }); + + it("auto-enables native contact events only while an active collision callback exists", () => { + const scene = enginePhysX.sceneManager.activeScene; + const physicsScene = scene.physics; + const root = scene.createRootEntity("contact-demand-enabled"); + const entity = root.createChild("body"); + const collider = entity.addComponent(StaticCollider); + collider.addShape(new BoxColliderShape()); + const script = entity.addComponent(CollisionDemandScript); + const contactEventDemand = watchNativeContactEventDemand(physicsScene); + + try { + physicsScene._update(physicsScene.fixedTimeStep); + expect(getLastContactEventDemandCall(contactEventDemand.calls)).to.eq(true); + + script.enabled = false; + physicsScene._update(physicsScene.fixedTimeStep); + expect(getLastContactEventDemandCall(contactEventDemand.calls)).to.eq(false); + } finally { + contactEventDemand.restore(); + root.destroy(); + } + }); + + it("keeps native contact events disabled when only trigger callbacks exist", () => { + const scene = enginePhysX.sceneManager.activeScene; + const physicsScene = scene.physics; + const root = scene.createRootEntity("contact-demand-trigger-only"); + const entity = root.createChild("body"); + const collider = entity.addComponent(StaticCollider); + collider.addShape(new BoxColliderShape()); + entity.addComponent(TriggerDemandScript); + const contactEventDemand = watchNativeContactEventDemand(physicsScene); + + try { + physicsScene._update(physicsScene.fixedTimeStep); + expect(getLastContactEventDemandCall(contactEventDemand.calls)).to.eq(false); + } finally { + contactEventDemand.restore(); + root.destroy(); + } + }); + it("raycast", () => { const scene = enginePhysX.sceneManager.activeScene; const physicsScene = scene.physics; @@ -469,16 +560,15 @@ describe("Physics Test", () => { expect(outHitResult.normal).to.be.deep.include({ x: 0, y: 0, z: 0 }); expect(outHitResult.entity).to.be.null; - // Test that return origin point if origin is inside collider. + // Test that initial overlap is skipped when ray origin is inside collider. + // Use a strictly-inside origin (2.9,2.9,2.9) rather than the box corner + // (3,3,3), which is a boundary point whose hit/miss depends on PhysX edge + // tolerance and can flake regardless of the initial-overlap-skip behavior. boxShape.size = new Vector3(6, 6, 6); - ray = new Ray(new Vector3(3, 3, 3), new Vector3(0, -1, 0).normalize()); - expect(physicsScene.raycast(ray, outHitResult)).to.eq(true); + ray = new Ray(new Vector3(2.9, 2.9, 2.9), new Vector3(0, -1, 0).normalize()); + expect(physicsScene.raycast(ray, outHitResult)).to.eq(false); expect(outHitResult.distance).to.be.eq(0); - expect(outHitResult.point).to.be.deep.include({ x: 3, y: 3, z: 3 }); - expect(outHitResult.normal.x).to.be.eq(0); - expect(outHitResult.normal.y).to.be.eq(1); - expect(outHitResult.normal.z).to.be.eq(0); - expect(outHitResult.entity).to.be.eq(raycastTestRoot); + expect(outHitResult.entity).to.be.null; // Test that raycast works correctly if shape is not at origin of coordinate. boxShape.size = new Vector3(1, 1, 1); @@ -508,6 +598,7 @@ describe("Physics Test", () => { rootEntityCharacter.transform.position = new Vector3(0, 0, 0); const characterController = rootEntityCharacter.addComponent(CharacterController); + characterController.collisionLayer = Layer.Layer3; const boxShape2 = new BoxColliderShape(); boxShape2.size.set(1, 1, 1); boxShape2.position = new Vector3(0, 0, 0); @@ -554,7 +645,7 @@ describe("Physics Test", () => { const halfExtents = new Vector3(0.5, 0.5, 0.5); const direction = new Vector3(0, 1, 0); const orientation = new Quaternion(); - expect(physicsScene.boxCast(center, halfExtents, direction, orientation)).to.eq(false); + expect(physicsScene.boxCast(center, halfExtents, direction)).to.eq(false); // Test boxCast with hit direction.set(-1, -1, -1); @@ -584,7 +675,7 @@ describe("Physics Test", () => { physicsScene.boxCast(center, halfExtents, direction, orientation, 0.1, Layer.Everything, outHitResult) ).to.eq(false); - // Test boxCast when box is inside collider + // Test that initial overlap is skipped when sweep starts inside a collider. center.set(0, 0, 0); expect( physicsScene.boxCast( @@ -596,8 +687,7 @@ describe("Physics Test", () => { Layer.Everything, outHitResult ) - ).to.eq(true); - expect(outHitResult.distance).to.be.eq(0); + ).to.eq(false); // Test boxCast with rotation Quaternion.rotationEuler(0, Math.PI / 4, 0, orientation); @@ -728,12 +818,11 @@ describe("Physics Test", () => { // Test sphereCast with distance limit expect(physicsScene.sphereCast(center, radius, direction, 0.1, Layer.Everything, outHitResult)).to.eq(false); - // Test sphereCast when sphere is inside collider + // Test that initial overlap is skipped when sphere starts inside a collider. center.set(0, 0, 0); expect( physicsScene.sphereCast(center, radius, direction, Number.MAX_VALUE, Layer.Everything, outHitResult) - ).to.eq(true); - expect(outHitResult.distance).to.be.eq(0); + ).to.eq(false); // Test sphereCast with multiple colliders const collider2 = sweepTestRoot.addComponent(StaticCollider); @@ -855,7 +944,7 @@ describe("Physics Test", () => { physicsScene.capsuleCast(center, radius, height, direction, orientation, 0.1, Layer.Everything, outHitResult) ).to.eq(false); - // Test capsuleCast when capsule is inside collider + // Test that initial overlap is skipped when capsule starts inside a collider. center.set(0, 0, 0); expect( physicsScene.capsuleCast( @@ -868,8 +957,7 @@ describe("Physics Test", () => { Layer.Everything, outHitResult ) - ).to.eq(true); - expect(outHitResult.distance).to.be.eq(0); + ).to.eq(false); // Test capsuleCast with rotation Quaternion.rotationEuler(0, Math.PI / 4, 0, orientation); @@ -1511,14 +1599,16 @@ describe("Physics Test", () => { collisionTestScript.useLite = false; // Test that collision works correctly, A is dynamic and kinematic, B is static. + // SceneDesc.staticKineFilteringMode = eKEEP + Collider.move() routing kinematic + // to setKinematicTarget make static-kinematic pairs generate contact events. resetSpy(); setColliderProps(entity1, true, false, true); setColliderProps(entity2, false, false, false); updatePhysics(physicsMgr); - expect(collisionTestScript.onCollisionEnter).not.toHaveBeenCalled(); - expect(collisionTestScript.onCollisionStay).not.toHaveBeenCalled(); - expect(collisionTestScript.onCollisionExit).not.toHaveBeenCalled(); + expect(collisionTestScript.onCollisionEnter).toHaveBeenCalled(); + expect(collisionTestScript.onCollisionStay).toHaveBeenCalled(); + expect(collisionTestScript.onCollisionExit).toHaveBeenCalled(); expect(collisionTestScript.onTriggerEnter).not.toHaveBeenCalled(); expect(collisionTestScript.onTriggerStay).not.toHaveBeenCalled(); expect(collisionTestScript.onTriggerExit).not.toHaveBeenCalled(); @@ -1631,14 +1721,16 @@ describe("Physics Test", () => { collisionTestScript.useLite = false; // Test that collision works correctly, both A,B are dynamic, kinematic. + // SceneDesc.kineKineFilteringMode = eKEEP + Collider.move() routing kinematic + // to setKinematicTarget make kine-kine pairs generate contact events. resetSpy(); setColliderProps(entity1, true, false, true); setColliderProps(entity2, true, false, true); updatePhysics(physicsMgr); - expect(collisionTestScript.onCollisionEnter).not.toHaveBeenCalled(); - expect(collisionTestScript.onCollisionStay).not.toHaveBeenCalled(); - expect(collisionTestScript.onCollisionExit).not.toHaveBeenCalled(); + expect(collisionTestScript.onCollisionEnter).toHaveBeenCalled(); + expect(collisionTestScript.onCollisionStay).toHaveBeenCalled(); + expect(collisionTestScript.onCollisionExit).toHaveBeenCalled(); expect(collisionTestScript.onTriggerEnter).not.toHaveBeenCalled(); expect(collisionTestScript.onTriggerStay).not.toHaveBeenCalled(); expect(collisionTestScript.onTriggerExit).not.toHaveBeenCalled(); diff --git a/tests/src/loader/GLTFLoader.test.ts b/tests/src/loader/GLTFLoader.test.ts index 7b35b7b330..164dd788aa 100644 --- a/tests/src/loader/GLTFLoader.test.ts +++ b/tests/src/loader/GLTFLoader.test.ts @@ -39,6 +39,156 @@ beforeAll(async function () { @registerGLTFParser(GLTFParserType.Schema) class GLTFCustomJSONParser extends GLTFParser { parse(context: GLTFParserContext) { + if (context.glTFResource.url.endsWith("testSkinRoot.gltf")) { + context.buffers = [new ArrayBuffer(128)]; + return Promise.resolve({ + asset: { + version: "2.0" + }, + scene: 0, + scenes: [ + { + nodes: [0, 1] + } + ], + nodes: [ + { + name: "Character_Man", + skin: 0 + }, + { + name: "mixamorig:Hips", + children: [2] + }, + { + name: "mixamorig:Spine" + } + ], + skins: [ + { + inverseBindMatrices: 0, + joints: [1, 2] + } + ], + accessors: [ + { + bufferView: 0, + byteOffset: 0, + componentType: 5126, + count: 2, + type: "MAT4" + } + ], + bufferViews: [ + { + buffer: 0, + byteOffset: 0, + byteLength: 128 + } + ], + buffers: [ + { + byteLength: 128 + } + ] + }); + } + + if (context.glTFResource.url.endsWith("testSkinRootBounds.gltf")) { + const buffer = new ArrayBuffer(152); + const floats = new Float32Array(buffer); + // Inverse bind matrices for Hips and Spine. Their bind pose world x is + // Character_Group(3) + Hips(10), so inverse bind translates by -13. + floats.set([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -13, 0, 0, 1], 0); + floats.set([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -13, 0, 0, 1], 16); + floats.set([9, -1, -1, 11, 1, 1], 32); + context.buffers = [buffer]; + return Promise.resolve({ + asset: { + version: "2.0" + }, + scene: 0, + scenes: [ + { + nodes: [0] + } + ], + nodes: [ + { + name: "Character_Group", + translation: [3, 0, 0], + children: [1, 2] + }, + { + name: "Character_Man", + mesh: 0, + skin: 0 + }, + { + name: "mixamorig:Hips", + translation: [10, 0, 0], + children: [3] + }, + { + name: "mixamorig:Spine" + } + ], + skins: [ + { + inverseBindMatrices: 0, + joints: [2, 3] + } + ], + meshes: [ + { + primitives: [ + { + attributes: { + POSITION: 1 + }, + mode: 4 + } + ] + } + ], + accessors: [ + { + bufferView: 0, + byteOffset: 0, + componentType: 5126, + count: 2, + type: "MAT4" + }, + { + bufferView: 1, + byteOffset: 0, + componentType: 5126, + count: 2, + type: "VEC3", + min: [9, -1, -1], + max: [11, 1, 1] + } + ], + bufferViews: [ + { + buffer: 0, + byteOffset: 0, + byteLength: 128 + }, + { + buffer: 0, + byteOffset: 128, + byteLength: 24 + } + ], + buffers: [ + { + byteLength: 152 + } + ] + }); + } + const glTF = { buffers: [ { @@ -393,7 +543,7 @@ beforeAll(async function () { afterAll(() => { @registerGLTFParser(GLTFParserType.Schema) - class test extends GLTFSchemaParser { } + class test extends GLTFSchemaParser {} }); describe("glTF Loader test", function () { @@ -481,6 +631,17 @@ describe("glTF Loader test", function () { expect(renderer).to.exist; expect(renderer.blendShapeWeights).to.deep.include([1, 1]); }); + + it("single-root animation root channel should bind to the root node path", async () => { + const glTFResource: GLTFResource = await engine.resourceManager.load({ + type: AssetType.GLTF, + url: "mock/path/testA.gltf" + }); + + const clip = glTFResource.animations?.[0]; + expect(clip).to.exist; + expect(clip.curveBindings[0].relativePath).to.equal("entity1"); + }); }); describe("glTF instance test", function () { @@ -517,6 +678,50 @@ describe("glTF instance test", function () { }); }); +describe("glTF scene root structure", function () { + it("Single root scene should have GLTF_ROOT container", async () => { + const glTFResource: GLTFResource = await engine.resourceManager.load({ + type: AssetType.GLTF, + url: "mock/path/testRoot.gltf" + }); + const { defaultSceneRoot } = glTFResource; + + // Should always create GLTF_ROOT container, even for single-root scenes + expect(defaultSceneRoot.name).to.equal("GLTF_ROOT"); + expect(defaultSceneRoot.children.length).to.equal(1); + expect(defaultSceneRoot.children[0].name).to.equal("entity1"); + }); + + it("Multi-root skins without skeleton should use the scene wrapper as rootBone", async () => { + const glTFResource: GLTFResource = await engine.resourceManager.load({ + type: AssetType.GLTF, + url: "mock/path/testSkinRoot.gltf" + }); + const { defaultSceneRoot, skins } = glTFResource; + + expect(defaultSceneRoot.name).to.equal("GLTF_ROOT"); + expect(defaultSceneRoot.children.length).to.equal(2); + expect(skins[0].rootBone).to.equal(defaultSceneRoot); + }); + + it("Skinned mesh bounds should stay in rootBone space when inferred rootBone is outside joints", async () => { + const glTFResource: GLTFResource = await engine.resourceManager.load({ + type: AssetType.GLTF, + url: "mock/path/testSkinRootBounds.gltf" + }); + const { defaultSceneRoot, skins } = glTFResource; + const characterGroup = defaultSceneRoot.children[0]; + const characterMesh = characterGroup.children[0]; + const renderer = characterMesh.getComponent(SkinnedMeshRenderer); + + expect(skins[0].rootBone).to.equal(characterGroup); + expect(renderer.localBounds.min.x).to.be.closeTo(6, 1e-5); + expect(renderer.localBounds.max.x).to.be.closeTo(8, 1e-5); + expect(renderer.bounds.min.x).to.be.closeTo(9, 1e-5); + expect(renderer.bounds.max.x).to.be.closeTo(11, 1e-5); + }); +}); + describe("glTF instance test", function () { it("GLTFResource destroy directly", async () => { const glTFResource: GLTFResource = await engine.resourceManager.load({ diff --git a/tests/src/loader/ScenePhysics.test.ts b/tests/src/loader/ScenePhysics.test.ts new file mode 100644 index 0000000000..74a1a452cf --- /dev/null +++ b/tests/src/loader/ScenePhysics.test.ts @@ -0,0 +1,50 @@ +import { AssetType, BackgroundMode, Scene } from "@galacean/engine"; +import "@galacean/engine-loader"; +import { PhysXPhysics } from "@galacean/engine-physics-physx"; +import { WebGLEngine } from "@galacean/engine-rhi-webgl"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +let engine: WebGLEngine; + +beforeAll(async () => { + engine = await WebGLEngine.create({ + canvas: document.createElement("canvas"), + physics: new PhysXPhysics() + }); +}); + +afterAll(() => { + engine?.destroy(); +}); + +describe("SceneLoader physics settings", () => { + it("applies serialized scene physics settings to PhysicsScene", async () => { + const sceneData = { + name: "physics-scene", + entities: [], + scene: { + background: { + mode: BackgroundMode.SolidColor, + color: { r: 0, g: 0, b: 0, a: 1 } + }, + physics: { + gravity: { x: 0, y: -3200, z: 0 }, + fixedTimeStep: 1 / 120 + } + }, + files: [] + }; + const sceneUrl = + URL.createObjectURL(new Blob([JSON.stringify(sceneData)], { type: "application/json" })) + "#.scene"; + + const scene = await engine.resourceManager.load({ + url: sceneUrl, + type: AssetType.Scene + }); + + expect(scene.physics.gravity.x).toBe(0); + expect(scene.physics.gravity.y).toBe(-3200); + expect(scene.physics.gravity.z).toBe(0); + expect(scene.physics.fixedTimeStep).toBe(1 / 120); + }); +}); diff --git a/tests/src/loader/StrippedEntity.test.ts b/tests/src/loader/StrippedEntity.test.ts new file mode 100644 index 0000000000..6f078d1d17 --- /dev/null +++ b/tests/src/loader/StrippedEntity.test.ts @@ -0,0 +1,277 @@ +/** + * Tests stripped entities that proxy internal prefab-instance entities. + * + * A stripped entity can resolve to an entity inside a nested prefab instance and + * serve as the parent for newly added entities. + */ +import { expect, beforeAll, afterAll, describe, it } from "vitest"; +import { WebGLEngine } from "@galacean/engine-rhi-webgl"; +import type { IHierarchyFile } from "@galacean/engine-loader"; +import { Vector3 } from "@galacean/engine-math"; +import { PrefabParser } from "../../../packages/loader/src/prefab/PrefabParser"; + +let engine: WebGLEngine; + +function getResourceObjectPool(): Record { + return (engine.resourceManager as unknown as { _objectPool: Record })._objectPool; +} + +function cachePrefab(url: string, prefab: unknown): void { + getResourceObjectPool()[url] = prefab; +} + +function removeCachedPrefab(url: string): void { + delete getResourceObjectPool()[url]; +} + +beforeAll(async () => { + const canvas = document.createElement("canvas"); + canvas.width = 256; + canvas.height = 256; + engine = await WebGLEngine.create({ canvas }); +}); + +afterAll(() => { + engine?.destroy(); +}); + +describe("IStrippedEntity as addedEntities mechanism", () => { + it("keeps nested prefab children after instantiation", async () => { + const nestedPrefabData: IHierarchyFile = { + entities: [ + { id: "0", name: "nestedRoot", components: [], children: ["1", "2"] }, + { id: "1", name: "boneHand", parent: "0", components: [] }, + { id: "2", name: "otherChild", parent: "0", components: [] } + ] + }; + const nestedPrefab = await PrefabParser.parse(engine, "sanity.prefab", nestedPrefabData); + const inst = nestedPrefab.instantiate(); + const names = inst.children.map((c) => c.name); + expect(names).toContain("boneHand"); + expect(names).toContain("otherChild"); + inst.destroy(); + }); + + it("should attach new child entity under an internal entity of a nested prefab", async () => { + // Nested prefab: + // root + // - boneHand (proxied by the stripped entity) + // - otherChild + const nestedPrefabData: IHierarchyFile = { + entities: [ + { id: "0", name: "nestedRoot", components: [], children: ["1", "2"] }, + { id: "1", name: "boneHand", parent: "0", components: [] }, + { id: "2", name: "otherChild", parent: "0", components: [] } + ] + }; + const nestedPrefab = await PrefabParser.parse(engine, "char.prefab", nestedPrefabData); + cachePrefab("char.prefab", nestedPrefab); + + // Outer prefab: + // - IRefEntity instantiating the nested prefab (only direct child of outerRoot) + // - IStrippedEntity proxying to boneHand, not in outerRoot.children + // - weaponslot listed in stripped entity's children + const outerPrefabData: IHierarchyFile = { + entities: [ + { + id: "outerRoot", + name: "Character", + components: [], + children: ["charInst"] + }, + { + id: "charInst", + name: "char", + parent: "outerRoot", + components: [], + assetUrl: "char.prefab", + isClone: true, + modifications: [], + removedEntities: [], + removedComponents: [] + } as any, + { + // Do not set `name`: _applyEntityData would override the internal entity's name. + strippedId: "boneHandle", + prefabInstanceId: "charInst", + prefabSource: { assetId: "", entityId: "0" }, + components: [], + children: ["weaponslot"] + } as any, + { + id: "weaponslot", + name: "weaponslot1", + parent: "boneHandle", + components: [], + position: { x: 0.5, y: 0.25, z: 0 } + } + ] + }; + + const outerPrefab = await PrefabParser.parse(engine, "outer.prefab", outerPrefabData); + const root = outerPrefab.instantiate(); + + const charInst = root.children.find((c) => c.name === "char"); + expect(charInst, "char instance should be a child of outerRoot").not.toBeUndefined(); + + const boneHand = charInst!.children.find((c) => c.name === "boneHand"); + expect(boneHand, "boneHand should exist inside nested prefab").not.toBeUndefined(); + + const weaponslot = boneHand!.children.find((c) => c.name === "weaponslot1"); + expect(weaponslot, "weaponslot1 should be attached as child of boneHand via stripped entity").not.toBeUndefined(); + expect(weaponslot!.transform.position.x).toBeCloseTo(0.5, 5); + expect(weaponslot!.transform.position.y).toBeCloseTo(0.25, 5); + expect(weaponslot!.transform.position.z).toBeCloseTo(0, 5); + + root.destroy(); + removeCachedPrefab("char.prefab"); + }); + + it("should support multiple stripped entities targeting different internal entities (LeftHand / RightHand socket case)", async () => { + // Nested prefab: root -> [LeftHand, RightHand] + const nestedPrefabData: IHierarchyFile = { + entities: [ + { id: "0", name: "nestedRoot", components: [], children: ["1", "2"] }, + { id: "1", name: "LeftHand", parent: "0", components: [] }, + { id: "2", name: "RightHand", parent: "0", components: [] } + ] + }; + const nestedPrefab = await PrefabParser.parse(engine, "body.prefab", nestedPrefabData); + cachePrefab("body.prefab", nestedPrefab); + + // entityId is the path string generated by _generateInstanceContext: + // nestedRoot = "" + // nestedRoot.children[0] = LeftHand -> "0" + // nestedRoot.children[1] = RightHand -> "1" + const outerPrefabData: IHierarchyFile = { + entities: [ + { + id: "outerRoot", + name: "Player", + components: [], + children: ["bodyInst"] + }, + { + id: "bodyInst", + name: "body", + parent: "outerRoot", + components: [], + assetUrl: "body.prefab", + isClone: true, + modifications: [], + removedEntities: [], + removedComponents: [] + } as any, + { + strippedId: "lhHandle", + prefabInstanceId: "bodyInst", + prefabSource: { assetId: "", entityId: "0" }, + components: [], + children: ["ws1"] + } as any, + { + strippedId: "rhHandle", + prefabInstanceId: "bodyInst", + prefabSource: { assetId: "", entityId: "1" }, + components: [], + children: ["ws2", "ws3"] + } as any, + { id: "ws1", name: "weaponslot1", parent: "lhHandle", components: [] }, + { id: "ws2", name: "weaponslot2", parent: "rhHandle", components: [] }, + { id: "ws3", name: "weaponslot3", parent: "rhHandle", components: [] } + ] + }; + + const outerPrefab = await PrefabParser.parse(engine, "player.prefab", outerPrefabData); + const root = outerPrefab.instantiate(); + const bodyInst = root.children.find((c) => c.name === "body")!; + const leftHand = bodyInst.children.find((c) => c.name === "LeftHand")!; + const rightHand = bodyInst.children.find((c) => c.name === "RightHand")!; + + const slot1 = leftHand.children.find((c) => c.name === "weaponslot1"); + const slot2 = rightHand.children.find((c) => c.name === "weaponslot2"); + const slot3 = rightHand.children.find((c) => c.name === "weaponslot3"); + + expect(slot1, "weaponslot1 under LeftHand").not.toBeUndefined(); + expect(slot2, "weaponslot2 under RightHand").not.toBeUndefined(); + expect(slot3, "weaponslot3 under RightHand").not.toBeUndefined(); + + // No new entities should remain as direct children of outerRoot after reparenting + expect(root.children.find((c) => c.name === "weaponslot1")).toBeUndefined(); + expect(root.children.find((c) => c.name === "lh-proxy")).toBeUndefined(); + + root.destroy(); + removeCachedPrefab("body.prefab"); + }); + + it("should propagate internal entity transform changes to the attached child (bone-drives-weapon behavior)", async () => { + // When an internal entity moves, the child attached through the stripped + // entity follows through normal hierarchy transforms. + const nestedPrefabData: IHierarchyFile = { + entities: [ + { id: "0", name: "nestedRoot", components: [], children: ["1"] }, + { id: "1", name: "bone", parent: "0", components: [] } + ] + }; + const nestedPrefab = await PrefabParser.parse(engine, "skel.prefab", nestedPrefabData); + cachePrefab("skel.prefab", nestedPrefab); + + const outerPrefabData: IHierarchyFile = { + entities: [ + { + id: "outerRoot", + name: "Actor", + components: [], + children: ["skelInst"] + }, + { + id: "skelInst", + name: "skel", + parent: "outerRoot", + components: [], + assetUrl: "skel.prefab", + isClone: true, + modifications: [], + removedEntities: [], + removedComponents: [] + } as any, + { + strippedId: "boneHandle", + prefabInstanceId: "skelInst", + prefabSource: { assetId: "", entityId: "0" }, + components: [], + children: ["weapon"] + } as any, + { + id: "weapon", + name: "weapon", + parent: "boneHandle", + components: [], + position: { x: 1, y: 0, z: 0 } + } + ] + }; + + const outerPrefab = await PrefabParser.parse(engine, "actor.prefab", outerPrefabData); + const root = outerPrefab.instantiate(); + const bone = root.children.find((c) => c.name === "skel")!.children.find((c) => c.name === "bone")!; + const weapon = bone.children.find((c) => c.name === "weapon")!; + + // Initial: bone at origin, weapon world position = (1, 0, 0) + const worldPos = new Vector3(); + worldPos.copyFrom(weapon.transform.worldPosition); + expect(worldPos.x).toBeCloseTo(1, 5); + expect(worldPos.y).toBeCloseTo(0, 5); + expect(worldPos.z).toBeCloseTo(0, 5); + + // Simulate transform animation: move bone to (10, 5, -3) + bone.transform.setPosition(10, 5, -3); + worldPos.copyFrom(weapon.transform.worldPosition); + expect(worldPos.x).toBeCloseTo(11, 5); // 10 + 1 + expect(worldPos.y).toBeCloseTo(5, 5); // 5 + 0 + expect(worldPos.z).toBeCloseTo(-3, 5); // -3 + 0 + + root.destroy(); + removeCachedPrefab("skel.prefab"); + }); +}); diff --git a/tests/src/shader-lab/ShaderLab.test.ts b/tests/src/shader-lab/ShaderLab.test.ts index e998533e5e..b1d5085044 100644 --- a/tests/src/shader-lab/ShaderLab.test.ts +++ b/tests/src/shader-lab/ShaderLab.test.ts @@ -247,8 +247,128 @@ describe("ShaderLab", async () => { glslValidate(engine, shaderSource, shaderLabRelease); }); + it("macro-negate-number (!0, !1 in #if expressions)", async () => { + const shaderSource = await readFile("./shaders/macro-negate-number.shader"); + glslValidate(engine, shaderSource, shaderLabVerbose); + glslValidate(engine, shaderSource, shaderLabRelease); + }); + it("mrt-struct", async () => { const shaderSource = await readFile("./shaders/mrt-struct.shader"); glslValidate(engine, shaderSource, shaderLabRelease); }); + + it("texture-generic (GVec4 → vec4 resolve)", async () => { + const shaderSource = await readFile("./shaders/texture-generic.shader"); + glslValidate(engine, shaderSource, shaderLabRelease); + }); + + it("generic-return-type (builtin generic return as arg to user function)", async () => { + const shaderSource = await readFile("./shaders/generic-return-type.shader"); + glslValidate(engine, shaderSource, shaderLabRelease); + }); + + it("define-struct-access-global (global #define with struct member access)", async () => { + const shaderSource = await readFile("./shaders/define-struct-access-global.shader"); + glslValidate(engine, shaderSource, shaderLabRelease); + + const shader = shaderLabVerbose._parseShaderSource(shaderSource); + const passSource = shader.subShaders[0].passes[0]; + const { vertex, fragment } = shaderLabVerbose._parseShaderPass( + passSource.contents, + passSource.vertexEntry, + passSource.fragmentEntry, + 0, + "" + )!; + + const expectedVert = await readFile("./expected/define-struct-access-global.vert.glsl"); + const expectedFrag = await readFile("./expected/define-struct-access-global.frag.glsl"); + expect(vertex).to.equal(expectedVert); + expect(fragment).to.equal(expectedFrag); + }); + + it("define-struct-access (function-body #define with struct member access)", async () => { + const shaderSource = await readFile("./shaders/define-struct-access.shader"); + glslValidate(engine, shaderSource, shaderLabRelease); + + const shader = shaderLabVerbose._parseShaderSource(shaderSource); + const passSource = shader.subShaders[0].passes[0]; + const { vertex, fragment } = shaderLabVerbose._parseShaderPass( + passSource.contents, + passSource.vertexEntry, + passSource.fragmentEntry, + 0, + "" + )!; + + const expectedVert = await readFile("./expected/define-struct-access.vert.glsl"); + const expectedFrag = await readFile("./expected/define-struct-access.frag.glsl"); + expect(vertex).to.equal(expectedVert); + expect(fragment).to.equal(expectedFrag); + }); + + it("macro-member-access-builtin-arg (Cocos FSInput pattern: member access macro as builtin fn arg)", async () => { + const shaderSource = await readFile("./shaders/macro-member-access-builtin-arg.shader"); + glslValidate(engine, shaderSource, shaderLabRelease); + + // Also verify verbose mode (semantic analysis) succeeds — this was the original bug: + // member access macros like #define FSInput_worldNormal v.v_normal.xyz resolved to + // struct type "Varyings" instead of TypeAny, causing builtin overload matching to fail. + const shader = shaderLabVerbose._parseShaderSource(shaderSource); + const passSource = shader.subShaders[0].passes[0]; + const { vertex, fragment } = shaderLabVerbose._parseShaderPass( + passSource.contents, + passSource.vertexEntry, + passSource.fragmentEntry, + 0, + "" + )!; + + expect(vertex).to.be.a("string").and.not.empty; + expect(fragment).to.be.a("string").and.not.empty; + + // Verify key builtins are present in output (macros expanded correctly) + expect(fragment).to.contain("normalize"); + expect(fragment).to.contain("dot"); + expect(fragment).to.contain("texture2D"); + }); + + it("global-varying-var (Cocos VSOutput pattern: global Varyings var with #define macros)", async () => { + const shaderSource = await readFile("./shaders/global-varying-var.shader"); + glslValidate(engine, shaderSource, shaderLabRelease); + + // Verify verbose mode: global "Varyings o;" should not produce "uniform Varyings o;" + // and should not duplicate varying declarations. + const shader = shaderLabVerbose._parseShaderSource(shaderSource); + const passSource = shader.subShaders[0].passes[0]; + const { vertex, fragment } = shaderLabVerbose._parseShaderPass( + passSource.contents, + passSource.vertexEntry, + passSource.fragmentEntry, + 0, + "" + )!; + + expect(vertex).to.be.a("string").and.not.empty; + expect(fragment).to.be.a("string").and.not.empty; + + // No "uniform Varyings o;" in output + expect(vertex).to.not.contain("uniform Varyings"); + expect(fragment).to.not.contain("uniform Varyings"); + + // Macros should be transformed: "o.v_worldPos" → "v_worldPos" + expect(vertex).to.contain("#define VSOutput_worldPos v_worldPos"); + expect(vertex).to.contain("#define VSOutput_worldNormal v_normal.xyz"); + + // No duplicate varying declarations + const varyingMatches = vertex.match(/varying vec3 v_worldPos/g); + expect(varyingMatches).to.have.lengthOf(1); + }); + + it("frag-return-vec4 (Cocos pattern: fragment entry returns vec4 instead of void)", async () => { + const shaderSource = await readFile("./shaders/frag-return-vec4.shader"); + glslValidate(engine, shaderSource, shaderLabRelease); + glslValidate(engine, shaderSource, shaderLabVerbose); + }); }); diff --git a/tests/src/shader-lab/ShaderValidate.ts b/tests/src/shader-lab/ShaderValidate.ts index da6d83407b..7689c721bd 100644 --- a/tests/src/shader-lab/ShaderValidate.ts +++ b/tests/src/shader-lab/ShaderValidate.ts @@ -65,7 +65,7 @@ export function glslValidate( }); // @ts-ignore - const shaderProgram = shaderPass._getCanonicalShaderProgram(engine, macroMockCollection); + const shaderProgram = shaderPass._compileShaderProgram(engine, macroMockCollection); expect(shaderProgram.isValid).to.be.true; }); }); diff --git a/tests/src/shader-lab/expected/define-struct-access-global.frag.glsl b/tests/src/shader-lab/expected/define-struct-access-global.frag.glsl new file mode 100644 index 0000000000..e766b6e6a1 --- /dev/null +++ b/tests/src/shader-lab/expected/define-struct-access-global.frag.glsl @@ -0,0 +1,10 @@ +varying vec2 v_uv; + +uniform sampler2D u_texture; +#define ATTR_POS POSITION + +#define VARYING_UV v_uv + +#define FRAG_UV v_uv + +void main() { gl_FragColor = texture2D ( u_texture , FRAG_UV ) ; } \ No newline at end of file diff --git a/tests/src/shader-lab/expected/define-struct-access-global.vert.glsl b/tests/src/shader-lab/expected/define-struct-access-global.vert.glsl new file mode 100644 index 0000000000..782ad3ee52 --- /dev/null +++ b/tests/src/shader-lab/expected/define-struct-access-global.vert.glsl @@ -0,0 +1,16 @@ +uniform mat4 renderer_MVPMat; +attribute vec4 POSITION; +attribute vec2 TEXCOORD_0; + +varying vec2 v_uv; + +#define ATTR_POS POSITION + +#define VARYING_UV v_uv + +#define FRAG_UV v_uv + +void main() { +gl_Position = renderer_MVPMat * ATTR_POS ; +VARYING_UV = TEXCOORD_0 ; + } \ No newline at end of file diff --git a/tests/src/shader-lab/expected/define-struct-access.frag.glsl b/tests/src/shader-lab/expected/define-struct-access.frag.glsl new file mode 100644 index 0000000000..2c8ba502ac --- /dev/null +++ b/tests/src/shader-lab/expected/define-struct-access.frag.glsl @@ -0,0 +1,11 @@ +varying vec2 v_uv; +varying vec3 v_normal; + +uniform sampler2D u_texture; +uniform vec3 u_lightDir; +void main() { #define FRAG_UV v_uv + +#define FRAG_NORMAL v_normal + +float NdotL = dot ( FRAG_NORMAL , u_lightDir ) ; +gl_FragColor = texture2D ( u_texture , FRAG_UV ) * NdotL ; } \ No newline at end of file diff --git a/tests/src/shader-lab/expected/define-struct-access.vert.glsl b/tests/src/shader-lab/expected/define-struct-access.vert.glsl new file mode 100644 index 0000000000..1ee6d28522 --- /dev/null +++ b/tests/src/shader-lab/expected/define-struct-access.vert.glsl @@ -0,0 +1,18 @@ +uniform mat4 renderer_MVPMat; +attribute vec4 POSITION; +attribute vec2 TEXCOORD_0; + +varying vec2 v_uv; +varying vec3 v_normal; + +void main() { +#define ATTR_POS POSITION + +#define VARYING_UV v_uv + +#define VARYING_NORMAL v_normal + +gl_Position = renderer_MVPMat * ATTR_POS ; +VARYING_UV = TEXCOORD_0 ; +VARYING_NORMAL = vec3 ( 0.0 , 1.0 , 0.0 ) ; + } \ No newline at end of file diff --git a/tests/src/shader-lab/shaders/define-struct-access-global.shader b/tests/src/shader-lab/shaders/define-struct-access-global.shader new file mode 100644 index 0000000000..a8abffe570 --- /dev/null +++ b/tests/src/shader-lab/shaders/define-struct-access-global.shader @@ -0,0 +1,31 @@ +Shader "define-struct-access-global-test" { + SubShader "Default" { + Pass "Forward" { + mat4 renderer_MVPMat; + + struct Attributes { vec4 POSITION; vec2 TEXCOORD_0; }; + struct Varyings { vec2 v_uv; }; + + VertexShader = vert; + FragmentShader = frag; + + sampler2D u_texture; + + // Global #define referencing entry function parameter names + #define ATTR_POS attr.POSITION + #define VARYING_UV o.v_uv + #define FRAG_UV v.v_uv + + Varyings vert(Attributes attr) { + Varyings o; + gl_Position = renderer_MVPMat * ATTR_POS; + VARYING_UV = attr.TEXCOORD_0; + return o; + } + + void frag(Varyings v) { + gl_FragColor = texture2D(u_texture, FRAG_UV); + } + } + } +} diff --git a/tests/src/shader-lab/shaders/define-struct-access.shader b/tests/src/shader-lab/shaders/define-struct-access.shader new file mode 100644 index 0000000000..a28171dc40 --- /dev/null +++ b/tests/src/shader-lab/shaders/define-struct-access.shader @@ -0,0 +1,34 @@ +Shader "define-struct-access-test" { + SubShader "Default" { + Pass "Forward" { + mat4 renderer_MVPMat; + + struct Attributes { vec4 POSITION; vec2 TEXCOORD_0; }; + struct Varyings { vec2 v_uv; vec3 v_normal; }; + + VertexShader = vert; + FragmentShader = frag; + + sampler2D u_texture; + vec3 u_lightDir; + + Varyings vert(Attributes attr) { + Varyings o; + #define ATTR_POS attr.POSITION + #define VARYING_UV o.v_uv + #define VARYING_NORMAL o.v_normal + gl_Position = renderer_MVPMat * ATTR_POS; + VARYING_UV = attr.TEXCOORD_0; + VARYING_NORMAL = vec3(0.0, 1.0, 0.0); + return o; + } + + void frag(Varyings v) { + #define FRAG_UV v.v_uv + #define FRAG_NORMAL v.v_normal + float NdotL = dot(FRAG_NORMAL, u_lightDir); + gl_FragColor = texture2D(u_texture, FRAG_UV) * NdotL; + } + } + } +} diff --git a/tests/src/shader-lab/shaders/frag-return-vec4.shader b/tests/src/shader-lab/shaders/frag-return-vec4.shader new file mode 100644 index 0000000000..9b3ea3d4ca --- /dev/null +++ b/tests/src/shader-lab/shaders/frag-return-vec4.shader @@ -0,0 +1,41 @@ +Shader "frag-return-vec4-test" { + SubShader "Default" { + Pass "Forward" { + mat4 renderer_MVPMat; + + struct Attributes { vec3 POSITION; vec3 NORMAL; vec2 TEXCOORD_0; }; + struct Varyings { vec3 v_worldPos; vec4 v_normal; vec2 v_uv; }; + + VertexShader = vert; + FragmentShader = frag; + + sampler2D u_texture; + vec3 u_lightDir; + + vec3 SRGBToLinear(vec3 gamma) { return gamma * gamma; } + vec3 LinearToSRGB(vec3 linear) { return sqrt(linear); } + + vec4 SurfacesFragmentModifyBaseColorAndTransparency(Varyings input) { + vec4 color = texture2D(u_texture, input.v_uv); + color.rgb = SRGBToLinear(color.rgb); + return color; + } + + Varyings vert(Attributes attr) { + Varyings o; + vec4 pos = renderer_MVPMat * vec4(attr.POSITION, 1.0); + o.v_worldPos = pos.xyz; + o.v_normal = vec4(attr.NORMAL, 1.0); + o.v_uv = attr.TEXCOORD_0; + gl_Position = pos; + return o; + } + + vec4 frag(Varyings input) { + vec4 color = SurfacesFragmentModifyBaseColorAndTransparency(input); + color.rgb = LinearToSRGB(color.rgb); + return color; + } + } + } +} diff --git a/tests/src/shader-lab/shaders/generic-return-type.shader b/tests/src/shader-lab/shaders/generic-return-type.shader new file mode 100644 index 0000000000..72e96b7f3d --- /dev/null +++ b/tests/src/shader-lab/shaders/generic-return-type.shader @@ -0,0 +1,52 @@ +Shader "generic-return-type-test" { + SubShader "Default" { + Pass "Forward" { + mat4 renderer_MVPMat; + + struct Attributes { vec4 POSITION; }; + struct Varyings { vec2 uv; vec3 worldNormal; vec3 worldTangent; }; + + VertexShader = vert; + FragmentShader = frag; + + // Test: user-defined function with concrete parameter types + vec3 CalculateNormalFromTangentSpace(vec3 normalFromTangentSpace, float normalStrength, vec3 normal, vec3 tangent, float mirrorNormal) { + vec3 binormal = cross(normal, tangent) * mirrorNormal; + return (normalFromTangentSpace.x * normalStrength) * normalize(tangent) + + (normalFromTangentSpace.y * normalStrength) * normalize(binormal) + + normalFromTangentSpace.z * normalize(normal); + } + + sampler2D u_normalMap; + vec4 u_scaleAndStrength; + + Varyings vert(Attributes attr) { + Varyings o; + gl_Position = renderer_MVPMat * attr.POSITION; + o.uv = attr.POSITION.xy; + o.worldNormal = attr.POSITION.xyz; + o.worldTangent = attr.POSITION.xyz; + return o; + } + + void frag(Varyings v) { + vec3 normal = v.worldNormal; + + // Test: builtin generic functions (normalize) on swizzled values, + // passed as arguments to a user-defined function. + // normalize(vec3) should not produce EGenType.GenType (200) as return type, + // it should produce TypeAny so overload matching succeeds. + vec3 nmmp = texture(u_normalMap, v.uv).xyz - vec3(0.5); + normal = CalculateNormalFromTangentSpace( + nmmp, + u_scaleAndStrength.w, + normalize(normal.xyz), + normalize(v.worldTangent), + 1.0 + ); + + gl_FragColor = vec4(normalize(normal), 1.0); + } + } + } +} diff --git a/tests/src/shader-lab/shaders/global-varying-var.shader b/tests/src/shader-lab/shaders/global-varying-var.shader new file mode 100644 index 0000000000..bd6a5cf477 --- /dev/null +++ b/tests/src/shader-lab/shaders/global-varying-var.shader @@ -0,0 +1,42 @@ +Shader "global-varying-var-test" { + SubShader "Default" { + Pass "Forward" { + mat4 renderer_MVPMat; + + struct Attributes { vec3 POSITION; vec3 NORMAL; vec2 TEXCOORD_0; }; + struct Varyings { vec3 v_worldPos; vec4 v_normal; vec2 v_uv; vec4 v_shadowBiasAndProbeId; }; + + VertexShader = vert; + FragmentShader = frag; + + sampler2D u_texture; + vec3 u_lightDir; + + #define VSOutput_worldPos o.v_worldPos + #define VSOutput_worldNormal o.v_normal.xyz + #define VSOutput_faceSideSign o.v_normal.w + #define VSOutput_texcoord o.v_uv + #define VSOutput_shadowBias o.v_shadowBiasAndProbeId.xy + + Varyings o; + + Varyings vert(Attributes input) { + mat4 matWorld = renderer_MVPMat; + vec4 pos = matWorld * vec4(input.POSITION, 1.0); + VSOutput_worldPos = pos.xyz; + VSOutput_worldNormal = input.NORMAL; + VSOutput_faceSideSign = 1.0; + VSOutput_texcoord = input.TEXCOORD_0; + VSOutput_shadowBias = vec2(0.0, 0.0); + gl_Position = pos; + return o; + } + + void frag(Varyings v) { + vec3 N = normalize(v.v_normal.xyz); + float NdotL = dot(N, u_lightDir); + gl_FragColor = texture2D(u_texture, v.v_uv) * NdotL; + } + } + } +} diff --git a/tests/src/shader-lab/shaders/macro-member-access-builtin-arg.shader b/tests/src/shader-lab/shaders/macro-member-access-builtin-arg.shader new file mode 100644 index 0000000000..70ae36f3bc --- /dev/null +++ b/tests/src/shader-lab/shaders/macro-member-access-builtin-arg.shader @@ -0,0 +1,49 @@ +Shader "macro-member-access-builtin-arg-test" { + SubShader "Default" { + Pass "Forward" { + mat4 renderer_MVPMat; + + struct Attributes { vec4 POSITION; vec3 NORMAL; vec2 TEXCOORD_0; }; + struct Varyings { vec2 v_uv; vec4 v_normal; vec3 v_worldPos; }; + + VertexShader = vert; + FragmentShader = frag; + + sampler2D u_texture; + vec3 u_lightDir; + vec3 u_cameraPos; + + // Cocos-style FSInput macros: member access used as builtin function args + #define FSInput_worldNormal v.v_normal.xyz + #define FSInput_faceSideSign v.v_normal.w + #define FSInput_worldPos v.v_worldPos + #define FSInput_texcoord v.v_uv + + Varyings vert(Attributes attr) { + Varyings o; + gl_Position = renderer_MVPMat * attr.POSITION; + o.v_uv = attr.TEXCOORD_0; + o.v_normal = vec4(attr.NORMAL, 1.0); + o.v_worldPos = attr.POSITION.xyz; + return o; + } + + void frag(Varyings v) { + // normalize() with member access macro as arg + vec3 N = normalize(FSInput_worldNormal); + + // dot() with member access macro as arg + float NdotL = dot(N, u_lightDir); + + // texture2D() with member access macro as arg + vec4 albedo = texture2D(u_texture, FSInput_texcoord); + + // mix() with member access macro and scalar macro + vec3 viewDir = normalize(u_cameraPos - FSInput_worldPos); + float rim = 1.0 - dot(N, viewDir); + + gl_FragColor = albedo * NdotL + vec4(vec3(rim), 0.0); + } + } + } +} diff --git a/tests/src/shader-lab/shaders/macro-negate-number.shader b/tests/src/shader-lab/shaders/macro-negate-number.shader new file mode 100644 index 0000000000..a224605393 --- /dev/null +++ b/tests/src/shader-lab/shaders/macro-negate-number.shader @@ -0,0 +1,41 @@ +Shader "macro-negate-number-test" { + SubShader "default" { + Pass "default" { + struct a2v { + vec4 POSITION; + }; + + mat4 renderer_MVPMat; + + #define SCENE_FOG_MODE 1 + + VertexShader = vert; + FragmentShader = frag; + + void vert(a2v v) { + gl_Position = renderer_MVPMat * v.POSITION; + } + + void frag() { + vec4 color = vec4(1.0); + + // Test: !0 should evaluate to true (like C preprocessor) + #if SCENE_FOG_MODE != 4 && (!0 || SCENE_FOG_MODE) + color = vec4(0.0, 1.0, 0.0, 1.0); + #endif + + // Test: !1 should evaluate to false + #if !1 + color = vec4(1.0, 0.0, 0.0, 1.0); + #endif + + // Test: !0 alone + #if !0 + color = vec4(0.0, 0.0, 1.0, 1.0); + #endif + + gl_FragColor = color; + } + } + } +} diff --git a/tests/src/shader-lab/shaders/mrt-struct.shader b/tests/src/shader-lab/shaders/mrt-struct.shader index f29056199e..85a47193c9 100644 --- a/tests/src/shader-lab/shaders/mrt-struct.shader +++ b/tests/src/shader-lab/shaders/mrt-struct.shader @@ -1,4 +1,4 @@ -Shader "/mrt-test.gs" { +Shader "/mrt-test.shader" { SubShader "Default" { diff --git a/tests/src/shader-lab/shaders/texture-generic.shader b/tests/src/shader-lab/shaders/texture-generic.shader new file mode 100644 index 0000000000..a27789175c --- /dev/null +++ b/tests/src/shader-lab/shaders/texture-generic.shader @@ -0,0 +1,94 @@ +Shader "texture-generic-test" { + SubShader "Default" { + Pass "Forward" { + mat4 renderer_MVPMat; + + struct Attributes { vec4 POSITION; }; + struct Varyings { vec2 uv; }; + + VertexShader = vert; + FragmentShader = frag; + + // Test: user-defined function taking vec4, called with texture() return value. + // texture(sampler2D, vec2) returns GVec4 which must resolve to vec4. + float decode32(vec4 rgba) { + rgba = rgba * 255.0; + float Sign = 1.0 - step(128.0, rgba[3] + 0.5) * 2.0; + float Exponent = 2.0 * mod(float(int(rgba[3] + 0.5)), 128.0) + step(128.0, rgba[2] + 0.5) - 127.0; + float Mantissa = mod(float(int(rgba[2] + 0.5)), 128.0) * 65536.0 + rgba[1] * 256.0 + rgba[0] + 8388608.0; + return Sign * exp2(Exponent - 23.0) * Mantissa; + } + + sampler2D u_texture; + + // Test: texture() result passed to user function (GVec4 → vec4 resolve) + mat4 getJointMatrix(float i) { + float x = 4.0 * i; + vec4 v1 = vec4( + decode32(texture(u_texture, vec2((x + 0.5) / 1024.0, 0.5))), + decode32(texture(u_texture, vec2((x + 1.5) / 1024.0, 0.5))), + decode32(texture(u_texture, vec2((x + 2.5) / 1024.0, 0.5))), + decode32(texture(u_texture, vec2((x + 3.5) / 1024.0, 0.5))) + ); + return mat4(v1, v1, v1, vec4(0.0, 0.0, 0.0, 1.0)); + } + + // Test: texture() result used directly in arithmetic (GVec4 → vec4) + vec4 sampleAndScale(sampler2D tex, vec2 coord, float scale) { + vec4 color = texture(tex, coord); + return color * scale; + } + + // Test: textureLod also returns GVec4 + vec4 sampleLod(sampler2D tex, vec2 coord, float lod) { + vec4 color = textureLod(tex, coord, lod); + return color; + } + + // Test: texture2DLod (ES 1.0 function, concrete types) + vec4 sampleLod2D(sampler2D tex, vec2 coord, float lod) { + return texture2DLod(tex, coord, lod); + } + + // Test: texture2DLod result passed to user function + float decodeLod2D(sampler2D tex, vec2 coord) { + return decode32(texture2DLod(tex, coord, 0.0)); + } + + // Test: texture() with samplerCube (GVec4 → vec4 resolve via GSamplerCube) + samplerCube u_cubeMap; + vec4 sampleCube(samplerCube tex, vec3 dir) { + return texture(tex, dir); + } + + // Test: textureLod with samplerCube (GVec4 → vec4 resolve via GSamplerCube) + vec4 sampleCubeLod(samplerCube tex, vec3 dir, float lod) { + return textureLod(tex, dir, lod); + } + + // Test: texture(samplerCube) result passed to user function (vec4 param matching) + float decodeCube(vec4 rgba) { + return rgba.r + rgba.g; + } + float sampleAndDecode(samplerCube tex, vec3 dir) { + return decodeCube(texture(tex, dir)); + } + + Varyings vert(Attributes attr) { + Varyings o; + gl_Position = renderer_MVPMat * attr.POSITION; + o.uv = attr.POSITION.xy; + return o; + } + + void frag(Varyings v) { + vec4 sampled = sampleAndScale(u_texture, v.uv, 1.0); + vec4 lodSampled = sampleLod(u_texture, v.uv, 0.0); + vec4 cubeSampled = sampleCube(u_cubeMap, vec3(v.uv, 1.0)); + vec4 cubeLodSampled = sampleCubeLod(u_cubeMap, vec3(v.uv, 1.0), 0.0); + float decoded = sampleAndDecode(u_cubeMap, vec3(v.uv, 1.0)); + gl_FragColor = sampled + lodSampled + cubeSampled + cubeLodSampled + vec4(decoded); + } + } + } +} diff --git a/tests/src/spine/LoaderUtils.test.ts b/tests/src/spine/LoaderUtils.test.ts new file mode 100644 index 0000000000..fa97f9afcf --- /dev/null +++ b/tests/src/spine/LoaderUtils.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; +import { LoaderUtils } from "../../../packages/spine/src/loader/LoaderUtils"; + +describe("LoaderUtils.getBaseUrl", () => { + it("strips the file name from a local path", () => { + expect(LoaderUtils.getBaseUrl("/assets/spine/data.json")).to.equal("/assets/spine/"); + }); + + it("returns empty string for a bare file name", () => { + expect(LoaderUtils.getBaseUrl("data.json")).to.equal(""); + }); + + it("strips the file name from an http url", () => { + expect(LoaderUtils.getBaseUrl("https://cdn.example.com/a/b/data.json")).to.equal("https://cdn.example.com/a/b/"); + }); +}); diff --git a/tests/src/spine/Pool.test.ts b/tests/src/spine/Pool.test.ts new file mode 100644 index 0000000000..0d835b408a --- /dev/null +++ b/tests/src/spine/Pool.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; +import { ClearablePool } from "../../../packages/spine-core-4.2/src/util/ClearablePool"; +import { ReturnablePool } from "../../../packages/spine-core-4.2/src/util/ReturnablePool"; + +class Item { + value = 0; +} + +describe("ClearablePool", () => { + it("hands out distinct elements, then reuses them after clear", () => { + const pool = new ClearablePool(Item); + const a = pool.get(); + const b = pool.get(); + expect(a).to.not.equal(b); + + pool.clear(); + // clear() only resets the cursor; existing elements are reused in order. + expect(pool.get()).to.equal(a); + expect(pool.get()).to.equal(b); + }); +}); + +describe("ReturnablePool", () => { + it("pre-fills initializeCount and reuses returned elements", () => { + const pool = new ReturnablePool(Item, 2); + const a = pool.get(); + const b = pool.get(); + expect(a).to.not.equal(b); + + pool.return(a); + expect(pool.get()).to.equal(a); + }); + + it("allocates a fresh element when the pool is exhausted", () => { + const pool = new ReturnablePool(Item, 1); + const a = pool.get(); + const fresh = pool.get(); + expect(fresh).to.be.instanceOf(Item); + expect(fresh).to.not.equal(a); + }); +}); diff --git a/tests/src/spine/SpineAnimationRenderer.test.ts b/tests/src/spine/SpineAnimationRenderer.test.ts new file mode 100644 index 0000000000..48de10c2e3 --- /dev/null +++ b/tests/src/spine/SpineAnimationRenderer.test.ts @@ -0,0 +1,72 @@ +import { describe, beforeAll, expect, it } from "vitest"; +import { Entity, Material, Shader, Texture2D, WebGLEngine } from "@galacean/engine"; +import { SpineAnimationRenderer } from "../../../packages/spine/src/renderer/SpineAnimationRenderer"; +import { SpineMaterial } from "../../../packages/spine/src/renderer/SpineMaterial"; +import { SpineBlendMode } from "../../../packages/spine/src/enums/SpineBlendMode"; + +describe("SpineAnimationRenderer", () => { + let engine: WebGLEngine; + + beforeAll(async () => { + engine = await WebGLEngine.create({ canvas: document.createElement("canvas") }); + }); + + it("initializes with default config", () => { + const renderer = new Entity(engine).addComponent(SpineAnimationRenderer); + expect(renderer.defaultConfig.animationName).to.be.null; + expect(renderer.defaultConfig.skinName).to.equal("default"); + expect(renderer.premultipliedAlpha).to.be.false; + expect(renderer.tintBlack).to.be.false; + }); + + it("tintBlack setter flags a buffer resize", () => { + const renderer = new Entity(engine).addComponent(SpineAnimationRenderer); + renderer.tintBlack = true; + expect(renderer.tintBlack).to.be.true; + expect((renderer as any)._needResizeBuffer).to.be.true; + }); + + it("_getMaterial caches by texture + blendMode", () => { + const renderer = new Entity(engine).addComponent(SpineAnimationRenderer); + const texture = new Texture2D(engine, 4, 4); + const normal = renderer._getMaterial(texture, SpineBlendMode.Normal); + // Same texture + blendMode must hit the cache (regression guard for the Map[key] bug). + expect(renderer._getMaterial(texture, SpineBlendMode.Normal)).to.equal(normal); + // Different blendMode must produce a different material. + expect(renderer._getMaterial(texture, SpineBlendMode.Additive)).to.not.equal(normal); + }); + + it("_getMaterial does not share a material across tintBlack variants", () => { + const renderer = new Entity(engine).addComponent(SpineAnimationRenderer); + const texture = new Texture2D(engine, 4, 4); + const plain = renderer._getMaterial(texture, SpineBlendMode.Normal); + renderer.tintBlack = true; + const tinted = renderer._getMaterial(texture, SpineBlendMode.Normal); + // Sharing one material would let renderers that differ only in tintBlack clobber + // each other's RENDERER_TINT_BLACK macro every frame. + expect(tinted).to.not.equal(plain); + expect(renderer._getMaterial(texture, SpineBlendMode.Normal)).to.equal(tinted); + renderer.tintBlack = false; + expect(renderer._getMaterial(texture, SpineBlendMode.Normal)).to.equal(plain); + }); + + it("destroy tolerates user-set materials and removes cached entries by key", () => { + const entity = new Entity(engine); + const renderer = entity.addComponent(SpineAnimationRenderer); + const texture = new Texture2D(engine, 4, 4); + const cached = renderer._getMaterial(texture, SpineBlendMode.Normal) as SpineMaterial; + renderer.setMaterial(0, cached); + // setMaterial is public API — destroy must not assume every entry is a SpineMaterial. + renderer.setMaterial(1, new Material(engine, Shader.find("2D/Spine"))); + const cacheKey = cached._cacheKey; + expect(SpineAnimationRenderer._materialCacheMap.has(cacheKey)).to.be.true; + expect(() => entity.destroy()).to.not.throw(); + expect(SpineAnimationRenderer._materialCacheMap.has(cacheKey)).to.be.false; + }); + + it("cloning an entity whose renderer has no skeleton does not throw", () => { + const entity = new Entity(engine); + entity.addComponent(SpineAnimationRenderer); + expect(() => entity.clone()).to.not.throw(); + }); +}); diff --git a/tests/src/spine/SpineConstant.test.ts b/tests/src/spine/SpineConstant.test.ts new file mode 100644 index 0000000000..38113402c6 --- /dev/null +++ b/tests/src/spine/SpineConstant.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; +import { SpineVertexStride } from "../../../packages/spine/src/SpineConstant"; +import { SpineBlendMode } from "../../../packages/spine/src/enums/SpineBlendMode"; + +describe("SpineVertexStride", () => { + it("is 9 floats without tint and 12 with tint black", () => { + // [x,y,z,u,v,r,g,b,a] vs [...,dr,dg,db] — must match the renderer's vertex layout. + expect(SpineVertexStride.withoutTint).to.equal(9); + expect(SpineVertexStride.withTint).to.equal(12); + }); +}); + +describe("SpineBlendMode", () => { + it("ordinals match the spine-core BlendMode order", () => { + expect(SpineBlendMode.Normal).to.equal(0); + expect(SpineBlendMode.Additive).to.equal(1); + expect(SpineBlendMode.Multiply).to.equal(2); + expect(SpineBlendMode.Screen).to.equal(3); + }); +}); diff --git a/tests/src/spine/SpineLoader.test.ts b/tests/src/spine/SpineLoader.test.ts new file mode 100644 index 0000000000..3e2903f986 --- /dev/null +++ b/tests/src/spine/SpineLoader.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; +import { SpineLoader } from "../../../packages/spine/src/loader/SpineLoader"; + +describe("SpineLoader._getUrlExtension", () => { + it("extracts the extension for spine asset types", () => { + expect(SpineLoader._getUrlExtension("path/to/spine.json")).to.equal("json"); + expect(SpineLoader._getUrlExtension("path/to/spine.skel")).to.equal("skel"); + expect(SpineLoader._getUrlExtension("path/to/spine.atlas")).to.equal("atlas"); + }); + + it("ignores query strings", () => { + expect(SpineLoader._getUrlExtension("spine.json?v=2")).to.equal("json"); + }); + + it("returns null when there is no extension", () => { + expect(SpineLoader._getUrlExtension("spine")).to.be.null; + }); +}); diff --git a/tests/src/spine/SpineMaterial.test.ts b/tests/src/spine/SpineMaterial.test.ts new file mode 100644 index 0000000000..2bb4e42bcd --- /dev/null +++ b/tests/src/spine/SpineMaterial.test.ts @@ -0,0 +1,36 @@ +import { describe, beforeAll, expect, it } from "vitest"; +import { BlendFactor, Shader, ShaderProperty, WebGLEngine } from "@galacean/engine"; +import { SpineMaterial } from "../../../packages/spine/src/renderer/SpineMaterial"; +import { SpineBlendMode } from "../../../packages/spine/src/enums/SpineBlendMode"; + +describe("SpineMaterial", () => { + let engine: WebGLEngine; + const srcColor = ShaderProperty.getByName("sourceColorBlendFactor"); + const dstColor = ShaderProperty.getByName("destinationColorBlendFactor"); + + beforeAll(async () => { + engine = await WebGLEngine.create({ canvas: document.createElement("canvas") }); + }); + + it("uses the built-in 2D/Spine shader and defaults to Normal blend", () => { + const material = new SpineMaterial(engine); + expect(material.shader).to.equal(Shader.find("2D/Spine")); + expect(material._getBlendMode()).to.equal(SpineBlendMode.Normal); + }); + + it("additive blend sets source=SourceAlpha, destination=One", () => { + const material = new SpineMaterial(engine); + material._setBlendMode(SpineBlendMode.Additive, false); + expect(material._getBlendMode()).to.equal(SpineBlendMode.Additive); + expect(material.shaderData.getInt(srcColor)).to.equal(BlendFactor.SourceAlpha); + expect(material.shaderData.getInt(dstColor)).to.equal(BlendFactor.One); + }); + + it("premultipliedAlpha switches the source color factor to One", () => { + const material = new SpineMaterial(engine); + material._setBlendMode(SpineBlendMode.Normal, true); + expect(material.shaderData.getInt(srcColor)).to.equal(BlendFactor.One); + material._setBlendMode(SpineBlendMode.Normal, false); + expect(material.shaderData.getInt(srcColor)).to.equal(BlendFactor.SourceAlpha); + }); +}); diff --git a/tests/src/spine/SpineResource.test.ts b/tests/src/spine/SpineResource.test.ts new file mode 100644 index 0000000000..6f281f81ba --- /dev/null +++ b/tests/src/spine/SpineResource.test.ts @@ -0,0 +1,33 @@ +import { Texture2D, WebGLEngine } from "@galacean/engine"; +import { beforeAll, describe, expect, it } from "vitest"; +import { SpineResource } from "../../../packages/spine/src/loader/SpineResource"; +import { SpineTexture } from "../../../packages/spine-core-4.2/src/SpineTexture"; + +describe("SpineResource._associationTextureInSkeletonData", () => { + let engine: WebGLEngine; + + beforeAll(async () => { + engine = await WebGLEngine.create({ canvas: document.createElement("canvas") }); + }); + + it("associates the attachment's Texture2D and protects it from GC while the resource is alive", () => { + const texture2D = new Texture2D(engine, 4, 4); + const attachment = { region: { texture: new SpineTexture(texture2D) } }; + const skeletonData = { + skins: [{ getAttachment: () => attachment }], + slots: [{ index: 0, name: "slot0" }] + }; + // Stands in for the SpineResource instance: only the fields `_associationTextureInSkeletonData` + // reads/writes (`_texturesInSpineAtlas`) and the super-resource GC check reads (`refCount`). + const fakeResource = { _texturesInSpineAtlas: [] as Texture2D[], refCount: 1 }; + + // @ts-ignore + SpineResource.prototype._associationTextureInSkeletonData.call(fakeResource, skeletonData); + + expect(fakeResource._texturesInSpineAtlas).to.deep.equal([texture2D]); + // Regression guard: region.texture is already the SpineTexture, so reading `.texture` off it again + // (instead of `.getImage()`) resolved to `undefined` on the 4.2 backend, and the association above + // silently never happened - leaving the texture unprotected from a GC pass while still in use. + expect(texture2D.destroy(false, true)).to.equal(false); + }); +}); diff --git a/tests/src/spine/SpineRuntimeRegistry.test.ts b/tests/src/spine/SpineRuntimeRegistry.test.ts new file mode 100644 index 0000000000..d9234a9b8f --- /dev/null +++ b/tests/src/spine/SpineRuntimeRegistry.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { registerSpineRuntime, getSpineRuntime } from "../../../packages/spine/src/runtime/SpineRuntimeRegistry"; +import type { ISpineRuntime } from "../../../packages/spine/src/runtime/ISpineRuntime"; + +describe("SpineRuntimeRegistry", () => { + // Must run first: the module singleton starts null and there is no reset. + it("throws when no runtime is registered", () => { + expect(() => getSpineRuntime()).to.throw(/no spine runtime registered/); + }); + + it("returns the registered runtime", () => { + const runtime = {} as ISpineRuntime; + registerSpineRuntime(runtime); + expect(getSpineRuntime()).to.equal(runtime); + }); + + it("last registration wins", () => { + const a = {} as ISpineRuntime; + const b = {} as ISpineRuntime; + registerSpineRuntime(a); + registerSpineRuntime(b); + expect(getSpineRuntime()).to.equal(b); + }); +}); diff --git a/tests/src/spine/SpineTexture.test.ts b/tests/src/spine/SpineTexture.test.ts new file mode 100644 index 0000000000..ac6b76e5f6 --- /dev/null +++ b/tests/src/spine/SpineTexture.test.ts @@ -0,0 +1,44 @@ +import { describe, beforeAll, expect, it } from "vitest"; +import { Texture2D, TextureFilterMode, WebGLEngine } from "@galacean/engine"; +import { TextureFilter as TextureFilter42 } from "@esotericsoftware/spine-core"; +import { SpineTexture as SpineTexture42 } from "../../../packages/spine-core-4.2/src/SpineTexture"; +import { SpineTexture as SpineTexture38 } from "../../../packages/spine-core-3.8/src/SpineTexture"; +import { TextureFilter as TextureFilter38 } from "../../../packages/spine-core-3.8/src/spine-core/Texture"; + +describe("SpineTexture.setFilters", () => { + let engine: WebGLEngine; + + beforeAll(async () => { + engine = await WebGLEngine.create({ canvas: document.createElement("canvas") }); + }); + + it("maps atlas min filters to engine filter modes (4.2)", () => { + const texture = new Texture2D(engine, 4, 4); + const spineTexture = new SpineTexture42(texture); + + spineTexture.setFilters(TextureFilter42.Nearest, TextureFilter42.Nearest); + expect(texture.filterMode).to.equal(TextureFilterMode.Point); + + spineTexture.setFilters(TextureFilter42.Linear, TextureFilter42.Linear); + expect(texture.filterMode).to.equal(TextureFilterMode.Bilinear); + + // Atlases pair a MipMap* min filter with a plain Linear mag filter — the min filter + // alone decides whether mipmap sampling (Trilinear) is requested. + spineTexture.setFilters(TextureFilter42.MipMapLinearLinear, TextureFilter42.Linear); + expect(texture.filterMode).to.equal(TextureFilterMode.Trilinear); + }); + + it("maps atlas min filters to engine filter modes (3.8)", () => { + const texture = new Texture2D(engine, 4, 4); + const spineTexture = new SpineTexture38(texture); + + spineTexture.setFilters(TextureFilter38.Nearest, TextureFilter38.Nearest); + expect(texture.filterMode).to.equal(TextureFilterMode.Point); + + spineTexture.setFilters(TextureFilter38.Linear, TextureFilter38.Linear); + expect(texture.filterMode).to.equal(TextureFilterMode.Bilinear); + + spineTexture.setFilters(TextureFilter38.MipMapLinearLinear, TextureFilter38.Linear); + expect(texture.filterMode).to.equal(TextureFilterMode.Trilinear); + }); +}); diff --git a/tests/src/ui/Mask.test.ts b/tests/src/ui/Mask.test.ts new file mode 100644 index 0000000000..d002fb27d7 --- /dev/null +++ b/tests/src/ui/Mask.test.ts @@ -0,0 +1,76 @@ +import { Sprite, SpriteMaskInteraction, SpriteMaskLayer, Texture2D } from "@galacean/engine-core"; +import { WebGLEngine } from "@galacean/engine-rhi-webgl"; +import { CanvasRenderMode, Image, Mask, UICanvas, UITransform } from "@galacean/engine-ui"; +import { describe, expect, it } from "vitest"; + +describe("Mask", async () => { + const canvas = document.createElement("canvas"); + const engine = await WebGLEngine.create({ canvas }); + const webCanvas = engine.canvas; + webCanvas.width = 300; + webCanvas.height = 300; + const scene = engine.sceneManager.scenes[0]; + const root = scene.createRootEntity("root"); + + const canvasEntity = root.createChild("canvas"); + const rootCanvas = canvasEntity.addComponent(UICanvas); + rootCanvas.renderMode = CanvasRenderMode.ScreenSpaceOverlay; + rootCanvas.referenceResolutionPerUnit = 50; + rootCanvas.referenceResolution.set(300, 300); + + const imageEntity = canvasEntity.createChild("image"); + const image = imageEntity.addComponent(Image); + (imageEntity.transform).size.set(300, 300); + + const maskEntity = canvasEntity.createChild("mask"); + const mask = maskEntity.addComponent(Mask); + (maskEntity.transform).size.set(100, 100); + mask.sprite = createSolidSprite(engine); + + it("Set and Get sprite", () => { + const texture = new Texture2D(engine, 1, 1); + const sprite = new Sprite(engine, texture); + mask.sprite = sprite; + expect(mask.sprite).to.eq(sprite); + + mask.sprite = null; + expect(mask.sprite).to.eq(null); + + mask.sprite = createSolidSprite(engine); + expect(mask.sprite).not.to.eq(null); + }); + + it("Set and Get alphaCutoff", () => { + expect(mask.alphaCutoff).to.eq(0.5); + mask.alphaCutoff = 0.2; + expect(mask.alphaCutoff).to.eq(0.2); + }); + + it("Set and Get influenceLayers", () => { + expect(mask.influenceLayers).to.eq(SpriteMaskLayer.Everything); + mask.influenceLayers = SpriteMaskLayer.Layer1; + expect(mask.influenceLayers).to.eq(SpriteMaskLayer.Layer1); + mask.influenceLayers = SpriteMaskLayer.Everything; + }); + + it("Set and Get flipX/flipY", () => { + mask.flipX = true; + expect(mask.flipX).to.eq(true); + mask.flipY = true; + expect(mask.flipY).to.eq(true); + mask.flipX = false; + mask.flipY = false; + }); + + it("UI image maskInteraction default is None", () => { + expect(image.maskInteraction).to.eq(SpriteMaskInteraction.None); + image.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + expect(image.maskInteraction).to.eq(SpriteMaskInteraction.VisibleInsideMask); + }); +}); + +function createSolidSprite(engine: WebGLEngine): Sprite { + const texture = new Texture2D(engine, 1, 1); + texture.setPixelBuffer(new Uint8Array([255, 255, 255, 255])); + return new Sprite(engine, texture); +} diff --git a/tests/src/ui/RectMask2D.test.ts b/tests/src/ui/RectMask2D.test.ts new file mode 100644 index 0000000000..47286e97eb --- /dev/null +++ b/tests/src/ui/RectMask2D.test.ts @@ -0,0 +1,79 @@ +import { Vector2, Vector3, Vector4 } from "@galacean/engine-math"; +import { WebGLEngine } from "@galacean/engine-rhi-webgl"; +import { CanvasRenderMode, RectMask2D, UICanvas, UITransform } from "@galacean/engine-ui"; +import { describe, expect, it } from "vitest"; + +describe("RectMask2D", async () => { + const canvas = document.createElement("canvas"); + const engine = await WebGLEngine.create({ canvas }); + const webCanvas = engine.canvas; + webCanvas.width = 300; + webCanvas.height = 300; + const scene = engine.sceneManager.scenes[0]; + const root = scene.createRootEntity("root"); + + const canvasEntity = root.createChild("canvas"); + const rootCanvas = canvasEntity.addComponent(UICanvas); + rootCanvas.renderMode = CanvasRenderMode.ScreenSpaceOverlay; + rootCanvas.referenceResolutionPerUnit = 1; + rootCanvas.referenceResolution.set(300, 300); + + it("should return false when mask size is zero", () => { + const maskEntity = canvasEntity.createChild("mask-zero"); + const rectMask = maskEntity.addComponent(RectMask2D); + const transform = maskEntity.transform as UITransform; + transform.size.set(0, 100); + const worldRect = new Vector4(); + + expect(rectMask._getWorldRect(worldRect)).to.eq(false); + }); + + it("should clamp negative softness values", () => { + const rectMask = canvasEntity.createChild("mask-softness").addComponent(RectMask2D); + + rectMask.softness.set(-4, 6); + expect(rectMask.softness.x).to.eq(0); + expect(rectMask.softness.y).to.eq(6); + + rectMask.softness = new Vector2(5, -3); + expect(rectMask.softness.x).to.eq(5); + expect(rectMask.softness.y).to.eq(0); + }); + + it("should toggle alphaClip", () => { + const rectMask = canvasEntity.createChild("mask-alphaclip").addComponent(RectMask2D); + expect(rectMask.alphaClip).to.eq(false); + rectMask.alphaClip = true; + expect(rectMask.alphaClip).to.eq(true); + }); + + it("should compute world rect when size and pivot set", () => { + const maskEntity = canvasEntity.createChild("mask-rect"); + const transform = maskEntity.transform as UITransform; + transform.pivot.set(0.5, 0.5); + transform.size.set(100, 80); + transform.setPosition(0, 0, 0); + const rectMask = maskEntity.addComponent(RectMask2D); + const worldRect = new Vector4(); + expect(rectMask._getWorldRect(worldRect)).to.eq(true); + // The canvas applies adaptation; just verify width/height match + expect(worldRect.z - worldRect.x).to.be.closeTo(100, 1); + expect(worldRect.w - worldRect.y).to.be.closeTo(80, 1); + }); + + it("should test contains world point", () => { + const maskEntity = canvasEntity.createChild("mask-contains"); + const transform = maskEntity.transform as UITransform; + transform.pivot.set(0.5, 0.5); + transform.size.set(100, 100); + transform.setPosition(0, 0, 0); + const rectMask = maskEntity.addComponent(RectMask2D); + + const rect = new Vector4(); + rectMask._getWorldRect(rect); + const cx = (rect.x + rect.z) * 0.5; + const cy = (rect.y + rect.w) * 0.5; + expect(rectMask._containsWorldPoint(new Vector3(cx, cy, 0))).to.eq(true); + expect(rectMask._containsWorldPoint(new Vector3(rect.x - 5, rect.y - 5, 0))).to.eq(false); + }); +}); diff --git a/tests/src/ui/UIEvent.test.ts b/tests/src/ui/UIEvent.test.ts index d2057a26cb..dc49457ba1 100644 --- a/tests/src/ui/UIEvent.test.ts +++ b/tests/src/ui/UIEvent.test.ts @@ -74,17 +74,20 @@ describe("UIEvent", async () => { // Add Image const imageEntity1 = canvasEntity.createChild("Image1"); - imageEntity1.addComponent(Image); + const image1 = imageEntity1.addComponent(Image); + image1.raycastEnabled = true; (imageEntity1.transform).size.set(300, 300); const script1 = imageEntity1.addComponent(TestScript); const imageEntity2 = imageEntity1.createChild("Image2"); - imageEntity2.addComponent(Image); + const image2 = imageEntity2.addComponent(Image); + image2.raycastEnabled = true; (imageEntity2.transform).size.set(200, 200); const script2 = imageEntity2.addComponent(TestScript); const imageEntity3 = imageEntity2.createChild("Image3"); const image3 = imageEntity3.addComponent(Image); + image3.raycastEnabled = true; (imageEntity3.transform).size.set(100, 100); const script3 = imageEntity3.addComponent(TestScript); @@ -93,6 +96,7 @@ describe("UIEvent", async () => { const { _pointerManager: pointerManager } = inputManager; const { _target: target } = pointerManager; const { left, top } = target.getBoundingClientRect(); + target.dispatchEvent(generatePointerEvent("pointerdown", 2, left + 2, top + 2)); engine.update(); diff --git a/tests/src/ui/UIInteractive.test.ts b/tests/src/ui/UIInteractive.test.ts index 8ee57381a8..96ab8be99f 100644 --- a/tests/src/ui/UIInteractive.test.ts +++ b/tests/src/ui/UIInteractive.test.ts @@ -1,7 +1,16 @@ import { Camera, PointerEventData, Script, SpriteDrawMode } from "@galacean/engine-core"; import { Color, Vector3 } from "@galacean/engine-math"; import { WebGLEngine } from "@galacean/engine-rhi-webgl"; -import { Button, ColorTransition, Image, ScaleTransition, Text, UICanvas, UIGroup, UITransform } from "@galacean/engine-ui"; +import { + Button, + ColorTransition, + Image, + ScaleTransition, + Text, + UICanvas, + UIGroup, + UITransform +} from "@galacean/engine-ui"; import { describe, expect, it, vi } from "vitest"; class ClickHandler extends Script { @@ -12,7 +21,7 @@ class ClickHandler extends Script { this.callCount++; } - handleClickWithPrefix(prefix: string) { + handleClickWithPrefix(_event: PointerEventData, prefix: string) { this.callCount++; this.lastPrefix = prefix; } @@ -40,7 +49,7 @@ describe("Button", async () => { const canvasEntity = root.createChild("canvas"); canvasEntity.addComponent(UIGroup); - const commonTextEntity = canvasEntity.createChild("commonText") + const commonTextEntity = canvasEntity.createChild("commonText"); const commonText = commonTextEntity.addComponent(Text); // Create button @@ -55,7 +64,6 @@ describe("Button", async () => { text.color.set(0, 0, 0, 1); const button = buttonEntity.addComponent(Button); - it("Set and Get", () => { // Click let clickCount = 0; @@ -135,7 +143,7 @@ describe("Button", async () => { const cloneButton = cloneButtonEntity.getComponent(Button); const cloneTransitions = cloneButton.transitions; const cloneTransition = cloneTransitions[0]; - expect(cloneTransition.target).to.eq(cloneButtonEntity.getComponent(Image)) + expect(cloneTransition.target).to.eq(cloneButtonEntity.getComponent(Image)); const cloneTransitionOne = cloneTransitions[1]; expect(cloneTransitionOne.target).to.eq(cloneButtonEntity.findByName("Text").getComponent(Text)); diff --git a/tests/vitest.config.ts b/tests/vitest.config.ts index 91a68ffbca..e11fe737fa 100644 --- a/tests/vitest.config.ts +++ b/tests/vitest.config.ts @@ -1,9 +1,18 @@ +import glsl from "../rollup-plugin-glsl"; import { defineProject } from "vitest/config"; export default defineProject({ server: { port: 51204 }, + plugins: [ + glsl({ + include: [/\.(glsl|gs)$/] + }) + ], + resolve: { + mainFields: ["debug", "module", "main"] + }, optimizeDeps: { exclude: [ "@galacean/engine",